fix(pi-claude-cli): unblock parameterless MCP tool calls in triage

Triage with claude-sonnet-4-6 via pi-claude-cli kept looping on
fn_review_spec calls that were rejected by pi's validator with
"root: must be object". Parameterless MCP tools (schema
{type:"object", properties:{}}) emit zero input_json_delta events,
so partialJson stayed "" and the catch fell through to
finalArgs = "" — a string, which TypeBox's Type.Object({}) rightly
refuses. Default empty partialJson to {} so the call lands.

Also:
- Add a 2-step reminder loop in triage before swapping to the
  fallback planning model — primary models that wrote PROMPT.md
  but forgot fn_review_spec recover from a nudge, no need to pay
  the cold-start tax of a new triage on a different model.
- Inject @runfusion/fusion's own pi extension into dashboard/
  daemon/serve sessions and propagate the path to createFnAgent
  via setHostExtensionPaths so fn_* tools register globally
  without requiring `pi install npm:@runfusion/fusion`.
- Drop the "historical" qualifier from replayed tool labels —
  Claude was reading "TOOL RESULT (historical Read):" as
  "previous session, ignore" and looping on verification.
- Remove subprocess-lifecycle stderr debug logs that landed for
  hang diagnosis — root cause is fixed, the noise can go.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
gsxdsm
2026-04-26 00:00:49 -07:00
parent 1d1d1edfb3
commit 371f2d8e01
13 changed files with 269 additions and 89 deletions

View File

@@ -840,11 +840,20 @@ describe("Triage re-pick after restart", () => {
const task = makeTask("FN-062", "triage");
store.getTask.mockResolvedValue(makeTaskDetail("FN-062", "triage"));
// Slow agent to keep task in processing
// Slow agent to keep task in processing — only the first prompt() blocks;
// subsequent calls (e.g. the no-APPROVE reminder loop in triage) resolve
// immediately so cleanup can drain.
let resolvePrompt: (() => void) | undefined;
let promptCallCount = 0;
mockedCreateFnAgent.mockResolvedValue({
session: {
prompt: vi.fn().mockImplementation(() => new Promise<void>((r) => { resolvePrompt = r; })),
prompt: vi.fn().mockImplementation(() => {
promptCallCount += 1;
if (promptCallCount === 1) {
return new Promise<void>((r) => { resolvePrompt = r; });
}
return Promise.resolve();
}),
dispose: vi.fn(),
},
} as any);