@oh-my-pi/pi-coding-agent 17.2.11 → 17.2.12
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 +13 -0
- package/dist/{CHANGELOG-d9xenpn9.md → CHANGELOG-k9ghy5sn.md} +13 -0
- package/dist/cli.js +10795 -10791
- package/dist/types/advisor/runtime.d.ts +1 -1
- package/dist/types/config/custom-models.d.ts +31 -0
- package/dist/types/config/model-config-values.d.ts +19 -0
- package/dist/types/config/model-patch.d.ts +93 -0
- package/dist/types/config/model-provider-discovery.d.ts +51 -0
- package/dist/types/config/model-registry.d.ts +5 -52
- package/dist/types/config/settings.d.ts +5 -3
- package/dist/types/debug/raw-sse-buffer.d.ts +9 -0
- package/dist/types/discovery/agent-plugin-format.d.ts +6 -13
- package/dist/types/discovery/contained-path.d.ts +2 -3
- package/dist/types/eval/executor-base.d.ts +8 -2
- package/dist/types/eval/kernel-session-registry.d.ts +60 -0
- package/dist/types/export/share.d.ts +1 -1
- package/dist/types/lsp/diagnostics.d.ts +96 -0
- package/dist/types/lsp/index.d.ts +7 -128
- package/dist/types/lsp/servers.d.ts +72 -0
- package/dist/types/lsp/tool.d.ts +42 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +12 -0
- package/dist/types/lsp/writethrough.d.ts +33 -0
- package/dist/types/mcp/transports/header-policy.d.ts +2 -1
- package/dist/types/modes/components/agent-hub-projection.d.ts +2 -7
- package/dist/types/modes/components/agent-hub-renderer.d.ts +0 -7
- package/dist/types/modes/theme/color.d.ts +19 -0
- package/dist/types/modes/theme/loader.d.ts +19 -0
- package/dist/types/modes/theme/schema.d.ts +175 -0
- package/dist/types/modes/theme/symbols.d.ts +28 -0
- package/dist/types/modes/theme/theme-class.d.ts +244 -0
- package/dist/types/modes/theme/theme.d.ts +9 -280
- package/dist/types/modes/theme/tui-adapters.d.ts +13 -0
- package/dist/types/registry/agent-registry.d.ts +1 -3
- package/dist/types/secrets/index.d.ts +3 -1
- package/dist/types/secrets/message-transform.d.ts +40 -0
- package/dist/types/secrets/obfuscator.d.ts +0 -108
- package/dist/types/secrets/placeholder-scan.d.ts +95 -0
- package/dist/types/secrets/placeholder.d.ts +94 -0
- package/dist/types/secrets/replacement.d.ts +82 -0
- package/dist/types/session/agent-session-types.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/session-handoff.d.ts +3 -3
- package/dist/types/session/session-manager.d.ts +32 -0
- package/dist/types/session/session-provider-boundary.d.ts +1 -1
- package/dist/types/session/turn-recovery.d.ts +2 -2
- package/dist/types/slash-commands/builtin-collaboration.d.ts +2 -0
- package/dist/types/slash-commands/builtin-completions.d.ts +36 -0
- package/dist/types/slash-commands/builtin-control.d.ts +2 -0
- package/dist/types/slash-commands/builtin-lifecycle.d.ts +3 -0
- package/dist/types/slash-commands/builtin-marketplace.d.ts +11 -0
- package/dist/types/slash-commands/builtin-modes.d.ts +5 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/slash-commands/builtin-session.d.ts +2 -0
- package/dist/types/tools/gh-common.d.ts +69 -0
- package/dist/types/tools/gh-pr-checkout.d.ts +84 -0
- package/dist/types/tools/gh-pr-diff.d.ts +102 -0
- package/dist/types/tools/gh-run-watch.d.ts +78 -0
- package/dist/types/tools/gh-search.d.ts +61 -0
- package/dist/types/tools/gh-types.d.ts +340 -0
- package/dist/types/tools/gh-view.d.ts +88 -0
- package/dist/types/tools/gh.d.ts +4 -201
- package/dist/types/tools/read-archive.d.ts +16 -0
- package/dist/types/tools/read-format.d.ts +104 -0
- package/dist/types/tools/read-path-resolution.d.ts +19 -0
- package/dist/types/tools/read-pdf-images.d.ts +12 -0
- package/dist/types/tools/read-renderer.d.ts +24 -0
- package/dist/types/tools/read-selector.d.ts +27 -0
- package/dist/types/tools/read-sqlite.d.ts +16 -0
- package/dist/types/tools/read-summary.d.ts +19 -0
- package/dist/types/tools/read.d.ts +1 -23
- package/dist/types/tools/shell-tokenize.d.ts +2 -1
- package/dist/types/utils/changelog.d.ts +0 -5
- package/package.json +13 -13
- package/src/advisor/runtime.ts +12 -7
- package/src/config/custom-models.ts +188 -0
- package/src/config/model-config-values.ts +128 -0
- package/src/config/model-patch.ts +252 -0
- package/src/config/model-provider-discovery.ts +132 -0
- package/src/config/model-registry.ts +64 -704
- package/src/config/settings.ts +7 -3
- package/src/debug/raw-sse-buffer.ts +20 -0
- package/src/discovery/agent-plugin-format.ts +7 -7
- package/src/discovery/contained-path.ts +2 -5
- package/src/edit/hashline/diff.ts +5 -1
- package/src/eval/executor-base.ts +39 -6
- package/src/eval/jl/executor.ts +82 -371
- package/src/eval/kernel-session-registry.ts +398 -0
- package/src/eval/py/executor.ts +53 -261
- package/src/eval/rb/executor.ts +36 -279
- package/src/export/share.ts +2 -1
- package/src/lsp/diagnostics.ts +516 -0
- package/src/lsp/index.ts +20 -2819
- package/src/lsp/servers.ts +296 -0
- package/src/lsp/tool.ts +1352 -0
- package/src/lsp/workspace-diagnostics.ts +170 -0
- package/src/lsp/writethrough.ts +561 -0
- package/src/mcp/transports/header-policy.ts +1 -1
- package/src/modes/components/agent-hub-projection.ts +2 -2
- package/src/modes/components/agent-hub-renderer.ts +3 -7
- package/src/modes/controllers/command-controller.ts +4 -1
- package/src/modes/theme/color.ts +133 -0
- package/src/modes/theme/loader.ts +178 -0
- package/src/modes/theme/schema.ts +263 -0
- package/src/modes/theme/symbols.ts +1000 -0
- package/src/modes/theme/theme-class.ts +611 -0
- package/src/modes/theme/theme.ts +27 -2453
- package/src/modes/theme/tui-adapters.ts +279 -0
- package/src/registry/agent-registry.ts +2 -2
- package/src/secrets/index.ts +6 -12
- package/src/secrets/message-transform.ts +287 -0
- package/src/secrets/obfuscator.ts +39 -1303
- package/src/secrets/placeholder-scan.ts +506 -0
- package/src/secrets/placeholder.ts +309 -0
- package/src/secrets/replacement.ts +216 -0
- package/src/session/agent-session-types.ts +6 -0
- package/src/session/agent-session.ts +113 -7
- package/src/session/indexed-session-storage.ts +7 -2
- package/src/session/session-advisors.ts +32 -34
- package/src/session/session-handoff.ts +39 -20
- package/src/session/session-manager.ts +67 -3
- package/src/session/session-provider-boundary.ts +3 -6
- package/src/session/turn-recovery.ts +21 -12
- package/src/slash-commands/builtin-collaboration.ts +504 -0
- package/src/slash-commands/builtin-completions.ts +291 -0
- package/src/slash-commands/builtin-control.ts +78 -0
- package/src/slash-commands/builtin-lifecycle.ts +506 -0
- package/src/slash-commands/builtin-marketplace.ts +567 -0
- package/src/slash-commands/builtin-modes.ts +518 -0
- package/src/slash-commands/builtin-registry.ts +21 -3000
- package/src/slash-commands/builtin-session.ts +592 -0
- package/src/task/executor.ts +17 -14
- package/src/tools/gh-common.ts +288 -0
- package/src/tools/gh-pr-checkout.ts +679 -0
- package/src/tools/gh-pr-diff.ts +473 -0
- package/src/tools/gh-run-watch.ts +1015 -0
- package/src/tools/gh-search.ts +500 -0
- package/src/tools/gh-types.ts +379 -0
- package/src/tools/gh-view.ts +612 -0
- package/src/tools/gh.ts +109 -3821
- package/src/tools/read-archive.ts +209 -0
- package/src/tools/read-format.ts +593 -0
- package/src/tools/read-path-resolution.ts +36 -0
- package/src/tools/read-pdf-images.ts +250 -0
- package/src/tools/read-renderer.ts +289 -0
- package/src/tools/read-selector.ts +84 -0
- package/src/tools/read-sqlite.ts +215 -0
- package/src/tools/read-summary.ts +199 -0
- package/src/tools/read.ts +77 -1820
- package/src/tools/shell-tokenize.ts +1 -1
- package/src/utils/changelog.ts +1 -1
- package/src/utils/title-generator.ts +3 -1
- package/src/web/search/providers/zai.ts +12 -3
|
@@ -174,8 +174,8 @@ import {
|
|
|
174
174
|
deobfuscateSessionContext,
|
|
175
175
|
deobfuscateToolArguments,
|
|
176
176
|
obfuscateProviderContext,
|
|
177
|
-
|
|
178
|
-
} from "../secrets/obfuscator";
|
|
177
|
+
} from "../secrets/message-transform";
|
|
178
|
+
import type { SecretObfuscator } from "../secrets/obfuscator";
|
|
179
179
|
import {
|
|
180
180
|
AUTO_THINKING,
|
|
181
181
|
type ConfiguredThinkingLevel,
|
|
@@ -2056,6 +2056,41 @@ export class AgentSession {
|
|
|
2056
2056
|
*/
|
|
2057
2057
|
#prunedTerminalRefusal: AssistantMessage | undefined = undefined;
|
|
2058
2058
|
|
|
2059
|
+
/**
|
|
2060
|
+
* In-flight {@link #dispatchAgentEvent} promises. agent-core invokes the
|
|
2061
|
+
* event subscriber fire-and-forget, so a `message_end`/`agent_end` handler
|
|
2062
|
+
* can still be awaiting extension/subscriber/maintenance work — and thus its
|
|
2063
|
+
* `sessionManager`/`agent.state` append — after `agent.waitForIdle()`
|
|
2064
|
+
* resolves. Dispose drains this set so the late append lands *before* the
|
|
2065
|
+
* memory release, never after it.
|
|
2066
|
+
*/
|
|
2067
|
+
#inFlightEventHandlers = new Set<Promise<void>>();
|
|
2068
|
+
|
|
2069
|
+
/**
|
|
2070
|
+
* Subscriber entry point. Delegates to {@link #dispatchAgentEvent} and
|
|
2071
|
+
* records the dispatch in {@link #inFlightEventHandlers} until it settles so
|
|
2072
|
+
* {@link #drainInFlightEventHandlers} can await the session's async
|
|
2073
|
+
* event/persistence pipeline during teardown.
|
|
2074
|
+
*/
|
|
2075
|
+
#handleAgentEvent = (event: AgentEvent): Promise<void> => {
|
|
2076
|
+
const processing = this.#dispatchAgentEvent(event);
|
|
2077
|
+
this.#inFlightEventHandlers.add(processing);
|
|
2078
|
+
void processing.finally(() => this.#inFlightEventHandlers.delete(processing)).catch(() => {});
|
|
2079
|
+
return processing;
|
|
2080
|
+
};
|
|
2081
|
+
|
|
2082
|
+
/**
|
|
2083
|
+
* Await every in-flight event handler (and any it chains into) so a late
|
|
2084
|
+
* message/entry append cannot land after the caller clears session memory.
|
|
2085
|
+
* The agent must already be idle — otherwise new events keep arriving and
|
|
2086
|
+
* this never drains.
|
|
2087
|
+
*/
|
|
2088
|
+
async #drainInFlightEventHandlers(): Promise<void> {
|
|
2089
|
+
while (this.#inFlightEventHandlers.size > 0) {
|
|
2090
|
+
await Promise.allSettled([...this.#inFlightEventHandlers]);
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2059
2094
|
/** Internal handler for agent events - shared by subscribe and reconnect.
|
|
2060
2095
|
*
|
|
2061
2096
|
* `agent_end` handling schedules deferred post-prompt recovery work
|
|
@@ -2070,7 +2105,7 @@ export class AgentSession {
|
|
|
2070
2105
|
* `#postPromptTasksPromise` is set the moment `#emit` invokes this handler, so
|
|
2071
2106
|
* the recovery wait always sees the in-flight handler and blocks until it — and
|
|
2072
2107
|
* everything it schedules — settles. */
|
|
2073
|
-
#
|
|
2108
|
+
#dispatchAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
2074
2109
|
if (event.type === "tool_execution_end" && this.#isTerminalYieldToolResult(event)) {
|
|
2075
2110
|
const alreadyTerminated = this.#synchronouslyTerminatedYieldToolCallIds.delete(event.toolCallId);
|
|
2076
2111
|
if (!alreadyTerminated) {
|
|
@@ -2648,7 +2683,7 @@ export class AgentSession {
|
|
|
2648
2683
|
// here after maintenance routing, tagged isTerminal so subscribers can
|
|
2649
2684
|
// tell final settles from scheduled continuations.
|
|
2650
2685
|
await this.#emitSessionEvent({ ...event, isTerminal: !options?.willContinue });
|
|
2651
|
-
void this.#emitAgentEndNotification(activeMessages, options).catch(err => {
|
|
2686
|
+
void this.#emitAgentEndNotification([...activeMessages], options).catch(err => {
|
|
2652
2687
|
logger.error("Agent end extension notification failed", { err });
|
|
2653
2688
|
});
|
|
2654
2689
|
};
|
|
@@ -3835,8 +3870,6 @@ export class AgentSession {
|
|
|
3835
3870
|
this.#releasePowerAssertion();
|
|
3836
3871
|
await cleanupEmptyMoveSession(this.sessionManager, this.#movedFromEmptySessionFile);
|
|
3837
3872
|
this.#movedFromEmptySessionFile = undefined;
|
|
3838
|
-
// All teardown branches that can append session entries have settled.
|
|
3839
|
-
await this.sessionManager.close();
|
|
3840
3873
|
this.#closeAllProviderSessions("dispose");
|
|
3841
3874
|
this.setHindsightSessionState(undefined);
|
|
3842
3875
|
hindsightState?.dispose();
|
|
@@ -3851,6 +3884,77 @@ export class AgentSession {
|
|
|
3851
3884
|
}
|
|
3852
3885
|
this.#eventListeners = [];
|
|
3853
3886
|
this.#sessionChangeCallbacks.clear();
|
|
3887
|
+
|
|
3888
|
+
// A dispose triggered mid-turn (Ctrl-C / timeout / hard-killed subagent)
|
|
3889
|
+
// only *signals* the agent loop via the earlier abort(); the loop and the
|
|
3890
|
+
// session's fire-and-forget event handlers still unwind asynchronously.
|
|
3891
|
+
// Detach the response/SSE interceptors so a late frame cannot re-record
|
|
3892
|
+
// into rawSseDebugBuffer, then wait (bounded) for both the core run AND
|
|
3893
|
+
// the in-flight event/persistence handlers to settle — the latter can
|
|
3894
|
+
// still append the finished message/entries after agent.waitForIdle()
|
|
3895
|
+
// alone. Without this the release races the unwind and a disposed session
|
|
3896
|
+
// is repopulated with exactly the state we are trying to drop.
|
|
3897
|
+
this.agent.setProviderResponseInterceptor(undefined);
|
|
3898
|
+
this.agent.setRawSseEventInterceptor(undefined);
|
|
3899
|
+
let drained = false;
|
|
3900
|
+
try {
|
|
3901
|
+
await withTimeout(
|
|
3902
|
+
(async () => {
|
|
3903
|
+
await this.agent.waitForIdle();
|
|
3904
|
+
await this.#drainInFlightEventHandlers();
|
|
3905
|
+
})(),
|
|
3906
|
+
options.drainTimeoutMs ?? POST_PROMPT_DRAIN_TIMEOUT_MS,
|
|
3907
|
+
"Timed out waiting for the active agent run to settle during dispose",
|
|
3908
|
+
);
|
|
3909
|
+
drained = true;
|
|
3910
|
+
} catch (error) {
|
|
3911
|
+
logger.warn("Active agent run still settling at dispose deadline", { error: String(error) });
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
// Event handlers can reopen the append writer while they persist their
|
|
3915
|
+
// terminal message; that pipeline has drained (or hit the deadline).
|
|
3916
|
+
// Raise the write barrier BEFORE the final close: a handler that
|
|
3917
|
+
// outlived the deadline could otherwise enqueue disk work behind the
|
|
3918
|
+
// closing tail while we await it, and that work would run against the
|
|
3919
|
+
// file after a revival reopens it. The seal also bumps the disk epoch,
|
|
3920
|
+
// superseding queued tail work and fencing already-running atomic
|
|
3921
|
+
// rewrites at their commit guard; hot-path appends drained above are
|
|
3922
|
+
// already durable, and close() (scheduled post-seal) still flushes and
|
|
3923
|
+
// closes the writer.
|
|
3924
|
+
this.sessionManager.seal();
|
|
3925
|
+
await this.sessionManager.close();
|
|
3926
|
+
|
|
3927
|
+
// Release retained conversation memory. dispose() is terminal, and every
|
|
3928
|
+
// revival path reopens the transcript from disk (AgentLifecycleManager
|
|
3929
|
+
// reviver / persisted-revive / `history://`), so the in-memory copy is
|
|
3930
|
+
// dead weight from here on. Dropping it lets a parked subagent's session
|
|
3931
|
+
// graph shed its heavy payloads even while the lifecycle adoption record's
|
|
3932
|
+
// reviver closure still references the session object. Fixes #8003.
|
|
3933
|
+
this.#releaseRetainedSessionMemory();
|
|
3934
|
+
|
|
3935
|
+
// The deadline does not cancel the drain: a handler parked in a slow
|
|
3936
|
+
// extension hook resumes afterwards and would repopulate exactly the
|
|
3937
|
+
// state released above. Its disk writes are already dead — the release
|
|
3938
|
+
// SEALED the session manager (a revival may reopen the same JSONL
|
|
3939
|
+
// through a new manager the moment dispose returns, and this manager
|
|
3940
|
+
// must never race that writer) — so re-run only the in-memory reset
|
|
3941
|
+
// once the pipeline genuinely settles. The extension runner bounds hook
|
|
3942
|
+
// runtime, so this deferred pass is not unbounded.
|
|
3943
|
+
if (!drained) {
|
|
3944
|
+
void (async () => {
|
|
3945
|
+
await this.agent.waitForIdle();
|
|
3946
|
+
await this.#drainInFlightEventHandlers();
|
|
3947
|
+
this.#releaseRetainedSessionMemory();
|
|
3948
|
+
})().catch(error => logger.warn("Deferred dispose finalization failed", { error: String(error) }));
|
|
3949
|
+
}
|
|
3950
|
+
}
|
|
3951
|
+
|
|
3952
|
+
/** Drop the in-memory conversation state after the terminal dispose flush. */
|
|
3953
|
+
#releaseRetainedSessionMemory(): void {
|
|
3954
|
+
this.agent.reset();
|
|
3955
|
+
this.agent.setAppendOnlyContext(undefined);
|
|
3956
|
+
this.rawSseDebugBuffer.clear();
|
|
3957
|
+
this.sessionManager.releaseRetainedEntries();
|
|
3854
3958
|
}
|
|
3855
3959
|
|
|
3856
3960
|
#closeAllProviderSessions(reason: string): void {
|
|
@@ -6219,6 +6323,9 @@ export class AgentSession {
|
|
|
6219
6323
|
this.abortRetry();
|
|
6220
6324
|
this.#promptGeneration++;
|
|
6221
6325
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
6326
|
+
// Abort the handoff first so generic compaction cancellation cannot replace
|
|
6327
|
+
// the harness reason with an unreasoned "Handoff cancelled".
|
|
6328
|
+
this.#handoff.abortHandoff(new Error(options?.reason ?? "Handoff aborted by session"));
|
|
6222
6329
|
if (options?.preserveCompaction) {
|
|
6223
6330
|
// Manual `/compact` installed its own #compactionAbortController before
|
|
6224
6331
|
// this internal abort and must keep it alive (that marker is what makes
|
|
@@ -6230,7 +6337,6 @@ export class AgentSession {
|
|
|
6230
6337
|
} else {
|
|
6231
6338
|
this.abortCompaction();
|
|
6232
6339
|
}
|
|
6233
|
-
this.abortHandoff();
|
|
6234
6340
|
this.abortBash();
|
|
6235
6341
|
this.abortEval();
|
|
6236
6342
|
const postPromptDrain = this.#cancelPostPromptTasks();
|
|
@@ -120,8 +120,13 @@ export class IndexedSessionStorage implements SessionStorage {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
async drain(): Promise<void> {
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
// Quiesce EVERY pending backend operation, not just the drain-tracked
|
|
124
|
+
// fire-and-forget publishes: an atomic write whose commit guard passed
|
|
125
|
+
// just before a terminal seal is still on the wire with
|
|
126
|
+
// `trackDrain: false`, and a graceful shutdown (SessionManager.close)
|
|
127
|
+
// must not return while it can still publish under a reopened path.
|
|
128
|
+
while (this.#drainPending.size > 0 || this.#pathPending.size > 0) {
|
|
129
|
+
await Promise.allSettled([...this.#drainPending, ...this.#pathPending.values()]);
|
|
125
130
|
}
|
|
126
131
|
const error = this.#firstDrainError;
|
|
127
132
|
this.#firstDrainError = undefined;
|
|
@@ -1167,47 +1167,42 @@ export class SessionAdvisors {
|
|
|
1167
1167
|
const failedMessage = failedMessages.findLast(
|
|
1168
1168
|
(message): message is AssistantMessage => message.role === "assistant",
|
|
1169
1169
|
);
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
// an HTTP 429 thrown from prompt()); classify the raw error so a
|
|
1173
|
-
// structural usage limit still marks the exhausted credential.
|
|
1174
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1175
|
-
if (!AIError.isUsageLimit(error) && !isUsageLimitOutcome(extractHttpStatusFromError(error), message)) {
|
|
1176
|
-
return false;
|
|
1177
|
-
}
|
|
1178
|
-
const currentModel = advisor.agent.state.model;
|
|
1179
|
-
const outcome = await this.#host.modelRegistry.authStorage.markUsageLimitReached(
|
|
1180
|
-
currentModel.provider,
|
|
1181
|
-
advisor.providerSessionId,
|
|
1182
|
-
{
|
|
1183
|
-
retryAfterMs: extractRetryHint(undefined, message),
|
|
1184
|
-
baseUrl: currentModel.baseUrl,
|
|
1185
|
-
modelId: currentModel.id,
|
|
1186
|
-
signal,
|
|
1187
|
-
},
|
|
1188
|
-
);
|
|
1189
|
-
return outcome.switched;
|
|
1190
|
-
}
|
|
1191
|
-
if (failedMessage.content.some(block => block.type === "toolCall")) return false;
|
|
1170
|
+
const assistantFailure = failedMessage?.stopReason === "error" ? failedMessage : undefined;
|
|
1171
|
+
if (assistantFailure?.content.some(block => block.type === "toolCall")) return false;
|
|
1192
1172
|
|
|
1193
1173
|
const currentModel = advisor.agent.state.model;
|
|
1194
|
-
const message =
|
|
1195
|
-
const errorId =
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1174
|
+
const message = assistantFailure?.errorMessage ?? (error instanceof Error ? error.message : String(error));
|
|
1175
|
+
const errorId = assistantFailure
|
|
1176
|
+
? AIError.classifyMessage({
|
|
1177
|
+
api: currentModel.api,
|
|
1178
|
+
errorId: assistantFailure.errorId,
|
|
1179
|
+
errorMessage: message,
|
|
1180
|
+
errorStatus: assistantFailure.errorStatus,
|
|
1181
|
+
})
|
|
1182
|
+
: AIError.classify(error, currentModel.api);
|
|
1201
1183
|
if (AIError.is(errorId, AIError.Flag.Abort) || AIError.is(errorId, AIError.Flag.UserInterrupt)) return false;
|
|
1202
|
-
if (
|
|
1184
|
+
if (
|
|
1185
|
+
AIError.is(errorId, AIError.Flag.ContextOverflow) ||
|
|
1186
|
+
(assistantFailure && AIError.isContextOverflow(assistantFailure, currentModel.contextWindow ?? 0))
|
|
1187
|
+
) {
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1203
1190
|
|
|
1204
|
-
const
|
|
1191
|
+
const accountPolicyDenial = AIError.is(errorId, AIError.Flag.AccountPolicy);
|
|
1192
|
+
if (accountPolicyDenial) {
|
|
1193
|
+
const switched = await this.#host.modelRegistry.authStorage.rotateSessionCredential(
|
|
1194
|
+
currentModel.provider,
|
|
1195
|
+
advisor.providerSessionId,
|
|
1196
|
+
{ error: message, modelId: currentModel.id, signal },
|
|
1197
|
+
);
|
|
1198
|
+
if (switched) return true;
|
|
1199
|
+
}
|
|
1205
1200
|
|
|
1206
1201
|
const retryAfterMs = extractRetryHint(undefined, message);
|
|
1207
|
-
|
|
1202
|
+
const usageLimit =
|
|
1208
1203
|
AIError.is(errorId, AIError.Flag.UsageLimit) ||
|
|
1209
|
-
isUsageLimitOutcome(extractHttpStatusFromError(error), message)
|
|
1210
|
-
) {
|
|
1204
|
+
isUsageLimitOutcome(extractHttpStatusFromError(error), message);
|
|
1205
|
+
if (usageLimit) {
|
|
1211
1206
|
const outcome = await this.#host.modelRegistry.authStorage.markUsageLimitReached(
|
|
1212
1207
|
currentModel.provider,
|
|
1213
1208
|
advisor.providerSessionId,
|
|
@@ -1220,6 +1215,9 @@ export class SessionAdvisors {
|
|
|
1220
1215
|
);
|
|
1221
1216
|
if (outcome.switched) return true;
|
|
1222
1217
|
}
|
|
1218
|
+
if (!assistantFailure && !accountPolicyDenial && !usageLimit) return false;
|
|
1219
|
+
|
|
1220
|
+
const currentSelector = formatRetryFallbackSelector(currentModel, advisor.thinkingLevel);
|
|
1223
1221
|
|
|
1224
1222
|
const retrySettings = this.#host.settings.getGroup("retry");
|
|
1225
1223
|
if (!retrySettings.enabled || !retrySettings.modelFallback) return false;
|
|
@@ -14,7 +14,8 @@ import { logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
|
14
14
|
import type { ModelRegistry } from "../config/model-registry";
|
|
15
15
|
import type { Settings } from "../config/settings";
|
|
16
16
|
import type { ExtensionRunner, SessionBeforeSwitchResult } from "../extensibility/extensions";
|
|
17
|
-
import { obfuscateProviderContext
|
|
17
|
+
import { obfuscateProviderContext } from "../secrets/message-transform";
|
|
18
|
+
import type { SecretObfuscator } from "../secrets/obfuscator";
|
|
18
19
|
import type { HandoffResult, SessionHandoffOptions } from "./agent-session-types";
|
|
19
20
|
import type { BashSessionTransition } from "./bash-runner";
|
|
20
21
|
import type { SessionContext } from "./session-context";
|
|
@@ -29,6 +30,17 @@ function createHandoffFileName(date = new Date()): string {
|
|
|
29
30
|
return `handoff-${fileTimestamp}.md`;
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function throwIfHandoffAborted(signal: AbortSignal): void {
|
|
34
|
+
if (!signal.aborted) return;
|
|
35
|
+
const reason = signal.reason;
|
|
36
|
+
if (reason instanceof DOMException && reason.name === "AbortError") {
|
|
37
|
+
throw new Error("Handoff cancelled");
|
|
38
|
+
}
|
|
39
|
+
if (reason instanceof Error) throw reason;
|
|
40
|
+
if (typeof reason === "string" && reason.length > 0) throw new Error(reason);
|
|
41
|
+
throw new Error("Handoff aborted by session");
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
/** Capabilities borrowed from the owning AgentSession. */
|
|
33
45
|
export interface SessionHandoffHost {
|
|
34
46
|
agent: Agent;
|
|
@@ -80,10 +92,10 @@ export class SessionHandoff {
|
|
|
80
92
|
this.#host = host;
|
|
81
93
|
}
|
|
82
94
|
/**
|
|
83
|
-
* Cancel in-progress handoff generation.
|
|
95
|
+
* Cancel in-progress handoff generation, preserving a harness-provided reason.
|
|
84
96
|
*/
|
|
85
|
-
abortHandoff(): void {
|
|
86
|
-
this.#handoffAbortController?.abort();
|
|
97
|
+
abortHandoff(reason?: Error): void {
|
|
98
|
+
this.#handoffAbortController?.abort(reason);
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
/**
|
|
@@ -117,7 +129,7 @@ export class SessionHandoff {
|
|
|
117
129
|
const sourceSignal = options?.signal;
|
|
118
130
|
const onSourceAbort = () => {
|
|
119
131
|
if (!handoffSignal.aborted) {
|
|
120
|
-
handoffAbortController.abort();
|
|
132
|
+
handoffAbortController.abort(sourceSignal?.reason);
|
|
121
133
|
}
|
|
122
134
|
};
|
|
123
135
|
if (sourceSignal) {
|
|
@@ -130,9 +142,7 @@ export class SessionHandoff {
|
|
|
130
142
|
let advisorRecordersDetached = false;
|
|
131
143
|
let sessionTransitioned = false;
|
|
132
144
|
try {
|
|
133
|
-
|
|
134
|
-
throw new Error("Handoff cancelled");
|
|
135
|
-
}
|
|
145
|
+
throwIfHandoffAborted(handoffSignal);
|
|
136
146
|
|
|
137
147
|
const model = this.#host.model();
|
|
138
148
|
if (!model) {
|
|
@@ -207,11 +217,23 @@ export class SessionHandoff {
|
|
|
207
217
|
);
|
|
208
218
|
const handoffText = this.#host.deobfuscateFromProvider(rawHandoffText);
|
|
209
219
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
220
|
+
throwIfHandoffAborted(handoffSignal);
|
|
221
|
+
if (!handoffText || handoffText.trim().length === 0) {
|
|
222
|
+
// Empty/whitespace-only generation is a real failure, not a user
|
|
223
|
+
// cancellation. #7904 stopped masking provider errors as "Handoff
|
|
224
|
+
// cancelled"; an empty document is the remaining path that produced the
|
|
225
|
+
// same misleading, undebuggable message (#7993).
|
|
226
|
+
logger.warn("Handoff generation produced no content", {
|
|
227
|
+
sessionId: this.#host.sessionId(),
|
|
228
|
+
autoTriggered: options?.autoTriggered ?? false,
|
|
229
|
+
});
|
|
230
|
+
// Auto-handoff is best-effort: returning undefined lets maintenance fall
|
|
231
|
+
// back to context-full compaction. A user-initiated handoff must surface
|
|
232
|
+
// the failure instead of a silent, misleading "cancelled".
|
|
233
|
+
if (options?.autoTriggered) {
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
throw new Error("Handoff generation produced no content");
|
|
215
237
|
}
|
|
216
238
|
|
|
217
239
|
// Start a new session
|
|
@@ -309,13 +331,10 @@ export class SessionHandoff {
|
|
|
309
331
|
|
|
310
332
|
return { document: handoffText, savedPath };
|
|
311
333
|
} catch (error) {
|
|
312
|
-
// Only a genuine
|
|
313
|
-
//
|
|
314
|
-
//
|
|
315
|
-
|
|
316
|
-
if (handoffSignal.aborted) {
|
|
317
|
-
throw new Error("Handoff cancelled");
|
|
318
|
-
}
|
|
334
|
+
// Only a genuine cancellation (user Esc or an unreasoned source-signal
|
|
335
|
+
// abort) maps to "Handoff cancelled". A harness-provided abort reason and
|
|
336
|
+
// provider failures surface verbatim.
|
|
337
|
+
throwIfHandoffAborted(handoffSignal);
|
|
319
338
|
throw error;
|
|
320
339
|
} finally {
|
|
321
340
|
if (advisorRecordersDetached) {
|
|
@@ -468,6 +468,8 @@ export class SessionManager {
|
|
|
468
468
|
|
|
469
469
|
/** The single open append writer; the manager only ever writes one file at a time. */
|
|
470
470
|
#writer: SessionStorageWriter | undefined;
|
|
471
|
+
/** Sealed by {@link releaseRetainedEntries}: every later append/title/rewrite is a dropped no-op. */
|
|
472
|
+
#released = false;
|
|
471
473
|
/** Serializes async disk work (flush/close/atomic rewrite). Appends are synchronous and bypass it. */
|
|
472
474
|
#diskTail: Promise<void> = Promise.resolve();
|
|
473
475
|
#diskFailure: Error | undefined;
|
|
@@ -641,6 +643,16 @@ export class SessionManager {
|
|
|
641
643
|
}
|
|
642
644
|
|
|
643
645
|
async #authoritativelyRewriteCurrentStateLocked(operationError: Error): Promise<void> {
|
|
646
|
+
if (this.#released) {
|
|
647
|
+
// Terminal seal: repair would reset the disk tail (escaping the
|
|
648
|
+
// close() serialization) and atomically publish #fileBody() — after
|
|
649
|
+
// release that truncates, and a revival may already own the file.
|
|
650
|
+
// The original operation error still propagates to the caller.
|
|
651
|
+
logger.warn("Skipped authoritative session repair after terminal release", {
|
|
652
|
+
error: String(operationError),
|
|
653
|
+
});
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
644
656
|
if (!this.#persist || !this.#sessionFile) return;
|
|
645
657
|
const previousDiskTail = this.#diskTail;
|
|
646
658
|
const writer = this.#writer;
|
|
@@ -687,7 +699,7 @@ export class SessionManager {
|
|
|
687
699
|
const body = this.#fileBody();
|
|
688
700
|
try {
|
|
689
701
|
await this.#storage.writeTextAtomic(sessionFile, body, {
|
|
690
|
-
commitGuard: () => this.#diskEpoch === epoch,
|
|
702
|
+
commitGuard: () => !this.#released && this.#diskEpoch === epoch,
|
|
691
703
|
});
|
|
692
704
|
} catch (error) {
|
|
693
705
|
const recoveryErrors = [toError(error)];
|
|
@@ -791,6 +803,7 @@ export class SessionManager {
|
|
|
791
803
|
* concurrent completed entries are durable without recreating a vacated source.
|
|
792
804
|
*/
|
|
793
805
|
#rewriteSynchronously(): void {
|
|
806
|
+
if (this.#released) return;
|
|
794
807
|
if (!this.#persist || !this.#shouldHaveSessionFile()) return;
|
|
795
808
|
const targetPath = this.#liveRelocationWritePath() ?? this.#sessionFile;
|
|
796
809
|
if (!targetPath) return;
|
|
@@ -833,6 +846,7 @@ export class SessionManager {
|
|
|
833
846
|
*/
|
|
834
847
|
async #rewriteAtomically(): Promise<void> {
|
|
835
848
|
if (!this.#persist || !this.#sessionFile) return;
|
|
849
|
+
if (this.#released) return;
|
|
836
850
|
|
|
837
851
|
const startEpoch = this.#diskEpoch;
|
|
838
852
|
await this.#scheduleDiskWork(
|
|
@@ -858,6 +872,7 @@ export class SessionManager {
|
|
|
858
872
|
* their post-publish state updates.
|
|
859
873
|
*/
|
|
860
874
|
async #runFencedAtomicRewrite(epoch: number): Promise<boolean> {
|
|
875
|
+
if (this.#released) return false;
|
|
861
876
|
this.#atomicRewriteFenceEpoch = epoch;
|
|
862
877
|
try {
|
|
863
878
|
do {
|
|
@@ -867,7 +882,7 @@ export class SessionManager {
|
|
|
867
882
|
if (!sessionFile) return false;
|
|
868
883
|
if (this.#diskEpoch !== epoch) return false;
|
|
869
884
|
await this.#storage.writeTextAtomic(sessionFile, this.#fileBody(), {
|
|
870
|
-
commitGuard: () => this.#diskEpoch === epoch,
|
|
885
|
+
commitGuard: () => !this.#released && this.#diskEpoch === epoch,
|
|
871
886
|
});
|
|
872
887
|
if (this.#diskEpoch !== epoch) return false;
|
|
873
888
|
} while (this.#atomicRewriteDirty);
|
|
@@ -884,7 +899,7 @@ export class SessionManager {
|
|
|
884
899
|
}
|
|
885
900
|
|
|
886
901
|
#appendToSessionFile(entry: SessionEntry): void {
|
|
887
|
-
if (!this.#persist || !this.#sessionFile) return;
|
|
902
|
+
if (this.#released || !this.#persist || !this.#sessionFile) return;
|
|
888
903
|
if (this.#atomicEntryBatch) {
|
|
889
904
|
this.#fileIsCurrent = false;
|
|
890
905
|
this.#rewriteRequired = true;
|
|
@@ -979,6 +994,7 @@ export class SessionManager {
|
|
|
979
994
|
const line = this.#lineFor(entry);
|
|
980
995
|
await this.#scheduleDiskWork(
|
|
981
996
|
async () => {
|
|
997
|
+
if (this.#released) return;
|
|
982
998
|
const sessionFile = this.#sessionFile;
|
|
983
999
|
if (!sessionFile) return;
|
|
984
1000
|
try {
|
|
@@ -1093,6 +1109,10 @@ export class SessionManager {
|
|
|
1093
1109
|
}
|
|
1094
1110
|
|
|
1095
1111
|
#recordEntry(entry: SessionEntry): void {
|
|
1112
|
+
if (this.#released) {
|
|
1113
|
+
logger.warn("Dropped session entry appended after terminal release", { type: entry.type });
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1096
1116
|
this.#entries.push(entry);
|
|
1097
1117
|
this.#index.insert(entry);
|
|
1098
1118
|
const batch = this.#atomicEntryBatch;
|
|
@@ -1707,6 +1727,49 @@ export class SessionManager {
|
|
|
1707
1727
|
if (this.#diskFailure) throw this.#diskFailure;
|
|
1708
1728
|
}
|
|
1709
1729
|
|
|
1730
|
+
/**
|
|
1731
|
+
* Raise the terminal write barrier ahead of the final {@link close}. Once
|
|
1732
|
+
* sealed:
|
|
1733
|
+
* - every later append, title change, and rewrite is a dropped no-op —
|
|
1734
|
+
* including work an event handler tries to enqueue while dispose is
|
|
1735
|
+
* awaiting `close()` on the disk tail;
|
|
1736
|
+
* - the disk epoch is bumped, so queued-but-unexecuted tail work is
|
|
1737
|
+
* superseded and an ALREADY-RUNNING fenced/repair rewrite (awaiting the
|
|
1738
|
+
* tail, drain, writer close, or the atomic stage) fails its commit guard
|
|
1739
|
+
* at the rename fence instead of publishing over a revived file.
|
|
1740
|
+
* The final `close()` itself is scheduled after the bump and still runs;
|
|
1741
|
+
* pre-seal hot-path appends are already in the page cache. Idempotent;
|
|
1742
|
+
* terminal.
|
|
1743
|
+
*/
|
|
1744
|
+
seal(): void {
|
|
1745
|
+
if (this.#released) return;
|
|
1746
|
+
this.#released = true;
|
|
1747
|
+
this.#diskEpoch++;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/**
|
|
1751
|
+
* Terminal release: drop the in-memory transcript and complete the
|
|
1752
|
+
* {@link seal}. The entry journal and its index mirror the agent's message
|
|
1753
|
+
* array (tool results, file contents, base64 frame images); on a disposed
|
|
1754
|
+
* session — e.g. a parked subagent still referenced by the lifecycle
|
|
1755
|
+
* adoption record — they would otherwise stay pinned for the process
|
|
1756
|
+
* lifetime.
|
|
1757
|
+
*
|
|
1758
|
+
* Closes the append writer; with the seal up, nothing can reopen it. A
|
|
1759
|
+
* revival may reopen the same JSONL through a NEW manager the moment
|
|
1760
|
+
* dispose returns; a late event handler resuming on THIS manager must
|
|
1761
|
+
* never race that writer — and a post-release rewrite would persist the
|
|
1762
|
+
* now-empty entry list, truncating the transcript. Reads after this point
|
|
1763
|
+
* reopen from disk (revival, `history://`). Only call from session
|
|
1764
|
+
* dispose, after the final `close()`; idempotent.
|
|
1765
|
+
*/
|
|
1766
|
+
releaseRetainedEntries(): void {
|
|
1767
|
+
this.seal();
|
|
1768
|
+
this.#entries = [];
|
|
1769
|
+
this.#index.clear();
|
|
1770
|
+
this.#closeWriterEventually();
|
|
1771
|
+
}
|
|
1772
|
+
|
|
1710
1773
|
getCwd(): string {
|
|
1711
1774
|
return this.#cwd;
|
|
1712
1775
|
}
|
|
@@ -1924,6 +1987,7 @@ export class SessionManager {
|
|
|
1924
1987
|
* Auto titles are ignored once the user has set a name.
|
|
1925
1988
|
*/
|
|
1926
1989
|
async setSessionName(name: string, source: SessionTitleSource = "auto", trigger?: string): Promise<boolean> {
|
|
1990
|
+
if (this.#released) return false;
|
|
1927
1991
|
if (this.#titleSource === "user" && source === "auto") return false;
|
|
1928
1992
|
|
|
1929
1993
|
const title = SessionManager.#cleanTitle(name);
|
|
@@ -10,12 +10,9 @@ import { formatModelString } from "../config/model-resolver";
|
|
|
10
10
|
import type { Settings } from "../config/settings";
|
|
11
11
|
import { validateProviderMaxInFlightRequests } from "../config/settings";
|
|
12
12
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
type SecretObfuscator,
|
|
17
|
-
stripPendingSecretPlaceholderSuffix,
|
|
18
|
-
} from "../secrets/obfuscator";
|
|
13
|
+
import { deobfuscateSessionContext, obfuscateMessages } from "../secrets/message-transform";
|
|
14
|
+
import type { SecretObfuscator } from "../secrets/obfuscator";
|
|
15
|
+
import { stripPendingSecretPlaceholderSuffix } from "../secrets/placeholder";
|
|
19
16
|
import { normalizeModelContextImages } from "../utils/image-loading";
|
|
20
17
|
import { describeAttachedImagesForTextModel } from "../utils/image-vision-fallback";
|
|
21
18
|
import { type CustomMessage, convertToLlm } from "./messages";
|
|
@@ -924,9 +924,9 @@ export class TurnRecovery {
|
|
|
924
924
|
}
|
|
925
925
|
|
|
926
926
|
/**
|
|
927
|
-
* Check if an error is retryable (transient errors
|
|
927
|
+
* Check if an error is retryable (transient errors, usage limits, or
|
|
928
|
+
* account-scoped policy denials that can rotate credentials).
|
|
928
929
|
* Context overflow is NOT retryable (handled by compaction instead).
|
|
929
|
-
* Usage-limit errors are retryable because the retry handler performs credential switching.
|
|
930
930
|
*/
|
|
931
931
|
isRetryableError(message: AssistantMessage): boolean {
|
|
932
932
|
if (message.stopReason !== "error") return false;
|
|
@@ -937,12 +937,11 @@ export class TurnRecovery {
|
|
|
937
937
|
const contextWindow = this.#host.model()?.contextWindow ?? 0;
|
|
938
938
|
if (AIError.isContextOverflow(message, contextWindow)) return false;
|
|
939
939
|
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
// discarded and replayed.
|
|
940
|
+
// Credential rotation and classifier fallbacks are safe only before
|
|
941
|
+
// committed text, images, tool calls, or server tools. Thinking-only
|
|
942
|
+
// output remains replay-safe.
|
|
944
943
|
if (this.#hasReplayUnsafeOutput(message)) return false;
|
|
945
|
-
if (this.isClassifierRefusal(message)) return true;
|
|
944
|
+
if (AIError.is(id, AIError.Flag.AccountPolicy) || this.isClassifierRefusal(message)) return true;
|
|
946
945
|
return AIError.retriable(id);
|
|
947
946
|
}
|
|
948
947
|
|
|
@@ -1603,6 +1602,7 @@ export class TurnRecovery {
|
|
|
1603
1602
|
const id = this.#classifyRetryMessage(message);
|
|
1604
1603
|
const rateLimitReason = parseRateLimitReason(errorMessage);
|
|
1605
1604
|
const staleOpenAIResponsesReplayError = AIError.is(id, AIError.Flag.StaleResponsesItem);
|
|
1605
|
+
const accountPolicyDenial = AIError.is(id, AIError.Flag.AccountPolicy);
|
|
1606
1606
|
const recordedUsageLimitOutcome = await this.#usageLimitOutcomes.get(message);
|
|
1607
1607
|
const parsedRetryAfterMs = this.#parseRetryAfterMsFromError(errorMessage);
|
|
1608
1608
|
let delayMs = staleOpenAIResponsesReplayError
|
|
@@ -1672,6 +1672,14 @@ export class TurnRecovery {
|
|
|
1672
1672
|
const currentSelector = currentModel
|
|
1673
1673
|
? formatRetryFallbackSelector(currentModel, this.#host.thinkingLevel())
|
|
1674
1674
|
: undefined;
|
|
1675
|
+
if (accountPolicyDenial && currentModel) {
|
|
1676
|
+
switchedCredential = await this.#host.modelRegistry.authStorage.rotateSessionCredential(
|
|
1677
|
+
currentModel.provider,
|
|
1678
|
+
this.#host.sessionId(),
|
|
1679
|
+
{ error: errorMessage, modelId: currentModel.id },
|
|
1680
|
+
);
|
|
1681
|
+
if (switchedCredential) delayMs = 0;
|
|
1682
|
+
}
|
|
1675
1683
|
if (!staleOpenAIResponsesReplayError && !switchedCredential && currentSelector) {
|
|
1676
1684
|
// A refusal chain stops at the retry budget: the exhausted-attempt
|
|
1677
1685
|
// last resort is for provider failures, not classifier decisions.
|
|
@@ -1696,7 +1704,7 @@ export class TurnRecovery {
|
|
|
1696
1704
|
}
|
|
1697
1705
|
|
|
1698
1706
|
if (retryBudgetExhausted) {
|
|
1699
|
-
if (!switchedModel) {
|
|
1707
|
+
if (!switchedModel && !switchedCredential) {
|
|
1700
1708
|
const attempt = this.#retryAttempt - 1;
|
|
1701
1709
|
message.errorMessage = `Retry budget exhausted after ${attempt} ${attempt === 1 ? "retry" : "retries"}: ${errorMessage}`;
|
|
1702
1710
|
await this.persistTerminalEmptyErrorTurn(message);
|
|
@@ -1713,11 +1721,12 @@ export class TurnRecovery {
|
|
|
1713
1721
|
this.resolveRetry(); // Resolve so waitForRetry() completes
|
|
1714
1722
|
return false;
|
|
1715
1723
|
}
|
|
1716
|
-
//
|
|
1717
|
-
//
|
|
1718
|
-
|
|
1724
|
+
// A fallback model gets a fresh retry budget. Credential rotation
|
|
1725
|
+
// instead keeps the cumulative attempt count while bypassing the
|
|
1726
|
+
// same-route budget: every distinct account must be tried first.
|
|
1727
|
+
if (switchedModel) this.#retryAttempt = 1;
|
|
1719
1728
|
}
|
|
1720
|
-
if (classifierRefusal && !switchedModel) {
|
|
1729
|
+
if ((classifierRefusal || accountPolicyDenial) && !switchedCredential && !switchedModel) {
|
|
1721
1730
|
// A prior attempt in this saga already announced `auto_retry_start`
|
|
1722
1731
|
// (retryAttempt was incremented for each call to this method, so > 1
|
|
1723
1732
|
// means at least one earlier attempt started the loop) but this
|