Skip to content

fix: auto-resolve two code-review bugs — plugin reattach guard (#496) + truncated tool-call corruption (#494) - #503

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

fix: auto-resolve two code-review bugs — plugin reattach guard (#496) + truncated tool-call corruption (#494)#503
TYRMars wants to merge 2 commits into
mainfrom
claude/vibrant-dijkstra-n3474s

Conversation

@TYRMars

@TYRMars TYRMars commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Auto-resolved by the scheduled issue-triage routine. Two independent, self-contained bug fixes; each is one commit with its own regression tests.


#494 — core agent.ts: truncated tool-call turn bricks a persisted conversation

isToolCallTurn gated tool dispatch on finish_reason === "tool_calls", but the assistant message was pushed into conversation.messages unconditionally. When a provider reported finish_reason: "length" (OpenAI) / stop_reason: "max_tokens" (Anthropic) alongside a non-empty, well-formed tool_calls array — reachable when the model hits the output-token cap mid-turn — the loop skipped dispatch and terminated, persisting an assistant message carrying tool_calls with zero matching role:"tool" replies. Both providers 400 on that history shape, so every later turn and every resume on the conversation failed permanently, with no in-product way to repair it.

Fix: rename the guard to messageHasToolCalls and drop the finish_reason conjunct — dispatch whenever the assistant turn carries tool calls. A truncated turn (length) still ends the loop, but only after its tool calls are answered, so the persisted history is always a valid shape. Any other finish_reason continues so the model can react to the results. Applied to both the blocking (runWithUsage) and streaming (runStream) paths, with regression tests on each.

#496 — plugin manager.ts: one bad SKILL.md silently kills a plugin's MCP servers + sibling skills

PluginManager.#reattachOne iterated a plugin's skills with no per-skill error guard, and the MCP-server loop ran after it. A single unreadable / unparseable SKILL.md (e.g. a frontmatter typo introduced after install) threw, aborting the rest of that plugin's reattach — every sibling skill and every MCP server it declares. reattachInstalled's bare .catch(() => {}) swallowed it, so the failure was completely silent while GET /v1/plugins kept reporting the plugin as fully installed.

Fix: wrap the skills-loop body in a per-skill try/catch that skips the bad skill and continues — matching SkillCatalog.scanRoot and the MCP loop's own best-effort handling directly below it. A corrupt skill now drops out in isolation; its siblings and the plugin's MCP servers still come up. Regression test included.


Testing

  • @jarvis/core: 15/15 tests pass (2 new), typecheck clean, eslint clean.
  • @jarvis/plugin: 15/15 tests pass (1 new), typecheck clean, eslint clean.

🤖 Generated with Claude Code

claude added 2 commits July 22, 2026 01:23
…l sibling skills + MCP servers (#496)

`PluginManager.#reattachOne` iterated a plugin's skills with no per-skill
error guard, and the MCP-server loop ran after it. A single unreadable or
unparseable SKILL.md (e.g. a frontmatter typo introduced after install)
threw, aborting the rest of that plugin's reattach — every sibling skill
and every MCP server it declares. `reattachInstalled`'s bare
`.catch(() => {})` then swallowed the rejection, so the failure was
completely silent while `GET /v1/plugins` kept reporting the plugin as
fully installed.

Wrap the skills-loop body in a per-skill try/catch that skips the bad
skill and continues, matching `SkillCatalog.scanRoot` and the MCP loop's
own best-effort handling directly below it. A corrupt skill now drops out
in isolation; its siblings and the plugin's MCP servers still come up.

Adds a regression test asserting that after one skill's frontmatter is
corrupted, `reattachInstalled` still registers the sibling skill and the
MCP server.
…ated turn can't brick a conversation (#494)

`isToolCallTurn` gated tool dispatch on `finish_reason === "tool_calls"`,
but the assistant message was pushed into `conversation.messages`
unconditionally. When a provider reported `finish_reason: "length"`
(OpenAI) / `stop_reason: "max_tokens"` (Anthropic) alongside a non-empty,
well-formed `tool_calls` array — reachable when the model hits the
output-token cap mid-turn — the loop skipped dispatch and terminated,
persisting an assistant message carrying `tool_calls` with zero matching
`role:"tool"` replies. Both providers 400 on that history shape, so every
later turn and every resume on the conversation failed permanently, with
no in-product way to repair it.

Rename the guard to `messageHasToolCalls` and drop the finish_reason
conjunct: dispatch whenever the assistant turn carries tool calls. A
truncated turn (`length`) still ends the loop — but only after its tool
calls are answered, so the persisted history is always a valid shape. Any
other finish_reason continues so the model can react to the results.
Applied to both the blocking (`runWithUsage`) and streaming (`runStream`)
paths.

Adds regression tests on both paths asserting a length-tagged tool-call
turn dispatches the tools, appends the role:"tool" reply, and terminates
as length_limited with no dangling tool_calls.
@TYRMars TYRMars changed the title fix(plugin): guard per-skill reattach so one bad SKILL.md doesn't kill sibling skills + MCP servers (#496) fix: auto-resolve two code-review bugs — plugin reattach guard (#496) + truncated tool-call corruption (#494) Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants