fix(advisor): halt permanently rejected advisors and chunk large delta renders
Two shared failure modes with a single misbehaving advisor: - A permanently rejected request (invalid_request_error, e.g. a model the account no longer supports) retried forever: one notice, then silent re-attempts on every turn, rebuilding heavy context each cycle. Quota exhaustion already paused with a notice; this class now hard-stops the runtime after a permanent rejection or three consecutive backlog-drop cycles, with a visible notice. An explicit reset (/new, config rebuild, restart) re-enables it, and waitForCatchup resolves while halted so the primary agent never parks on a runtime that cannot drain. - The delta render ran synchronously on the event loop; replaying a multi-MB transcript after a reset blocked it for 600ms+ per render (measured 675ms at ~54MB). Large deltas now render in size- and count-bounded chunks that yield between slices (675ms -> single-digit ms stalls). Tool call/result pairing survives chunk boundaries via a shared whole-delta result index in formatSessionHistoryMarkdown; small per-turn deltas keep the synchronous fast path.
This commit is contained in:
@@ -46,6 +46,15 @@ export interface HistoryFormatOptions {
|
||||
* this so it sees what changed without re-reading the file.
|
||||
*/
|
||||
expandEditDiffs?: boolean;
|
||||
/**
|
||||
* Chunked rendering support: a caller formatting one logical transcript in
|
||||
* several calls (the advisor's chunked delta render) passes a result index
|
||||
* built over the WHOLE delta plus one shared consumed-id set, so a toolCall
|
||||
* finds its toolResult across chunk boundaries and the result is never
|
||||
* re-rendered as an orphan in a later chunk.
|
||||
*/
|
||||
toolResultIndex?: ReadonlyMap<string, ToolResultMessage>;
|
||||
consumedToolCallIds?: Set<string>;
|
||||
}
|
||||
|
||||
/** Max length of the primary-arg summary inside `→ tool(...)` lines. */
|
||||
@@ -273,13 +282,19 @@ export function formatSessionHistoryMarkdown(messages: unknown[], opts?: History
|
||||
}
|
||||
|
||||
// Index tool results by call id so each toolCall collapses to one line.
|
||||
const resultsByCallId = new Map<string, ToolResultMessage>();
|
||||
for (const msg of typed) {
|
||||
if (msg.role === "toolResult") {
|
||||
resultsByCallId.set(msg.toolCallId, msg);
|
||||
// Chunked callers supply a whole-delta index + shared consumed set so
|
||||
// call/result pairs resolve across chunk boundaries.
|
||||
let resultsByCallId = opts?.toolResultIndex;
|
||||
if (!resultsByCallId) {
|
||||
const local = new Map<string, ToolResultMessage>();
|
||||
for (const msg of typed) {
|
||||
if (msg.role === "toolResult") {
|
||||
local.set(msg.toolCallId, msg);
|
||||
}
|
||||
}
|
||||
resultsByCallId = local;
|
||||
}
|
||||
const consumed = new Set<string>();
|
||||
const consumed = opts?.consumedToolCallIds ?? new Set<string>();
|
||||
// In watched mode, consecutive same-role messages collapse under one label
|
||||
// (the watched agent emits one assistant message per tool call, so otherwise
|
||||
// every call repeats `**agent**:`). Cleared whenever a
|
||||
|
||||
Reference in New Issue
Block a user