@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +162 -40
- package/dist/cli.js +4559 -4543
- package/dist/types/advisor/config.d.ts +14 -6
- package/dist/types/advisor/runtime.d.ts +20 -11
- package/dist/types/autolearn/controller.d.ts +1 -0
- package/dist/types/config/model-discovery.d.ts +17 -0
- package/dist/types/config/model-resolver.d.ts +6 -2
- package/dist/types/config/settings-schema.d.ts +39 -10
- package/dist/types/config/settings.d.ts +50 -0
- package/dist/types/cursor.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +5 -2
- package/dist/types/eval/agent-bridge.d.ts +7 -19
- package/dist/types/exec/bash-executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
- package/dist/types/extensibility/extensions/runner.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +18 -0
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +55 -1
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/extensibility/utils.d.ts +2 -2
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/lsp/types.d.ts +3 -0
- package/dist/types/mcp/transports/stdio.d.ts +42 -1
- package/dist/types/modes/components/advisor-config.d.ts +8 -1
- package/dist/types/modes/components/agent-hub.d.ts +15 -0
- package/dist/types/modes/components/hook-editor.d.ts +7 -0
- package/dist/types/modes/components/model-hub.d.ts +5 -2
- package/dist/types/modes/components/session-selector.d.ts +2 -0
- package/dist/types/modes/controllers/command-controller.d.ts +8 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/print-mode.d.ts +1 -1
- package/dist/types/modes/warp-events.d.ts +24 -0
- package/dist/types/modes/warp-events.test.d.ts +1 -0
- package/dist/types/plan-mode/model-transition.d.ts +47 -0
- package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
- package/dist/types/registry/agent-lifecycle.d.ts +26 -1
- package/dist/types/registry/persisted-agents.d.ts +3 -0
- package/dist/types/sdk.d.ts +27 -5
- package/dist/types/session/agent-session.d.ts +34 -10
- package/dist/types/session/session-entries.d.ts +6 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-manager.d.ts +19 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
- package/dist/types/task/executor.d.ts +26 -1
- package/dist/types/task/index.d.ts +1 -1
- package/dist/types/task/label.d.ts +1 -1
- package/dist/types/task/parallel.d.ts +14 -0
- package/dist/types/task/structured-subagent.d.ts +111 -0
- package/dist/types/task/types.d.ts +51 -0
- package/dist/types/telemetry-export.d.ts +34 -9
- package/dist/types/tools/approval.d.ts +8 -0
- package/dist/types/tools/bash.d.ts +2 -0
- package/dist/types/tools/essential-tools.d.ts +29 -0
- package/dist/types/tools/fetch.d.ts +4 -5
- package/dist/types/tools/hub/messaging.d.ts +5 -1
- package/dist/types/tools/image-gen.d.ts +2 -1
- package/dist/types/tools/index.d.ts +17 -1
- package/dist/types/tools/todo.d.ts +31 -0
- package/dist/types/tools/xdev.d.ts +11 -2
- package/dist/types/tui/tree-list.d.ts +7 -0
- package/dist/types/utils/markit.d.ts +11 -0
- package/dist/types/utils/title-generator.d.ts +2 -1
- package/dist/types/web/search/providers/kimi.d.ts +4 -1
- package/dist/types/web/search/types.d.ts +1 -1
- package/package.json +21 -16
- package/src/advisor/__tests__/advisor.test.ts +1304 -42
- package/src/advisor/__tests__/config.test.ts +58 -2
- package/src/advisor/config.ts +76 -24
- package/src/advisor/runtime.ts +445 -92
- package/src/autolearn/controller.ts +23 -28
- package/src/cli/file-processor.ts +1 -2
- package/src/cli.ts +5 -1
- package/src/config/model-discovery.ts +81 -21
- package/src/config/model-registry.ts +25 -6
- package/src/config/model-resolver.ts +14 -7
- package/src/config/settings-schema.ts +46 -9
- package/src/config/settings.ts +405 -25
- package/src/cursor.ts +20 -3
- package/src/debug/report-bundle.ts +40 -4
- package/src/discovery/helpers.ts +28 -5
- package/src/eval/__tests__/agent-bridge.test.ts +133 -47
- package/src/eval/__tests__/prelude-agent.test.ts +29 -0
- package/src/eval/agent-bridge.ts +104 -477
- package/src/eval/jl/prelude.jl +7 -6
- package/src/eval/js/shared/prelude.txt +5 -4
- package/src/eval/py/prelude.py +11 -39
- package/src/eval/rb/prelude.rb +5 -6
- package/src/exec/bash-executor.ts +14 -5
- package/src/extensibility/custom-tools/loader.ts +3 -3
- package/src/extensibility/custom-tools/wrapper.ts +2 -1
- package/src/extensibility/extensions/loader.ts +3 -3
- package/src/extensibility/extensions/managed-timers.ts +83 -0
- package/src/extensibility/extensions/runner.ts +26 -0
- package/src/extensibility/extensions/types.ts +18 -0
- package/src/extensibility/extensions/wrapper.ts +2 -1
- package/src/extensibility/hooks/loader.ts +3 -3
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +149 -8
- package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
- package/src/extensibility/plugins/manager.ts +2 -2
- package/src/extensibility/shared-events.ts +6 -0
- package/src/extensibility/utils.ts +91 -25
- package/src/irc/bus.ts +22 -3
- package/src/launch/broker.ts +3 -2
- package/src/launch/client.ts +2 -2
- package/src/launch/presence.ts +2 -2
- package/src/lsp/client.ts +58 -1
- package/src/lsp/index.ts +62 -6
- package/src/lsp/types.ts +3 -0
- package/src/main.ts +11 -8
- package/src/mcp/manager.ts +9 -3
- package/src/mcp/oauth-flow.ts +20 -0
- package/src/mcp/transports/stdio.test.ts +269 -1
- package/src/mcp/transports/stdio.ts +255 -24
- package/src/modes/components/advisor-config.ts +65 -3
- package/src/modes/components/agent-hub.ts +1 -72
- package/src/modes/components/ask-dialog.ts +1 -1
- package/src/modes/components/bash-execution.ts +7 -3
- package/src/modes/components/eval-execution.ts +3 -1
- package/src/modes/components/hook-editor.ts +18 -3
- package/src/modes/components/model-hub.ts +138 -42
- package/src/modes/components/session-selector.ts +4 -0
- package/src/modes/components/status-line/component.test.ts +1 -0
- package/src/modes/components/status-line/segments.ts +21 -6
- package/src/modes/controllers/command-controller.ts +167 -47
- package/src/modes/controllers/event-controller.ts +5 -0
- package/src/modes/controllers/extension-ui-controller.ts +4 -22
- package/src/modes/controllers/input-controller.ts +12 -12
- package/src/modes/controllers/selector-controller.ts +191 -31
- package/src/modes/interactive-mode.ts +169 -62
- package/src/modes/print-mode.ts +3 -3
- package/src/modes/rpc/host-tools.ts +2 -1
- package/src/modes/rpc/rpc-mode.ts +19 -4
- package/src/modes/warp-events.test.ts +794 -0
- package/src/modes/warp-events.ts +232 -0
- package/src/plan-mode/model-transition.test.ts +60 -0
- package/src/plan-mode/model-transition.ts +51 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/tools/eval.md +5 -2
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/task.md +12 -8
- package/src/prompts/tools/write.md +1 -1
- package/src/registry/agent-lifecycle.ts +133 -18
- package/src/registry/persisted-agents.ts +74 -0
- package/src/sdk.ts +343 -122
- package/src/session/agent-session.ts +1359 -368
- package/src/session/session-entries.ts +6 -1
- package/src/session/session-history-format.ts +20 -5
- package/src/session/session-manager.ts +57 -0
- package/src/session/streaming-output.ts +41 -1
- package/src/slash-commands/builtin-registry.ts +7 -0
- package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/executor.ts +100 -22
- package/src/task/index.ts +258 -429
- package/src/task/label.ts +2 -0
- package/src/task/parallel.ts +43 -0
- package/src/task/persisted-revive.ts +19 -7
- package/src/task/structured-subagent.ts +642 -0
- package/src/task/types.ts +58 -0
- package/src/telemetry-export.ts +453 -97
- package/src/tools/__tests__/eval-description.test.ts +1 -1
- package/src/tools/approval.ts +11 -0
- package/src/tools/bash.ts +71 -38
- package/src/tools/essential-tools.ts +45 -0
- package/src/tools/fetch.ts +28 -105
- package/src/tools/gh.ts +169 -2
- package/src/tools/hub/messaging.ts +16 -3
- package/src/tools/image-gen.ts +69 -7
- package/src/tools/index.ts +50 -15
- package/src/tools/path-utils.ts +1 -0
- package/src/tools/read.ts +62 -29
- package/src/tools/todo.ts +126 -13
- package/src/tools/write.ts +22 -4
- package/src/tools/xdev.ts +14 -3
- package/src/tui/tree-list.ts +39 -0
- package/src/utils/markit.ts +12 -0
- package/src/utils/title-generator.ts +15 -4
- package/src/utils/zip.ts +16 -1
- package/src/web/search/providers/kimi.ts +18 -12
- package/src/web/search/types.ts +6 -1
package/src/advisor/runtime.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { estimateTokens } from "@oh-my-pi/pi-agent-core/compaction";
|
|
3
3
|
import type { AssistantMessage, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
4
|
+
import * as AIError from "@oh-my-pi/pi-ai/error";
|
|
4
5
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
6
|
import { obfuscateToolArguments, type SecretObfuscator } from "../secrets/obfuscator";
|
|
6
7
|
import { formatSessionHistoryMarkdown, PRIMARY_CONTEXT_CUSTOM_TYPES } from "../session/session-history-format";
|
|
@@ -35,10 +36,10 @@ export interface AdvisorRuntimeHost {
|
|
|
35
36
|
* Pre-prompt context maintenance for the advisor's own append-only context.
|
|
36
37
|
* Promotes the advisor model to a larger sibling when its context nears the
|
|
37
38
|
* window (mirroring the primary's promote-first policy) and resolves `true`
|
|
38
|
-
* when the advisor
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
39
|
+
* when the advisor must clear its own context before sending the current
|
|
40
|
+
* incremental update. The cursor stays at the current primary position: this
|
|
41
|
+
* recovery path must never replay the full primary transcript.
|
|
42
|
+
* Optional: hosts that omit it get no proactive maintenance.
|
|
42
43
|
*/
|
|
43
44
|
maintainContext?(incomingTokens: number): Promise<boolean>;
|
|
44
45
|
/**
|
|
@@ -51,16 +52,36 @@ export interface AdvisorRuntimeHost {
|
|
|
51
52
|
/**
|
|
52
53
|
* Called with the error of every failed advisor turn, before the retry sleep
|
|
53
54
|
* or the dropped-after-3 path. Lets the host apply credential-level remedies
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* and swallowed.
|
|
55
|
+
* and configured model fallback that the advisor loop cannot perform itself.
|
|
56
|
+
* Return `true` after switching models so the same clean batch is retried
|
|
57
|
+
* immediately with a fresh failure budget. `failedMessages` contains the
|
|
58
|
+
* failed prompt's appended turns before rollback. Errors thrown here are
|
|
59
|
+
* logged and swallowed.
|
|
60
60
|
*/
|
|
61
|
-
onTurnError?(
|
|
61
|
+
onTurnError?(
|
|
62
|
+
error: unknown,
|
|
63
|
+
failedMessages: readonly AgentMessage[],
|
|
64
|
+
): Promise<boolean | undefined> | boolean | undefined;
|
|
65
|
+
/** Called after a successful advisor turn so the host can finish fallback lifecycle reporting. */
|
|
66
|
+
onTurnSuccess?(): Promise<void> | void;
|
|
62
67
|
/** Surface a non-recovering advisor failure to the host UI without adding model-visible context. */
|
|
63
68
|
notifyFailure?(error: unknown): void;
|
|
69
|
+
/** Signal that the advisor paused on a quota/rate-limit after host-level
|
|
70
|
+
* recovery (credential switch, fallback chain) declined. Cleared only by
|
|
71
|
+
* an explicit reset (`/new`, config rebuild, session restart). */
|
|
72
|
+
notifyQuotaExhausted?(): void;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* A request rejection that no amount of retrying can fix for this advisor
|
|
77
|
+
* configuration: the provider refuses the model/request shape outright (e.g.
|
|
78
|
+
* "The 'gpt-5.3-codex-spark' model is not supported when using Codex with a
|
|
79
|
+
* ChatGPT account", code=invalid_request_error). Distinct from quota errors,
|
|
80
|
+
* which pause via the dedicated quota path and auto-resume on reset.
|
|
81
|
+
*/
|
|
82
|
+
function isPermanentAdvisorError(error: unknown): boolean {
|
|
83
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
84
|
+
return /invalid_request_error|model[_ ]not[_ ]found|is not supported when|does not exist/i.test(message);
|
|
64
85
|
}
|
|
65
86
|
|
|
66
87
|
const ADVISOR_QUARANTINE_PREFIX = "Advisor response quarantined";
|
|
@@ -178,11 +199,22 @@ export function buildAdvisorQuarantineSourceText(currentInput: string, messages:
|
|
|
178
199
|
*/
|
|
179
200
|
const MAX_COALESCE_ROUNDS = 3;
|
|
180
201
|
|
|
202
|
+
const ADVISOR_RENDER_OPTIONS = {
|
|
203
|
+
includeThinking: true,
|
|
204
|
+
includeToolIntent: true,
|
|
205
|
+
watchedRoles: true,
|
|
206
|
+
expandPrimaryContext: true,
|
|
207
|
+
expandEditDiffs: true,
|
|
208
|
+
} as const;
|
|
209
|
+
|
|
181
210
|
interface PendingDelta {
|
|
182
211
|
text: string;
|
|
212
|
+
rawMessages: AgentMessage[];
|
|
213
|
+
renderRevision: number;
|
|
183
214
|
turns: number;
|
|
184
215
|
/** Whether the primary was mid-turn (willContinue:true) when this delta was rendered. */
|
|
185
216
|
wip: boolean;
|
|
217
|
+
overflowRecovery?: boolean;
|
|
186
218
|
}
|
|
187
219
|
|
|
188
220
|
interface CatchupWaiter {
|
|
@@ -192,19 +224,55 @@ interface CatchupWaiter {
|
|
|
192
224
|
timer?: NodeJS.Timeout;
|
|
193
225
|
}
|
|
194
226
|
|
|
227
|
+
interface DeliveredMessage {
|
|
228
|
+
message: AgentMessage;
|
|
229
|
+
fingerprint: bigint | undefined;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function fingerprintMessage(message: AgentMessage): bigint | undefined {
|
|
233
|
+
try {
|
|
234
|
+
const serialized = JSON.stringify(message);
|
|
235
|
+
if (serialized === undefined) return undefined;
|
|
236
|
+
return Bun.hash.wyhash(serialized);
|
|
237
|
+
} catch {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
195
242
|
export class AdvisorRuntime {
|
|
196
243
|
#lastCount = 0;
|
|
244
|
+
/**
|
|
245
|
+
* Delivered prefix identities. References make the normal append-only path
|
|
246
|
+
* allocation-free; fingerprints preserve identity across equivalent clones.
|
|
247
|
+
*/
|
|
248
|
+
#deliveredPrefix: DeliveredMessage[] = [];
|
|
197
249
|
/** Last-shown body, keyed by primary-context customType (plan/goal mode rules,
|
|
198
250
|
* approved plan). These prompts are re-injected verbatim every primary turn;
|
|
199
251
|
* this lets {@link #renderDelta} collapse an unchanged copy to a one-line
|
|
200
252
|
* marker so the advisor isn't re-fed the full ~1k-token rules each turn.
|
|
201
253
|
* Cleared on every re-prime/seed and when a failed batch is dropped. */
|
|
202
254
|
#seenContext = new Map<string, string>();
|
|
255
|
+
/** Incremented whenever the advisor loses context so queued raw deltas are re-rendered against fresh dedupe state. */
|
|
256
|
+
#renderRevision = 0;
|
|
203
257
|
#pending: PendingDelta[] = [];
|
|
204
258
|
#busy = false;
|
|
205
259
|
#backlog = 0;
|
|
206
260
|
#consecutiveFailures = 0;
|
|
207
261
|
#failureNotified = false;
|
|
262
|
+
/** Completed 3-failure backlog-drop cycles since the last success/reset. */
|
|
263
|
+
#droppedBacklogs = 0;
|
|
264
|
+
/**
|
|
265
|
+
* Hard stop after repeated drop cycles or a permanent request rejection
|
|
266
|
+
* (e.g. "model not supported"): without it the advisor re-attempts on every
|
|
267
|
+
* new delta forever, and in a shared daemon that unbounded churn burns CPU
|
|
268
|
+
* and starves every hosted session's event loop. Cleared only by an
|
|
269
|
+
* explicit {@link reset} (config rebuild, /new, session restart).
|
|
270
|
+
*/
|
|
271
|
+
#halted = false;
|
|
272
|
+
/** True from the moment an advisor turn fails until one succeeds (or an
|
|
273
|
+
* explicit reset/seed). While set, {@link waitForCatchup} resolves
|
|
274
|
+
* immediately: the primary agent NEVER parks on a failing advisor. */
|
|
275
|
+
#failing = false;
|
|
208
276
|
#latestMessages?: AgentMessage[];
|
|
209
277
|
#waiters: CatchupWaiter[] = [];
|
|
210
278
|
/** Bumped by every external {@link reset}/{@link dispose}. A drain iteration
|
|
@@ -213,6 +281,13 @@ export class AdvisorRuntime {
|
|
|
213
281
|
* being retried/requeued into the post-reset conversation. */
|
|
214
282
|
#epoch = 0;
|
|
215
283
|
disposed = false;
|
|
284
|
+
/** Quota/rate-limit pause state. When `true`, the advisor stops processing
|
|
285
|
+
* turns and drops new deltas until an explicit {@link reset} clears it
|
|
286
|
+
* (triggered by `/new`, config rebuild, or session restart). There is no
|
|
287
|
+
* timer-based auto-resume: provider quota windows (5h/7d) are far longer
|
|
288
|
+
* than any reasonable timer, and premature retries waste calls and
|
|
289
|
+
* re-trigger the same error. */
|
|
290
|
+
#quotaExhausted = false;
|
|
216
291
|
|
|
217
292
|
constructor(
|
|
218
293
|
private readonly agent: AdvisorAgent,
|
|
@@ -223,6 +298,16 @@ export class AdvisorRuntime {
|
|
|
223
298
|
get backlog(): number {
|
|
224
299
|
return this.#backlog;
|
|
225
300
|
}
|
|
301
|
+
get quotaExhausted(): boolean {
|
|
302
|
+
return this.#quotaExhausted;
|
|
303
|
+
}
|
|
304
|
+
get failureNotified(): boolean {
|
|
305
|
+
return this.#failureNotified;
|
|
306
|
+
}
|
|
307
|
+
/** True after the runtime hard-stopped on repeated or permanent failures. */
|
|
308
|
+
get halted(): boolean {
|
|
309
|
+
return this.#halted;
|
|
310
|
+
}
|
|
226
311
|
|
|
227
312
|
/**
|
|
228
313
|
* True when `#pending` is non-empty while the drain loop is busy — i.e., newer
|
|
@@ -247,13 +332,34 @@ export class AdvisorRuntime {
|
|
|
247
332
|
* the delta and forwarded to the reprime path so it is never silently dropped.
|
|
248
333
|
*/
|
|
249
334
|
onTurnEnd(messages?: AgentMessage[], opts?: { willContinue?: boolean }): void {
|
|
250
|
-
if (this.disposed) return;
|
|
335
|
+
if (this.disposed || this.#quotaExhausted || this.#halted) return;
|
|
251
336
|
const all = messages ?? this.host.snapshotMessages();
|
|
252
337
|
this.#latestMessages = all;
|
|
253
338
|
const wip = opts?.willContinue ?? false;
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
339
|
+
let rendered: Omit<PendingDelta, "turns" | "overflowRecovery"> | null = null;
|
|
340
|
+
// #renderDelta advances the cursor/prefix/dedup state before formatting
|
|
341
|
+
// can throw; snapshot them so a formatter bug loses NOTHING — the next
|
|
342
|
+
// turn re-renders this delta (a prefix change mid-render self-heals via
|
|
343
|
+
// the fingerprint scan, at worst costing one full replay).
|
|
344
|
+
const cursorBefore = this.#lastCount;
|
|
345
|
+
const prefixBefore = this.#deliveredPrefix.slice();
|
|
346
|
+
const seenBefore = [...this.#seenContext];
|
|
347
|
+
try {
|
|
348
|
+
rendered = this.#renderDelta(all, wip);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
// A render bug must never propagate into the primary agent's
|
|
351
|
+
// turn-end callback: the advisor skips this delta and stops gating
|
|
352
|
+
// the catch-up wait until a turn succeeds.
|
|
353
|
+
this.#lastCount = cursorBefore;
|
|
354
|
+
this.#deliveredPrefix = prefixBefore;
|
|
355
|
+
this.#seenContext.clear();
|
|
356
|
+
for (const [key, value] of seenBefore) this.#seenContext.set(key, value);
|
|
357
|
+
this.#failing = true;
|
|
358
|
+
this.#wakeAllWaiters();
|
|
359
|
+
logger.warn("advisor delta render failed", { err: String(err) });
|
|
360
|
+
}
|
|
361
|
+
if (rendered) {
|
|
362
|
+
this.#pending.push({ ...rendered, turns: 1 });
|
|
257
363
|
this.#backlog++;
|
|
258
364
|
this.#notifyWaiters();
|
|
259
365
|
void this.#drain();
|
|
@@ -261,7 +367,18 @@ export class AdvisorRuntime {
|
|
|
261
367
|
}
|
|
262
368
|
|
|
263
369
|
waitForCatchup(maxMs: number, threshold: number, signal?: AbortSignal): Promise<void> {
|
|
264
|
-
if (
|
|
370
|
+
if (
|
|
371
|
+
this.disposed ||
|
|
372
|
+
signal?.aborted ||
|
|
373
|
+
this.#backlog < threshold ||
|
|
374
|
+
this.#quotaExhausted ||
|
|
375
|
+
this.#halted ||
|
|
376
|
+
// An advisor mid-failure/retry must NEVER gate the primary agent:
|
|
377
|
+
// its backlog cannot drain until the retry cycle resolves, and the
|
|
378
|
+
// primary would otherwise park for the full catch-up budget.
|
|
379
|
+
this.#failing
|
|
380
|
+
)
|
|
381
|
+
return Promise.resolve();
|
|
265
382
|
const { promise, resolve } = Promise.withResolvers<void>();
|
|
266
383
|
let waiter!: CatchupWaiter;
|
|
267
384
|
const finish = (): void => {
|
|
@@ -293,24 +410,53 @@ export class AdvisorRuntime {
|
|
|
293
410
|
} catch {}
|
|
294
411
|
}
|
|
295
412
|
|
|
413
|
+
#clearSeenContext(): void {
|
|
414
|
+
this.#seenContext.clear();
|
|
415
|
+
this.#renderRevision++;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
#clearAdvisorContextAtCurrentCursor(): void {
|
|
419
|
+
this.#consecutiveFailures = 0;
|
|
420
|
+
this.#failureNotified = false;
|
|
421
|
+
this.#clearSeenContext();
|
|
422
|
+
try {
|
|
423
|
+
this.agent.reset();
|
|
424
|
+
} catch {}
|
|
425
|
+
try {
|
|
426
|
+
this.agent.abort("advisor reset");
|
|
427
|
+
} catch {}
|
|
428
|
+
}
|
|
429
|
+
|
|
296
430
|
#resetAdvisorContext(clearBacklog: boolean, wakeWaiters: boolean): void {
|
|
297
431
|
this.#lastCount = 0;
|
|
432
|
+
this.#deliveredPrefix = [];
|
|
298
433
|
this.#pending = [];
|
|
299
|
-
this.#
|
|
300
|
-
this.#failureNotified = false;
|
|
301
|
-
this.#seenContext.clear();
|
|
434
|
+
this.#clearAdvisorContextAtCurrentCursor();
|
|
302
435
|
if (clearBacklog) {
|
|
303
436
|
this.#backlog = 0;
|
|
304
437
|
}
|
|
305
438
|
if (wakeWaiters) {
|
|
306
439
|
this.#wakeAllWaiters();
|
|
307
440
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Account one completed 3-failure backlog-drop cycle. Repeated cycles (or a
|
|
445
|
+
* single permanent request rejection, e.g. "model not supported") hard-stop
|
|
446
|
+
* the runtime: in a shared daemon, unbounded advisor churn re-builds heavy
|
|
447
|
+
* context on every new delta and starves every hosted session's event loop.
|
|
448
|
+
* Only an explicit {@link reset} (config rebuild, /new, restart) resumes.
|
|
449
|
+
*/
|
|
450
|
+
#noteDroppedBacklog(error: unknown): void {
|
|
451
|
+
this.#droppedBacklogs++;
|
|
452
|
+
if (this.#droppedBacklogs < 3 && !isPermanentAdvisorError(error)) return;
|
|
453
|
+
this.#halted = true;
|
|
454
|
+
this.#pending = [];
|
|
455
|
+
this.#wakeAllWaiters();
|
|
456
|
+
logger.warn("advisor halted after repeated failures; use /advisor or reload config to re-enable", {
|
|
457
|
+
droppedBacklogs: this.#droppedBacklogs,
|
|
458
|
+
err: String(error),
|
|
459
|
+
});
|
|
314
460
|
}
|
|
315
461
|
|
|
316
462
|
/**
|
|
@@ -322,6 +468,10 @@ export class AdvisorRuntime {
|
|
|
322
468
|
*/
|
|
323
469
|
reset(): void {
|
|
324
470
|
this.#epoch++;
|
|
471
|
+
this.#quotaExhausted = false;
|
|
472
|
+
this.#halted = false;
|
|
473
|
+
this.#failing = false;
|
|
474
|
+
this.#droppedBacklogs = 0;
|
|
325
475
|
this.#resetAdvisorContext(true, true);
|
|
326
476
|
}
|
|
327
477
|
|
|
@@ -331,42 +481,72 @@ export class AdvisorRuntime {
|
|
|
331
481
|
* advisor (which would be expensive and likely stale).
|
|
332
482
|
*/
|
|
333
483
|
seedTo(count: number): void {
|
|
334
|
-
|
|
484
|
+
const messages = this.host.snapshotMessages().slice(0, count);
|
|
485
|
+
this.#lastCount = messages.length;
|
|
486
|
+
this.#deliveredPrefix = messages.map(message => ({
|
|
487
|
+
message,
|
|
488
|
+
fingerprint: fingerprintMessage(message),
|
|
489
|
+
}));
|
|
335
490
|
this.#pending = [];
|
|
336
491
|
this.#backlog = 0;
|
|
337
492
|
this.#consecutiveFailures = 0;
|
|
493
|
+
this.#failing = false;
|
|
494
|
+
this.#droppedBacklogs = 0;
|
|
338
495
|
this.#failureNotified = false;
|
|
339
|
-
this.#
|
|
496
|
+
this.#clearSeenContext();
|
|
340
497
|
this.#wakeAllWaiters();
|
|
341
498
|
}
|
|
342
499
|
|
|
343
|
-
#
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
this.#
|
|
347
|
-
this.#seenContext.clear();
|
|
348
|
-
return null;
|
|
349
|
-
}
|
|
350
|
-
const delta = all
|
|
351
|
-
.slice(this.#lastCount)
|
|
352
|
-
.filter(m => !(m.role === "custom" && m.customType === "advisor"))
|
|
353
|
-
.map(m => this.#dedupContextMessage(m));
|
|
354
|
-
this.#lastCount = all.length;
|
|
500
|
+
#formatRawDelta(rawMessages: AgentMessage[], wip = false): string | null {
|
|
501
|
+
const delta = rawMessages
|
|
502
|
+
.filter(message => !(message.role === "custom" && message.customType === "advisor"))
|
|
503
|
+
.map(message => this.#dedupContextMessage(message));
|
|
355
504
|
if (delta.length === 0) return null;
|
|
356
505
|
const obfuscator = this.host.obfuscator;
|
|
357
506
|
const formattedDelta = obfuscator?.hasSecrets() ? obfuscateAdvisorDelta(obfuscator, delta) : delta;
|
|
358
|
-
const md = formatSessionHistoryMarkdown(formattedDelta,
|
|
359
|
-
includeThinking: true,
|
|
360
|
-
includeToolIntent: true,
|
|
361
|
-
watchedRoles: true,
|
|
362
|
-
expandPrimaryContext: true,
|
|
363
|
-
expandEditDiffs: true,
|
|
364
|
-
});
|
|
507
|
+
const md = formatSessionHistoryMarkdown(formattedDelta, ADVISOR_RENDER_OPTIONS);
|
|
365
508
|
if (!md.trim()) return null;
|
|
366
509
|
const heading = wip ? "### Session update [in progress — more steps follow]" : "### Session update";
|
|
367
510
|
return `${heading}\n\n${md}`;
|
|
368
511
|
}
|
|
369
512
|
|
|
513
|
+
#renderDelta(messages?: AgentMessage[], wip = false): Omit<PendingDelta, "turns" | "overflowRecovery"> | null {
|
|
514
|
+
const all = messages ?? this.#latestMessages ?? this.host.snapshotMessages();
|
|
515
|
+
let prefixChanged = all.length < this.#lastCount;
|
|
516
|
+
for (let i = 0; !prefixChanged && i < this.#lastCount; i++) {
|
|
517
|
+
const delivered = this.#deliveredPrefix[i];
|
|
518
|
+
const current = all[i];
|
|
519
|
+
if (delivered === undefined || current === undefined) {
|
|
520
|
+
prefixChanged = true;
|
|
521
|
+
break;
|
|
522
|
+
}
|
|
523
|
+
if (delivered.message === current) continue;
|
|
524
|
+
const fingerprint = fingerprintMessage(current);
|
|
525
|
+
if (
|
|
526
|
+
delivered.fingerprint === undefined ||
|
|
527
|
+
fingerprint === undefined ||
|
|
528
|
+
delivered.fingerprint !== fingerprint
|
|
529
|
+
) {
|
|
530
|
+
prefixChanged = true;
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
delivered.message = current;
|
|
534
|
+
}
|
|
535
|
+
if (prefixChanged) {
|
|
536
|
+
this.#epoch++;
|
|
537
|
+
this.#resetAdvisorContext(true, true);
|
|
538
|
+
}
|
|
539
|
+
const rawMessages = all.slice(this.#lastCount);
|
|
540
|
+
for (let i = this.#lastCount; i < all.length; i++) {
|
|
541
|
+
const message = all[i];
|
|
542
|
+
if (message === undefined) continue;
|
|
543
|
+
this.#deliveredPrefix.push({ message, fingerprint: fingerprintMessage(message) });
|
|
544
|
+
}
|
|
545
|
+
this.#lastCount = all.length;
|
|
546
|
+
const text = this.#formatRawDelta(rawMessages, wip);
|
|
547
|
+
return text ? { text, rawMessages, renderRevision: this.#renderRevision, wip } : null;
|
|
548
|
+
}
|
|
549
|
+
|
|
370
550
|
/**
|
|
371
551
|
* Collapse a re-injected primary-context prompt (plan/goal mode rules, the
|
|
372
552
|
* approved plan) to a short marker when its body is byte-identical to the
|
|
@@ -425,40 +605,50 @@ export class AdvisorRuntime {
|
|
|
425
605
|
}
|
|
426
606
|
|
|
427
607
|
/**
|
|
428
|
-
* Collect
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
608
|
+
* Collect the popped deltas into one batch, running `maintainContext` for
|
|
609
|
+
* correct token budgeting. Loops until the pending queue is stable (no new
|
|
610
|
+
* deltas arrived during a maintenance check) or the round cap is reached.
|
|
611
|
+
* Every `await` inside the loop has an epoch guard so a reset/dispose
|
|
612
|
+
* mid-await cannot leak a stale batch into the post-reset conversation.
|
|
613
|
+
*
|
|
614
|
+
* When maintenance requests recovery, only the advisor Agent/log is reset
|
|
615
|
+
* (at the current primary cursor) and the already-collected raw batch is
|
|
616
|
+
* re-rendered — older, already-delivered primary transcript is never
|
|
617
|
+
* replayed.
|
|
434
618
|
*
|
|
435
619
|
* The coalescing loop is capped at {@link MAX_COALESCE_ROUNDS} iterations so
|
|
436
620
|
* a pathologically fast primary combined with a slow `maintainContext` cannot
|
|
437
621
|
* stall dispatch indefinitely — any items still in `#pending` after the cap
|
|
438
|
-
* are left for the next drain iteration.
|
|
622
|
+
* are left for the next drain iteration. Overflow-recovery batches skip
|
|
623
|
+
* coalescing entirely: they retry exactly the bounded batch that overflowed.
|
|
439
624
|
*
|
|
440
625
|
* Returns `null` when the epoch was invalidated — caller should `continue`.
|
|
441
|
-
* Returns `{ batch: null, finalTurns }` when there is nothing to render but
|
|
442
|
-
* backlog still needs to be decremented.
|
|
443
626
|
*/
|
|
444
627
|
async #collectAndMaintainBatch(
|
|
445
628
|
epoch: number,
|
|
446
|
-
|
|
447
|
-
|
|
629
|
+
initial: PendingDelta[],
|
|
630
|
+
recoveringOverflow: boolean,
|
|
631
|
+
): Promise<{
|
|
632
|
+
batch: string | null;
|
|
633
|
+
rawMessages: AgentMessage[];
|
|
634
|
+
finalTurns: number;
|
|
635
|
+
wip: boolean;
|
|
636
|
+
resetContext: boolean;
|
|
637
|
+
} | null> {
|
|
448
638
|
let batchText = initial.map(b => b.text).join("\n\n");
|
|
639
|
+
let rawMessages = initial.flatMap(b => b.rawMessages);
|
|
449
640
|
let turns = initial.reduce((sum, b) => sum + b.turns, 0);
|
|
450
|
-
// Track WIP state of the most recent delta — forwarded to the
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
// #drain so the retry-requeue path preserves it on failed turns.
|
|
641
|
+
// Track WIP state of the most recent delta — forwarded to the re-render
|
|
642
|
+
// so a willContinue:true turn keeps its [in progress] heading. Also
|
|
643
|
+
// returned to #drain so the retry-requeue path preserves it on failed turns.
|
|
454
644
|
let wip = initial.at(-1)?.wip ?? false;
|
|
455
645
|
|
|
456
646
|
for (let round = 0; round < MAX_COALESCE_ROUNDS; round++) {
|
|
457
647
|
if (this.host.maintainContext) {
|
|
458
648
|
const incomingTokens = estimateTokens({ role: "user", content: batchText, timestamp: Date.now() });
|
|
459
|
-
let
|
|
649
|
+
let shouldResetContext = false;
|
|
460
650
|
try {
|
|
461
|
-
|
|
651
|
+
shouldResetContext = await this.host.maintainContext(incomingTokens);
|
|
462
652
|
} catch (err) {
|
|
463
653
|
logger.debug("advisor context maintenance failed", { err: String(err) });
|
|
464
654
|
}
|
|
@@ -466,18 +656,41 @@ export class AdvisorRuntime {
|
|
|
466
656
|
// invalidates this batch.
|
|
467
657
|
if (this.#epoch !== epoch) return null;
|
|
468
658
|
|
|
469
|
-
if (
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
659
|
+
if (shouldResetContext) {
|
|
660
|
+
// Once coalescing has begun (round > 0), deltas that arrived during
|
|
661
|
+
// this await are part of the coalescing window: tally them so
|
|
662
|
+
// finalTurns stays accurate for backlog accounting and their raw
|
|
663
|
+
// messages join the bounded re-render. On the initial round the
|
|
664
|
+
// popped batch stays bounded exactly as dispatched — later arrivals
|
|
665
|
+
// remain queued and ship as their own subsequent batch.
|
|
666
|
+
if (round > 0) {
|
|
667
|
+
const lateItems = this.#pending.splice(0);
|
|
668
|
+
turns += lateItems.reduce((sum, b) => sum + b.turns, 0);
|
|
669
|
+
if (lateItems.length > 0) {
|
|
670
|
+
wip = lateItems.at(-1)!.wip;
|
|
671
|
+
rawMessages = rawMessages.concat(lateItems.flatMap(b => b.rawMessages));
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
// Reset only the advisor Agent/log. The primary cursor, backlog,
|
|
675
|
+
// waiters, latest snapshot, and epoch stay untouched. Re-render only
|
|
676
|
+
// this already-popped raw batch so active plan/reference bodies are
|
|
677
|
+
// restored without replaying any older primary transcript.
|
|
678
|
+
this.#clearAdvisorContextAtCurrentCursor();
|
|
679
|
+
const rerendered = this.#formatRawDelta(rawMessages, wip);
|
|
680
|
+
return {
|
|
681
|
+
batch: rerendered ?? (batchText || null),
|
|
682
|
+
rawMessages,
|
|
683
|
+
finalTurns: turns,
|
|
684
|
+
wip,
|
|
685
|
+
resetContext: true,
|
|
686
|
+
};
|
|
478
687
|
}
|
|
479
688
|
}
|
|
480
689
|
|
|
690
|
+
// Overflow-recovery batches retry exactly the bounded batch that
|
|
691
|
+
// overflowed; pending updates stay queued behind them.
|
|
692
|
+
if (recoveringOverflow) break;
|
|
693
|
+
|
|
481
694
|
// On the final round stop here — any late arrivals would ship without
|
|
482
695
|
// a subsequent maintainContext budget check. Leave them in #pending for
|
|
483
696
|
// the next drain iteration where they will be properly budgeted.
|
|
@@ -489,11 +702,31 @@ export class AdvisorRuntime {
|
|
|
489
702
|
const late = this.#pending.splice(0);
|
|
490
703
|
if (late.length === 0) break;
|
|
491
704
|
batchText = [batchText, ...late.map(b => b.text)].join("\n\n");
|
|
705
|
+
rawMessages = rawMessages.concat(late.flatMap(b => b.rawMessages));
|
|
492
706
|
turns += late.reduce((sum, b) => sum + b.turns, 0);
|
|
493
707
|
wip = late.at(-1)!.wip;
|
|
494
708
|
}
|
|
495
709
|
|
|
496
|
-
return { batch: batchText || null, finalTurns: turns, wip };
|
|
710
|
+
return { batch: batchText || null, rawMessages, finalTurns: turns, wip, resetContext: false };
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
#terminalAssistantFailure(snapshot: number): AssistantMessage | undefined {
|
|
714
|
+
const messages = this.agent.state.messages;
|
|
715
|
+
for (let i = messages.length - 1; i >= snapshot; i--) {
|
|
716
|
+
const message = messages[i];
|
|
717
|
+
if (message.role === "assistant" && message.stopReason === "error") return message;
|
|
718
|
+
}
|
|
719
|
+
return undefined;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
#notifyFailureOnce(error: unknown): void {
|
|
723
|
+
if (this.#failureNotified) return;
|
|
724
|
+
this.#failureNotified = true;
|
|
725
|
+
try {
|
|
726
|
+
this.host.notifyFailure?.(error);
|
|
727
|
+
} catch (notifyErr) {
|
|
728
|
+
logger.warn("advisor failure notification failed", { err: String(notifyErr) });
|
|
729
|
+
}
|
|
497
730
|
}
|
|
498
731
|
|
|
499
732
|
async #drain(): Promise<void> {
|
|
@@ -501,13 +734,28 @@ export class AdvisorRuntime {
|
|
|
501
734
|
this.#busy = true;
|
|
502
735
|
try {
|
|
503
736
|
while (!this.disposed && this.#pending.length) {
|
|
737
|
+
let popped: PendingDelta[];
|
|
738
|
+
if (this.#pending[0]?.overflowRecovery) {
|
|
739
|
+
const recovery = this.#pending.shift();
|
|
740
|
+
if (!recovery) continue;
|
|
741
|
+
popped = [recovery];
|
|
742
|
+
} else {
|
|
743
|
+
popped = this.#pending.splice(0);
|
|
744
|
+
}
|
|
504
745
|
const epoch = this.#epoch;
|
|
505
|
-
const
|
|
746
|
+
for (const delta of popped) {
|
|
747
|
+
if (delta.renderRevision === this.#renderRevision) continue;
|
|
748
|
+
const refreshed = this.#formatRawDelta(delta.rawMessages, delta.wip);
|
|
749
|
+
if (refreshed) delta.text = refreshed;
|
|
750
|
+
delta.renderRevision = this.#renderRevision;
|
|
751
|
+
}
|
|
752
|
+
const recoveringOverflow = popped.some(delta => delta.overflowRecovery === true);
|
|
753
|
+
const result = await this.#collectAndMaintainBatch(epoch, popped, recoveringOverflow);
|
|
506
754
|
|
|
507
755
|
// Epoch was invalidated during batch collection; restart the loop.
|
|
508
756
|
if (result === null) continue;
|
|
509
757
|
|
|
510
|
-
const { batch, finalTurns, wip } = result;
|
|
758
|
+
const { batch, rawMessages, finalTurns, wip, resetContext } = result;
|
|
511
759
|
|
|
512
760
|
if (this.disposed || batch === null) {
|
|
513
761
|
this.#backlog = Math.max(0, this.#backlog - finalTurns);
|
|
@@ -522,6 +770,7 @@ export class AdvisorRuntime {
|
|
|
522
770
|
// batch on top of stale turns and the dropped-after-3 path would leak
|
|
523
771
|
// orphan failures into the next successful run's context.
|
|
524
772
|
const messageSnapshot = this.agent.state.messages.length;
|
|
773
|
+
const contextWasFresh = resetContext || recoveringOverflow || messageSnapshot === 0;
|
|
525
774
|
try {
|
|
526
775
|
// Reset the host's per-update advisor state (one-advise-per-update
|
|
527
776
|
// gate) before each model cycle so the new batch starts fresh.
|
|
@@ -540,16 +789,47 @@ export class AdvisorRuntime {
|
|
|
540
789
|
const turnError = getAdvisorTurnError(this.agent.state.messages.slice(messageSnapshot));
|
|
541
790
|
if (turnError) throw turnError;
|
|
542
791
|
success = true;
|
|
792
|
+
this.#failing = false;
|
|
543
793
|
this.#consecutiveFailures = 0;
|
|
544
794
|
this.#failureNotified = false;
|
|
795
|
+
this.#droppedBacklogs = 0;
|
|
796
|
+
if (this.host.onTurnSuccess) {
|
|
797
|
+
try {
|
|
798
|
+
await this.host.onTurnSuccess();
|
|
799
|
+
} catch (hookErr) {
|
|
800
|
+
logger.debug("advisor onTurnSuccess hook failed", { err: String(hookErr) });
|
|
801
|
+
}
|
|
802
|
+
}
|
|
545
803
|
} catch (err) {
|
|
546
804
|
// reset()/dispose() aborts the in-flight prompt; treat it as a
|
|
547
805
|
// reset, not a transient failure — drop the stale batch.
|
|
548
806
|
if (this.#epoch !== epoch) continue;
|
|
807
|
+
// Release any parked primary-agent waiters IMMEDIATELY — before
|
|
808
|
+
// the async onTurnError hook or any retry sleep — and refuse new
|
|
809
|
+
// parks until a turn succeeds. A failing advisor must never hold
|
|
810
|
+
// the primary on the catch-up gate.
|
|
811
|
+
this.#failing = true;
|
|
812
|
+
this.#wakeAllWaiters();
|
|
813
|
+
const failedMessages = this.agent.state.messages.slice(messageSnapshot);
|
|
814
|
+
const terminalFailure = this.#terminalAssistantFailure(messageSnapshot);
|
|
815
|
+
const terminalFailureId =
|
|
816
|
+
terminalFailure === undefined ? undefined : AIError.classifyMessage(terminalFailure);
|
|
817
|
+
const contextOverflow =
|
|
818
|
+
(terminalFailureId !== undefined && AIError.is(terminalFailureId, AIError.Flag.ContextOverflow)) ||
|
|
819
|
+
AIError.is(AIError.classify(err), AIError.Flag.ContextOverflow);
|
|
820
|
+
// A terminal provider failure that is neither retriable nor an
|
|
821
|
+
// overflow (e.g. a blocked prompt) will fail identically on every
|
|
822
|
+
// retry — classify it before rollback so the batch is dropped after
|
|
823
|
+
// one attempt instead of burning the 3-attempt budget (#5468).
|
|
824
|
+
const terminalFailureRetriable =
|
|
825
|
+
terminalFailureId === undefined ||
|
|
826
|
+
AIError.retriable(terminalFailureId) ||
|
|
827
|
+
AIError.is(terminalFailureId, AIError.Flag.ContextOverflow);
|
|
549
828
|
this.#rollbackFailedTurn(messageSnapshot);
|
|
550
829
|
logger.debug("advisor turn failed", { err: String(err) });
|
|
830
|
+
let recovered = false;
|
|
551
831
|
try {
|
|
552
|
-
await this.host.onTurnError?.(err);
|
|
832
|
+
recovered = (await this.host.onTurnError?.(err, failedMessages)) === true;
|
|
553
833
|
} catch (hookErr) {
|
|
554
834
|
logger.debug("advisor onTurnError hook failed", { err: String(hookErr) });
|
|
555
835
|
}
|
|
@@ -563,26 +843,99 @@ export class AdvisorRuntime {
|
|
|
563
843
|
}
|
|
564
844
|
// Epoch guard after the async error hook.
|
|
565
845
|
if (this.#epoch !== epoch) continue;
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
846
|
+
if (recovered) {
|
|
847
|
+
this.#consecutiveFailures = 0;
|
|
848
|
+
this.#failureNotified = false;
|
|
849
|
+
this.#pending.unshift({
|
|
850
|
+
text: batch,
|
|
851
|
+
rawMessages,
|
|
852
|
+
renderRevision: this.#renderRevision,
|
|
853
|
+
turns: finalTurns,
|
|
854
|
+
wip,
|
|
855
|
+
overflowRecovery: recoveringOverflow || undefined,
|
|
856
|
+
});
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
if (AIError.isUsageLimit(err)) {
|
|
860
|
+
// Host recovery (credential switch / fallback chain) declined:
|
|
861
|
+
// pause on the quota latch instead of burning retries — provider
|
|
862
|
+
// quota windows (5h/7d) outlast any retry budget. The batch is
|
|
863
|
+
// requeued and the backlog stays visible so reset() replays it.
|
|
864
|
+
logger.warn("advisor quota exhausted", { err: String(err) });
|
|
865
|
+
this.#quotaExhausted = true;
|
|
866
|
+
this.#consecutiveFailures = 0;
|
|
867
|
+
this.#failureNotified = false;
|
|
868
|
+
this.#clearSeenContext();
|
|
869
|
+
this.#pending.unshift({
|
|
870
|
+
text: batch,
|
|
871
|
+
rawMessages,
|
|
872
|
+
renderRevision: this.#renderRevision,
|
|
873
|
+
turns: finalTurns,
|
|
874
|
+
wip,
|
|
875
|
+
overflowRecovery: recoveringOverflow || undefined,
|
|
876
|
+
});
|
|
877
|
+
this.#wakeAllWaiters();
|
|
878
|
+
try {
|
|
879
|
+
this.host.notifyQuotaExhausted?.();
|
|
880
|
+
} catch (notifyErr) {
|
|
881
|
+
logger.warn("advisor quota notification failed", { err: String(notifyErr) });
|
|
576
882
|
}
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
if (!terminalFailureRetriable) {
|
|
886
|
+
logger.warn("advisor terminal failure is non-retriable; dropping bounded batch");
|
|
887
|
+
this.#notifyFailureOnce(err);
|
|
577
888
|
this.#consecutiveFailures = 0;
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
|
|
581
|
-
this.#
|
|
889
|
+
// The dropped batch may carry primary-context we never delivered; drop
|
|
890
|
+
// the seen-state too so queued raw deltas re-expand before delivery.
|
|
891
|
+
this.#clearSeenContext();
|
|
892
|
+
this.#noteDroppedBacklog(err);
|
|
582
893
|
success = true;
|
|
894
|
+
} else if (contextOverflow) {
|
|
895
|
+
this.#clearAdvisorContextAtCurrentCursor();
|
|
896
|
+
if (contextWasFresh) {
|
|
897
|
+
// The bounded update cannot fit even with no advisor history. Drop
|
|
898
|
+
// only this batch after its one fresh-context retry; pending and later
|
|
899
|
+
// deltas remain eligible so one oversized update cannot disable the advisor.
|
|
900
|
+
logger.warn("advisor update overflowed a fresh context; dropping bounded batch");
|
|
901
|
+
this.#notifyFailureOnce(err);
|
|
902
|
+
success = true;
|
|
903
|
+
} else {
|
|
904
|
+
// Retry once against the fresh advisor context, using only the same
|
|
905
|
+
// bounded raw batch. Pending updates remain queued behind it.
|
|
906
|
+
const recoveryBatch = this.#formatRawDelta(rawMessages, wip) ?? batch;
|
|
907
|
+
this.#pending.unshift({
|
|
908
|
+
text: recoveryBatch,
|
|
909
|
+
rawMessages,
|
|
910
|
+
renderRevision: this.#renderRevision,
|
|
911
|
+
turns: finalTurns,
|
|
912
|
+
wip,
|
|
913
|
+
overflowRecovery: true,
|
|
914
|
+
});
|
|
915
|
+
logger.debug("advisor context overflow recovered at current primary cursor");
|
|
916
|
+
}
|
|
583
917
|
} else {
|
|
584
|
-
this.#
|
|
585
|
-
|
|
918
|
+
this.#consecutiveFailures++;
|
|
919
|
+
if (this.#consecutiveFailures >= 3) {
|
|
920
|
+
logger.warn("advisor failed consecutively 3 times; dropping backlog to prevent stall");
|
|
921
|
+
this.#notifyFailureOnce(err);
|
|
922
|
+
this.#consecutiveFailures = 0;
|
|
923
|
+
// The dropped batch may carry primary-context we never delivered; drop
|
|
924
|
+
// the seen-state too so queued raw deltas re-expand before delivery.
|
|
925
|
+
this.#clearSeenContext();
|
|
926
|
+
this.#noteDroppedBacklog(err);
|
|
927
|
+
success = true;
|
|
928
|
+
} else {
|
|
929
|
+
this.#pending.unshift({
|
|
930
|
+
text: batch,
|
|
931
|
+
rawMessages,
|
|
932
|
+
renderRevision: this.#renderRevision,
|
|
933
|
+
turns: finalTurns,
|
|
934
|
+
wip,
|
|
935
|
+
overflowRecovery: recoveringOverflow || undefined,
|
|
936
|
+
});
|
|
937
|
+
await Bun.sleep(this.retryDelayMs);
|
|
938
|
+
}
|
|
586
939
|
}
|
|
587
940
|
}
|
|
588
941
|
|