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:
@@ -1043,6 +1043,44 @@ export class TriageProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
// Before swapping to the fallback model, give the primary one more
|
||||
// shot with a pointed reminder. The model may have written PROMPT.md
|
||||
// but stopped without calling fn_review_spec — that's recoverable
|
||||
// with a nudge, no need to discard the session and pay the cold-start
|
||||
// tax of a new triage on a different model.
|
||||
const MAX_REVIEW_REMINDERS = 2;
|
||||
let reviewReminders = 0;
|
||||
while (
|
||||
specReviewVerdictRef.current !== "APPROVE" &&
|
||||
!this.pauseAborted.has(task.id) &&
|
||||
!this.stuckAborted.has(task.id) &&
|
||||
createdSubtasksRef.current.length === 0 &&
|
||||
reviewReminders < MAX_REVIEW_REMINDERS
|
||||
) {
|
||||
reviewReminders += 1;
|
||||
const verdictDesc =
|
||||
specReviewVerdictRef.current === null
|
||||
? "fn_review_spec was never called"
|
||||
: `verdict was ${specReviewVerdictRef.current}`;
|
||||
triageLog.warn(
|
||||
`${task.id} primary planning model returned without APPROVE (${verdictDesc}) — reminder ${reviewReminders}/${MAX_REVIEW_REMINDERS}`,
|
||||
);
|
||||
await this.store.logEntry(
|
||||
task.id,
|
||||
`Primary planning model returned without APPROVE (${verdictDesc}) — reminder ${reviewReminders}/${MAX_REVIEW_REMINDERS}`,
|
||||
);
|
||||
const reminder =
|
||||
specReviewVerdictRef.current === null
|
||||
? "You wrote the PROMPT.md but did not call `fn_review_spec()`. Call `fn_review_spec()` now to validate the spec. Do not stop until the verdict is APPROVE."
|
||||
: `Spec review verdict was ${specReviewVerdictRef.current}. Address the feedback, rewrite the PROMPT.md as needed, and call \`fn_review_spec()\` again. Do not stop until the verdict is APPROVE.`;
|
||||
stuckDetector?.recordActivity(task.id);
|
||||
await promptWithFallback(session, reminder);
|
||||
checkSessionError(session);
|
||||
if (this.pauseAborted.has(task.id) || this.stuckAborted.has(task.id)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const planningFallbackProvider = settings.planningFallbackProvider;
|
||||
const planningFallbackModelId = settings.planningFallbackModelId;
|
||||
const canRetryWithPlanningFallback =
|
||||
|
||||
Reference in New Issue
Block a user