Commit Graph

366 Commits

Author SHA1 Message Date
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 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
Ryan Chen b2f0b9f9f1 Add conversation file upload and file_key in stream-query
POST /api/conversation/upload takes an image (via process_image) or a
PDF/txt/md/csv file, checks the conversation belongs to the caller, and
stores files at conversations/<id>/files/<uuid>.<ext> with the original
name in S3 metadata. Anything else gets a 415.

stream-query accepts file_key (must sit under this conversation's
files/ prefix), extracts its text capped at 12k chars, appends it to the
agent's user turn as <attached file> and records 📎 name on the saved
message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:41:56 -04:00
ryan 55c42fdfc1 Merge pull request 'Cowork D: artifact side panel with per-kind renderers' (#65) from feat/cowork-artifact-panel into main 2026-10-12 18:41:43 -04:00
Ryan Chen b6bb9ec8cf Merge artifact scenarios into the shared Cowork mock server
One mock server now serves both the timeline scenarios and the artifact
panel's: per-kind fixtures from artifacts/<kind>.json, an "everything"
conversation with one saved tool message per kind, keyword scenarios
(recipe, budget, trend, philz, everything, broken, mystery, ...), a
500ing "broken" artifact, and small delays on /api/artifacts* so the
panel's loading states are visible. SPENDING now uses positive amounts
like the backend.

Also: renderers reuse ArtifactSourceRef, and the collapsed rail's
buttons say "Show <title>" so they don't share the timeline chip's
"Open <title>" accessible name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:40:31 -04:00
Ryan Chen d065b70667 Reconcile artifact renderers and fixtures with the backend shapes
Workers A/B are merged, so match what blueprints/ynab/tools.py and
conversation/agents.py actually emit:

- ynab.spending_report rows and ynab.summary.spent are positive for
  spending; bars use the positive amount and a net refund (negative
  row) sorts last with no bar.
- ynab.transactions start_date may be null (open-ended search); the
  header reads "Through Oct 12, 2026". Amounts stay signed with
  outflows as −$, and total is the net.
- Recipe image_url is never sent, search.documents has no source_ref,
  memo/age_of_money/goal_target are optional; fixtures now match
  (backend titles, ordering, float quantities, no empty fields).
- Mock server: drop the placeholder recipe image, positive month
  spending, and a "philz" open-ended transactions scenario.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:37:36 -04:00
Ryan Chen 0ae87ee610 Add artifact panel with per-kind renderers
Right-hand pane (440px, lg and up) with tabs, collapse rail and a
library of the conversation's artifacts; a bottom sheet below lg.
useArtifactPanel holds tabs/active and caches fetched artifacts by id;
tabs reset on conversation change.

Artifacts from tool_end auto-open on windows >= 1280px (newest wins,
and a panel the user collapsed stays collapsed). ChatScreen exposes
openArtifact for the activity timeline's onOpenArtifact and adds an
Artifacts button to the chat header.

One renderer per v1 kind (YNAB summary, category budget, spending
report, transactions; Mealie recipe, meal plan, shopping lists;
Obsidian note; document search) using CSS bars only, plus a JSON
fallback for unknown kinds or renderer errors.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:37:33 -04:00
Ryan Chen 396e24f2f0 Add artifact API service, per-kind fixtures and Cowork mock server
artifactTypes.ts mirrors the contract (worker C creates the same file).
artifactService.ts wraps GET /api/artifacts and /api/artifacts/<id>
with the shared fetchWithAuth helper.

test-fixtures/cowork/artifacts/<kind>.json holds one ToolArtifact per
v1 kind. mock-server.mjs is a dependency-free Node server that serves
dist/ and speaks the contract (SSE v2 tool events, artifacts API) for
Playwright checks without Docker or OIDC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:37:07 -04:00
ryan f2b02a188d Merge pull request 'Cowork C: activity timeline replaces tool pills' (#64) from feat/cowork-timeline into main 2026-10-12 18:36:30 -04:00
Ryan Chen e7bade119c Timeline: calendar icon for verb-first calendar tools
get_calendar_events / create_calendar_event never matched the "calendar"
prefix and fell back to the wrench icon; match "calendar" anywhere in the
tool name instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:34:06 -04:00
Ryan Chen f85607d62d Activity timeline replaces ToolBubble
Tool calls in a turn now render as one ActivityTimeline block: expanded
with "Simba is working…" while running (icon, label, input summary,
spinner/✓/✗, duration, Open chip for artifacts), collapsing to
"Simba worked · N steps · 4.2 s" when the turn finishes. useChat keys tool
rows by run_id (parallel calls), synthesises ids for legacy servers that
send only {tool}, and restores rows from the conversation payload.

Adds src/api/artifactTypes.ts (shared with the artifact panel) and a
dependency-free mock API server under test-fixtures/cowork/.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:34:06 -04:00
ryan ce50979ff7 Merge pull request 'Cowork B: Mealie, Obsidian and search tools return artifacts' (#61) from feat/cowork-tool-artifacts into main 2026-10-12 18:31:35 -04:00
Ryan Chen ca705701bd State spending-report outflows and inflows separately
The ynab_spending_report 'total' was a net figure: inflows (deposits into
budget accounts) subtracted from the spending total, which the model
repeated back as spending. The report now states total outflows and
inflows separately, drops net-negative groups from the per-group list,
and computes the monthly average from outflows.
2026-10-12 18:28:16 -04:00
Ryan Chen 20ec7ca8b6 Return artifacts from YNAB budget, transaction and spending tools
Convert ynab_budget_summary, ynab_search_transactions, ynab_spending_report
and ynab_category_budget to content_and_artifact (ynab.summary,
ynab.transactions, ynab.spending_report, ynab.category_budget). The text
the LLM sees is unchanged. Not-synced, bad-input, empty and error paths
return no artifact; Decimals become floats and dates ISO strings.

ynab_search_transactions now fetches up to 500 rows in one query (with an
id tie-break so ordering is stable) and lists the first `limit` in the
text; the artifact carries all of them. ynab_account_balances,
ynab_recent_changes and ynab_sql stay text-only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 18:12:23 -04:00
Ryan Chen 50d123e4d2 Return artifacts from Mealie, Obsidian and search tools
Convert mealie_get_recipe, mealie_meal_plans, mealie_todays_meals,
mealie_shopping_lists, obsidian_read_note, journal_get_today,
obsidian_search_notes and simba_search to content_and_artifact. The text
the LLM sees is unchanged; the artifact is a JSON-safe ToolArtifact dict
per the Cowork UI contract (mealie.recipe, mealie.meal_plan,
mealie.shopping_lists, obsidian.note, search.documents). Error, empty and
not-configured paths return no artifact.

simba_search's artifact changes from a list of Documents (unused) to a
search.documents dict.

New optional env vars: MEALIE_GROUP_SLUG (default "home") for Mealie v2
recipe links, OBSIDIAN_VAULT_NAME for obsidian://open links.

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