Commit Graph

379 Commits

Author SHA1 Message Date
Ryan Chen bd4b8936e4 Install Yarn in the frontend CI job instead of relying on setup-node's cache
CI / backend (pull_request) Successful in 41s
CI / frontend (pull_request) Successful in 30s
CI / backend (push) Successful in 42s
CI / frontend (push) Successful in 30s
setup-node's `cache: yarn` needs yarn already on PATH, and the Node it
installs does not ship one, so the frontend job failed at setup on every
run. Install the lockfile's Yarn 1.22.22 explicitly instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:36:03 -04:00
ryan bae2e75625 Merge pull request 'Sync Obsidian both ways so notes Simba writes reach your devices' (#86) from feat/obsidian-write-sync into main
CI / backend (push) Successful in 42s
CI / frontend (push) Failing after 7s
2026-10-13 08:11:46 -04:00
Ryan Chen b02bf0a6df Sync Obsidian both ways so notes Simba writes reach your devices
CI / backend (pull_request) Successful in 42s
CI / frontend (pull_request) Failing after 6s
CI / backend (push) Successful in 41s
CI / frontend (push) Failing after 7s
The container ran Obsidian Sync in pull-only mode, so notes, tasks and journal
entries the agent wrote stayed in its local copy. OBSIDIAN_SYNC_MODE now
defaults to bidirectional (merge on conflict); pull-only and mirror-remote
remain available.

Blue and green share the vault volume, and two sync clients writing one vault
and its sync state would race during a deploy. Setup and sync now run under a
flock on a file in the vault, so a new container starts syncing only after the
old container's sync process exits. Clearing Obsidian's stale sync lock is safe
inside that lock. Setup runs in the background, so startup no longer waits for
Obsidian login.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:10:48 -04:00
ryan 61acd85954 Merge pull request 'Only presign and accept images in the caller's own conversations' (#81) from fix/image-ownership into main
CI / backend (push) Successful in 42s
CI / frontend (push) Failing after 7s
2026-10-13 07:53:05 -04:00
ryan 9034a5bcec Merge pull request 'Create scheduled_messages for databases that never had it' (#80) from fix/scheduled-messages-migration into main
CI / backend (push) Successful in 42s
CI / frontend (push) Failing after 7s
2026-10-13 07:52:55 -04:00
ryan fe18a8371d Merge pull request 'Prune LangGraph checkpoints per thread and delete them with the conversation' (#85) from perf/prune-checkpoints into main
CI / backend (push) Successful in 44s
CI / frontend (push) Failing after 7s
2026-10-13 03:42:05 -04:00
Ryan Chen c329d15f7f Prune LangGraph checkpoints per thread and delete them with the conversation
CI / backend (pull_request) Successful in 42s
CI / frontend (pull_request) Failing after 7s
CI / backend (push) Successful in 42s
CI / frontend (push) Failing after 7s
Every graph step stores a full snapshot of the conversation, nothing pruned
old ones, and deleting a conversation left its thread behind (about 1.6 MB per
checkpoint at 200 turns). After each turn, only the newest
CHECKPOINT_KEEP_PER_THREAD (default 20) checkpoints of that thread are kept,
plus their pending writes; state reads only use the newest, and pruning a
200-checkpoint thread takes about 2 ms. Deleting a conversation also deletes
its thread, without failing the request if the checkpoint store errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 03:41:47 -04:00
ryan 0a50557d3d Merge pull request 'Run the Obsidian sync off the event loop and delete chunks by containment' (#84) from perf/obsidian-sync-off-loop into main
CI / backend (push) Successful in 42s
CI / frontend (push) Failing after 7s
2026-10-13 03:41:37 -04:00
ryan 8c0cedb3db Merge pull request 'Give the model full retrieved chunks instead of 400-character snippets' (#83) from perf/full-search-chunks into main
CI / backend (push) Successful in 41s
CI / frontend (push) Failing after 7s
2026-10-13 03:41:12 -04:00
Ryan Chen e16ade4725 Run the Obsidian sync off the event loop and delete chunks by containment
CI / backend (pull_request) Successful in 42s
CI / frontend (pull_request) Failing after 7s
CI / backend (push) Successful in 41s
CI / frontend (push) Failing after 7s
The periodic sync walked the vault, read files and ran synchronous database
deletes on the event loop, freezing active chat streams while it ran. Those
blocking steps now run in threads. Deletes use JSONB containment, which the
cmetadata GIN index (jsonb_path_ops) supports, and are batched into one
transaction per pass. Deleted files leave the index cache only after their
chunks are deleted, so a failed delete is retried on the next sync.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 03:41:12 -04:00
Ryan Chen b8ccc88b58 Give the model full retrieved chunks instead of 400-character snippets
CI / backend (pull_request) Successful in 41s
CI / frontend (pull_request) Failing after 7s
CI / backend (push) Successful in 42s
CI / frontend (push) Failing after 6s
Compaction replaces a tool's text with its artifact, and the search artifact
only carries UI snippets, so the full chunk the retriever found never reached
the model. Search tools now attach the full text as model_context, which
compaction prefers and shortens under budget pressure while keeping source
ids; the persisted artifact and UI are unchanged. The per-chunk header drops
the raw metadata dict (about a fifth of search output tokens) for title, date
and source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 03:40:52 -04:00
ryan 0b1600e524 Merge pull request 'Answer concisely by default and drop prompt text the tool schemas repeat' (#82) from perf/concise-answers into main
CI / backend (push) Successful in 42s
CI / frontend (push) Failing after 7s
2026-10-13 03:40:43 -04:00
Ryan Chen ef3a438048 Answer concisely by default and drop prompt text the tool schemas repeat
CI / backend (pull_request) Successful in 41s
CI / frontend (pull_request) Failing after 7s
CI / backend (push) Successful in 42s
CI / frontend (push) Failing after 7s
The local model is decode-bound, so output length drives latency. The system
prompt asked for "thorough, detailed answers" and to "think things through",
which lengthened both answers and reasoning. It now leads with the answer and
expands only on request. Per-integration tool lists that duplicated the bound
tool docstrings collapse to one paragraph of routing and duplicate-avoidance
rules. The prompt drops from about 1,446 to 897 tokens and stays static, so
the llama-server prompt cache still hits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 03:40:17 -04:00
ryan 2a843c29f2 Merge pull request 'Durable chat runs, shared agent runtime, reliable jobs, history tools and citations' (#79) from feat/application-foundation into main
CI / backend (push) Successful in 42s
CI / frontend (push) Failing after 7s
2026-10-12 23:27:29 -04:00
ryan 46cac94aa9 Merge pull request 'Validate production config, add CI, and build from locked dependencies' (#78) from feat/config-ci into main
CI / backend (push) Successful in 36s
CI / frontend (push) Failing after 6s
2026-10-12 23:27:25 -04:00
Ryan Chen 6f093e3f6f Only presign and accept images in the caller's own conversations
GET /api/conversation/image/<key> presigned any S3 key for any logged-in
user, and /upload-image wrote into whatever conversation_id the form named.
Both now require the conversation in the key (or form) to belong to the
caller and return 404 otherwise.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:18:41 -04:00
Ryan Chen 1cd387c59f Create scheduled_messages for databases that never had it
No committed migration creates the table: migrations 6-10 were generated
outside the repository, so fresh databases fail once anything alters it.
The new migration is CREATE TABLE IF NOT EXISTS, a no-op where the table
already exists. Aerich cannot generate it because its saved state already
includes the table; the DDL is Tortoise's schema output for the model.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:17:06 -04:00
Ryan Chen 5809ad3adb Add durable chat runs, shared agent runtime, reliable jobs, history tools and citations
CI / backend (pull_request) Successful in 42s
CI / frontend (pull_request) Failing after 7s
CI / backend (push) Successful in 41s
CI / frontend (push) Failing after 7s
Shared runtime
- One agent entry point (runtime.invoke_agent_for_user / prepare_agent_turn) for
  web, email, iMessage, WhatsApp, scheduled and cron, so identity, permissions,
  memories, summaries and metrics match across channels.
- Serialize turns per checkpoint thread; /query validates input and returns 409
  while a durable run owns the conversation.

Reconnectable chat (durable runs)
- POST /runs creates an idempotent, owner-scoped run; GET .../events replays an
  ordered SSE log by cursor, so generation survives refresh and device switches.
  Stop cancels; disconnect only detaches. /stream-query is a compatibility adapter.
- Coalesce streamed deltas (150 ms) and wake same-process subscribers instead of
  polling; keep buffered text when a run is stopped.
- Drain runs on shutdown; serve via Hypercorn with a shutdown timeout covering
  the drain (and no longer in debug mode). CHAT_RUN_* settings are forwarded.

Scheduled messages and cron
- Atomic delivery claims, attempt history and explicit retries; ambiguous sends
  become "uncertain" and are never resent automatically.
- Cron triggers need a run_id/Idempotency-Key; legacy jobs need an explicit owner.
- Cancelling never rewrites a started delivery; series stop via tombstones.
  Legacy sent rows are not forked into new series. Malformed bodies return 400.

History, integrations and retrieval
- Message-content search, pinned conversations, JSON/Markdown export and a
  memory editor.
- Integration status panel and YNAB data-freshness provenance.
- Date-filtered document/note search with stable citations and a source viewer.

Migration 20 is generated by aerich.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:11:04 -04:00
Ryan Chen de88679f25 Validate production config, add CI, and build from locked dependencies
CI / backend (pull_request) Successful in 1m23s
CI / frontend (pull_request) Failing after 37s
CI / backend (push) Successful in 36s
CI / frontend (push) Failing after 7s
- Fail startup on unsafe or malformed production settings (scripts/validate_config.py),
  run it before migrations, and stop on a failed `aerich upgrade`.
- Forward the context-budget, timeout and metrics settings through Compose; accept
  the historical TAVILIY_API_KEY spelling.
- Build the image from uv.lock and yarn.lock in a multi-stage Dockerfile.
- Add Gitea Actions CI: Ruff, unit tests, context evaluation, frontend tests,
  typecheck and build.
- Update README and deployment/development docs to match the current stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:09:22 -04:00
ryan 24f6764c64 Merge pull request 'Run image decoding and resizing off the event loop' (#75) from perf/offload-image-processing into main 2026-10-12 23:06:40 -04:00
ryan 0eb55560fa Merge pull request 'Sync YNAB in the background inside the tools' freshness window' (#74) from perf/ynab-sync-interval into main 2026-10-12 23:06:35 -04:00
ryan aefae43a6d Merge pull request 'Batch Paperless chunk embeddings during indexing' (#73) from perf/batch-paperless-embeddings into main 2026-10-12 23:06:29 -04:00
ryan b2a53821f8 Merge pull request 'Index conversation, message, memory and artifact foreign keys' (#76) from perf/fk-indexes into main 2026-10-12 23:06:09 -04:00
ryan e0ab2657ed Merge pull request 'Keep the chat prompt prefix stable for llama-server prompt caching' (#77) from perf/stable-prompt-prefix into main 2026-10-12 23:06:08 -04:00
Ryan Chen 266c6a8d79 Drop dropped-turn excerpts when the summary ledger is included
The ledger supersedes the "Earlier conversation excerpts" message, as on
main; sending both duplicated dropped-turn content. Removal is consistent
whenever the ledger is included, so the cached prefix stays stable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:05:21 -04:00
Ryan Chen 1e638e1e3b Place conversation evidence before the current turn for prompt caching
The summary ledger is regenerated every turn. At the head of history it
invalidated the cached retained turns on every request once trimming
started. Insert it immediately before the current turn's HumanMessage so
system, tools and retained history stay byte-identical; excerpts of dropped
turns stay at the head since they only change when the cut moves.

Reserve a fixed ledger ceiling (SUMMARY_RESERVE_TOKENS) rather than its
current size, so a shrinking ledger cannot pull the cut back and change the
retained prefix; fall back to the actual size for tight budgets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:04:24 -04:00
Ryan Chen 891f44c1a9 Keep the chat prompt prefix stable for llama-server prompt caching
llama-server only reuses a byte-identical prompt prefix. Several things
changed the prefix every turn, forcing most of the prompt to be re-prefilled:

- Memories: rank still chooses which memories fit the budget, but the chosen
  ones are emitted in stored order, so the block is identical across queries.
- History trimming: the conversation-evidence summary is only included when
  turns were actually dropped (it duplicated retained turns otherwise). On
  overflow, old turns are dropped at stable cut points down to ~65% of the
  budget, so the retained prefix holds for several turns instead of shifting
  by one turn every request.
- Tools: TOOL_ROUTING_MODE=static (default) binds the full role-appropriate
  tool set in registration order with no discovery tool; admin gating is
  unchanged. TOOL_ROUTING_MODE=dynamic keeps the keyword router.
- Observability: per-call model_calls in the stream_request log now include
  cached_prompt_tokens when the provider reports cache reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 23:01:46 -04:00
Ryan Chen 746aaa4844 Index conversation, message, memory and artifact foreign keys
Postgres had only primary keys on these tables, so loading a
conversation, listing conversations, checking for a first message on
every send, and fetching memories/artifacts were sequential scans.
Migration generated with aerich; index-only apart from the no-op JSONB
re-casts aerich emits on every run (as in migrations 16 and 18), which
were checked not to rewrite tables. Upgrade and downgrade verified on a
throwaway pgvector database.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:57:35 -04:00
Ryan Chen 68a3085ac1 Run image decoding and resizing off the event loop
process_image takes ~560ms on a 12MP photo and image_content_block
decodes HEIC/JPEG with PIL; both ran synchronously inside async
handlers, freezing every concurrent chat stream. Run them via
asyncio.to_thread in the upload handlers, the chat stream, and iMessage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:56:13 -04:00
Ryan Chen d49907aefe Sync YNAB in the background inside the tools' freshness window
Agent tools call ensure_fresh(120s) but the background loop ran every
900s, so most finance questions waited on an inline YNAB fetch. Default
the loop to 90s (40 requests/hour, well under YNAB's 200/hour limit).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:55:33 -04:00
Ryan Chen c80125f7fe Batch Paperless chunk embeddings during indexing
index_documents sent one embedding request and one insert per chunk.
Send chunks in batches of 32 and, only when a batch fails, retry it one
chunk at a time so the rejected chunk is still named in the logs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:55:04 -04:00
ryan 030c5d6c21 Merge pull request 'Fix Google Calendar/Gmail disabled by read-only token mount' (#72) from fix/google-token-readonly into main 2026-10-12 22:46:03 -04:00
Ryan Chen 9214afb206 Keep Google service enabled when token mount is read-only
token.json is mounted :ro, so writing refreshed creds back raised
PermissionError at startup once the stored access token expired. The broad
except in agents.py then disabled Calendar and Gmail tools entirely.
Keep the refreshed creds in memory when the write fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:42:37 -04:00
ryan 3ab7faf139 Merge pull request 'Improve context retention, tool routing, memory retrieval and stream metrics' (#71) from feat/context-quality into main 2026-10-12 20:19:51 -04:00
Ryan Chen 4645920c64 Route tools and retain structured conversation context with durable evidence 2026-10-12 20:19:12 -04:00
ryan e4b757fb2b Merge pull request 'Use gross outflows consistently in spending reports' (#70) from fix/ynab-outflow-consistency into main 2026-10-12 20:16:42 -04:00
Ryan Chen a1cdc60699 Align spending report groups and totals with gross outflows 2026-10-12 20:15:49 -04:00
ryan 2104e9f694 Merge pull request 'State spending-report outflows and inflows separately' (#63) from feat/ynab-report-totals into main 2026-10-12 19:48:17 -04:00
Ryan Chen a5ea220cdd Merge main into spending report totals and preserve artifact output 2026-10-12 19:47:52 -04:00
ryan 5d504e0314 Merge pull request 'Budget conversation context and preserve complete tool exchanges' (#69) from feat/context-management into main 2026-10-12 19:46:36 -04:00
Ryan Chen 1c9405cfdf Budget conversation context while preserving current turns and tool exchanges 2026-10-12 19:45:51 -04:00
ryan 78259855c8 Merge pull request 'Prevent active chat streams from being cancelled after 60 seconds' (#68) from fix/stream-stall-timeout into main 2026-10-12 19:38:22 -04:00
Ryan Chen 2cb131bbd2 Prevent Quart from cancelling active chat streams after 60 seconds 2026-10-12 19:38:04 -04:00
ryan 9e993f420f Merge pull request 'Treat thinking as progress in stream timeout; trim replayed history' (#67) from fix/stream-stall-timeout into main 2026-10-12 19:14:30 -04:00
Ryan Chen 4ce254193e Treat thinking as progress in stream timeout; trim replayed history
The 30s time-to-first-token guard measured from the turn's first model call
to the first answer token, so thinking, tool execution and the second call's
prefill all counted against it. Long reasoning on a long conversation was
cancelled mid-generation (llama-server: stop: cancel task at n_gen=1287).

- stream_query: the timeout now fires only when an in-flight model call goes
  silent for TTFT_TIMEOUT_SECONDS. Any chunk (thinking or empty) resets the
  clock, each model call starts a fresh one, and time spent in tools between
  calls doesn't count.
- Trim replayed history (blueprints/conversation/context.py): tool results
  and user messages from earlier turns are capped at 4000 chars (attachments
  included) and their images replaced by a placeholder. The current turn is
  untouched. The window no longer starts with orphaned ToolMessages.
- Log per-call prefill time, duration and prompt/output tokens
  (stream_usage=True) plus max_prompt_tokens in the stream_request line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:14:02 -04:00
ryan 37a8b95887 Merge pull request 'Cowork E: drop files into the chat (PDF, text, CSV, images)' (#66) from feat/cowork-file-drop into main 2026-10-12 18:47:29 -04:00
Ryan Chen 62416fcf2e Drop overlay sits above the artifact sheet below lg
Beside the panel (lg and up) it still covers only the chat column. Below
lg the panel is a fixed z-40 sheet, so the overlay covers the window at
z-[45] (under the z-50 mobile drawer) instead of being hidden by it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:45:56 -04:00
Ryan Chen cc29ae1ef8 Extract attachment text off the event loop; reject text-less files
- extract_text (PyMuPDF for PDFs) now runs via asyncio.to_thread in both
  /upload and stream-query, so a large PDF doesn't stall other streams.
- /upload returns 422 when extraction yields no text: a scanned PDF
  without a text layer, or an empty text file.
- Mock server: *scan*.pdf uploads return that 422.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:43:10 -04:00
Ryan Chen 35faeb0fb7 Frontend: attach files by picker or drop, with an attachment chip
- pendingImage becomes pendingAttachment (image or PDF/TXT/MD/CSV), uploaded
  through POST /api/conversation/upload and sent as image_key or file_key.
- MessageInput shows an icon/name/size chip for files, an inline error for
  unsupported or oversized files, and a paperclip picker.
- DropOverlay covers the chat area with "Drop to attach" while files are
  dragged over the window; drags without files are ignored.
- QuestionBubble renders a saved trailing "📎 name" as a chip.
- test-fixtures/cowork/mock-server.mjs: dependency-free mock API (static
  dist/, conversations, contract-shaped SSE, uploads, artifacts) for
  Playwright runs without Docker or OIDC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:43:05 -04:00
Ryan Chen 3e50aecb96 Desktop: let HTML5 file drops reach the webview
Tauri v2 intercepts drag-and-drop by default, so the chat's drop overlay
never saw dropped files. Disable its handler on the main window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:41:56 -04:00