@gajae-code/coding-agent 0.12.0 → 0.12.1
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 +82 -1
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/config/settings-schema.d.ts +54 -0
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +16 -2
- package/dist/types/extensibility/gjc-plugins/index.d.ts +12 -2
- package/dist/types/extensibility/gjc-plugins/installer.d.ts +63 -7
- package/dist/types/extensibility/gjc-plugins/lifecycle-reconciliation.d.ts +55 -0
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +64 -0
- package/dist/types/extensibility/gjc-plugins/runtime-quarantine.d.ts +67 -0
- package/dist/types/extensibility/gjc-plugins/session-validation.d.ts +3 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +124 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +18 -0
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +25 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +10 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
- package/dist/types/modes/acp/acp-agent.d.ts +2 -19
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +18 -0
- package/dist/types/modes/components/settings-selector.d.ts +9 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/index.d.ts +29 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +7 -3
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/query/revision-store.d.ts +2 -0
- package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
- package/dist/types/sdk/prompt-status.d.ts +23 -3
- package/dist/types/sdk/session.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +38 -2
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
- package/dist/types/session/session-manager.d.ts +23 -6
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +36 -7
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -0
- package/dist/types/skill-state/workflow-hud.d.ts +6 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +7 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/output-meta.d.ts +23 -2
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read.d.ts +0 -6
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/utils/gh.d.ts +13 -0
- package/dist/types/utils/image-resize.d.ts +1 -0
- package/dist/types/web/search/providers/kimi.d.ts +2 -0
- package/dist/types/web/search/providers/searxng.d.ts +7 -0
- package/dist/types/web/search/providers/xai.d.ts +2 -0
- package/package.json +10 -8
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +285 -0
- package/src/async/job-manager.ts +78 -20
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/commands/team.ts +19 -1
- package/src/config/file-lock.ts +11 -2
- package/src/config/settings-schema.ts +51 -0
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +19 -16
- package/src/defaults/gjc/skills/team/SKILL.md +2 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +37 -24
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/exa/mcp-client.ts +12 -3
- package/src/exec/bash-executor.ts +144 -27
- package/src/extensibility/extensions/runner.ts +15 -0
- package/src/extensibility/extensions/types.ts +18 -1
- package/src/extensibility/gjc-plugins/constrained-hooks.ts +8 -2
- package/src/extensibility/gjc-plugins/index.ts +19 -2
- package/src/extensibility/gjc-plugins/installer.ts +189 -50
- package/src/extensibility/gjc-plugins/lifecycle-reconciliation.ts +199 -0
- package/src/extensibility/gjc-plugins/lifecycle.ts +626 -0
- package/src/extensibility/gjc-plugins/observability.ts +3 -3
- package/src/extensibility/gjc-plugins/prompt-appendix.ts +5 -1
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +12 -2
- package/src/extensibility/gjc-plugins/runtime-quarantine.ts +121 -0
- package/src/extensibility/gjc-plugins/schema.ts +84 -12
- package/src/extensibility/gjc-plugins/session-validation.ts +9 -3
- package/src/extensibility/gjc-plugins/types.ts +141 -0
- package/src/extensibility/gjc-plugins/validation.ts +9 -0
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/ralplan-runtime.ts +411 -17
- package/src/gjc-runtime/team-launch.ts +8 -0
- package/src/gjc-runtime/team-runtime.ts +140 -32
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +10 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-runtime.ts +401 -611
- package/src/gjc-runtime/workflow-manifest.generated.json +8 -99
- package/src/gjc-runtime/workflow-manifest.ts +4 -68
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/modes/DESIGN.md +51 -0
- package/src/modes/acp/acp-agent.ts +298 -186
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/notifications-settings-editor.ts +12 -1
- package/src/modes/components/oauth-selector.ts +68 -30
- package/src/modes/components/session-observer-overlay.ts +305 -86
- package/src/modes/components/settings-selector.ts +56 -5
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-status-header.ts +3 -14
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/extension-ui-controller.ts +7 -0
- package/src/modes/controllers/selector-controller.ts +45 -0
- package/src/modes/runtime-init.ts +3 -0
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -4
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/search-tool-bm25.md +5 -0
- package/src/prompts/tools/task.md +1 -1
- package/src/reminders/star-reminder.ts +4 -42
- package/src/runtime-mcp/manager.ts +19 -4
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +6 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/index.ts +661 -126
- package/src/sdk/bus/kind-aware-reconciliation.ts +148 -73
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/reconciliation-store.ts +95 -16
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +22 -0
- package/src/sdk/session.ts +88 -9
- package/src/session/agent-session.ts +822 -172
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +44 -6
- package/src/session/internal/managed-session-storage.ts +48 -6
- package/src/session/session-manager.ts +1614 -435
- package/src/session/session-storage.ts +9 -1
- package/src/session/streaming-output.ts +238 -68
- package/src/setup/credential-import.ts +75 -13
- package/src/skill-state/active-state.ts +32 -0
- package/src/skill-state/workflow-hud.ts +17 -0
- package/src/skill-state/workflow-mutation-guard.ts +496 -2
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +23 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +23 -7
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +86 -55
- package/src/tools/ast-edit.ts +1 -0
- package/src/tools/ast-grep.ts +1 -0
- package/src/tools/bash-interactive.ts +8 -3
- package/src/tools/bash.ts +503 -60
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/output-meta.ts +108 -14
- package/src/tools/path-utils.ts +3 -2
- package/src/tools/read.ts +18 -0
- package/src/tools/review.ts +5 -7
- package/src/tools/search.ts +1 -0
- package/src/tools/subagent-render.ts +6 -2
- package/src/tools/subagent.ts +6 -0
- package/src/tools/telegram-send.ts +6 -3
- package/src/utils/gh.ts +39 -0
- package/src/utils/image-resize.ts +3 -0
- package/src/web/search/providers/anthropic.ts +4 -4
- package/src/web/search/providers/kimi.ts +15 -5
- package/src/web/search/providers/searxng.ts +47 -4
- package/src/web/search/providers/xai.ts +7 -2
- package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +0 -23
- package/src/defaults/gjc/skills/ultragoal/pipeline-validation-contracts.md +0 -30
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
type ManagedAttemptDecision,
|
|
37
37
|
type ManagedAttemptOutcome,
|
|
38
38
|
type MidRunMaintenanceOutcome,
|
|
39
|
+
type RunSettlementProof,
|
|
39
40
|
resolveTelemetry,
|
|
40
41
|
type StablePrefixSnapshot,
|
|
41
42
|
ThinkingLevel,
|
|
@@ -82,6 +83,7 @@ import type {
|
|
|
82
83
|
ProviderSessionState,
|
|
83
84
|
ServiceTier,
|
|
84
85
|
SimpleStreamOptions,
|
|
86
|
+
StopReason,
|
|
85
87
|
TextContent,
|
|
86
88
|
ToolCall,
|
|
87
89
|
ToolChoice,
|
|
@@ -105,6 +107,7 @@ import {
|
|
|
105
107
|
classifyFallbackTrigger,
|
|
106
108
|
type FallbackAttemptToken,
|
|
107
109
|
type FallbackTriggerClass,
|
|
110
|
+
STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
|
|
108
111
|
} from "@gajae-code/ai/utils/fallback-transport";
|
|
109
112
|
import {
|
|
110
113
|
BTW_MAX_ANSWER_UTF8_BYTES,
|
|
@@ -116,6 +119,7 @@ import {
|
|
|
116
119
|
truncateUtf8,
|
|
117
120
|
utf8ByteLength,
|
|
118
121
|
} from "./btw-contract";
|
|
122
|
+
import { DEFAULT_ARTIFACT_MAX_BYTES, truncateHeadBytes } from "./streaming-output";
|
|
119
123
|
|
|
120
124
|
export interface ForkContextSeedMetadata {
|
|
121
125
|
sourceSessionId: string;
|
|
@@ -162,12 +166,15 @@ export interface ForkContextSeedOptions {
|
|
|
162
166
|
import { MacOSPowerAssertion } from "@gajae-code/natives";
|
|
163
167
|
import {
|
|
164
168
|
extractRetryHint,
|
|
169
|
+
hasFsCode,
|
|
170
|
+
isEacces,
|
|
165
171
|
isEnoent,
|
|
166
172
|
isUnexpectedSocketCloseMessage,
|
|
167
173
|
logger,
|
|
168
174
|
prompt,
|
|
169
175
|
Snowflake,
|
|
170
176
|
} from "@gajae-code/utils";
|
|
177
|
+
|
|
171
178
|
import { createAppendOnlyContextManager, resolveAppendOnlyMode } from "../append-only-mode";
|
|
172
179
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
173
180
|
import { reset as resetCapabilities } from "../capability";
|
|
@@ -208,7 +215,7 @@ import {
|
|
|
208
215
|
executePython as executePythonCommand,
|
|
209
216
|
type PythonResult,
|
|
210
217
|
} from "../eval/py/executor";
|
|
211
|
-
import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
|
|
218
|
+
import { type BashArtifactSaveResult, type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
|
|
212
219
|
import { exportSessionToHtml } from "../export/html";
|
|
213
220
|
import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
|
|
214
221
|
import type { LoadedCustomCommand } from "../extensibility/custom-commands";
|
|
@@ -242,6 +249,7 @@ import {
|
|
|
242
249
|
resolveSubskillActivationForSkillInvocation,
|
|
243
250
|
} from "../extensibility/gjc-plugins";
|
|
244
251
|
import { resolveCurrentPhaseForParent } from "../extensibility/gjc-plugins/injection";
|
|
252
|
+
import type { GjcRuntimeSnapshotProvider } from "../extensibility/gjc-plugins/runtime-quarantine";
|
|
245
253
|
import { readActiveSubskillsForParent, toActiveSubskillEntry } from "../extensibility/gjc-plugins/state";
|
|
246
254
|
import { loadActiveSubskillTools } from "../extensibility/gjc-plugins/tools";
|
|
247
255
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
@@ -272,6 +280,7 @@ import {
|
|
|
272
280
|
registerCoordinatorRuntimeStateFinalizer,
|
|
273
281
|
} from "../gjc-runtime/session-state-sidecar";
|
|
274
282
|
import { requestGjcWorkerIntegrationAttempt } from "../gjc-runtime/team-runtime";
|
|
283
|
+
import { GjcTeamWorkerHeartbeatReporter } from "../gjc-runtime/team-worker-heartbeat";
|
|
275
284
|
import { GoalRuntime } from "../goals/runtime";
|
|
276
285
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
277
286
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
@@ -305,6 +314,7 @@ import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obf
|
|
|
305
314
|
import { formatNoCredentialOnboardingError, formatNoModelOnboardingError } from "../setup/model-onboarding-guidance";
|
|
306
315
|
import {
|
|
307
316
|
isCanonicalGjcWorkflowSkill,
|
|
317
|
+
isWorkflowContinuationInert,
|
|
308
318
|
readVisibleSkillActiveState,
|
|
309
319
|
syncSkillActiveState,
|
|
310
320
|
} from "../skill-state/active-state";
|
|
@@ -417,6 +427,32 @@ import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
|
417
427
|
import { pruneSupersededMaintenanceReminders, pruneSupersededVolatileProjectContext } from "./volatile-context-pruning";
|
|
418
428
|
import { YieldQueue } from "./yield-queue";
|
|
419
429
|
|
|
430
|
+
interface CompactionStateSnapshot {
|
|
431
|
+
goal: { objective: string; status: Goal["status"]; enabled: boolean } | undefined;
|
|
432
|
+
openTodos: string[];
|
|
433
|
+
activeSkills: Array<{ skill: string; phase: string }>;
|
|
434
|
+
queuedMessages: boolean;
|
|
435
|
+
lastAssistantStopReason: StopReason | undefined;
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Escape XML-ish metacharacters and flatten newlines so state text cannot break compaction prompt framing. */
|
|
439
|
+
function sanitizeCompactionStateText(value: string, maxLength: number): string {
|
|
440
|
+
return value
|
|
441
|
+
.replace(/&/g, "&")
|
|
442
|
+
.replace(/</g, "<")
|
|
443
|
+
.replace(/>/g, ">")
|
|
444
|
+
.replace(/\r\n/g, " ")
|
|
445
|
+
.replace(/[\r\n]/g, " ")
|
|
446
|
+
.slice(0, maxLength);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function appendCompactionStateContext(summary: string, stateContext: string[]): string {
|
|
450
|
+
if (stateContext.length === 0) return summary;
|
|
451
|
+
return `${summary}\n\n<compaction-state>\n${stateContext.join("\n")}\n</compaction-state>`;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const PRUNED_ARTIFACT_REF_MAX_CHARS = 64;
|
|
455
|
+
|
|
420
456
|
/** Session-specific events that extend the core AgentEvent */
|
|
421
457
|
export type AutoCompactionContinuationSkipReason = "auto_continue_disabled_non_resumable_tail";
|
|
422
458
|
|
|
@@ -506,6 +542,11 @@ export interface AgentSessionConfig {
|
|
|
506
542
|
workerIntegrationRequest?: (signal: AbortSignal) => Promise<void>;
|
|
507
543
|
/** Bound terminal worker-integration settlement for embedded hosts and deterministic lifecycle tests. */
|
|
508
544
|
workerIntegrationTimeoutMs?: number;
|
|
545
|
+
/**
|
|
546
|
+
* Override the runtime-owned `gjc team` worker heartbeat reporter. Defaults to a
|
|
547
|
+
* reporter derived from the team worker environment, or none outside a worker pane.
|
|
548
|
+
*/
|
|
549
|
+
teamWorkerHeartbeatReporter?: GjcTeamWorkerHeartbeatReporter;
|
|
509
550
|
|
|
510
551
|
/** Loaded skills (already discovered by SDK) */
|
|
511
552
|
skills?: Skill[];
|
|
@@ -828,6 +869,7 @@ type RetryErrorClassification =
|
|
|
828
869
|
|
|
829
870
|
const BARE_DEFAULT_WATCHDOG_ERROR =
|
|
830
871
|
/^(?:[A-Za-z][A-Za-z0-9-]*(?: [A-Za-z][A-Za-z0-9-]*){0,3} )stream (?:timed out while waiting for the first event|stalled while waiting for the next event)$/;
|
|
872
|
+
const BARE_DEFAULT_CODEX_OVERLOAD_ERROR = /^Codex error event(?:: .*)? \(code=server_is_overloaded(?:, [^)]+)*\)$/;
|
|
831
873
|
const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
|
|
832
874
|
"anthropic-messages": new Set([
|
|
833
875
|
"Provider stream timed out while waiting for the first event",
|
|
@@ -864,6 +906,14 @@ function hasBareDefaultRetryDisqualifyingFacts(message: AssistantMessage): boole
|
|
|
864
906
|
(facts.headers !== undefined && Object.keys(facts.headers).length > 0)
|
|
865
907
|
);
|
|
866
908
|
}
|
|
909
|
+
function isBareDefaultCodexOverload(message: AssistantMessage): boolean {
|
|
910
|
+
return (
|
|
911
|
+
message.api === "openai-codex-responses" &&
|
|
912
|
+
BARE_DEFAULT_CODEX_OVERLOAD_ERROR.test(message.errorMessage ?? "") &&
|
|
913
|
+
!hasBareDefaultRetryDisqualifyingFacts(message) &&
|
|
914
|
+
!assistantMessageHasVisibleOrToolContent(message)
|
|
915
|
+
);
|
|
916
|
+
}
|
|
867
917
|
|
|
868
918
|
function assistantMessageHasVisibleOrToolContent(message: AssistantMessage): boolean {
|
|
869
919
|
return message.content.some(content => {
|
|
@@ -966,6 +1016,50 @@ interface EphemeralTurnResult {
|
|
|
966
1016
|
*/
|
|
967
1017
|
const SIGNAL_TEARDOWN_TIMEOUT_MS = 5_000;
|
|
968
1018
|
|
|
1019
|
+
const AGENT_BASH_ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256;
|
|
1020
|
+
|
|
1021
|
+
function boundAgentBashArtifactSaveDiagnostic(error: unknown): string {
|
|
1022
|
+
const message = (error instanceof Error ? error.message : String(error)).replace(/\s+/gu, " ").trim();
|
|
1023
|
+
const normalized = message || "unknown storage error";
|
|
1024
|
+
return truncateHeadBytes(normalized, AGENT_BASH_ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES).text;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
function summarizeAgentBashArtifactSave(
|
|
1028
|
+
artifactId: string,
|
|
1029
|
+
originalText: string,
|
|
1030
|
+
): Extract<BashArtifactSaveResult, { status: "saved" }> {
|
|
1031
|
+
const originalBytes = utf8ByteLength(originalText);
|
|
1032
|
+
if (originalBytes <= DEFAULT_ARTIFACT_MAX_BYTES) {
|
|
1033
|
+
return { status: "saved", artifactId, complete: true };
|
|
1034
|
+
}
|
|
1035
|
+
const retainedBytes = truncateHeadBytes(originalText, DEFAULT_ARTIFACT_MAX_BYTES).bytes;
|
|
1036
|
+
return {
|
|
1037
|
+
status: "saved",
|
|
1038
|
+
artifactId,
|
|
1039
|
+
complete: false,
|
|
1040
|
+
omittedBytes: originalBytes - retainedBytes,
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
export interface AgentBashArtifactStore {
|
|
1045
|
+
saveArtifact(content: string, toolType: string): Promise<string | undefined>;
|
|
1046
|
+
getArtifactPath(id: string): Promise<string | null>;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
export async function saveAgentBashOriginalArtifact(
|
|
1050
|
+
store: AgentBashArtifactStore,
|
|
1051
|
+
originalText: string,
|
|
1052
|
+
): Promise<BashArtifactSaveResult> {
|
|
1053
|
+
try {
|
|
1054
|
+
const artifactId = await store.saveArtifact(originalText, "bash-original");
|
|
1055
|
+
if (!artifactId) return { status: "failed", diagnostic: "storage returned no artifact id" };
|
|
1056
|
+
const artifactPath = await store.getArtifactPath(artifactId);
|
|
1057
|
+
return artifactPath ? summarizeAgentBashArtifactSave(artifactId, originalText) : { status: "unavailable" };
|
|
1058
|
+
} catch (error) {
|
|
1059
|
+
return { status: "failed", diagnostic: boundAgentBashArtifactSaveDiagnostic(error) };
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
969
1063
|
/**
|
|
970
1064
|
* Throttle window for the per-turn volatile workspace-tree scan. Date/cwd are
|
|
971
1065
|
* refreshed every turn; the mtime-sorted tree is rescanned + re-embedded at most
|
|
@@ -1698,6 +1792,7 @@ export class AgentSession {
|
|
|
1698
1792
|
#resetRetryReplaySafety(): void {
|
|
1699
1793
|
this.#retryReplayEpoch++;
|
|
1700
1794
|
this.#retryReplayUnsafeEpoch = undefined;
|
|
1795
|
+
this.#firstEventTimeoutRetryStartedAt = Date.now();
|
|
1701
1796
|
if (
|
|
1702
1797
|
this.#extensionRunner?.hasHandlers("context") ||
|
|
1703
1798
|
this.#extensionRunner?.hasHandlers("before_provider_request") ||
|
|
@@ -1731,6 +1826,7 @@ export class AgentSession {
|
|
|
1731
1826
|
// Retry state
|
|
1732
1827
|
#retryAbortController: AbortController | undefined = undefined;
|
|
1733
1828
|
#retryNowRequested = false;
|
|
1829
|
+
#firstEventTimeoutRetryStartedAt: number | undefined;
|
|
1734
1830
|
#retryAttempt = 0;
|
|
1735
1831
|
#retryPromise: Promise<void> | undefined = undefined;
|
|
1736
1832
|
#retryResolve: (() => void) | undefined = undefined;
|
|
@@ -1795,6 +1891,10 @@ export class AgentSession {
|
|
|
1795
1891
|
#turnIndex = 0;
|
|
1796
1892
|
#workerIntegrationScheduler: WorkerIntegrationRequestScheduler;
|
|
1797
1893
|
#workerIntegrationRequestedForTurn = false;
|
|
1894
|
+
/** Runtime-owned team worker heartbeat; `undefined` outside a `gjc team` worker pane. */
|
|
1895
|
+
#teamWorkerHeartbeat: GjcTeamWorkerHeartbeatReporter | undefined;
|
|
1896
|
+
#teamWorkerTurnsInFlight = 0;
|
|
1897
|
+
#unregisterTeamWorkerAsyncJobChange: (() => void) | undefined;
|
|
1798
1898
|
// First-party internal before-agent-start contributors (not user hooks).
|
|
1799
1899
|
#beforeAgentStartContributors: BeforeAgentStartContributor[] = [];
|
|
1800
1900
|
|
|
@@ -2149,6 +2249,8 @@ export class AgentSession {
|
|
|
2149
2249
|
this.#promptInFlightCount++;
|
|
2150
2250
|
if (this.#promptInFlightCount === 1) {
|
|
2151
2251
|
this.#acquirePowerAssertion();
|
|
2252
|
+
// Runtime-owned worker liveness follows Agent turns and background jobs
|
|
2253
|
+
// in #refreshTeamWorkerHeartbeat(), including internally dispatched turns.
|
|
2152
2254
|
}
|
|
2153
2255
|
}
|
|
2154
2256
|
|
|
@@ -2205,6 +2307,15 @@ export class AgentSession {
|
|
|
2205
2307
|
this.#flushPendingAgentEnd();
|
|
2206
2308
|
}
|
|
2207
2309
|
|
|
2310
|
+
#retainDeferredAgentEndAfterContinuationBusy(
|
|
2311
|
+
hold: symbol | undefined,
|
|
2312
|
+
pending: AgentSessionEvent | undefined,
|
|
2313
|
+
): void {
|
|
2314
|
+
if (!hold || !pending) return;
|
|
2315
|
+
this.#pendingAgentEndEmit = pending;
|
|
2316
|
+
this.#pendingAgentEndContinuationHolds.set(hold, pending);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2208
2319
|
#releaseDeferredAgentEndContinuation(hold: symbol | undefined): void {
|
|
2209
2320
|
if (!hold) return;
|
|
2210
2321
|
const pending = this.#pendingAgentEndContinuationHolds.get(hold);
|
|
@@ -2255,11 +2366,23 @@ export class AgentSession {
|
|
|
2255
2366
|
this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
|
|
2256
2367
|
if (this.#promptInFlightCount === 0) {
|
|
2257
2368
|
this.#releasePowerAssertion();
|
|
2369
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
2258
2370
|
this.#flushPendingBackgroundExchanges();
|
|
2259
2371
|
this.#flushPendingAgentEnd();
|
|
2260
2372
|
}
|
|
2261
2373
|
}
|
|
2262
2374
|
|
|
2375
|
+
#refreshTeamWorkerHeartbeat(): void {
|
|
2376
|
+
const hasOwnedBackgroundJob =
|
|
2377
|
+
this.#agentId !== undefined &&
|
|
2378
|
+
(this.#ownedAsyncJobManager?.getRunningJobs({ ownerId: this.#agentId }).length ?? 0) > 0;
|
|
2379
|
+
if (this.#teamWorkerTurnsInFlight > 0 || hasOwnedBackgroundJob) {
|
|
2380
|
+
this.#teamWorkerHeartbeat?.start();
|
|
2381
|
+
} else {
|
|
2382
|
+
this.#teamWorkerHeartbeat?.stop();
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2263
2386
|
#flushPendingAgentEnd(): void {
|
|
2264
2387
|
if (
|
|
2265
2388
|
this.#promptInFlightCount > 0 ||
|
|
@@ -2311,6 +2434,14 @@ export class AgentSession {
|
|
|
2311
2434
|
}),
|
|
2312
2435
|
config.workerIntegrationTimeoutMs,
|
|
2313
2436
|
);
|
|
2437
|
+
// One publisher per worker process: subagent sessions share this process and its
|
|
2438
|
+
// team env, so letting each create a reporter would run N timers writing the same
|
|
2439
|
+
// record. The top-level session owns the pane's liveness.
|
|
2440
|
+
this.#teamWorkerHeartbeat =
|
|
2441
|
+
config.teamWorkerHeartbeatReporter ??
|
|
2442
|
+
((config.taskDepth ?? 0) === 0
|
|
2443
|
+
? GjcTeamWorkerHeartbeatReporter.forProcess(() => this.sessionManager.getCwd())
|
|
2444
|
+
: undefined);
|
|
2314
2445
|
this.notificationSessionController = config.notificationSessionController;
|
|
2315
2446
|
this.taskDepth = config.taskDepth ?? 0;
|
|
2316
2447
|
// Register this session with the process-wide resource GC (idle/RSS browser-tab eviction
|
|
@@ -2472,6 +2603,9 @@ export class AgentSession {
|
|
|
2472
2603
|
this.#ttsrManager = config.ttsrManager;
|
|
2473
2604
|
this.#obfuscator = config.obfuscator;
|
|
2474
2605
|
this.#agentId = config.agentId;
|
|
2606
|
+
this.#unregisterTeamWorkerAsyncJobChange = this.#ownedAsyncJobManager?.onChange(() => {
|
|
2607
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
2608
|
+
});
|
|
2475
2609
|
this.#agentRegistry = config.agentRegistry;
|
|
2476
2610
|
this.#providerSessionId = config.providerSessionId;
|
|
2477
2611
|
this.#providerCacheSessionId = config.providerCacheSessionId;
|
|
@@ -2566,7 +2700,24 @@ export class AgentSession {
|
|
|
2566
2700
|
|
|
2567
2701
|
/** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */
|
|
2568
2702
|
nextToolChoice(): ToolChoice | undefined {
|
|
2569
|
-
|
|
2703
|
+
const choice = this.#toolChoiceQueue.nextToolChoice();
|
|
2704
|
+
if (!choice || typeof choice === "string") return choice;
|
|
2705
|
+
|
|
2706
|
+
const toolName = choice.type === "tool" ? choice.name : "function" in choice ? choice.function.name : choice.name;
|
|
2707
|
+
const activeTool = this.agent.state.tools.find(
|
|
2708
|
+
tool => tool.name === toolName || tool.customWireName === toolName,
|
|
2709
|
+
);
|
|
2710
|
+
if (!activeTool) {
|
|
2711
|
+
this.#toolChoiceQueue.degradeInFlight(`Tool "${toolName}" is no longer active.`);
|
|
2712
|
+
return undefined;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
const refreshed = buildNamedToolChoiceResult(activeTool.name, this.model);
|
|
2716
|
+
if (!refreshed.exactNamed || !refreshed.choice) {
|
|
2717
|
+
this.#toolChoiceQueue.degradeInFlight(refreshed.resolved?.reason ?? "Named tool choice is unavailable.");
|
|
2718
|
+
return undefined;
|
|
2719
|
+
}
|
|
2720
|
+
return refreshed.choice;
|
|
2570
2721
|
}
|
|
2571
2722
|
|
|
2572
2723
|
/**
|
|
@@ -3196,19 +3347,26 @@ export class AgentSession {
|
|
|
3196
3347
|
return queued;
|
|
3197
3348
|
}
|
|
3198
3349
|
|
|
3199
|
-
#trackAgentEvent =
|
|
3350
|
+
#trackAgentEvent = (event: AgentEvent): Promise<void> => {
|
|
3351
|
+
const activePromptHandle = this.activePromptHandle;
|
|
3200
3352
|
const agentEndHandled = event.type === "agent_end" ? Promise.withResolvers<void>() : undefined;
|
|
3201
3353
|
if (agentEndHandled) this.#agentEndHandlingPromise = agentEndHandled.promise;
|
|
3202
3354
|
this.#agentEventHandlersInFlight++;
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3355
|
+
const handler = (async (): Promise<void> => {
|
|
3356
|
+
try {
|
|
3357
|
+
await this.#handleAgentEvent(event, activePromptHandle);
|
|
3358
|
+
} catch (error) {
|
|
3359
|
+
logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
|
|
3360
|
+
} finally {
|
|
3361
|
+
this.#agentEventHandlersInFlight = Math.max(0, this.#agentEventHandlersInFlight - 1);
|
|
3362
|
+
this.#flushPendingAgentEnd();
|
|
3363
|
+
agentEndHandled?.resolve();
|
|
3364
|
+
}
|
|
3365
|
+
})();
|
|
3366
|
+
if (activePromptHandle) {
|
|
3367
|
+
this.agent.resourceLedger.track(activePromptHandle, "post_prompt", "agent-session-event", handler);
|
|
3211
3368
|
}
|
|
3369
|
+
return handler;
|
|
3212
3370
|
};
|
|
3213
3371
|
|
|
3214
3372
|
#persistRuntimeStateInBackground(event: AgentSessionEvent): void {
|
|
@@ -3228,6 +3386,18 @@ export class AgentSession {
|
|
|
3228
3386
|
} else if (event.type === "turn_end") {
|
|
3229
3387
|
this.#closedExtensionTurnGeneration = this.#extensionTurnGeneration;
|
|
3230
3388
|
}
|
|
3389
|
+
if (event.type === "turn_start") {
|
|
3390
|
+
this.#teamWorkerTurnsInFlight++;
|
|
3391
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
3392
|
+
} else if (event.type === "turn_end") {
|
|
3393
|
+
this.#teamWorkerTurnsInFlight = Math.max(0, this.#teamWorkerTurnsInFlight - 1);
|
|
3394
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
3395
|
+
} else if (event.type === "agent_end") {
|
|
3396
|
+
// Failed Agent runs can emit agent_end without turn_end. Reset rather than
|
|
3397
|
+
// leaking a positive count that would report an idle/failed worker forever.
|
|
3398
|
+
this.#teamWorkerTurnsInFlight = 0;
|
|
3399
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
3400
|
+
}
|
|
3231
3401
|
if (event.type === "message_update") {
|
|
3232
3402
|
// Fast path: message_update maps to no sidecar state, so we must not
|
|
3233
3403
|
// build the persistRuntimeState closure here (per-token hot path).
|
|
@@ -3347,7 +3517,7 @@ export class AgentSession {
|
|
|
3347
3517
|
}
|
|
3348
3518
|
|
|
3349
3519
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
3350
|
-
#handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
3520
|
+
#handleAgentEvent = async (event: AgentEvent, activePromptHandle?: string): Promise<void> => {
|
|
3351
3521
|
if (this.#extensionRunner?.hasHandlers(event.type)) this.#markRetryReplayUnsafe();
|
|
3352
3522
|
if (
|
|
3353
3523
|
event.type === "tool_execution_start" ||
|
|
@@ -3696,9 +3866,10 @@ export class AgentSession {
|
|
|
3696
3866
|
this.#ttsrAbortPending = false;
|
|
3697
3867
|
this.#resolveTtsrResume();
|
|
3698
3868
|
},
|
|
3869
|
+
resourceRunId: activePromptHandle,
|
|
3699
3870
|
});
|
|
3700
3871
|
},
|
|
3701
|
-
{ delayMs: 50 },
|
|
3872
|
+
{ delayMs: 50, resourceRunId: activePromptHandle },
|
|
3702
3873
|
);
|
|
3703
3874
|
return;
|
|
3704
3875
|
}
|
|
@@ -3857,7 +4028,11 @@ export class AgentSession {
|
|
|
3857
4028
|
maintenanceGeneration !== undefined &&
|
|
3858
4029
|
this.#promptGeneration === maintenanceGeneration
|
|
3859
4030
|
) {
|
|
3860
|
-
this.#scheduleAgentContinue({
|
|
4031
|
+
this.#scheduleAgentContinue({
|
|
4032
|
+
generation: maintenanceGeneration,
|
|
4033
|
+
skipCompactionCheck: true,
|
|
4034
|
+
resourceRunId: activePromptHandle,
|
|
4035
|
+
});
|
|
3861
4036
|
}
|
|
3862
4037
|
return;
|
|
3863
4038
|
}
|
|
@@ -3937,8 +4112,11 @@ export class AgentSession {
|
|
|
3937
4112
|
}
|
|
3938
4113
|
this.#resolveRetry();
|
|
3939
4114
|
|
|
3940
|
-
const compactionTask = this.#checkCompaction(msg);
|
|
3941
|
-
this.#trackPostPromptTask(
|
|
4115
|
+
const compactionTask = this.#checkCompaction(msg, true, undefined, activePromptHandle);
|
|
4116
|
+
this.#trackPostPromptTask(
|
|
4117
|
+
compactionTask.then(() => undefined),
|
|
4118
|
+
activePromptHandle,
|
|
4119
|
+
);
|
|
3942
4120
|
await compactionTask;
|
|
3943
4121
|
// Check for incomplete todos only after a final assistant stop, not intermediate tool-use turns.
|
|
3944
4122
|
const hasToolCalls = msg.content.some(content => content.type === "toolCall");
|
|
@@ -4002,9 +4180,12 @@ export class AgentSession {
|
|
|
4002
4180
|
this.#postPromptTasksPromise = undefined;
|
|
4003
4181
|
}
|
|
4004
4182
|
|
|
4005
|
-
#trackPostPromptTask(task: Promise<void
|
|
4183
|
+
#trackPostPromptTask(task: Promise<void>, resourceRunId = this.activePromptHandle): void {
|
|
4006
4184
|
this.#postPromptTasks.add(task);
|
|
4007
4185
|
this.#ensurePostPromptTasksPromise();
|
|
4186
|
+
if (resourceRunId) {
|
|
4187
|
+
this.agent.resourceLedger.track(resourceRunId, "post_prompt", "agent-session", task);
|
|
4188
|
+
}
|
|
4008
4189
|
void task
|
|
4009
4190
|
.catch(() => {})
|
|
4010
4191
|
.finally(() => {
|
|
@@ -4017,7 +4198,7 @@ export class AgentSession {
|
|
|
4017
4198
|
|
|
4018
4199
|
#schedulePostPromptTask(
|
|
4019
4200
|
task: (signal: AbortSignal) => Promise<void>,
|
|
4020
|
-
options?: { delayMs?: number; generation?: number; onSkip?: () => void },
|
|
4201
|
+
options?: { delayMs?: number; generation?: number; onSkip?: () => void; resourceRunId?: string },
|
|
4021
4202
|
): Promise<void> {
|
|
4022
4203
|
const delayMs = options?.delayMs ?? 0;
|
|
4023
4204
|
const signal = this.#postPromptTasksAbortController.signal;
|
|
@@ -4040,10 +4221,14 @@ export class AgentSession {
|
|
|
4040
4221
|
}
|
|
4041
4222
|
await task(signal);
|
|
4042
4223
|
})();
|
|
4043
|
-
this.#trackPostPromptTask(scheduled);
|
|
4224
|
+
this.#trackPostPromptTask(scheduled, options?.resourceRunId);
|
|
4044
4225
|
return scheduled;
|
|
4045
4226
|
}
|
|
4046
4227
|
|
|
4228
|
+
#isAgentBusyError(error: unknown): error is AgentBusyError {
|
|
4229
|
+
return error instanceof AgentBusyError;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
4047
4232
|
#scheduleAgentContinue(options?: {
|
|
4048
4233
|
delayMs?: number;
|
|
4049
4234
|
generation?: number;
|
|
@@ -4052,7 +4237,9 @@ export class AgentSession {
|
|
|
4052
4237
|
shouldContinue?: () => boolean;
|
|
4053
4238
|
onSkip?: (reason: "generation_changed" | "aborted_signal" | "queue_drained" | "handoff_in_progress") => void;
|
|
4054
4239
|
allowDuringCancelAndSubmit?: boolean;
|
|
4240
|
+
rescheduleOnBusy?: boolean;
|
|
4055
4241
|
onError?: (error: unknown) => void;
|
|
4242
|
+
resourceRunId?: string;
|
|
4056
4243
|
}): Promise<void> {
|
|
4057
4244
|
const predecessorAgentEndHold = options?.suppressPredecessorAgentEnd
|
|
4058
4245
|
? this.#reserveDeferredAgentEndForContinuation()
|
|
@@ -4075,81 +4262,92 @@ export class AgentSession {
|
|
|
4075
4262
|
};
|
|
4076
4263
|
const scheduledGeneration = options?.generation;
|
|
4077
4264
|
const signal = this.#postPromptTasksAbortController.signal;
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
4088
|
-
|
|
4089
|
-
|
|
4090
|
-
|
|
4091
|
-
|
|
4092
|
-
|
|
4093
|
-
|
|
4094
|
-
|
|
4095
|
-
|
|
4096
|
-
|
|
4097
|
-
|
|
4098
|
-
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
if (!options?.skipCompactionCheck) {
|
|
4102
|
-
await this.#checkEstimatedContextBeforePrompt();
|
|
4103
|
-
if (!canContinue()) return;
|
|
4104
|
-
}
|
|
4105
|
-
if (signal.aborted) {
|
|
4106
|
-
skip("aborted_signal");
|
|
4107
|
-
return;
|
|
4108
|
-
}
|
|
4109
|
-
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
4110
|
-
skip("generation_changed");
|
|
4111
|
-
return;
|
|
4112
|
-
}
|
|
4113
|
-
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
4114
|
-
skip("queue_drained");
|
|
4115
|
-
return;
|
|
4116
|
-
}
|
|
4117
|
-
// A continuation scheduled before a handoff engaged must not start a
|
|
4118
|
-
// turn against the session being handed off (or the restored
|
|
4119
|
-
// predecessor). rearmIdle / normal delivery resumes after the fence.
|
|
4120
|
-
if (this.#handoffTransitionActive) {
|
|
4121
|
-
skip("handoff_in_progress");
|
|
4122
|
-
return;
|
|
4123
|
-
}
|
|
4124
|
-
const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
|
|
4125
|
-
const startsQueuedSuccessor =
|
|
4126
|
-
this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
|
|
4265
|
+
const scheduleAttempt = (delayMs = options?.delayMs): Promise<void> =>
|
|
4266
|
+
this.#schedulePostPromptTask(
|
|
4267
|
+
async () => {
|
|
4268
|
+
const canContinue = (): boolean => {
|
|
4269
|
+
if (signal.aborted || this.#isDisposed) {
|
|
4270
|
+
skip("aborted_signal");
|
|
4271
|
+
return false;
|
|
4272
|
+
}
|
|
4273
|
+
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
4274
|
+
skip("generation_changed");
|
|
4275
|
+
return false;
|
|
4276
|
+
}
|
|
4277
|
+
if (this.#cancelAndSubmitInProgress && !options?.allowDuringCancelAndSubmit) {
|
|
4278
|
+
skip("queue_drained");
|
|
4279
|
+
return false;
|
|
4280
|
+
}
|
|
4281
|
+
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
4282
|
+
skip("queue_drained");
|
|
4283
|
+
return false;
|
|
4284
|
+
}
|
|
4285
|
+
return true;
|
|
4286
|
+
};
|
|
4287
|
+
if (!canContinue()) return;
|
|
4127
4288
|
try {
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
}
|
|
4289
|
+
if (!options?.skipCompactionCheck) {
|
|
4290
|
+
await this.#checkEstimatedContextBeforePrompt();
|
|
4291
|
+
if (!canContinue()) return;
|
|
4292
|
+
}
|
|
4293
|
+
if (signal.aborted) {
|
|
4294
|
+
skip("aborted_signal");
|
|
4295
|
+
return;
|
|
4296
|
+
}
|
|
4297
|
+
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
4298
|
+
skip("generation_changed");
|
|
4299
|
+
return;
|
|
4300
|
+
}
|
|
4301
|
+
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
4302
|
+
skip("queue_drained");
|
|
4303
|
+
return;
|
|
4304
|
+
}
|
|
4305
|
+
// A continuation scheduled before a handoff engaged must not start a
|
|
4306
|
+
// turn against the session being handed off (or the restored
|
|
4307
|
+
// predecessor). rearmIdle / normal delivery resumes after the fence.
|
|
4308
|
+
if (this.#handoffTransitionActive) {
|
|
4309
|
+
skip("handoff_in_progress");
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
|
|
4313
|
+
const startsQueuedSuccessor =
|
|
4314
|
+
this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
|
|
4315
|
+
try {
|
|
4316
|
+
await this.agent.continue({
|
|
4317
|
+
...this.#managedFallbackPromptOptions(),
|
|
4318
|
+
// Reset only after continue() has claimed the queued turn. Skipped or stale
|
|
4319
|
+
// continuations retain predecessor accounting, and resetAttemptBudget keeps
|
|
4320
|
+
// the sticky fallback cursor unchanged.
|
|
4321
|
+
onRunAccepted: startsQueuedSuccessor
|
|
4322
|
+
? () => {
|
|
4323
|
+
this.#defaultFallbackChain().resetAttemptBudget();
|
|
4324
|
+
this.#overflowMaintenanceAttempts = 0;
|
|
4325
|
+
}
|
|
4326
|
+
: undefined,
|
|
4327
|
+
});
|
|
4328
|
+
} catch (error) {
|
|
4329
|
+
if (options?.rescheduleOnBusy && this.#isAgentBusyError(error) && !terminalized && canContinue()) {
|
|
4330
|
+
this.#retainDeferredAgentEndAfterContinuationBusy(predecessorAgentEndHold, predecessorAgentEnd);
|
|
4331
|
+
logger.debug("agent.continue busy after scheduling; rescheduling", {
|
|
4332
|
+
error: error.message,
|
|
4333
|
+
});
|
|
4334
|
+
void scheduleAttempt(100);
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
4337
|
+
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
4338
|
+
throw error;
|
|
4339
|
+
}
|
|
4140
4340
|
} catch (error) {
|
|
4141
|
-
|
|
4142
|
-
throw error;
|
|
4341
|
+
fail(error);
|
|
4143
4342
|
}
|
|
4144
|
-
}
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
);
|
|
4343
|
+
},
|
|
4344
|
+
{
|
|
4345
|
+
delayMs,
|
|
4346
|
+
onSkip: () => skip("aborted_signal"),
|
|
4347
|
+
resourceRunId: options?.resourceRunId,
|
|
4348
|
+
},
|
|
4349
|
+
);
|
|
4350
|
+
return scheduleAttempt();
|
|
4153
4351
|
}
|
|
4154
4352
|
|
|
4155
4353
|
#logCompactionContinuationSkipped(
|
|
@@ -4165,7 +4363,7 @@ export class AgentSession {
|
|
|
4165
4363
|
): void {
|
|
4166
4364
|
logger.warn("Auto-compaction continuation failed", {
|
|
4167
4365
|
source,
|
|
4168
|
-
reason:
|
|
4366
|
+
reason: "not_resumable_tail",
|
|
4169
4367
|
error: error instanceof Error ? error.message : String(error),
|
|
4170
4368
|
});
|
|
4171
4369
|
}
|
|
@@ -4193,14 +4391,24 @@ export class AgentSession {
|
|
|
4193
4391
|
return compactionSettings.autoContinue === false ? "auto_continue_disabled_non_resumable_tail" : undefined;
|
|
4194
4392
|
}
|
|
4195
4393
|
|
|
4196
|
-
#scheduleOverflowRetryContinuation(generation: number): boolean {
|
|
4394
|
+
async #scheduleOverflowRetryContinuation(generation: number, resourceRunId?: string): Promise<boolean> {
|
|
4197
4395
|
this.#stripOverflowFailedTurnForRetry();
|
|
4396
|
+
const snapshot = await this.#compactionStateSnapshot();
|
|
4397
|
+
if (
|
|
4398
|
+
snapshot.goal?.status === "paused" &&
|
|
4399
|
+
!snapshot.queuedMessages &&
|
|
4400
|
+
snapshot.lastAssistantStopReason !== "length"
|
|
4401
|
+
) {
|
|
4402
|
+
this.#logCompactionContinuationSkipped("overflow_retry", "paused_goal");
|
|
4403
|
+
return false;
|
|
4404
|
+
}
|
|
4198
4405
|
if (this.#isResumableAgentTail()) {
|
|
4199
4406
|
this.#scheduleAgentContinue({
|
|
4200
4407
|
delayMs: 100,
|
|
4201
4408
|
generation,
|
|
4202
4409
|
suppressPredecessorAgentEnd: true,
|
|
4203
|
-
|
|
4410
|
+
resourceRunId,
|
|
4411
|
+
rescheduleOnBusy: true,
|
|
4204
4412
|
onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
|
|
4205
4413
|
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
4206
4414
|
});
|
|
@@ -4209,7 +4417,7 @@ export class AgentSession {
|
|
|
4209
4417
|
|
|
4210
4418
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
4211
4419
|
if (compactionSettings.autoContinue !== false) {
|
|
4212
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
4420
|
+
this.#scheduleAutoContinuePrompt(generation, false);
|
|
4213
4421
|
return true;
|
|
4214
4422
|
}
|
|
4215
4423
|
|
|
@@ -4217,8 +4425,32 @@ export class AgentSession {
|
|
|
4217
4425
|
return false;
|
|
4218
4426
|
}
|
|
4219
4427
|
|
|
4220
|
-
#scheduleAutoContinuePrompt(generation: number): void {
|
|
4428
|
+
#scheduleAutoContinuePrompt(generation: number, requireUnfinishedWork = true): void {
|
|
4221
4429
|
const predecessorAgentEndHold = this.#reserveDeferredAgentEndForContinuation();
|
|
4430
|
+
const scheduledGeneration = generation;
|
|
4431
|
+
const signal = this.#postPromptTasksAbortController.signal;
|
|
4432
|
+
const continuationAuthorized = async (hasPendingNextTurnMessages = false): Promise<boolean> => {
|
|
4433
|
+
const snapshot = await this.#compactionStateSnapshot();
|
|
4434
|
+
if (signal.aborted) {
|
|
4435
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
4436
|
+
return false;
|
|
4437
|
+
}
|
|
4438
|
+
if (this.#isDisposed || this.#promptGeneration !== scheduledGeneration) {
|
|
4439
|
+
this.#logCompactionContinuationSkipped(
|
|
4440
|
+
"auto_continue_prompt",
|
|
4441
|
+
this.#isDisposed ? "session_disposed" : "generation_changed",
|
|
4442
|
+
);
|
|
4443
|
+
return false;
|
|
4444
|
+
}
|
|
4445
|
+
const authorized = requireUnfinishedWork
|
|
4446
|
+
? this.#hasUnfinishedWork(snapshot) || hasPendingNextTurnMessages
|
|
4447
|
+
: snapshot.queuedMessages ||
|
|
4448
|
+
hasPendingNextTurnMessages ||
|
|
4449
|
+
snapshot.lastAssistantStopReason === "length" ||
|
|
4450
|
+
snapshot.goal?.status !== "paused";
|
|
4451
|
+
if (!authorized) this.emitNotice("info", "Auto-continue skipped: no unfinished work detected");
|
|
4452
|
+
return authorized;
|
|
4453
|
+
};
|
|
4222
4454
|
const continuePrompt = async () => {
|
|
4223
4455
|
await this.#promptWithMessage(
|
|
4224
4456
|
{
|
|
@@ -4232,14 +4464,15 @@ export class AgentSession {
|
|
|
4232
4464
|
skipPostPromptRecoveryWait: true,
|
|
4233
4465
|
skipCompactionCheck: true,
|
|
4234
4466
|
predecessorAgentEndHold,
|
|
4467
|
+
onFinalPreflight: ({ hasPendingNextTurnMessages }) => continuationAuthorized(hasPendingNextTurnMessages),
|
|
4235
4468
|
},
|
|
4236
4469
|
);
|
|
4237
4470
|
};
|
|
4238
|
-
const
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
(async () => {
|
|
4471
|
+
const scheduleAttempt = (delayMs = 0) => {
|
|
4472
|
+
const attempt = (async () => {
|
|
4473
|
+
let rescheduled = false;
|
|
4242
4474
|
try {
|
|
4475
|
+
if (delayMs > 0) await scheduler.wait(delayMs, { signal });
|
|
4243
4476
|
await Promise.resolve();
|
|
4244
4477
|
if (signal.aborted) {
|
|
4245
4478
|
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
@@ -4249,14 +4482,31 @@ export class AgentSession {
|
|
|
4249
4482
|
this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
|
|
4250
4483
|
return;
|
|
4251
4484
|
}
|
|
4485
|
+
if (!(await continuationAuthorized())) return;
|
|
4252
4486
|
await continuePrompt();
|
|
4253
4487
|
} catch (error) {
|
|
4488
|
+
if (signal.aborted) {
|
|
4489
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
4490
|
+
return;
|
|
4491
|
+
}
|
|
4492
|
+
if (this.#isAgentBusyError(error) && this.#promptGeneration === scheduledGeneration) {
|
|
4493
|
+
logger.debug("Auto-compaction continuation busy; rescheduling", {
|
|
4494
|
+
source: "auto_continue_prompt",
|
|
4495
|
+
reason: "queue_drained",
|
|
4496
|
+
error: error.message,
|
|
4497
|
+
});
|
|
4498
|
+
rescheduled = true;
|
|
4499
|
+
scheduleAttempt(100);
|
|
4500
|
+
return;
|
|
4501
|
+
}
|
|
4254
4502
|
this.#logCompactionContinuationError("auto_continue_prompt", error);
|
|
4255
4503
|
} finally {
|
|
4256
|
-
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
4504
|
+
if (!rescheduled) this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
4257
4505
|
}
|
|
4258
|
-
})()
|
|
4259
|
-
|
|
4506
|
+
})();
|
|
4507
|
+
this.#trackPostPromptTask(attempt);
|
|
4508
|
+
};
|
|
4509
|
+
scheduleAttempt();
|
|
4260
4510
|
}
|
|
4261
4511
|
|
|
4262
4512
|
async #cancelPostPromptTasks(): Promise<void> {
|
|
@@ -4749,6 +4999,19 @@ export class AgentSession {
|
|
|
4749
4999
|
};
|
|
4750
5000
|
}
|
|
4751
5001
|
|
|
5002
|
+
/** Defer local:// provisioning when a read-only session load cannot create its artifact root. */
|
|
5003
|
+
async #initializeLocalRootForLoadedSession(): Promise<void> {
|
|
5004
|
+
try {
|
|
5005
|
+
await initializeLocalRoot(this.#localProtocolOptions());
|
|
5006
|
+
} catch (error) {
|
|
5007
|
+
if (!isEacces(error) && !hasFsCode(error, "EPERM") && !hasFsCode(error, "EROFS")) throw error;
|
|
5008
|
+
logger.debug("Deferred local root initialization for read-only session load", {
|
|
5009
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
5010
|
+
error: String(error),
|
|
5011
|
+
});
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
|
|
4752
5015
|
#maybeAbortStreamingEdit(event: AgentEvent, generation: number): void {
|
|
4753
5016
|
if (!this.settings.get("edit.streamingAbort")) return;
|
|
4754
5017
|
if (this.#streamingEditAbortTriggered) return;
|
|
@@ -5334,6 +5597,9 @@ export class AgentSession {
|
|
|
5334
5597
|
await disposeKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
5335
5598
|
await disposeVmContextsByOwner(this.#evalKernelOwnerId);
|
|
5336
5599
|
this.#releasePowerAssertion();
|
|
5600
|
+
this.#unregisterTeamWorkerAsyncJobChange?.();
|
|
5601
|
+
this.#unregisterTeamWorkerAsyncJobChange = undefined;
|
|
5602
|
+
this.#teamWorkerHeartbeat?.dispose();
|
|
5337
5603
|
// Disconnect the agent event listener BEFORE closing session resources so a late
|
|
5338
5604
|
// provider/tool message_end cannot append to the closing SessionManager.
|
|
5339
5605
|
this.#disconnectFromAgent();
|
|
@@ -5378,6 +5644,9 @@ export class AgentSession {
|
|
|
5378
5644
|
const kernelOwnerId = this.#evalKernelOwnerId;
|
|
5379
5645
|
this.#unregisterResourceGc?.();
|
|
5380
5646
|
this.#unregisterResourceGc = undefined;
|
|
5647
|
+
this.#unregisterTeamWorkerAsyncJobChange?.();
|
|
5648
|
+
this.#unregisterTeamWorkerAsyncJobChange = undefined;
|
|
5649
|
+
this.#teamWorkerHeartbeat?.dispose();
|
|
5381
5650
|
const work = Promise.allSettled([
|
|
5382
5651
|
// kill:true so a forced exit also reaps spawned-app Chrome we own (headless
|
|
5383
5652
|
// always closes; connected/attached browsers only disconnect — never killed).
|
|
@@ -6517,6 +6786,11 @@ export class AgentSession {
|
|
|
6517
6786
|
return this.#postPromptTasks.size > 0;
|
|
6518
6787
|
}
|
|
6519
6788
|
|
|
6789
|
+
/** Stable resource ownership identifier for the active prompt run. */
|
|
6790
|
+
get activePromptHandle(): string | undefined {
|
|
6791
|
+
return this.agent.activeResourceRunId;
|
|
6792
|
+
}
|
|
6793
|
+
|
|
6520
6794
|
/** All messages including custom types like BashExecutionMessage */
|
|
6521
6795
|
get messages(): AgentMessage[] {
|
|
6522
6796
|
return this.agent.state.messages;
|
|
@@ -7768,6 +8042,7 @@ export class AgentSession {
|
|
|
7768
8042
|
predecessorAgentEndHold?: symbol;
|
|
7769
8043
|
admissionLease?: SessionAdmissionLease;
|
|
7770
8044
|
onRunAccepted?: () => void;
|
|
8045
|
+
onFinalPreflight?: (context: { hasPendingNextTurnMessages: boolean }) => Promise<boolean>;
|
|
7771
8046
|
resetRetryReplaySafety?: boolean;
|
|
7772
8047
|
},
|
|
7773
8048
|
): Promise<void> {
|
|
@@ -7783,6 +8058,8 @@ export class AgentSession {
|
|
|
7783
8058
|
const generation = this.#promptGeneration;
|
|
7784
8059
|
const preflightSignal = this.#promptPreflightAbortController.signal;
|
|
7785
8060
|
const rosterClaim = this.#claimIrcRosterCandidate();
|
|
8061
|
+
let hasPendingNextTurnMessages = false;
|
|
8062
|
+
let pendingNextTurnMessageCount = 0;
|
|
7786
8063
|
let hindsightRecall: string | undefined;
|
|
7787
8064
|
try {
|
|
7788
8065
|
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
@@ -7874,11 +8151,11 @@ export class AgentSession {
|
|
|
7874
8151
|
}
|
|
7875
8152
|
|
|
7876
8153
|
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
7877
|
-
|
|
8154
|
+
pendingNextTurnMessageCount = this.#pendingNextTurnMessages.length;
|
|
8155
|
+
hasPendingNextTurnMessages = pendingNextTurnMessageCount > 0;
|
|
8156
|
+
for (const msg of this.#pendingNextTurnMessages.slice(0, pendingNextTurnMessageCount)) {
|
|
7878
8157
|
messages.push(msg);
|
|
7879
8158
|
}
|
|
7880
|
-
this.#pendingNextTurnMessages = [];
|
|
7881
|
-
if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
|
|
7882
8159
|
|
|
7883
8160
|
// Auto-read @filepath mentions
|
|
7884
8161
|
const fileMentions = extractFileMentions(expandedText);
|
|
@@ -7998,7 +8275,15 @@ export class AgentSession {
|
|
|
7998
8275
|
};
|
|
7999
8276
|
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
8000
8277
|
else options?.onPreflightAccepted?.();
|
|
8278
|
+
if (options?.onFinalPreflight && !(await options.onFinalPreflight({ hasPendingNextTurnMessages }))) {
|
|
8279
|
+
this.#resetInjectedContextSignatures();
|
|
8280
|
+
return;
|
|
8281
|
+
}
|
|
8001
8282
|
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
8283
|
+
if (pendingNextTurnMessageCount > 0) {
|
|
8284
|
+
this.#pendingNextTurnMessages.splice(0, pendingNextTurnMessageCount);
|
|
8285
|
+
}
|
|
8286
|
+
if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
|
|
8002
8287
|
await this.#promptAgentWithIdleRetry(messages, agentPromptOptions, predecessorAgentEndHold);
|
|
8003
8288
|
const terminalAssistant = this.#findLastAssistantMessage();
|
|
8004
8289
|
if (
|
|
@@ -8039,6 +8324,7 @@ export class AgentSession {
|
|
|
8039
8324
|
await this.#agentEndPublicationPromise;
|
|
8040
8325
|
} else {
|
|
8041
8326
|
await this.#agentEndHandlingPromise;
|
|
8327
|
+
await this.#waitForPostPromptRecovery();
|
|
8042
8328
|
await this.#agentEndPublicationPromise;
|
|
8043
8329
|
}
|
|
8044
8330
|
}
|
|
@@ -8087,6 +8373,7 @@ export class AgentSession {
|
|
|
8087
8373
|
sessionManager: createReadonlySessionManager(this.sessionManager),
|
|
8088
8374
|
modelRegistry: this.#modelRegistry,
|
|
8089
8375
|
model: this.model ?? undefined,
|
|
8376
|
+
getActivePromptHandle: () => this.activePromptHandle,
|
|
8090
8377
|
isIdle: () => !this.isStreaming,
|
|
8091
8378
|
abort: () => {
|
|
8092
8379
|
void this.abort();
|
|
@@ -8862,6 +9149,103 @@ export class AgentSession {
|
|
|
8862
9149
|
this.setTodoPhases(phases.filter(p => p.tasks.length > 0));
|
|
8863
9150
|
}
|
|
8864
9151
|
|
|
9152
|
+
async #compactionStateSnapshot(): Promise<CompactionStateSnapshot> {
|
|
9153
|
+
const snapshot: CompactionStateSnapshot = {
|
|
9154
|
+
goal: undefined,
|
|
9155
|
+
openTodos: [],
|
|
9156
|
+
activeSkills: [],
|
|
9157
|
+
queuedMessages: false,
|
|
9158
|
+
lastAssistantStopReason: undefined,
|
|
9159
|
+
};
|
|
9160
|
+
try {
|
|
9161
|
+
const goalState = this.getGoalModeState();
|
|
9162
|
+
const goal = goalState?.goal;
|
|
9163
|
+
if (goal) snapshot.goal = { objective: goal.objective, status: goal.status, enabled: goalState.enabled };
|
|
9164
|
+
} catch (error) {
|
|
9165
|
+
logger.warn("Failed to read goal state for compaction snapshot", {
|
|
9166
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9167
|
+
});
|
|
9168
|
+
}
|
|
9169
|
+
try {
|
|
9170
|
+
const todos = this.getTodoPhases()
|
|
9171
|
+
.flatMap(phase => phase.tasks)
|
|
9172
|
+
.filter(task => task.status === "pending" || task.status === "in_progress")
|
|
9173
|
+
.slice(0, 10)
|
|
9174
|
+
.map(task => task.content);
|
|
9175
|
+
snapshot.openTodos = todos;
|
|
9176
|
+
} catch (error) {
|
|
9177
|
+
logger.warn("Failed to read todo state for compaction snapshot", {
|
|
9178
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9179
|
+
});
|
|
9180
|
+
}
|
|
9181
|
+
try {
|
|
9182
|
+
const state = await readVisibleSkillActiveState(this.sessionManager.getCwd(), this.sessionId, {
|
|
9183
|
+
bypassCache: true,
|
|
9184
|
+
});
|
|
9185
|
+
snapshot.activeSkills = (state?.active_skills ?? [])
|
|
9186
|
+
.filter(entry => entry.active !== false)
|
|
9187
|
+
.slice(0, 5)
|
|
9188
|
+
.map(entry => ({ skill: entry.skill, phase: entry.phase ?? "unknown" }));
|
|
9189
|
+
} catch (error) {
|
|
9190
|
+
logger.warn("Failed to read workflow state for compaction snapshot", {
|
|
9191
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9192
|
+
});
|
|
9193
|
+
}
|
|
9194
|
+
try {
|
|
9195
|
+
snapshot.queuedMessages = this.agent.hasQueuedMessages() || this.#pendingNextTurnMessages.length > 0;
|
|
9196
|
+
} catch (error) {
|
|
9197
|
+
logger.warn("Failed to read queued-message state for compaction snapshot", {
|
|
9198
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9199
|
+
});
|
|
9200
|
+
}
|
|
9201
|
+
try {
|
|
9202
|
+
for (let index = this.messages.length - 1; index >= 0; index--) {
|
|
9203
|
+
const message = this.messages[index];
|
|
9204
|
+
if (message.role !== "assistant") continue;
|
|
9205
|
+
snapshot.lastAssistantStopReason = (message as AssistantMessage).stopReason;
|
|
9206
|
+
break;
|
|
9207
|
+
}
|
|
9208
|
+
} catch (error) {
|
|
9209
|
+
logger.warn("Failed to read assistant stop state for compaction snapshot", {
|
|
9210
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9211
|
+
});
|
|
9212
|
+
}
|
|
9213
|
+
return snapshot;
|
|
9214
|
+
}
|
|
9215
|
+
|
|
9216
|
+
#compactionStateContext(snapshot: CompactionStateSnapshot): string[] {
|
|
9217
|
+
const context: string[] = [];
|
|
9218
|
+
const goal = snapshot.goal;
|
|
9219
|
+
if (goal?.enabled) {
|
|
9220
|
+
context.push(
|
|
9221
|
+
`Active goal: ${sanitizeCompactionStateText(goal.objective, 160)} (status: ${sanitizeCompactionStateText(goal.status, 40)})`,
|
|
9222
|
+
);
|
|
9223
|
+
} else if (goal?.status === "paused") {
|
|
9224
|
+
context.push(`Paused goal: ${sanitizeCompactionStateText(goal.objective, 160)} (status: paused)`);
|
|
9225
|
+
}
|
|
9226
|
+
for (const entry of snapshot.activeSkills) {
|
|
9227
|
+
const skill = sanitizeCompactionStateText(entry.skill, 100);
|
|
9228
|
+
const phase = sanitizeCompactionStateText(entry.phase, 80);
|
|
9229
|
+
if (skill) context.push(`Active skill: ${skill} phase=${phase}`);
|
|
9230
|
+
}
|
|
9231
|
+
if (snapshot.openTodos.length > 0) {
|
|
9232
|
+
const todos = snapshot.openTodos.map(todo => sanitizeCompactionStateText(todo, 120));
|
|
9233
|
+
context.push(`Open todos: ${todos.join("; ")}`);
|
|
9234
|
+
}
|
|
9235
|
+
return context;
|
|
9236
|
+
}
|
|
9237
|
+
|
|
9238
|
+
#hasUnfinishedWork(snapshot: CompactionStateSnapshot): boolean {
|
|
9239
|
+
if (snapshot.queuedMessages) return true;
|
|
9240
|
+
if (snapshot.lastAssistantStopReason === "length") return true;
|
|
9241
|
+
// A verified paused goal is a human-wait terminus. It suppresses stale
|
|
9242
|
+
// todo/workflow activity until a queued user message resumes the run.
|
|
9243
|
+
if (snapshot.goal?.status === "paused") return false;
|
|
9244
|
+
if (snapshot.goal?.enabled && snapshot.goal.status === "active") return true;
|
|
9245
|
+
if (snapshot.openTodos.length > 0) return true;
|
|
9246
|
+
return snapshot.activeSkills.some(entry => !isWorkflowContinuationInert(entry.skill, entry.phase));
|
|
9247
|
+
}
|
|
9248
|
+
|
|
8865
9249
|
async #applyCompactionPostAppend(
|
|
8866
9250
|
compactionEntryId: string,
|
|
8867
9251
|
firstKeptEntryId: string,
|
|
@@ -9060,6 +9444,26 @@ export class AgentSession {
|
|
|
9060
9444
|
const outcome = await this.#abortWithOutcome(options);
|
|
9061
9445
|
if (outcome.kind === "error") throw outcome.cause;
|
|
9062
9446
|
}
|
|
9447
|
+
/**
|
|
9448
|
+
* Abort a specific active prompt and prove whether its tracked resources settled.
|
|
9449
|
+
*/
|
|
9450
|
+
async abortPromptAndWait(handle: string, options: { graceMs: number }): Promise<RunSettlementProof> {
|
|
9451
|
+
// Only the abort trigger is gated on active ownership; settlement is always proven
|
|
9452
|
+
// from the ledger, because a run can go inactive while its resources still run.
|
|
9453
|
+
if (handle === this.agent.activeResourceRunId)
|
|
9454
|
+
// Awaiting the abort unboundedly would let a provider or tool that ignores
|
|
9455
|
+
// cancellation defer the grace forever, so the proof races the fixed grace
|
|
9456
|
+
// independently of abort completion.
|
|
9457
|
+
void this.abort({ timeoutMs: options.graceMs }).catch(() => {});
|
|
9458
|
+
|
|
9459
|
+
const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
|
|
9460
|
+
if (proof.status === "unfenced") {
|
|
9461
|
+
// Detach the unsettled resources so they can never re-enter this run; their real
|
|
9462
|
+
// promises still release them inside the ledger.
|
|
9463
|
+
this.agent.resourceLedger.quarantine(handle);
|
|
9464
|
+
}
|
|
9465
|
+
return proof;
|
|
9466
|
+
}
|
|
9063
9467
|
|
|
9064
9468
|
/** Atomically interrupt the active run and make text the next prompt. */
|
|
9065
9469
|
async cancelAndSubmit(text: string, options?: { queuedEntryId?: string }): Promise<CancelAndSubmitOutcome> {
|
|
@@ -9713,6 +10117,16 @@ export class AgentSession {
|
|
|
9713
10117
|
getSessionDefaultModelSelector(): string | undefined {
|
|
9714
10118
|
return getSessionContextForInternalRead(this.sessionManager).models.default;
|
|
9715
10119
|
}
|
|
10120
|
+
/**
|
|
10121
|
+
* Resolve the model that `modelRoles.default` currently points to, independent
|
|
10122
|
+
* of whatever model this session's log last recorded. Used by the TUI resume
|
|
10123
|
+
* flow's `session.resumeModelBehavior: "ask"` prompt to offer the currently
|
|
10124
|
+
* configured default as an alternative to the session's saved model.
|
|
10125
|
+
*/
|
|
10126
|
+
resolveConfiguredDefaultModel(): Model | undefined {
|
|
10127
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
10128
|
+
return this.#resolveRoleModelFull("default", availableModels, undefined).model;
|
|
10129
|
+
}
|
|
9716
10130
|
|
|
9717
10131
|
/**
|
|
9718
10132
|
* Re-assert the session resume default ("provider/id") in the session log
|
|
@@ -10604,6 +11018,16 @@ export class AgentSession {
|
|
|
10604
11018
|
return getSupportedEfforts(this.model);
|
|
10605
11019
|
}
|
|
10606
11020
|
|
|
11021
|
+
/**
|
|
11022
|
+
* Runtime evidence published for the current GJC bundle activation
|
|
11023
|
+
* generation, set once by `createAgentSession` after every producer has run.
|
|
11024
|
+
* Undefined until that publication happens, so consumers report runtime
|
|
11025
|
+
* status as unavailable rather than falsely clear.
|
|
11026
|
+
*/
|
|
11027
|
+
gjcRuntimeSnapshot?: GjcRuntimeSnapshotProvider;
|
|
11028
|
+
/** Activation generation a published snapshot must match to be merged. */
|
|
11029
|
+
gjcActivationGeneration?: number;
|
|
11030
|
+
|
|
10607
11031
|
// =========================================================================
|
|
10608
11032
|
// Message Queue Mode Management
|
|
10609
11033
|
// =========================================================================
|
|
@@ -10645,13 +11069,93 @@ export class AgentSession {
|
|
|
10645
11069
|
async #pruneToolOutputs(
|
|
10646
11070
|
signal?: AbortSignal,
|
|
10647
11071
|
overThreshold = false,
|
|
10648
|
-
|
|
11072
|
+
options?: { commitGate?: (actual: { prunedCount: number; tokensSaved: number }) => boolean },
|
|
11073
|
+
): Promise<{ prunedCount: number; tokensSaved: number; committed: boolean } | undefined> {
|
|
10649
11074
|
const branchEntries = this.sessionManager.getBranch();
|
|
10650
|
-
const
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
11075
|
+
const artifactManager = this.sessionManager.getArtifactManager();
|
|
11076
|
+
const prunedArtifacts: Array<{ entryId: string; id: string; toolType: string; originalText: string }> = [];
|
|
11077
|
+
let reservedArtifactId: string | undefined;
|
|
11078
|
+
if (artifactManager) {
|
|
11079
|
+
try {
|
|
11080
|
+
reservedArtifactId = (await artifactManager.allocatePath("tool-output")).id;
|
|
11081
|
+
} catch (error) {
|
|
11082
|
+
logger.warn("Failed to reserve artifact ID for pruned tool output", {
|
|
11083
|
+
error: error instanceof Error ? error.message : String(error),
|
|
11084
|
+
});
|
|
11085
|
+
}
|
|
11086
|
+
}
|
|
11087
|
+
const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG, {
|
|
11088
|
+
relaxedMinimum: overThreshold ? 0 : undefined,
|
|
11089
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11090
|
+
artifactRef: candidate => {
|
|
11091
|
+
if (!artifactManager) return undefined;
|
|
11092
|
+
const id = reservedArtifactId ?? String(artifactManager.allocateId());
|
|
11093
|
+
reservedArtifactId = undefined;
|
|
11094
|
+
const toolType = (candidate.toolName ?? "tool-output").replace(/[^a-zA-Z0-9_-]/g, "_") || "tool-output";
|
|
11095
|
+
prunedArtifacts.push({ entryId: candidate.entryId, id, toolType, originalText: candidate.originalText });
|
|
11096
|
+
return `artifact://${id}`;
|
|
11097
|
+
},
|
|
11098
|
+
});
|
|
11099
|
+
const failedArtifactEntryIds = new Set<string>();
|
|
11100
|
+
const publishedArtifacts: typeof prunedArtifacts = [];
|
|
11101
|
+
for (const artifact of prunedArtifacts.filter(artifact =>
|
|
11102
|
+
result.originals.some(original => original.entryId === artifact.entryId),
|
|
11103
|
+
)) {
|
|
11104
|
+
try {
|
|
11105
|
+
await artifactManager?.publishNamedNoReplace(
|
|
11106
|
+
`${artifact.id}.${artifact.toolType}.log`,
|
|
11107
|
+
new TextEncoder().encode(artifact.originalText),
|
|
11108
|
+
);
|
|
11109
|
+
publishedArtifacts.push(artifact);
|
|
11110
|
+
} catch (error) {
|
|
11111
|
+
failedArtifactEntryIds.add(artifact.entryId);
|
|
11112
|
+
logger.warn("Failed to persist pruned tool output artifact", {
|
|
11113
|
+
artifactId: artifact.id,
|
|
11114
|
+
error: error instanceof Error ? error.message : String(error),
|
|
11115
|
+
});
|
|
11116
|
+
}
|
|
11117
|
+
}
|
|
11118
|
+
const rollbackPublishedArtifacts = async (reason: "aborted" | "commit_gate_rejected") => {
|
|
11119
|
+
let removedArtifacts = 0;
|
|
11120
|
+
let unremovedArtifacts = 0;
|
|
11121
|
+
for (const artifact of publishedArtifacts) {
|
|
11122
|
+
const filename = `${artifact.id}.${artifact.toolType}.log`;
|
|
11123
|
+
let removed = false;
|
|
11124
|
+
for (let attempt = 0; attempt < 3 && !removed; attempt++) {
|
|
11125
|
+
removed = (await artifactManager?.removeNamedBestEffort(filename)) ?? false;
|
|
11126
|
+
if (!removed && attempt < 2) await Bun.sleep(10);
|
|
11127
|
+
}
|
|
11128
|
+
if (removed) removedArtifacts++;
|
|
11129
|
+
else {
|
|
11130
|
+
unremovedArtifacts++;
|
|
11131
|
+
logger.warn("Failed to roll back staged pruned-output artifact after retries", {
|
|
11132
|
+
artifactId: artifact.id,
|
|
11133
|
+
reason,
|
|
11134
|
+
});
|
|
11135
|
+
}
|
|
11136
|
+
}
|
|
11137
|
+
return { removedArtifacts, unremovedArtifacts };
|
|
11138
|
+
};
|
|
11139
|
+
let toolTokensSaved = result.tokensSaved;
|
|
11140
|
+
let toolPrunedCount = result.prunedCount;
|
|
11141
|
+
const committedToolEntries = result.prunedEntries.filter(entry => {
|
|
11142
|
+
if (!failedArtifactEntryIds.has(entry.id)) return true;
|
|
11143
|
+
const original = result.originals.find(candidate => candidate.entryId === entry.id);
|
|
11144
|
+
if (!original) return true;
|
|
11145
|
+
const message = entry.message as ToolResultMessage;
|
|
11146
|
+
toolTokensSaved -=
|
|
11147
|
+
original.tokens -
|
|
11148
|
+
estimateTextTokensHeuristic(
|
|
11149
|
+
message.content
|
|
11150
|
+
.filter((part): part is TextContent => part.type === "text")
|
|
11151
|
+
.map(part => part.text)
|
|
11152
|
+
.join(""),
|
|
11153
|
+
);
|
|
11154
|
+
toolPrunedCount--;
|
|
11155
|
+
message.content = [{ type: "text", text: original.originalText }];
|
|
11156
|
+
delete message.prunedAt;
|
|
11157
|
+
return false;
|
|
11158
|
+
});
|
|
10655
11159
|
const argumentResult = pruneAssistantToolArguments(branchEntries, DEFAULT_PRUNE_CONFIG);
|
|
10656
11160
|
const fileMentionResult = pruneStaleFileMentions(branchEntries, p =>
|
|
10657
11161
|
resolveReadPath(p, this.sessionManager.getCwd()),
|
|
@@ -10659,22 +11163,37 @@ export class AgentSession {
|
|
|
10659
11163
|
const volatileContextResult = pruneSupersededVolatileProjectContext(branchEntries);
|
|
10660
11164
|
const reminderResult = pruneSupersededMaintenanceReminders(branchEntries);
|
|
10661
11165
|
const tokensSaved =
|
|
10662
|
-
|
|
11166
|
+
toolTokensSaved +
|
|
10663
11167
|
argumentResult.argumentTokensSaved +
|
|
10664
11168
|
Math.round((fileMentionResult.bytesSaved + volatileContextResult.bytesSaved + reminderResult.bytesSaved) / 4);
|
|
10665
11169
|
const prunedCount =
|
|
10666
|
-
|
|
11170
|
+
toolPrunedCount +
|
|
10667
11171
|
argumentResult.argumentPrunedCount +
|
|
10668
11172
|
fileMentionResult.changed.length +
|
|
10669
11173
|
volatileContextResult.changed.length +
|
|
10670
11174
|
reminderResult.changed.length;
|
|
10671
11175
|
if (prunedCount === 0 || signal?.aborted) {
|
|
11176
|
+
if (publishedArtifacts.length > 0) await rollbackPublishedArtifacts("aborted");
|
|
11177
|
+
return undefined;
|
|
11178
|
+
}
|
|
11179
|
+
if (options?.commitGate && !options.commitGate({ prunedCount, tokensSaved })) {
|
|
11180
|
+
const rollback = await rollbackPublishedArtifacts("commit_gate_rejected");
|
|
11181
|
+
logger.info("Below-threshold maintenance pruning staged but not committed", {
|
|
11182
|
+
prunedCount,
|
|
11183
|
+
tokensSaved,
|
|
11184
|
+
rolledBackArtifacts: rollback.removedArtifacts,
|
|
11185
|
+
unremovedArtifacts: rollback.unremovedArtifacts,
|
|
11186
|
+
});
|
|
11187
|
+
return { prunedCount, tokensSaved, committed: false };
|
|
11188
|
+
}
|
|
11189
|
+
if (signal?.aborted) {
|
|
11190
|
+
await rollbackPublishedArtifacts("aborted");
|
|
10672
11191
|
return undefined;
|
|
10673
11192
|
}
|
|
10674
11193
|
|
|
10675
11194
|
// getBranch() returns materialized copies for blob-externalized entries, so
|
|
10676
11195
|
// the pruning mutations must be written back into the canonical store.
|
|
10677
|
-
const combined = [...
|
|
11196
|
+
const combined = [...committedToolEntries, ...argumentResult.prunedEntries, ...fileMentionResult.changed];
|
|
10678
11197
|
this.sessionManager.applyEntryMessageUpdates(combined);
|
|
10679
11198
|
this.sessionManager.applyCustomMessageEntryUpdates([...volatileContextResult.changed, ...reminderResult.changed]);
|
|
10680
11199
|
await this.sessionManager.rewriteEntries();
|
|
@@ -10685,7 +11204,7 @@ export class AgentSession {
|
|
|
10685
11204
|
this.#resetInjectedContextSignatures();
|
|
10686
11205
|
this.#syncTodoPhasesFromBranch();
|
|
10687
11206
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
10688
|
-
return { prunedCount, tokensSaved };
|
|
11207
|
+
return { prunedCount, tokensSaved, committed: true };
|
|
10689
11208
|
}
|
|
10690
11209
|
|
|
10691
11210
|
/**
|
|
@@ -10718,7 +11237,9 @@ export class AgentSession {
|
|
|
10718
11237
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
10719
11238
|
if (!compactionSettings.maintenancePruningEnabled) return;
|
|
10720
11239
|
|
|
10721
|
-
const estimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG
|
|
11240
|
+
const estimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
|
|
11241
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11242
|
+
});
|
|
10722
11243
|
const cacheEpochResetCost = this.#lastCacheEpochResetCost();
|
|
10723
11244
|
if (
|
|
10724
11245
|
!shouldRunMaintenancePrune({
|
|
@@ -10731,10 +11252,30 @@ export class AgentSession {
|
|
|
10731
11252
|
return;
|
|
10732
11253
|
}
|
|
10733
11254
|
|
|
10734
|
-
const pruneResult = await this.#pruneToolOutputs(
|
|
11255
|
+
const pruneResult = await this.#pruneToolOutputs(undefined, false, {
|
|
11256
|
+
commitGate: actual => actual.tokensSaved > cacheEpochResetCost,
|
|
11257
|
+
});
|
|
10735
11258
|
if (!pruneResult || pruneResult.prunedCount === 0) return;
|
|
10736
|
-
|
|
10737
|
-
|
|
11259
|
+
if (!pruneResult.committed) {
|
|
11260
|
+
const message = `Maintenance pruning skipped: actual savings ${pruneResult.tokensSaved} tokens <= cache-epoch reset cost ${cacheEpochResetCost} after artifact notices/publication rollback.`;
|
|
11261
|
+
await this.#emitSessionEvent({
|
|
11262
|
+
type: "notice",
|
|
11263
|
+
level: "info",
|
|
11264
|
+
source: "maintenance-prune",
|
|
11265
|
+
message,
|
|
11266
|
+
});
|
|
11267
|
+
logger.info("Below-threshold maintenance pruning skipped", {
|
|
11268
|
+
tokensSaved: pruneResult.tokensSaved,
|
|
11269
|
+
prunedCount: pruneResult.prunedCount,
|
|
11270
|
+
cacheEpochResetCost,
|
|
11271
|
+
minSavings: compactionSettings.maintenancePruningMinSavingsTokens,
|
|
11272
|
+
});
|
|
11273
|
+
return;
|
|
11274
|
+
}
|
|
11275
|
+
const resetReason =
|
|
11276
|
+
pruneResult.tokensSaved > cacheEpochResetCost
|
|
11277
|
+
? `below-threshold maintenance: reclaimed ${pruneResult.tokensSaved} tokens > cache-epoch reset cost ${cacheEpochResetCost}`
|
|
11278
|
+
: `below-threshold maintenance: reclaimed ${pruneResult.tokensSaved} tokens <= cache-epoch reset cost ${cacheEpochResetCost}`;
|
|
10738
11279
|
await this.#emitSessionEvent({
|
|
10739
11280
|
type: "notice",
|
|
10740
11281
|
level: "info",
|
|
@@ -10768,6 +11309,8 @@ export class AgentSession {
|
|
|
10768
11309
|
await this.abort();
|
|
10769
11310
|
const compactionAbortController = new AbortController();
|
|
10770
11311
|
this.#compactionAbortController = compactionAbortController;
|
|
11312
|
+
// Take this invocation's state snapshot for the summarizer context.
|
|
11313
|
+
const compactionStateSnapshot = await this.#compactionStateSnapshot();
|
|
10771
11314
|
|
|
10772
11315
|
try {
|
|
10773
11316
|
if (!this.model) {
|
|
@@ -10813,7 +11356,11 @@ export class AgentSession {
|
|
|
10813
11356
|
}
|
|
10814
11357
|
}
|
|
10815
11358
|
|
|
10816
|
-
const compactionPrep = await this.#prepareCompactionFromHooks(
|
|
11359
|
+
const compactionPrep = await this.#prepareCompactionFromHooks(
|
|
11360
|
+
preparation,
|
|
11361
|
+
hookCompaction,
|
|
11362
|
+
compactionStateSnapshot,
|
|
11363
|
+
);
|
|
10817
11364
|
|
|
10818
11365
|
let summary: string;
|
|
10819
11366
|
let shortSummary: string | undefined;
|
|
@@ -11305,6 +11852,7 @@ export class AgentSession {
|
|
|
11305
11852
|
assistantMessage: AssistantMessage,
|
|
11306
11853
|
skipAbortedCheck = true,
|
|
11307
11854
|
onTerminalOverflowNoop?: () => void,
|
|
11855
|
+
resourceRunId?: string,
|
|
11308
11856
|
): Promise<boolean> {
|
|
11309
11857
|
// Safety stops are terminal and must not trigger context maintenance.
|
|
11310
11858
|
if (
|
|
@@ -11351,7 +11899,12 @@ export class AgentSession {
|
|
|
11351
11899
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
11352
11900
|
if (promoted) {
|
|
11353
11901
|
// Retry on the promoted (larger) model without compacting
|
|
11354
|
-
this.#scheduleAgentContinue({
|
|
11902
|
+
this.#scheduleAgentContinue({
|
|
11903
|
+
delayMs: 100,
|
|
11904
|
+
generation,
|
|
11905
|
+
suppressPredecessorAgentEnd: true,
|
|
11906
|
+
resourceRunId,
|
|
11907
|
+
});
|
|
11355
11908
|
|
|
11356
11909
|
return true;
|
|
11357
11910
|
}
|
|
@@ -11367,10 +11920,11 @@ export class AgentSession {
|
|
|
11367
11920
|
this.agent.appendMessage(assistantMessage);
|
|
11368
11921
|
}
|
|
11369
11922
|
},
|
|
11923
|
+
resourceRunId,
|
|
11370
11924
|
});
|
|
11371
11925
|
return "continuationScheduled" in status && status.continuationScheduled === true;
|
|
11372
11926
|
}
|
|
11373
|
-
return this.#scheduleOverflowRetryContinuation(generation);
|
|
11927
|
+
return await this.#scheduleOverflowRetryContinuation(generation, resourceRunId);
|
|
11374
11928
|
}
|
|
11375
11929
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
11376
11930
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
|
|
@@ -11390,6 +11944,7 @@ export class AgentSession {
|
|
|
11390
11944
|
return true;
|
|
11391
11945
|
const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
|
|
11392
11946
|
relaxedMinimum: 0,
|
|
11947
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11393
11948
|
});
|
|
11394
11949
|
if (
|
|
11395
11950
|
pruneEstimate.tokensSaved > 0 &&
|
|
@@ -11494,6 +12049,7 @@ export class AgentSession {
|
|
|
11494
12049
|
// prompt-cache epoch via #closeCodexProviderSessionsForHistoryRewrite.
|
|
11495
12050
|
const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
|
|
11496
12051
|
relaxedMinimum: 0,
|
|
12052
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11497
12053
|
});
|
|
11498
12054
|
let pruneResult: { prunedCount: number; tokensSaved: number } | undefined;
|
|
11499
12055
|
if (
|
|
@@ -11675,6 +12231,7 @@ export class AgentSession {
|
|
|
11675
12231
|
|
|
11676
12232
|
const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
|
|
11677
12233
|
relaxedMinimum: 0,
|
|
12234
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11678
12235
|
});
|
|
11679
12236
|
if (
|
|
11680
12237
|
pruneEstimate.tokensSaved > 0 &&
|
|
@@ -12614,6 +13171,7 @@ export class AgentSession {
|
|
|
12614
13171
|
async #prepareCompactionFromHooks(
|
|
12615
13172
|
preparation: CompactionPreparation,
|
|
12616
13173
|
hookCompaction: CompactionResult | undefined,
|
|
13174
|
+
stateSnapshot: CompactionStateSnapshot,
|
|
12617
13175
|
): Promise<
|
|
12618
13176
|
| {
|
|
12619
13177
|
kind: "fromHook";
|
|
@@ -12653,11 +13211,17 @@ export class AgentSession {
|
|
|
12653
13211
|
hookContext = hookContext ? [...hookContext, memoryBackendContext] : [memoryBackendContext];
|
|
12654
13212
|
}
|
|
12655
13213
|
|
|
13214
|
+
const stateContext = this.#compactionStateContext(stateSnapshot);
|
|
13215
|
+
|
|
13216
|
+
if (stateContext.length > 0) {
|
|
13217
|
+
hookContext = hookContext ? [...hookContext, ...stateContext] : stateContext;
|
|
13218
|
+
}
|
|
13219
|
+
|
|
12656
13220
|
if (hookCompaction) {
|
|
12657
13221
|
preserveData ??= hookCompaction.preserveData;
|
|
12658
13222
|
return {
|
|
12659
13223
|
kind: "fromHook",
|
|
12660
|
-
summary: hookCompaction.summary,
|
|
13224
|
+
summary: appendCompactionStateContext(hookCompaction.summary, stateContext),
|
|
12661
13225
|
shortSummary: hookCompaction.shortSummary,
|
|
12662
13226
|
firstKeptEntryId: hookCompaction.firstKeptEntryId,
|
|
12663
13227
|
tokensBefore: hookCompaction.tokensBefore,
|
|
@@ -12682,6 +13246,7 @@ export class AgentSession {
|
|
|
12682
13246
|
force?: boolean;
|
|
12683
13247
|
signal?: AbortSignal;
|
|
12684
13248
|
beforeTerminalOverflowNoop?: () => void;
|
|
13249
|
+
resourceRunId?: string;
|
|
12685
13250
|
},
|
|
12686
13251
|
): Promise<AutoCompactionTerminalStatus> {
|
|
12687
13252
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
@@ -12703,7 +13268,7 @@ export class AgentSession {
|
|
|
12703
13268
|
if (signal.aborted) return;
|
|
12704
13269
|
await this.#runAutoCompaction(reason, willRetry, true, options);
|
|
12705
13270
|
},
|
|
12706
|
-
{ generation },
|
|
13271
|
+
{ generation, resourceRunId: options?.resourceRunId },
|
|
12707
13272
|
);
|
|
12708
13273
|
return { kind: "skipped" };
|
|
12709
13274
|
}
|
|
@@ -12735,6 +13300,10 @@ export class AgentSession {
|
|
|
12735
13300
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
12736
13301
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
12737
13302
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
13303
|
+
const compactionStateSnapshot = await this.#compactionStateSnapshot();
|
|
13304
|
+
if (autoCompactionSignal.aborted || this.#isDisposed || this.#promptGeneration !== generation) {
|
|
13305
|
+
return await emitAborted();
|
|
13306
|
+
}
|
|
12738
13307
|
|
|
12739
13308
|
if (compactionSettings.strategy === "handoff" && reason !== "overflow") {
|
|
12740
13309
|
const handoffFocus = AUTO_HANDOFF_THRESHOLD_FOCUS;
|
|
@@ -12774,6 +13343,7 @@ export class AgentSession {
|
|
|
12774
13343
|
if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
12775
13344
|
this.#scheduleAutoContinuePrompt(generation);
|
|
12776
13345
|
}
|
|
13346
|
+
|
|
12777
13347
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
12778
13348
|
return { kind: "compacted" };
|
|
12779
13349
|
}
|
|
@@ -12827,15 +13397,22 @@ export class AgentSession {
|
|
|
12827
13397
|
const overflowNoopWouldReplay = reason === "overflow" && willRetry && this.#isResumableAgentTail();
|
|
12828
13398
|
const continuationSkipReason =
|
|
12829
13399
|
!overflowNoopWouldReplay && willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
13400
|
+
if (continuationSkipReason) {
|
|
13401
|
+
this.#logCompactionContinuationSkipped("overflow_retry", continuationSkipReason);
|
|
13402
|
+
}
|
|
12830
13403
|
if (overflowNoopWouldReplay) {
|
|
12831
13404
|
options?.beforeTerminalOverflowNoop?.();
|
|
12832
13405
|
}
|
|
13406
|
+
const overflowContinuationScheduled =
|
|
13407
|
+
!overflowNoopWouldReplay && willRetry && !continuationSkipReason
|
|
13408
|
+
? await this.#scheduleOverflowRetryContinuation(generation)
|
|
13409
|
+
: false;
|
|
12833
13410
|
await this.#emitSessionEvent({
|
|
12834
13411
|
type: "auto_compaction_end",
|
|
12835
13412
|
action,
|
|
12836
13413
|
result: undefined,
|
|
12837
13414
|
aborted: false,
|
|
12838
|
-
willRetry:
|
|
13415
|
+
willRetry: overflowContinuationScheduled,
|
|
12839
13416
|
errorMessage: overflowNoopWouldReplay
|
|
12840
13417
|
? "Context overflow recovery skipped: nothing eligible to compact. Run /clear to preserve this session ID, or switch to a larger-context model before retrying."
|
|
12841
13418
|
: undefined,
|
|
@@ -12848,6 +13425,7 @@ export class AgentSession {
|
|
|
12848
13425
|
delayMs: 100,
|
|
12849
13426
|
generation,
|
|
12850
13427
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
13428
|
+
rescheduleOnBusy: true,
|
|
12851
13429
|
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
12852
13430
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
12853
13431
|
});
|
|
@@ -12856,7 +13434,9 @@ export class AgentSession {
|
|
|
12856
13434
|
return { kind: "skipped" };
|
|
12857
13435
|
}
|
|
12858
13436
|
if (willRetry) {
|
|
12859
|
-
return
|
|
13437
|
+
return overflowContinuationScheduled
|
|
13438
|
+
? { kind: "skipped", continuationScheduled: true }
|
|
13439
|
+
: { kind: "skipped" };
|
|
12860
13440
|
}
|
|
12861
13441
|
if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
12862
13442
|
this.#scheduleAgentContinue({
|
|
@@ -12865,6 +13445,7 @@ export class AgentSession {
|
|
|
12865
13445
|
suppressPredecessorAgentEnd: true,
|
|
12866
13446
|
|
|
12867
13447
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
13448
|
+
rescheduleOnBusy: true,
|
|
12868
13449
|
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
12869
13450
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
12870
13451
|
});
|
|
@@ -12905,7 +13486,11 @@ export class AgentSession {
|
|
|
12905
13486
|
}
|
|
12906
13487
|
}
|
|
12907
13488
|
|
|
12908
|
-
const compactionPrep = await this.#prepareCompactionFromHooks(
|
|
13489
|
+
const compactionPrep = await this.#prepareCompactionFromHooks(
|
|
13490
|
+
preparation,
|
|
13491
|
+
hookCompaction,
|
|
13492
|
+
compactionStateSnapshot,
|
|
13493
|
+
);
|
|
12909
13494
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
12910
13495
|
|
|
12911
13496
|
let summary: string;
|
|
@@ -13088,18 +13673,25 @@ export class AgentSession {
|
|
|
13088
13673
|
this.#lastOversizedAutoMaintenanceAttemptSignature = undefined;
|
|
13089
13674
|
|
|
13090
13675
|
const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
13676
|
+
if (continuationSkipReason) {
|
|
13677
|
+
this.#logCompactionContinuationSkipped("overflow_retry", continuationSkipReason);
|
|
13678
|
+
}
|
|
13679
|
+
const overflowContinuationScheduled =
|
|
13680
|
+
willRetry && !continuationSkipReason ? await this.#scheduleOverflowRetryContinuation(generation) : false;
|
|
13091
13681
|
await this.#emitSessionEvent({
|
|
13092
13682
|
type: "auto_compaction_end",
|
|
13093
13683
|
action,
|
|
13094
13684
|
result,
|
|
13095
13685
|
aborted: false,
|
|
13096
|
-
willRetry:
|
|
13686
|
+
willRetry: overflowContinuationScheduled,
|
|
13097
13687
|
continuationSkipReason,
|
|
13098
13688
|
});
|
|
13099
13689
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
13100
13690
|
|
|
13101
13691
|
if (willRetry) {
|
|
13102
|
-
return
|
|
13692
|
+
return overflowContinuationScheduled
|
|
13693
|
+
? { kind: "compacted", continuationScheduled: true }
|
|
13694
|
+
: { kind: "compacted" };
|
|
13103
13695
|
}
|
|
13104
13696
|
if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
13105
13697
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
@@ -13109,6 +13701,7 @@ export class AgentSession {
|
|
|
13109
13701
|
generation,
|
|
13110
13702
|
suppressPredecessorAgentEnd: true,
|
|
13111
13703
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
13704
|
+
rescheduleOnBusy: true,
|
|
13112
13705
|
onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
|
|
13113
13706
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
13114
13707
|
});
|
|
@@ -13223,6 +13816,10 @@ export class AgentSession {
|
|
|
13223
13816
|
return false;
|
|
13224
13817
|
}
|
|
13225
13818
|
|
|
13819
|
+
#isTypedFirstEventTimeout(message: AssistantMessage): boolean {
|
|
13820
|
+
return message.transportFailure?.providerCode?.toLowerCase() === STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE;
|
|
13821
|
+
}
|
|
13822
|
+
|
|
13226
13823
|
#isTerminalProviderFirstEventTimeout(message: AssistantMessage): boolean {
|
|
13227
13824
|
return this.#isKimiCodeFirstEventTimeout(message) || this.#isAlibabaTokenPlanFirstEventTimeout(message);
|
|
13228
13825
|
}
|
|
@@ -13348,6 +13945,7 @@ export class AgentSession {
|
|
|
13348
13945
|
#classifyErrorForRetry(message: AssistantMessage): RetryErrorClassification {
|
|
13349
13946
|
if (message.stopReason !== "error") return "none";
|
|
13350
13947
|
if (message.errorKind === "provider_safety_stop") return "terminal";
|
|
13948
|
+
if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
|
|
13351
13949
|
if (!message.errorMessage) return "none";
|
|
13352
13950
|
const err = message.errorMessage;
|
|
13353
13951
|
// Provider safety refusals (e.g. Anthropic stop_reason "refusal" /
|
|
@@ -13388,10 +13986,8 @@ export class AgentSession {
|
|
|
13388
13986
|
if (this.#isTerminalProviderFirstEventTimeout(message)) {
|
|
13389
13987
|
return "terminal";
|
|
13390
13988
|
}
|
|
13391
|
-
//
|
|
13392
|
-
//
|
|
13393
|
-
// the full request to a remote, billable backend, so an unbounded loop
|
|
13394
|
-
// can silently spike usage (#713). Bound it to retry.maxRetries instead.
|
|
13989
|
+
// Legacy providers that have not yet stamped the typed timeout fact retain
|
|
13990
|
+
// their existing bounded ollama-cloud behavior.
|
|
13395
13991
|
if (this.#isFirstEventTimeoutErrorMessage(err) && this.#shouldFailClosedOnFirstEventTimeout(message)) {
|
|
13396
13992
|
return "first_event_timeout";
|
|
13397
13993
|
}
|
|
@@ -13727,25 +14323,25 @@ export class AgentSession {
|
|
|
13727
14323
|
return `Model fallback chain exhausted; models tried: ${tried}; models skipped: ${skipped}`;
|
|
13728
14324
|
}
|
|
13729
14325
|
|
|
13730
|
-
async #
|
|
13731
|
-
class: FallbackTriggerClass;
|
|
13732
|
-
retryAfterMs?: number;
|
|
13733
|
-
}): Promise<boolean> {
|
|
14326
|
+
async #markFailedCredential(trigger: { class: FallbackTriggerClass; retryAfterMs?: number }): Promise<boolean> {
|
|
13734
14327
|
if (!this.model || (trigger.class !== "auth" && trigger.class !== "quota" && trigger.class !== "rate_limit")) {
|
|
13735
14328
|
return false;
|
|
13736
14329
|
}
|
|
13737
14330
|
const authStorage = this.#modelRegistry.authStorage;
|
|
14331
|
+
const providerAffinitySessionId = this.agent.providerSessionId ?? this.agent.sessionId ?? this.sessionId;
|
|
13738
14332
|
if (trigger.class === "auth") {
|
|
13739
|
-
const apiKey = await this.#modelRegistry.getApiKey(this.model,
|
|
14333
|
+
const apiKey = await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId);
|
|
13740
14334
|
if (!isAuthenticated(apiKey)) return false;
|
|
13741
|
-
return authStorage.invalidateCredentialMatching(this.model.provider, apiKey, {
|
|
14335
|
+
return authStorage.invalidateCredentialMatching(this.model.provider, apiKey, {
|
|
14336
|
+
sessionId: providerAffinitySessionId,
|
|
14337
|
+
});
|
|
13742
14338
|
}
|
|
13743
14339
|
if (authStorage.hasRuntimeApiKey(this.model.provider)) return false;
|
|
13744
|
-
const activeApiKey = await this.#modelRegistry.getApiKey(this.model,
|
|
13745
|
-
const rotated = await authStorage.markUsageLimitReached(this.model.provider,
|
|
14340
|
+
const activeApiKey = await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId);
|
|
14341
|
+
const rotated = await authStorage.markUsageLimitReached(this.model.provider, providerAffinitySessionId, {
|
|
13746
14342
|
retryAfterMs: trigger.retryAfterMs,
|
|
13747
14343
|
});
|
|
13748
|
-
return rotated && (await this.#modelRegistry.getApiKey(this.model,
|
|
14344
|
+
return rotated && (await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId)) !== activeApiKey;
|
|
13749
14345
|
}
|
|
13750
14346
|
|
|
13751
14347
|
/** Handle retryable errors with exponential backoff. */
|
|
@@ -13766,16 +14362,16 @@ export class AgentSession {
|
|
|
13766
14362
|
// user opt-out.
|
|
13767
14363
|
if (!managedFallback && !retrySettings.enabled) return false;
|
|
13768
14364
|
const classification = managedFallback ? undefined : this.#classifyErrorForRetry(message);
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
|
|
13772
|
-
|
|
13773
|
-
|
|
13774
|
-
|
|
13775
|
-
|
|
13776
|
-
)
|
|
13777
|
-
|
|
13778
|
-
|
|
14365
|
+
if (classification === "first_event_timeout") {
|
|
14366
|
+
this.#firstEventTimeoutRetryStartedAt ??= Date.now();
|
|
14367
|
+
}
|
|
14368
|
+
// First-event timeouts are replay-safe only before any model progress,
|
|
14369
|
+
// abort, or extension/provider lifecycle participation.
|
|
14370
|
+
if (
|
|
14371
|
+
classification === "first_event_timeout" &&
|
|
14372
|
+
(!this.#hasCleanRetryReplaySafety || assistantMessageHasVisibleOrToolContent(message))
|
|
14373
|
+
) {
|
|
14374
|
+
return false;
|
|
13779
14375
|
}
|
|
13780
14376
|
const trigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
|
|
13781
14377
|
if (!trigger) {
|
|
@@ -13783,6 +14379,39 @@ export class AgentSession {
|
|
|
13783
14379
|
? this.#managedFallbackExhaustionDecision(message, message.errorMessage || "Model fallback attempt failed")
|
|
13784
14380
|
: false;
|
|
13785
14381
|
}
|
|
14382
|
+
// Credential rotation: a content-free quota/rate-limit failure has no
|
|
14383
|
+
// observable state to corrupt, so it is replay-safe regardless of
|
|
14384
|
+
// extension lifecycle participation. Mark the failed credential and
|
|
14385
|
+
// retry with the next stored credential of the same provider.
|
|
14386
|
+
let credentialRotated =
|
|
14387
|
+
!managedFallback &&
|
|
14388
|
+
!assistantMessageHasVisibleOrToolContent(message) &&
|
|
14389
|
+
(trigger.class === "quota" || trigger.class === "rate_limit") &&
|
|
14390
|
+
(await this.#markFailedCredential(trigger));
|
|
14391
|
+
// A content-free credential rotation is inherently replay-safe: no partial
|
|
14392
|
+
// output, no tool calls, no extension-observable streaming state was produced
|
|
14393
|
+
// before the failure. This bypasses #hasCleanRetryReplaySafety because the
|
|
14394
|
+
// content-free check is the replay-safety guarantee for credential rotation.
|
|
14395
|
+
const canReplayRotatedCredential = credentialRotated;
|
|
14396
|
+
// Bare defaults admit only clean, side-effect-free canonical stream watchdog failures,
|
|
14397
|
+
// content-free quota/rate-limit credential rotations, and the explicit Codex
|
|
14398
|
+
// capacity-overload event.
|
|
14399
|
+
if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential) {
|
|
14400
|
+
const bareDefaultCodexOverload = isBareDefaultCodexOverload(message);
|
|
14401
|
+
// Content-free Codex overload is replay-safe by the same reasoning as
|
|
14402
|
+
// credential rotation: no partial output means no observable state.
|
|
14403
|
+
const canReplayCodexOverload = bareDefaultCodexOverload;
|
|
14404
|
+
if (
|
|
14405
|
+
(!canReplayCodexOverload &&
|
|
14406
|
+
!this.#isTypedFirstEventTimeout(message) &&
|
|
14407
|
+
(hasBareDefaultRetryDisqualifyingFacts(message) ||
|
|
14408
|
+
(classification !== "transient" && classification !== "first_event_timeout") ||
|
|
14409
|
+
!BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))) ||
|
|
14410
|
+
(!canReplayCodexOverload && !this.#hasCleanRetryReplaySafety)
|
|
14411
|
+
) {
|
|
14412
|
+
return false;
|
|
14413
|
+
}
|
|
14414
|
+
}
|
|
13786
14415
|
const legacyUnbounded = classification === "transient";
|
|
13787
14416
|
const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
|
|
13788
14417
|
const failedSelector = managedFallback ? controller.currentSelector() : undefined;
|
|
@@ -13791,12 +14420,14 @@ export class AgentSession {
|
|
|
13791
14420
|
: legacyUnbounded || attemptsUsed <= retrySettings.maxRetries
|
|
13792
14421
|
? "retry"
|
|
13793
14422
|
: "exhausted";
|
|
13794
|
-
|
|
13795
|
-
|
|
13796
|
-
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
14423
|
+
// Credential rotation is unbounded: a fresh credential is a different
|
|
14424
|
+
// retry dimension from transient-error backoff, so it overrides maxRetries
|
|
14425
|
+
// exhaustion and forces an immediate same-model retry.
|
|
14426
|
+
if (managedFallback && outcome === "advance" && (trigger.class === "quota" || trigger.class === "rate_limit")) {
|
|
14427
|
+
credentialRotated = await this.#markFailedCredential(trigger);
|
|
14428
|
+
}
|
|
14429
|
+
if (credentialRotated) {
|
|
14430
|
+
if (managedFallback) controller.restorePreviousEntryForRetry();
|
|
13800
14431
|
outcome = "retry";
|
|
13801
14432
|
}
|
|
13802
14433
|
if (outcome === "exhausted") {
|
|
@@ -13807,6 +14438,10 @@ export class AgentSession {
|
|
|
13807
14438
|
controller.resetSticky();
|
|
13808
14439
|
return managedOutcome ? this.#managedFallbackExhaustionDecision(message, errorMessage) : false;
|
|
13809
14440
|
}
|
|
14441
|
+
if (classification === "first_event_timeout") {
|
|
14442
|
+
const elapsedMs = Date.now() - (this.#firstEventTimeoutRetryStartedAt ?? Date.now());
|
|
14443
|
+
message.errorMessage = `First-event stream timeout exhausted after ${attemptsUsed} attempts; waited ${elapsedMs}ms total: ${message.errorMessage ?? "Unknown error"}`;
|
|
14444
|
+
}
|
|
13810
14445
|
return false;
|
|
13811
14446
|
}
|
|
13812
14447
|
|
|
@@ -13828,7 +14463,7 @@ export class AgentSession {
|
|
|
13828
14463
|
}
|
|
13829
14464
|
|
|
13830
14465
|
const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
|
|
13831
|
-
if (managedFallback && !credentialRotated) await this.#
|
|
14466
|
+
if (managedFallback && !credentialRotated) await this.#markFailedCredential(trigger);
|
|
13832
14467
|
let advanced = outcome !== "advance";
|
|
13833
14468
|
let resolutionError: unknown;
|
|
13834
14469
|
if (outcome === "advance") {
|
|
@@ -13870,7 +14505,12 @@ export class AgentSession {
|
|
|
13870
14505
|
await this.#emitSessionEvent({
|
|
13871
14506
|
type: "auto_retry_start",
|
|
13872
14507
|
attempt: this.#retryAttempt,
|
|
13873
|
-
maxAttempts:
|
|
14508
|
+
maxAttempts:
|
|
14509
|
+
classification === "first_event_timeout"
|
|
14510
|
+
? retrySettings.maxRetries + 1
|
|
14511
|
+
: managedFallback
|
|
14512
|
+
? controller.maxAttempts
|
|
14513
|
+
: retrySettings.maxRetries,
|
|
13874
14514
|
delayMs,
|
|
13875
14515
|
errorMessage,
|
|
13876
14516
|
unbounded: managedFallback ? false : legacyUnbounded,
|
|
@@ -13878,9 +14518,19 @@ export class AgentSession {
|
|
|
13878
14518
|
|
|
13879
14519
|
const messages = this.agent.state.messages;
|
|
13880
14520
|
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
13881
|
-
|
|
14521
|
+
let end = messages.length - 1;
|
|
14522
|
+
// A wedged turn can leave more than one assistant tail behind (e.g. the
|
|
14523
|
+
// agent loop's invalid_prompt repair resend commits its own rejected
|
|
14524
|
+
// message). agent.continue() refuses ANY assistant tail, so strip the
|
|
14525
|
+
// whole trailing run of failed attempts, not just the last one.
|
|
14526
|
+
while (end > 0) {
|
|
14527
|
+
const previous = messages[end - 1];
|
|
14528
|
+
if (previous.role !== "assistant") break;
|
|
14529
|
+
if (previous.stopReason !== "error" && previous.stopReason !== "aborted") break;
|
|
14530
|
+
end--;
|
|
14531
|
+
}
|
|
14532
|
+
this.agent.replaceMessages(messages.slice(0, end));
|
|
13882
14533
|
}
|
|
13883
|
-
|
|
13884
14534
|
try {
|
|
13885
14535
|
await scheduler.wait(delayMs, { signal: retryAbortController.signal });
|
|
13886
14536
|
} catch {
|
|
@@ -14120,12 +14770,8 @@ export class AgentSession {
|
|
|
14120
14770
|
// Bash Execution
|
|
14121
14771
|
// =========================================================================
|
|
14122
14772
|
|
|
14123
|
-
async #saveBashOriginalArtifact(originalText: string): Promise<
|
|
14124
|
-
|
|
14125
|
-
return await this.sessionManager.saveArtifact(originalText, "bash-original");
|
|
14126
|
-
} catch {
|
|
14127
|
-
return undefined;
|
|
14128
|
-
}
|
|
14773
|
+
async #saveBashOriginalArtifact(originalText: string): Promise<BashArtifactSaveResult> {
|
|
14774
|
+
return saveAgentBashOriginalArtifact(this.sessionManager, originalText);
|
|
14129
14775
|
}
|
|
14130
14776
|
|
|
14131
14777
|
/**
|
|
@@ -15119,7 +15765,7 @@ export class AgentSession {
|
|
|
15119
15765
|
// workflow-gate emitter, and hooks cannot resolve against an ungated
|
|
15120
15766
|
// root. The manager-rotation window itself is tracked in #3138
|
|
15121
15767
|
// (#2797 / #2925).
|
|
15122
|
-
if (switchingToDifferentSession) await
|
|
15768
|
+
if (switchingToDifferentSession) await this.#initializeLocalRootForLoadedSession();
|
|
15123
15769
|
this.#syncAgentSessionId();
|
|
15124
15770
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
15125
15771
|
|
|
@@ -15141,9 +15787,13 @@ export class AgentSession {
|
|
|
15141
15787
|
this.#rebindProviderSessionState(new Map());
|
|
15142
15788
|
}
|
|
15143
15789
|
|
|
15790
|
+
const resumeModelBehavior = this.settings.get("session.resumeModelBehavior");
|
|
15144
15791
|
const configuredDefaultChain = sessionContext.configuredModelChains.default?.entries;
|
|
15792
|
+
const settingsDefaultEntries = normalizeModelSelectorValue(this.settings.getModelRole("default"));
|
|
15145
15793
|
const defaultEntries =
|
|
15146
|
-
|
|
15794
|
+
resumeModelBehavior === "useCurrentDefault"
|
|
15795
|
+
? settingsDefaultEntries
|
|
15796
|
+
: (configuredDefaultChain ?? (sessionContext.models.default ? [sessionContext.models.default] : []));
|
|
15147
15797
|
this.#defaultFallbackController = undefined;
|
|
15148
15798
|
if (defaultEntries.length > 0) {
|
|
15149
15799
|
const resolution = await resolveModelChainWithAuth(
|
|
@@ -15200,7 +15850,7 @@ export class AgentSession {
|
|
|
15200
15850
|
// Establish the successor's durable session identity only after every
|
|
15201
15851
|
// restored state facet is live. Identity-bound extension hooks run below.
|
|
15202
15852
|
await this.sessionManager.ensureOnDisk();
|
|
15203
|
-
if (!switchingToDifferentSession) await
|
|
15853
|
+
if (!switchingToDifferentSession) await this.#initializeLocalRootForLoadedSession();
|
|
15204
15854
|
|
|
15205
15855
|
if (switchingToDifferentSession) {
|
|
15206
15856
|
// The local:// migration gate for this successor already ran above,
|