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:
DarkPhilosophy
2026-07-17 05:47:01 +03:00
parent 4695db1ed6
commit 3be0663bf2
4 changed files with 612 additions and 26 deletions
@@ -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