@gajae-code/coding-agent 0.11.11 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +131 -4
- package/dist/types/async/job-manager.d.ts +17 -3
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +83 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/exa/mcp-client.d.ts +10 -1
- package/dist/types/exec/bash-executor.d.ts +45 -12
- package/dist/types/extensibility/extensions/types.d.ts +38 -4
- 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/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/launch-tmux.d.ts +1 -0
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +70 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +27 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +43 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +17 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +31 -80
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +10 -19
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/gjc-bundle-settings.d.ts +34 -0
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/notifications-settings-editor.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +22 -0
- package/dist/types/modes/components/settings-selector.d.ts +11 -0
- package/dist/types/modes/components/status-line/gh.d.ts +5 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +18 -1
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/reminders/star-reminder.d.ts +3 -13
- package/dist/types/runtime-mcp/manager.d.ts +1 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/adapter.d.ts +3 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
- package/dist/types/sdk/broker/transport.d.ts +2 -0
- package/dist/types/sdk/bus/attachment-registry.d.ts +1 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -1
- package/dist/types/sdk/bus/config.d.ts +6 -2
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +44 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +44 -0
- package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/reconciliation-store.d.ts +69 -0
- package/dist/types/sdk/bus/rich-draft.d.ts +1 -0
- package/dist/types/sdk/bus/rich-render.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +165 -0
- package/dist/types/sdk/bus/telegram-cli.d.ts +12 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +11 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +25 -1
- package/dist/types/sdk/bus/telegram-reference.d.ts +11 -1
- package/dist/types/sdk/bus/threaded-render.d.ts +3 -2
- package/dist/types/sdk/bus/topic-registry.d.ts +11 -1
- package/dist/types/sdk/host/control/operations.d.ts +1 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- 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 +82 -12
- package/dist/types/session/artifacts.d.ts +8 -0
- package/dist/types/session/blob-store.d.ts +57 -4
- package/dist/types/session/internal/managed-session-storage.d.ts +8 -1
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +48 -19
- package/dist/types/session/session-storage.d.ts +4 -0
- package/dist/types/session/streaming-output.d.ts +96 -12
- package/dist/types/setup/credential-import.d.ts +12 -0
- package/dist/types/skill-state/active-state.d.ts +12 -4
- package/dist/types/skill-state/workflow-hud.d.ts +9 -0
- package/dist/types/task/executor.d.ts +3 -2
- package/dist/types/task/index.d.ts +3 -1
- package/dist/types/task/provider-retry-status.d.ts +8 -0
- package/dist/types/task/receipt.d.ts +7 -1
- package/dist/types/task/types.d.ts +23 -0
- package/dist/types/tools/ask-answer-registry.d.ts +2 -2
- package/dist/types/tools/bash-interactive.d.ts +4 -1
- package/dist/types/tools/bash.d.ts +3 -1
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/image-gen.d.ts +4 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/output-meta.d.ts +44 -3
- package/dist/types/tools/path-utils.d.ts +1 -0
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -7
- package/dist/types/tools/resource-gc.d.ts +4 -0
- package/dist/types/tools/review.d.ts +5 -7
- package/dist/types/tools/subagent.d.ts +2 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- 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 +11 -13
- package/scripts/acp-conformance-agent.ts +218 -0
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-gjc-bundle-settings.ts +306 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +5 -0
- package/scripts/resident-memory-bench.ts +1341 -0
- package/scripts/run-acp-conformance.ts +285 -0
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/async/job-manager.ts +78 -20
- package/src/capability/index.ts +8 -0
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/plugin-cli.ts +176 -18
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +1 -0
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/team.ts +20 -2
- package/src/config/file-lock.ts +11 -2
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +74 -1
- package/src/config/settings.ts +246 -108
- package/src/coordinator-mcp/question-gate-codec.ts +1 -1
- package/src/coordinator-mcp/server.ts +5 -12
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +230 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +77 -41
- package/src/defaults/gjc/skills/team/SKILL.md +3 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +62 -39
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +12 -0
- package/src/defaults/gjc-defaults.ts +4 -4
- package/src/eval/py/executor.ts +7 -2
- 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 +46 -5
- 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/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -943
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/launch-tmux.ts +25 -6
- package/src/gjc-runtime/managed-owner-admission.ts +1 -1
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +1149 -98
- package/src/gjc-runtime/state-runtime.ts +2 -1
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +42 -5
- package/src/gjc-runtime/team-runtime.ts +1085 -40
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-heartbeat.ts +149 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +28 -0
- package/src/gjc-runtime/tmux-sessions.ts +58 -13
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +414 -618
- package/src/gjc-runtime/workflow-manifest.generated.json +105 -449
- package/src/gjc-runtime/workflow-manifest.ts +38 -247
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +21 -22
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +98 -0
- package/src/modes/acp/acp-agent.ts +339 -191
- package/src/modes/acp/acp-event-mapper.ts +14 -7
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/gjc-bundle-settings.ts +508 -0
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +61 -4
- package/src/modes/components/notifications-settings-editor.ts +12 -1
- package/src/modes/components/oauth-selector.ts +96 -28
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +457 -83
- package/src/modes/components/settings-selector.ts +116 -23
- package/src/modes/components/status-line/gh.ts +16 -0
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +3 -14
- package/src/modes/components/transcript-viewer-overlay.ts +99 -24
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +26 -2
- package/src/modes/controllers/input-controller.ts +18 -3
- package/src/modes/controllers/selector-controller.ts +127 -36
- package/src/modes/interactive-mode.ts +62 -3
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +22 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agent-fragments/ralplan-persistence.md +4 -3
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/system/eager-todo.md +8 -4
- package/src/prompts/system/system-prompt.md +11 -5
- package/src/prompts/tools/bash.md +5 -5
- package/src/prompts/tools/read.md +4 -2
- 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/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/acp/adapter.ts +22 -6
- package/src/sdk/broker/lifecycle.ts +25 -6
- package/src/sdk/broker/transport.ts +35 -3
- package/src/sdk/bus/attachment-registry.ts +5 -1
- package/src/sdk/bus/chat-daemon-cli.ts +4 -39
- package/src/sdk/bus/chat-daemon-control.ts +6 -3
- package/src/sdk/bus/config.ts +18 -2
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +984 -146
- package/src/sdk/bus/kind-aware-reconciliation.ts +317 -0
- package/src/sdk/bus/lifecycle-commands.ts +4 -1
- package/src/sdk/bus/lifecycle-control-runtime.ts +403 -88
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/reconciliation-store.ts +303 -0
- package/src/sdk/bus/rich-draft.ts +1 -1
- package/src/sdk/bus/rich-render.ts +2 -2
- package/src/sdk/bus/telegram-adoption-intent.ts +574 -0
- package/src/sdk/bus/telegram-cli.ts +33 -8
- package/src/sdk/bus/telegram-daemon-cli.ts +1 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +11 -4
- package/src/sdk/bus/telegram-daemon.ts +916 -23
- package/src/sdk/bus/telegram-reference.ts +40 -3
- package/src/sdk/bus/threaded-render.ts +48 -3
- package/src/sdk/bus/topic-registry.ts +20 -0
- package/src/sdk/host/control/dispatch.ts +5 -1
- package/src/sdk/host/control/operations.ts +1 -1
- package/src/sdk/host/host.ts +23 -1
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/host/query/revision-store.ts +36 -2
- package/src/sdk/host/reverse-leases.ts +44 -7
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/prompt-status.ts +16 -3
- package/src/sdk/protocol/operation-inventory.generated.json +51 -0
- package/src/sdk/protocol/operation-registry.ts +7 -2
- package/src/sdk/session.ts +101 -10
- package/src/session/agent-session.ts +1286 -220
- package/src/session/artifacts.ts +22 -0
- package/src/session/auth-broker-config.ts +10 -3
- package/src/session/blob-store.ts +915 -24
- package/src/session/internal/managed-session-scope.ts +44 -6
- package/src/session/internal/managed-session-storage.ts +48 -6
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +2364 -438
- package/src/session/session-storage.ts +9 -1
- package/src/session/streaming-output.ts +500 -111
- package/src/setup/credential-import.ts +75 -13
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +32 -10
- package/src/skill-state/workflow-hud.ts +42 -2
- package/src/skill-state/workflow-mutation-guard.ts +560 -8
- package/src/task/executor.ts +122 -39
- package/src/task/index.ts +23 -14
- package/src/task/provider-retry-status.ts +16 -0
- package/src/task/receipt.ts +23 -7
- package/src/task/render.ts +6 -2
- package/src/task/types.ts +108 -0
- package/src/tools/ask-answer-registry.ts +12 -6
- package/src/tools/ask.ts +169 -118
- 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/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/find.ts +1 -0
- package/src/tools/image-gen.ts +29 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/output-meta.ts +281 -20
- package/src/tools/path-utils.ts +8 -3
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1560 -310
- package/src/tools/resource-gc.ts +182 -18
- 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/tools/tool-result.ts +14 -1
- 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/codex.ts +1 -1
- 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/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- 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/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
- package/src/gjc-runtime/ultragoal-pipeline.ts +0 -306
|
@@ -18,6 +18,7 @@ import * as crypto from "node:crypto";
|
|
|
18
18
|
import * as fs from "node:fs";
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import { scheduler } from "node:timers/promises";
|
|
21
|
+
import * as util from "node:util";
|
|
21
22
|
import {
|
|
22
23
|
type AfterToolCallContext,
|
|
23
24
|
type AfterToolCallResult,
|
|
@@ -35,6 +36,7 @@ import {
|
|
|
35
36
|
type ManagedAttemptDecision,
|
|
36
37
|
type ManagedAttemptOutcome,
|
|
37
38
|
type MidRunMaintenanceOutcome,
|
|
39
|
+
type RunSettlementProof,
|
|
38
40
|
resolveTelemetry,
|
|
39
41
|
type StablePrefixSnapshot,
|
|
40
42
|
ThinkingLevel,
|
|
@@ -81,6 +83,7 @@ import type {
|
|
|
81
83
|
ProviderSessionState,
|
|
82
84
|
ServiceTier,
|
|
83
85
|
SimpleStreamOptions,
|
|
86
|
+
StopReason,
|
|
84
87
|
TextContent,
|
|
85
88
|
ToolCall,
|
|
86
89
|
ToolChoice,
|
|
@@ -104,6 +107,7 @@ import {
|
|
|
104
107
|
classifyFallbackTrigger,
|
|
105
108
|
type FallbackAttemptToken,
|
|
106
109
|
type FallbackTriggerClass,
|
|
110
|
+
STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
|
|
107
111
|
} from "@gajae-code/ai/utils/fallback-transport";
|
|
108
112
|
import {
|
|
109
113
|
BTW_MAX_ANSWER_UTF8_BYTES,
|
|
@@ -115,6 +119,7 @@ import {
|
|
|
115
119
|
truncateUtf8,
|
|
116
120
|
utf8ByteLength,
|
|
117
121
|
} from "./btw-contract";
|
|
122
|
+
import { DEFAULT_ARTIFACT_MAX_BYTES, truncateHeadBytes } from "./streaming-output";
|
|
118
123
|
|
|
119
124
|
export interface ForkContextSeedMetadata {
|
|
120
125
|
sourceSessionId: string;
|
|
@@ -161,12 +166,15 @@ export interface ForkContextSeedOptions {
|
|
|
161
166
|
import { MacOSPowerAssertion } from "@gajae-code/natives";
|
|
162
167
|
import {
|
|
163
168
|
extractRetryHint,
|
|
169
|
+
hasFsCode,
|
|
170
|
+
isEacces,
|
|
164
171
|
isEnoent,
|
|
165
172
|
isUnexpectedSocketCloseMessage,
|
|
166
173
|
logger,
|
|
167
174
|
prompt,
|
|
168
175
|
Snowflake,
|
|
169
176
|
} from "@gajae-code/utils";
|
|
177
|
+
|
|
170
178
|
import { createAppendOnlyContextManager, resolveAppendOnlyMode } from "../append-only-mode";
|
|
171
179
|
import { type AsyncJob, type AsyncJobDeliveryState, AsyncJobManager } from "../async";
|
|
172
180
|
import { reset as resetCapabilities } from "../capability";
|
|
@@ -207,7 +215,7 @@ import {
|
|
|
207
215
|
executePython as executePythonCommand,
|
|
208
216
|
type PythonResult,
|
|
209
217
|
} from "../eval/py/executor";
|
|
210
|
-
import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
|
|
218
|
+
import { type BashArtifactSaveResult, type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
|
|
211
219
|
import { exportSessionToHtml } from "../export/html";
|
|
212
220
|
import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
|
|
213
221
|
import type { LoadedCustomCommand } from "../extensibility/custom-commands";
|
|
@@ -241,9 +249,11 @@ import {
|
|
|
241
249
|
resolveSubskillActivationForSkillInvocation,
|
|
242
250
|
} from "../extensibility/gjc-plugins";
|
|
243
251
|
import { resolveCurrentPhaseForParent } from "../extensibility/gjc-plugins/injection";
|
|
252
|
+
import type { GjcRuntimeSnapshotProvider } from "../extensibility/gjc-plugins/runtime-quarantine";
|
|
244
253
|
import { readActiveSubskillsForParent, toActiveSubskillEntry } from "../extensibility/gjc-plugins/state";
|
|
245
254
|
import { loadActiveSubskillTools } from "../extensibility/gjc-plugins/tools";
|
|
246
255
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
256
|
+
import type { SessionSwitchEvent } from "../extensibility/shared-events";
|
|
247
257
|
import {
|
|
248
258
|
buildSkillPromptMessage,
|
|
249
259
|
getSkillSlashCommandName,
|
|
@@ -254,6 +264,11 @@ import {
|
|
|
254
264
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
255
265
|
import { assertDeepInterviewIntentManifest } from "../gjc-runtime/deep-interview-state";
|
|
256
266
|
import { buildGjcRuntimeSessionEnv, consumePendingGoalModeRequest } from "../gjc-runtime/goal-mode-request";
|
|
267
|
+
import {
|
|
268
|
+
isMemoryGuardClaimsLease,
|
|
269
|
+
isMemoryGuardClaimsLeaseForStateDir,
|
|
270
|
+
type MemoryGuardClaimsLease,
|
|
271
|
+
} from "../gjc-runtime/memory-guard-owner-claims";
|
|
257
272
|
import {
|
|
258
273
|
assertNonEmptyGjcSessionId,
|
|
259
274
|
modeStatePath as sessionModeStatePath,
|
|
@@ -265,6 +280,7 @@ import {
|
|
|
265
280
|
registerCoordinatorRuntimeStateFinalizer,
|
|
266
281
|
} from "../gjc-runtime/session-state-sidecar";
|
|
267
282
|
import { requestGjcWorkerIntegrationAttempt } from "../gjc-runtime/team-runtime";
|
|
283
|
+
import { GjcTeamWorkerHeartbeatReporter } from "../gjc-runtime/team-worker-heartbeat";
|
|
268
284
|
import { GoalRuntime } from "../goals/runtime";
|
|
269
285
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
270
286
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
@@ -298,6 +314,7 @@ import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obf
|
|
|
298
314
|
import { formatNoCredentialOnboardingError, formatNoModelOnboardingError } from "../setup/model-onboarding-guidance";
|
|
299
315
|
import {
|
|
300
316
|
isCanonicalGjcWorkflowSkill,
|
|
317
|
+
isWorkflowContinuationInert,
|
|
301
318
|
readVisibleSkillActiveState,
|
|
302
319
|
syncSkillActiveState,
|
|
303
320
|
} from "../skill-state/active-state";
|
|
@@ -368,6 +385,7 @@ import {
|
|
|
368
385
|
prepareContributionPrep,
|
|
369
386
|
} from "./contribution-prep";
|
|
370
387
|
import { pruneStaleFileMentions } from "./file-mention-pruning";
|
|
388
|
+
import type { MemoryGuardRestoreResult } from "./memory-guard-checkpoint-participant";
|
|
371
389
|
import {
|
|
372
390
|
type BashExecutionMessage,
|
|
373
391
|
type CompactionSummaryMessage,
|
|
@@ -409,6 +427,32 @@ import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
|
409
427
|
import { pruneSupersededMaintenanceReminders, pruneSupersededVolatileProjectContext } from "./volatile-context-pruning";
|
|
410
428
|
import { YieldQueue } from "./yield-queue";
|
|
411
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
|
+
|
|
412
456
|
/** Session-specific events that extend the core AgentEvent */
|
|
413
457
|
export type AutoCompactionContinuationSkipReason = "auto_continue_disabled_non_resumable_tail";
|
|
414
458
|
|
|
@@ -498,6 +542,11 @@ export interface AgentSessionConfig {
|
|
|
498
542
|
workerIntegrationRequest?: (signal: AbortSignal) => Promise<void>;
|
|
499
543
|
/** Bound terminal worker-integration settlement for embedded hosts and deterministic lifecycle tests. */
|
|
500
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;
|
|
501
550
|
|
|
502
551
|
/** Loaded skills (already discovered by SDK) */
|
|
503
552
|
skills?: Skill[];
|
|
@@ -613,6 +662,24 @@ export interface AgentSessionConfig {
|
|
|
613
662
|
providerCacheSessionId?: string;
|
|
614
663
|
}
|
|
615
664
|
|
|
665
|
+
export interface AgentSessionMemoryGuardRestoreInput
|
|
666
|
+
extends Omit<AgentSessionConfig, "sessionManager" | "recoveryHydrationContext"> {
|
|
667
|
+
staged: Extract<MemoryGuardRestoreResult, { kind: "staged" }>;
|
|
668
|
+
claimsLease: MemoryGuardClaimsLease;
|
|
669
|
+
claimsStateDir: string;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
export interface AgentMemoryGuardPromotionFence extends RecoveryHydrationPromotionFence {
|
|
673
|
+
readonly claimsLease: MemoryGuardClaimsLease;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
export type AgentMemoryGuardRestoreResult =
|
|
677
|
+
| { kind: "staged"; session: AgentSession; promotionFence: AgentMemoryGuardPromotionFence }
|
|
678
|
+
| {
|
|
679
|
+
kind: "blocked";
|
|
680
|
+
reason: "transcript-mismatch" | "hydration-context-mismatch" | "claim-mismatch" | "agent-messages-mismatch";
|
|
681
|
+
};
|
|
682
|
+
|
|
616
683
|
type MidRunMaintenanceLifecycle = Parameters<NonNullable<AgentLoopConfig["maintainContext"]>>[1];
|
|
617
684
|
|
|
618
685
|
type AutoCompactionTerminalStatus =
|
|
@@ -642,8 +709,16 @@ export interface PromptOptions {
|
|
|
642
709
|
/**
|
|
643
710
|
* Invoked after all prompt preflight checks pass and immediately before agent execution begins.
|
|
644
711
|
* Cancellation before this callback rejects the prompt.
|
|
712
|
+
* Prefer `onPreflightAcceptCommit` for async durable acceptance (#3031/#3032).
|
|
645
713
|
*/
|
|
646
714
|
onPreflightAccepted?: () => void;
|
|
715
|
+
/**
|
|
716
|
+
* Awaitable durable-accept fence. Called after preflight and before queue mutation
|
|
717
|
+
* or `#promptAgentWithIdleRetry`. SDK bus installs a closure that fsyncs acceptance.
|
|
718
|
+
*/
|
|
719
|
+
onPreflightAcceptCommit?: () => void | Promise<void>;
|
|
720
|
+
/** Skill-only: prepared metadata before the durable fence (path/lineCount/cleanedArgs). */
|
|
721
|
+
onSkillPrepared?: (meta: { name: string; path: string; lineCount?: number; cleanedArgs?: string }) => void;
|
|
647
722
|
}
|
|
648
723
|
|
|
649
724
|
function promptPreflightCancelledError(): Error {
|
|
@@ -794,6 +869,7 @@ type RetryErrorClassification =
|
|
|
794
869
|
|
|
795
870
|
const BARE_DEFAULT_WATCHDOG_ERROR =
|
|
796
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(?:, [^)]+)*\)$/;
|
|
797
873
|
const KIMI_CODE_FIRST_EVENT_TIMEOUT_MESSAGES = {
|
|
798
874
|
"anthropic-messages": new Set([
|
|
799
875
|
"Provider stream timed out while waiting for the first event",
|
|
@@ -830,6 +906,14 @@ function hasBareDefaultRetryDisqualifyingFacts(message: AssistantMessage): boole
|
|
|
830
906
|
(facts.headers !== undefined && Object.keys(facts.headers).length > 0)
|
|
831
907
|
);
|
|
832
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
|
+
}
|
|
833
917
|
|
|
834
918
|
function assistantMessageHasVisibleOrToolContent(message: AssistantMessage): boolean {
|
|
835
919
|
return message.content.some(content => {
|
|
@@ -932,6 +1016,50 @@ interface EphemeralTurnResult {
|
|
|
932
1016
|
*/
|
|
933
1017
|
const SIGNAL_TEARDOWN_TIMEOUT_MS = 5_000;
|
|
934
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
|
+
|
|
935
1063
|
/**
|
|
936
1064
|
* Throttle window for the per-turn volatile workspace-tree scan. Date/cwd are
|
|
937
1065
|
* refreshed every turn; the mtime-sorted tree is rescanned + re-embedded at most
|
|
@@ -1538,6 +1666,47 @@ export class AgentSession {
|
|
|
1538
1666
|
#sessionAdmissionClosed = false;
|
|
1539
1667
|
#sessionAdmissionContext = new AsyncLocalStorage<SessionAdmissionEntry>();
|
|
1540
1668
|
#defaultModelSelectionMutationRevision = 0;
|
|
1669
|
+
#thinkingLevelMutationRevision = 0;
|
|
1670
|
+
#thinkingVisibilityMutationRevision = 0;
|
|
1671
|
+
#thinkingLevelLiveMutationRevision = 0;
|
|
1672
|
+
#thinkingVisibilityLiveMutationRevision = 0;
|
|
1673
|
+
#reasoningControlContextGeneration = 0;
|
|
1674
|
+
#pendingThinkingLevelControlSuccess:
|
|
1675
|
+
| {
|
|
1676
|
+
level: ThinkingLevel;
|
|
1677
|
+
mutationRevision: number;
|
|
1678
|
+
sessionId: string;
|
|
1679
|
+
model: Model | undefined;
|
|
1680
|
+
contextGeneration: number;
|
|
1681
|
+
}
|
|
1682
|
+
| undefined;
|
|
1683
|
+
#pendingThinkingVisibilityControlSuccess:
|
|
1684
|
+
| {
|
|
1685
|
+
visibility: "visible" | "hidden";
|
|
1686
|
+
mutationRevision: number;
|
|
1687
|
+
sessionId: string;
|
|
1688
|
+
model: Model | undefined;
|
|
1689
|
+
contextGeneration: number;
|
|
1690
|
+
}
|
|
1691
|
+
| undefined;
|
|
1692
|
+
#pendingThinkingLevelControlFailure:
|
|
1693
|
+
| {
|
|
1694
|
+
mutationRevision: number;
|
|
1695
|
+
liveMutationRevision: number;
|
|
1696
|
+
sessionId: string;
|
|
1697
|
+
model: Model | undefined;
|
|
1698
|
+
contextGeneration: number;
|
|
1699
|
+
}
|
|
1700
|
+
| undefined;
|
|
1701
|
+
#pendingThinkingVisibilityControlFailure:
|
|
1702
|
+
| {
|
|
1703
|
+
mutationRevision: number;
|
|
1704
|
+
liveMutationRevision: number;
|
|
1705
|
+
sessionId: string;
|
|
1706
|
+
model: Model | undefined;
|
|
1707
|
+
contextGeneration: number;
|
|
1708
|
+
}
|
|
1709
|
+
| undefined;
|
|
1541
1710
|
#promptTemplates: PromptTemplate[];
|
|
1542
1711
|
#slashCommands: FileSlashCommand[];
|
|
1543
1712
|
|
|
@@ -1623,6 +1792,7 @@ export class AgentSession {
|
|
|
1623
1792
|
#resetRetryReplaySafety(): void {
|
|
1624
1793
|
this.#retryReplayEpoch++;
|
|
1625
1794
|
this.#retryReplayUnsafeEpoch = undefined;
|
|
1795
|
+
this.#firstEventTimeoutRetryStartedAt = Date.now();
|
|
1626
1796
|
if (
|
|
1627
1797
|
this.#extensionRunner?.hasHandlers("context") ||
|
|
1628
1798
|
this.#extensionRunner?.hasHandlers("before_provider_request") ||
|
|
@@ -1656,6 +1826,7 @@ export class AgentSession {
|
|
|
1656
1826
|
// Retry state
|
|
1657
1827
|
#retryAbortController: AbortController | undefined = undefined;
|
|
1658
1828
|
#retryNowRequested = false;
|
|
1829
|
+
#firstEventTimeoutRetryStartedAt: number | undefined;
|
|
1659
1830
|
#retryAttempt = 0;
|
|
1660
1831
|
#retryPromise: Promise<void> | undefined = undefined;
|
|
1661
1832
|
#retryResolve: (() => void) | undefined = undefined;
|
|
@@ -1720,6 +1891,10 @@ export class AgentSession {
|
|
|
1720
1891
|
#turnIndex = 0;
|
|
1721
1892
|
#workerIntegrationScheduler: WorkerIntegrationRequestScheduler;
|
|
1722
1893
|
#workerIntegrationRequestedForTurn = false;
|
|
1894
|
+
/** Runtime-owned team worker heartbeat; `undefined` outside a `gjc team` worker pane. */
|
|
1895
|
+
#teamWorkerHeartbeat: GjcTeamWorkerHeartbeatReporter | undefined;
|
|
1896
|
+
#teamWorkerTurnsInFlight = 0;
|
|
1897
|
+
#unregisterTeamWorkerAsyncJobChange: (() => void) | undefined;
|
|
1723
1898
|
// First-party internal before-agent-start contributors (not user hooks).
|
|
1724
1899
|
#beforeAgentStartContributors: BeforeAgentStartContributor[] = [];
|
|
1725
1900
|
|
|
@@ -1789,6 +1964,7 @@ export class AgentSession {
|
|
|
1789
1964
|
#constructorMCPToolSelection: string[] | undefined;
|
|
1790
1965
|
#constructorDiscoveredBuiltinToolSelection: string[] | undefined;
|
|
1791
1966
|
#recoveryHydrationContext: RecoveryHydrationContext | undefined;
|
|
1967
|
+
#memoryGuardClaimsLease: MemoryGuardClaimsLease | undefined;
|
|
1792
1968
|
|
|
1793
1969
|
// TTSR manager for time-traveling stream rules
|
|
1794
1970
|
#ttsrManager: TtsrManager | undefined = undefined;
|
|
@@ -2073,6 +2249,8 @@ export class AgentSession {
|
|
|
2073
2249
|
this.#promptInFlightCount++;
|
|
2074
2250
|
if (this.#promptInFlightCount === 1) {
|
|
2075
2251
|
this.#acquirePowerAssertion();
|
|
2252
|
+
// Runtime-owned worker liveness follows Agent turns and background jobs
|
|
2253
|
+
// in #refreshTeamWorkerHeartbeat(), including internally dispatched turns.
|
|
2076
2254
|
}
|
|
2077
2255
|
}
|
|
2078
2256
|
|
|
@@ -2129,6 +2307,15 @@ export class AgentSession {
|
|
|
2129
2307
|
this.#flushPendingAgentEnd();
|
|
2130
2308
|
}
|
|
2131
2309
|
|
|
2310
|
+
#retainDeferredAgentEndAfterContinuationBusy(
|
|
2311
|
+
hold: symbol | undefined,
|
|
2312
|
+
pending: AgentSessionEvent | undefined,
|
|
2313
|
+
): void {
|
|
2314
|
+
if (!hold || !pending) return;
|
|
2315
|
+
this.#pendingAgentEndEmit = pending;
|
|
2316
|
+
this.#pendingAgentEndContinuationHolds.set(hold, pending);
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2132
2319
|
#releaseDeferredAgentEndContinuation(hold: symbol | undefined): void {
|
|
2133
2320
|
if (!hold) return;
|
|
2134
2321
|
const pending = this.#pendingAgentEndContinuationHolds.get(hold);
|
|
@@ -2179,11 +2366,23 @@ export class AgentSession {
|
|
|
2179
2366
|
this.#promptInFlightCount = Math.max(0, this.#promptInFlightCount - 1);
|
|
2180
2367
|
if (this.#promptInFlightCount === 0) {
|
|
2181
2368
|
this.#releasePowerAssertion();
|
|
2369
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
2182
2370
|
this.#flushPendingBackgroundExchanges();
|
|
2183
2371
|
this.#flushPendingAgentEnd();
|
|
2184
2372
|
}
|
|
2185
2373
|
}
|
|
2186
2374
|
|
|
2375
|
+
#refreshTeamWorkerHeartbeat(): void {
|
|
2376
|
+
const hasOwnedBackgroundJob =
|
|
2377
|
+
this.#agentId !== undefined &&
|
|
2378
|
+
(this.#ownedAsyncJobManager?.getRunningJobs({ ownerId: this.#agentId }).length ?? 0) > 0;
|
|
2379
|
+
if (this.#teamWorkerTurnsInFlight > 0 || hasOwnedBackgroundJob) {
|
|
2380
|
+
this.#teamWorkerHeartbeat?.start();
|
|
2381
|
+
} else {
|
|
2382
|
+
this.#teamWorkerHeartbeat?.stop();
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
|
|
2187
2386
|
#flushPendingAgentEnd(): void {
|
|
2188
2387
|
if (
|
|
2189
2388
|
this.#promptInFlightCount > 0 ||
|
|
@@ -2235,6 +2434,14 @@ export class AgentSession {
|
|
|
2235
2434
|
}),
|
|
2236
2435
|
config.workerIntegrationTimeoutMs,
|
|
2237
2436
|
);
|
|
2437
|
+
// One publisher per worker process: subagent sessions share this process and its
|
|
2438
|
+
// team env, so letting each create a reporter would run N timers writing the same
|
|
2439
|
+
// record. The top-level session owns the pane's liveness.
|
|
2440
|
+
this.#teamWorkerHeartbeat =
|
|
2441
|
+
config.teamWorkerHeartbeatReporter ??
|
|
2442
|
+
((config.taskDepth ?? 0) === 0
|
|
2443
|
+
? GjcTeamWorkerHeartbeatReporter.forProcess(() => this.sessionManager.getCwd())
|
|
2444
|
+
: undefined);
|
|
2238
2445
|
this.notificationSessionController = config.notificationSessionController;
|
|
2239
2446
|
this.taskDepth = config.taskDepth ?? 0;
|
|
2240
2447
|
// Register this session with the process-wide resource GC (idle/RSS browser-tab eviction
|
|
@@ -2244,6 +2451,7 @@ export class AgentSession {
|
|
|
2244
2451
|
this.#unregisterResourceGc = registerResourceGcSession({
|
|
2245
2452
|
sessionId: resourceGcSessionId,
|
|
2246
2453
|
settings: this.settings,
|
|
2454
|
+
cwd: () => this.sessionManager.getCwd(),
|
|
2247
2455
|
});
|
|
2248
2456
|
}
|
|
2249
2457
|
this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer({
|
|
@@ -2395,6 +2603,9 @@ export class AgentSession {
|
|
|
2395
2603
|
this.#ttsrManager = config.ttsrManager;
|
|
2396
2604
|
this.#obfuscator = config.obfuscator;
|
|
2397
2605
|
this.#agentId = config.agentId;
|
|
2606
|
+
this.#unregisterTeamWorkerAsyncJobChange = this.#ownedAsyncJobManager?.onChange(() => {
|
|
2607
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
2608
|
+
});
|
|
2398
2609
|
this.#agentRegistry = config.agentRegistry;
|
|
2399
2610
|
this.#providerSessionId = config.providerSessionId;
|
|
2400
2611
|
this.#providerCacheSessionId = config.providerCacheSessionId;
|
|
@@ -2489,7 +2700,24 @@ export class AgentSession {
|
|
|
2489
2700
|
|
|
2490
2701
|
/** Advance the tool-choice queue and return the next directive for the upcoming LLM call. */
|
|
2491
2702
|
nextToolChoice(): ToolChoice | undefined {
|
|
2492
|
-
|
|
2703
|
+
const choice = this.#toolChoiceQueue.nextToolChoice();
|
|
2704
|
+
if (!choice || typeof choice === "string") return choice;
|
|
2705
|
+
|
|
2706
|
+
const toolName = choice.type === "tool" ? choice.name : "function" in choice ? choice.function.name : choice.name;
|
|
2707
|
+
const activeTool = this.agent.state.tools.find(
|
|
2708
|
+
tool => tool.name === toolName || tool.customWireName === toolName,
|
|
2709
|
+
);
|
|
2710
|
+
if (!activeTool) {
|
|
2711
|
+
this.#toolChoiceQueue.degradeInFlight(`Tool "${toolName}" is no longer active.`);
|
|
2712
|
+
return undefined;
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
const refreshed = buildNamedToolChoiceResult(activeTool.name, this.model);
|
|
2716
|
+
if (!refreshed.exactNamed || !refreshed.choice) {
|
|
2717
|
+
this.#toolChoiceQueue.degradeInFlight(refreshed.resolved?.reason ?? "Named tool choice is unavailable.");
|
|
2718
|
+
return undefined;
|
|
2719
|
+
}
|
|
2720
|
+
return refreshed.choice;
|
|
2493
2721
|
}
|
|
2494
2722
|
|
|
2495
2723
|
/**
|
|
@@ -2671,9 +2899,15 @@ export class AgentSession {
|
|
|
2671
2899
|
this.#defaultFallbackController = scope.fallbackController;
|
|
2672
2900
|
const previousEditMode = this.#resolveActiveEditMode();
|
|
2673
2901
|
if (scope.model) {
|
|
2674
|
-
this
|
|
2902
|
+
this.#setAgentModelWithReasoningContext(scope.model);
|
|
2675
2903
|
this.#syncAppendOnlyContext(scope.model);
|
|
2676
2904
|
}
|
|
2905
|
+
this.#thinkingLevelMutationRevision++;
|
|
2906
|
+
this.#thinkingLevelLiveMutationRevision++;
|
|
2907
|
+
this.#pendingThinkingLevelControlSuccess = undefined;
|
|
2908
|
+
this.#pendingThinkingLevelControlFailure = undefined;
|
|
2909
|
+
this.#pendingThinkingVisibilityControlSuccess = undefined;
|
|
2910
|
+
this.#pendingThinkingVisibilityControlFailure = undefined;
|
|
2677
2911
|
this.#thinkingLevel = scope.thinkingLevel;
|
|
2678
2912
|
this.agent.setThinkingLevel(toReasoningEffort(scope.thinkingLevel));
|
|
2679
2913
|
void this.#syncEditToolModeAfterModelChange(previousEditMode);
|
|
@@ -3113,19 +3347,26 @@ export class AgentSession {
|
|
|
3113
3347
|
return queued;
|
|
3114
3348
|
}
|
|
3115
3349
|
|
|
3116
|
-
#trackAgentEvent =
|
|
3350
|
+
#trackAgentEvent = (event: AgentEvent): Promise<void> => {
|
|
3351
|
+
const activePromptHandle = this.activePromptHandle;
|
|
3117
3352
|
const agentEndHandled = event.type === "agent_end" ? Promise.withResolvers<void>() : undefined;
|
|
3118
3353
|
if (agentEndHandled) this.#agentEndHandlingPromise = agentEndHandled.promise;
|
|
3119
3354
|
this.#agentEventHandlersInFlight++;
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3355
|
+
const handler = (async (): Promise<void> => {
|
|
3356
|
+
try {
|
|
3357
|
+
await this.#handleAgentEvent(event, activePromptHandle);
|
|
3358
|
+
} catch (error) {
|
|
3359
|
+
logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
|
|
3360
|
+
} finally {
|
|
3361
|
+
this.#agentEventHandlersInFlight = Math.max(0, this.#agentEventHandlersInFlight - 1);
|
|
3362
|
+
this.#flushPendingAgentEnd();
|
|
3363
|
+
agentEndHandled?.resolve();
|
|
3364
|
+
}
|
|
3365
|
+
})();
|
|
3366
|
+
if (activePromptHandle) {
|
|
3367
|
+
this.agent.resourceLedger.track(activePromptHandle, "post_prompt", "agent-session-event", handler);
|
|
3128
3368
|
}
|
|
3369
|
+
return handler;
|
|
3129
3370
|
};
|
|
3130
3371
|
|
|
3131
3372
|
#persistRuntimeStateInBackground(event: AgentSessionEvent): void {
|
|
@@ -3145,6 +3386,18 @@ export class AgentSession {
|
|
|
3145
3386
|
} else if (event.type === "turn_end") {
|
|
3146
3387
|
this.#closedExtensionTurnGeneration = this.#extensionTurnGeneration;
|
|
3147
3388
|
}
|
|
3389
|
+
if (event.type === "turn_start") {
|
|
3390
|
+
this.#teamWorkerTurnsInFlight++;
|
|
3391
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
3392
|
+
} else if (event.type === "turn_end") {
|
|
3393
|
+
this.#teamWorkerTurnsInFlight = Math.max(0, this.#teamWorkerTurnsInFlight - 1);
|
|
3394
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
3395
|
+
} else if (event.type === "agent_end") {
|
|
3396
|
+
// Failed Agent runs can emit agent_end without turn_end. Reset rather than
|
|
3397
|
+
// leaking a positive count that would report an idle/failed worker forever.
|
|
3398
|
+
this.#teamWorkerTurnsInFlight = 0;
|
|
3399
|
+
this.#refreshTeamWorkerHeartbeat();
|
|
3400
|
+
}
|
|
3148
3401
|
if (event.type === "message_update") {
|
|
3149
3402
|
// Fast path: message_update maps to no sidecar state, so we must not
|
|
3150
3403
|
// build the persistRuntimeState closure here (per-token hot path).
|
|
@@ -3264,7 +3517,7 @@ export class AgentSession {
|
|
|
3264
3517
|
}
|
|
3265
3518
|
|
|
3266
3519
|
/** Internal handler for agent events - shared by subscribe and reconnect */
|
|
3267
|
-
#handleAgentEvent = async (event: AgentEvent): Promise<void> => {
|
|
3520
|
+
#handleAgentEvent = async (event: AgentEvent, activePromptHandle?: string): Promise<void> => {
|
|
3268
3521
|
if (this.#extensionRunner?.hasHandlers(event.type)) this.#markRetryReplayUnsafe();
|
|
3269
3522
|
if (
|
|
3270
3523
|
event.type === "tool_execution_start" ||
|
|
@@ -3613,9 +3866,10 @@ export class AgentSession {
|
|
|
3613
3866
|
this.#ttsrAbortPending = false;
|
|
3614
3867
|
this.#resolveTtsrResume();
|
|
3615
3868
|
},
|
|
3869
|
+
resourceRunId: activePromptHandle,
|
|
3616
3870
|
});
|
|
3617
3871
|
},
|
|
3618
|
-
{ delayMs: 50 },
|
|
3872
|
+
{ delayMs: 50, resourceRunId: activePromptHandle },
|
|
3619
3873
|
);
|
|
3620
3874
|
return;
|
|
3621
3875
|
}
|
|
@@ -3774,7 +4028,11 @@ export class AgentSession {
|
|
|
3774
4028
|
maintenanceGeneration !== undefined &&
|
|
3775
4029
|
this.#promptGeneration === maintenanceGeneration
|
|
3776
4030
|
) {
|
|
3777
|
-
this.#scheduleAgentContinue({
|
|
4031
|
+
this.#scheduleAgentContinue({
|
|
4032
|
+
generation: maintenanceGeneration,
|
|
4033
|
+
skipCompactionCheck: true,
|
|
4034
|
+
resourceRunId: activePromptHandle,
|
|
4035
|
+
});
|
|
3778
4036
|
}
|
|
3779
4037
|
return;
|
|
3780
4038
|
}
|
|
@@ -3854,8 +4112,11 @@ export class AgentSession {
|
|
|
3854
4112
|
}
|
|
3855
4113
|
this.#resolveRetry();
|
|
3856
4114
|
|
|
3857
|
-
const compactionTask = this.#checkCompaction(msg);
|
|
3858
|
-
this.#trackPostPromptTask(
|
|
4115
|
+
const compactionTask = this.#checkCompaction(msg, true, undefined, activePromptHandle);
|
|
4116
|
+
this.#trackPostPromptTask(
|
|
4117
|
+
compactionTask.then(() => undefined),
|
|
4118
|
+
activePromptHandle,
|
|
4119
|
+
);
|
|
3859
4120
|
await compactionTask;
|
|
3860
4121
|
// Check for incomplete todos only after a final assistant stop, not intermediate tool-use turns.
|
|
3861
4122
|
const hasToolCalls = msg.content.some(content => content.type === "toolCall");
|
|
@@ -3919,9 +4180,12 @@ export class AgentSession {
|
|
|
3919
4180
|
this.#postPromptTasksPromise = undefined;
|
|
3920
4181
|
}
|
|
3921
4182
|
|
|
3922
|
-
#trackPostPromptTask(task: Promise<void
|
|
4183
|
+
#trackPostPromptTask(task: Promise<void>, resourceRunId = this.activePromptHandle): void {
|
|
3923
4184
|
this.#postPromptTasks.add(task);
|
|
3924
4185
|
this.#ensurePostPromptTasksPromise();
|
|
4186
|
+
if (resourceRunId) {
|
|
4187
|
+
this.agent.resourceLedger.track(resourceRunId, "post_prompt", "agent-session", task);
|
|
4188
|
+
}
|
|
3925
4189
|
void task
|
|
3926
4190
|
.catch(() => {})
|
|
3927
4191
|
.finally(() => {
|
|
@@ -3934,7 +4198,7 @@ export class AgentSession {
|
|
|
3934
4198
|
|
|
3935
4199
|
#schedulePostPromptTask(
|
|
3936
4200
|
task: (signal: AbortSignal) => Promise<void>,
|
|
3937
|
-
options?: { delayMs?: number; generation?: number; onSkip?: () => void },
|
|
4201
|
+
options?: { delayMs?: number; generation?: number; onSkip?: () => void; resourceRunId?: string },
|
|
3938
4202
|
): Promise<void> {
|
|
3939
4203
|
const delayMs = options?.delayMs ?? 0;
|
|
3940
4204
|
const signal = this.#postPromptTasksAbortController.signal;
|
|
@@ -3957,10 +4221,14 @@ export class AgentSession {
|
|
|
3957
4221
|
}
|
|
3958
4222
|
await task(signal);
|
|
3959
4223
|
})();
|
|
3960
|
-
this.#trackPostPromptTask(scheduled);
|
|
4224
|
+
this.#trackPostPromptTask(scheduled, options?.resourceRunId);
|
|
3961
4225
|
return scheduled;
|
|
3962
4226
|
}
|
|
3963
4227
|
|
|
4228
|
+
#isAgentBusyError(error: unknown): error is AgentBusyError {
|
|
4229
|
+
return error instanceof AgentBusyError;
|
|
4230
|
+
}
|
|
4231
|
+
|
|
3964
4232
|
#scheduleAgentContinue(options?: {
|
|
3965
4233
|
delayMs?: number;
|
|
3966
4234
|
generation?: number;
|
|
@@ -3969,7 +4237,9 @@ export class AgentSession {
|
|
|
3969
4237
|
shouldContinue?: () => boolean;
|
|
3970
4238
|
onSkip?: (reason: "generation_changed" | "aborted_signal" | "queue_drained" | "handoff_in_progress") => void;
|
|
3971
4239
|
allowDuringCancelAndSubmit?: boolean;
|
|
4240
|
+
rescheduleOnBusy?: boolean;
|
|
3972
4241
|
onError?: (error: unknown) => void;
|
|
4242
|
+
resourceRunId?: string;
|
|
3973
4243
|
}): Promise<void> {
|
|
3974
4244
|
const predecessorAgentEndHold = options?.suppressPredecessorAgentEnd
|
|
3975
4245
|
? this.#reserveDeferredAgentEndForContinuation()
|
|
@@ -3992,81 +4262,92 @@ export class AgentSession {
|
|
|
3992
4262
|
};
|
|
3993
4263
|
const scheduledGeneration = options?.generation;
|
|
3994
4264
|
const signal = this.#postPromptTasksAbortController.signal;
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
if (!options?.skipCompactionCheck) {
|
|
4019
|
-
await this.#checkEstimatedContextBeforePrompt();
|
|
4020
|
-
if (!canContinue()) return;
|
|
4021
|
-
}
|
|
4022
|
-
if (signal.aborted) {
|
|
4023
|
-
skip("aborted_signal");
|
|
4024
|
-
return;
|
|
4025
|
-
}
|
|
4026
|
-
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
4027
|
-
skip("generation_changed");
|
|
4028
|
-
return;
|
|
4029
|
-
}
|
|
4030
|
-
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
4031
|
-
skip("queue_drained");
|
|
4032
|
-
return;
|
|
4033
|
-
}
|
|
4034
|
-
// A continuation scheduled before a handoff engaged must not start a
|
|
4035
|
-
// turn against the session being handed off (or the restored
|
|
4036
|
-
// predecessor). rearmIdle / normal delivery resumes after the fence.
|
|
4037
|
-
if (this.#handoffTransitionActive) {
|
|
4038
|
-
skip("handoff_in_progress");
|
|
4039
|
-
return;
|
|
4040
|
-
}
|
|
4041
|
-
const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
|
|
4042
|
-
const startsQueuedSuccessor =
|
|
4043
|
-
this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
|
|
4265
|
+
const scheduleAttempt = (delayMs = options?.delayMs): Promise<void> =>
|
|
4266
|
+
this.#schedulePostPromptTask(
|
|
4267
|
+
async () => {
|
|
4268
|
+
const canContinue = (): boolean => {
|
|
4269
|
+
if (signal.aborted || this.#isDisposed) {
|
|
4270
|
+
skip("aborted_signal");
|
|
4271
|
+
return false;
|
|
4272
|
+
}
|
|
4273
|
+
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
4274
|
+
skip("generation_changed");
|
|
4275
|
+
return false;
|
|
4276
|
+
}
|
|
4277
|
+
if (this.#cancelAndSubmitInProgress && !options?.allowDuringCancelAndSubmit) {
|
|
4278
|
+
skip("queue_drained");
|
|
4279
|
+
return false;
|
|
4280
|
+
}
|
|
4281
|
+
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
4282
|
+
skip("queue_drained");
|
|
4283
|
+
return false;
|
|
4284
|
+
}
|
|
4285
|
+
return true;
|
|
4286
|
+
};
|
|
4287
|
+
if (!canContinue()) return;
|
|
4044
4288
|
try {
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
}
|
|
4289
|
+
if (!options?.skipCompactionCheck) {
|
|
4290
|
+
await this.#checkEstimatedContextBeforePrompt();
|
|
4291
|
+
if (!canContinue()) return;
|
|
4292
|
+
}
|
|
4293
|
+
if (signal.aborted) {
|
|
4294
|
+
skip("aborted_signal");
|
|
4295
|
+
return;
|
|
4296
|
+
}
|
|
4297
|
+
if (scheduledGeneration !== undefined && this.#promptGeneration !== scheduledGeneration) {
|
|
4298
|
+
skip("generation_changed");
|
|
4299
|
+
return;
|
|
4300
|
+
}
|
|
4301
|
+
if (options?.shouldContinue && !options.shouldContinue()) {
|
|
4302
|
+
skip("queue_drained");
|
|
4303
|
+
return;
|
|
4304
|
+
}
|
|
4305
|
+
// A continuation scheduled before a handoff engaged must not start a
|
|
4306
|
+
// turn against the session being handed off (or the restored
|
|
4307
|
+
// predecessor). rearmIdle / normal delivery resumes after the fence.
|
|
4308
|
+
if (this.#handoffTransitionActive) {
|
|
4309
|
+
skip("handoff_in_progress");
|
|
4310
|
+
return;
|
|
4311
|
+
}
|
|
4312
|
+
const predecessorAgentEnd = this.#claimDeferredAgentEndForContinuation(predecessorAgentEndHold);
|
|
4313
|
+
const startsQueuedSuccessor =
|
|
4314
|
+
this.agent.state.messages.at(-1)?.role === "assistant" && this.agent.hasQueuedMessages();
|
|
4315
|
+
try {
|
|
4316
|
+
await this.agent.continue({
|
|
4317
|
+
...this.#managedFallbackPromptOptions(),
|
|
4318
|
+
// Reset only after continue() has claimed the queued turn. Skipped or stale
|
|
4319
|
+
// continuations retain predecessor accounting, and resetAttemptBudget keeps
|
|
4320
|
+
// the sticky fallback cursor unchanged.
|
|
4321
|
+
onRunAccepted: startsQueuedSuccessor
|
|
4322
|
+
? () => {
|
|
4323
|
+
this.#defaultFallbackChain().resetAttemptBudget();
|
|
4324
|
+
this.#overflowMaintenanceAttempts = 0;
|
|
4325
|
+
}
|
|
4326
|
+
: undefined,
|
|
4327
|
+
});
|
|
4328
|
+
} catch (error) {
|
|
4329
|
+
if (options?.rescheduleOnBusy && this.#isAgentBusyError(error) && !terminalized && canContinue()) {
|
|
4330
|
+
this.#retainDeferredAgentEndAfterContinuationBusy(predecessorAgentEndHold, predecessorAgentEnd);
|
|
4331
|
+
logger.debug("agent.continue busy after scheduling; rescheduling", {
|
|
4332
|
+
error: error.message,
|
|
4333
|
+
});
|
|
4334
|
+
void scheduleAttempt(100);
|
|
4335
|
+
return;
|
|
4336
|
+
}
|
|
4337
|
+
this.#restoreDeferredAgentEndAfterContinuationFailure(predecessorAgentEnd);
|
|
4338
|
+
throw error;
|
|
4339
|
+
}
|
|
4057
4340
|
} catch (error) {
|
|
4058
|
-
|
|
4059
|
-
throw error;
|
|
4341
|
+
fail(error);
|
|
4060
4342
|
}
|
|
4061
|
-
}
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
4068
|
-
|
|
4069
|
-
);
|
|
4343
|
+
},
|
|
4344
|
+
{
|
|
4345
|
+
delayMs,
|
|
4346
|
+
onSkip: () => skip("aborted_signal"),
|
|
4347
|
+
resourceRunId: options?.resourceRunId,
|
|
4348
|
+
},
|
|
4349
|
+
);
|
|
4350
|
+
return scheduleAttempt();
|
|
4070
4351
|
}
|
|
4071
4352
|
|
|
4072
4353
|
#logCompactionContinuationSkipped(
|
|
@@ -4082,7 +4363,7 @@ export class AgentSession {
|
|
|
4082
4363
|
): void {
|
|
4083
4364
|
logger.warn("Auto-compaction continuation failed", {
|
|
4084
4365
|
source,
|
|
4085
|
-
reason:
|
|
4366
|
+
reason: "not_resumable_tail",
|
|
4086
4367
|
error: error instanceof Error ? error.message : String(error),
|
|
4087
4368
|
});
|
|
4088
4369
|
}
|
|
@@ -4110,14 +4391,24 @@ export class AgentSession {
|
|
|
4110
4391
|
return compactionSettings.autoContinue === false ? "auto_continue_disabled_non_resumable_tail" : undefined;
|
|
4111
4392
|
}
|
|
4112
4393
|
|
|
4113
|
-
#scheduleOverflowRetryContinuation(generation: number): boolean {
|
|
4394
|
+
async #scheduleOverflowRetryContinuation(generation: number, resourceRunId?: string): Promise<boolean> {
|
|
4114
4395
|
this.#stripOverflowFailedTurnForRetry();
|
|
4396
|
+
const snapshot = await this.#compactionStateSnapshot();
|
|
4397
|
+
if (
|
|
4398
|
+
snapshot.goal?.status === "paused" &&
|
|
4399
|
+
!snapshot.queuedMessages &&
|
|
4400
|
+
snapshot.lastAssistantStopReason !== "length"
|
|
4401
|
+
) {
|
|
4402
|
+
this.#logCompactionContinuationSkipped("overflow_retry", "paused_goal");
|
|
4403
|
+
return false;
|
|
4404
|
+
}
|
|
4115
4405
|
if (this.#isResumableAgentTail()) {
|
|
4116
4406
|
this.#scheduleAgentContinue({
|
|
4117
4407
|
delayMs: 100,
|
|
4118
4408
|
generation,
|
|
4119
4409
|
suppressPredecessorAgentEnd: true,
|
|
4120
|
-
|
|
4410
|
+
resourceRunId,
|
|
4411
|
+
rescheduleOnBusy: true,
|
|
4121
4412
|
onSkip: reason => this.#logCompactionContinuationSkipped("overflow_retry", reason),
|
|
4122
4413
|
onError: error => this.#logCompactionContinuationError("overflow_retry", error),
|
|
4123
4414
|
});
|
|
@@ -4126,7 +4417,7 @@ export class AgentSession {
|
|
|
4126
4417
|
|
|
4127
4418
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
4128
4419
|
if (compactionSettings.autoContinue !== false) {
|
|
4129
|
-
this.#scheduleAutoContinuePrompt(generation);
|
|
4420
|
+
this.#scheduleAutoContinuePrompt(generation, false);
|
|
4130
4421
|
return true;
|
|
4131
4422
|
}
|
|
4132
4423
|
|
|
@@ -4134,8 +4425,32 @@ export class AgentSession {
|
|
|
4134
4425
|
return false;
|
|
4135
4426
|
}
|
|
4136
4427
|
|
|
4137
|
-
#scheduleAutoContinuePrompt(generation: number): void {
|
|
4428
|
+
#scheduleAutoContinuePrompt(generation: number, requireUnfinishedWork = true): void {
|
|
4138
4429
|
const predecessorAgentEndHold = this.#reserveDeferredAgentEndForContinuation();
|
|
4430
|
+
const scheduledGeneration = generation;
|
|
4431
|
+
const signal = this.#postPromptTasksAbortController.signal;
|
|
4432
|
+
const continuationAuthorized = async (hasPendingNextTurnMessages = false): Promise<boolean> => {
|
|
4433
|
+
const snapshot = await this.#compactionStateSnapshot();
|
|
4434
|
+
if (signal.aborted) {
|
|
4435
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
4436
|
+
return false;
|
|
4437
|
+
}
|
|
4438
|
+
if (this.#isDisposed || this.#promptGeneration !== scheduledGeneration) {
|
|
4439
|
+
this.#logCompactionContinuationSkipped(
|
|
4440
|
+
"auto_continue_prompt",
|
|
4441
|
+
this.#isDisposed ? "session_disposed" : "generation_changed",
|
|
4442
|
+
);
|
|
4443
|
+
return false;
|
|
4444
|
+
}
|
|
4445
|
+
const authorized = requireUnfinishedWork
|
|
4446
|
+
? this.#hasUnfinishedWork(snapshot) || hasPendingNextTurnMessages
|
|
4447
|
+
: snapshot.queuedMessages ||
|
|
4448
|
+
hasPendingNextTurnMessages ||
|
|
4449
|
+
snapshot.lastAssistantStopReason === "length" ||
|
|
4450
|
+
snapshot.goal?.status !== "paused";
|
|
4451
|
+
if (!authorized) this.emitNotice("info", "Auto-continue skipped: no unfinished work detected");
|
|
4452
|
+
return authorized;
|
|
4453
|
+
};
|
|
4139
4454
|
const continuePrompt = async () => {
|
|
4140
4455
|
await this.#promptWithMessage(
|
|
4141
4456
|
{
|
|
@@ -4149,14 +4464,15 @@ export class AgentSession {
|
|
|
4149
4464
|
skipPostPromptRecoveryWait: true,
|
|
4150
4465
|
skipCompactionCheck: true,
|
|
4151
4466
|
predecessorAgentEndHold,
|
|
4467
|
+
onFinalPreflight: ({ hasPendingNextTurnMessages }) => continuationAuthorized(hasPendingNextTurnMessages),
|
|
4152
4468
|
},
|
|
4153
4469
|
);
|
|
4154
4470
|
};
|
|
4155
|
-
const
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
(async () => {
|
|
4471
|
+
const scheduleAttempt = (delayMs = 0) => {
|
|
4472
|
+
const attempt = (async () => {
|
|
4473
|
+
let rescheduled = false;
|
|
4159
4474
|
try {
|
|
4475
|
+
if (delayMs > 0) await scheduler.wait(delayMs, { signal });
|
|
4160
4476
|
await Promise.resolve();
|
|
4161
4477
|
if (signal.aborted) {
|
|
4162
4478
|
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
@@ -4166,14 +4482,31 @@ export class AgentSession {
|
|
|
4166
4482
|
this.#logCompactionContinuationSkipped("auto_continue_prompt", "generation_changed");
|
|
4167
4483
|
return;
|
|
4168
4484
|
}
|
|
4485
|
+
if (!(await continuationAuthorized())) return;
|
|
4169
4486
|
await continuePrompt();
|
|
4170
4487
|
} catch (error) {
|
|
4488
|
+
if (signal.aborted) {
|
|
4489
|
+
this.#logCompactionContinuationSkipped("auto_continue_prompt", "aborted_signal");
|
|
4490
|
+
return;
|
|
4491
|
+
}
|
|
4492
|
+
if (this.#isAgentBusyError(error) && this.#promptGeneration === scheduledGeneration) {
|
|
4493
|
+
logger.debug("Auto-compaction continuation busy; rescheduling", {
|
|
4494
|
+
source: "auto_continue_prompt",
|
|
4495
|
+
reason: "queue_drained",
|
|
4496
|
+
error: error.message,
|
|
4497
|
+
});
|
|
4498
|
+
rescheduled = true;
|
|
4499
|
+
scheduleAttempt(100);
|
|
4500
|
+
return;
|
|
4501
|
+
}
|
|
4171
4502
|
this.#logCompactionContinuationError("auto_continue_prompt", error);
|
|
4172
4503
|
} finally {
|
|
4173
|
-
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
4504
|
+
if (!rescheduled) this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
4174
4505
|
}
|
|
4175
|
-
})()
|
|
4176
|
-
|
|
4506
|
+
})();
|
|
4507
|
+
this.#trackPostPromptTask(attempt);
|
|
4508
|
+
};
|
|
4509
|
+
scheduleAttempt();
|
|
4177
4510
|
}
|
|
4178
4511
|
|
|
4179
4512
|
async #cancelPostPromptTasks(): Promise<void> {
|
|
@@ -4666,6 +4999,19 @@ export class AgentSession {
|
|
|
4666
4999
|
};
|
|
4667
5000
|
}
|
|
4668
5001
|
|
|
5002
|
+
/** Defer local:// provisioning when a read-only session load cannot create its artifact root. */
|
|
5003
|
+
async #initializeLocalRootForLoadedSession(): Promise<void> {
|
|
5004
|
+
try {
|
|
5005
|
+
await initializeLocalRoot(this.#localProtocolOptions());
|
|
5006
|
+
} catch (error) {
|
|
5007
|
+
if (!isEacces(error) && !hasFsCode(error, "EPERM") && !hasFsCode(error, "EROFS")) throw error;
|
|
5008
|
+
logger.debug("Deferred local root initialization for read-only session load", {
|
|
5009
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
5010
|
+
error: String(error),
|
|
5011
|
+
});
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
|
|
4669
5015
|
#maybeAbortStreamingEdit(event: AgentEvent, generation: number): void {
|
|
4670
5016
|
if (!this.settings.get("edit.streamingAbort")) return;
|
|
4671
5017
|
if (this.#streamingEditAbortTriggered) return;
|
|
@@ -5117,6 +5463,7 @@ export class AgentSession {
|
|
|
5117
5463
|
* such event.
|
|
5118
5464
|
*/
|
|
5119
5465
|
#syncAgentSessionId(sessionId?: string): void {
|
|
5466
|
+
this.#reasoningControlContextGeneration++;
|
|
5120
5467
|
const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
|
|
5121
5468
|
this.agent.sessionId = sid;
|
|
5122
5469
|
this.agent.providerSessionId = this.#providerCacheSessionId ?? sid;
|
|
@@ -5250,6 +5597,9 @@ export class AgentSession {
|
|
|
5250
5597
|
await disposeKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
5251
5598
|
await disposeVmContextsByOwner(this.#evalKernelOwnerId);
|
|
5252
5599
|
this.#releasePowerAssertion();
|
|
5600
|
+
this.#unregisterTeamWorkerAsyncJobChange?.();
|
|
5601
|
+
this.#unregisterTeamWorkerAsyncJobChange = undefined;
|
|
5602
|
+
this.#teamWorkerHeartbeat?.dispose();
|
|
5253
5603
|
// Disconnect the agent event listener BEFORE closing session resources so a late
|
|
5254
5604
|
// provider/tool message_end cannot append to the closing SessionManager.
|
|
5255
5605
|
this.#disconnectFromAgent();
|
|
@@ -5294,6 +5644,9 @@ export class AgentSession {
|
|
|
5294
5644
|
const kernelOwnerId = this.#evalKernelOwnerId;
|
|
5295
5645
|
this.#unregisterResourceGc?.();
|
|
5296
5646
|
this.#unregisterResourceGc = undefined;
|
|
5647
|
+
this.#unregisterTeamWorkerAsyncJobChange?.();
|
|
5648
|
+
this.#unregisterTeamWorkerAsyncJobChange = undefined;
|
|
5649
|
+
this.#teamWorkerHeartbeat?.dispose();
|
|
5297
5650
|
const work = Promise.allSettled([
|
|
5298
5651
|
// kill:true so a forced exit also reaps spawned-app Chrome we own (headless
|
|
5299
5652
|
// always closes; connected/attached browsers only disconnect — never killed).
|
|
@@ -6433,6 +6786,11 @@ export class AgentSession {
|
|
|
6433
6786
|
return this.#postPromptTasks.size > 0;
|
|
6434
6787
|
}
|
|
6435
6788
|
|
|
6789
|
+
/** Stable resource ownership identifier for the active prompt run. */
|
|
6790
|
+
get activePromptHandle(): string | undefined {
|
|
6791
|
+
return this.agent.activeResourceRunId;
|
|
6792
|
+
}
|
|
6793
|
+
|
|
6436
6794
|
/** All messages including custom types like BashExecutionMessage */
|
|
6437
6795
|
get messages(): AgentMessage[] {
|
|
6438
6796
|
return this.agent.state.messages;
|
|
@@ -6441,17 +6799,65 @@ export class AgentSession {
|
|
|
6441
6799
|
return this.#promptGeneration;
|
|
6442
6800
|
}
|
|
6443
6801
|
|
|
6802
|
+
static async restoreFromMemoryGuardCheckpoint(
|
|
6803
|
+
input: AgentSessionMemoryGuardRestoreInput,
|
|
6804
|
+
): Promise<AgentMemoryGuardRestoreResult> {
|
|
6805
|
+
const { staged, claimsLease, claimsStateDir, ...config } = input;
|
|
6806
|
+
if (
|
|
6807
|
+
staged.manager.getSessionId() !== staged.transcriptIdentity.sessionId ||
|
|
6808
|
+
staged.hydrationContext.identity.sessionId !== staged.transcriptIdentity.sessionId
|
|
6809
|
+
) {
|
|
6810
|
+
await staged.cleanup();
|
|
6811
|
+
return { kind: "blocked", reason: "transcript-mismatch" };
|
|
6812
|
+
}
|
|
6813
|
+
if (
|
|
6814
|
+
!isMemoryGuardClaimsLeaseForStateDir(claimsLease, claimsStateDir) ||
|
|
6815
|
+
claimsLease.owner.sessionId !== staged.transcriptIdentity.sessionId
|
|
6816
|
+
) {
|
|
6817
|
+
await staged.cleanup();
|
|
6818
|
+
return { kind: "blocked", reason: "claim-mismatch" };
|
|
6819
|
+
}
|
|
6820
|
+
const checkpointMessages = staged.manager.buildSessionContext().messages;
|
|
6821
|
+
if (!util.isDeepStrictEqual(config.agent.state.messages, checkpointMessages)) {
|
|
6822
|
+
await staged.cleanup();
|
|
6823
|
+
return { kind: "blocked", reason: "agent-messages-mismatch" };
|
|
6824
|
+
}
|
|
6825
|
+
const session = new AgentSession({
|
|
6826
|
+
...config,
|
|
6827
|
+
sessionManager: staged.manager,
|
|
6828
|
+
recoveryHydrationContext: staged.hydrationContext,
|
|
6829
|
+
});
|
|
6830
|
+
session.#memoryGuardClaimsLease = claimsLease;
|
|
6831
|
+
if (session.recoveryHydrationContext !== staged.hydrationContext) {
|
|
6832
|
+
await session.dispose();
|
|
6833
|
+
await staged.cleanup();
|
|
6834
|
+
return { kind: "blocked", reason: "hydration-context-mismatch" };
|
|
6835
|
+
}
|
|
6836
|
+
return {
|
|
6837
|
+
kind: "staged",
|
|
6838
|
+
session,
|
|
6839
|
+
promotionFence: Object.freeze({ ownershipReady: true, claimsLease }),
|
|
6840
|
+
};
|
|
6841
|
+
}
|
|
6842
|
+
|
|
6444
6843
|
/** The immutable recovery authority, present only before external ownership promotion. */
|
|
6445
6844
|
get recoveryHydrationContext(): RecoveryHydrationContext | undefined {
|
|
6446
6845
|
return this.#recoveryHydrationContext;
|
|
6447
6846
|
}
|
|
6448
6847
|
|
|
6449
6848
|
/** Enables normal session mutations after the owner has published its durable fence and writer lease. */
|
|
6450
|
-
async promoteRecoveryHydrationAfterOwnershipReadyFence(fence:
|
|
6849
|
+
async promoteRecoveryHydrationAfterOwnershipReadyFence(fence: AgentMemoryGuardPromotionFence): Promise<void> {
|
|
6451
6850
|
const context = this.#recoveryHydrationContext;
|
|
6452
6851
|
if (!context) throw new Error("Agent session is not awaiting recovery hydration promotion.");
|
|
6852
|
+
if (!isMemoryGuardClaimsLease(fence.claimsLease)) {
|
|
6853
|
+
throw new Error("Recovery hydration promotion requires a live memory-guard claims lease.");
|
|
6854
|
+
}
|
|
6855
|
+
if (this.#memoryGuardClaimsLease !== fence.claimsLease) {
|
|
6856
|
+
throw new Error("Recovery hydration promotion requires the acquired memory-guard claims lease.");
|
|
6857
|
+
}
|
|
6453
6858
|
await this.sessionManager.promoteRecoveryHydrationAfterOwnershipReadyFence(context, fence);
|
|
6454
6859
|
this.#recoveryHydrationContext = undefined;
|
|
6860
|
+
this.#memoryGuardClaimsLease = undefined;
|
|
6455
6861
|
}
|
|
6456
6862
|
|
|
6457
6863
|
/** Recovery hydration must not start a continuation before ownership promotion. */
|
|
@@ -6662,7 +7068,7 @@ export class AgentSession {
|
|
|
6662
7068
|
async invokeSkill(
|
|
6663
7069
|
name: string,
|
|
6664
7070
|
args = "",
|
|
6665
|
-
options?: Pick<PromptOptions, "onPreflightAccepted">,
|
|
7071
|
+
options?: Pick<PromptOptions, "onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared">,
|
|
6666
7072
|
): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
|
|
6667
7073
|
const skillName = name.trim();
|
|
6668
7074
|
if (!skillName) throw Object.assign(new Error("skill.invoke requires a skill name."), { code: "invalid_input" });
|
|
@@ -6695,6 +7101,12 @@ export class AgentSession {
|
|
|
6695
7101
|
attribution: "user" as const,
|
|
6696
7102
|
};
|
|
6697
7103
|
this.#deepInterviewPreclaimedCustomInputEpochs.set(skillPromptMessage, deepInterviewUserIntentEpoch);
|
|
7104
|
+
options?.onSkillPrepared?.({
|
|
7105
|
+
name: skill.name,
|
|
7106
|
+
path: skill.filePath,
|
|
7107
|
+
lineCount: built.details.lineCount,
|
|
7108
|
+
cleanedArgs: activation.cleanedArgs || undefined,
|
|
7109
|
+
});
|
|
6698
7110
|
await this.promptCustomMessage(skillPromptMessage, options);
|
|
6699
7111
|
return {
|
|
6700
7112
|
name: skill.name,
|
|
@@ -7350,6 +7762,7 @@ export class AgentSession {
|
|
|
7350
7762
|
* @throws Error if no model selected or no API key available (when not streaming)
|
|
7351
7763
|
*/
|
|
7352
7764
|
async prompt(text: string, options?: PromptOptions): Promise<void> {
|
|
7765
|
+
this.#assertRecoveryHydrationPromoted();
|
|
7353
7766
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
7354
7767
|
|
|
7355
7768
|
if (expandPromptTemplates && text.startsWith("/skill:") && !options?.images?.length) {
|
|
@@ -7361,7 +7774,14 @@ export class AgentSession {
|
|
|
7361
7774
|
await this.invokeSkill(
|
|
7362
7775
|
invocation.skill.name,
|
|
7363
7776
|
invocation.args,
|
|
7364
|
-
options?.onPreflightAccepted
|
|
7777
|
+
options?.onPreflightAccepted || options?.onPreflightAcceptCommit
|
|
7778
|
+
? {
|
|
7779
|
+
...(options.onPreflightAccepted ? { onPreflightAccepted: options.onPreflightAccepted } : {}),
|
|
7780
|
+
...(options.onPreflightAcceptCommit
|
|
7781
|
+
? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
|
|
7782
|
+
: {}),
|
|
7783
|
+
}
|
|
7784
|
+
: undefined,
|
|
7365
7785
|
);
|
|
7366
7786
|
return;
|
|
7367
7787
|
}
|
|
@@ -7415,7 +7835,8 @@ export class AgentSession {
|
|
|
7415
7835
|
if (workflowIntentDiff) {
|
|
7416
7836
|
this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
|
|
7417
7837
|
}
|
|
7418
|
-
options?.
|
|
7838
|
+
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
7839
|
+
else options?.onPreflightAccepted?.();
|
|
7419
7840
|
return;
|
|
7420
7841
|
}
|
|
7421
7842
|
|
|
@@ -7547,7 +7968,10 @@ export class AgentSession {
|
|
|
7547
7968
|
|
|
7548
7969
|
async promptCustomMessage<T = unknown>(
|
|
7549
7970
|
message: Pick<CustomMessage<T>, "customType" | "content" | "display" | "details" | "attribution">,
|
|
7550
|
-
options?: Pick<
|
|
7971
|
+
options?: Pick<
|
|
7972
|
+
PromptOptions,
|
|
7973
|
+
"streamingBehavior" | "toolChoice" | "followUpQueuePolicy" | "onPreflightAccepted" | "onPreflightAcceptCommit"
|
|
7974
|
+
>,
|
|
7551
7975
|
): Promise<void> {
|
|
7552
7976
|
const textContent =
|
|
7553
7977
|
typeof message.content === "string"
|
|
@@ -7609,12 +8033,16 @@ export class AgentSession {
|
|
|
7609
8033
|
async #promptWithMessage(
|
|
7610
8034
|
message: AgentMessage,
|
|
7611
8035
|
expandedText: string,
|
|
7612
|
-
options?: Pick<
|
|
8036
|
+
options?: Pick<
|
|
8037
|
+
PromptOptions,
|
|
8038
|
+
"toolChoice" | "images" | "skipCompactionCheck" | "onPreflightAccepted" | "onPreflightAcceptCommit"
|
|
8039
|
+
> & {
|
|
7613
8040
|
prependMessages?: AgentMessage[];
|
|
7614
8041
|
skipPostPromptRecoveryWait?: boolean;
|
|
7615
8042
|
predecessorAgentEndHold?: symbol;
|
|
7616
8043
|
admissionLease?: SessionAdmissionLease;
|
|
7617
8044
|
onRunAccepted?: () => void;
|
|
8045
|
+
onFinalPreflight?: (context: { hasPendingNextTurnMessages: boolean }) => Promise<boolean>;
|
|
7618
8046
|
resetRetryReplaySafety?: boolean;
|
|
7619
8047
|
},
|
|
7620
8048
|
): Promise<void> {
|
|
@@ -7630,6 +8058,8 @@ export class AgentSession {
|
|
|
7630
8058
|
const generation = this.#promptGeneration;
|
|
7631
8059
|
const preflightSignal = this.#promptPreflightAbortController.signal;
|
|
7632
8060
|
const rosterClaim = this.#claimIrcRosterCandidate();
|
|
8061
|
+
let hasPendingNextTurnMessages = false;
|
|
8062
|
+
let pendingNextTurnMessageCount = 0;
|
|
7633
8063
|
let hindsightRecall: string | undefined;
|
|
7634
8064
|
try {
|
|
7635
8065
|
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
@@ -7716,16 +8146,16 @@ export class AgentSession {
|
|
|
7716
8146
|
// A newer abort/prompt cycle superseded this preflight. Callers awaiting
|
|
7717
8147
|
// acceptance (onPreflightAccepted) must be told it never ran; direct
|
|
7718
8148
|
// callers (e.g. prompt() aborted during a TTSR wait) resolve gracefully.
|
|
7719
|
-
if (options?.onPreflightAccepted) throw promptPreflightCancelledError();
|
|
8149
|
+
if (options?.onPreflightAccepted || options?.onPreflightAcceptCommit) throw promptPreflightCancelledError();
|
|
7720
8150
|
return;
|
|
7721
8151
|
}
|
|
7722
8152
|
|
|
7723
8153
|
// Inject any pending "nextTurn" messages as context alongside the user message
|
|
7724
|
-
|
|
8154
|
+
pendingNextTurnMessageCount = this.#pendingNextTurnMessages.length;
|
|
8155
|
+
hasPendingNextTurnMessages = pendingNextTurnMessageCount > 0;
|
|
8156
|
+
for (const msg of this.#pendingNextTurnMessages.slice(0, pendingNextTurnMessageCount)) {
|
|
7725
8157
|
messages.push(msg);
|
|
7726
8158
|
}
|
|
7727
|
-
this.#pendingNextTurnMessages = [];
|
|
7728
|
-
if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
|
|
7729
8159
|
|
|
7730
8160
|
// Auto-read @filepath mentions
|
|
7731
8161
|
const fileMentions = extractFileMentions(expandedText);
|
|
@@ -7830,7 +8260,7 @@ export class AgentSession {
|
|
|
7830
8260
|
this.#resetInjectedContextSignatures();
|
|
7831
8261
|
// Ack-waiting callers are told the preflight never ran; direct callers
|
|
7832
8262
|
// (aborted after setup) resolve gracefully as before f24f46ff5.
|
|
7833
|
-
if (options?.onPreflightAccepted) throw promptPreflightCancelledError();
|
|
8263
|
+
if (options?.onPreflightAccepted || options?.onPreflightAcceptCommit) throw promptPreflightCancelledError();
|
|
7834
8264
|
return;
|
|
7835
8265
|
}
|
|
7836
8266
|
|
|
@@ -7843,8 +8273,17 @@ export class AgentSession {
|
|
|
7843
8273
|
if (hindsightRecall) this.getHindsightSessionState()?.markRecallSnippetInjected(hindsightRecall);
|
|
7844
8274
|
},
|
|
7845
8275
|
};
|
|
7846
|
-
options?.
|
|
8276
|
+
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
8277
|
+
else options?.onPreflightAccepted?.();
|
|
8278
|
+
if (options?.onFinalPreflight && !(await options.onFinalPreflight({ hasPendingNextTurnMessages }))) {
|
|
8279
|
+
this.#resetInjectedContextSignatures();
|
|
8280
|
+
return;
|
|
8281
|
+
}
|
|
7847
8282
|
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
8283
|
+
if (pendingNextTurnMessageCount > 0) {
|
|
8284
|
+
this.#pendingNextTurnMessages.splice(0, pendingNextTurnMessageCount);
|
|
8285
|
+
}
|
|
8286
|
+
if (this.#cancelAndSubmitInProgress) this.#cancelAndSubmitPendingNextTurnDrained = true;
|
|
7848
8287
|
await this.#promptAgentWithIdleRetry(messages, agentPromptOptions, predecessorAgentEndHold);
|
|
7849
8288
|
const terminalAssistant = this.#findLastAssistantMessage();
|
|
7850
8289
|
if (
|
|
@@ -7862,7 +8301,12 @@ export class AgentSession {
|
|
|
7862
8301
|
// Session identity changes historically cancel local setup silently. Only SDK
|
|
7863
8302
|
// submissions provide an acceptance callback and require an explicit terminal
|
|
7864
8303
|
// preflight failure for their remote request authority.
|
|
7865
|
-
if (
|
|
8304
|
+
if (
|
|
8305
|
+
isPromptPreflightCancelledError(error) &&
|
|
8306
|
+
!options?.onPreflightAccepted &&
|
|
8307
|
+
!options?.onPreflightAcceptCommit
|
|
8308
|
+
)
|
|
8309
|
+
return;
|
|
7866
8310
|
throw error;
|
|
7867
8311
|
} finally {
|
|
7868
8312
|
this.#removeEphemeralCustomMessages();
|
|
@@ -7880,6 +8324,7 @@ export class AgentSession {
|
|
|
7880
8324
|
await this.#agentEndPublicationPromise;
|
|
7881
8325
|
} else {
|
|
7882
8326
|
await this.#agentEndHandlingPromise;
|
|
8327
|
+
await this.#waitForPostPromptRecovery();
|
|
7883
8328
|
await this.#agentEndPublicationPromise;
|
|
7884
8329
|
}
|
|
7885
8330
|
}
|
|
@@ -7928,6 +8373,7 @@ export class AgentSession {
|
|
|
7928
8373
|
sessionManager: createReadonlySessionManager(this.sessionManager),
|
|
7929
8374
|
modelRegistry: this.#modelRegistry,
|
|
7930
8375
|
model: this.model ?? undefined,
|
|
8376
|
+
getActivePromptHandle: () => this.activePromptHandle,
|
|
7931
8377
|
isIdle: () => !this.isStreaming,
|
|
7932
8378
|
abort: () => {
|
|
7933
8379
|
void this.abort();
|
|
@@ -7963,7 +8409,7 @@ export class AgentSession {
|
|
|
7963
8409
|
}
|
|
7964
8410
|
return false;
|
|
7965
8411
|
},
|
|
7966
|
-
invokeSkill: (name, args) => this.invokeSkill(name, args),
|
|
8412
|
+
invokeSkill: (name, args, options) => this.invokeSkill(name, args, options),
|
|
7967
8413
|
setPlanMode: on => this.setSdkPlanMode(on),
|
|
7968
8414
|
operateGoal: (op, objective) => this.operateGoal(op, objective),
|
|
7969
8415
|
getSkillState: () => this.skills.map(skill => ({ name: skill.name, description: skill.description })),
|
|
@@ -8078,6 +8524,7 @@ export class AgentSession {
|
|
|
8078
8524
|
* Queue a steering message to interrupt the agent mid-run.
|
|
8079
8525
|
*/
|
|
8080
8526
|
async steer(text: string, images?: ImageContent[]): Promise<void> {
|
|
8527
|
+
this.#assertRecoveryHydrationPromoted();
|
|
8081
8528
|
if (text.startsWith("/")) {
|
|
8082
8529
|
this.#throwIfExtensionCommand(text);
|
|
8083
8530
|
}
|
|
@@ -8095,6 +8542,7 @@ export class AgentSession {
|
|
|
8095
8542
|
images?: ImageContent[],
|
|
8096
8543
|
options?: Pick<PromptOptions, "followUpQueuePolicy">,
|
|
8097
8544
|
): Promise<void> {
|
|
8545
|
+
this.#assertRecoveryHydrationPromoted();
|
|
8098
8546
|
if (text.startsWith("/")) {
|
|
8099
8547
|
this.#throwIfExtensionCommand(text);
|
|
8100
8548
|
}
|
|
@@ -8329,6 +8777,7 @@ export class AgentSession {
|
|
|
8329
8777
|
followUpQueuePolicy?: "respect-mode" | "sequential";
|
|
8330
8778
|
},
|
|
8331
8779
|
): Promise<void> {
|
|
8780
|
+
this.#assertRecoveryHydrationPromoted();
|
|
8332
8781
|
const appMessage: CustomMessage<T> = {
|
|
8333
8782
|
role: "custom",
|
|
8334
8783
|
customType: message.customType,
|
|
@@ -8470,8 +8919,13 @@ export class AgentSession {
|
|
|
8470
8919
|
*/
|
|
8471
8920
|
async sendUserMessage(
|
|
8472
8921
|
content: string | (TextContent | ImageContent)[],
|
|
8473
|
-
options?: {
|
|
8922
|
+
options?: {
|
|
8923
|
+
deliverAs?: "steer" | "followUp";
|
|
8924
|
+
onPreflightAccepted?: () => void;
|
|
8925
|
+
onPreflightAcceptCommit?: () => void | Promise<void>;
|
|
8926
|
+
},
|
|
8474
8927
|
): Promise<void> {
|
|
8928
|
+
this.#assertRecoveryHydrationPromoted();
|
|
8475
8929
|
// Normalize content to text string + optional images
|
|
8476
8930
|
let text: string;
|
|
8477
8931
|
let images: ImageContent[] | undefined;
|
|
@@ -8493,11 +8947,13 @@ export class AgentSession {
|
|
|
8493
8947
|
}
|
|
8494
8948
|
|
|
8495
8949
|
if (options?.deliverAs === "followUp") {
|
|
8950
|
+
if (options.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
8496
8951
|
await this.#queueFollowUp(text, images, { claimsGenuineUserIntent: true });
|
|
8497
8952
|
options.onPreflightAccepted?.();
|
|
8498
8953
|
return;
|
|
8499
8954
|
}
|
|
8500
8955
|
if (options?.deliverAs === "steer") {
|
|
8956
|
+
if (options.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
8501
8957
|
await this.#queueSteer(text, images, { claimsGenuineUserIntent: true });
|
|
8502
8958
|
options.onPreflightAccepted?.();
|
|
8503
8959
|
return;
|
|
@@ -8509,6 +8965,7 @@ export class AgentSession {
|
|
|
8509
8965
|
// in-flight compaction internally, and #queueSteer would otherwise park
|
|
8510
8966
|
// the message in the steering queue with no turn to consume it.
|
|
8511
8967
|
if (this.isStreaming) {
|
|
8968
|
+
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
8512
8969
|
await this.#queueSteer(text, images, { claimsGenuineUserIntent: true });
|
|
8513
8970
|
options?.onPreflightAccepted?.();
|
|
8514
8971
|
return;
|
|
@@ -8519,6 +8976,7 @@ export class AgentSession {
|
|
|
8519
8976
|
expandPromptTemplates: false,
|
|
8520
8977
|
images,
|
|
8521
8978
|
onPreflightAccepted: options?.onPreflightAccepted,
|
|
8979
|
+
onPreflightAcceptCommit: options?.onPreflightAcceptCommit,
|
|
8522
8980
|
});
|
|
8523
8981
|
}
|
|
8524
8982
|
|
|
@@ -8691,6 +9149,103 @@ export class AgentSession {
|
|
|
8691
9149
|
this.setTodoPhases(phases.filter(p => p.tasks.length > 0));
|
|
8692
9150
|
}
|
|
8693
9151
|
|
|
9152
|
+
async #compactionStateSnapshot(): Promise<CompactionStateSnapshot> {
|
|
9153
|
+
const snapshot: CompactionStateSnapshot = {
|
|
9154
|
+
goal: undefined,
|
|
9155
|
+
openTodos: [],
|
|
9156
|
+
activeSkills: [],
|
|
9157
|
+
queuedMessages: false,
|
|
9158
|
+
lastAssistantStopReason: undefined,
|
|
9159
|
+
};
|
|
9160
|
+
try {
|
|
9161
|
+
const goalState = this.getGoalModeState();
|
|
9162
|
+
const goal = goalState?.goal;
|
|
9163
|
+
if (goal) snapshot.goal = { objective: goal.objective, status: goal.status, enabled: goalState.enabled };
|
|
9164
|
+
} catch (error) {
|
|
9165
|
+
logger.warn("Failed to read goal state for compaction snapshot", {
|
|
9166
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9167
|
+
});
|
|
9168
|
+
}
|
|
9169
|
+
try {
|
|
9170
|
+
const todos = this.getTodoPhases()
|
|
9171
|
+
.flatMap(phase => phase.tasks)
|
|
9172
|
+
.filter(task => task.status === "pending" || task.status === "in_progress")
|
|
9173
|
+
.slice(0, 10)
|
|
9174
|
+
.map(task => task.content);
|
|
9175
|
+
snapshot.openTodos = todos;
|
|
9176
|
+
} catch (error) {
|
|
9177
|
+
logger.warn("Failed to read todo state for compaction snapshot", {
|
|
9178
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9179
|
+
});
|
|
9180
|
+
}
|
|
9181
|
+
try {
|
|
9182
|
+
const state = await readVisibleSkillActiveState(this.sessionManager.getCwd(), this.sessionId, {
|
|
9183
|
+
bypassCache: true,
|
|
9184
|
+
});
|
|
9185
|
+
snapshot.activeSkills = (state?.active_skills ?? [])
|
|
9186
|
+
.filter(entry => entry.active !== false)
|
|
9187
|
+
.slice(0, 5)
|
|
9188
|
+
.map(entry => ({ skill: entry.skill, phase: entry.phase ?? "unknown" }));
|
|
9189
|
+
} catch (error) {
|
|
9190
|
+
logger.warn("Failed to read workflow state for compaction snapshot", {
|
|
9191
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9192
|
+
});
|
|
9193
|
+
}
|
|
9194
|
+
try {
|
|
9195
|
+
snapshot.queuedMessages = this.agent.hasQueuedMessages() || this.#pendingNextTurnMessages.length > 0;
|
|
9196
|
+
} catch (error) {
|
|
9197
|
+
logger.warn("Failed to read queued-message state for compaction snapshot", {
|
|
9198
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9199
|
+
});
|
|
9200
|
+
}
|
|
9201
|
+
try {
|
|
9202
|
+
for (let index = this.messages.length - 1; index >= 0; index--) {
|
|
9203
|
+
const message = this.messages[index];
|
|
9204
|
+
if (message.role !== "assistant") continue;
|
|
9205
|
+
snapshot.lastAssistantStopReason = (message as AssistantMessage).stopReason;
|
|
9206
|
+
break;
|
|
9207
|
+
}
|
|
9208
|
+
} catch (error) {
|
|
9209
|
+
logger.warn("Failed to read assistant stop state for compaction snapshot", {
|
|
9210
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9211
|
+
});
|
|
9212
|
+
}
|
|
9213
|
+
return snapshot;
|
|
9214
|
+
}
|
|
9215
|
+
|
|
9216
|
+
#compactionStateContext(snapshot: CompactionStateSnapshot): string[] {
|
|
9217
|
+
const context: string[] = [];
|
|
9218
|
+
const goal = snapshot.goal;
|
|
9219
|
+
if (goal?.enabled) {
|
|
9220
|
+
context.push(
|
|
9221
|
+
`Active goal: ${sanitizeCompactionStateText(goal.objective, 160)} (status: ${sanitizeCompactionStateText(goal.status, 40)})`,
|
|
9222
|
+
);
|
|
9223
|
+
} else if (goal?.status === "paused") {
|
|
9224
|
+
context.push(`Paused goal: ${sanitizeCompactionStateText(goal.objective, 160)} (status: paused)`);
|
|
9225
|
+
}
|
|
9226
|
+
for (const entry of snapshot.activeSkills) {
|
|
9227
|
+
const skill = sanitizeCompactionStateText(entry.skill, 100);
|
|
9228
|
+
const phase = sanitizeCompactionStateText(entry.phase, 80);
|
|
9229
|
+
if (skill) context.push(`Active skill: ${skill} phase=${phase}`);
|
|
9230
|
+
}
|
|
9231
|
+
if (snapshot.openTodos.length > 0) {
|
|
9232
|
+
const todos = snapshot.openTodos.map(todo => sanitizeCompactionStateText(todo, 120));
|
|
9233
|
+
context.push(`Open todos: ${todos.join("; ")}`);
|
|
9234
|
+
}
|
|
9235
|
+
return context;
|
|
9236
|
+
}
|
|
9237
|
+
|
|
9238
|
+
#hasUnfinishedWork(snapshot: CompactionStateSnapshot): boolean {
|
|
9239
|
+
if (snapshot.queuedMessages) return true;
|
|
9240
|
+
if (snapshot.lastAssistantStopReason === "length") return true;
|
|
9241
|
+
// A verified paused goal is a human-wait terminus. It suppresses stale
|
|
9242
|
+
// todo/workflow activity until a queued user message resumes the run.
|
|
9243
|
+
if (snapshot.goal?.status === "paused") return false;
|
|
9244
|
+
if (snapshot.goal?.enabled && snapshot.goal.status === "active") return true;
|
|
9245
|
+
if (snapshot.openTodos.length > 0) return true;
|
|
9246
|
+
return snapshot.activeSkills.some(entry => !isWorkflowContinuationInert(entry.skill, entry.phase));
|
|
9247
|
+
}
|
|
9248
|
+
|
|
8694
9249
|
async #applyCompactionPostAppend(
|
|
8695
9250
|
compactionEntryId: string,
|
|
8696
9251
|
firstKeptEntryId: string,
|
|
@@ -8889,6 +9444,26 @@ export class AgentSession {
|
|
|
8889
9444
|
const outcome = await this.#abortWithOutcome(options);
|
|
8890
9445
|
if (outcome.kind === "error") throw outcome.cause;
|
|
8891
9446
|
}
|
|
9447
|
+
/**
|
|
9448
|
+
* Abort a specific active prompt and prove whether its tracked resources settled.
|
|
9449
|
+
*/
|
|
9450
|
+
async abortPromptAndWait(handle: string, options: { graceMs: number }): Promise<RunSettlementProof> {
|
|
9451
|
+
// Only the abort trigger is gated on active ownership; settlement is always proven
|
|
9452
|
+
// from the ledger, because a run can go inactive while its resources still run.
|
|
9453
|
+
if (handle === this.agent.activeResourceRunId)
|
|
9454
|
+
// Awaiting the abort unboundedly would let a provider or tool that ignores
|
|
9455
|
+
// cancellation defer the grace forever, so the proof races the fixed grace
|
|
9456
|
+
// independently of abort completion.
|
|
9457
|
+
void this.abort({ timeoutMs: options.graceMs }).catch(() => {});
|
|
9458
|
+
|
|
9459
|
+
const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
|
|
9460
|
+
if (proof.status === "unfenced") {
|
|
9461
|
+
// Detach the unsettled resources so they can never re-enter this run; their real
|
|
9462
|
+
// promises still release them inside the ledger.
|
|
9463
|
+
this.agent.resourceLedger.quarantine(handle);
|
|
9464
|
+
}
|
|
9465
|
+
return proof;
|
|
9466
|
+
}
|
|
8892
9467
|
|
|
8893
9468
|
/** Atomically interrupt the active run and make text the next prompt. */
|
|
8894
9469
|
async cancelAndSubmit(text: string, options?: { queuedEntryId?: string }): Promise<CancelAndSubmitOutcome> {
|
|
@@ -9258,6 +9833,12 @@ export class AgentSession {
|
|
|
9258
9833
|
): Promise<void> {
|
|
9259
9834
|
this.#clearConstructorToolSelectionAuthority();
|
|
9260
9835
|
const inheritedThinkingLevel = resolveThinkingLevelForModel(this.model, this.#getInheritedThinkingLevel());
|
|
9836
|
+
this.#thinkingLevelMutationRevision++;
|
|
9837
|
+
this.#thinkingLevelLiveMutationRevision++;
|
|
9838
|
+
this.#pendingThinkingLevelControlSuccess = undefined;
|
|
9839
|
+
this.#pendingThinkingLevelControlFailure = undefined;
|
|
9840
|
+
this.#pendingThinkingVisibilityControlSuccess = undefined;
|
|
9841
|
+
this.#pendingThinkingVisibilityControlFailure = undefined;
|
|
9261
9842
|
this.#thinkingLevel = inheritedThinkingLevel;
|
|
9262
9843
|
this.agent.setThinkingLevel(toReasoningEffort(inheritedThinkingLevel));
|
|
9263
9844
|
this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
|
|
@@ -9536,6 +10117,16 @@ export class AgentSession {
|
|
|
9536
10117
|
getSessionDefaultModelSelector(): string | undefined {
|
|
9537
10118
|
return getSessionContextForInternalRead(this.sessionManager).models.default;
|
|
9538
10119
|
}
|
|
10120
|
+
/**
|
|
10121
|
+
* Resolve the model that `modelRoles.default` currently points to, independent
|
|
10122
|
+
* of whatever model this session's log last recorded. Used by the TUI resume
|
|
10123
|
+
* flow's `session.resumeModelBehavior: "ask"` prompt to offer the currently
|
|
10124
|
+
* configured default as an alternative to the session's saved model.
|
|
10125
|
+
*/
|
|
10126
|
+
resolveConfiguredDefaultModel(): Model | undefined {
|
|
10127
|
+
const availableModels = this.#modelRegistry.getAvailable();
|
|
10128
|
+
return this.#resolveRoleModelFull("default", availableModels, undefined).model;
|
|
10129
|
+
}
|
|
9539
10130
|
|
|
9540
10131
|
/**
|
|
9541
10132
|
* Re-assert the session resume default ("provider/id") in the session log
|
|
@@ -9607,7 +10198,7 @@ export class AgentSession {
|
|
|
9607
10198
|
const ownsScope = scope !== undefined && !suppliedScope;
|
|
9608
10199
|
try {
|
|
9609
10200
|
if (isTemporaryOperation) {
|
|
9610
|
-
this
|
|
10201
|
+
this.#setAgentModelWithReasoningContext(model);
|
|
9611
10202
|
this.#syncAppendOnlyContext(model);
|
|
9612
10203
|
} else {
|
|
9613
10204
|
this.#setModelAuthoritatively(model, options?.cause ?? "temporary-operation");
|
|
@@ -9692,6 +10283,12 @@ export class AgentSession {
|
|
|
9692
10283
|
this.#clearActiveRetryFallback();
|
|
9693
10284
|
this.#setModelWithProviderSessionReset(model);
|
|
9694
10285
|
const thinkingLevelChanged = this.#thinkingLevel !== thinkingLevel;
|
|
10286
|
+
this.#thinkingLevelMutationRevision++;
|
|
10287
|
+
this.#thinkingLevelLiveMutationRevision++;
|
|
10288
|
+
this.#pendingThinkingLevelControlSuccess = undefined;
|
|
10289
|
+
this.#pendingThinkingLevelControlFailure = undefined;
|
|
10290
|
+
this.#pendingThinkingVisibilityControlSuccess = undefined;
|
|
10291
|
+
this.#pendingThinkingVisibilityControlFailure = undefined;
|
|
9695
10292
|
this.#thinkingLevel = thinkingLevel;
|
|
9696
10293
|
this.agent.setThinkingLevel(toReasoningEffort(thinkingLevel));
|
|
9697
10294
|
if (thinkingLevelChanged) {
|
|
@@ -10000,7 +10597,19 @@ export class AgentSession {
|
|
|
10000
10597
|
* Set thinking level.
|
|
10001
10598
|
* Saves the effective metadata-clamped level to the session, and to settings when requested.
|
|
10002
10599
|
*/
|
|
10600
|
+
#assertDurableSettingsWritable(): void {
|
|
10601
|
+
if (this.settings.canWriteDurableConfig()) return;
|
|
10602
|
+
throw new Error(
|
|
10603
|
+
"Cannot change settings while config.yml has invalid YAML syntax. Repair config.yml and reload settings.",
|
|
10604
|
+
);
|
|
10605
|
+
}
|
|
10606
|
+
|
|
10003
10607
|
setThinkingLevel(level: ThinkingLevel | undefined, persist: boolean = false): void {
|
|
10608
|
+
if (persist) this.#assertDurableSettingsWritable();
|
|
10609
|
+
this.#thinkingLevelMutationRevision++;
|
|
10610
|
+
this.#thinkingLevelLiveMutationRevision++;
|
|
10611
|
+
this.#pendingThinkingLevelControlSuccess = undefined;
|
|
10612
|
+
this.#pendingThinkingLevelControlFailure = undefined;
|
|
10004
10613
|
const effectiveLevel = resolveThinkingLevelForModel(this.model, level);
|
|
10005
10614
|
const isChanging = effectiveLevel !== this.#thinkingLevel;
|
|
10006
10615
|
|
|
@@ -10020,11 +10629,10 @@ export class AgentSession {
|
|
|
10020
10629
|
}
|
|
10021
10630
|
|
|
10022
10631
|
/**
|
|
10023
|
-
* Set thinking level from a control surface. Global changes
|
|
10632
|
+
* Set thinking level from a control surface. Global changes commit before affecting live state.
|
|
10024
10633
|
*/
|
|
10025
10634
|
async setThinkingLevelForControl(level: ThinkingLevel, persist: boolean): Promise<void> {
|
|
10026
10635
|
const previousThinkingLevel = this.thinkingLevel;
|
|
10027
|
-
const previousScope = this.getThinkingScopeForControl();
|
|
10028
10636
|
if (!persist) {
|
|
10029
10637
|
this.setThinkingLevel(level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : level);
|
|
10030
10638
|
if (level === ThinkingLevel.Inherit || this.thinkingLevel === previousThinkingLevel) {
|
|
@@ -10033,26 +10641,99 @@ export class AgentSession {
|
|
|
10033
10641
|
return;
|
|
10034
10642
|
}
|
|
10035
10643
|
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10039
|
-
this
|
|
10040
|
-
|
|
10041
|
-
|
|
10042
|
-
|
|
10043
|
-
|
|
10644
|
+
this.#assertDurableSettingsWritable();
|
|
10645
|
+
const effectiveLevel = resolveThinkingLevelForModel(
|
|
10646
|
+
this.model,
|
|
10647
|
+
level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : level,
|
|
10648
|
+
);
|
|
10649
|
+
const persistedLevel = level === ThinkingLevel.Inherit ? getDefault("defaultThinkingLevel") : effectiveLevel;
|
|
10650
|
+
const mutationRevision = ++this.#thinkingLevelMutationRevision;
|
|
10651
|
+
const expectedLiveMutationRevision = this.#thinkingLevelLiveMutationRevision;
|
|
10652
|
+
const expectedSessionId = this.sessionManager.getSessionId();
|
|
10653
|
+
const expectedModel = this.model;
|
|
10654
|
+
const expectedContextGeneration = this.#reasoningControlContextGeneration;
|
|
10044
10655
|
try {
|
|
10045
|
-
await this.settings.
|
|
10046
|
-
this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
|
|
10656
|
+
await this.settings.commitAtomicBatch([{ path: "defaultThinkingLevel", op: "set", value: persistedLevel }]);
|
|
10047
10657
|
} catch {
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10658
|
+
if (
|
|
10659
|
+
mutationRevision === this.#thinkingLevelMutationRevision &&
|
|
10660
|
+
this.#reasoningControlContextGeneration === expectedContextGeneration &&
|
|
10661
|
+
this.sessionManager.getSessionId() === expectedSessionId &&
|
|
10662
|
+
this.model === expectedModel
|
|
10663
|
+
) {
|
|
10664
|
+
const pending = this.#pendingThinkingLevelControlSuccess;
|
|
10665
|
+
this.#pendingThinkingLevelControlSuccess = undefined;
|
|
10666
|
+
if (
|
|
10667
|
+
pending &&
|
|
10668
|
+
pending.contextGeneration === expectedContextGeneration &&
|
|
10669
|
+
this.sessionManager.getSessionId() === pending.sessionId &&
|
|
10670
|
+
this.model === pending.model
|
|
10671
|
+
) {
|
|
10672
|
+
this.setThinkingLevel(
|
|
10673
|
+
pending.level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : pending.level,
|
|
10674
|
+
);
|
|
10675
|
+
this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
|
|
10676
|
+
} else {
|
|
10677
|
+
this.#pendingThinkingLevelControlFailure = {
|
|
10678
|
+
mutationRevision,
|
|
10679
|
+
liveMutationRevision: expectedLiveMutationRevision,
|
|
10680
|
+
sessionId: expectedSessionId,
|
|
10681
|
+
model: expectedModel,
|
|
10682
|
+
contextGeneration: expectedContextGeneration,
|
|
10683
|
+
};
|
|
10684
|
+
}
|
|
10685
|
+
}
|
|
10054
10686
|
throw new Error("Unable to persist reasoning settings.");
|
|
10055
10687
|
}
|
|
10688
|
+
|
|
10689
|
+
if (
|
|
10690
|
+
mutationRevision === this.#thinkingLevelMutationRevision &&
|
|
10691
|
+
this.#thinkingLevelLiveMutationRevision === expectedLiveMutationRevision
|
|
10692
|
+
) {
|
|
10693
|
+
this.setThinkingLevel(level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : level);
|
|
10694
|
+
this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
|
|
10695
|
+
return;
|
|
10696
|
+
}
|
|
10697
|
+
if (
|
|
10698
|
+
mutationRevision !== this.#thinkingLevelMutationRevision ||
|
|
10699
|
+
this.#reasoningControlContextGeneration !== expectedContextGeneration ||
|
|
10700
|
+
this.sessionManager.getSessionId() !== expectedSessionId ||
|
|
10701
|
+
this.model !== expectedModel
|
|
10702
|
+
) {
|
|
10703
|
+
if (
|
|
10704
|
+
this.#thinkingLevelLiveMutationRevision === expectedLiveMutationRevision &&
|
|
10705
|
+
this.#reasoningControlContextGeneration === expectedContextGeneration &&
|
|
10706
|
+
this.sessionManager.getSessionId() === expectedSessionId &&
|
|
10707
|
+
this.model === expectedModel &&
|
|
10708
|
+
(this.#pendingThinkingLevelControlSuccess?.mutationRevision ?? -1) < mutationRevision
|
|
10709
|
+
) {
|
|
10710
|
+
this.#pendingThinkingLevelControlSuccess = {
|
|
10711
|
+
level,
|
|
10712
|
+
mutationRevision,
|
|
10713
|
+
sessionId: expectedSessionId,
|
|
10714
|
+
model: expectedModel,
|
|
10715
|
+
contextGeneration: expectedContextGeneration,
|
|
10716
|
+
};
|
|
10717
|
+
const failure = this.#pendingThinkingLevelControlFailure;
|
|
10718
|
+
if (
|
|
10719
|
+
failure &&
|
|
10720
|
+
failure.mutationRevision === this.#thinkingLevelMutationRevision &&
|
|
10721
|
+
failure.liveMutationRevision === expectedLiveMutationRevision &&
|
|
10722
|
+
failure.sessionId === expectedSessionId &&
|
|
10723
|
+
failure.model === expectedModel &&
|
|
10724
|
+
failure.contextGeneration === expectedContextGeneration
|
|
10725
|
+
) {
|
|
10726
|
+
this.#pendingThinkingLevelControlFailure = undefined;
|
|
10727
|
+
this.setThinkingLevel(
|
|
10728
|
+
level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : effectiveLevel,
|
|
10729
|
+
);
|
|
10730
|
+
this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
|
|
10731
|
+
}
|
|
10732
|
+
}
|
|
10733
|
+
return;
|
|
10734
|
+
}
|
|
10735
|
+
this.setThinkingLevel(level === ThinkingLevel.Inherit ? this.#getInheritedThinkingLevel() : effectiveLevel);
|
|
10736
|
+
this.sessionManager.appendThinkingLevelChange(ThinkingLevel.Inherit);
|
|
10056
10737
|
}
|
|
10057
10738
|
|
|
10058
10739
|
getThinkingScopeForControl(): "session" | "global config" {
|
|
@@ -10068,6 +10749,11 @@ export class AgentSession {
|
|
|
10068
10749
|
}
|
|
10069
10750
|
|
|
10070
10751
|
setThinkingVisibility(visibility: "visible" | "hidden", persist: boolean = false): void {
|
|
10752
|
+
if (persist) this.#assertDurableSettingsWritable();
|
|
10753
|
+
this.#thinkingVisibilityMutationRevision++;
|
|
10754
|
+
this.#thinkingVisibilityLiveMutationRevision++;
|
|
10755
|
+
this.#pendingThinkingVisibilityControlSuccess = undefined;
|
|
10756
|
+
this.#pendingThinkingVisibilityControlFailure = undefined;
|
|
10071
10757
|
this.agent.hideThinkingSummary = visibility === "hidden";
|
|
10072
10758
|
if (persist) {
|
|
10073
10759
|
this.settings.set("hideThinkingBlock", visibility === "hidden");
|
|
@@ -10075,7 +10761,7 @@ export class AgentSession {
|
|
|
10075
10761
|
}
|
|
10076
10762
|
|
|
10077
10763
|
/**
|
|
10078
|
-
* Set thinking visibility from a control surface. Global changes
|
|
10764
|
+
* Set thinking visibility from a control surface. Global changes commit before affecting live state.
|
|
10079
10765
|
*/
|
|
10080
10766
|
async setThinkingVisibilityForControl(visibility: "visible" | "hidden", persist: boolean): Promise<void> {
|
|
10081
10767
|
if (!persist) {
|
|
@@ -10083,17 +10769,88 @@ export class AgentSession {
|
|
|
10083
10769
|
return;
|
|
10084
10770
|
}
|
|
10085
10771
|
|
|
10086
|
-
|
|
10087
|
-
const
|
|
10088
|
-
this
|
|
10772
|
+
this.#assertDurableSettingsWritable();
|
|
10773
|
+
const mutationRevision = ++this.#thinkingVisibilityMutationRevision;
|
|
10774
|
+
const expectedLiveMutationRevision = this.#thinkingVisibilityLiveMutationRevision;
|
|
10775
|
+
const expectedSessionId = this.sessionManager.getSessionId();
|
|
10776
|
+
const expectedModel = this.model;
|
|
10777
|
+
const expectedContextGeneration = this.#reasoningControlContextGeneration;
|
|
10089
10778
|
try {
|
|
10090
|
-
await this.settings.
|
|
10779
|
+
await this.settings.commitAtomicBatch([
|
|
10780
|
+
{ path: "hideThinkingBlock", op: "set", value: visibility === "hidden" },
|
|
10781
|
+
]);
|
|
10091
10782
|
} catch {
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10783
|
+
if (
|
|
10784
|
+
mutationRevision === this.#thinkingVisibilityMutationRevision &&
|
|
10785
|
+
this.#reasoningControlContextGeneration === expectedContextGeneration &&
|
|
10786
|
+
this.sessionManager.getSessionId() === expectedSessionId &&
|
|
10787
|
+
this.model === expectedModel
|
|
10788
|
+
) {
|
|
10789
|
+
const pending = this.#pendingThinkingVisibilityControlSuccess;
|
|
10790
|
+
this.#pendingThinkingVisibilityControlSuccess = undefined;
|
|
10791
|
+
if (
|
|
10792
|
+
pending &&
|
|
10793
|
+
pending.contextGeneration === expectedContextGeneration &&
|
|
10794
|
+
this.sessionManager.getSessionId() === pending.sessionId &&
|
|
10795
|
+
this.model === pending.model
|
|
10796
|
+
) {
|
|
10797
|
+
this.setThinkingVisibility(pending.visibility);
|
|
10798
|
+
} else {
|
|
10799
|
+
this.#pendingThinkingVisibilityControlFailure = {
|
|
10800
|
+
mutationRevision,
|
|
10801
|
+
liveMutationRevision: expectedLiveMutationRevision,
|
|
10802
|
+
sessionId: expectedSessionId,
|
|
10803
|
+
model: expectedModel,
|
|
10804
|
+
contextGeneration: expectedContextGeneration,
|
|
10805
|
+
};
|
|
10806
|
+
}
|
|
10807
|
+
}
|
|
10095
10808
|
throw new Error("Unable to persist reasoning settings.");
|
|
10096
10809
|
}
|
|
10810
|
+
if (
|
|
10811
|
+
mutationRevision === this.#thinkingVisibilityMutationRevision &&
|
|
10812
|
+
this.#thinkingVisibilityLiveMutationRevision === expectedLiveMutationRevision
|
|
10813
|
+
) {
|
|
10814
|
+
this.setThinkingVisibility(visibility);
|
|
10815
|
+
return;
|
|
10816
|
+
}
|
|
10817
|
+
|
|
10818
|
+
if (
|
|
10819
|
+
mutationRevision !== this.#thinkingVisibilityMutationRevision ||
|
|
10820
|
+
this.#reasoningControlContextGeneration !== expectedContextGeneration ||
|
|
10821
|
+
this.sessionManager.getSessionId() !== expectedSessionId ||
|
|
10822
|
+
this.model !== expectedModel
|
|
10823
|
+
) {
|
|
10824
|
+
if (
|
|
10825
|
+
this.#thinkingVisibilityLiveMutationRevision === expectedLiveMutationRevision &&
|
|
10826
|
+
this.#reasoningControlContextGeneration === expectedContextGeneration &&
|
|
10827
|
+
this.sessionManager.getSessionId() === expectedSessionId &&
|
|
10828
|
+
this.model === expectedModel &&
|
|
10829
|
+
(this.#pendingThinkingVisibilityControlSuccess?.mutationRevision ?? -1) < mutationRevision
|
|
10830
|
+
) {
|
|
10831
|
+
this.#pendingThinkingVisibilityControlSuccess = {
|
|
10832
|
+
visibility,
|
|
10833
|
+
mutationRevision,
|
|
10834
|
+
sessionId: expectedSessionId,
|
|
10835
|
+
model: expectedModel,
|
|
10836
|
+
contextGeneration: expectedContextGeneration,
|
|
10837
|
+
};
|
|
10838
|
+
const failure = this.#pendingThinkingVisibilityControlFailure;
|
|
10839
|
+
if (
|
|
10840
|
+
failure &&
|
|
10841
|
+
failure.mutationRevision === this.#thinkingVisibilityMutationRevision &&
|
|
10842
|
+
failure.liveMutationRevision === expectedLiveMutationRevision &&
|
|
10843
|
+
failure.sessionId === expectedSessionId &&
|
|
10844
|
+
failure.model === expectedModel &&
|
|
10845
|
+
failure.contextGeneration === expectedContextGeneration
|
|
10846
|
+
) {
|
|
10847
|
+
this.#pendingThinkingVisibilityControlFailure = undefined;
|
|
10848
|
+
this.setThinkingVisibility(visibility);
|
|
10849
|
+
}
|
|
10850
|
+
}
|
|
10851
|
+
return;
|
|
10852
|
+
}
|
|
10853
|
+
this.setThinkingVisibility(visibility);
|
|
10097
10854
|
}
|
|
10098
10855
|
|
|
10099
10856
|
/**
|
|
@@ -10261,6 +11018,16 @@ export class AgentSession {
|
|
|
10261
11018
|
return getSupportedEfforts(this.model);
|
|
10262
11019
|
}
|
|
10263
11020
|
|
|
11021
|
+
/**
|
|
11022
|
+
* Runtime evidence published for the current GJC bundle activation
|
|
11023
|
+
* generation, set once by `createAgentSession` after every producer has run.
|
|
11024
|
+
* Undefined until that publication happens, so consumers report runtime
|
|
11025
|
+
* status as unavailable rather than falsely clear.
|
|
11026
|
+
*/
|
|
11027
|
+
gjcRuntimeSnapshot?: GjcRuntimeSnapshotProvider;
|
|
11028
|
+
/** Activation generation a published snapshot must match to be merged. */
|
|
11029
|
+
gjcActivationGeneration?: number;
|
|
11030
|
+
|
|
10264
11031
|
// =========================================================================
|
|
10265
11032
|
// Message Queue Mode Management
|
|
10266
11033
|
// =========================================================================
|
|
@@ -10270,6 +11037,7 @@ export class AgentSession {
|
|
|
10270
11037
|
* Saves to settings.
|
|
10271
11038
|
*/
|
|
10272
11039
|
setSteeringMode(mode: "all" | "one-at-a-time"): void {
|
|
11040
|
+
this.#assertDurableSettingsWritable();
|
|
10273
11041
|
this.agent.setSteeringMode(mode);
|
|
10274
11042
|
this.settings.set("steeringMode", mode);
|
|
10275
11043
|
}
|
|
@@ -10279,6 +11047,7 @@ export class AgentSession {
|
|
|
10279
11047
|
* Saves to settings.
|
|
10280
11048
|
*/
|
|
10281
11049
|
setFollowUpMode(mode: "all" | "one-at-a-time"): void {
|
|
11050
|
+
this.#assertDurableSettingsWritable();
|
|
10282
11051
|
this.agent.setFollowUpMode(mode);
|
|
10283
11052
|
this.settings.set("followUpMode", mode);
|
|
10284
11053
|
}
|
|
@@ -10288,6 +11057,7 @@ export class AgentSession {
|
|
|
10288
11057
|
* Saves to settings.
|
|
10289
11058
|
*/
|
|
10290
11059
|
setInterruptMode(mode: "immediate" | "wait"): void {
|
|
11060
|
+
this.#assertDurableSettingsWritable();
|
|
10291
11061
|
this.agent.setInterruptMode(mode);
|
|
10292
11062
|
this.settings.set("interruptMode", mode);
|
|
10293
11063
|
}
|
|
@@ -10299,13 +11069,93 @@ export class AgentSession {
|
|
|
10299
11069
|
async #pruneToolOutputs(
|
|
10300
11070
|
signal?: AbortSignal,
|
|
10301
11071
|
overThreshold = false,
|
|
10302
|
-
|
|
11072
|
+
options?: { commitGate?: (actual: { prunedCount: number; tokensSaved: number }) => boolean },
|
|
11073
|
+
): Promise<{ prunedCount: number; tokensSaved: number; committed: boolean } | undefined> {
|
|
10303
11074
|
const branchEntries = this.sessionManager.getBranch();
|
|
10304
|
-
const
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
11075
|
+
const artifactManager = this.sessionManager.getArtifactManager();
|
|
11076
|
+
const prunedArtifacts: Array<{ entryId: string; id: string; toolType: string; originalText: string }> = [];
|
|
11077
|
+
let reservedArtifactId: string | undefined;
|
|
11078
|
+
if (artifactManager) {
|
|
11079
|
+
try {
|
|
11080
|
+
reservedArtifactId = (await artifactManager.allocatePath("tool-output")).id;
|
|
11081
|
+
} catch (error) {
|
|
11082
|
+
logger.warn("Failed to reserve artifact ID for pruned tool output", {
|
|
11083
|
+
error: error instanceof Error ? error.message : String(error),
|
|
11084
|
+
});
|
|
11085
|
+
}
|
|
11086
|
+
}
|
|
11087
|
+
const result = pruneToolOutputs(branchEntries, DEFAULT_PRUNE_CONFIG, {
|
|
11088
|
+
relaxedMinimum: overThreshold ? 0 : undefined,
|
|
11089
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11090
|
+
artifactRef: candidate => {
|
|
11091
|
+
if (!artifactManager) return undefined;
|
|
11092
|
+
const id = reservedArtifactId ?? String(artifactManager.allocateId());
|
|
11093
|
+
reservedArtifactId = undefined;
|
|
11094
|
+
const toolType = (candidate.toolName ?? "tool-output").replace(/[^a-zA-Z0-9_-]/g, "_") || "tool-output";
|
|
11095
|
+
prunedArtifacts.push({ entryId: candidate.entryId, id, toolType, originalText: candidate.originalText });
|
|
11096
|
+
return `artifact://${id}`;
|
|
11097
|
+
},
|
|
11098
|
+
});
|
|
11099
|
+
const failedArtifactEntryIds = new Set<string>();
|
|
11100
|
+
const publishedArtifacts: typeof prunedArtifacts = [];
|
|
11101
|
+
for (const artifact of prunedArtifacts.filter(artifact =>
|
|
11102
|
+
result.originals.some(original => original.entryId === artifact.entryId),
|
|
11103
|
+
)) {
|
|
11104
|
+
try {
|
|
11105
|
+
await artifactManager?.publishNamedNoReplace(
|
|
11106
|
+
`${artifact.id}.${artifact.toolType}.log`,
|
|
11107
|
+
new TextEncoder().encode(artifact.originalText),
|
|
11108
|
+
);
|
|
11109
|
+
publishedArtifacts.push(artifact);
|
|
11110
|
+
} catch (error) {
|
|
11111
|
+
failedArtifactEntryIds.add(artifact.entryId);
|
|
11112
|
+
logger.warn("Failed to persist pruned tool output artifact", {
|
|
11113
|
+
artifactId: artifact.id,
|
|
11114
|
+
error: error instanceof Error ? error.message : String(error),
|
|
11115
|
+
});
|
|
11116
|
+
}
|
|
11117
|
+
}
|
|
11118
|
+
const rollbackPublishedArtifacts = async (reason: "aborted" | "commit_gate_rejected") => {
|
|
11119
|
+
let removedArtifacts = 0;
|
|
11120
|
+
let unremovedArtifacts = 0;
|
|
11121
|
+
for (const artifact of publishedArtifacts) {
|
|
11122
|
+
const filename = `${artifact.id}.${artifact.toolType}.log`;
|
|
11123
|
+
let removed = false;
|
|
11124
|
+
for (let attempt = 0; attempt < 3 && !removed; attempt++) {
|
|
11125
|
+
removed = (await artifactManager?.removeNamedBestEffort(filename)) ?? false;
|
|
11126
|
+
if (!removed && attempt < 2) await Bun.sleep(10);
|
|
11127
|
+
}
|
|
11128
|
+
if (removed) removedArtifacts++;
|
|
11129
|
+
else {
|
|
11130
|
+
unremovedArtifacts++;
|
|
11131
|
+
logger.warn("Failed to roll back staged pruned-output artifact after retries", {
|
|
11132
|
+
artifactId: artifact.id,
|
|
11133
|
+
reason,
|
|
11134
|
+
});
|
|
11135
|
+
}
|
|
11136
|
+
}
|
|
11137
|
+
return { removedArtifacts, unremovedArtifacts };
|
|
11138
|
+
};
|
|
11139
|
+
let toolTokensSaved = result.tokensSaved;
|
|
11140
|
+
let toolPrunedCount = result.prunedCount;
|
|
11141
|
+
const committedToolEntries = result.prunedEntries.filter(entry => {
|
|
11142
|
+
if (!failedArtifactEntryIds.has(entry.id)) return true;
|
|
11143
|
+
const original = result.originals.find(candidate => candidate.entryId === entry.id);
|
|
11144
|
+
if (!original) return true;
|
|
11145
|
+
const message = entry.message as ToolResultMessage;
|
|
11146
|
+
toolTokensSaved -=
|
|
11147
|
+
original.tokens -
|
|
11148
|
+
estimateTextTokensHeuristic(
|
|
11149
|
+
message.content
|
|
11150
|
+
.filter((part): part is TextContent => part.type === "text")
|
|
11151
|
+
.map(part => part.text)
|
|
11152
|
+
.join(""),
|
|
11153
|
+
);
|
|
11154
|
+
toolPrunedCount--;
|
|
11155
|
+
message.content = [{ type: "text", text: original.originalText }];
|
|
11156
|
+
delete message.prunedAt;
|
|
11157
|
+
return false;
|
|
11158
|
+
});
|
|
10309
11159
|
const argumentResult = pruneAssistantToolArguments(branchEntries, DEFAULT_PRUNE_CONFIG);
|
|
10310
11160
|
const fileMentionResult = pruneStaleFileMentions(branchEntries, p =>
|
|
10311
11161
|
resolveReadPath(p, this.sessionManager.getCwd()),
|
|
@@ -10313,22 +11163,37 @@ export class AgentSession {
|
|
|
10313
11163
|
const volatileContextResult = pruneSupersededVolatileProjectContext(branchEntries);
|
|
10314
11164
|
const reminderResult = pruneSupersededMaintenanceReminders(branchEntries);
|
|
10315
11165
|
const tokensSaved =
|
|
10316
|
-
|
|
11166
|
+
toolTokensSaved +
|
|
10317
11167
|
argumentResult.argumentTokensSaved +
|
|
10318
11168
|
Math.round((fileMentionResult.bytesSaved + volatileContextResult.bytesSaved + reminderResult.bytesSaved) / 4);
|
|
10319
11169
|
const prunedCount =
|
|
10320
|
-
|
|
11170
|
+
toolPrunedCount +
|
|
10321
11171
|
argumentResult.argumentPrunedCount +
|
|
10322
11172
|
fileMentionResult.changed.length +
|
|
10323
11173
|
volatileContextResult.changed.length +
|
|
10324
11174
|
reminderResult.changed.length;
|
|
10325
11175
|
if (prunedCount === 0 || signal?.aborted) {
|
|
11176
|
+
if (publishedArtifacts.length > 0) await rollbackPublishedArtifacts("aborted");
|
|
11177
|
+
return undefined;
|
|
11178
|
+
}
|
|
11179
|
+
if (options?.commitGate && !options.commitGate({ prunedCount, tokensSaved })) {
|
|
11180
|
+
const rollback = await rollbackPublishedArtifacts("commit_gate_rejected");
|
|
11181
|
+
logger.info("Below-threshold maintenance pruning staged but not committed", {
|
|
11182
|
+
prunedCount,
|
|
11183
|
+
tokensSaved,
|
|
11184
|
+
rolledBackArtifacts: rollback.removedArtifacts,
|
|
11185
|
+
unremovedArtifacts: rollback.unremovedArtifacts,
|
|
11186
|
+
});
|
|
11187
|
+
return { prunedCount, tokensSaved, committed: false };
|
|
11188
|
+
}
|
|
11189
|
+
if (signal?.aborted) {
|
|
11190
|
+
await rollbackPublishedArtifacts("aborted");
|
|
10326
11191
|
return undefined;
|
|
10327
11192
|
}
|
|
10328
11193
|
|
|
10329
11194
|
// getBranch() returns materialized copies for blob-externalized entries, so
|
|
10330
11195
|
// the pruning mutations must be written back into the canonical store.
|
|
10331
|
-
const combined = [...
|
|
11196
|
+
const combined = [...committedToolEntries, ...argumentResult.prunedEntries, ...fileMentionResult.changed];
|
|
10332
11197
|
this.sessionManager.applyEntryMessageUpdates(combined);
|
|
10333
11198
|
this.sessionManager.applyCustomMessageEntryUpdates([...volatileContextResult.changed, ...reminderResult.changed]);
|
|
10334
11199
|
await this.sessionManager.rewriteEntries();
|
|
@@ -10339,7 +11204,7 @@ export class AgentSession {
|
|
|
10339
11204
|
this.#resetInjectedContextSignatures();
|
|
10340
11205
|
this.#syncTodoPhasesFromBranch();
|
|
10341
11206
|
this.#closeCodexProviderSessionsForHistoryRewrite();
|
|
10342
|
-
return { prunedCount, tokensSaved };
|
|
11207
|
+
return { prunedCount, tokensSaved, committed: true };
|
|
10343
11208
|
}
|
|
10344
11209
|
|
|
10345
11210
|
/**
|
|
@@ -10372,7 +11237,9 @@ export class AgentSession {
|
|
|
10372
11237
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
10373
11238
|
if (!compactionSettings.maintenancePruningEnabled) return;
|
|
10374
11239
|
|
|
10375
|
-
const estimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG
|
|
11240
|
+
const estimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
|
|
11241
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11242
|
+
});
|
|
10376
11243
|
const cacheEpochResetCost = this.#lastCacheEpochResetCost();
|
|
10377
11244
|
if (
|
|
10378
11245
|
!shouldRunMaintenancePrune({
|
|
@@ -10385,10 +11252,30 @@ export class AgentSession {
|
|
|
10385
11252
|
return;
|
|
10386
11253
|
}
|
|
10387
11254
|
|
|
10388
|
-
const pruneResult = await this.#pruneToolOutputs(
|
|
11255
|
+
const pruneResult = await this.#pruneToolOutputs(undefined, false, {
|
|
11256
|
+
commitGate: actual => actual.tokensSaved > cacheEpochResetCost,
|
|
11257
|
+
});
|
|
10389
11258
|
if (!pruneResult || pruneResult.prunedCount === 0) return;
|
|
10390
|
-
|
|
10391
|
-
|
|
11259
|
+
if (!pruneResult.committed) {
|
|
11260
|
+
const message = `Maintenance pruning skipped: actual savings ${pruneResult.tokensSaved} tokens <= cache-epoch reset cost ${cacheEpochResetCost} after artifact notices/publication rollback.`;
|
|
11261
|
+
await this.#emitSessionEvent({
|
|
11262
|
+
type: "notice",
|
|
11263
|
+
level: "info",
|
|
11264
|
+
source: "maintenance-prune",
|
|
11265
|
+
message,
|
|
11266
|
+
});
|
|
11267
|
+
logger.info("Below-threshold maintenance pruning skipped", {
|
|
11268
|
+
tokensSaved: pruneResult.tokensSaved,
|
|
11269
|
+
prunedCount: pruneResult.prunedCount,
|
|
11270
|
+
cacheEpochResetCost,
|
|
11271
|
+
minSavings: compactionSettings.maintenancePruningMinSavingsTokens,
|
|
11272
|
+
});
|
|
11273
|
+
return;
|
|
11274
|
+
}
|
|
11275
|
+
const resetReason =
|
|
11276
|
+
pruneResult.tokensSaved > cacheEpochResetCost
|
|
11277
|
+
? `below-threshold maintenance: reclaimed ${pruneResult.tokensSaved} tokens > cache-epoch reset cost ${cacheEpochResetCost}`
|
|
11278
|
+
: `below-threshold maintenance: reclaimed ${pruneResult.tokensSaved} tokens <= cache-epoch reset cost ${cacheEpochResetCost}`;
|
|
10392
11279
|
await this.#emitSessionEvent({
|
|
10393
11280
|
type: "notice",
|
|
10394
11281
|
level: "info",
|
|
@@ -10422,6 +11309,8 @@ export class AgentSession {
|
|
|
10422
11309
|
await this.abort();
|
|
10423
11310
|
const compactionAbortController = new AbortController();
|
|
10424
11311
|
this.#compactionAbortController = compactionAbortController;
|
|
11312
|
+
// Take this invocation's state snapshot for the summarizer context.
|
|
11313
|
+
const compactionStateSnapshot = await this.#compactionStateSnapshot();
|
|
10425
11314
|
|
|
10426
11315
|
try {
|
|
10427
11316
|
if (!this.model) {
|
|
@@ -10467,7 +11356,11 @@ export class AgentSession {
|
|
|
10467
11356
|
}
|
|
10468
11357
|
}
|
|
10469
11358
|
|
|
10470
|
-
const compactionPrep = await this.#prepareCompactionFromHooks(
|
|
11359
|
+
const compactionPrep = await this.#prepareCompactionFromHooks(
|
|
11360
|
+
preparation,
|
|
11361
|
+
hookCompaction,
|
|
11362
|
+
compactionStateSnapshot,
|
|
11363
|
+
);
|
|
10471
11364
|
|
|
10472
11365
|
let summary: string;
|
|
10473
11366
|
let shortSummary: string | undefined;
|
|
@@ -10955,7 +11848,12 @@ export class AgentSession {
|
|
|
10955
11848
|
* @param assistantMessage The assistant message to check
|
|
10956
11849
|
* @param skipAbortedCheck If false, include aborted messages (for pre-prompt check). Default: true
|
|
10957
11850
|
*/
|
|
10958
|
-
async #checkCompaction(
|
|
11851
|
+
async #checkCompaction(
|
|
11852
|
+
assistantMessage: AssistantMessage,
|
|
11853
|
+
skipAbortedCheck = true,
|
|
11854
|
+
onTerminalOverflowNoop?: () => void,
|
|
11855
|
+
resourceRunId?: string,
|
|
11856
|
+
): Promise<boolean> {
|
|
10959
11857
|
// Safety stops are terminal and must not trigger context maintenance.
|
|
10960
11858
|
if (
|
|
10961
11859
|
assistantMessage.errorKind === "provider_safety_stop" ||
|
|
@@ -10991,15 +11889,22 @@ export class AgentSession {
|
|
|
10991
11889
|
// Remove the error message from agent state (it IS saved to session for history,
|
|
10992
11890
|
// but we don't want it in context for the retry)
|
|
10993
11891
|
const messages = this.agent.state.messages;
|
|
11892
|
+
let removedOverflowAssistant = false;
|
|
10994
11893
|
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
10995
11894
|
this.agent.replaceMessages(messages.slice(0, -1));
|
|
11895
|
+
removedOverflowAssistant = true;
|
|
10996
11896
|
}
|
|
10997
11897
|
|
|
10998
11898
|
// Try context promotion first - switch to a larger model and retry without compacting
|
|
10999
11899
|
const promoted = await this.#tryContextPromotion(assistantMessage);
|
|
11000
11900
|
if (promoted) {
|
|
11001
11901
|
// Retry on the promoted (larger) model without compacting
|
|
11002
|
-
this.#scheduleAgentContinue({
|
|
11902
|
+
this.#scheduleAgentContinue({
|
|
11903
|
+
delayMs: 100,
|
|
11904
|
+
generation,
|
|
11905
|
+
suppressPredecessorAgentEnd: true,
|
|
11906
|
+
resourceRunId,
|
|
11907
|
+
});
|
|
11003
11908
|
|
|
11004
11909
|
return true;
|
|
11005
11910
|
}
|
|
@@ -11007,10 +11912,19 @@ export class AgentSession {
|
|
|
11007
11912
|
// No promotion target available fall through to compaction
|
|
11008
11913
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
11009
11914
|
if (compactionSettings.enabled && compactionSettings.strategy !== "off") {
|
|
11010
|
-
const status = await this.#runAutoCompaction("overflow", true
|
|
11915
|
+
const status = await this.#runAutoCompaction("overflow", true, false, {
|
|
11916
|
+
beforeTerminalOverflowNoop: () => {
|
|
11917
|
+
if (onTerminalOverflowNoop) {
|
|
11918
|
+
onTerminalOverflowNoop();
|
|
11919
|
+
} else if (removedOverflowAssistant) {
|
|
11920
|
+
this.agent.appendMessage(assistantMessage);
|
|
11921
|
+
}
|
|
11922
|
+
},
|
|
11923
|
+
resourceRunId,
|
|
11924
|
+
});
|
|
11011
11925
|
return "continuationScheduled" in status && status.continuationScheduled === true;
|
|
11012
11926
|
}
|
|
11013
|
-
return this.#scheduleOverflowRetryContinuation(generation);
|
|
11927
|
+
return await this.#scheduleOverflowRetryContinuation(generation, resourceRunId);
|
|
11014
11928
|
}
|
|
11015
11929
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
11016
11930
|
if (!compactionSettings.enabled || compactionSettings.strategy === "off") return false;
|
|
@@ -11030,6 +11944,7 @@ export class AgentSession {
|
|
|
11030
11944
|
return true;
|
|
11031
11945
|
const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
|
|
11032
11946
|
relaxedMinimum: 0,
|
|
11947
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11033
11948
|
});
|
|
11034
11949
|
if (
|
|
11035
11950
|
pruneEstimate.tokensSaved > 0 &&
|
|
@@ -11134,6 +12049,7 @@ export class AgentSession {
|
|
|
11134
12049
|
// prompt-cache epoch via #closeCodexProviderSessionsForHistoryRewrite.
|
|
11135
12050
|
const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
|
|
11136
12051
|
relaxedMinimum: 0,
|
|
12052
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11137
12053
|
});
|
|
11138
12054
|
let pruneResult: { prunedCount: number; tokensSaved: number } | undefined;
|
|
11139
12055
|
if (
|
|
@@ -11315,6 +12231,7 @@ export class AgentSession {
|
|
|
11315
12231
|
|
|
11316
12232
|
const pruneEstimate = estimateToolOutputPruneSavings(this.sessionManager.getBranch(), DEFAULT_PRUNE_CONFIG, {
|
|
11317
12233
|
relaxedMinimum: 0,
|
|
12234
|
+
artifactRefMaxChars: PRUNED_ARTIFACT_REF_MAX_CHARS,
|
|
11318
12235
|
});
|
|
11319
12236
|
if (
|
|
11320
12237
|
pruneEstimate.tokensSaved > 0 &&
|
|
@@ -11775,13 +12692,18 @@ export class AgentSession {
|
|
|
11775
12692
|
this.#defaultFallbackExhaustedLastTurn = false;
|
|
11776
12693
|
}
|
|
11777
12694
|
|
|
12695
|
+
#setAgentModelWithReasoningContext(model: Model | undefined): void {
|
|
12696
|
+
this.#reasoningControlContextGeneration++;
|
|
12697
|
+
this.agent.setModel(model);
|
|
12698
|
+
}
|
|
12699
|
+
|
|
11778
12700
|
#setModelWithProviderSessionReset(model: Model | undefined): void {
|
|
11779
12701
|
this.#defaultModelSelectionMutationRevision++;
|
|
11780
12702
|
const currentModel = this.model;
|
|
11781
12703
|
if (currentModel) {
|
|
11782
12704
|
this.#closeProviderSessionsForModelSwitch(currentModel, model);
|
|
11783
12705
|
}
|
|
11784
|
-
this
|
|
12706
|
+
this.#setAgentModelWithReasoningContext(model);
|
|
11785
12707
|
}
|
|
11786
12708
|
|
|
11787
12709
|
#setModelAuthoritatively(model: Model, cause: ModelChangeCause): void {
|
|
@@ -11790,7 +12712,7 @@ export class AgentSession {
|
|
|
11790
12712
|
if (cause !== "temporary-operation") this.#commitAllTemporaryProviderSessionScopes();
|
|
11791
12713
|
const currentModel = this.model;
|
|
11792
12714
|
if (currentModel) this.#closeProviderSessionsForModelSwitch(currentModel, model);
|
|
11793
|
-
this
|
|
12715
|
+
this.#setAgentModelWithReasoningContext(model);
|
|
11794
12716
|
this.#syncAppendOnlyContext(model);
|
|
11795
12717
|
}
|
|
11796
12718
|
|
|
@@ -12249,6 +13171,7 @@ export class AgentSession {
|
|
|
12249
13171
|
async #prepareCompactionFromHooks(
|
|
12250
13172
|
preparation: CompactionPreparation,
|
|
12251
13173
|
hookCompaction: CompactionResult | undefined,
|
|
13174
|
+
stateSnapshot: CompactionStateSnapshot,
|
|
12252
13175
|
): Promise<
|
|
12253
13176
|
| {
|
|
12254
13177
|
kind: "fromHook";
|
|
@@ -12288,11 +13211,17 @@ export class AgentSession {
|
|
|
12288
13211
|
hookContext = hookContext ? [...hookContext, memoryBackendContext] : [memoryBackendContext];
|
|
12289
13212
|
}
|
|
12290
13213
|
|
|
13214
|
+
const stateContext = this.#compactionStateContext(stateSnapshot);
|
|
13215
|
+
|
|
13216
|
+
if (stateContext.length > 0) {
|
|
13217
|
+
hookContext = hookContext ? [...hookContext, ...stateContext] : stateContext;
|
|
13218
|
+
}
|
|
13219
|
+
|
|
12291
13220
|
if (hookCompaction) {
|
|
12292
13221
|
preserveData ??= hookCompaction.preserveData;
|
|
12293
13222
|
return {
|
|
12294
13223
|
kind: "fromHook",
|
|
12295
|
-
summary: hookCompaction.summary,
|
|
13224
|
+
summary: appendCompactionStateContext(hookCompaction.summary, stateContext),
|
|
12296
13225
|
shortSummary: hookCompaction.shortSummary,
|
|
12297
13226
|
firstKeptEntryId: hookCompaction.firstKeptEntryId,
|
|
12298
13227
|
tokensBefore: hookCompaction.tokensBefore,
|
|
@@ -12316,6 +13245,8 @@ export class AgentSession {
|
|
|
12316
13245
|
deferHandoffMaintenance?: boolean;
|
|
12317
13246
|
force?: boolean;
|
|
12318
13247
|
signal?: AbortSignal;
|
|
13248
|
+
beforeTerminalOverflowNoop?: () => void;
|
|
13249
|
+
resourceRunId?: string;
|
|
12319
13250
|
},
|
|
12320
13251
|
): Promise<AutoCompactionTerminalStatus> {
|
|
12321
13252
|
const compactionSettings = this.settings.getGroup("compaction");
|
|
@@ -12337,7 +13268,7 @@ export class AgentSession {
|
|
|
12337
13268
|
if (signal.aborted) return;
|
|
12338
13269
|
await this.#runAutoCompaction(reason, willRetry, true, options);
|
|
12339
13270
|
},
|
|
12340
|
-
{ generation },
|
|
13271
|
+
{ generation, resourceRunId: options?.resourceRunId },
|
|
12341
13272
|
);
|
|
12342
13273
|
return { kind: "skipped" };
|
|
12343
13274
|
}
|
|
@@ -12369,6 +13300,10 @@ export class AgentSession {
|
|
|
12369
13300
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
12370
13301
|
await this.#emitSessionEvent({ type: "auto_compaction_start", reason, action });
|
|
12371
13302
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
13303
|
+
const compactionStateSnapshot = await this.#compactionStateSnapshot();
|
|
13304
|
+
if (autoCompactionSignal.aborted || this.#isDisposed || this.#promptGeneration !== generation) {
|
|
13305
|
+
return await emitAborted();
|
|
13306
|
+
}
|
|
12372
13307
|
|
|
12373
13308
|
if (compactionSettings.strategy === "handoff" && reason !== "overflow") {
|
|
12374
13309
|
const handoffFocus = AUTO_HANDOFF_THRESHOLD_FOCUS;
|
|
@@ -12408,6 +13343,7 @@ export class AgentSession {
|
|
|
12408
13343
|
if (continueAfterMaintenance && reason !== "idle" && compactionSettings.autoContinue !== false) {
|
|
12409
13344
|
this.#scheduleAutoContinuePrompt(generation);
|
|
12410
13345
|
}
|
|
13346
|
+
|
|
12411
13347
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
12412
13348
|
return { kind: "compacted" };
|
|
12413
13349
|
}
|
|
@@ -12453,18 +13389,54 @@ export class AgentSession {
|
|
|
12453
13389
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
12454
13390
|
|
|
12455
13391
|
if (!preparation) {
|
|
12456
|
-
|
|
13392
|
+
// #checkCompaction removes the failed overflow assistant before entering
|
|
13393
|
+
// this path. A resumable tail would therefore resend the same oversized
|
|
13394
|
+
// request even though maintenance made no change. Non-resumable tails
|
|
13395
|
+
// retain the existing synthetic auto-continue recovery, which changes
|
|
13396
|
+
// the request and is covered by the continuation contract.
|
|
13397
|
+
const overflowNoopWouldReplay = reason === "overflow" && willRetry && this.#isResumableAgentTail();
|
|
13398
|
+
const continuationSkipReason =
|
|
13399
|
+
!overflowNoopWouldReplay && willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
13400
|
+
if (continuationSkipReason) {
|
|
13401
|
+
this.#logCompactionContinuationSkipped("overflow_retry", continuationSkipReason);
|
|
13402
|
+
}
|
|
13403
|
+
if (overflowNoopWouldReplay) {
|
|
13404
|
+
options?.beforeTerminalOverflowNoop?.();
|
|
13405
|
+
}
|
|
13406
|
+
const overflowContinuationScheduled =
|
|
13407
|
+
!overflowNoopWouldReplay && willRetry && !continuationSkipReason
|
|
13408
|
+
? await this.#scheduleOverflowRetryContinuation(generation)
|
|
13409
|
+
: false;
|
|
12457
13410
|
await this.#emitSessionEvent({
|
|
12458
13411
|
type: "auto_compaction_end",
|
|
12459
13412
|
action,
|
|
12460
13413
|
result: undefined,
|
|
12461
13414
|
aborted: false,
|
|
12462
|
-
willRetry:
|
|
13415
|
+
willRetry: overflowContinuationScheduled,
|
|
13416
|
+
errorMessage: overflowNoopWouldReplay
|
|
13417
|
+
? "Context overflow recovery skipped: nothing eligible to compact. Run /clear to preserve this session ID, or switch to a larger-context model before retrying."
|
|
13418
|
+
: undefined,
|
|
12463
13419
|
skipped: true,
|
|
12464
13420
|
continuationSkipReason,
|
|
12465
13421
|
});
|
|
13422
|
+
if (overflowNoopWouldReplay) {
|
|
13423
|
+
if (continueAfterMaintenance && this.agent.hasQueuedMessages()) {
|
|
13424
|
+
this.#scheduleAgentContinue({
|
|
13425
|
+
delayMs: 100,
|
|
13426
|
+
generation,
|
|
13427
|
+
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
13428
|
+
rescheduleOnBusy: true,
|
|
13429
|
+
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
13430
|
+
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
13431
|
+
});
|
|
13432
|
+
return { kind: "skipped", continuationScheduled: true };
|
|
13433
|
+
}
|
|
13434
|
+
return { kind: "skipped" };
|
|
13435
|
+
}
|
|
12466
13436
|
if (willRetry) {
|
|
12467
|
-
return
|
|
13437
|
+
return overflowContinuationScheduled
|
|
13438
|
+
? { kind: "skipped", continuationScheduled: true }
|
|
13439
|
+
: { kind: "skipped" };
|
|
12468
13440
|
}
|
|
12469
13441
|
if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
12470
13442
|
this.#scheduleAgentContinue({
|
|
@@ -12473,6 +13445,7 @@ export class AgentSession {
|
|
|
12473
13445
|
suppressPredecessorAgentEnd: true,
|
|
12474
13446
|
|
|
12475
13447
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
13448
|
+
rescheduleOnBusy: true,
|
|
12476
13449
|
onSkip: skipReason => this.#logCompactionContinuationSkipped("queued_continue", skipReason),
|
|
12477
13450
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
12478
13451
|
});
|
|
@@ -12513,7 +13486,11 @@ export class AgentSession {
|
|
|
12513
13486
|
}
|
|
12514
13487
|
}
|
|
12515
13488
|
|
|
12516
|
-
const compactionPrep = await this.#prepareCompactionFromHooks(
|
|
13489
|
+
const compactionPrep = await this.#prepareCompactionFromHooks(
|
|
13490
|
+
preparation,
|
|
13491
|
+
hookCompaction,
|
|
13492
|
+
compactionStateSnapshot,
|
|
13493
|
+
);
|
|
12517
13494
|
if (autoCompactionSignal.aborted) return await emitAborted();
|
|
12518
13495
|
|
|
12519
13496
|
let summary: string;
|
|
@@ -12696,18 +13673,25 @@ export class AgentSession {
|
|
|
12696
13673
|
this.#lastOversizedAutoMaintenanceAttemptSignature = undefined;
|
|
12697
13674
|
|
|
12698
13675
|
const continuationSkipReason = willRetry ? this.#detectOverflowRetryContinuationSkip() : undefined;
|
|
13676
|
+
if (continuationSkipReason) {
|
|
13677
|
+
this.#logCompactionContinuationSkipped("overflow_retry", continuationSkipReason);
|
|
13678
|
+
}
|
|
13679
|
+
const overflowContinuationScheduled =
|
|
13680
|
+
willRetry && !continuationSkipReason ? await this.#scheduleOverflowRetryContinuation(generation) : false;
|
|
12699
13681
|
await this.#emitSessionEvent({
|
|
12700
13682
|
type: "auto_compaction_end",
|
|
12701
13683
|
action,
|
|
12702
13684
|
result,
|
|
12703
13685
|
aborted: false,
|
|
12704
|
-
willRetry:
|
|
13686
|
+
willRetry: overflowContinuationScheduled,
|
|
12705
13687
|
continuationSkipReason,
|
|
12706
13688
|
});
|
|
12707
13689
|
if (autoCompactionSignal.aborted) return { kind: "aborted", source: "signal" };
|
|
12708
13690
|
|
|
12709
13691
|
if (willRetry) {
|
|
12710
|
-
return
|
|
13692
|
+
return overflowContinuationScheduled
|
|
13693
|
+
? { kind: "compacted", continuationScheduled: true }
|
|
13694
|
+
: { kind: "compacted" };
|
|
12711
13695
|
}
|
|
12712
13696
|
if (continueAfterMaintenance && reason !== "idle" && this.agent.hasQueuedMessages()) {
|
|
12713
13697
|
// Auto-compaction can complete while follow-up/steering/custom messages are waiting.
|
|
@@ -12717,6 +13701,7 @@ export class AgentSession {
|
|
|
12717
13701
|
generation,
|
|
12718
13702
|
suppressPredecessorAgentEnd: true,
|
|
12719
13703
|
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
13704
|
+
rescheduleOnBusy: true,
|
|
12720
13705
|
onSkip: reason => this.#logCompactionContinuationSkipped("queued_continue", reason),
|
|
12721
13706
|
onError: error => this.#logCompactionContinuationError("queued_continue", error),
|
|
12722
13707
|
});
|
|
@@ -12831,6 +13816,10 @@ export class AgentSession {
|
|
|
12831
13816
|
return false;
|
|
12832
13817
|
}
|
|
12833
13818
|
|
|
13819
|
+
#isTypedFirstEventTimeout(message: AssistantMessage): boolean {
|
|
13820
|
+
return message.transportFailure?.providerCode?.toLowerCase() === STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE;
|
|
13821
|
+
}
|
|
13822
|
+
|
|
12834
13823
|
#isTerminalProviderFirstEventTimeout(message: AssistantMessage): boolean {
|
|
12835
13824
|
return this.#isKimiCodeFirstEventTimeout(message) || this.#isAlibabaTokenPlanFirstEventTimeout(message);
|
|
12836
13825
|
}
|
|
@@ -12956,6 +13945,7 @@ export class AgentSession {
|
|
|
12956
13945
|
#classifyErrorForRetry(message: AssistantMessage): RetryErrorClassification {
|
|
12957
13946
|
if (message.stopReason !== "error") return "none";
|
|
12958
13947
|
if (message.errorKind === "provider_safety_stop") return "terminal";
|
|
13948
|
+
if (this.#isTypedFirstEventTimeout(message)) return "first_event_timeout";
|
|
12959
13949
|
if (!message.errorMessage) return "none";
|
|
12960
13950
|
const err = message.errorMessage;
|
|
12961
13951
|
// Provider safety refusals (e.g. Anthropic stop_reason "refusal" /
|
|
@@ -12996,10 +13986,8 @@ export class AgentSession {
|
|
|
12996
13986
|
if (this.#isTerminalProviderFirstEventTimeout(message)) {
|
|
12997
13987
|
return "terminal";
|
|
12998
13988
|
}
|
|
12999
|
-
//
|
|
13000
|
-
//
|
|
13001
|
-
// the full request to a remote, billable backend, so an unbounded loop
|
|
13002
|
-
// can silently spike usage (#713). Bound it to retry.maxRetries instead.
|
|
13989
|
+
// Legacy providers that have not yet stamped the typed timeout fact retain
|
|
13990
|
+
// their existing bounded ollama-cloud behavior.
|
|
13003
13991
|
if (this.#isFirstEventTimeoutErrorMessage(err) && this.#shouldFailClosedOnFirstEventTimeout(message)) {
|
|
13004
13992
|
return "first_event_timeout";
|
|
13005
13993
|
}
|
|
@@ -13169,8 +14157,15 @@ export class AgentSession {
|
|
|
13169
14157
|
type: "maintenance",
|
|
13170
14158
|
continuation: async ownership => {
|
|
13171
14159
|
if (!ownership.isCurrent()) return;
|
|
13172
|
-
|
|
13173
|
-
|
|
14160
|
+
let terminalized = false;
|
|
14161
|
+
const successorScheduled = await this.#checkCompaction(outcome.message, true, () => {
|
|
14162
|
+
terminalized = true;
|
|
14163
|
+
this.agent.requestRunTerminal(ownership.logicalRunId, {
|
|
14164
|
+
stopReason: "error",
|
|
14165
|
+
messages: [outcome.message],
|
|
14166
|
+
});
|
|
14167
|
+
});
|
|
14168
|
+
if (terminalized || successorScheduled || !ownership.isCurrent()) return;
|
|
13174
14169
|
this.agent.requestRunTerminal(ownership.logicalRunId, {
|
|
13175
14170
|
stopReason: "error",
|
|
13176
14171
|
messages: [outcome.message],
|
|
@@ -13328,25 +14323,25 @@ export class AgentSession {
|
|
|
13328
14323
|
return `Model fallback chain exhausted; models tried: ${tried}; models skipped: ${skipped}`;
|
|
13329
14324
|
}
|
|
13330
14325
|
|
|
13331
|
-
async #
|
|
13332
|
-
class: FallbackTriggerClass;
|
|
13333
|
-
retryAfterMs?: number;
|
|
13334
|
-
}): Promise<boolean> {
|
|
14326
|
+
async #markFailedCredential(trigger: { class: FallbackTriggerClass; retryAfterMs?: number }): Promise<boolean> {
|
|
13335
14327
|
if (!this.model || (trigger.class !== "auth" && trigger.class !== "quota" && trigger.class !== "rate_limit")) {
|
|
13336
14328
|
return false;
|
|
13337
14329
|
}
|
|
13338
14330
|
const authStorage = this.#modelRegistry.authStorage;
|
|
14331
|
+
const providerAffinitySessionId = this.agent.providerSessionId ?? this.agent.sessionId ?? this.sessionId;
|
|
13339
14332
|
if (trigger.class === "auth") {
|
|
13340
|
-
const apiKey = await this.#modelRegistry.getApiKey(this.model,
|
|
14333
|
+
const apiKey = await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId);
|
|
13341
14334
|
if (!isAuthenticated(apiKey)) return false;
|
|
13342
|
-
return authStorage.invalidateCredentialMatching(this.model.provider, apiKey, {
|
|
14335
|
+
return authStorage.invalidateCredentialMatching(this.model.provider, apiKey, {
|
|
14336
|
+
sessionId: providerAffinitySessionId,
|
|
14337
|
+
});
|
|
13343
14338
|
}
|
|
13344
14339
|
if (authStorage.hasRuntimeApiKey(this.model.provider)) return false;
|
|
13345
|
-
const activeApiKey = await this.#modelRegistry.getApiKey(this.model,
|
|
13346
|
-
const rotated = await authStorage.markUsageLimitReached(this.model.provider,
|
|
14340
|
+
const activeApiKey = await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId);
|
|
14341
|
+
const rotated = await authStorage.markUsageLimitReached(this.model.provider, providerAffinitySessionId, {
|
|
13347
14342
|
retryAfterMs: trigger.retryAfterMs,
|
|
13348
14343
|
});
|
|
13349
|
-
return rotated && (await this.#modelRegistry.getApiKey(this.model,
|
|
14344
|
+
return rotated && (await this.#modelRegistry.getApiKey(this.model, providerAffinitySessionId)) !== activeApiKey;
|
|
13350
14345
|
}
|
|
13351
14346
|
|
|
13352
14347
|
/** Handle retryable errors with exponential backoff. */
|
|
@@ -13367,16 +14362,16 @@ export class AgentSession {
|
|
|
13367
14362
|
// user opt-out.
|
|
13368
14363
|
if (!managedFallback && !retrySettings.enabled) return false;
|
|
13369
14364
|
const classification = managedFallback ? undefined : this.#classifyErrorForRetry(message);
|
|
13370
|
-
|
|
13371
|
-
|
|
13372
|
-
|
|
13373
|
-
|
|
13374
|
-
|
|
13375
|
-
|
|
13376
|
-
|
|
13377
|
-
)
|
|
13378
|
-
|
|
13379
|
-
|
|
14365
|
+
if (classification === "first_event_timeout") {
|
|
14366
|
+
this.#firstEventTimeoutRetryStartedAt ??= Date.now();
|
|
14367
|
+
}
|
|
14368
|
+
// First-event timeouts are replay-safe only before any model progress,
|
|
14369
|
+
// abort, or extension/provider lifecycle participation.
|
|
14370
|
+
if (
|
|
14371
|
+
classification === "first_event_timeout" &&
|
|
14372
|
+
(!this.#hasCleanRetryReplaySafety || assistantMessageHasVisibleOrToolContent(message))
|
|
14373
|
+
) {
|
|
14374
|
+
return false;
|
|
13380
14375
|
}
|
|
13381
14376
|
const trigger = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
|
|
13382
14377
|
if (!trigger) {
|
|
@@ -13384,6 +14379,39 @@ export class AgentSession {
|
|
|
13384
14379
|
? this.#managedFallbackExhaustionDecision(message, message.errorMessage || "Model fallback attempt failed")
|
|
13385
14380
|
: false;
|
|
13386
14381
|
}
|
|
14382
|
+
// Credential rotation: a content-free quota/rate-limit failure has no
|
|
14383
|
+
// observable state to corrupt, so it is replay-safe regardless of
|
|
14384
|
+
// extension lifecycle participation. Mark the failed credential and
|
|
14385
|
+
// retry with the next stored credential of the same provider.
|
|
14386
|
+
let credentialRotated =
|
|
14387
|
+
!managedFallback &&
|
|
14388
|
+
!assistantMessageHasVisibleOrToolContent(message) &&
|
|
14389
|
+
(trigger.class === "quota" || trigger.class === "rate_limit") &&
|
|
14390
|
+
(await this.#markFailedCredential(trigger));
|
|
14391
|
+
// A content-free credential rotation is inherently replay-safe: no partial
|
|
14392
|
+
// output, no tool calls, no extension-observable streaming state was produced
|
|
14393
|
+
// before the failure. This bypasses #hasCleanRetryReplaySafety because the
|
|
14394
|
+
// content-free check is the replay-safety guarantee for credential rotation.
|
|
14395
|
+
const canReplayRotatedCredential = credentialRotated;
|
|
14396
|
+
// Bare defaults admit only clean, side-effect-free canonical stream watchdog failures,
|
|
14397
|
+
// content-free quota/rate-limit credential rotations, and the explicit Codex
|
|
14398
|
+
// capacity-overload event.
|
|
14399
|
+
if (!managedFallback && !legacyRetryConfigured && !canReplayRotatedCredential) {
|
|
14400
|
+
const bareDefaultCodexOverload = isBareDefaultCodexOverload(message);
|
|
14401
|
+
// Content-free Codex overload is replay-safe by the same reasoning as
|
|
14402
|
+
// credential rotation: no partial output means no observable state.
|
|
14403
|
+
const canReplayCodexOverload = bareDefaultCodexOverload;
|
|
14404
|
+
if (
|
|
14405
|
+
(!canReplayCodexOverload &&
|
|
14406
|
+
!this.#isTypedFirstEventTimeout(message) &&
|
|
14407
|
+
(hasBareDefaultRetryDisqualifyingFacts(message) ||
|
|
14408
|
+
(classification !== "transient" && classification !== "first_event_timeout") ||
|
|
14409
|
+
!BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))) ||
|
|
14410
|
+
(!canReplayCodexOverload && !this.#hasCleanRetryReplaySafety)
|
|
14411
|
+
) {
|
|
14412
|
+
return false;
|
|
14413
|
+
}
|
|
14414
|
+
}
|
|
13387
14415
|
const legacyUnbounded = classification === "transient";
|
|
13388
14416
|
const attemptsUsed = managedFallback ? controller.attemptsUsed || 1 : this.#retryAttempt + 1;
|
|
13389
14417
|
const failedSelector = managedFallback ? controller.currentSelector() : undefined;
|
|
@@ -13392,12 +14420,14 @@ export class AgentSession {
|
|
|
13392
14420
|
: legacyUnbounded || attemptsUsed <= retrySettings.maxRetries
|
|
13393
14421
|
? "retry"
|
|
13394
14422
|
: "exhausted";
|
|
13395
|
-
|
|
13396
|
-
|
|
13397
|
-
|
|
13398
|
-
|
|
13399
|
-
|
|
13400
|
-
|
|
14423
|
+
// Credential rotation is unbounded: a fresh credential is a different
|
|
14424
|
+
// retry dimension from transient-error backoff, so it overrides maxRetries
|
|
14425
|
+
// exhaustion and forces an immediate same-model retry.
|
|
14426
|
+
if (managedFallback && outcome === "advance" && (trigger.class === "quota" || trigger.class === "rate_limit")) {
|
|
14427
|
+
credentialRotated = await this.#markFailedCredential(trigger);
|
|
14428
|
+
}
|
|
14429
|
+
if (credentialRotated) {
|
|
14430
|
+
if (managedFallback) controller.restorePreviousEntryForRetry();
|
|
13401
14431
|
outcome = "retry";
|
|
13402
14432
|
}
|
|
13403
14433
|
if (outcome === "exhausted") {
|
|
@@ -13408,6 +14438,10 @@ export class AgentSession {
|
|
|
13408
14438
|
controller.resetSticky();
|
|
13409
14439
|
return managedOutcome ? this.#managedFallbackExhaustionDecision(message, errorMessage) : false;
|
|
13410
14440
|
}
|
|
14441
|
+
if (classification === "first_event_timeout") {
|
|
14442
|
+
const elapsedMs = Date.now() - (this.#firstEventTimeoutRetryStartedAt ?? Date.now());
|
|
14443
|
+
message.errorMessage = `First-event stream timeout exhausted after ${attemptsUsed} attempts; waited ${elapsedMs}ms total: ${message.errorMessage ?? "Unknown error"}`;
|
|
14444
|
+
}
|
|
13411
14445
|
return false;
|
|
13412
14446
|
}
|
|
13413
14447
|
|
|
@@ -13429,7 +14463,7 @@ export class AgentSession {
|
|
|
13429
14463
|
}
|
|
13430
14464
|
|
|
13431
14465
|
const retry = async (ownership?: ManagedAttemptContinuationOwnership): Promise<void> => {
|
|
13432
|
-
if (managedFallback && !credentialRotated) await this.#
|
|
14466
|
+
if (managedFallback && !credentialRotated) await this.#markFailedCredential(trigger);
|
|
13433
14467
|
let advanced = outcome !== "advance";
|
|
13434
14468
|
let resolutionError: unknown;
|
|
13435
14469
|
if (outcome === "advance") {
|
|
@@ -13471,7 +14505,12 @@ export class AgentSession {
|
|
|
13471
14505
|
await this.#emitSessionEvent({
|
|
13472
14506
|
type: "auto_retry_start",
|
|
13473
14507
|
attempt: this.#retryAttempt,
|
|
13474
|
-
maxAttempts:
|
|
14508
|
+
maxAttempts:
|
|
14509
|
+
classification === "first_event_timeout"
|
|
14510
|
+
? retrySettings.maxRetries + 1
|
|
14511
|
+
: managedFallback
|
|
14512
|
+
? controller.maxAttempts
|
|
14513
|
+
: retrySettings.maxRetries,
|
|
13475
14514
|
delayMs,
|
|
13476
14515
|
errorMessage,
|
|
13477
14516
|
unbounded: managedFallback ? false : legacyUnbounded,
|
|
@@ -13479,9 +14518,19 @@ export class AgentSession {
|
|
|
13479
14518
|
|
|
13480
14519
|
const messages = this.agent.state.messages;
|
|
13481
14520
|
if (messages.length > 0 && messages[messages.length - 1].role === "assistant") {
|
|
13482
|
-
|
|
14521
|
+
let end = messages.length - 1;
|
|
14522
|
+
// A wedged turn can leave more than one assistant tail behind (e.g. the
|
|
14523
|
+
// agent loop's invalid_prompt repair resend commits its own rejected
|
|
14524
|
+
// message). agent.continue() refuses ANY assistant tail, so strip the
|
|
14525
|
+
// whole trailing run of failed attempts, not just the last one.
|
|
14526
|
+
while (end > 0) {
|
|
14527
|
+
const previous = messages[end - 1];
|
|
14528
|
+
if (previous.role !== "assistant") break;
|
|
14529
|
+
if (previous.stopReason !== "error" && previous.stopReason !== "aborted") break;
|
|
14530
|
+
end--;
|
|
14531
|
+
}
|
|
14532
|
+
this.agent.replaceMessages(messages.slice(0, end));
|
|
13483
14533
|
}
|
|
13484
|
-
|
|
13485
14534
|
try {
|
|
13486
14535
|
await scheduler.wait(delayMs, { signal: retryAbortController.signal });
|
|
13487
14536
|
} catch {
|
|
@@ -13721,12 +14770,8 @@ export class AgentSession {
|
|
|
13721
14770
|
// Bash Execution
|
|
13722
14771
|
// =========================================================================
|
|
13723
14772
|
|
|
13724
|
-
async #saveBashOriginalArtifact(originalText: string): Promise<
|
|
13725
|
-
|
|
13726
|
-
return await this.sessionManager.saveArtifact(originalText, "bash-original");
|
|
13727
|
-
} catch {
|
|
13728
|
-
return undefined;
|
|
13729
|
-
}
|
|
14773
|
+
async #saveBashOriginalArtifact(originalText: string): Promise<BashArtifactSaveResult> {
|
|
14774
|
+
return saveAgentBashOriginalArtifact(this.sessionManager, originalText);
|
|
13730
14775
|
}
|
|
13731
14776
|
|
|
13732
14777
|
/**
|
|
@@ -14656,7 +15701,10 @@ export class AgentSession {
|
|
|
14656
15701
|
* Listeners are preserved and will continue receiving events.
|
|
14657
15702
|
* @returns true if switch completed, false if cancelled by hook
|
|
14658
15703
|
*/
|
|
14659
|
-
async switchSession(
|
|
15704
|
+
async switchSession(
|
|
15705
|
+
sessionPath: string,
|
|
15706
|
+
options?: { transition?: SessionSwitchEvent["transition"] },
|
|
15707
|
+
): Promise<boolean> {
|
|
14660
15708
|
this.#beginSessionTransition("switch-session");
|
|
14661
15709
|
try {
|
|
14662
15710
|
const previousSessionFile = this.sessionManager.getSessionFile();
|
|
@@ -14717,7 +15765,7 @@ export class AgentSession {
|
|
|
14717
15765
|
// workflow-gate emitter, and hooks cannot resolve against an ungated
|
|
14718
15766
|
// root. The manager-rotation window itself is tracked in #3138
|
|
14719
15767
|
// (#2797 / #2925).
|
|
14720
|
-
if (switchingToDifferentSession) await
|
|
15768
|
+
if (switchingToDifferentSession) await this.#initializeLocalRootForLoadedSession();
|
|
14721
15769
|
this.#syncAgentSessionId();
|
|
14722
15770
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
14723
15771
|
|
|
@@ -14739,9 +15787,13 @@ export class AgentSession {
|
|
|
14739
15787
|
this.#rebindProviderSessionState(new Map());
|
|
14740
15788
|
}
|
|
14741
15789
|
|
|
15790
|
+
const resumeModelBehavior = this.settings.get("session.resumeModelBehavior");
|
|
14742
15791
|
const configuredDefaultChain = sessionContext.configuredModelChains.default?.entries;
|
|
15792
|
+
const settingsDefaultEntries = normalizeModelSelectorValue(this.settings.getModelRole("default"));
|
|
14743
15793
|
const defaultEntries =
|
|
14744
|
-
|
|
15794
|
+
resumeModelBehavior === "useCurrentDefault"
|
|
15795
|
+
? settingsDefaultEntries
|
|
15796
|
+
: (configuredDefaultChain ?? (sessionContext.models.default ? [sessionContext.models.default] : []));
|
|
14745
15797
|
this.#defaultFallbackController = undefined;
|
|
14746
15798
|
if (defaultEntries.length > 0) {
|
|
14747
15799
|
const resolution = await resolveModelChainWithAuth(
|
|
@@ -14782,6 +15834,12 @@ export class AgentSession {
|
|
|
14782
15834
|
? this.#getInheritedThinkingLevel()
|
|
14783
15835
|
: persistedThinkingLevel,
|
|
14784
15836
|
);
|
|
15837
|
+
this.#thinkingLevelMutationRevision++;
|
|
15838
|
+
this.#thinkingLevelLiveMutationRevision++;
|
|
15839
|
+
this.#pendingThinkingLevelControlSuccess = undefined;
|
|
15840
|
+
this.#pendingThinkingLevelControlFailure = undefined;
|
|
15841
|
+
this.#pendingThinkingVisibilityControlSuccess = undefined;
|
|
15842
|
+
this.#pendingThinkingVisibilityControlFailure = undefined;
|
|
14785
15843
|
this.#thinkingLevel = nextThinkingLevel;
|
|
14786
15844
|
this.agent.setThinkingLevel(toReasoningEffort(nextThinkingLevel));
|
|
14787
15845
|
this.agent.serviceTier = hasServiceTierEntry
|
|
@@ -14792,6 +15850,7 @@ export class AgentSession {
|
|
|
14792
15850
|
// Establish the successor's durable session identity only after every
|
|
14793
15851
|
// restored state facet is live. Identity-bound extension hooks run below.
|
|
14794
15852
|
await this.sessionManager.ensureOnDisk();
|
|
15853
|
+
if (!switchingToDifferentSession) await this.#initializeLocalRootForLoadedSession();
|
|
14795
15854
|
|
|
14796
15855
|
if (switchingToDifferentSession) {
|
|
14797
15856
|
// The local:// migration gate for this successor already ran above,
|
|
@@ -14814,6 +15873,7 @@ export class AgentSession {
|
|
|
14814
15873
|
type: "session_switch",
|
|
14815
15874
|
reason: "resume",
|
|
14816
15875
|
previousSessionFile,
|
|
15876
|
+
...(options?.transition ? { transition: options.transition } : {}),
|
|
14817
15877
|
});
|
|
14818
15878
|
}
|
|
14819
15879
|
return true;
|
|
@@ -14849,8 +15909,14 @@ export class AgentSession {
|
|
|
14849
15909
|
this.agent.restoreSteering(previousAgentSteeringQueue);
|
|
14850
15910
|
this.agent.restoreFollowUp(previousAgentFollowUpQueue);
|
|
14851
15911
|
if (previousModel) {
|
|
14852
|
-
this
|
|
15912
|
+
this.#setAgentModelWithReasoningContext(previousModel);
|
|
14853
15913
|
}
|
|
15914
|
+
this.#thinkingLevelMutationRevision++;
|
|
15915
|
+
this.#thinkingLevelLiveMutationRevision++;
|
|
15916
|
+
this.#pendingThinkingLevelControlSuccess = undefined;
|
|
15917
|
+
this.#pendingThinkingLevelControlFailure = undefined;
|
|
15918
|
+
this.#pendingThinkingVisibilityControlSuccess = undefined;
|
|
15919
|
+
this.#pendingThinkingVisibilityControlFailure = undefined;
|
|
14854
15920
|
this.#thinkingLevel = previousThinkingLevel;
|
|
14855
15921
|
this.agent.setThinkingLevel(toReasoningEffort(previousThinkingLevel));
|
|
14856
15922
|
this.agent.serviceTier = previousServiceTier;
|