Skip to content

fix: auto-resolve tool-call stream drop (#475) + JSON workspace-registry collision (#469) - #480

Draft
TYRMars wants to merge 2 commits into
mainfrom
claude/vibrant-dijkstra-fzewut
Draft

fix: auto-resolve tool-call stream drop (#475) + JSON workspace-registry collision (#469)#480
TYRMars wants to merge 2 commits into
mainfrom
claude/vibrant-dijkstra-fzewut

Conversation

@TYRMars

@TYRMars TYRMars commented Jul 20, 2026

Copy link
Copy Markdown
Owner

Auto-resolves two open bug reports. Both fixes are isolated to their respective packages with regression tests.


Fixes #475@jarvis/llm streaming tool-call drop

StreamAccumulator.#finalise (packages/llm/src/openai.ts) reconstructed tool calls behind a guard requiring both an id and a name. The guard was only meant to skip placeholder padding slots, but it also silently discarded any real tool call whose streamed deltas never carried an id. OpenAI-compatible backends (Kimi/Ollama, explicitly in scope) commonly stream {index, function:{name, arguments}} with no id, so the call vanished while finish_reason stayed "tool_calls" — the turn ended with an empty assistant message and the tool never ran, whereas the same backend worked over non-streaming POST /v1/chat/completions.

Change: skip only slots that never carried a name; synthesise call_<index> when the id is missing (mirrors the Google provider's gem_<index>). Adds regression tests for the missing-id and padding-slot cases.

Fixes #469@jarvis/store JSON workspace-registry collision

JsonFileWorkspaceStore wrote workspaces.json flat into the base dir it shares with JsonFileConversationStore's <id>.json rows. JsonFileConversationStore.list accepted any *.json and dereferenced stored.messages.length, so after the first POST /v1/workspaces (or workspaces.bind on resume) the stray file crashed GET /v1/conversations permanently — the SPA sidebar went dead for the lifetime of the data dir. The reverse collision let a conversation with id "workspaces" clobber the registry.

Change:

  • Move the registry to <base>/workspaces/workspaces.json, matching every other domain store (projects/, workflows/, …). A legacy flat file is migrated in on first open — the new file is written before the old one is removed, so a mid-migration crash can't lose it.
  • Harden JsonFileConversationStore.list to skip any *.json without a messages array, so a stray/legacy non-conversation file can never crash the sidebar again.
  • Regression tests for the migration, the new subdir layout, and the conversation-store guard.

Verification (local, all green)

  • pnpm --filter @jarvis/llm test — 158/158
  • pnpm --filter @jarvis/store test — 438/438 (incl. SQLite once the native binding is built)
  • typecheck (@jarvis/llm, @jarvis/store) — clean
  • eslint packages/llm packages/store — clean

🤖 Generated with Claude Code

https://claude.ai/code/session_01AjcBxxZuarAPFtYuz7CsaC

claude added 2 commits July 20, 2026 01:22
StreamAccumulator.#finalise dropped any real tool call whose streamed
deltas never carried an `id`, since the guard required both `id` and
`name`. OpenAI-compatible backends (Kimi/Ollama) commonly stream
`{index, function:{name, arguments}}` with no id, so the call was
silently discarded while finish_reason stayed "tool_calls" — the turn
ended with an empty assistant message and the tool never ran, whereas
the same backend worked over non-streaming /v1/chat/completions.

Skip only placeholder padding slots (no name) and synthesise
`call_<index>` when the id is missing, mirroring the Google provider's
`gem_<index>`. Adds regression tests for the missing-id and padding-slot
cases.

Fixes #475

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjcBxxZuarAPFtYuz7CsaC
JsonFileWorkspaceStore wrote workspaces.json flat into the base dir it
shares with JsonFileConversationStore's <id>.json rows. JsonFileConversation
Store.list accepted any *.json and dereferenced stored.messages.length, so
after the first POST /v1/workspaces (or workspaces.bind on resume) the stray
workspaces.json crashed GET /v1/conversations permanently — the SPA sidebar
went dead for the lifetime of the data dir. The reverse collision let a
conversation with id "workspaces" clobber the registry.

- Move the registry to <base>/workspaces/workspaces.json, matching every
  other domain store (projects/, workflows/, …). A legacy flat file is
  migrated in on first open (new file written before the old one is removed,
  so a mid-migration crash can't lose it).
- Harden JsonFileConversationStore.list to skip any *.json without a
  messages array, so a stray/legacy non-conversation file can never crash
  the sidebar again.

Regression tests for the migration, the new subdir layout, and the
conversation-store guard.

Fixes #469

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AjcBxxZuarAPFtYuz7CsaC
@TYRMars TYRMars changed the title fix(llm): synthesise tool-call id on OpenAI stream when backend omits it (#475) fix: auto-resolve tool-call stream drop (#475) + JSON workspace-registry collision (#469) Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment