Two native-inference hardening features from boocode_code_review_v2 §1 #12. MistakeTracker: new pure mistake-tracker.ts tracks consecutive heterogeneous tool failures (kinds surfaced per tool from tool-phase.ts). On 3 in a row the turn loop soft-nudges (model-facing recovery guidance + mistake_recovery sentinel + reset), then escalates to stopping the turn (cap-hit-style, Continue affordance) on a re-trip. Complements doom-loop (identical repeats) + cap-hit. File-provenance ledger: compaction.ts derives a deterministic ## Files Read list from the head messages' read-tool calls and injects it into the rolling-summary prompt so provenance survives compaction (no new table; read-only). mistake_recovery sentinel: MessageMetadata arm (server + web) + MessageBubble render branch. Built by 2 parallel agents. Server 545 tests passing (23 new); build + web tsc clean. Native-inference only. Builds on v2.7.3. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
66 lines
2.5 KiB
TypeScript
66 lines
2.5 KiB
TypeScript
import type { Message, ToolCall } from '../../types/api.js';
|
|
|
|
// v1.11.6: doom-loop guard. When the model calls the same tool with the
|
|
// same arguments DOOM_LOOP_THRESHOLD times in a row within one user-message
|
|
// turn, abort the recursion and run the same wrap-up summary path as the
|
|
// cap-hit case. Ported from opencode (DOOM_LOOP_THRESHOLD in
|
|
// session/processor.ts). Threshold of 3 is the smallest value that doesn't
|
|
// false-positive on a model that retries once after a transient error.
|
|
export const DOOM_LOOP_THRESHOLD = 3;
|
|
|
|
// Returns the name + args of the looping tool when the LAST
|
|
// DOOM_LOOP_THRESHOLD entries in `recentToolCalls` are identical (same name
|
|
// AND deep-equal args via JSON.stringify). Returns null otherwise.
|
|
// Pure; exported for unit-test access.
|
|
export function detectDoomLoop(
|
|
recentToolCalls: ToolCall[],
|
|
): { name: string; args: Record<string, unknown> } | null {
|
|
if (recentToolCalls.length < DOOM_LOOP_THRESHOLD) return null;
|
|
const last = recentToolCalls.slice(-DOOM_LOOP_THRESHOLD);
|
|
const ref = last[0]!;
|
|
const refArgs = JSON.stringify(ref.args);
|
|
for (let i = 1; i < last.length; i++) {
|
|
const tc = last[i]!;
|
|
if (tc.name !== ref.name) return null;
|
|
if (JSON.stringify(tc.args) !== refArgs) return null;
|
|
}
|
|
return { name: ref.name, args: ref.args };
|
|
}
|
|
|
|
export function isCapHitSentinel(m: Message): boolean {
|
|
return (
|
|
m.role === 'system' &&
|
|
m.metadata !== null &&
|
|
typeof m.metadata === 'object' &&
|
|
(m.metadata as { kind?: unknown }).kind === 'cap_hit'
|
|
);
|
|
}
|
|
|
|
// v1.11.6: parallel predicate. Same UI-only semantics as cap-hit sentinels —
|
|
// never sent to the LLM (filtered by buildMessagesPayload through the
|
|
// isAnySentinel check below).
|
|
export function isDoomLoopSentinel(m: Message): boolean {
|
|
return (
|
|
m.role === 'system' &&
|
|
m.metadata !== null &&
|
|
typeof m.metadata === 'object' &&
|
|
(m.metadata as { kind?: unknown }).kind === 'doom_loop'
|
|
);
|
|
}
|
|
|
|
// #12: mistake-recovery sentinel. Same UI-only semantics as cap-hit /
|
|
// doom-loop — never sent to the LLM (filtered via the isAnySentinel check
|
|
// below, which buildMessagesPayload + buildHeadPayload both consult).
|
|
export function isMistakeRecoverySentinel(m: Message): boolean {
|
|
return (
|
|
m.role === 'system' &&
|
|
m.metadata !== null &&
|
|
typeof m.metadata === 'object' &&
|
|
(m.metadata as { kind?: unknown }).kind === 'mistake_recovery'
|
|
);
|
|
}
|
|
|
|
export function isAnySentinel(m: Message): boolean {
|
|
return isCapHitSentinel(m) || isDoomLoopSentinel(m) || isMistakeRecoverySentinel(m);
|
|
}
|