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>
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>
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>
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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>