@gajae-code/coding-agent 0.12.0 → 0.12.2
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 +111 -0
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/config/settings-schema.d.ts +56 -2
- 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-review-conflicts.d.ts +114 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +23 -3
- 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 +37 -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/irc-sidebar.d.ts +16 -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/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +1 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +32 -0
- 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 +5 -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 +12 -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 +40 -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 +30 -4
- 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 +7 -1
- package/dist/types/task/output-manager.d.ts +1 -0
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +9 -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 +6 -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/dogfood-ralplan-review-conflicts.ts +272 -0
- package/scripts/generate-sdk-operation-inventory.ts +5 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +290 -0
- package/src/async/job-manager.ts +99 -39
- 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 +53 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/debug/index.ts +42 -17
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -0
- package/src/defaults/gjc/skills/ralplan/SKILL.md +25 -20
- package/src/defaults/gjc/skills/team/SKILL.md +2 -0
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +39 -26
- 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/launch-worktree.ts +103 -1
- package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
- package/src/gjc-runtime/ralplan-runtime.ts +491 -27
- 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-evidence.ts +6 -74
- package/src/gjc-runtime/ultragoal-runtime.ts +635 -612
- package/src/gjc-runtime/workflow-manifest.generated.json +37 -99
- package/src/gjc-runtime/workflow-manifest.ts +21 -70
- package/src/internal-urls/docs-index.generated.ts +16 -16
- package/src/modes/DESIGN.md +104 -0
- package/src/modes/acp/acp-agent.ts +298 -186
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/irc-sidebar.ts +30 -1
- 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 -15
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +42 -34
- package/src/modes/controllers/event-controller.ts +27 -16
- package/src/modes/controllers/extension-ui-controller.ts +193 -34
- package/src/modes/controllers/selector-controller.ts +160 -22
- package/src/modes/interactive-mode.ts +189 -62
- package/src/modes/jobs-observer.ts +1 -1
- package/src/modes/runtime-init.ts +3 -0
- package/src/modes/theme/theme.ts +11 -9
- package/src/modes/types.ts +88 -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/lifecycle.ts +18 -13
- 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 +7 -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 +12 -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 +33 -0
- package/src/sdk/session.ts +89 -9
- package/src/session/agent-session.ts +844 -173
- 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 +200 -111
- package/src/session/internal/managed-session-storage.ts +492 -62
- package/src/session/session-manager.ts +1614 -435
- package/src/session/session-storage.ts +38 -45
- 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 +280 -45
- package/src/task/output-manager.ts +24 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +34 -8
- 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 +6 -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;
|
|
@@ -1788,6 +1884,7 @@ export class AgentSession {
|
|
|
1788
1884
|
#providerCacheSessionId: string | undefined;
|
|
1789
1885
|
#isDisposed = false;
|
|
1790
1886
|
#disposePromise: Promise<void> | undefined;
|
|
1887
|
+
readonly #toolSessionCleanups = new Set<() => Promise<void> | void>();
|
|
1791
1888
|
#newSessionTransition: Promise<boolean> | undefined;
|
|
1792
1889
|
// Extension system
|
|
1793
1890
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
@@ -1795,6 +1892,10 @@ export class AgentSession {
|
|
|
1795
1892
|
#turnIndex = 0;
|
|
1796
1893
|
#workerIntegrationScheduler: WorkerIntegrationRequestScheduler;
|
|
1797
1894
|
#workerIntegrationRequestedForTurn = false;
|
|
1895
|
+
/** Runtime-owned team worker heartbeat; `undefined` outside a `gjc team` worker pane. */
|
|
1896
|
+
#teamWorkerHeartbeat: GjcTeamWorkerHeartbeatReporter | undefined;
|
|
1897
|
+
#teamWorkerTurnsInFlight = 0;
|
|
1898
|
+
#unregisterTeamWorkerAsyncJobChange: (() => void) | undefined;
|
|
1798
1899
|
// First-party internal before-agent-start contributors (not user hooks).
|
|
1799
1900
|
#beforeAgentStartContributors: BeforeAgentStartContributor[] = [];
|
|
1800
1901
|
|
|
@@ -2149,6 +2250,8 @@ export class AgentSession {
|
|
|
2149
2250
|
this.#promptInFlightCount++;
|
|
2150
2251
|
if (this.#promptInFlightCount === 1) {
|
|
2151
2252
|
this.#acquirePowerAssertion();
|
|
2253
|
+
// Runtime-owned worker liveness follows Agent turns and background jobs
|
|
2254
|
+
// in #refreshTeamWorkerHeartbeat(), including internally dispatched turns.
|
|
2152
2255
|
}
|
|
2153
2256
|
}
|
|
2154
2257
|
|
|
@@ -2205,6 +2308,15 @@ export class AgentSession {
|
|
|
2205
2308
|
this.#flushPendingAgentEnd();
|
|
2206
2309
|
}
|
|
2207
2310
|
|
|
2311
|
+
#retainDeferredAgentEndAfterContinuationBusy(
|
|
2312
|
+
hold: symbol | undefined,
|
|
2313
|
+
pending: AgentSessionEvent | undefined,
|
|
2314
|
+
): void {
|
|
2315
|
+
if (!hold || !pending) return;
|
|
2316
|
+
this.#pendingAgentEndEmit = pending;
|
|
2317
|
+
this.#pendingAgentEndContinuationHolds.set(hold, pending);
|
|
2318
|
+
}
|
|
2319
|
+
|
|
2208
2320
|
#releaseDeferredAgentEndContinuation(hold: symbol | undefined): void {
|
|
2209
2321
|
if (!hold) return;
|
|
2210
2322
|
const pending = this.#pendingAgentEndContinuationHolds.get(hold);
|
|
@@ -2255,11 +2367,23 @@ export class AgentSession {
|
|
|
2255
2367
|
this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
|
|
2256
2368
|
if (this.#promptInFlightCount === 0) {
|
|
2257
2369
|
this.#releasePowerAssertion();
|
|
2370
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
2258
2371
|
this.#flushPendingBackgroundExchanges();
|
|
2259
2372
|
this.#flushPendingAgentEnd();
|
|
2260
2373
|
}
|
|
2261
2374
|
}
|
|
2262
2375
|
|
|
2376
|
+
#refreshTeamWorkerHeartbeat(): void {
|
|
2377
|
+
const hasOwnedBackgroundJob =
|
|
2378
|
+
this.#agentId !== undefined &&
|
|
2379
|
+
(this.#ownedAsyncJobManager?.getRunningJobs({ ownerId: this.#agentId }).length ?? 0) > 0;
|
|
2380
|
+
if (this.#teamWorkerTurnsInFlight > 0 || hasOwnedBackgroundJob) {
|
|
2381
|
+
this.#teamWorkerHeartbeat?.start();
|
|
2382
|
+
} else {
|
|
2383
|
+
this.#teamWorkerHeartbeat?.stop();
|
|
2384
|
+
}
|
|
2385
|
+
}
|
|
2386
|
+
|
|
2263
2387
|
#flushPendingAgentEnd(): void {
|
|
2264
2388
|
if (
|
|
2265
2389
|
this.#promptInFlightCount > 0 ||
|
|
@@ -2311,6 +2435,14 @@ export class AgentSession {
|
|
|
2311
2435
|
}),
|
|
2312
2436
|
config.workerIntegrationTimeoutMs,
|
|
2313
2437
|
);
|
|
2438
|
+
// One publisher per worker process: subagent sessions share this process and its
|
|
2439
|
+
// team env, so letting each create a reporter would run N timers writing the same
|
|
2440
|
+
// record. The top-level session owns the pane's liveness.
|
|
2441
|
+
this.#teamWorkerHeartbeat =
|
|
2442
|
+
config.teamWorkerHeartbeatReporter ??
|
|
2443
|
+
((config.taskDepth ?? 0) === 0
|
|
2444
|
+
? GjcTeamWorkerHeartbeatReporter.forProcess(() => this.sessionManager.getCwd())
|
|
2445
|
+
: undefined);
|
|
2314
2446
|
this.notificationSessionController = config.notificationSessionController;
|
|
2315
2447
|
this.taskDepth = config.taskDepth ?? 0;
|
|
2316
2448
|
// Register this session with the process-wide resource GC (idle/RSS browser-tab eviction
|
|
@@ -2472,6 +2604,9 @@ export class AgentSession {
|
|
|
2472
2604
|
this.#ttsrManager = config.ttsrManager;
|
|
2473
2605
|
this.#obfuscator = config.obfuscator;
|
|
2474
2606
|
this.#agentId = config.agentId;
|
|
2607
|
+
this.#unregisterTeamWorkerAsyncJobChange = this.#ownedAsyncJobManager?.onChange(() => {
|
|
2608
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
2609
|
+
});
|
|
2475
2610
|
this.#agentRegistry = config.agentRegistry;
|
|
2476
2611
|
this.#providerSessionId = config.providerSessionId;
|
|
2477
2612
|
this.#providerCacheSessionId = config.providerCacheSessionId;
|
|
@@ -2566,7 +2701,24 @@ export class AgentSession {
|
|
|
2566
2701
|
|
|
2567
2702
|
/** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */
|
|
2568
2703
|
nextToolChoice(): ToolChoice | undefined {
|
|
2569
|
-
|
|
2704
|
+
const choice = this.#toolChoiceQueue.nextToolChoice();
|
|
2705
|
+
if (!choice || typeof choice === "string") return choice;
|
|
2706
|
+
|
|
2707
|
+
const toolName = choice.type === "tool" ? choice.name : "function" in choice ? choice.function.name : choice.name;
|
|
2708
|
+
const activeTool = this.agent.state.tools.find(
|
|
2709
|
+
tool => tool.name === toolName || tool.customWireName === toolName,
|
|
2710
|
+
);
|
|
2711
|
+
if (!activeTool) {
|
|
2712
|
+
this.#toolChoiceQueue.degradeInFlight(`Tool "${toolName}" is no longer active.`);
|
|
2713
|
+
return undefined;
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
const refreshed = buildNamedToolChoiceResult(activeTool.name, this.model);
|
|
2717
|
+
if (!refreshed.exactNamed || !refreshed.choice) {
|
|
2718
|
+
this.#toolChoiceQueue.degradeInFlight(refreshed.resolved?.reason ?? "Named tool choice is unavailable.");
|
|
2719
|
+
return undefined;
|
|
2720
|
+
}
|
|
2721
|
+
return refreshed.choice;
|
|
2570
2722
|
}
|
|
2571
2723
|
|
|
2572
2724
|
/**
|
|
@@ -3082,6 +3234,21 @@ export class AgentSession {
|
|
|
3082
3234
|
return this.#agentId;
|
|
3083
3235
|
}
|
|
3084
3236
|
|
|
3237
|
+
registerToolSessionCleanup(cleanup: () => Promise<void> | void): () => void {
|
|
3238
|
+
if (this.#isDisposed) throw new Error("Cannot register tool cleanup after session disposal has started.");
|
|
3239
|
+
this.#toolSessionCleanups.add(cleanup);
|
|
3240
|
+
return () => this.#toolSessionCleanups.delete(cleanup);
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
async #runToolSessionCleanups(): Promise<void> {
|
|
3244
|
+
const cleanups = Array.from(this.#toolSessionCleanups);
|
|
3245
|
+
this.#toolSessionCleanups.clear();
|
|
3246
|
+
const results = await Promise.allSettled(cleanups.map(async cleanup => await cleanup()));
|
|
3247
|
+
for (const result of results) {
|
|
3248
|
+
if (result.status === "rejected") logger.warn("Tool session cleanup failed", { error: String(result.reason) });
|
|
3249
|
+
}
|
|
3250
|
+
}
|
|
3251
|
+
|
|
3085
3252
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
3086
3253
|
const manager = AsyncJobManager.instance();
|
|
3087
3254
|
if (!manager) return null;
|
|
@@ -3196,19 +3363,26 @@ export class AgentSession {
|
|
|
3196
3363
|
return queued;
|
|
3197
3364
|
}
|
|
3198
3365
|
|
|
3199
|
-
#trackAgentEvent =
|
|
3366
|
+
#trackAgentEvent = (event: AgentEvent): Promise<void> => {
|
|
3367
|
+
const activePromptHandle = this.activePromptHandle;
|
|
3200
3368
|
const agentEndHandled = event.type === "agent_end" ? Promise.withResolvers<void>() : undefined;
|
|
3201
3369
|
if (agentEndHandled) this.#agentEndHandlingPromise = agentEndHandled.promise;
|
|
3202
3370
|
this.#agentEventHandlersInFlight++;
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3371
|
+
const handler = (async (): Promise<void> => {
|
|
3372
|
+
try {
|
|
3373
|
+
await this.#handleAgentEvent(event, activePromptHandle);
|
|
3374
|
+
} catch (error) {
|
|
3375
|
+
logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
|
|
3376
|
+
} finally {
|
|
3377
|
+
this.#agentEventHandlersInFlight = Math.max(0, this.#agentEventHandlersInFlight - 1);
|
|
3378
|
+
this.#flushPendingAgentEnd();
|
|
3379
|
+
agentEndHandled?.resolve();
|
|
3380
|
+
}
|
|
3381
|
+
})();
|
|
3382
|
+
if (activePromptHandle) {
|
|
3383
|
+
this.agent.resourceLedger.track(activePromptHandle, "post_prompt", "agent-session-event", handler);
|
|
3211
3384
|
}
|
|
3385
|
+
return handler;
|
|
3212
3386
|
};
|
|
3213
3387
|
|
|
3214
3388
|
#persistRuntimeStateInBackground(event: AgentSessionEvent): void {
|
|
@@ -3228,6 +3402,18 @@ export class AgentSession {
|
|
|
3228
3402
|
} else if (event.type === "turn_end") {
|
|
3229
3403
|
this.#closedExtensionTurnGeneration = this.#extensionTurnGeneration;
|
|
3230
3404
|
}
|
|
3405
|
+
if (event.type === "turn_start") {
|
|
3406
|
+
this.#teamWorkerTurnsInFlight++;
|
|
3407
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
3408
|
+
} else if (event.type === "turn_end") {
|
|
3409
|
+
this.#teamWorkerTurnsInFlight = Math.max(0, this.#teamWorkerTurnsInFlight - 1);
|
|
3410
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
3411
|
+
} else if (event.type === "agent_end") {
|
|
3412
|
+
// Failed Agent runs can emit agent_end without turn_end. Reset rather than
|
|
3413
|
+
// leaking a positive count that would report an idle/failed worker forever.
|
|
3414
|
+
this.#teamWorkerTurnsInFlight = 0;
|
|
3415
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
3416
|
+
}
|
|
3231
3417
|
if (event.type === "message_update") {
|
|
3232
3418
|
// Fast path: message_update maps to no sidecar state, so we must not
|
|
3233
3419
|
// build the persistRuntimeState closure here (per-token hot path).
|
|
@@ -3347,7 +3533,7 @@ export class AgentSession {
|
|
|
3347
3533
|
}
|
|
3348
3534
|
|
|
3349
3535
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
3350
|
-
#handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
3536
|
+
#handleAgentEvent = async (event: AgentEvent, activePromptHandle?: string): Promise<void> => {
|
|
3351
3537
|
if (this.#extensionRunner?.hasHandlers(event.type)) this.#markRetryReplayUnsafe();
|
|
3352
3538
|
if (
|
|
3353
3539
|
event.type === "tool_execution_start" ||
|
|
@@ -3696,9 +3882,10 @@ export class AgentSession {
|
|
|
3696
3882
|
this.#ttsrAbortPending = false;
|
|
3697
3883
|
this.#resolveTtsrResume();
|
|
3698
3884
|
},
|
|
3885
|
+
resourceRunId: activePromptHandle,
|
|
3699
3886
|
});
|
|
3700
3887
|
},
|
|
3701
|
-
{ delayMs: 50 },
|
|
3888
|
+
{ delayMs: 50, resourceRunId: activePromptHandle },
|
|
3702
3889
|
);
|
|
3703
3890
|
return;
|
|
3704
3891
|
}
|
|
@@ -3857,7 +4044,11 @@ export class AgentSession {
|
|
|
3857
4044
|
maintenanceGeneration !== undefined &&
|
|
3858
4045
|
this.#promptGeneration === maintenanceGeneration
|
|
3859
4046
|
) {
|
|
3860
|
-
this.#scheduleAgentContinue({
|
|
4047
|
+
this.#scheduleAgentContinue({
|
|
4048
|
+
generation: maintenanceGeneration,
|
|
4049
|
+
skipCompactionCheck: true,
|
|
4050
|
+
resourceRunId: activePromptHandle,
|
|
4051
|
+
});
|
|
3861
4052
|
}
|
|
3862
4053
|
return;
|
|
3863
4054
|
}
|
|
@@ -3937,8 +4128,11 @@ export class AgentSession {
|
|
|
3937
4128
|
}
|
|
3938
4129
|
this.#resolveRetry();
|
|
3939
4130
|
|
|
3940
|
-
const compactionTask = this.#checkCompaction(msg);
|
|
3941
|
-
this.#trackPostPromptTask(
|
|
4131
|
+
const compactionTask = this.#checkCompaction(msg, true, undefined, activePromptHandle);
|
|
4132
|
+
this.#trackPostPromptTask(
|
|
4133
|
+
compactionTask.then(() => undefined),
|
|
4134
|
+
activePromptHandle,
|
|
4135
|
+
);
|
|
3942
4136
|
await compactionTask;
|
|
3943
4137
|
// Check for incomplete todos only after a final assistant stop, not intermediate tool-use turns.
|
|
3944
4138
|
const hasToolCalls = msg.content.some(content => content.type === "toolCall");
|
|
@@ -4002,9 +4196,12 @@ export class AgentSession {
|
|
|
4002
4196
|
this.#postPromptTasksPromise = undefined;
|
|
4003
4197
|
}
|
|
4004
4198
|
|
|
4005
|
-
#trackPostPromptTask(task: Promise<void
|
|
4199
|
+
#trackPostPromptTask(task: Promise<void>, resourceRunId = this.activePromptHandle): void {
|
|
4006
4200
|
this.#postPromptTasks.add(task);
|
|
4007
4201
|
this.#ensurePostPromptTasksPromise();
|
|
4202
|
+
if (resourceRunId) {
|
|
4203
|
+
this.agent.resourceLedger.track(resourceRunId, "post_prompt", "agent-session", task);
|
|
4204
|
+
}
|
|
4008
4205
|
void task
|
|
4009
4206
|
.catch(() => {})
|
|
4010
4207
|
.finally(() => {
|
|
@@ -4017,7 +4214,7 @@ export class AgentSession {
|
|
|
4017
4214
|
|
|
4018
4215
|
#schedulePostPromptTask(
|
|
4019
4216
|
task: (signal: AbortSignal) => Promise<void>,
|
|
4020
|
-
options?: { delayMs?: number; generation?: number; onSkip?: () => void },
|
|
4217
|
+
options?: { delayMs?: number; generation?: number; onSkip?: () => void; resourceRunId?: string },
|
|
4021
4218
|
): Promise<void> {
|
|
4022
4219
|
const delayMs = options?.delayMs ?? 0;
|
|
4023
4220
|
const signal = this.#postPromptTasksAbortController.signal;
|
|
@@ -4040,10 +4237,14 @@ export class AgentSession {
|
|
|
4040
4237
|
}
|
|
4041
4238
|
await task(signal);
|
|
4042
4239
|
})();
|
|
4043
|
-
this.#trackPostPromptTask(scheduled);
|
|
4240
|
+
this.#trackPostPromptTask(scheduled, options?.resourceRunId);
|
|
4044
4241
|
return scheduled;
|
|
4045
4242
|
}
|
|
4046
4243
|
|
|
4244
|
+
#isAgentBusyError(error: unknown): error is AgentBusyError {
|
|
4245
|
+
return error instanceof AgentBusyError;
|
|
4246
|
+
}
|
|
4247
|
+
|
|
4047
4248
|
#scheduleAgentContinue(options?: {
|
|
4048
4249
|
delayMs?: number;
|
|
4049
4250
|
generation?: number;
|
|
@@ -4052,7 +4253,9 @@ export class AgentSession {
|
|
|
4052
4253
|
shouldContinue?: () => boolean;
|
|
4053
4254
|
onSkip?: (reason: "generation_changed" | "aborted_signal" | "queue_drained" | "handoff_in_progress") => void;
|
|
4054
4255
|
allowDuringCancelAndSubmit?: boolean;
|
|
4256
|
+
rescheduleOnBusy?: boolean;
|
|
4055
4257
|
onError?: (error: unknown) => void;
|
|
4258
|
+
resourceRunId?: string;
|
|
4056
4259
|
}): Promise<void> {
|
|
4057
4260
|
const predecessorAgentEndHold = options?.suppressPredecessorAgentEnd
|
|
4058
4261
|
? this.#reserveDeferredAgentEndForContinuation()
|
|
@@ -4075,81 +4278,92 @@ export class AgentSession {
|
|
|
4075
4278
|
};
|
|
4076
4279
|
const scheduledGeneration = options?.generation;
|
|
4077
4280
|
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();
|
|
4281
|
+
const scheduleAttempt = (delayMs = options?.delayMs): Promise<void> =>
|
|
4282
|
+
this.#schedulePostPromptTask(
|
|
4283
|
+
async () => {
|
|
4284
|
+
const canContinue = (): boolean => {
|
|
4285
|
+
if (signal.aborted || this.#isDisposed) {
|
|
4286
|
+
skip("aborted_signal");
|
|
4287
|
+
return false;
|
|
4288
|
+
}
|
|
4289
|
+
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
4290
|
+
skip("generation_changed");
|
|
4291
|
+
return false;
|
|
4292
|
+
}
|
|
4293
|
+
if (this.#cancelAndSubmitInProgress && !options?.allowDuringCancelAndSubmit) {
|
|
4294
|
+
skip("queue_drained");
|
|
4295
|
+
return false;
|
|
4296
|
+
}
|
|
4297
|
+
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
4298
|
+
skip("queue_drained");
|
|
4299
|
+
return false;
|
|
4300
|
+
}
|
|
4301
|
+
return true;
|
|
4302
|
+
};
|
|
4303
|
+
if (!canContinue()) return;
|
|
4127
4304
|
try {
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
4131
|
-
|
|
4132
|
-
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
}
|
|
4305
|
+
if (!options?.skipCompactionCheck) {
|
|
4306
|
+
await this.#checkEstimatedContextBeforePrompt();
|
|
4307
|
+
if (!canContinue()) return;
|
|
4308
|
+
}
|
|
4309
|
+
if (signal.aborted) {
|
|
4310
|
+
skip("aborted_signal");
|
|
4311
|
+
return;
|
|
4312
|
+
}
|
|
4313
|
+
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
4314
|
+
skip("generation_changed");
|
|
4315
|
+
return;
|
|
4316
|
+
}
|
|
4317
|
+
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
4318
|
+
skip("queue_drained");
|
|
4319
|
+
return;
|
|
4320
|
+
}
|
|
4321
|
+
// A continuation scheduled before a handoff engaged must not start a
|
|
4322
|
+
// turn against the session being handed off (or the restored
|
|
4323
|
+
// predecessor). rearmIdle / normal delivery resumes after the fence.
|
|
4324
|
+
if (this.#handoffTransitionActive) {
|
|
4325
|
+
skip("handoff_in_progress");
|
|
4326
|
+
return;
|
|
4327
|
+
}
|
|
4328
|
+
const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
|
|
4329
|
+
const startsQueuedSuccessor =
|
|
4330
|
+
this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
|
|
4331
|
+
try {
|
|
4332
|
+
await this.agent.continue({
|
|
4333
|
+
...this.#managedFallbackPromptOptions(),
|
|
4334
|
+
// Reset only after continue() has claimed the queued turn. Skipped or stale
|
|
4335
|
+
// continuations retain predecessor accounting, and resetAttemptBudget keeps
|
|
4336
|
+
// the sticky fallback cursor unchanged.
|
|
4337
|
+
onRunAccepted: startsQueuedSuccessor
|
|
4338
|
+
? () => {
|
|
4339
|
+
this.#defaultFallbackChain().resetAttemptBudget();
|
|
4340
|
+
this.#overflowMaintenanceAttempts = 0;
|
|
4341
|
+
}
|
|
4342
|
+
: undefined,
|
|
4343
|
+
});
|
|
4344
|
+
} catch (error) {
|
|
4345
|
+
if (options?.rescheduleOnBusy && this.#isAgentBusyError(error) && !terminalized && canContinue()) {
|
|
4346
|
+
this.#retainDeferredAgentEndAfterContinuationBusy(predecessorAgentEndHold, predecessorAgentEnd);
|
|
4347
|
+
logger.debug("agent.continue busy after scheduling; rescheduling", {
|
|
4348
|
+
error: error.message,
|
|
4349
|
+
});
|
|
4350
|
+
void scheduleAttempt(100);
|
|
4351
|
+
return;
|
|
4352
|
+
}
|
|
4353
|
+
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
4354
|
+
throw error;
|
|
4355
|
+
}
|
|
4140
4356
|
} catch (error) {
|
|
4141
|
-
|
|
4142
|
-
throw error;
|
|
4357
|
+
fail(error);
|
|
4143
4358
|
}
|
|
4144
|
-
}
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
);
|
|
4359
|
+
},
|
|
4360
|
+
{
|
|
4361
|
+
delayMs,
|
|
4362
|
+
onSkip: () => skip("aborted_signal"),
|
|
4363
|
+
resourceRunId: options?.resourceRunId,
|
|
4364
|
+
},
|
|
4365
|
+
);
|
|
4366
|
+
return scheduleAttempt();
|
|
4153
4367
|
}
|
|
4154
4368
|
|
|
4155
4369
|
#logCompactionContinuationSkipped(
|
|
@@ -4165,7 +4379,7 @@ export class AgentSession {
|
|
|
4165
4379
|
): void {
|
|
4166
4380
|
logger.warn("Auto-compaction continuation failed", {
|
|
4167
4381
|
source,
|
|
4168
|
-
reason:
|
|
4382
|
+
reason: "not_resumable_tail",
|
|
4169
4383
|
error: error instanceof Error ? error.message : String(error),
|
|
4170
4384
|
});
|
|
4171
4385
|
}
|
|
@@ -4193,14 +4407,24 @@ export class AgentSession {
|
|
|
4193
4407
|
return compactionSettings.autoContinue === false ? "auto_continue_disabled_non_resumable_tail" : undefined;
|
|
4194
4408
|
}
|
|
4195
4409
|
|
|
4196
|
-
#scheduleOverflowRetryContinuation(generation: number): boolean {
|
|
4410
|
+
async #scheduleOverflowRetryContinuation(generation: number, resourceRunId?: string): Promise<boolean> {
|
|
4197
4411
|
this.#stripOverflowFailedTurnForRetry();
|
|
4412
|
+
const snapshot = await this.#compactionStateSnapshot();
|
|
4413
|
+
if (
|
|
4414
|
+
snapshot.goal?.status === "paused" &&
|
|
4415
|
+
!snapshot.queuedMessages &&
|
|
4416
|
+
snapshot.lastAssistantStopReason !== "length"
|
|
4417
|
+
) {
|
|
4418
|
+
this.#logCompactionContinuationSkipped("overflow_retry", "paused_goal");
|
|
4419
|
+
return false;
|
|
4420
|
+
}
|
|
4198
4421
|
if (this.#isResumableAgentTail()) {
|
|
4199
4422
|
this.#scheduleAgentContinue({
|
|
4200
4423
|
delayMs: 100,
|
|
4201
4424
|
generation,
|
|
4202
4425
|
suppressPredecessorAgentEnd: true,
|
|
4203
|
-
|
|
4426
|
+
resourceRunId,
|
|
4427
|
+
rescheduleOnBusy: true,
|
|
4204
4428
|
onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
|
|
4205
4429
|
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
4206
4430
|
});
|
|
@@ -4209,7 +4433,7 @@ export class AgentSession {
|
|
|
4209
4433
|
|
|
4210
4434
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
4211
4435
|
if (compactionSettings.autoContinue !== false) {
|
|
4212
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
4436
|
+
this.#scheduleAutoContinuePrompt(generation, false);
|
|
4213
4437
|
return true;
|
|
4214
4438
|
}
|
|
4215
4439
|
|
|
@@ -4217,8 +4441,32 @@ export class AgentSession {
|
|
|
4217
4441
|
return false;
|
|
4218
4442
|
}
|
|
4219
4443
|
|
|
4220
|
-
#scheduleAutoContinuePrompt(generation: number): void {
|
|
4444
|
+
#scheduleAutoContinuePrompt(generation: number, requireUnfinishedWork = true): void {
|
|
4221
4445
|
const predecessorAgentEndHold = this.#reserveDeferredAgentEndForContinuation();
|
|
4446
|
+
const scheduledGeneration = generation;
|
|
4447
|
+
const signal = this.#postPromptTasksAbortController.signal;
|
|
4448
|
+
const continuationAuthorized = async (hasPendingNextTurnMessages = false): Promise<boolean> => {
|
|
4449
|
+
const snapshot = await this.#compactionStateSnapshot();
|
|
4450
|
+
if (signal.aborted) {
|
|
4451
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
4452
|
+
return false;
|
|
4453
|
+
}
|
|
4454
|
+
if (this.#isDisposed || this.#promptGeneration !== scheduledGeneration) {
|
|
4455
|
+
this.#logCompactionContinuationSkipped(
|
|
4456
|
+
"auto_continue_prompt",
|
|
4457
|
+
this.#isDisposed ? "session_disposed" : "generation_changed",
|
|
4458
|
+
);
|
|
4459
|
+
return false;
|
|
4460
|
+
}
|
|
4461
|
+
const authorized = requireUnfinishedWork
|
|
4462
|
+
? this.#hasUnfinishedWork(snapshot) || hasPendingNextTurnMessages
|
|
4463
|
+
: snapshot.queuedMessages ||
|
|
4464
|
+
hasPendingNextTurnMessages ||
|
|
4465
|
+
snapshot.lastAssistantStopReason === "length" ||
|
|
4466
|
+
snapshot.goal?.status !== "paused";
|
|
4467
|
+
if (!authorized) this.emitNotice("info", "Auto-continue skipped: no unfinished work detected");
|
|
4468
|
+
return authorized;
|
|
4469
|
+
};
|
|
4222
4470
|
const continuePrompt = async () => {
|
|
4223
4471
|
await this.#promptWithMessage(
|
|
4224
4472
|
{
|
|
@@ -4232,14 +4480,15 @@ export class AgentSession {
|
|
|
4232
4480
|
skipPostPromptRecoveryWait: true,
|
|
4233
4481
|
skipCompactionCheck: true,
|
|
4234
4482
|
predecessorAgentEndHold,
|
|
4483
|
+
onFinalPreflight: ({ hasPendingNextTurnMessages }) => continuationAuthorized(hasPendingNextTurnMessages),
|
|
4235
4484
|
},
|
|
4236
4485
|
);
|
|
4237
4486
|
};
|
|
4238
|
-
const
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
(async () => {
|
|
4487
|
+
const scheduleAttempt = (delayMs = 0) => {
|
|
4488
|
+
const attempt = (async () => {
|
|
4489
|
+
let rescheduled = false;
|
|
4242
4490
|
try {
|
|
4491
|
+
if (delayMs > 0) await scheduler.wait(delayMs, { signal });
|
|
4243
4492
|
await Promise.resolve();
|
|
4244
4493
|
if (signal.aborted) {
|
|
4245
4494
|
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
@@ -4249,14 +4498,31 @@ export class AgentSession {
|
|
|
4249
4498
|
this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
|
|
4250
4499
|
return;
|
|
4251
4500
|
}
|
|
4501
|
+
if (!(await continuationAuthorized())) return;
|
|
4252
4502
|
await continuePrompt();
|
|
4253
4503
|
} catch (error) {
|
|
4504
|
+
if (signal.aborted) {
|
|
4505
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
4506
|
+
return;
|
|
4507
|
+
}
|
|
4508
|
+
if (this.#isAgentBusyError(error) && this.#promptGeneration === scheduledGeneration) {
|
|
4509
|
+
logger.debug("Auto-compaction continuation busy; rescheduling", {
|
|
4510
|
+
source: "auto_continue_prompt",
|
|
4511
|
+
reason: "queue_drained",
|
|
4512
|
+
error: error.message,
|
|
4513
|
+
});
|
|
4514
|
+
rescheduled = true;
|
|
4515
|
+
scheduleAttempt(100);
|
|
4516
|
+
return;
|
|
4517
|
+
}
|
|
4254
4518
|
this.#logCompactionContinuationError("auto_continue_prompt", error);
|
|
4255
4519
|
} finally {
|
|
4256
|
-
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
4520
|
+
if (!rescheduled) this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
4257
4521
|
}
|
|
4258
|
-
})()
|
|
4259
|
-
|
|
4522
|
+
})();
|
|
4523
|
+
this.#trackPostPromptTask(attempt);
|
|
4524
|
+
};
|
|
4525
|
+
scheduleAttempt();
|
|
4260
4526
|
}
|
|
4261
4527
|
|
|
4262
4528
|
async #cancelPostPromptTasks(): Promise<void> {
|
|
@@ -4749,6 +5015,19 @@ export class AgentSession {
|
|
|
4749
5015
|
};
|
|
4750
5016
|
}
|
|
4751
5017
|
|
|
5018
|
+
/** Defer local:// provisioning when a read-only session load cannot create its artifact root. */
|
|
5019
|
+
async #initializeLocalRootForLoadedSession(): Promise<void> {
|
|
5020
|
+
try {
|
|
5021
|
+
await initializeLocalRoot(this.#localProtocolOptions());
|
|
5022
|
+
} catch (error) {
|
|
5023
|
+
if (!isEacces(error) && !hasFsCode(error, "EPERM") && !hasFsCode(error, "EROFS")) throw error;
|
|
5024
|
+
logger.debug("Deferred local root initialization for read-only session load", {
|
|
5025
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
5026
|
+
error: String(error),
|
|
5027
|
+
});
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
|
|
4752
5031
|
#maybeAbortStreamingEdit(event: AgentEvent, generation: number): void {
|
|
4753
5032
|
if (!this.settings.get("edit.streamingAbort")) return;
|
|
4754
5033
|
if (this.#streamingEditAbortTriggered) return;
|
|
@@ -5302,6 +5581,7 @@ export class AgentSession {
|
|
|
5302
5581
|
AsyncJobManager.setInstance(undefined);
|
|
5303
5582
|
}
|
|
5304
5583
|
}
|
|
5584
|
+
await this.#runToolSessionCleanups();
|
|
5305
5585
|
// Only disconnect the MCP manager THIS session owns (top-level sessions that
|
|
5306
5586
|
// connected plugin-bundle MCP servers). Subagents and callers that merely
|
|
5307
5587
|
// observe the process-global manager must never tear down a manager they do
|
|
@@ -5334,6 +5614,9 @@ export class AgentSession {
|
|
|
5334
5614
|
await disposeKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
5335
5615
|
await disposeVmContextsByOwner(this.#evalKernelOwnerId);
|
|
5336
5616
|
this.#releasePowerAssertion();
|
|
5617
|
+
this.#unregisterTeamWorkerAsyncJobChange?.();
|
|
5618
|
+
this.#unregisterTeamWorkerAsyncJobChange = undefined;
|
|
5619
|
+
this.#teamWorkerHeartbeat?.dispose();
|
|
5337
5620
|
// Disconnect the agent event listener BEFORE closing session resources so a late
|
|
5338
5621
|
// provider/tool message_end cannot append to the closing SessionManager.
|
|
5339
5622
|
this.#disconnectFromAgent();
|
|
@@ -5378,6 +5661,9 @@ export class AgentSession {
|
|
|
5378
5661
|
const kernelOwnerId = this.#evalKernelOwnerId;
|
|
5379
5662
|
this.#unregisterResourceGc?.();
|
|
5380
5663
|
this.#unregisterResourceGc = undefined;
|
|
5664
|
+
this.#unregisterTeamWorkerAsyncJobChange?.();
|
|
5665
|
+
this.#unregisterTeamWorkerAsyncJobChange = undefined;
|
|
5666
|
+
this.#teamWorkerHeartbeat?.dispose();
|
|
5381
5667
|
const work = Promise.allSettled([
|
|
5382
5668
|
// kill:true so a forced exit also reaps spawned-app Chrome we own (headless
|
|
5383
5669
|
// always closes; connected/attached browsers only disconnect — never killed).
|
|
@@ -6517,6 +6803,11 @@ export class AgentSession {
|
|
|
6517
6803
|
return this.#postPromptTasks.size > 0;
|
|
6518
6804
|
}
|
|
6519
6805
|
|
|
6806
|
+
/** Stable resource ownership identifier for the active prompt run. */
|
|
6807
|
+
get activePromptHandle(): string | undefined {
|
|
6808
|
+
return this.agent.activeResourceRunId;
|
|
6809
|
+
}
|
|
6810
|
+
|
|
6520
6811
|
/** All messages including custom types like BashExecutionMessage */
|
|
6521
6812
|
get messages(): AgentMessage[] {
|
|
6522
6813
|
return this.agent.state.messages;
|
|
@@ -7768,6 +8059,7 @@ export class AgentSession {
|
|
|
7768
8059
|
predecessorAgentEndHold?: symbol;
|
|
7769
8060
|
admissionLease?: SessionAdmissionLease;
|
|
7770
8061
|
onRunAccepted?: () => void;
|
|
8062
|
+
onFinalPreflight?: (context: { hasPendingNextTurnMessages: boolean }) => Promise<boolean>;
|
|
7771
8063
|
resetRetryReplaySafety?: boolean;
|
|
7772
8064
|
},
|
|
7773
8065
|
): Promise<void> {
|
|
@@ -7783,6 +8075,8 @@ export class AgentSession {
|
|
|
7783
8075
|
const generation = this.#promptGeneration;
|
|
7784
8076
|
const preflightSignal = this.#promptPreflightAbortController.signal;
|
|
7785
8077
|
const rosterClaim = this.#claimIrcRosterCandidate();
|
|
8078
|
+
let hasPendingNextTurnMessages = false;
|
|
8079
|
+
let pendingNextTurnMessageCount = 0;
|
|
7786
8080
|
let hindsightRecall: string | undefined;
|
|
7787
8081
|
try {
|
|
7788
8082
|
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
@@ -7874,11 +8168,11 @@ export class AgentSession {
|
|
|
7874
8168
|
}
|
|
7875
8169
|
|
|
7876
8170
|
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
7877
|
-
|
|
8171
|
+
pendingNextTurnMessageCount = this.#pendingNextTurnMessages.length;
|
|
8172
|
+
hasPendingNextTurnMessages = pendingNextTurnMessageCount > 0;
|
|
8173
|
+
for (const msg of this.#pendingNextTurnMessages.slice(0, pendingNextTurnMessageCount)) {
|
|
7878
8174
|
messages.push(msg);
|
|
7879
8175
|
}
|
|
7880
|
-
this.#pendingNextTurnMessages = [];
|
|
7881
|
-
if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
|
|
7882
8176
|
|
|
7883
8177
|
// Auto-read @filepath mentions
|
|
7884
8178
|
const fileMentions = extractFileMentions(expandedText);
|
|
@@ -7998,7 +8292,15 @@ export class AgentSession {
|
|
|
7998
8292
|
};
|
|
7999
8293
|
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
8000
8294
|
else options?.onPreflightAccepted?.();
|
|
8295
|
+
if (options?.onFinalPreflight && !(await options.onFinalPreflight({ hasPendingNextTurnMessages }))) {
|
|
8296
|
+
this.#resetInjectedContextSignatures();
|
|
8297
|
+
return;
|
|
8298
|
+
}
|
|
8001
8299
|
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
8300
|
+
if (pendingNextTurnMessageCount > 0) {
|
|
8301
|
+
this.#pendingNextTurnMessages.splice(0, pendingNextTurnMessageCount);
|
|
8302
|
+
}
|
|
8303
|
+
if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
|
|
8002
8304
|
await this.#promptAgentWithIdleRetry(messages, agentPromptOptions, predecessorAgentEndHold);
|
|
8003
8305
|
const terminalAssistant = this.#findLastAssistantMessage();
|
|
8004
8306
|
if (
|
|
@@ -8039,6 +8341,7 @@ export class AgentSession {
|
|
|
8039
8341
|
await this.#agentEndPublicationPromise;
|
|
8040
8342
|
} else {
|
|
8041
8343
|
await this.#agentEndHandlingPromise;
|
|
8344
|
+
await this.#waitForPostPromptRecovery();
|
|
8042
8345
|
await this.#agentEndPublicationPromise;
|
|
8043
8346
|
}
|
|
8044
8347
|
}
|
|
@@ -8087,6 +8390,7 @@ export class AgentSession {
|
|
|
8087
8390
|
sessionManager: createReadonlySessionManager(this.sessionManager),
|
|
8088
8391
|
modelRegistry: this.#modelRegistry,
|
|
8089
8392
|
model: this.model ?? undefined,
|
|
8393
|
+
getActivePromptHandle: () => this.activePromptHandle,
|
|
8090
8394
|
isIdle: () => !this.isStreaming,
|
|
8091
8395
|
abort: () => {
|
|
8092
8396
|
void this.abort();
|
|
@@ -8862,6 +9166,103 @@ export class AgentSession {
|
|
|
8862
9166
|
this.setTodoPhases(phases.filter(p => p.tasks.length > 0));
|
|
8863
9167
|
}
|
|
8864
9168
|
|
|
9169
|
+
async #compactionStateSnapshot(): Promise<CompactionStateSnapshot> {
|
|
9170
|
+
const snapshot: CompactionStateSnapshot = {
|
|
9171
|
+
goal: undefined,
|
|
9172
|
+
openTodos: [],
|
|
9173
|
+
activeSkills: [],
|
|
9174
|
+
queuedMessages: false,
|
|
9175
|
+
lastAssistantStopReason: undefined,
|
|
9176
|
+
};
|
|
9177
|
+
try {
|
|
9178
|
+
const goalState = this.getGoalModeState();
|
|
9179
|
+
const goal = goalState?.goal;
|
|
9180
|
+
if (goal) snapshot.goal = { objective: goal.objective, status: goal.status, enabled: goalState.enabled };
|
|
9181
|
+
} catch (error) {
|
|
9182
|
+
logger.warn("Failed to read goal state for compaction snapshot", {
|
|
9183
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9184
|
+
});
|
|
9185
|
+
}
|
|
9186
|
+
try {
|
|
9187
|
+
const todos = this.getTodoPhases()
|
|
9188
|
+
.flatMap(phase => phase.tasks)
|
|
9189
|
+
.filter(task => task.status === "pending" || task.status === "in_progress")
|
|
9190
|
+
.slice(0, 10)
|
|
9191
|
+
.map(task => task.content);
|
|
9192
|
+
snapshot.openTodos = todos;
|
|
9193
|
+
} catch (error) {
|
|
9194
|
+
logger.warn("Failed to read todo state for compaction snapshot", {
|
|
9195
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9196
|
+
});
|
|
9197
|
+
}
|
|
9198
|
+
try {
|
|
9199
|
+
const state = await readVisibleSkillActiveState(this.sessionManager.getCwd(), this.sessionId, {
|
|
9200
|
+
bypassCache: true,
|
|
9201
|
+
});
|
|
9202
|
+
snapshot.activeSkills = (state?.active_skills ?? [])
|
|
9203
|
+
.filter(entry => entry.active !== false)
|
|
9204
|
+
.slice(0, 5)
|
|
9205
|
+
.map(entry => ({ skill: entry.skill, phase: entry.phase ?? "unknown" }));
|
|
9206
|
+
} catch (error) {
|
|
9207
|
+
logger.warn("Failed to read workflow state for compaction snapshot", {
|
|
9208
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9209
|
+
});
|
|
9210
|
+
}
|
|
9211
|
+
try {
|
|
9212
|
+
snapshot.queuedMessages = this.agent.hasQueuedMessages() || this.#pendingNextTurnMessages.length > 0;
|
|
9213
|
+
} catch (error) {
|
|
9214
|
+
logger.warn("Failed to read queued-message state for compaction snapshot", {
|
|
9215
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9216
|
+
});
|
|
9217
|
+
}
|
|
9218
|
+
try {
|
|
9219
|
+
for (let index = this.messages.length - 1; index >= 0; index--) {
|
|
9220
|
+
const message = this.messages[index];
|
|
9221
|
+
if (message.role !== "assistant") continue;
|
|
9222
|
+
snapshot.lastAssistantStopReason = (message as AssistantMessage).stopReason;
|
|
9223
|
+
break;
|
|
9224
|
+
}
|
|
9225
|
+
} catch (error) {
|
|
9226
|
+
logger.warn("Failed to read assistant stop state for compaction snapshot", {
|
|
9227
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9228
|
+
});
|
|
9229
|
+
}
|
|
9230
|
+
return snapshot;
|
|
9231
|
+
}
|
|
9232
|
+
|
|
9233
|
+
#compactionStateContext(snapshot: CompactionStateSnapshot): string[] {
|
|
9234
|
+
const context: string[] = [];
|
|
9235
|
+
const goal = snapshot.goal;
|
|
9236
|
+
if (goal?.enabled) {
|
|
9237
|
+
context.push(
|
|
9238
|
+
`Active goal: ${sanitizeCompactionStateText(goal.objective, 160)} (status: ${sanitizeCompactionStateText(goal.status, 40)})`,
|
|
9239
|
+
);
|
|
9240
|
+
} else if (goal?.status === "paused") {
|
|
9241
|
+
context.push(`Paused goal: ${sanitizeCompactionStateText(goal.objective, 160)} (status: paused)`);
|
|
9242
|
+
}
|
|
9243
|
+
for (const entry of snapshot.activeSkills) {
|
|
9244
|
+
const skill = sanitizeCompactionStateText(entry.skill, 100);
|
|
9245
|
+
const phase = sanitizeCompactionStateText(entry.phase, 80);
|
|
9246
|
+
if (skill) context.push(`Active skill: ${skill} phase=${phase}`);
|
|
9247
|
+
}
|
|
9248
|
+
if (snapshot.openTodos.length > 0) {
|
|
9249
|
+
const todos = snapshot.openTodos.map(todo => sanitizeCompactionStateText(todo, 120));
|
|
9250
|
+
context.push(`Open todos: ${todos.join("; ")}`);
|
|
9251
|
+
}
|
|
9252
|
+
return context;
|
|
9253
|
+
}
|
|
9254
|
+
|
|
9255
|
+
#hasUnfinishedWork(snapshot: CompactionStateSnapshot): boolean {
|
|
9256
|
+
if (snapshot.queuedMessages) return true;
|
|
9257
|
+
if (snapshot.lastAssistantStopReason === "length") return true;
|
|
9258
|
+
// A verified paused goal is a human-wait terminus. It suppresses stale
|
|
9259
|
+
// todo/workflow activity until a queued user message resumes the run.
|
|
9260
|
+
if (snapshot.goal?.status === "paused") return false;
|
|
9261
|
+
if (snapshot.goal?.enabled && snapshot.goal.status === "active") return true;
|
|
9262
|
+
if (snapshot.openTodos.length > 0) return true;
|
|
9263
|
+
return snapshot.activeSkills.some(entry => !isWorkflowContinuationInert(entry.skill, entry.phase));
|
|
9264
|
+
}
|
|
9265
|
+
|
|
8865
9266
|
async #applyCompactionPostAppend(
|
|
8866
9267
|
compactionEntryId: string,
|
|
8867
9268
|
firstKeptEntryId: string,
|
|
@@ -9060,6 +9461,26 @@ export class AgentSession {
|
|
|
9060
9461
|
const outcome = await this.#abortWithOutcome(options);
|
|
9061
9462
|
if (outcome.kind === "error") throw outcome.cause;
|
|
9062
9463
|
}
|
|
9464
|
+
/**
|
|
9465
|
+
* Abort a specific active prompt and prove whether its tracked resources settled.
|
|
9466
|
+
*/
|
|
9467
|
+
async abortPromptAndWait(handle: string, options: { graceMs: number }): Promise<RunSettlementProof> {
|
|
9468
|
+
// Only the abort trigger is gated on active ownership; settlement is always proven
|
|
9469
|
+
// from the ledger, because a run can go inactive while its resources still run.
|
|
9470
|
+
if (handle === this.agent.activeResourceRunId)
|
|
9471
|
+
// Awaiting the abort unboundedly would let a provider or tool that ignores
|
|
9472
|
+
// cancellation defer the grace forever, so the proof races the fixed grace
|
|
9473
|
+
// independently of abort completion.
|
|
9474
|
+
void this.abort({ timeoutMs: options.graceMs }).catch(() => {});
|
|
9475
|
+
|
|
9476
|
+
const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
|
|
9477
|
+
if (proof.status === "unfenced") {
|
|
9478
|
+
// Detach the unsettled resources so they can never re-enter this run; their real
|
|
9479
|
+
// promises still release them inside the ledger.
|
|
9480
|
+
this.agent.resourceLedger.quarantine(handle);
|
|
9481
|
+
}
|
|
9482
|
+
return proof;
|
|
9483
|
+
}
|
|
9063
9484
|
|
|
9064
9485
|
/** Atomically interrupt the active run and make text the next prompt. */
|
|
9065
9486
|
async cancelAndSubmit(text: string, options?: { queuedEntryId?: string }): Promise<CancelAndSubmitOutcome> {
|
|
@@ -9713,6 +10134,16 @@ export class AgentSession {
|
|
|
9713
10134
|
getSessionDefaultModelSelector(): string | undefined {
|
|
9714
10135
|
return getSessionContextForInternalRead(this.sessionManager).models.default;
|
|
9715
10136
|
}
|
|
10137
|
+
/**
|
|
10138
|
+
* Resolve the model that `modelRoles.default` currently points to, independent
|
|
10139
|
+
* of whatever model this session's log last recorded. Used by the TUI resume
|
|
10140
|
+
* flow's `session.resumeModelBehavior: "ask"` prompt to offer the currently
|
|
10141
|
+
* configured default as an alternative to the session's saved model.
|
|
10142
|
+
*/
|
|
10143
|
+
resolveConfiguredDefaultModel(): Model | undefined {
|
|
10144
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
10145
|
+
return this.#resolveRoleModelFull("default", availableModels, undefined).model;
|
|
10146
|
+
}
|
|
9716
10147
|
|
|
9717
10148
|
/**
|
|
9718
10149
|
* Re-assert the session resume default ("provider/id") in the session log
|
|
@@ -10604,6 +11035,16 @@ export class AgentSession {
|
|
|
10604
11035
|
return getSupportedEfforts(this.model);
|
|
10605
11036
|
}
|
|
10606
11037
|
|
|
11038
|
+
/**
|
|
11039
|
+
* Runtime evidence published for the current GJC bundle activation
|
|
11040
|
+
* generation, set once by `createAgentSession` after every producer has run.
|
|
11041
|
+
* Undefined until that publication happens, so consumers report runtime
|
|
11042
|
+
* status as unavailable rather than falsely clear.
|
|
11043
|
+
*/
|
|
11044
|
+
gjcRuntimeSnapshot?: GjcRuntimeSnapshotProvider;
|
|
11045
|
+
/** Activation generation a published snapshot must match to be merged. */
|
|
11046
|
+
gjcActivationGeneration?: number;
|
|
11047
|
+
|
|
10607
11048
|
// =========================================================================
|
|
10608
11049
|
// Message Queue Mode Management
|
|
10609
11050
|
// =========================================================================
|
|
@@ -10645,13 +11086,93 @@ export class AgentSession {
|
|
|
10645
11086
|
async #pruneToolOutputs(
|
|
10646
11087
|
signal?: AbortSignal,
|
|
10647
11088
|
overThreshold = false,
|
|
10648
|
-
|
|
11089
|
+
options?: { commitGate?: (actual: { prunedCount: number; tokensSaved: number }) => boolean },
|
|
11090
|
+
): Promise<{ prunedCount: number; tokensSaved: number; committed: boolean } | undefined> {
|
|
10649
11091
|
const branchEntries = this.sessionManager.getBranch();
|
|
10650
|
-
const
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
10654
|
-
|
|
11092
|
+
const artifactManager = this.sessionManager.getArtifactManager();
|
|
11093
|
+
const prunedArtifacts: Array<{ entryId: string; id: string; toolType: string; originalText: string }> = [];
|
|
11094
|
+
let reservedArtifactId: string | undefined;
|
|
11095
|
+
if (artifactManager) {
|
|
11096
|
+
try {
|
|
11097
|
+
reservedArtifactId = (await artifactManager.allocatePath("tool-output")).id;
|
|
11098
|
+
} catch (error) {
|
|
11099
|
+
logger.warn("Failed to reserve artifact ID for pruned tool output", {
|
|
11100
|
+
error: error instanceof Error ? error.message : String(error),
|
|
11101
|
+
});
|
|
11102
|
+
}
|
|
11103
|
+
}
|
|
11104
|
+
const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG, {
|
|
11105
|
+
relaxedMinimum: overThreshold ? 0 : undefined,
|
|
11106
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11107
|
+
artifactRef: candidate => {
|
|
11108
|
+
if (!artifactManager) return undefined;
|
|
11109
|
+
const id = reservedArtifactId ?? String(artifactManager.allocateId());
|
|
11110
|
+
reservedArtifactId = undefined;
|
|
11111
|
+
const toolType = (candidate.toolName ?? "tool-output").replace(/[^a-zA-Z0-9_-]/g, "_") || "tool-output";
|
|
11112
|
+
prunedArtifacts.push({ entryId: candidate.entryId, id, toolType, originalText: candidate.originalText });
|
|
11113
|
+
return `artifact://${id}`;
|
|
11114
|
+
},
|
|
11115
|
+
});
|
|
11116
|
+
const failedArtifactEntryIds = new Set<string>();
|
|
11117
|
+
const publishedArtifacts: typeof prunedArtifacts = [];
|
|
11118
|
+
for (const artifact of prunedArtifacts.filter(artifact =>
|
|
11119
|
+
result.originals.some(original => original.entryId === artifact.entryId),
|
|
11120
|
+
)) {
|
|
11121
|
+
try {
|
|
11122
|
+
await artifactManager?.publishNamedNoReplace(
|
|
11123
|
+
`${artifact.id}.${artifact.toolType}.log`,
|
|
11124
|
+
new TextEncoder().encode(artifact.originalText),
|
|
11125
|
+
);
|
|
11126
|
+
publishedArtifacts.push(artifact);
|
|
11127
|
+
} catch (error) {
|
|
11128
|
+
failedArtifactEntryIds.add(artifact.entryId);
|
|
11129
|
+
logger.warn("Failed to persist pruned tool output artifact", {
|
|
11130
|
+
artifactId: artifact.id,
|
|
11131
|
+
error: error instanceof Error ? error.message : String(error),
|
|
11132
|
+
});
|
|
11133
|
+
}
|
|
11134
|
+
}
|
|
11135
|
+
const rollbackPublishedArtifacts = async (reason: "aborted" | "commit_gate_rejected") => {
|
|
11136
|
+
let removedArtifacts = 0;
|
|
11137
|
+
let unremovedArtifacts = 0;
|
|
11138
|
+
for (const artifact of publishedArtifacts) {
|
|
11139
|
+
const filename = `${artifact.id}.${artifact.toolType}.log`;
|
|
11140
|
+
let removed = false;
|
|
11141
|
+
for (let attempt = 0; attempt < 3 && !removed; attempt++) {
|
|
11142
|
+
removed = (await artifactManager?.removeNamedBestEffort(filename)) ?? false;
|
|
11143
|
+
if (!removed && attempt < 2) await Bun.sleep(10);
|
|
11144
|
+
}
|
|
11145
|
+
if (removed) removedArtifacts++;
|
|
11146
|
+
else {
|
|
11147
|
+
unremovedArtifacts++;
|
|
11148
|
+
logger.warn("Failed to roll back staged pruned-output artifact after retries", {
|
|
11149
|
+
artifactId: artifact.id,
|
|
11150
|
+
reason,
|
|
11151
|
+
});
|
|
11152
|
+
}
|
|
11153
|
+
}
|
|
11154
|
+
return { removedArtifacts, unremovedArtifacts };
|
|
11155
|
+
};
|
|
11156
|
+
let toolTokensSaved = result.tokensSaved;
|
|
11157
|
+
let toolPrunedCount = result.prunedCount;
|
|
11158
|
+
const committedToolEntries = result.prunedEntries.filter(entry => {
|
|
11159
|
+
if (!failedArtifactEntryIds.has(entry.id)) return true;
|
|
11160
|
+
const original = result.originals.find(candidate => candidate.entryId === entry.id);
|
|
11161
|
+
if (!original) return true;
|
|
11162
|
+
const message = entry.message as ToolResultMessage;
|
|
11163
|
+
toolTokensSaved -=
|
|
11164
|
+
original.tokens -
|
|
11165
|
+
estimateTextTokensHeuristic(
|
|
11166
|
+
message.content
|
|
11167
|
+
.filter((part): part is TextContent => part.type === "text")
|
|
11168
|
+
.map(part => part.text)
|
|
11169
|
+
.join(""),
|
|
11170
|
+
);
|
|
11171
|
+
toolPrunedCount--;
|
|
11172
|
+
message.content = [{ type: "text", text: original.originalText }];
|
|
11173
|
+
delete message.prunedAt;
|
|
11174
|
+
return false;
|
|
11175
|
+
});
|
|
10655
11176
|
const argumentResult = pruneAssistantToolArguments(branchEntries, DEFAULT_PRUNE_CONFIG);
|
|
10656
11177
|
const fileMentionResult = pruneStaleFileMentions(branchEntries, p =>
|
|
10657
11178
|
resolveReadPath(p, this.sessionManager.getCwd()),
|
|
@@ -10659,22 +11180,37 @@ export class AgentSession {
|
|
|
10659
11180
|
const volatileContextResult = pruneSupersededVolatileProjectContext(branchEntries);
|
|
10660
11181
|
const reminderResult = pruneSupersededMaintenanceReminders(branchEntries);
|
|
10661
11182
|
const tokensSaved =
|
|
10662
|
-
|
|
11183
|
+
toolTokensSaved +
|
|
10663
11184
|
argumentResult.argumentTokensSaved +
|
|
10664
11185
|
Math.round((fileMentionResult.bytesSaved + volatileContextResult.bytesSaved + reminderResult.bytesSaved) / 4);
|
|
10665
11186
|
const prunedCount =
|
|
10666
|
-
|
|
11187
|
+
toolPrunedCount +
|
|
10667
11188
|
argumentResult.argumentPrunedCount +
|
|
10668
11189
|
fileMentionResult.changed.length +
|
|
10669
11190
|
volatileContextResult.changed.length +
|
|
10670
11191
|
reminderResult.changed.length;
|
|
10671
11192
|
if (prunedCount === 0 || signal?.aborted) {
|
|
11193
|
+
if (publishedArtifacts.length > 0) await rollbackPublishedArtifacts("aborted");
|
|
11194
|
+
return undefined;
|
|
11195
|
+
}
|
|
11196
|
+
if (options?.commitGate && !options.commitGate({ prunedCount, tokensSaved })) {
|
|
11197
|
+
const rollback = await rollbackPublishedArtifacts("commit_gate_rejected");
|
|
11198
|
+
logger.info("Below-threshold maintenance pruning staged but not committed", {
|
|
11199
|
+
prunedCount,
|
|
11200
|
+
tokensSaved,
|
|
11201
|
+
rolledBackArtifacts: rollback.removedArtifacts,
|
|
11202
|
+
unremovedArtifacts: rollback.unremovedArtifacts,
|
|
11203
|
+
});
|
|
11204
|
+
return { prunedCount, tokensSaved, committed: false };
|
|
11205
|
+
}
|
|
11206
|
+
if (signal?.aborted) {
|
|
11207
|
+
await rollbackPublishedArtifacts("aborted");
|
|
10672
11208
|
return undefined;
|
|
10673
11209
|
}
|
|
10674
11210
|
|
|
10675
11211
|
// getBranch() returns materialized copies for blob-externalized entries, so
|
|
10676
11212
|
// the pruning mutations must be written back into the canonical store.
|
|
10677
|
-
const combined = [...
|
|
11213
|
+
const combined = [...committedToolEntries, ...argumentResult.prunedEntries, ...fileMentionResult.changed];
|
|
10678
11214
|
this.sessionManager.applyEntryMessageUpdates(combined);
|
|
10679
11215
|
this.sessionManager.applyCustomMessageEntryUpdates([...volatileContextResult.changed, ...reminderResult.changed]);
|
|
10680
11216
|
await this.sessionManager.rewriteEntries();
|
|
@@ -10685,7 +11221,7 @@ export class AgentSession {
|
|
|
10685
11221
|
this.#resetInjectedContextSignatures();
|
|
10686
11222
|
this.#syncTodoPhasesFromBranch();
|
|
10687
11223
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
10688
|
-
return { prunedCount, tokensSaved };
|
|
11224
|
+
return { prunedCount, tokensSaved, committed: true };
|
|
10689
11225
|
}
|
|
10690
11226
|
|
|
10691
11227
|
/**
|
|
@@ -10718,7 +11254,9 @@ export class AgentSession {
|
|
|
10718
11254
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
10719
11255
|
if (!compactionSettings.maintenancePruningEnabled) return;
|
|
10720
11256
|
|
|
10721
|
-
const estimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG
|
|
11257
|
+
const estimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
|
|
11258
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11259
|
+
});
|
|
10722
11260
|
const cacheEpochResetCost = this.#lastCacheEpochResetCost();
|
|
10723
11261
|
if (
|
|
10724
11262
|
!shouldRunMaintenancePrune({
|
|
@@ -10731,10 +11269,30 @@ export class AgentSession {
|
|
|
10731
11269
|
return;
|
|
10732
11270
|
}
|
|
10733
11271
|
|
|
10734
|
-
const pruneResult = await this.#pruneToolOutputs(
|
|
11272
|
+
const pruneResult = await this.#pruneToolOutputs(undefined, false, {
|
|
11273
|
+
commitGate: actual => actual.tokensSaved > cacheEpochResetCost,
|
|
11274
|
+
});
|
|
10735
11275
|
if (!pruneResult || pruneResult.prunedCount === 0) return;
|
|
10736
|
-
|
|
10737
|
-
|
|
11276
|
+
if (!pruneResult.committed) {
|
|
11277
|
+
const message = `Maintenance pruning skipped: actual savings ${pruneResult.tokensSaved} tokens <= cache-epoch reset cost ${cacheEpochResetCost} after artifact notices/publication rollback.`;
|
|
11278
|
+
await this.#emitSessionEvent({
|
|
11279
|
+
type: "notice",
|
|
11280
|
+
level: "info",
|
|
11281
|
+
source: "maintenance-prune",
|
|
11282
|
+
message,
|
|
11283
|
+
});
|
|
11284
|
+
logger.info("Below-threshold maintenance pruning skipped", {
|
|
11285
|
+
tokensSaved: pruneResult.tokensSaved,
|
|
11286
|
+
prunedCount: pruneResult.prunedCount,
|
|
11287
|
+
cacheEpochResetCost,
|
|
11288
|
+
minSavings: compactionSettings.maintenancePruningMinSavingsTokens,
|
|
11289
|
+
});
|
|
11290
|
+
return;
|
|
11291
|
+
}
|
|
11292
|
+
const resetReason =
|
|
11293
|
+
pruneResult.tokensSaved > cacheEpochResetCost
|
|
11294
|
+
? `below-threshold maintenance: reclaimed ${pruneResult.tokensSaved} tokens > cache-epoch reset cost ${cacheEpochResetCost}`
|
|
11295
|
+
: `below-threshold maintenance: reclaimed ${pruneResult.tokensSaved} tokens <= cache-epoch reset cost ${cacheEpochResetCost}`;
|
|
10738
11296
|
await this.#emitSessionEvent({
|
|
10739
11297
|
type: "notice",
|
|
10740
11298
|
level: "info",
|
|
@@ -10768,6 +11326,8 @@ export class AgentSession {
|
|
|
10768
11326
|
await this.abort();
|
|
10769
11327
|
const compactionAbortController = new AbortController();
|
|
10770
11328
|
this.#compactionAbortController = compactionAbortController;
|
|
11329
|
+
// Take this invocation's state snapshot for the summarizer context.
|
|
11330
|
+
const compactionStateSnapshot = await this.#compactionStateSnapshot();
|
|
10771
11331
|
|
|
10772
11332
|
try {
|
|
10773
11333
|
if (!this.model) {
|
|
@@ -10813,7 +11373,11 @@ export class AgentSession {
|
|
|
10813
11373
|
}
|
|
10814
11374
|
}
|
|
10815
11375
|
|
|
10816
|
-
const compactionPrep = await this.#prepareCompactionFromHooks(
|
|
11376
|
+
const compactionPrep = await this.#prepareCompactionFromHooks(
|
|
11377
|
+
preparation,
|
|
11378
|
+
hookCompaction,
|
|
11379
|
+
compactionStateSnapshot,
|
|
11380
|
+
);
|
|
10817
11381
|
|
|
10818
11382
|
let summary: string;
|
|
10819
11383
|
let shortSummary: string | undefined;
|
|
@@ -11305,6 +11869,7 @@ export class AgentSession {
|
|
|
11305
11869
|
assistantMessage: AssistantMessage,
|
|
11306
11870
|
skipAbortedCheck = true,
|
|
11307
11871
|
onTerminalOverflowNoop?: () => void,
|
|
11872
|
+
resourceRunId?: string,
|
|
11308
11873
|
): Promise<boolean> {
|
|
11309
11874
|
// Safety stops are terminal and must not trigger context maintenance.
|
|
11310
11875
|
if (
|
|
@@ -11351,7 +11916,12 @@ export class AgentSession {
|
|
|
11351
11916
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
11352
11917
|
if (promoted) {
|
|
11353
11918
|
// Retry on the promoted (larger) model without compacting
|
|
11354
|
-
this.#scheduleAgentContinue({
|
|
11919
|
+
this.#scheduleAgentContinue({
|
|
11920
|
+
delayMs: 100,
|
|
11921
|
+
generation,
|
|
11922
|
+
suppressPredecessorAgentEnd: true,
|
|
11923
|
+
resourceRunId,
|
|
11924
|
+
});
|
|
11355
11925
|
|
|
11356
11926
|
return true;
|
|
11357
11927
|
}
|
|
@@ -11367,10 +11937,11 @@ export class AgentSession {
|
|
|
11367
11937
|
this.agent.appendMessage(assistantMessage);
|
|
11368
11938
|
}
|
|
11369
11939
|
},
|
|
11940
|
+
resourceRunId,
|
|
11370
11941
|
});
|
|
11371
11942
|
return "continuationScheduled" in status && status.continuationScheduled === true;
|
|
11372
11943
|
}
|
|
11373
|
-
return this.#scheduleOverflowRetryContinuation(generation);
|
|
11944
|
+
return await this.#scheduleOverflowRetryContinuation(generation, resourceRunId);
|
|
11374
11945
|
}
|
|
11375
11946
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
11376
11947
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
|
|
@@ -11390,6 +11961,7 @@ export class AgentSession {
|
|
|
11390
11961
|
return true;
|
|
11391
11962
|
const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
|
|
11392
11963
|
relaxedMinimum: 0,
|
|
11964
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11393
11965
|
});
|
|
11394
11966
|
if (
|
|
11395
11967
|
pruneEstimate.tokensSaved > 0 &&
|
|
@@ -11494,6 +12066,7 @@ export class AgentSession {
|
|
|
11494
12066
|
// prompt-cache epoch via #closeCodexProviderSessionsForHistoryRewrite.
|
|
11495
12067
|
const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
|
|
11496
12068
|
relaxedMinimum: 0,
|
|
12069
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11497
12070
|
});
|
|
11498
12071
|
let pruneResult: { prunedCount: number; tokensSaved: number } | undefined;
|
|
11499
12072
|
if (
|
|
@@ -11675,6 +12248,7 @@ export class AgentSession {
|
|
|
11675
12248
|
|
|
11676
12249
|
const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
|
|
11677
12250
|
relaxedMinimum: 0,
|
|
12251
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11678
12252
|
});
|
|
11679
12253
|
if (
|
|
11680
12254
|
pruneEstimate.tokensSaved > 0 &&
|
|
@@ -12614,6 +13188,7 @@ export class AgentSession {
|
|
|
12614
13188
|
async #prepareCompactionFromHooks(
|
|
12615
13189
|
preparation: CompactionPreparation,
|
|
12616
13190
|
hookCompaction: CompactionResult | undefined,
|
|
13191
|
+
stateSnapshot: CompactionStateSnapshot,
|
|
12617
13192
|
): Promise<
|
|
12618
13193
|
| {
|
|
12619
13194
|
kind: "fromHook";
|
|
@@ -12653,11 +13228,17 @@ export class AgentSession {
|
|
|
12653
13228
|
hookContext = hookContext ? [...hookContext, memoryBackendContext] : [memoryBackendContext];
|
|
12654
13229
|
}
|
|
12655
13230
|
|
|
13231
|
+
const stateContext = this.#compactionStateContext(stateSnapshot);
|
|
13232
|
+
|
|
13233
|
+
if (stateContext.length > 0) {
|
|
13234
|
+
hookContext = hookContext ? [...hookContext, ...stateContext] : stateContext;
|
|
13235
|
+
}
|
|
13236
|
+
|
|
12656
13237
|
if (hookCompaction) {
|
|
12657
13238
|
preserveData ??= hookCompaction.preserveData;
|
|
12658
13239
|
return {
|
|
12659
13240
|
kind: "fromHook",
|
|
12660
|
-
summary: hookCompaction.summary,
|
|
13241
|
+
summary: appendCompactionStateContext(hookCompaction.summary, stateContext),
|
|
12661
13242
|
shortSummary: hookCompaction.shortSummary,
|
|
12662
13243
|
firstKeptEntryId: hookCompaction.firstKeptEntryId,
|
|
12663
13244
|
tokensBefore: hookCompaction.tokensBefore,
|
|
@@ -12682,6 +13263,7 @@ export class AgentSession {
|
|
|
12682
13263
|
force?: boolean;
|
|
12683
13264
|
signal?: AbortSignal;
|
|
12684
13265
|
beforeTerminalOverflowNoop?: () => void;
|
|
13266
|
+
resourceRunId?: string;
|
|
12685
13267
|
},
|
|
12686
13268
|
): Promise<AutoCompactionTerminalStatus> {
|
|
12687
13269
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
@@ -12703,7 +13285,7 @@ export class AgentSession {
|
|
|
12703
13285
|
if (signal.aborted) return;
|
|
12704
13286
|
await this.#runAutoCompaction(reason, willRetry, true, options);
|
|
12705
13287
|
},
|
|
12706
|
-
{ generation },
|
|
13288
|
+
{ generation, resourceRunId: options?.resourceRunId },
|
|
12707
13289
|
);
|
|
12708
13290
|
return { kind: "skipped" };
|
|
12709
13291
|
}
|
|
@@ -12735,6 +13317,10 @@ export class AgentSession {
|
|
|
12735
13317
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
12736
13318
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
12737
13319
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
13320
|
+
const compactionStateSnapshot = await this.#compactionStateSnapshot();
|
|
13321
|
+
if (autoCompactionSignal.aborted || this.#isDisposed || this.#promptGeneration !== generation) {
|
|
13322
|
+
return await emitAborted();
|
|
13323
|
+
}
|
|
12738
13324
|
|
|
12739
13325
|
if (compactionSettings.strategy === "handoff" && reason !== "overflow") {
|
|
12740
13326
|
const handoffFocus = AUTO_HANDOFF_THRESHOLD_FOCUS;
|
|
@@ -12774,6 +13360,7 @@ export class AgentSession {
|
|
|
12774
13360
|
if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
12775
13361
|
this.#scheduleAutoContinuePrompt(generation);
|
|
12776
13362
|
}
|
|
13363
|
+
|
|
12777
13364
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
12778
13365
|
return { kind: "compacted" };
|
|
12779
13366
|
}
|
|
@@ -12827,15 +13414,22 @@ export class AgentSession {
|
|
|
12827
13414
|
const overflowNoopWouldReplay = reason === "overflow" && willRetry && this.#isResumableAgentTail();
|
|
12828
13415
|
const continuationSkipReason =
|
|
12829
13416
|
!overflowNoopWouldReplay && willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
13417
|
+
if (continuationSkipReason) {
|
|
13418
|
+
this.#logCompactionContinuationSkipped("overflow_retry", continuationSkipReason);
|
|
13419
|
+
}
|
|
12830
13420
|
if (overflowNoopWouldReplay) {
|
|
12831
13421
|
options?.beforeTerminalOverflowNoop?.();
|
|
12832
13422
|
}
|
|
13423
|
+
const overflowContinuationScheduled =
|
|
13424
|
+
!overflowNoopWouldReplay && willRetry && !continuationSkipReason
|
|
13425
|
+
? await this.#scheduleOverflowRetryContinuation(generation)
|
|
13426
|
+
: false;
|
|
12833
13427
|
await this.#emitSessionEvent({
|
|
12834
13428
|
type: "auto_compaction_end",
|
|
12835
13429
|
action,
|
|
12836
13430
|
result: undefined,
|
|
12837
13431
|
aborted: false,
|
|
12838
|
-
willRetry:
|
|
13432
|
+
willRetry: overflowContinuationScheduled,
|
|
12839
13433
|
errorMessage: overflowNoopWouldReplay
|
|
12840
13434
|
? "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
13435
|
: undefined,
|
|
@@ -12848,6 +13442,7 @@ export class AgentSession {
|
|
|
12848
13442
|
delayMs: 100,
|
|
12849
13443
|
generation,
|
|
12850
13444
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
13445
|
+
rescheduleOnBusy: true,
|
|
12851
13446
|
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
12852
13447
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
12853
13448
|
});
|
|
@@ -12856,7 +13451,9 @@ export class AgentSession {
|
|
|
12856
13451
|
return { kind: "skipped" };
|
|
12857
13452
|
}
|
|
12858
13453
|
if (willRetry) {
|
|
12859
|
-
return
|
|
13454
|
+
return overflowContinuationScheduled
|
|
13455
|
+
? { kind: "skipped", continuationScheduled: true }
|
|
13456
|
+
: { kind: "skipped" };
|
|
12860
13457
|
}
|
|
12861
13458
|
if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
12862
13459
|
this.#scheduleAgentContinue({
|
|
@@ -12865,6 +13462,7 @@ export class AgentSession {
|
|
|
12865
13462
|
suppressPredecessorAgentEnd: true,
|
|
12866
13463
|
|
|
12867
13464
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
13465
|
+
rescheduleOnBusy: true,
|
|
12868
13466
|
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
12869
13467
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
12870
13468
|
});
|
|
@@ -12905,7 +13503,11 @@ export class AgentSession {
|
|
|
12905
13503
|
}
|
|
12906
13504
|
}
|
|
12907
13505
|
|
|
12908
|
-
const compactionPrep = await this.#prepareCompactionFromHooks(
|
|
13506
|
+
const compactionPrep = await this.#prepareCompactionFromHooks(
|
|
13507
|
+
preparation,
|
|
13508
|
+
hookCompaction,
|
|
13509
|
+
compactionStateSnapshot,
|
|
13510
|
+
);
|
|
12909
13511
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
12910
13512
|
|
|
12911
13513
|
let summary: string;
|
|
@@ -13088,18 +13690,25 @@ export class AgentSession {
|
|
|
13088
13690
|
this.#lastOversizedAutoMaintenanceAttemptSignature = undefined;
|
|
13089
13691
|
|
|
13090
13692
|
const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
13693
|
+
if (continuationSkipReason) {
|
|
13694
|
+
this.#logCompactionContinuationSkipped("overflow_retry", continuationSkipReason);
|
|
13695
|
+
}
|
|
13696
|
+
const overflowContinuationScheduled =
|
|
13697
|
+
willRetry && !continuationSkipReason ? await this.#scheduleOverflowRetryContinuation(generation) : false;
|
|
13091
13698
|
await this.#emitSessionEvent({
|
|
13092
13699
|
type: "auto_compaction_end",
|
|
13093
13700
|
action,
|
|
13094
13701
|
result,
|
|
13095
13702
|
aborted: false,
|
|
13096
|
-
willRetry:
|
|
13703
|
+
willRetry: overflowContinuationScheduled,
|
|
13097
13704
|
continuationSkipReason,
|
|
13098
13705
|
});
|
|
13099
13706
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
13100
13707
|
|
|
13101
13708
|
if (willRetry) {
|
|
13102
|
-
return
|
|
13709
|
+
return overflowContinuationScheduled
|
|
13710
|
+
? { kind: "compacted", continuationScheduled: true }
|
|
13711
|
+
: { kind: "compacted" };
|
|
13103
13712
|
}
|
|
13104
13713
|
if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
13105
13714
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
@@ -13109,6 +13718,7 @@ export class AgentSession {
|
|
|
13109
13718
|
generation,
|
|
13110
13719
|
suppressPredecessorAgentEnd: true,
|
|
13111
13720
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
13721
|
+
rescheduleOnBusy: true,
|
|
13112
13722
|
onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
|
|
13113
13723
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
13114
13724
|
});
|
|
@@ -13223,6 +13833,10 @@ export class AgentSession {
|
|
|
13223
13833
|
return false;
|
|
13224
13834
|
}
|
|
13225
13835
|
|
|
13836
|
+
#isTypedFirstEventTimeout(message: AssistantMessage): boolean {
|
|
13837
|
+
return message.transportFailure?.providerCode?.toLowerCase() === STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE;
|
|
13838
|
+
}
|
|
13839
|
+
|
|
13226
13840
|
#isTerminalProviderFirstEventTimeout(message: AssistantMessage): boolean {
|
|
13227
13841
|
return this.#isKimiCodeFirstEventTimeout(message) || this.#isAlibabaTokenPlanFirstEventTimeout(message);
|
|
13228
13842
|
}
|
|
@@ -13348,6 +13962,7 @@ export class AgentSession {
|
|
|
13348
13962
|
#classifyErrorForRetry(message: AssistantMessage): RetryErrorClassification {
|
|
13349
13963
|
if (message.stopReason !== "error") return "none";
|
|
13350
13964
|
if (message.errorKind === "provider_safety_stop") return "terminal";
|
|
13965
|
+
if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
|
|
13351
13966
|
if (!message.errorMessage) return "none";
|
|
13352
13967
|
const err = message.errorMessage;
|
|
13353
13968
|
// Provider safety refusals (e.g. Anthropic stop_reason "refusal" /
|
|
@@ -13388,10 +14003,8 @@ export class AgentSession {
|
|
|
13388
14003
|
if (this.#isTerminalProviderFirstEventTimeout(message)) {
|
|
13389
14004
|
return "terminal";
|
|
13390
14005
|
}
|
|
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.
|
|
14006
|
+
// Legacy providers that have not yet stamped the typed timeout fact retain
|
|
14007
|
+
// their existing bounded ollama-cloud behavior.
|
|
13395
14008
|
if (this.#isFirstEventTimeoutErrorMessage(err) && this.#shouldFailClosedOnFirstEventTimeout(message)) {
|
|
13396
14009
|
return "first_event_timeout";
|
|
13397
14010
|
}
|
|
@@ -13727,25 +14340,25 @@ export class AgentSession {
|
|
|
13727
14340
|
return `Model fallback chain exhausted; models tried: ${tried}; models skipped: ${skipped}`;
|
|
13728
14341
|
}
|
|
13729
14342
|
|
|
13730
|
-
async #
|
|
13731
|
-
class: FallbackTriggerClass;
|
|
13732
|
-
retryAfterMs?: number;
|
|
13733
|
-
}): Promise<boolean> {
|
|
14343
|
+
async #markFailedCredential(trigger: { class: FallbackTriggerClass; retryAfterMs?: number }): Promise<boolean> {
|
|
13734
14344
|
if (!this.model || (trigger.class !== "auth" && trigger.class !== "quota" && trigger.class !== "rate_limit")) {
|
|
13735
14345
|
return false;
|
|
13736
14346
|
}
|
|
13737
14347
|
const authStorage = this.#modelRegistry.authStorage;
|
|
14348
|
+
const providerAffinitySessionId = this.agent.providerSessionId ?? this.agent.sessionId ?? this.sessionId;
|
|
13738
14349
|
if (trigger.class === "auth") {
|
|
13739
|
-
const apiKey = await this.#modelRegistry.getApiKey(this.model,
|
|
14350
|
+
const apiKey = await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId);
|
|
13740
14351
|
if (!isAuthenticated(apiKey)) return false;
|
|
13741
|
-
return authStorage.invalidateCredentialMatching(this.model.provider, apiKey, {
|
|
14352
|
+
return authStorage.invalidateCredentialMatching(this.model.provider, apiKey, {
|
|
14353
|
+
sessionId: providerAffinitySessionId,
|
|
14354
|
+
});
|
|
13742
14355
|
}
|
|
13743
14356
|
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,
|
|
14357
|
+
const activeApiKey = await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId);
|
|
14358
|
+
const rotated = await authStorage.markUsageLimitReached(this.model.provider, providerAffinitySessionId, {
|
|
13746
14359
|
retryAfterMs: trigger.retryAfterMs,
|
|
13747
14360
|
});
|
|
13748
|
-
return rotated && (await this.#modelRegistry.getApiKey(this.model,
|
|
14361
|
+
return rotated && (await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId)) !== activeApiKey;
|
|
13749
14362
|
}
|
|
13750
14363
|
|
|
13751
14364
|
/** Handle retryable errors with exponential backoff. */
|
|
@@ -13766,16 +14379,16 @@ export class AgentSession {
|
|
|
13766
14379
|
// user opt-out.
|
|
13767
14380
|
if (!managedFallback && !retrySettings.enabled) return false;
|
|
13768
14381
|
const classification = managedFallback ? undefined : this.#classifyErrorForRetry(message);
|
|
13769
|
-
|
|
13770
|
-
|
|
13771
|
-
|
|
13772
|
-
|
|
13773
|
-
|
|
13774
|
-
|
|
13775
|
-
|
|
13776
|
-
)
|
|
13777
|
-
|
|
13778
|
-
|
|
14382
|
+
if (classification === "first_event_timeout") {
|
|
14383
|
+
this.#firstEventTimeoutRetryStartedAt ??= Date.now();
|
|
14384
|
+
}
|
|
14385
|
+
// First-event timeouts are replay-safe only before any model progress,
|
|
14386
|
+
// abort, or extension/provider lifecycle participation.
|
|
14387
|
+
if (
|
|
14388
|
+
classification === "first_event_timeout" &&
|
|
14389
|
+
(!this.#hasCleanRetryReplaySafety || assistantMessageHasVisibleOrToolContent(message))
|
|
14390
|
+
) {
|
|
14391
|
+
return false;
|
|
13779
14392
|
}
|
|
13780
14393
|
const trigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
|
|
13781
14394
|
if (!trigger) {
|
|
@@ -13783,6 +14396,39 @@ export class AgentSession {
|
|
|
13783
14396
|
? this.#managedFallbackExhaustionDecision(message, message.errorMessage || "Model fallback attempt failed")
|
|
13784
14397
|
: false;
|
|
13785
14398
|
}
|
|
14399
|
+
// Credential rotation: a content-free quota/rate-limit failure has no
|
|
14400
|
+
// observable state to corrupt, so it is replay-safe regardless of
|
|
14401
|
+
// extension lifecycle participation. Mark the failed credential and
|
|
14402
|
+
// retry with the next stored credential of the same provider.
|
|
14403
|
+
let credentialRotated =
|
|
14404
|
+
!managedFallback &&
|
|
14405
|
+
!assistantMessageHasVisibleOrToolContent(message) &&
|
|
14406
|
+
(trigger.class === "quota" || trigger.class === "rate_limit") &&
|
|
14407
|
+
(await this.#markFailedCredential(trigger));
|
|
14408
|
+
// A content-free credential rotation is inherently replay-safe: no partial
|
|
14409
|
+
// output, no tool calls, no extension-observable streaming state was produced
|
|
14410
|
+
// before the failure. This bypasses #hasCleanRetryReplaySafety because the
|
|
14411
|
+
// content-free check is the replay-safety guarantee for credential rotation.
|
|
14412
|
+
const canReplayRotatedCredential = credentialRotated;
|
|
14413
|
+
// Bare defaults admit only clean, side-effect-free canonical stream watchdog failures,
|
|
14414
|
+
// content-free quota/rate-limit credential rotations, and the explicit Codex
|
|
14415
|
+
// capacity-overload event.
|
|
14416
|
+
if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential) {
|
|
14417
|
+
const bareDefaultCodexOverload = isBareDefaultCodexOverload(message);
|
|
14418
|
+
// Content-free Codex overload is replay-safe by the same reasoning as
|
|
14419
|
+
// credential rotation: no partial output means no observable state.
|
|
14420
|
+
const canReplayCodexOverload = bareDefaultCodexOverload;
|
|
14421
|
+
if (
|
|
14422
|
+
(!canReplayCodexOverload &&
|
|
14423
|
+
!this.#isTypedFirstEventTimeout(message) &&
|
|
14424
|
+
(hasBareDefaultRetryDisqualifyingFacts(message) ||
|
|
14425
|
+
(classification !== "transient" && classification !== "first_event_timeout") ||
|
|
14426
|
+
!BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))) ||
|
|
14427
|
+
(!canReplayCodexOverload && !this.#hasCleanRetryReplaySafety)
|
|
14428
|
+
) {
|
|
14429
|
+
return false;
|
|
14430
|
+
}
|
|
14431
|
+
}
|
|
13786
14432
|
const legacyUnbounded = classification === "transient";
|
|
13787
14433
|
const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
|
|
13788
14434
|
const failedSelector = managedFallback ? controller.currentSelector() : undefined;
|
|
@@ -13791,12 +14437,14 @@ export class AgentSession {
|
|
|
13791
14437
|
: legacyUnbounded || attemptsUsed <= retrySettings.maxRetries
|
|
13792
14438
|
? "retry"
|
|
13793
14439
|
: "exhausted";
|
|
13794
|
-
|
|
13795
|
-
|
|
13796
|
-
|
|
13797
|
-
|
|
13798
|
-
|
|
13799
|
-
|
|
14440
|
+
// Credential rotation is unbounded: a fresh credential is a different
|
|
14441
|
+
// retry dimension from transient-error backoff, so it overrides maxRetries
|
|
14442
|
+
// exhaustion and forces an immediate same-model retry.
|
|
14443
|
+
if (managedFallback && outcome === "advance" && (trigger.class === "quota" || trigger.class === "rate_limit")) {
|
|
14444
|
+
credentialRotated = await this.#markFailedCredential(trigger);
|
|
14445
|
+
}
|
|
14446
|
+
if (credentialRotated) {
|
|
14447
|
+
if (managedFallback) controller.restorePreviousEntryForRetry();
|
|
13800
14448
|
outcome = "retry";
|
|
13801
14449
|
}
|
|
13802
14450
|
if (outcome === "exhausted") {
|
|
@@ -13807,6 +14455,10 @@ export class AgentSession {
|
|
|
13807
14455
|
controller.resetSticky();
|
|
13808
14456
|
return managedOutcome ? this.#managedFallbackExhaustionDecision(message, errorMessage) : false;
|
|
13809
14457
|
}
|
|
14458
|
+
if (classification === "first_event_timeout") {
|
|
14459
|
+
const elapsedMs = Date.now() - (this.#firstEventTimeoutRetryStartedAt ?? Date.now());
|
|
14460
|
+
message.errorMessage = `First-event stream timeout exhausted after ${attemptsUsed} attempts; waited ${elapsedMs}ms total: ${message.errorMessage ?? "Unknown error"}`;
|
|
14461
|
+
}
|
|
13810
14462
|
return false;
|
|
13811
14463
|
}
|
|
13812
14464
|
|
|
@@ -13828,7 +14480,7 @@ export class AgentSession {
|
|
|
13828
14480
|
}
|
|
13829
14481
|
|
|
13830
14482
|
const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
|
|
13831
|
-
if (managedFallback && !credentialRotated) await this.#
|
|
14483
|
+
if (managedFallback && !credentialRotated) await this.#markFailedCredential(trigger);
|
|
13832
14484
|
let advanced = outcome !== "advance";
|
|
13833
14485
|
let resolutionError: unknown;
|
|
13834
14486
|
if (outcome === "advance") {
|
|
@@ -13870,7 +14522,12 @@ export class AgentSession {
|
|
|
13870
14522
|
await this.#emitSessionEvent({
|
|
13871
14523
|
type: "auto_retry_start",
|
|
13872
14524
|
attempt: this.#retryAttempt,
|
|
13873
|
-
maxAttempts:
|
|
14525
|
+
maxAttempts:
|
|
14526
|
+
classification === "first_event_timeout"
|
|
14527
|
+
? retrySettings.maxRetries + 1
|
|
14528
|
+
: managedFallback
|
|
14529
|
+
? controller.maxAttempts
|
|
14530
|
+
: retrySettings.maxRetries,
|
|
13874
14531
|
delayMs,
|
|
13875
14532
|
errorMessage,
|
|
13876
14533
|
unbounded: managedFallback ? false : legacyUnbounded,
|
|
@@ -13878,9 +14535,19 @@ export class AgentSession {
|
|
|
13878
14535
|
|
|
13879
14536
|
const messages = this.agent.state.messages;
|
|
13880
14537
|
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
13881
|
-
|
|
14538
|
+
let end = messages.length - 1;
|
|
14539
|
+
// A wedged turn can leave more than one assistant tail behind (e.g. the
|
|
14540
|
+
// agent loop's invalid_prompt repair resend commits its own rejected
|
|
14541
|
+
// message). agent.continue() refuses ANY assistant tail, so strip the
|
|
14542
|
+
// whole trailing run of failed attempts, not just the last one.
|
|
14543
|
+
while (end > 0) {
|
|
14544
|
+
const previous = messages[end - 1];
|
|
14545
|
+
if (previous.role !== "assistant") break;
|
|
14546
|
+
if (previous.stopReason !== "error" && previous.stopReason !== "aborted") break;
|
|
14547
|
+
end--;
|
|
14548
|
+
}
|
|
14549
|
+
this.agent.replaceMessages(messages.slice(0, end));
|
|
13882
14550
|
}
|
|
13883
|
-
|
|
13884
14551
|
try {
|
|
13885
14552
|
await scheduler.wait(delayMs, { signal: retryAbortController.signal });
|
|
13886
14553
|
} catch {
|
|
@@ -14120,12 +14787,8 @@ export class AgentSession {
|
|
|
14120
14787
|
// Bash Execution
|
|
14121
14788
|
// =========================================================================
|
|
14122
14789
|
|
|
14123
|
-
async #saveBashOriginalArtifact(originalText: string): Promise<
|
|
14124
|
-
|
|
14125
|
-
return await this.sessionManager.saveArtifact(originalText, "bash-original");
|
|
14126
|
-
} catch {
|
|
14127
|
-
return undefined;
|
|
14128
|
-
}
|
|
14790
|
+
async #saveBashOriginalArtifact(originalText: string): Promise<BashArtifactSaveResult> {
|
|
14791
|
+
return saveAgentBashOriginalArtifact(this.sessionManager, originalText);
|
|
14129
14792
|
}
|
|
14130
14793
|
|
|
14131
14794
|
/**
|
|
@@ -15057,7 +15720,10 @@ export class AgentSession {
|
|
|
15057
15720
|
*/
|
|
15058
15721
|
async switchSession(
|
|
15059
15722
|
sessionPath: string,
|
|
15060
|
-
options?: {
|
|
15723
|
+
options?: {
|
|
15724
|
+
transition?: SessionSwitchEvent["transition"];
|
|
15725
|
+
onTransitionMutationStarted?: () => void;
|
|
15726
|
+
},
|
|
15061
15727
|
): Promise<boolean> {
|
|
15062
15728
|
this.#beginSessionTransition("switch-session");
|
|
15063
15729
|
try {
|
|
@@ -15077,6 +15743,7 @@ export class AgentSession {
|
|
|
15077
15743
|
return false;
|
|
15078
15744
|
}
|
|
15079
15745
|
}
|
|
15746
|
+
options?.onTransitionMutationStarted?.();
|
|
15080
15747
|
|
|
15081
15748
|
this.#disconnectFromAgent();
|
|
15082
15749
|
await this.abort();
|
|
@@ -15119,7 +15786,7 @@ export class AgentSession {
|
|
|
15119
15786
|
// workflow-gate emitter, and hooks cannot resolve against an ungated
|
|
15120
15787
|
// root. The manager-rotation window itself is tracked in #3138
|
|
15121
15788
|
// (#2797 / #2925).
|
|
15122
|
-
if (switchingToDifferentSession) await
|
|
15789
|
+
if (switchingToDifferentSession) await this.#initializeLocalRootForLoadedSession();
|
|
15123
15790
|
this.#syncAgentSessionId();
|
|
15124
15791
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
15125
15792
|
|
|
@@ -15141,9 +15808,13 @@ export class AgentSession {
|
|
|
15141
15808
|
this.#rebindProviderSessionState(new Map());
|
|
15142
15809
|
}
|
|
15143
15810
|
|
|
15811
|
+
const resumeModelBehavior = this.settings.get("session.resumeModelBehavior");
|
|
15144
15812
|
const configuredDefaultChain = sessionContext.configuredModelChains.default?.entries;
|
|
15813
|
+
const settingsDefaultEntries = normalizeModelSelectorValue(this.settings.getModelRole("default"));
|
|
15145
15814
|
const defaultEntries =
|
|
15146
|
-
|
|
15815
|
+
resumeModelBehavior === "useCurrentDefault"
|
|
15816
|
+
? settingsDefaultEntries
|
|
15817
|
+
: (configuredDefaultChain ?? (sessionContext.models.default ? [sessionContext.models.default] : []));
|
|
15147
15818
|
this.#defaultFallbackController = undefined;
|
|
15148
15819
|
if (defaultEntries.length > 0) {
|
|
15149
15820
|
const resolution = await resolveModelChainWithAuth(
|
|
@@ -15200,7 +15871,7 @@ export class AgentSession {
|
|
|
15200
15871
|
// Establish the successor's durable session identity only after every
|
|
15201
15872
|
// restored state facet is live. Identity-bound extension hooks run below.
|
|
15202
15873
|
await this.sessionManager.ensureOnDisk();
|
|
15203
|
-
if (!switchingToDifferentSession) await
|
|
15874
|
+
if (!switchingToDifferentSession) await this.#initializeLocalRootForLoadedSession();
|
|
15204
15875
|
|
|
15205
15876
|
if (switchingToDifferentSession) {
|
|
15206
15877
|
// The local:// migration gate for this successor already ran above,
|