@gajae-code/coding-agent 0.15.6 → 0.16.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 +99 -0
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +14 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +27 -8
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +79 -2
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +21 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +9 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +12 -5
- package/dist/types/sdk/cli/session-cli.d.ts +20 -3
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +70 -17
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +3 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +6 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +52 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +79 -2
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +13 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +140 -23
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +135 -28
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +178 -23
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +291 -42
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +27 -8
- package/src/config/settings.ts +51 -15
- package/src/coordinator-mcp/projection-scan.ts +27 -6
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +115 -21
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +213 -14
- package/src/gjc-runtime/session-state-sidecar.ts +938 -39
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +61 -3
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +610 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +30 -2
- package/src/modes/controllers/selector-controller.ts +200 -98
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +1646 -27
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +81 -3
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +440 -75
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +591 -0
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +43 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +84 -7
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +41 -15
- package/src/sdk/cli/session-cli.ts +297 -26
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +101 -40
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/events.ts +1 -0
- package/src/sdk/host/host.ts +29 -2
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +673 -14
- package/src/sdk/host/session-runtime.ts +674 -103
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +17 -4
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +105 -3
- package/src/sdk/protocol/operation-registry.ts +14 -4
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +219 -118
- package/src/sdk/session.ts +416 -195
- package/src/sdk/startup-capability.ts +1 -0
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1628 -318
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +850 -97
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +412 -75
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +10 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
|
@@ -201,6 +201,8 @@ import {
|
|
|
201
201
|
type AsyncJobDeliveryState,
|
|
202
202
|
AsyncJobManager,
|
|
203
203
|
asyncJobEndpointId as deriveAsyncJobEndpointId,
|
|
204
|
+
type FoldReason,
|
|
205
|
+
type JobFoldEvent,
|
|
204
206
|
type OwnerSubagentShutdownLease,
|
|
205
207
|
} from "../async";
|
|
206
208
|
import { reset as resetCapabilities } from "../capability";
|
|
@@ -296,6 +298,7 @@ import type { SessionSwitchEvent } from "../extensibility/shared-events";
|
|
|
296
298
|
import {
|
|
297
299
|
buildSkillPromptMessage,
|
|
298
300
|
getSkillSlashCommandName,
|
|
301
|
+
isNamespacedSkillSlashCommandName,
|
|
299
302
|
parseSkillInvocations,
|
|
300
303
|
type Skill,
|
|
301
304
|
type SkillWarning,
|
|
@@ -311,14 +314,23 @@ import {
|
|
|
311
314
|
import {
|
|
312
315
|
assertNonEmptyGjcSessionId,
|
|
313
316
|
modeStatePath as sessionModeStatePath,
|
|
317
|
+
sessionRuntimeDir,
|
|
314
318
|
sessionStateDir,
|
|
315
319
|
} from "../gjc-runtime/session-layout";
|
|
320
|
+
import { sessionStateLockFailureFields, shouldWarnPersistFailure } from "../gjc-runtime/session-state-lock";
|
|
316
321
|
import {
|
|
317
322
|
type CoordinatorToolObservation,
|
|
323
|
+
clearCoordinatorRuntimeStateRescope,
|
|
324
|
+
GJC_COORDINATOR_SESSION_STATE_FILE_ENV,
|
|
325
|
+
hasCoordinatorRuntimeStateRescopeJournal,
|
|
326
|
+
markCoordinatorRuntimeStateRescopePublishing,
|
|
318
327
|
ownerTerminalContextFromEnvironment,
|
|
319
328
|
persistCoordinatorRuntimeStateFromEvent,
|
|
320
329
|
persistCoordinatorWorkerIntegrationOutcome,
|
|
330
|
+
prepareCoordinatorRuntimeStateRescope,
|
|
331
|
+
recoverCoordinatorRuntimeStateRescope,
|
|
321
332
|
registerCoordinatorRuntimeStateFinalizer,
|
|
333
|
+
relocateCoordinatorRuntimeStateForRescope,
|
|
322
334
|
UNPROVEN_TOOL_LABEL,
|
|
323
335
|
} from "../gjc-runtime/session-state-sidecar";
|
|
324
336
|
import {
|
|
@@ -447,7 +459,13 @@ import {
|
|
|
447
459
|
} from "./contribution-prep";
|
|
448
460
|
import { canonicalCoordinatorToolLabel } from "./coordinator-tool-label";
|
|
449
461
|
import { pruneStaleFileMentions } from "./file-mention-pruning";
|
|
450
|
-
import {
|
|
462
|
+
import {
|
|
463
|
+
describeFoldReceipt,
|
|
464
|
+
FOLD_WAKE_MERGE_WINDOW_MS,
|
|
465
|
+
type FoldAdapter,
|
|
466
|
+
FoldCoordinator,
|
|
467
|
+
type ForegroundFoldOutcome,
|
|
468
|
+
} from "./fold-coordinator";
|
|
451
469
|
import type { MemoryGuardRestoreResult } from "./memory-guard-checkpoint-participant";
|
|
452
470
|
import {
|
|
453
471
|
type BashExecutionMessage,
|
|
@@ -1056,6 +1074,8 @@ export interface PromptOptions {
|
|
|
1056
1074
|
streamingBehavior?: "steer" | "followUp";
|
|
1057
1075
|
/** When set to "sequential", this follow-up is delivered one prompt at a time even if followUpMode is "all". */
|
|
1058
1076
|
followUpQueuePolicy?: "respect-mode" | "sequential";
|
|
1077
|
+
/** When set to "sequential", this steer is delivered on its own even if steeringMode is "all". */
|
|
1078
|
+
steerQueuePolicy?: "respect-mode" | "sequential";
|
|
1059
1079
|
/** Optional tool choice override for the next LLM call. */
|
|
1060
1080
|
toolChoice?: ToolChoice;
|
|
1061
1081
|
/** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */
|
|
@@ -1089,6 +1109,7 @@ type InternalCustomMessageOptions = Pick<
|
|
|
1089
1109
|
| "streamingBehavior"
|
|
1090
1110
|
| "toolChoice"
|
|
1091
1111
|
| "followUpQueuePolicy"
|
|
1112
|
+
| "steerQueuePolicy"
|
|
1092
1113
|
| "onPreflightAccepted"
|
|
1093
1114
|
| "onPreflightAcceptCommit"
|
|
1094
1115
|
| "preflightSignal"
|
|
@@ -1556,6 +1577,21 @@ interface EphemeralTurnResult {
|
|
|
1556
1577
|
*/
|
|
1557
1578
|
const SIGNAL_TEARDOWN_TIMEOUT_MS = 5_000;
|
|
1558
1579
|
|
|
1580
|
+
export class SessionDisposalIncompleteError extends Error {
|
|
1581
|
+
constructor(label?: string) {
|
|
1582
|
+
super(
|
|
1583
|
+
label
|
|
1584
|
+
? `Session disposal exceeded its bounded caller deadline while waiting for ${label}.`
|
|
1585
|
+
: "Session disposal exceeded its bounded caller deadline.",
|
|
1586
|
+
);
|
|
1587
|
+
this.name = "SessionDisposalIncompleteError";
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
export function isSessionDisposalIncompleteError(error: unknown): error is SessionDisposalIncompleteError {
|
|
1592
|
+
return error instanceof Error && error.name === "SessionDisposalIncompleteError";
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1559
1595
|
const AGENT_BASH_ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256;
|
|
1560
1596
|
|
|
1561
1597
|
function boundAgentBashArtifactSaveDiagnostic(error: unknown): string {
|
|
@@ -2006,6 +2042,7 @@ function sanitizePostPublicationError(error: string | undefined): string | undef
|
|
|
2006
2042
|
|
|
2007
2043
|
export class WorkerIntegrationRequestScheduler {
|
|
2008
2044
|
#inFlight: Promise<WorkerIntegrationOutcome> | undefined = undefined;
|
|
2045
|
+
#ownedRequests = new Set<Promise<void>>();
|
|
2009
2046
|
#pending = false;
|
|
2010
2047
|
|
|
2011
2048
|
constructor(
|
|
@@ -2033,6 +2070,12 @@ export class WorkerIntegrationRequestScheduler {
|
|
|
2033
2070
|
return outcome;
|
|
2034
2071
|
}
|
|
2035
2072
|
|
|
2073
|
+
async joinOwnedRequests(): Promise<void> {
|
|
2074
|
+
while (this.#ownedRequests.size > 0) {
|
|
2075
|
+
await Promise.allSettled([...this.#ownedRequests]);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2036
2079
|
#start(): void {
|
|
2037
2080
|
this.#pending = false;
|
|
2038
2081
|
const controller = new AbortController();
|
|
@@ -2042,6 +2085,8 @@ export class WorkerIntegrationRequestScheduler {
|
|
|
2042
2085
|
} catch {
|
|
2043
2086
|
request = Promise.reject(new Error("Worker integration request failed before dispatch."));
|
|
2044
2087
|
}
|
|
2088
|
+
this.#ownedRequests.add(request);
|
|
2089
|
+
void request.finally(() => this.#ownedRequests.delete(request)).catch(() => {});
|
|
2045
2090
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
2046
2091
|
const deadline = new Promise<WorkerIntegrationOutcome>(resolve => {
|
|
2047
2092
|
timeout = setTimeout(() => {
|
|
@@ -2277,6 +2322,7 @@ type SessionAdmissionEntry = {
|
|
|
2277
2322
|
settled: PromiseWithResolvers<void>;
|
|
2278
2323
|
released: boolean;
|
|
2279
2324
|
selectionFenceGeneration: number;
|
|
2325
|
+
selectionTransaction?: symbol;
|
|
2280
2326
|
continuationCapability?: symbol;
|
|
2281
2327
|
};
|
|
2282
2328
|
|
|
@@ -2346,6 +2392,12 @@ interface CanonicalMessageAdmission {
|
|
|
2346
2392
|
release: () => void;
|
|
2347
2393
|
}
|
|
2348
2394
|
|
|
2395
|
+
type CoordinatorRuntimeStatePersistContext = {
|
|
2396
|
+
sessionId: string;
|
|
2397
|
+
cwd: string;
|
|
2398
|
+
sessionFile: string | undefined;
|
|
2399
|
+
stateFile: string | null;
|
|
2400
|
+
};
|
|
2349
2401
|
export class AgentSession {
|
|
2350
2402
|
#provisionalStreamingToolCallIds = new Set<string>();
|
|
2351
2403
|
readonly agent: Agent;
|
|
@@ -2386,12 +2438,15 @@ export class AgentSession {
|
|
|
2386
2438
|
#preProfileModel: Model | undefined;
|
|
2387
2439
|
#sessionAdmissionQueue: SessionAdmissionEntry[] = [];
|
|
2388
2440
|
#activeSessionAdmission: SessionAdmissionEntry | undefined;
|
|
2441
|
+
#startupPromptWaiters = new Set<Promise<void>>();
|
|
2389
2442
|
#sessionAdmissionClosing = false;
|
|
2390
2443
|
#sessionAdmissionClosed = false;
|
|
2391
2444
|
#sessionAdmissionContext = new AsyncLocalStorage<SessionAdmissionEntry>();
|
|
2392
2445
|
#selectionFenceGenerationContext = new AsyncLocalStorage<number>();
|
|
2393
2446
|
#selectionFenceTail: Promise<void> = Promise.resolve();
|
|
2394
2447
|
#pendingSelectionFences = 0;
|
|
2448
|
+
#selectionAwaitingMutationTransaction: symbol | undefined;
|
|
2449
|
+
#authorizedClosedSelectionTransaction: symbol | undefined;
|
|
2395
2450
|
#selectionFenceDeferredContinuations = new Map<number, number>();
|
|
2396
2451
|
#scopedSettlementWaiters = new Set<() => void>();
|
|
2397
2452
|
#oldestPendingSelectionFenceGeneration = 0;
|
|
@@ -2491,6 +2546,10 @@ export class AgentSession {
|
|
|
2491
2546
|
// continuation of the aborted attempt that the contract requires to be
|
|
2492
2547
|
// blocked (review thread P1).
|
|
2493
2548
|
readonly #externalSteerMessages = new WeakSet<AgentMessage>();
|
|
2549
|
+
/** Steers explicitly requested as one-per-poll; preserve this policy if a run disowns them into follow-ups. */
|
|
2550
|
+
readonly #sequentialSteerMessages = new WeakSet<AgentMessage>();
|
|
2551
|
+
/** Non-admitted steers collected while one abort unwind owns delivery. */
|
|
2552
|
+
#abortUnwindSteerFallbacks: AgentMessage[] = [];
|
|
2494
2553
|
/** Per-message SDK requester-ownership correlation for queued client steers:
|
|
2495
2554
|
* fired exactly once when the steer's run accepts it — via the idle
|
|
2496
2555
|
* auto-continue or the terminal-abort rearm (review thread P1). */
|
|
@@ -2536,6 +2595,157 @@ export class AgentSession {
|
|
|
2536
2595
|
this.#deepInterviewGenuineUserMessageEpochs.delete(message);
|
|
2537
2596
|
}
|
|
2538
2597
|
}
|
|
2598
|
+
|
|
2599
|
+
/**
|
|
2600
|
+
* Queued messages that are independent ROOT requests: they must not run under
|
|
2601
|
+
* a terminally fenced lineage, so a fresh identity is minted when one of them
|
|
2602
|
+
* is actually delivered. Tracked per message rather than as a session latch,
|
|
2603
|
+
* so removing the message (queue edit, producer purge, queue clear) removes
|
|
2604
|
+
* its requirement too and no unrelated continuation inherits it. The mint
|
|
2605
|
+
* happens at the last synchronous point before `agent.continue*` entry —
|
|
2606
|
+
* never at enqueue, which would race the ending run's event settlement, and
|
|
2607
|
+
* never lost when a busy owner defers delivery.
|
|
2608
|
+
*/
|
|
2609
|
+
#freshRootLineageRequests = new WeakSet<AgentMessage>();
|
|
2610
|
+
|
|
2611
|
+
/**
|
|
2612
|
+
* Bind a tag-only display chip (registered by `enqueueCustomMessageDisplay`
|
|
2613
|
+
* before dispatch) to the executable message that was actually queued, and
|
|
2614
|
+
* move it to the list matching how the message was queued.
|
|
2615
|
+
*
|
|
2616
|
+
* Without the binding the chip keeps no identity link, so edit/remove/pop fall
|
|
2617
|
+
* back to POSITIONAL addressing against the Agent queue — and a hidden
|
|
2618
|
+
* `display:false` custom message occupying an earlier queue slot makes that
|
|
2619
|
+
* index point at the wrong message. Migrating the entry also keeps the chip's
|
|
2620
|
+
* mode honest when a requested steer is rerouted to a follow-up.
|
|
2621
|
+
*/
|
|
2622
|
+
#bindCustomDisplayEntry(message: AgentMessage, mode: "steer" | "followUp"): void {
|
|
2623
|
+
const tag = message.role === "custom" ? readPendingDisplayTag(message.details) : undefined;
|
|
2624
|
+
if (tag === undefined) return;
|
|
2625
|
+
const from = mode === "steer" ? this.#followUpMessages : this.#steeringMessages;
|
|
2626
|
+
const to = mode === "steer" ? this.#steeringMessages : this.#followUpMessages;
|
|
2627
|
+
const already = to.find(entry => entry.tag === tag);
|
|
2628
|
+
if (already) {
|
|
2629
|
+
already.message = message;
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
const index = from.findIndex(entry => entry.tag === tag);
|
|
2633
|
+
if (index === -1) return;
|
|
2634
|
+
const [entry] = from.splice(index, 1);
|
|
2635
|
+
if (!entry) return;
|
|
2636
|
+
entry.message = message;
|
|
2637
|
+
to.push(entry);
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
/** True when a queued message still carries an unfulfilled fresh-root requirement. */
|
|
2641
|
+
#hasQueuedFreshRootRequest(): boolean {
|
|
2642
|
+
const { steering, followUp } = this.agent.snapshotQueues();
|
|
2643
|
+
return [...steering, ...followUp].some(message => this.#freshRootLineageRequests.has(message));
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
/**
|
|
2647
|
+
* Disposition of steering the current run leaves unconsumed when it exits.
|
|
2648
|
+
* Set by the abort paths before the run ends and consumed exactly once by
|
|
2649
|
+
* `#settleDisownedSteering` on the run's `agent_end`. Absent means the run
|
|
2650
|
+
* ended on its own (completed/error/pause): steering it never polled is
|
|
2651
|
+
* re-routed as sequential follow-ups owned by the next turn.
|
|
2652
|
+
*/
|
|
2653
|
+
#disownedSteeringDisposition:
|
|
2654
|
+
| { kind: "rearm" }
|
|
2655
|
+
| { kind: "terminal"; preserve: (message: AgentMessage) => boolean; snapshotSeq: number }
|
|
2656
|
+
| { kind: "drop" }
|
|
2657
|
+
| { kind: "held" }
|
|
2658
|
+
| undefined;
|
|
2659
|
+
|
|
2660
|
+
/**
|
|
2661
|
+
* The single owner of steering that a run admitted but never consumed. The
|
|
2662
|
+
* Agent already cleared its queue; this decides once, per message, whether
|
|
2663
|
+
* it is delivered as a follow-up of a fresh turn (user interrupt, natural
|
|
2664
|
+
* end, or a post-snapshot external steer surviving a terminal abort) or
|
|
2665
|
+
* dropped with its removal disposition (terminal abort, teardown), and keeps
|
|
2666
|
+
* the display list aligned with that decision.
|
|
2667
|
+
*/
|
|
2668
|
+
#settleDisownedSteering(event: Extract<AgentEvent, { type: "agent_end" }>): void {
|
|
2669
|
+
const disowned = event.disownedSteering ?? [];
|
|
2670
|
+
const disposition = this.#disownedSteeringDisposition;
|
|
2671
|
+
this.#disownedSteeringDisposition = undefined;
|
|
2672
|
+
// A cancel-and-submit snapshotted the queues before aborting and re-queues
|
|
2673
|
+
// them itself once the abort settles; the display list stays as-is.
|
|
2674
|
+
if (disposition?.kind === "held") return;
|
|
2675
|
+
const dropped: AgentMessage[] = [];
|
|
2676
|
+
const rearmed: AgentMessage[] = [];
|
|
2677
|
+
for (const message of disowned) {
|
|
2678
|
+
const keep =
|
|
2679
|
+
disposition === undefined || disposition.kind === "rearm"
|
|
2680
|
+
? true
|
|
2681
|
+
: disposition.kind === "terminal"
|
|
2682
|
+
? disposition.preserve(message)
|
|
2683
|
+
: false;
|
|
2684
|
+
(keep ? rearmed : dropped).push(message);
|
|
2685
|
+
}
|
|
2686
|
+
const droppedSet = new Set(dropped);
|
|
2687
|
+
const rearmedSet = new Set(rearmed);
|
|
2688
|
+
const rearmedDisplays: QueuedDisplayEntry[] = [];
|
|
2689
|
+
this.#steeringMessages = this.#steeringMessages.filter(entry => {
|
|
2690
|
+
if (entry.message !== undefined && droppedSet.has(entry.message)) return false;
|
|
2691
|
+
if (entry.message !== undefined && rearmedSet.has(entry.message)) {
|
|
2692
|
+
rearmedDisplays.push(entry);
|
|
2693
|
+
return false;
|
|
2694
|
+
}
|
|
2695
|
+
// Tag-only display entries (custom skill chips) belong to whichever
|
|
2696
|
+
// custom message carries their tag.
|
|
2697
|
+
if (entry.tag !== undefined) {
|
|
2698
|
+
const owner = disowned.find(
|
|
2699
|
+
message => message.role === "custom" && readPendingDisplayTag(message.details) === entry.tag,
|
|
2700
|
+
);
|
|
2701
|
+
if (owner && droppedSet.has(owner)) return false;
|
|
2702
|
+
if (owner && rearmedSet.has(owner)) {
|
|
2703
|
+
rearmedDisplays.push(entry);
|
|
2704
|
+
return false;
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
return true;
|
|
2708
|
+
});
|
|
2709
|
+
// Dropped steering leaves without consumption: terminalize its accepted
|
|
2710
|
+
// SDK submissions boundedly (#4668 review P1).
|
|
2711
|
+
this.#fireQueuedRemovalHooks(dropped);
|
|
2712
|
+
if (rearmed.length === 0) return;
|
|
2713
|
+
// Re-route as sequential follow-ups of the NEXT turn, ahead of any
|
|
2714
|
+
// follow-up queued after them, preserving submission order. Marking
|
|
2715
|
+
// each as sequential first, then restoring ahead of the existing queue,
|
|
2716
|
+
// keeps the existing follow-ups' identity and per-message marks intact.
|
|
2717
|
+
let batch: AgentMessage[] = [];
|
|
2718
|
+
const flushBatch = () => {
|
|
2719
|
+
if (batch.length > 1) this.agent.markFollowUpBatch(batch);
|
|
2720
|
+
batch = [];
|
|
2721
|
+
};
|
|
2722
|
+
for (const message of rearmed) {
|
|
2723
|
+
if (this.#sequentialSteerMessages.has(message)) {
|
|
2724
|
+
flushBatch();
|
|
2725
|
+
this.agent.markFollowUpSequential(message);
|
|
2726
|
+
} else if (this.agent.getSteeringMode() === "all") {
|
|
2727
|
+
batch.push(message);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
flushBatch();
|
|
2731
|
+
this.agent.restoreFollowUp(rearmed);
|
|
2732
|
+
this.#followUpMessages = [...rearmedDisplays, ...this.#followUpMessages];
|
|
2733
|
+
for (const message of rearmed) {
|
|
2734
|
+
// External SDK steers keep their promotion hook: it now fires at the
|
|
2735
|
+
// follow-up promotion boundary with startsOwnRun: true.
|
|
2736
|
+
const steerHook = this.#steerPromotionHooks.get(message);
|
|
2737
|
+
if (steerHook) {
|
|
2738
|
+
this.#steerPromotionHooks.delete(message);
|
|
2739
|
+
this.#followUpPromotionHooks.set(message, steerHook);
|
|
2740
|
+
}
|
|
2741
|
+
this.#externalFollowUps.add(message);
|
|
2742
|
+
}
|
|
2743
|
+
// Nothing else owns delivery of a rearm that did not come from an abort
|
|
2744
|
+
// (a natural end, or a fold whose wake turn must carry the steer): the
|
|
2745
|
+
// abort paths schedule their own continuation and this one is a no-op
|
|
2746
|
+
// once the queue is drained.
|
|
2747
|
+
this.#scheduleQueuedDelivery();
|
|
2748
|
+
}
|
|
2539
2749
|
#fireQueuedPromotionHooks(messages: readonly AgentMessage[], promotion?: { startsOwnRun?: boolean }): void {
|
|
2540
2750
|
for (const message of messages) {
|
|
2541
2751
|
const steerHook = this.#steerPromotionHooks.get(message);
|
|
@@ -2626,6 +2836,8 @@ export class AgentSession {
|
|
|
2626
2836
|
|
|
2627
2837
|
// Compaction state
|
|
2628
2838
|
#compactionAbortController: AbortController | undefined = undefined;
|
|
2839
|
+
#compactionCompletion: Promise<void> | undefined;
|
|
2840
|
+
#autoCompactionCompletions = new Set<Promise<AutoCompactionTerminalStatus>>();
|
|
2629
2841
|
#autoCompactionAbortController: AbortController | undefined = undefined;
|
|
2630
2842
|
|
|
2631
2843
|
/** Invocation-scoped EventStream drain barriers owned by active maintenance calls. */
|
|
@@ -2814,6 +3026,27 @@ export class AgentSession {
|
|
|
2814
3026
|
/** Idempotent unregister handle for this session's resource-GC registration. */
|
|
2815
3027
|
#unregisterResourceGc?: () => void;
|
|
2816
3028
|
#unregisterRuntimeStateFinalizer?: () => void;
|
|
3029
|
+
#unregisterBeforeMoveListener?: () => void;
|
|
3030
|
+
#unregisterMoveAbortListener?: () => void;
|
|
3031
|
+
#unregisterMovePublicationListener?: () => void;
|
|
3032
|
+
#unregisterAfterMoveListener?: () => void;
|
|
3033
|
+
/**
|
|
3034
|
+
* (Re)register the postmortem finalizer bound to the CURRENT cwd/session file. Called at
|
|
3035
|
+
* construction and after every committed rescope, so an unexpected exit writes terminal
|
|
3036
|
+
* state to the session's live location rather than the abandoned launch root.
|
|
3037
|
+
*/
|
|
3038
|
+
#registerRuntimeStateFinalizer(): void {
|
|
3039
|
+
this.#unregisterRuntimeStateFinalizer?.();
|
|
3040
|
+
const currentContext = () => ({
|
|
3041
|
+
sessionId: this.sessionId,
|
|
3042
|
+
cwd: this.sessionManager.getCwd(),
|
|
3043
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
3044
|
+
});
|
|
3045
|
+
this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer(
|
|
3046
|
+
currentContext(),
|
|
3047
|
+
currentContext,
|
|
3048
|
+
);
|
|
3049
|
+
}
|
|
2817
3050
|
#unregisterSessionMemorySettings?: () => void;
|
|
2818
3051
|
/**
|
|
2819
3052
|
* AsyncJobManager owned by this session (top-level only). Subagents leave
|
|
@@ -2823,6 +3056,7 @@ export class AgentSession {
|
|
|
2823
3056
|
readonly #disposeAsyncJobManager: boolean;
|
|
2824
3057
|
#ownedMcpManager: MCPManager | undefined;
|
|
2825
3058
|
#startupTurnBarrier: Promise<void> | undefined;
|
|
3059
|
+
#startupTurnBarrierPending = false;
|
|
2826
3060
|
#pendingPythonMessages: Array<{
|
|
2827
3061
|
message: PythonExecutionMessage;
|
|
2828
3062
|
onPersisted?: () => void;
|
|
@@ -2847,7 +3081,18 @@ export class AgentSession {
|
|
|
2847
3081
|
#providerCacheSessionId: string | undefined;
|
|
2848
3082
|
readonly #asyncJobProviderSessionId: string | undefined;
|
|
2849
3083
|
#isDisposed = false;
|
|
2850
|
-
#
|
|
3084
|
+
#disposeRunPromise: Promise<void> | undefined;
|
|
3085
|
+
#disposeCallerPromise: Promise<void> | undefined;
|
|
3086
|
+
#disposeCompleted = false;
|
|
3087
|
+
#disposeTerminalError: unknown;
|
|
3088
|
+
readonly #disposeAbortController = new AbortController();
|
|
3089
|
+
#disposeAdmissionClosed: Promise<void> | undefined;
|
|
3090
|
+
#disposePostPromptDrain: Promise<void> | undefined;
|
|
3091
|
+
#disposeDeadline = 0;
|
|
3092
|
+
#disposeDeadlineTimer: NodeJS.Timeout | undefined;
|
|
3093
|
+
#disposeDeadlineExpired: PromiseWithResolvers<void> | undefined;
|
|
3094
|
+
#disposeTimeoutMs = SIGNAL_TEARDOWN_TIMEOUT_MS;
|
|
3095
|
+
#disposeActiveStepLabel: string | undefined;
|
|
2851
3096
|
readonly #toolSessionCleanups = new Set<() => Promise<void> | void>();
|
|
2852
3097
|
readonly #toolSessionTransitionCleanups = new Set<() => Promise<void> | void>();
|
|
2853
3098
|
readonly #deferredOwnerShutdownFinalizations = new Set<Promise<void>>();
|
|
@@ -3019,6 +3264,7 @@ export class AgentSession {
|
|
|
3019
3264
|
#cancelAndSubmitAbortOutcomeProviderForTests: (() => Promise<AbortOutcome>) | undefined = undefined;
|
|
3020
3265
|
#postPromptTasks = new Set<Promise<void>>();
|
|
3021
3266
|
#postPromptTaskSelectionFenceGenerations = new Map<Promise<void>, number>();
|
|
3267
|
+
#postPromptTaskRecoveryExcluded = new Set<Promise<void>>();
|
|
3022
3268
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
3023
3269
|
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
3024
3270
|
#postPromptTasksAbortController = new AbortController();
|
|
@@ -3206,6 +3452,65 @@ export class AgentSession {
|
|
|
3206
3452
|
*/
|
|
3207
3453
|
#sessionTransitionKind: string | undefined;
|
|
3208
3454
|
#coordinatorPersistGeneration = 0;
|
|
3455
|
+
#coordinatorRescopeBarrier: Promise<void> | undefined;
|
|
3456
|
+
#releaseCoordinatorRescopeBarrier: (() => void) | undefined;
|
|
3457
|
+
#coordinatorRescopeMoveId: string | undefined;
|
|
3458
|
+
#coordinatorEventHandlers = new Set<Promise<void>>();
|
|
3459
|
+
#coordinatorUnbarrieredPersists = new Set<Promise<void>>();
|
|
3460
|
+
#coordinatorReleasedBarrierPersists = new Set<Promise<void>>();
|
|
3461
|
+
|
|
3462
|
+
#beginCoordinatorRescopeBarrier(): {
|
|
3463
|
+
eventHandlers: Promise<void>[];
|
|
3464
|
+
releasedBarrierPersists: Promise<void>[];
|
|
3465
|
+
} {
|
|
3466
|
+
if (this.#coordinatorRescopeBarrier) throw new Error("Coordinator rescope barrier is already active.");
|
|
3467
|
+
const releasedBarrierPersists = [...this.#coordinatorReleasedBarrierPersists];
|
|
3468
|
+
const barrier = Promise.withResolvers<void>();
|
|
3469
|
+
this.#coordinatorRescopeBarrier = barrier.promise;
|
|
3470
|
+
this.#releaseCoordinatorRescopeBarrier = barrier.resolve;
|
|
3471
|
+
this.#bindPendingAgentEndToRescopeBarrier();
|
|
3472
|
+
return { eventHandlers: [...this.#coordinatorEventHandlers], releasedBarrierPersists };
|
|
3473
|
+
}
|
|
3474
|
+
|
|
3475
|
+
#trackReleasedBarrierPersist(persist: Promise<void>): void {
|
|
3476
|
+
this.#coordinatorReleasedBarrierPersists.add(persist);
|
|
3477
|
+
void persist.then(
|
|
3478
|
+
() => this.#coordinatorReleasedBarrierPersists.delete(persist),
|
|
3479
|
+
() => this.#coordinatorReleasedBarrierPersists.delete(persist),
|
|
3480
|
+
);
|
|
3481
|
+
}
|
|
3482
|
+
|
|
3483
|
+
#bindPendingAgentEndToRescopeBarrier(): void {
|
|
3484
|
+
if (!this.#coordinatorRescopeBarrier || !this.#pendingAgentEndEmit) return;
|
|
3485
|
+
const admission = this.#agentEventAdmission.get(this.#pendingAgentEndEmit);
|
|
3486
|
+
if (admission) admission.persistBarrier = this.#coordinatorRescopeBarrier;
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
#appendCoordinatorPersist(run: () => Promise<void>): Promise<void> {
|
|
3490
|
+
const queued = this.#coordinatorPersistQueue.then(run, run);
|
|
3491
|
+
this.#coordinatorPersistQueue = queued.catch(() => {});
|
|
3492
|
+
return queued;
|
|
3493
|
+
}
|
|
3494
|
+
|
|
3495
|
+
#trackUnbarrieredCoordinatorPersist(persist: Promise<void>): void {
|
|
3496
|
+
this.#coordinatorUnbarrieredPersists.add(persist);
|
|
3497
|
+
void persist.then(
|
|
3498
|
+
() => this.#coordinatorUnbarrieredPersists.delete(persist),
|
|
3499
|
+
() => this.#coordinatorUnbarrieredPersists.delete(persist),
|
|
3500
|
+
);
|
|
3501
|
+
}
|
|
3502
|
+
|
|
3503
|
+
async #drainUnbarrieredCoordinatorPersists(): Promise<void> {
|
|
3504
|
+
while (this.#coordinatorUnbarrieredPersists.size > 0) {
|
|
3505
|
+
await Promise.allSettled([...this.#coordinatorUnbarrieredPersists]);
|
|
3506
|
+
}
|
|
3507
|
+
}
|
|
3508
|
+
|
|
3509
|
+
#endCoordinatorRescopeBarrier(): void {
|
|
3510
|
+
this.#releaseCoordinatorRescopeBarrier?.();
|
|
3511
|
+
this.#releaseCoordinatorRescopeBarrier = undefined;
|
|
3512
|
+
this.#coordinatorRescopeBarrier = undefined;
|
|
3513
|
+
}
|
|
3209
3514
|
|
|
3210
3515
|
#beginSessionTransition(kind: string): void {
|
|
3211
3516
|
if (this.#sessionTransitionKind !== undefined) {
|
|
@@ -3223,8 +3528,17 @@ export class AgentSession {
|
|
|
3223
3528
|
}
|
|
3224
3529
|
|
|
3225
3530
|
#activateNextSessionAdmission(): void {
|
|
3226
|
-
if (this.#activeSessionAdmission
|
|
3227
|
-
|
|
3531
|
+
if (this.#activeSessionAdmission) return;
|
|
3532
|
+
let next: SessionAdmissionEntry | undefined;
|
|
3533
|
+
if (this.#sessionAdmissionClosed) {
|
|
3534
|
+
const index = this.#sessionAdmissionQueue.findIndex(
|
|
3535
|
+
entry => entry.selectionTransaction === this.#authorizedClosedSelectionTransaction,
|
|
3536
|
+
);
|
|
3537
|
+
if (index < 0) return;
|
|
3538
|
+
[next] = this.#sessionAdmissionQueue.splice(index, 1);
|
|
3539
|
+
} else {
|
|
3540
|
+
next = this.#sessionAdmissionQueue.shift();
|
|
3541
|
+
}
|
|
3228
3542
|
if (!next) return;
|
|
3229
3543
|
this.#activeSessionAdmission = next;
|
|
3230
3544
|
next.ready.resolve();
|
|
@@ -3242,16 +3556,61 @@ export class AgentSession {
|
|
|
3242
3556
|
return { entry, capability: entry.continuationCapability };
|
|
3243
3557
|
}
|
|
3244
3558
|
|
|
3245
|
-
|
|
3246
|
-
|
|
3559
|
+
#setStartupTurnBarrier(barrier: Promise<void> | undefined): void {
|
|
3560
|
+
this.#startupTurnBarrier = barrier;
|
|
3561
|
+
this.#startupTurnBarrierPending = barrier !== undefined;
|
|
3247
3562
|
if (!barrier) return;
|
|
3248
|
-
|
|
3249
|
-
|
|
3563
|
+
void barrier.then(
|
|
3564
|
+
() => {
|
|
3565
|
+
if (this.#startupTurnBarrier === barrier) this.#startupTurnBarrierPending = false;
|
|
3566
|
+
},
|
|
3567
|
+
() => {
|
|
3568
|
+
if (this.#startupTurnBarrier === barrier) this.#startupTurnBarrierPending = false;
|
|
3569
|
+
},
|
|
3570
|
+
);
|
|
3571
|
+
}
|
|
3572
|
+
async #awaitStartupTurnBarrier(signal?: AbortSignal): Promise<void> {
|
|
3573
|
+
while (true) {
|
|
3574
|
+
const barrier = this.#startupTurnBarrier;
|
|
3575
|
+
if (!barrier) return;
|
|
3576
|
+
await awaitPromptInvocationPreflight(barrier, signal);
|
|
3577
|
+
// Cross one task boundary before clearing readiness. Promise reaction
|
|
3578
|
+
// chains fully drain before timers, so every extension published by the
|
|
3579
|
+
// captured barrier's settlement becomes visible without an arbitrary
|
|
3580
|
+
// microtask-depth cap.
|
|
3581
|
+
const publicationTurn = Promise.withResolvers<void>();
|
|
3582
|
+
const timer = setTimeout(publicationTurn.resolve, 0);
|
|
3583
|
+
try {
|
|
3584
|
+
await awaitPromptInvocationPreflight(publicationTurn.promise, signal);
|
|
3585
|
+
} finally {
|
|
3586
|
+
clearTimeout(timer);
|
|
3587
|
+
}
|
|
3588
|
+
if (this.#startupTurnBarrier !== barrier) continue;
|
|
3589
|
+
this.#setStartupTurnBarrier(undefined);
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
}
|
|
3593
|
+
async #awaitStartupPromptWaiters(signal?: AbortSignal): Promise<void> {
|
|
3594
|
+
while (this.#startupPromptWaiters.size > 0) {
|
|
3595
|
+
await awaitPromptInvocationPreflight(Promise.all([...this.#startupPromptWaiters]), signal);
|
|
3596
|
+
await Promise.resolve();
|
|
3597
|
+
}
|
|
3598
|
+
}
|
|
3599
|
+
#reserveStartupPromptWaiter(): () => void {
|
|
3600
|
+
const waiter = Promise.withResolvers<void>();
|
|
3601
|
+
this.#startupPromptWaiters.add(waiter.promise);
|
|
3602
|
+
let released = false;
|
|
3603
|
+
return () => {
|
|
3604
|
+
if (released) return;
|
|
3605
|
+
released = true;
|
|
3606
|
+
this.#startupPromptWaiters.delete(waiter.promise);
|
|
3607
|
+
waiter.resolve();
|
|
3608
|
+
};
|
|
3250
3609
|
}
|
|
3251
3610
|
extendStartupTurnBarrier(barrier: Promise<void>): void {
|
|
3252
3611
|
const current = this.#startupTurnBarrier;
|
|
3253
|
-
this.#
|
|
3254
|
-
void this.#startupTurnBarrier
|
|
3612
|
+
this.#setStartupTurnBarrier(current ? Promise.all([current, barrier]).then(() => {}) : barrier);
|
|
3613
|
+
void this.#startupTurnBarrier?.catch(() => {});
|
|
3255
3614
|
}
|
|
3256
3615
|
|
|
3257
3616
|
async #withSessionAdmission<T>(
|
|
@@ -3261,8 +3620,13 @@ export class AgentSession {
|
|
|
3261
3620
|
continuationAdmission?: ScheduledContinuationAdmission,
|
|
3262
3621
|
options?: {
|
|
3263
3622
|
allowDuringClosing?: boolean;
|
|
3623
|
+
closedSelectionTransaction?: symbol;
|
|
3624
|
+
selectionTransaction?: symbol;
|
|
3625
|
+
onAfterReadyForTests?: () => Promise<void>;
|
|
3264
3626
|
bypassSelectionFenceGeneration?: number;
|
|
3265
3627
|
allowPromptContinuationReentry?: boolean;
|
|
3628
|
+
idleDelivery?: boolean;
|
|
3629
|
+
startupPromptWaiterRelease?: () => void;
|
|
3266
3630
|
},
|
|
3267
3631
|
): Promise<T> {
|
|
3268
3632
|
const owner = this.#sessionAdmissionContext.getStore();
|
|
@@ -3277,7 +3641,21 @@ export class AgentSession {
|
|
|
3277
3641
|
}
|
|
3278
3642
|
throw this.#sessionAdmissionBusyError();
|
|
3279
3643
|
}
|
|
3280
|
-
|
|
3644
|
+
const releaseStartupPromptWaiter =
|
|
3645
|
+
kind === "prompt" && options?.idleDelivery !== true
|
|
3646
|
+
? (options?.startupPromptWaiterRelease ?? this.#reserveStartupPromptWaiter())
|
|
3647
|
+
: undefined;
|
|
3648
|
+
if (kind === "prompt") {
|
|
3649
|
+
const startupSignal = signal
|
|
3650
|
+
? AbortSignal.any([signal, this.#disposeAbortController.signal])
|
|
3651
|
+
: this.#disposeAbortController.signal;
|
|
3652
|
+
try {
|
|
3653
|
+
await awaitPromptInvocationPreflight(this.#awaitStartupTurnBarrier(startupSignal), startupSignal);
|
|
3654
|
+
} finally {
|
|
3655
|
+
releaseStartupPromptWaiter?.();
|
|
3656
|
+
}
|
|
3657
|
+
if (options?.idleDelivery === true) await this.#awaitStartupPromptWaiters(startupSignal);
|
|
3658
|
+
}
|
|
3281
3659
|
const bypassesSelectionFence =
|
|
3282
3660
|
options?.bypassSelectionFenceGeneration !== undefined &&
|
|
3283
3661
|
options.bypassSelectionFenceGeneration < this.#selectionFenceGeneration;
|
|
@@ -3289,8 +3667,11 @@ export class AgentSession {
|
|
|
3289
3667
|
) {
|
|
3290
3668
|
await awaitPromptInvocationPreflight(this.#selectionFenceTail, signal);
|
|
3291
3669
|
}
|
|
3670
|
+
const closedSelectionAuthorized = (): boolean =>
|
|
3671
|
+
options?.closedSelectionTransaction !== undefined &&
|
|
3672
|
+
options.closedSelectionTransaction === this.#authorizedClosedSelectionTransaction;
|
|
3292
3673
|
if (
|
|
3293
|
-
this.#sessionAdmissionClosed ||
|
|
3674
|
+
(this.#sessionAdmissionClosed && !closedSelectionAuthorized()) ||
|
|
3294
3675
|
((this.#sessionAdmissionClosing || this.#isDisposed) && options?.allowDuringClosing !== true)
|
|
3295
3676
|
)
|
|
3296
3677
|
throw this.#sessionAdmissionBusyError();
|
|
@@ -3311,6 +3692,7 @@ export class AgentSession {
|
|
|
3311
3692
|
settled: Promise.withResolvers<void>(),
|
|
3312
3693
|
released: false,
|
|
3313
3694
|
selectionFenceGeneration: this.#selectionFenceGeneration,
|
|
3695
|
+
...(options?.selectionTransaction ? { selectionTransaction: options.selectionTransaction } : {}),
|
|
3314
3696
|
...(kind === "prompt" ? { continuationCapability: Symbol("scheduled-continuation") } : {}),
|
|
3315
3697
|
};
|
|
3316
3698
|
const releaseEntry = () => {
|
|
@@ -3330,39 +3712,33 @@ export class AgentSession {
|
|
|
3330
3712
|
releaseEntry();
|
|
3331
3713
|
throw error;
|
|
3332
3714
|
}
|
|
3333
|
-
if (
|
|
3334
|
-
this.#sessionAdmissionClosed ||
|
|
3335
|
-
((this.#sessionAdmissionClosing || this.#isDisposed) && options?.allowDuringClosing !== true)
|
|
3336
|
-
) {
|
|
3337
|
-
entry.released = true;
|
|
3338
|
-
entry.settled.resolve();
|
|
3339
|
-
if (this.#activeSessionAdmission === entry) this.#activeSessionAdmission = undefined;
|
|
3340
|
-
this.#activateNextSessionAdmission();
|
|
3341
|
-
throw this.#sessionAdmissionBusyError();
|
|
3342
|
-
}
|
|
3343
|
-
// Re-check the handoff fence after activation: a prompt queued before the
|
|
3344
|
-
// transition began must not start once the fence is up.
|
|
3345
|
-
if (kind === "prompt" && this.#handoffTransitionActive) {
|
|
3346
|
-
entry.released = true;
|
|
3347
|
-
entry.settled.resolve();
|
|
3348
|
-
if (this.#activeSessionAdmission === entry) this.#activeSessionAdmission = undefined;
|
|
3349
|
-
this.#activateNextSessionAdmission();
|
|
3350
|
-
throw Object.assign(new AgentBusyError("Cannot start a turn while a handoff is in progress."), {
|
|
3351
|
-
code: "busy",
|
|
3352
|
-
});
|
|
3353
|
-
}
|
|
3354
|
-
|
|
3355
|
-
const release = () => {
|
|
3356
|
-
releaseEntry();
|
|
3357
|
-
};
|
|
3358
3715
|
try {
|
|
3716
|
+
await options?.onAfterReadyForTests?.();
|
|
3717
|
+
if (
|
|
3718
|
+
entry.released ||
|
|
3719
|
+
(this.#sessionAdmissionClosed && !closedSelectionAuthorized()) ||
|
|
3720
|
+
((this.#sessionAdmissionClosing || this.#isDisposed) && options?.allowDuringClosing !== true)
|
|
3721
|
+
) {
|
|
3722
|
+
throw this.#sessionAdmissionBusyError();
|
|
3723
|
+
}
|
|
3724
|
+
// Re-check the handoff fence after activation: a prompt queued before the
|
|
3725
|
+
// transition began must not start once the fence is up.
|
|
3726
|
+
if (kind === "prompt" && this.#handoffTransitionActive) {
|
|
3727
|
+
throw Object.assign(new AgentBusyError("Cannot start a turn while a handoff is in progress."), {
|
|
3728
|
+
code: "busy",
|
|
3729
|
+
});
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
const release = () => {
|
|
3733
|
+
releaseEntry();
|
|
3734
|
+
};
|
|
3359
3735
|
return await this.#sessionAdmissionContext.run(entry, () => body({ release }));
|
|
3360
3736
|
} finally {
|
|
3361
|
-
|
|
3737
|
+
releaseEntry();
|
|
3362
3738
|
}
|
|
3363
3739
|
}
|
|
3364
3740
|
|
|
3365
|
-
async #closeSessionAdmission(): Promise<void> {
|
|
3741
|
+
async #closeSessionAdmission(options?: { waitForActive?: boolean }): Promise<void> {
|
|
3366
3742
|
this.#sessionAdmissionClosing = true;
|
|
3367
3743
|
const active = this.#activeSessionAdmission;
|
|
3368
3744
|
if (active?.kind === "prompt") {
|
|
@@ -3370,6 +3746,29 @@ export class AgentSession {
|
|
|
3370
3746
|
this.#promptPreflightCancellationGeneration++;
|
|
3371
3747
|
this.#promptPreflightAbortController.abort();
|
|
3372
3748
|
}
|
|
3749
|
+
if (options?.waitForActive === false) {
|
|
3750
|
+
this.#authorizedClosedSelectionTransaction =
|
|
3751
|
+
active?.kind === "prompt"
|
|
3752
|
+
? undefined
|
|
3753
|
+
: (active?.selectionTransaction ?? this.#selectionAwaitingMutationTransaction);
|
|
3754
|
+
this.#sessionAdmissionClosed = true;
|
|
3755
|
+
const queued = this.#sessionAdmissionQueue.splice(0);
|
|
3756
|
+
for (const entry of queued) {
|
|
3757
|
+
entry.released = true;
|
|
3758
|
+
entry.ready.resolve();
|
|
3759
|
+
entry.settled.resolve();
|
|
3760
|
+
}
|
|
3761
|
+
// Only an active prompt may outlive bounded disposal. A selection mutates
|
|
3762
|
+
// session/model state and must finish (or observe the closed fence) before
|
|
3763
|
+
// teardown closes those resources; its own finally resolves the fence tail.
|
|
3764
|
+
if (active?.kind === "selection") await active.settled.promise;
|
|
3765
|
+
// An abort-ignoring prompt may have queued selection fences behind it, so
|
|
3766
|
+
// that one case must bypass the fence. Otherwise the fence covers a
|
|
3767
|
+
// selection between its validation and mutation admissions as well as one
|
|
3768
|
+
// currently admitted.
|
|
3769
|
+
if (active?.kind !== "prompt") await this.#selectionFenceTail;
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3373
3772
|
if (active) await active.settled.promise;
|
|
3374
3773
|
await this.#selectionFenceTail;
|
|
3375
3774
|
this.#sessionAdmissionClosed = true;
|
|
@@ -3874,11 +4273,8 @@ export class AgentSession {
|
|
|
3874
4273
|
lease: RunResourceProducerLease | undefined,
|
|
3875
4274
|
workerIntegrationSettled: boolean,
|
|
3876
4275
|
): Promise<void> {
|
|
3877
|
-
const publicationContext =
|
|
3878
|
-
|
|
3879
|
-
cwd: this.sessionManager.getCwd(),
|
|
3880
|
-
sessionFile: this.sessionManager.getSessionFile(),
|
|
3881
|
-
};
|
|
4276
|
+
const publicationContext = this.#captureCoordinatorRuntimeStatePersistContext();
|
|
4277
|
+
|
|
3882
4278
|
const publicationScope = (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope as
|
|
3883
4279
|
| AttemptScope
|
|
3884
4280
|
| undefined;
|
|
@@ -3949,7 +4345,6 @@ export class AgentSession {
|
|
|
3949
4345
|
error: String(error),
|
|
3950
4346
|
},
|
|
3951
4347
|
);
|
|
3952
|
-
logger.warn("Failed to persist terminal coordinator runtime state", { error: String(error) });
|
|
3953
4348
|
},
|
|
3954
4349
|
);
|
|
3955
4350
|
extensionDelivery = this.#queueExtensionEvent(
|
|
@@ -3969,9 +4364,7 @@ export class AgentSession {
|
|
|
3969
4364
|
),
|
|
3970
4365
|
);
|
|
3971
4366
|
await extensionDelivery;
|
|
3972
|
-
void terminalPersistence
|
|
3973
|
-
logger.warn("Terminal persistence continued after SDK publication", { error }),
|
|
3974
|
-
);
|
|
4367
|
+
void terminalPersistence;
|
|
3975
4368
|
} else {
|
|
3976
4369
|
// Non-SDK/public publication has already awaited worker integration above.
|
|
3977
4370
|
}
|
|
@@ -4039,10 +4432,96 @@ export class AgentSession {
|
|
|
4039
4432
|
cwd: () => this.sessionManager.getCwd(),
|
|
4040
4433
|
});
|
|
4041
4434
|
}
|
|
4042
|
-
this.#
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4435
|
+
this.#registerRuntimeStateFinalizer();
|
|
4436
|
+
this.#unregisterBeforeMoveListener = this.sessionManager.registerBeforeMoveListener(async move => {
|
|
4437
|
+
const admittedBeforeBarrier = this.#beginCoordinatorRescopeBarrier();
|
|
4438
|
+
await Promise.allSettled([
|
|
4439
|
+
...admittedBeforeBarrier.eventHandlers,
|
|
4440
|
+
...admittedBeforeBarrier.releasedBarrierPersists,
|
|
4441
|
+
]);
|
|
4442
|
+
this.#bindPendingAgentEndToRescopeBarrier();
|
|
4443
|
+
await this.#drainUnbarrieredCoordinatorPersists();
|
|
4444
|
+
this.#coordinatorPersistGeneration += 1;
|
|
4445
|
+
const moveId = crypto.randomUUID();
|
|
4446
|
+
this.#coordinatorRescopeMoveId = moveId;
|
|
4447
|
+
await prepareCoordinatorRuntimeStateRescope({
|
|
4448
|
+
moveId,
|
|
4449
|
+
sessionId: this.sessionId,
|
|
4450
|
+
previousCwd: move.previousCwd,
|
|
4451
|
+
newCwd: move.newCwd,
|
|
4452
|
+
previousCwdIdentity: move.previousCwdIdentity,
|
|
4453
|
+
newCwdIdentity: move.newCwdIdentity,
|
|
4454
|
+
previousSessionFile: move.previousSessionFile ?? null,
|
|
4455
|
+
newSessionFile: move.newSessionFile ?? null,
|
|
4456
|
+
});
|
|
4457
|
+
});
|
|
4458
|
+
this.#unregisterMoveAbortListener = this.sessionManager.registerMoveAbortListener(async move => {
|
|
4459
|
+
const moveId = this.#coordinatorRescopeMoveId;
|
|
4460
|
+
try {
|
|
4461
|
+
if (moveId && !move.preserveRecoveryJournal) {
|
|
4462
|
+
await clearCoordinatorRuntimeStateRescope(
|
|
4463
|
+
{
|
|
4464
|
+
sessionId: this.sessionId,
|
|
4465
|
+
cwd: move.newCwd,
|
|
4466
|
+
sessionFile: move.newSessionFile ?? null,
|
|
4467
|
+
},
|
|
4468
|
+
moveId,
|
|
4469
|
+
move.previousCwd,
|
|
4470
|
+
);
|
|
4471
|
+
}
|
|
4472
|
+
} finally {
|
|
4473
|
+
this.#coordinatorRescopeMoveId = undefined;
|
|
4474
|
+
this.#endCoordinatorRescopeBarrier();
|
|
4475
|
+
}
|
|
4476
|
+
});
|
|
4477
|
+
this.#unregisterMovePublicationListener = this.sessionManager.registerMovePublicationListener(async move => {
|
|
4478
|
+
const moveId = this.#coordinatorRescopeMoveId;
|
|
4479
|
+
if (!moveId) throw new Error("Coordinator rescope publication has no prepared move identity.");
|
|
4480
|
+
await markCoordinatorRuntimeStateRescopePublishing(
|
|
4481
|
+
{
|
|
4482
|
+
sessionId: this.sessionId,
|
|
4483
|
+
cwd: move.newCwd,
|
|
4484
|
+
sessionFile: move.newSessionFile ?? null,
|
|
4485
|
+
},
|
|
4486
|
+
move.previousCwd,
|
|
4487
|
+
moveId,
|
|
4488
|
+
);
|
|
4489
|
+
});
|
|
4490
|
+
// Every committed rescope (`move_session`, `/move`, SDK/ACP `session.cwd.move`) funnels
|
|
4491
|
+
// through SessionManager.moveTo, so one after-move listener covers all surfaces: it
|
|
4492
|
+
// relocates the coordinator-shared runtime state to the new cwd (otherwise the identity
|
|
4493
|
+
// fence refuses every later persist) and rebinds the postmortem finalizer, which
|
|
4494
|
+
// otherwise keeps writing terminal state to the launch root's cwd/session file.
|
|
4495
|
+
this.#unregisterAfterMoveListener = this.sessionManager.registerAfterMoveListener(async move => {
|
|
4496
|
+
let completed = false;
|
|
4497
|
+
try {
|
|
4498
|
+
const relocated = await relocateCoordinatorRuntimeStateForRescope(
|
|
4499
|
+
{
|
|
4500
|
+
sessionId: this.sessionId,
|
|
4501
|
+
cwd: move.newCwd,
|
|
4502
|
+
sessionFile: this.sessionManager.getSessionFile() ?? null,
|
|
4503
|
+
previousSessionFile: move.previousSessionFile ?? null,
|
|
4504
|
+
},
|
|
4505
|
+
move.previousCwd,
|
|
4506
|
+
);
|
|
4507
|
+
if (!relocated) throw new Error("Coordinator runtime state rescope was refused.");
|
|
4508
|
+
await clearCoordinatorRuntimeStateRescope(
|
|
4509
|
+
{
|
|
4510
|
+
sessionId: this.sessionId,
|
|
4511
|
+
cwd: move.newCwd,
|
|
4512
|
+
sessionFile: this.sessionManager.getSessionFile() ?? null,
|
|
4513
|
+
},
|
|
4514
|
+
this.#coordinatorRescopeMoveId,
|
|
4515
|
+
move.previousCwd,
|
|
4516
|
+
);
|
|
4517
|
+
completed = true;
|
|
4518
|
+
} finally {
|
|
4519
|
+
this.#registerRuntimeStateFinalizer();
|
|
4520
|
+
if (completed) {
|
|
4521
|
+
this.#coordinatorRescopeMoveId = undefined;
|
|
4522
|
+
this.#endCoordinatorRescopeBarrier();
|
|
4523
|
+
}
|
|
4524
|
+
}
|
|
4046
4525
|
});
|
|
4047
4526
|
// Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
|
|
4048
4527
|
this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
@@ -4051,7 +4530,19 @@ export class AgentSession {
|
|
|
4051
4530
|
this.#disposeAsyncJobManager = config.disposeAsyncJobManager ?? true;
|
|
4052
4531
|
this.#retainedMemorySampler = config.retainedMemorySampler;
|
|
4053
4532
|
this.#ownedMcpManager = config.ownedMcpManager;
|
|
4054
|
-
this.#
|
|
4533
|
+
this.#setStartupTurnBarrier(config.startupTurnBarrier);
|
|
4534
|
+
// Only arm the recovery barrier when a rescope journal is actually pending. Arming it
|
|
4535
|
+
// unconditionally flips #startupTurnBarrier from absent to a (resolved) promise for
|
|
4536
|
+
// every session, which makes barrier-gated post-turn work (e.g. the hidden-next-turn
|
|
4537
|
+
// drain) take an extra scheduler hop and reorder against terminal-abort requeue.
|
|
4538
|
+
const rescopeRecoveryContext = {
|
|
4539
|
+
sessionId: this.sessionId,
|
|
4540
|
+
cwd: this.sessionManager.getCwd(),
|
|
4541
|
+
sessionFile: this.sessionManager.getSessionFile() ?? null,
|
|
4542
|
+
};
|
|
4543
|
+
if (hasCoordinatorRuntimeStateRescopeJournal(rescopeRecoveryContext)) {
|
|
4544
|
+
this.extendStartupTurnBarrier(recoverCoordinatorRuntimeStateRescope(rescopeRecoveryContext));
|
|
4545
|
+
}
|
|
4055
4546
|
this.#scopedModels = config.scopedModels ?? [];
|
|
4056
4547
|
this.#thinkingLevel = config.thinkingLevel;
|
|
4057
4548
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
@@ -4139,7 +4630,7 @@ export class AgentSession {
|
|
|
4139
4630
|
// agent.prompt directly, allocates right before admission.
|
|
4140
4631
|
this.agent.followUp(message);
|
|
4141
4632
|
},
|
|
4142
|
-
injectIdle: async messages => {
|
|
4633
|
+
injectIdle: async (messages, signal) => {
|
|
4143
4634
|
// Mandated boundary comment (corrected turn semantics): same origin
|
|
4144
4635
|
// split as the streaming injector — an allowed owned-completion
|
|
4145
4636
|
// delivery starts a fresh turn attempt/lineage and is not a
|
|
@@ -4153,37 +4644,43 @@ export class AgentSession {
|
|
|
4153
4644
|
if (dropped.length > 0) this.#settleDeliveredOwnedRegistrations(dropped);
|
|
4154
4645
|
const first = survivors[0];
|
|
4155
4646
|
if (!first) return;
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
4160
|
-
|
|
4161
|
-
|
|
4162
|
-
// completion). Route the survivors through followUp — the
|
|
4163
|
-
// streaming injector's path — which allocates the fresh resume
|
|
4164
|
-
// lineage at actual admission (review thread P1).
|
|
4165
|
-
if (this.isStreaming) {
|
|
4166
|
-
for (const message of survivors) this.agent.followUp(message);
|
|
4167
|
-
return;
|
|
4168
|
-
}
|
|
4169
|
-
if (survivors.some(message => ownedCompletionResumeAction(message) === "fresh"))
|
|
4170
|
-
this.#resumeFromOwnedCompletion();
|
|
4647
|
+
const settleIfDisposing = (): boolean => {
|
|
4648
|
+
if (!this.#isDisposed && !this.#sessionAdmissionClosing && !this.#disposeAbortController.signal.aborted)
|
|
4649
|
+
return false;
|
|
4650
|
+
this.#settleDeliveredOwnedRegistrations(survivors);
|
|
4651
|
+
return true;
|
|
4652
|
+
};
|
|
4171
4653
|
try {
|
|
4172
|
-
|
|
4173
|
-
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4654
|
+
await this.#withSessionAdmission(
|
|
4655
|
+
"prompt",
|
|
4656
|
+
async () => {
|
|
4657
|
+
if (settleIfDisposing()) return;
|
|
4658
|
+
if (this.isStreaming) {
|
|
4659
|
+
await awaitPromptInvocationPreflight(this.agent.waitForIdle(), signal);
|
|
4660
|
+
if (settleIfDisposing()) return;
|
|
4661
|
+
}
|
|
4662
|
+
if (survivors.some(message => ownedCompletionResumeAction(message) === "fresh"))
|
|
4663
|
+
this.#resumeFromOwnedCompletion();
|
|
4664
|
+
if (survivors.length === 1) {
|
|
4665
|
+
await this.agent.prompt(first, {
|
|
4666
|
+
...this.#managedFallbackPromptOptions(),
|
|
4667
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
4668
|
+
if (handle) this.#acceptSdkAttemptRun(handle);
|
|
4669
|
+
},
|
|
4670
|
+
});
|
|
4671
|
+
} else {
|
|
4672
|
+
await this.agent.prompt(survivors, {
|
|
4673
|
+
...this.#managedFallbackPromptOptions(),
|
|
4674
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
4675
|
+
if (handle) this.#acceptSdkAttemptRun(handle);
|
|
4676
|
+
},
|
|
4677
|
+
});
|
|
4678
|
+
}
|
|
4679
|
+
},
|
|
4680
|
+
signal,
|
|
4681
|
+
undefined,
|
|
4682
|
+
{ idleDelivery: true },
|
|
4683
|
+
);
|
|
4187
4684
|
} finally {
|
|
4188
4685
|
// The owned completions were delivered OR the prompt attempt
|
|
4189
4686
|
// failed (e.g. provider rejection): either way the yield
|
|
@@ -4195,16 +4692,19 @@ export class AgentSession {
|
|
|
4195
4692
|
this.#settleDeliveredOwnedRegistrations(survivors);
|
|
4196
4693
|
}
|
|
4197
4694
|
},
|
|
4198
|
-
scheduleIdleFlush: run => {
|
|
4695
|
+
scheduleIdleFlush: (run, onSkip) => {
|
|
4696
|
+
// The startup barrier already gates injectIdle, so begin waiting on a
|
|
4697
|
+
// pending barrier immediately. Once readiness has settled, ordinary
|
|
4698
|
+
// idle wakes retain the fixed merge window.
|
|
4699
|
+
const delayMs = this.#startupTurnBarrierPending ? 0 : FOLD_WAKE_MERGE_WINDOW_MS;
|
|
4199
4700
|
this.#schedulePostPromptTask(
|
|
4200
|
-
async
|
|
4201
|
-
await run();
|
|
4701
|
+
async signal => {
|
|
4702
|
+
await run(signal);
|
|
4202
4703
|
},
|
|
4203
|
-
|
|
4204
|
-
// turn instead of each buying its own.
|
|
4205
|
-
{ delayMs: FOLD_WAKE_MERGE_WINDOW_MS },
|
|
4704
|
+
{ delayMs, onSkip, excludeFromPostPromptRecovery: true },
|
|
4206
4705
|
);
|
|
4207
4706
|
},
|
|
4707
|
+
getIdleFlushSignal: () => this.#postPromptTasksAbortController.signal,
|
|
4208
4708
|
});
|
|
4209
4709
|
this.agent.setOnBeforeYield(() => this.yieldQueue.flush("streaming"));
|
|
4210
4710
|
// Stop-after-result, never abort: a fold arms this once and the loop ends the
|
|
@@ -5116,21 +5616,26 @@ export class AgentSession {
|
|
|
5116
5616
|
|
|
5117
5617
|
async #runToolSessionTransitionCleanups(): Promise<void> {
|
|
5118
5618
|
const cleanups = Array.from(this.#toolSessionTransitionCleanups);
|
|
5119
|
-
this.#toolSessionTransitionCleanups.clear();
|
|
5120
5619
|
const results = await Promise.allSettled(cleanups.map(async cleanup => await cleanup()));
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
|
|
5620
|
+
const failures: unknown[] = [];
|
|
5621
|
+
for (let index = 0; index < results.length; index++) {
|
|
5622
|
+
const result = results[index]!;
|
|
5623
|
+
if (result.status === "fulfilled") this.#toolSessionTransitionCleanups.delete(cleanups[index]!);
|
|
5624
|
+
else failures.push(result.reason);
|
|
5124
5625
|
}
|
|
5626
|
+
if (failures.length > 0) throw new AggregateError(failures, "Tool session transition cleanup failed.");
|
|
5125
5627
|
}
|
|
5126
5628
|
|
|
5127
5629
|
async #runToolSessionCleanups(): Promise<void> {
|
|
5128
5630
|
const cleanups = Array.from(this.#toolSessionCleanups);
|
|
5129
|
-
this.#toolSessionCleanups.clear();
|
|
5130
5631
|
const results = await Promise.allSettled(cleanups.map(async cleanup => await cleanup()));
|
|
5131
|
-
|
|
5132
|
-
|
|
5632
|
+
const failures: unknown[] = [];
|
|
5633
|
+
for (let index = 0; index < results.length; index++) {
|
|
5634
|
+
const result = results[index]!;
|
|
5635
|
+
if (result.status === "fulfilled") this.#toolSessionCleanups.delete(cleanups[index]!);
|
|
5636
|
+
else failures.push(result.reason);
|
|
5133
5637
|
}
|
|
5638
|
+
if (failures.length > 0) throw new AggregateError(failures, "Tool session cleanup failed.");
|
|
5134
5639
|
}
|
|
5135
5640
|
|
|
5136
5641
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
@@ -5337,7 +5842,7 @@ export class AgentSession {
|
|
|
5337
5842
|
#coordinatorToolObservations = new WeakMap<object, CoordinatorToolObservation>();
|
|
5338
5843
|
#agentEventAdmission = new WeakMap<
|
|
5339
5844
|
object,
|
|
5340
|
-
{ scope?: AttemptScope; sdkRunToken?: string; persistGeneration: number }
|
|
5845
|
+
{ scope?: AttemptScope; sdkRunToken?: string; persistGeneration: number; persistBarrier?: Promise<void> }
|
|
5341
5846
|
>();
|
|
5342
5847
|
|
|
5343
5848
|
/**
|
|
@@ -5398,10 +5903,15 @@ export class AgentSession {
|
|
|
5398
5903
|
scope: this.#activeAttemptScope,
|
|
5399
5904
|
sdkRunToken: this.#activeSdkRunToken,
|
|
5400
5905
|
persistGeneration: this.#coordinatorPersistGeneration,
|
|
5906
|
+
persistBarrier: this.#coordinatorRescopeBarrier,
|
|
5401
5907
|
});
|
|
5402
5908
|
// First statement of the listener: the observation must precede every claim,
|
|
5403
5909
|
// reservation, and async hop this handler performs.
|
|
5404
5910
|
this.#observeCoordinatorToolEvent(event);
|
|
5911
|
+
// The run that owned the steering queue is over. Decide ONCE, synchronously
|
|
5912
|
+
// and before any ownership gate, what happens to steering it never consumed
|
|
5913
|
+
// — a late terminal of a superseded run must still settle its own steering.
|
|
5914
|
+
if (event.type === "agent_end" && event.disownedSteering?.length) this.#settleDisownedSteering(event);
|
|
5405
5915
|
// Reserve canonical message order synchronously. Agent listeners are not
|
|
5406
5916
|
// awaited, so a tool-result spill may yield while a later continuation
|
|
5407
5917
|
// otherwise overtakes it in persisted/display context.
|
|
@@ -5507,6 +6017,11 @@ export class AgentSession {
|
|
|
5507
6017
|
this.#resolveSessionSettlement();
|
|
5508
6018
|
}
|
|
5509
6019
|
})();
|
|
6020
|
+
this.#coordinatorEventHandlers.add(handler);
|
|
6021
|
+
void handler.then(
|
|
6022
|
+
() => this.#coordinatorEventHandlers.delete(handler),
|
|
6023
|
+
() => this.#coordinatorEventHandlers.delete(handler),
|
|
6024
|
+
);
|
|
5510
6025
|
if (eventLease) eventLease.track("post_prompt", "agent-session-event", handler);
|
|
5511
6026
|
return handler;
|
|
5512
6027
|
};
|
|
@@ -5515,7 +6030,7 @@ export class AgentSession {
|
|
|
5515
6030
|
#coordinatorPersistQueue: Promise<void> = Promise.resolve();
|
|
5516
6031
|
|
|
5517
6032
|
#recordPostPublicationOutcome(
|
|
5518
|
-
context:
|
|
6033
|
+
context: CoordinatorRuntimeStatePersistContext,
|
|
5519
6034
|
correlationId: string | undefined,
|
|
5520
6035
|
kind: "worker_integration" | "terminal_persistence",
|
|
5521
6036
|
outcome: { status: "completed" | "failed" | "timed_out"; error?: string },
|
|
@@ -5530,20 +6045,57 @@ export class AgentSession {
|
|
|
5530
6045
|
message: `${kind} ${outcome.status} for ${correlation}${safeError ? `: ${safeError}` : ""}`,
|
|
5531
6046
|
});
|
|
5532
6047
|
}
|
|
5533
|
-
const
|
|
5534
|
-
|
|
5535
|
-
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
6048
|
+
const generation = this.#coordinatorPersistGeneration;
|
|
6049
|
+
const barrier = this.#coordinatorRescopeBarrier;
|
|
6050
|
+
const persist = async () => {
|
|
6051
|
+
if (barrier) {
|
|
6052
|
+
await barrier;
|
|
6053
|
+
return await persistCoordinatorWorkerIntegrationOutcome(context, {
|
|
6054
|
+
...outcome,
|
|
6055
|
+
kind,
|
|
6056
|
+
correlationId,
|
|
6057
|
+
error: sanitizePostPublicationError(outcome.error),
|
|
6058
|
+
});
|
|
6059
|
+
}
|
|
6060
|
+
return generation === this.#coordinatorPersistGeneration
|
|
6061
|
+
? await persistCoordinatorWorkerIntegrationOutcome(context, {
|
|
6062
|
+
...outcome,
|
|
6063
|
+
kind,
|
|
6064
|
+
correlationId,
|
|
6065
|
+
error: sanitizePostPublicationError(outcome.error),
|
|
6066
|
+
})
|
|
6067
|
+
: undefined;
|
|
6068
|
+
};
|
|
6069
|
+
const queued = barrier
|
|
6070
|
+
? barrier.then(() => this.#appendCoordinatorPersist(persist))
|
|
6071
|
+
: this.#appendCoordinatorPersist(persist);
|
|
6072
|
+
if (barrier) this.#trackReleasedBarrierPersist(queued);
|
|
6073
|
+
else this.#trackUnbarrieredCoordinatorPersist(queued);
|
|
5542
6074
|
void queued.catch(error =>
|
|
5543
|
-
|
|
6075
|
+
this.#warnPersistFailure(
|
|
6076
|
+
"Failed to persist terminal reconciliation outcome",
|
|
6077
|
+
error,
|
|
6078
|
+
context.stateFile,
|
|
6079
|
+
context.sessionId,
|
|
6080
|
+
),
|
|
5544
6081
|
);
|
|
5545
6082
|
}
|
|
5546
6083
|
|
|
6084
|
+
#captureCoordinatorRuntimeStatePersistContext(): CoordinatorRuntimeStatePersistContext {
|
|
6085
|
+
const context = {
|
|
6086
|
+
sessionId: this.sessionId,
|
|
6087
|
+
cwd: this.sessionManager.getCwd(),
|
|
6088
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
6089
|
+
};
|
|
6090
|
+
const explicitStateFile = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
|
|
6091
|
+
const stateFile =
|
|
6092
|
+
explicitStateFile ||
|
|
6093
|
+
(context.sessionId.trim()
|
|
6094
|
+
? path.join(sessionRuntimeDir(context.cwd, context.sessionId), "runtime-state.json")
|
|
6095
|
+
: null);
|
|
6096
|
+
return { ...context, stateFile };
|
|
6097
|
+
}
|
|
6098
|
+
|
|
5547
6099
|
/**
|
|
5548
6100
|
* Reserve this event's place in the sidecar write order.
|
|
5549
6101
|
*
|
|
@@ -5559,35 +6111,66 @@ export class AgentSession {
|
|
|
5559
6111
|
#queueCoordinatorRuntimeStatePersist(event: AgentSessionEvent, propagateFailure = false): Promise<void> {
|
|
5560
6112
|
if (isNonDispatchedToolEvent(event)) return Promise.resolve();
|
|
5561
6113
|
const observation = this.#coordinatorToolObservations.get(event);
|
|
5562
|
-
const
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
6114
|
+
const admission = this.#agentEventAdmission.get(event);
|
|
6115
|
+
const barrier = admission?.persistBarrier;
|
|
6116
|
+
if (barrier) {
|
|
6117
|
+
const run = async () => {
|
|
6118
|
+
await barrier;
|
|
6119
|
+
const context = this.#captureCoordinatorRuntimeStatePersistContext();
|
|
6120
|
+
await this.#persistRuntimeStateInBackground(event, context, observation, propagateFailure);
|
|
6121
|
+
};
|
|
6122
|
+
const queued = barrier.then(() => this.#appendCoordinatorPersist(run));
|
|
6123
|
+
this.#trackReleasedBarrierPersist(queued);
|
|
6124
|
+
return queued;
|
|
6125
|
+
}
|
|
6126
|
+
const context = this.#captureCoordinatorRuntimeStatePersistContext();
|
|
6127
|
+
const generation = admission?.persistGeneration ?? this.#coordinatorPersistGeneration;
|
|
5568
6128
|
const run = () =>
|
|
5569
6129
|
generation === this.#coordinatorPersistGeneration
|
|
5570
6130
|
? this.#persistRuntimeStateInBackground(event, context, observation, propagateFailure)
|
|
5571
6131
|
: Promise.resolve();
|
|
5572
|
-
const queued = this.#
|
|
5573
|
-
this.#
|
|
6132
|
+
const queued = this.#appendCoordinatorPersist(run);
|
|
6133
|
+
this.#trackUnbarrieredCoordinatorPersist(queued);
|
|
5574
6134
|
return queued;
|
|
5575
6135
|
}
|
|
5576
6136
|
|
|
5577
6137
|
async #persistRuntimeStateInBackground(
|
|
5578
6138
|
event: AgentSessionEvent,
|
|
5579
|
-
context:
|
|
6139
|
+
context: CoordinatorRuntimeStatePersistContext,
|
|
5580
6140
|
observation: CoordinatorToolObservation | undefined,
|
|
5581
6141
|
propagateFailure: boolean,
|
|
5582
6142
|
): Promise<void> {
|
|
5583
6143
|
try {
|
|
5584
6144
|
await persistCoordinatorRuntimeStateFromEvent(event, context, observation);
|
|
5585
6145
|
} catch (error) {
|
|
5586
|
-
|
|
6146
|
+
this.#warnPersistFailure(
|
|
6147
|
+
"Failed to persist coordinator runtime state",
|
|
6148
|
+
error,
|
|
6149
|
+
context.stateFile,
|
|
6150
|
+
context.sessionId,
|
|
6151
|
+
{ event: event.type },
|
|
6152
|
+
);
|
|
5587
6153
|
if (propagateFailure) throw error;
|
|
5588
6154
|
}
|
|
5589
6155
|
}
|
|
5590
6156
|
|
|
6157
|
+
#warnPersistFailure(
|
|
6158
|
+
message: string,
|
|
6159
|
+
error: unknown,
|
|
6160
|
+
stateFile: string | null,
|
|
6161
|
+
sessionId: string,
|
|
6162
|
+
extra: Record<string, unknown> = {},
|
|
6163
|
+
): void {
|
|
6164
|
+
const fields = {
|
|
6165
|
+
...extra,
|
|
6166
|
+
...sessionStateLockFailureFields(error),
|
|
6167
|
+
...(stateFile === null ? {} : { stateFile }),
|
|
6168
|
+
};
|
|
6169
|
+
const key = stateFile ?? `session:${sessionId}`;
|
|
6170
|
+
if (shouldWarnPersistFailure(key, error)) logger.warn(message, fields);
|
|
6171
|
+
else logger.debug(message, { ...fields, suppressed: true });
|
|
6172
|
+
}
|
|
6173
|
+
|
|
5591
6174
|
async #emitSessionEvent(event: AgentSessionEvent, eventLease?: RunResourceProducerLease): Promise<void> {
|
|
5592
6175
|
const attemptScope = (event as AgentSessionEvent & { scope?: AttemptScope }).scope;
|
|
5593
6176
|
if (event.type === "turn_start") {
|
|
@@ -5645,9 +6228,7 @@ export class AgentSession {
|
|
|
5645
6228
|
// Do not let a lock-hostile sidecar suppress the terminal event itself.
|
|
5646
6229
|
const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(event);
|
|
5647
6230
|
this.#emit(event);
|
|
5648
|
-
void terminalPersistence
|
|
5649
|
-
logger.warn("Failed to persist terminal coordinator runtime state", { error: String(error) });
|
|
5650
|
-
});
|
|
6231
|
+
void terminalPersistence;
|
|
5651
6232
|
await this.#emitExtensionEvent(event);
|
|
5652
6233
|
return;
|
|
5653
6234
|
}
|
|
@@ -6530,9 +7111,11 @@ export class AgentSession {
|
|
|
6530
7111
|
});
|
|
6531
7112
|
}
|
|
6532
7113
|
this.#resolveRetry();
|
|
7114
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing) return;
|
|
6533
7115
|
|
|
6534
7116
|
const compactionTask = this.#schedulePostPromptTask(
|
|
6535
7117
|
async () => {
|
|
7118
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing) return;
|
|
6536
7119
|
await this.#checkCompaction(msg, true, undefined, activePromptHandle);
|
|
6537
7120
|
},
|
|
6538
7121
|
{ resourceRunId: activePromptHandle },
|
|
@@ -6605,9 +7188,11 @@ export class AgentSession {
|
|
|
6605
7188
|
selectionFenceGeneration: number,
|
|
6606
7189
|
lease?: RunResourceProducerLease,
|
|
6607
7190
|
leaseTask: Promise<void> = task,
|
|
7191
|
+
excludeFromRecovery = false,
|
|
6608
7192
|
): void {
|
|
6609
7193
|
this.#postPromptTasks.add(task);
|
|
6610
7194
|
this.#postPromptTaskSelectionFenceGenerations.set(task, selectionFenceGeneration);
|
|
7195
|
+
if (excludeFromRecovery) this.#postPromptTaskRecoveryExcluded.add(task);
|
|
6611
7196
|
this.#ensurePostPromptTasksPromise();
|
|
6612
7197
|
if (lease) {
|
|
6613
7198
|
lease.track("post_prompt", "agent-session", leaseTask);
|
|
@@ -6618,6 +7203,7 @@ export class AgentSession {
|
|
|
6618
7203
|
.finally(() => {
|
|
6619
7204
|
this.#postPromptTasks.delete(task);
|
|
6620
7205
|
this.#postPromptTaskSelectionFenceGenerations.delete(task);
|
|
7206
|
+
this.#postPromptTaskRecoveryExcluded.delete(task);
|
|
6621
7207
|
if (this.#postPromptTasks.size === 0) this.#resolvePostPromptTasks();
|
|
6622
7208
|
});
|
|
6623
7209
|
}
|
|
@@ -6631,6 +7217,7 @@ export class AgentSession {
|
|
|
6631
7217
|
resourceRunId?: string;
|
|
6632
7218
|
leaseTask?: Promise<void>;
|
|
6633
7219
|
selectionFenceGeneration?: number;
|
|
7220
|
+
excludeFromPostPromptRecovery?: boolean;
|
|
6634
7221
|
},
|
|
6635
7222
|
): Promise<void> {
|
|
6636
7223
|
const selectionFenceGeneration =
|
|
@@ -6657,7 +7244,11 @@ export class AgentSession {
|
|
|
6657
7244
|
options?.onSkip?.();
|
|
6658
7245
|
return Promise.resolve();
|
|
6659
7246
|
}
|
|
6660
|
-
const
|
|
7247
|
+
const taskSignal = reservation?.ok ? reservation.lease.signal : this.#postPromptTasksAbortController.signal;
|
|
7248
|
+
// Disposal is a session-wide terminal fence. Compose it with the selected
|
|
7249
|
+
// logical-resource signal so stale leases from earlier runs cannot keep a
|
|
7250
|
+
// post-prompt task alive after the current run was quarantined.
|
|
7251
|
+
const signal = AbortSignal.any([taskSignal, this.#disposeAbortController.signal]);
|
|
6661
7252
|
const runScheduled = async () => {
|
|
6662
7253
|
if (delayMs > 0) {
|
|
6663
7254
|
try {
|
|
@@ -6685,6 +7276,7 @@ export class AgentSession {
|
|
|
6685
7276
|
selectionFenceGeneration,
|
|
6686
7277
|
reservation?.ok ? reservation.lease : undefined,
|
|
6687
7278
|
options?.leaseTask,
|
|
7279
|
+
options?.excludeFromPostPromptRecovery,
|
|
6688
7280
|
);
|
|
6689
7281
|
return scheduled;
|
|
6690
7282
|
}
|
|
@@ -6818,7 +7410,7 @@ export class AgentSession {
|
|
|
6818
7410
|
settleLease();
|
|
6819
7411
|
return;
|
|
6820
7412
|
}
|
|
6821
|
-
await this.#awaitStartupTurnBarrier();
|
|
7413
|
+
await this.#awaitStartupTurnBarrier(scheduledSignal);
|
|
6822
7414
|
if (!canContinue()) {
|
|
6823
7415
|
settleLease();
|
|
6824
7416
|
return;
|
|
@@ -6840,10 +7432,14 @@ export class AgentSession {
|
|
|
6840
7432
|
skip("queue_drained");
|
|
6841
7433
|
return;
|
|
6842
7434
|
}
|
|
6843
|
-
//
|
|
6844
|
-
//
|
|
6845
|
-
//
|
|
6846
|
-
//
|
|
7435
|
+
// Final synchronous boundary before agent.continue* entry; no await
|
|
7436
|
+
// intervenes between here and method entry. A same-turn continuation
|
|
7437
|
+
// of a terminally aborted turn is denied here, while an independent
|
|
7438
|
+
// root request queued past that fence mints its fresh identity here —
|
|
7439
|
+
// so a cancelled or emptied continuation never leaves the session on
|
|
7440
|
+
// an identity that escapes the fence. Owned-completion deliveries are
|
|
7441
|
+
// NOT affected (they use followUp).
|
|
7442
|
+
if (this.#hasQueuedFreshRootRequest()) this.#resumeFromOwnedCompletion();
|
|
6847
7443
|
if (this.#isTurnContinuationBlocked()) {
|
|
6848
7444
|
skip("terminal_turn");
|
|
6849
7445
|
return;
|
|
@@ -7222,6 +7818,7 @@ export class AgentSession {
|
|
|
7222
7818
|
this.#postPromptTasksAbortController = new AbortController();
|
|
7223
7819
|
this.#postPromptTasks.clear();
|
|
7224
7820
|
this.#postPromptTaskSelectionFenceGenerations.clear();
|
|
7821
|
+
this.#postPromptTaskRecoveryExcluded.clear();
|
|
7225
7822
|
this.#releaseDeferredAgentEndContinuations();
|
|
7226
7823
|
this.#resolveTtsrResume();
|
|
7227
7824
|
this.#resolvePostPromptTasks();
|
|
@@ -7243,8 +7840,11 @@ export class AgentSession {
|
|
|
7243
7840
|
await this.#ttsrResumePromise;
|
|
7244
7841
|
continue;
|
|
7245
7842
|
}
|
|
7246
|
-
|
|
7247
|
-
|
|
7843
|
+
const recoveryTasks = [...this.#postPromptTasks].filter(
|
|
7844
|
+
task => !this.#postPromptTaskRecoveryExcluded.has(task),
|
|
7845
|
+
);
|
|
7846
|
+
if (recoveryTasks.length > 0) {
|
|
7847
|
+
await Promise.allSettled(recoveryTasks);
|
|
7248
7848
|
continue;
|
|
7249
7849
|
}
|
|
7250
7850
|
// Tracked post-prompt tasks cover deferred continuations scheduled from
|
|
@@ -7266,7 +7866,9 @@ export class AgentSession {
|
|
|
7266
7866
|
*/
|
|
7267
7867
|
async #waitForPostPromptTasksBeforeSelectionFence(selectionFenceGeneration: number): Promise<void> {
|
|
7268
7868
|
const precedingTasks = [...this.#postPromptTasks].filter(
|
|
7269
|
-
task =>
|
|
7869
|
+
task =>
|
|
7870
|
+
!this.#postPromptTaskRecoveryExcluded.has(task) &&
|
|
7871
|
+
(this.#postPromptTaskSelectionFenceGenerations.get(task) ?? 0) < selectionFenceGeneration,
|
|
7270
7872
|
);
|
|
7271
7873
|
if (precedingTasks.length > 0) await Promise.allSettled(precedingTasks);
|
|
7272
7874
|
}
|
|
@@ -8385,16 +8987,122 @@ export class AgentSession {
|
|
|
8385
8987
|
*/
|
|
8386
8988
|
dispose(): Promise<void> {
|
|
8387
8989
|
this.#evalExecutionDisposing = true;
|
|
8388
|
-
if (this.#
|
|
8389
|
-
|
|
8390
|
-
this.#
|
|
8391
|
-
|
|
8392
|
-
|
|
8990
|
+
if (this.#disposeCompleted) return Promise.resolve();
|
|
8991
|
+
if (this.#disposeTerminalError !== undefined) return Promise.reject(this.#disposeTerminalError);
|
|
8992
|
+
if (this.#disposeCallerPromise) return this.#disposeCallerPromise;
|
|
8993
|
+
if (!this.#disposeRunPromise) {
|
|
8994
|
+
this.#disposeDeadline = Date.now() + this.#disposeTimeoutMs;
|
|
8995
|
+
this.#disposeDeadlineExpired = Promise.withResolvers<void>();
|
|
8996
|
+
this.#disposeDeadlineTimer = setTimeout(() => this.#disposeDeadlineExpired?.resolve(), this.#disposeTimeoutMs);
|
|
8997
|
+
this.#disposeDeadlineTimer.unref?.();
|
|
8998
|
+
this.#abortAdmissionEpoch++;
|
|
8999
|
+
this.#isDisposed = true;
|
|
9000
|
+
// Invalidate every coordinator event admitted before disposal. Handlers may
|
|
9001
|
+
// still unwind, but their captured generation can no longer enqueue a write.
|
|
9002
|
+
this.#coordinatorPersistGeneration += 1;
|
|
9003
|
+
this.#disposeAbortController.abort();
|
|
9004
|
+
// Disposal owns a bounded Agent abort below. Waiting for the active prompt's
|
|
9005
|
+
// admission here would put that unbounded prompt ahead of the abort budget.
|
|
9006
|
+
this.#disposeAdmissionClosed = this.#closeSessionAdmission({ waitForActive: false });
|
|
9007
|
+
this.#disposePostPromptDrain = this.#cancelPostPromptTasks();
|
|
9008
|
+
this.#settleDeliveredOwnedRegistrations(this.#pendingNextTurnMessages.map(entry => entry.message));
|
|
9009
|
+
this.#pendingNextTurnMessages = [];
|
|
9010
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
9011
|
+
this.#abortActiveMidRunBarriers();
|
|
9012
|
+
this.abortCompaction();
|
|
9013
|
+
this.abortRetry();
|
|
9014
|
+
this.#quarantineAgentRunResources();
|
|
9015
|
+
this.agent.abort();
|
|
9016
|
+
this.agent.setMainAttemptScopeObserver(undefined);
|
|
9017
|
+
this.#disconnectFromAgent();
|
|
9018
|
+
this.#disposeRunPromise = this.#dispose()
|
|
9019
|
+
.then(
|
|
9020
|
+
() => {
|
|
9021
|
+
this.#disposeCompleted = true;
|
|
9022
|
+
},
|
|
9023
|
+
error => {
|
|
9024
|
+
this.#disposeTerminalError = error;
|
|
9025
|
+
throw error;
|
|
9026
|
+
},
|
|
9027
|
+
)
|
|
9028
|
+
.finally(() => {
|
|
9029
|
+
if (this.#disposeDeadlineTimer) clearTimeout(this.#disposeDeadlineTimer);
|
|
9030
|
+
this.#disposeDeadlineTimer = undefined;
|
|
9031
|
+
});
|
|
9032
|
+
void this.#disposeRunPromise.catch(() => {});
|
|
9033
|
+
}
|
|
9034
|
+
const teardown = this.#disposeRunPromise;
|
|
9035
|
+
const deadlineExpired = this.#disposeDeadlineExpired;
|
|
9036
|
+
if (!deadlineExpired || Date.now() >= this.#disposeDeadline) {
|
|
9037
|
+
return Promise.reject(new SessionDisposalIncompleteError(this.#disposeActiveStepLabel));
|
|
9038
|
+
}
|
|
9039
|
+
const boundedTeardown = Promise.race([
|
|
9040
|
+
teardown,
|
|
9041
|
+
deadlineExpired.promise.then(() => {
|
|
9042
|
+
throw new SessionDisposalIncompleteError(this.#disposeActiveStepLabel);
|
|
9043
|
+
}),
|
|
9044
|
+
]);
|
|
9045
|
+
this.#disposeCallerPromise = boundedTeardown;
|
|
9046
|
+
void boundedTeardown
|
|
9047
|
+
.finally(() => {
|
|
9048
|
+
if (this.#disposeCallerPromise === boundedTeardown) this.#disposeCallerPromise = undefined;
|
|
9049
|
+
})
|
|
9050
|
+
.catch(() => {});
|
|
9051
|
+
return boundedTeardown;
|
|
9052
|
+
}
|
|
9053
|
+
|
|
9054
|
+
/** Join the retained teardown owner without allocating another public deadline. */
|
|
9055
|
+
awaitDisposeCompletion(): Promise<void> {
|
|
9056
|
+
return this.#disposeRunPromise ?? this.dispose();
|
|
9057
|
+
}
|
|
9058
|
+
|
|
9059
|
+
setDisposeTimeoutForTests(timeoutMs: number): void {
|
|
9060
|
+
if (this.#disposeRunPromise) throw new Error("Cannot change the disposal timeout after disposal has started.");
|
|
9061
|
+
this.#disposeTimeoutMs = Math.max(0, timeoutMs);
|
|
9062
|
+
}
|
|
9063
|
+
|
|
9064
|
+
trackPostPromptTaskForTests(task: Promise<void>): void {
|
|
9065
|
+
this.#trackPostPromptTask(task, this.#selectionFenceGeneration);
|
|
9066
|
+
}
|
|
9067
|
+
|
|
9068
|
+
requestWorkerIntegrationForTests(): void {
|
|
9069
|
+
this.#requestWorkerIntegrationAttempt();
|
|
9070
|
+
}
|
|
9071
|
+
|
|
9072
|
+
/** Cancel the active logical run domain, including managed continuations detached from Agent's active attempt. */
|
|
9073
|
+
#quarantineAgentRunResources(): void {
|
|
9074
|
+
const resourceRunId = this.agent.currentManagedLogicalRunId ?? this.agent.activeResourceRunId;
|
|
9075
|
+
if (resourceRunId !== undefined) this.agent.resourceLedger.quarantine(String(resourceRunId));
|
|
8393
9076
|
}
|
|
8394
9077
|
|
|
8395
9078
|
async #dispose(): Promise<void> {
|
|
8396
|
-
|
|
8397
|
-
const
|
|
9079
|
+
const disposeFailures: Array<{ label: string; error: unknown; critical: boolean }> = [];
|
|
9080
|
+
const awaitDisposeStep = async <T>(
|
|
9081
|
+
label: string,
|
|
9082
|
+
operation: Promise<T>,
|
|
9083
|
+
critical = false,
|
|
9084
|
+
): Promise<T | undefined> => {
|
|
9085
|
+
this.#disposeActiveStepLabel = label;
|
|
9086
|
+
try {
|
|
9087
|
+
return await operation;
|
|
9088
|
+
} catch (error) {
|
|
9089
|
+
disposeFailures.push({ label, error, critical });
|
|
9090
|
+
logger.warn("Session dispose step failed", { label, error: String(error) });
|
|
9091
|
+
return undefined;
|
|
9092
|
+
} finally {
|
|
9093
|
+
if (this.#disposeActiveStepLabel === label) this.#disposeActiveStepLabel = undefined;
|
|
9094
|
+
}
|
|
9095
|
+
};
|
|
9096
|
+
const drainCoordinatorEventHandlers = async (): Promise<void> => {
|
|
9097
|
+
while (this.#coordinatorEventHandlers.size > 0) {
|
|
9098
|
+
await Promise.allSettled([...this.#coordinatorEventHandlers]);
|
|
9099
|
+
}
|
|
9100
|
+
};
|
|
9101
|
+
const admissionClosed = this.#disposeAdmissionClosed ?? this.#closeSessionAdmission();
|
|
9102
|
+
await awaitDisposeStep("session admission close", admissionClosed);
|
|
9103
|
+
await awaitDisposeStep("cwd move admission close", this.sessionManager.closeCwdMoveAdmission());
|
|
9104
|
+
await awaitDisposeStep("admitted cwd transition", this.sessionManager.joinCwdTransition(), true);
|
|
9105
|
+
await awaitDisposeStep("active cwd readers", this.sessionManager.joinCwdReaders(), true);
|
|
8398
9106
|
this.#isDisposed = true;
|
|
8399
9107
|
// Reject new direct Python starts as soon as disposal begins (synchronously,
|
|
8400
9108
|
// before any await) so callers cannot race a start against teardown.
|
|
@@ -8412,83 +9120,116 @@ export class AgentSession {
|
|
|
8412
9120
|
// R2-5: join any in-flight mid-run maintenance invocation before teardown so the
|
|
8413
9121
|
// abort-aware maintenance promise (already aborted above) settles and cannot touch
|
|
8414
9122
|
// torn-down state afterward.
|
|
8415
|
-
await this.#waitForActiveMidRunMaintenance();
|
|
9123
|
+
await awaitDisposeStep("mid-run maintenance", this.#waitForActiveMidRunMaintenance());
|
|
8416
9124
|
// R2-5: give the aborted Agent run a bounded chance to settle, then force-
|
|
8417
9125
|
// invalidate its run id so an abort-ignoring provider/tool cannot emit a late
|
|
8418
9126
|
// message_end after teardown. Mirrors AgentSession.abort({ timeoutMs }).
|
|
9127
|
+
const idleBudgetMs = Math.max(0, Math.min(2_000, this.#disposeDeadline - Date.now()));
|
|
8419
9128
|
const disposeIdleSettled = await Promise.race([
|
|
8420
9129
|
this.agent.waitForIdle().then(
|
|
8421
9130
|
() => true,
|
|
8422
9131
|
() => true,
|
|
8423
9132
|
),
|
|
8424
|
-
Bun.sleep(
|
|
9133
|
+
Bun.sleep(idleBudgetMs).then(() => false),
|
|
8425
9134
|
]);
|
|
8426
9135
|
if (!disposeIdleSettled) {
|
|
8427
|
-
|
|
8428
|
-
this.agent.forceAbort("Session disposed", this.#activeLogicalRunId);
|
|
8429
|
-
} catch {
|
|
8430
|
-
// AttemptScope handle may not be registered for sessions
|
|
8431
|
-
// that don't participate in the facility (e.g. test mocks).
|
|
8432
|
-
this.agent.forceAbort("Session disposed");
|
|
8433
|
-
}
|
|
9136
|
+
this.#forceSessionRecovery();
|
|
8434
9137
|
}
|
|
8435
|
-
await
|
|
8436
|
-
await
|
|
8437
|
-
|
|
9138
|
+
await awaitDisposeStep("active compaction", this.#compactionCompletion ?? Promise.resolve(), true);
|
|
9139
|
+
await awaitDisposeStep(
|
|
9140
|
+
"active auto-compaction",
|
|
9141
|
+
Promise.allSettled([...this.#autoCompactionCompletions]).then(() => {}),
|
|
9142
|
+
true,
|
|
9143
|
+
);
|
|
9144
|
+
await awaitDisposeStep("agent end publication", this.#agentEndPublicationPromise);
|
|
9145
|
+
await awaitDisposeStep("queued extension events", this.#queuedExtensionEvents);
|
|
9146
|
+
await awaitDisposeStep("agent end handling", this.#agentEndHandlingPromise);
|
|
9147
|
+
await awaitDisposeStep("coordinator event handlers", drainCoordinatorEventHandlers(), true);
|
|
8438
9148
|
// Drain the sidecar write order for the same reason the two queues above are
|
|
8439
9149
|
// drained: each entry writes under the native identity-bound state-file lock, so a
|
|
8440
|
-
// still-queued write would run after the session that owns it is gone — releasing
|
|
9150
|
+
// a still-queued write would run after the session that owns it is gone — releasing
|
|
8441
9151
|
// a lock whose owner no longer exists, and under `bun test --isolate` calling into
|
|
8442
9152
|
// the addon after the runtime tore down the context it was scheduled in. The chain
|
|
8443
9153
|
// is already failure-absorbing, so this only waits.
|
|
8444
|
-
await this.#coordinatorPersistQueue;
|
|
9154
|
+
await awaitDisposeStep("coordinator persistence", this.#coordinatorPersistQueue, true);
|
|
9155
|
+
// Terminal publication records its reconciliation from a promise reaction, which
|
|
9156
|
+
// can append after the queue snapshot above was awaited. Drain the tracked writes
|
|
9157
|
+
// again so disposal cannot return while that late reconciliation still owns a lock.
|
|
9158
|
+
await awaitDisposeStep("unbarriered coordinator persistence", this.#drainUnbarrieredCoordinatorPersists(), true);
|
|
8445
9159
|
this.#pendingBackgroundExchanges = [];
|
|
8446
|
-
this
|
|
9160
|
+
this.#drainTerminalOwnedYieldEntries();
|
|
8447
9161
|
|
|
8448
9162
|
this.agent.setOnBeforeYield(undefined);
|
|
8449
9163
|
try {
|
|
8450
9164
|
if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
|
|
8451
|
-
await
|
|
9165
|
+
await awaitDisposeStep(
|
|
9166
|
+
"session shutdown handlers",
|
|
9167
|
+
this.#extensionRunner.emit({ type: "session_shutdown" }),
|
|
9168
|
+
);
|
|
8452
9169
|
}
|
|
8453
9170
|
} catch (error) {
|
|
9171
|
+
if (isSessionDisposalIncompleteError(error)) throw error;
|
|
8454
9172
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
8455
9173
|
}
|
|
8456
9174
|
this.#workflowGateEmitter?.fence?.();
|
|
8457
9175
|
this.#workflowGateEmitter = undefined;
|
|
8458
9176
|
this.#notifyWorkflowGateEmitterChanged(this.sessionId, undefined);
|
|
8459
|
-
await this.#flushWorkerIntegrationAttempt();
|
|
8460
|
-
await
|
|
9177
|
+
await awaitDisposeStep("worker integration", this.#flushWorkerIntegrationAttempt(), true);
|
|
9178
|
+
await awaitDisposeStep(
|
|
9179
|
+
"worker integration ownership",
|
|
9180
|
+
this.#workerIntegrationScheduler?.joinOwnedRequests() ?? Promise.resolve(),
|
|
9181
|
+
true,
|
|
9182
|
+
);
|
|
9183
|
+
// Worker integration completion can enqueue terminal reconciliation while the
|
|
9184
|
+
// flush above is pending; drain that late producer before disposal resolves.
|
|
9185
|
+
await awaitDisposeStep("post-worker coordinator persistence", this.#coordinatorPersistQueue, true);
|
|
9186
|
+
await awaitDisposeStep("post-worker unbarriered persistence", this.#drainUnbarrieredCoordinatorPersists(), true);
|
|
9187
|
+
await awaitDisposeStep("post-prompt tasks", this.#disposePostPromptDrain ?? this.#cancelPostPromptTasks(), true);
|
|
8461
9188
|
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
8462
9189
|
// leak its background bash/task work into the parent's manager. Only
|
|
8463
9190
|
// the session that owns the manager goes on to dispose it (which itself
|
|
8464
9191
|
// nukes any leftover jobs and pending deliveries).
|
|
8465
9192
|
this.#cancelOwnAsyncJobs();
|
|
8466
|
-
|
|
9193
|
+
// Final drain (Codex review P2 on #5088): an async-job completion that
|
|
9194
|
+
// enqueued during the awaited session_shutdown window above survived the
|
|
9195
|
+
// first drain with a retained delivery claim, and the already-aborted
|
|
9196
|
+
// disposal signal makes its idle-flush task exit before YieldQueue.flush
|
|
9197
|
+
// can run. Producers are now cancelled, so drain once more to settle any
|
|
9198
|
+
// straggler envelope and release its claim before owned-manager teardown
|
|
9199
|
+
// — a shared manager would otherwise keep the claim past this session's
|
|
9200
|
+
// disposal.
|
|
9201
|
+
this.#drainTerminalOwnedYieldEntries();
|
|
9202
|
+
await awaitDisposeStep("deferred owner shutdown", Promise.allSettled(this.#deferredOwnerShutdownFinalizations));
|
|
8467
9203
|
const ownedAsyncManager = this.#ownedAsyncJobManager;
|
|
8468
9204
|
if (ownedAsyncManager && this.#disposeAsyncJobManager) {
|
|
8469
|
-
const drained = await ownedAsyncManager.dispose({ timeoutMs: 3_000 });
|
|
9205
|
+
const drained = await awaitDisposeStep("async job manager", ownedAsyncManager.dispose({ timeoutMs: 3_000 }));
|
|
8470
9206
|
const deliveryState = ownedAsyncManager.getDeliveryState();
|
|
8471
9207
|
if (drained === false && deliveryState) {
|
|
8472
9208
|
logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
|
|
8473
9209
|
}
|
|
8474
|
-
if (
|
|
8475
|
-
|
|
9210
|
+
if (drained === false) {
|
|
9211
|
+
await awaitDisposeStep(
|
|
9212
|
+
"async job manager retained runners",
|
|
9213
|
+
ownedAsyncManager.awaitRetainedDisposalCompletion(),
|
|
9214
|
+
true,
|
|
9215
|
+
);
|
|
8476
9216
|
}
|
|
9217
|
+
if (AsyncJobManager.instance() === ownedAsyncManager) AsyncJobManager.setInstance(undefined);
|
|
8477
9218
|
}
|
|
8478
|
-
await this.#runToolSessionTransitionCleanups();
|
|
8479
|
-
await this.#runToolSessionCleanups();
|
|
9219
|
+
await awaitDisposeStep("tool session transition cleanups", this.#runToolSessionTransitionCleanups(), true);
|
|
9220
|
+
await awaitDisposeStep("tool session cleanups", this.#runToolSessionCleanups(), true);
|
|
8480
9221
|
// Only disconnect the MCP manager THIS session owns (top-level sessions that
|
|
8481
9222
|
// connected plugin-bundle MCP servers). Subagents and callers that merely
|
|
8482
9223
|
// observe the process-global manager must never tear down a manager they do
|
|
8483
9224
|
// not own. Mirrors the ownedAsyncJobManager rule above.
|
|
8484
9225
|
const ownedMcpManager = this.#ownedMcpManager;
|
|
8485
9226
|
if (ownedMcpManager) {
|
|
8486
|
-
await ownedMcpManager.releaseLeases();
|
|
9227
|
+
await awaitDisposeStep("MCP lease release", ownedMcpManager.releaseLeases());
|
|
8487
9228
|
if (MCPManager.instance() === ownedMcpManager) {
|
|
8488
9229
|
MCPManager.setInstance(undefined);
|
|
8489
9230
|
}
|
|
8490
9231
|
}
|
|
8491
|
-
await shutdownAllLspClients();
|
|
9232
|
+
await awaitDisposeStep("LSP shutdown", shutdownAllLspClients());
|
|
8492
9233
|
// F13: release only THIS session's browser tabs on dispose (kill:false → remote
|
|
8493
9234
|
// browsers disconnect, headless close gracefully). Scoped by the session id the
|
|
8494
9235
|
// browser tool tagged tabs with, so other live sessions' tabs are untouched.
|
|
@@ -8499,29 +9240,48 @@ export class AgentSession {
|
|
|
8499
9240
|
this.#unregisterSessionMemorySettings = undefined;
|
|
8500
9241
|
if (ownerTerminalContextFromEnvironment() === null) this.#unregisterRuntimeStateFinalizer?.();
|
|
8501
9242
|
this.#unregisterRuntimeStateFinalizer = undefined;
|
|
8502
|
-
|
|
8503
|
-
|
|
9243
|
+
this.#unregisterBeforeMoveListener?.();
|
|
9244
|
+
this.#unregisterBeforeMoveListener = undefined;
|
|
9245
|
+
this.#unregisterMoveAbortListener?.();
|
|
9246
|
+
this.#unregisterMoveAbortListener = undefined;
|
|
9247
|
+
this.#unregisterMovePublicationListener?.();
|
|
9248
|
+
this.#unregisterMovePublicationListener = undefined;
|
|
9249
|
+
this.#unregisterAfterMoveListener?.();
|
|
9250
|
+
this.#unregisterAfterMoveListener = undefined;
|
|
9251
|
+
await awaitDisposeStep("browser tab release", releaseTabsForOwner(this.sessionManager.getSessionId()), true);
|
|
9252
|
+
const pythonExecutionsSettled = await awaitDisposeStep(
|
|
9253
|
+
"Python execution preparation",
|
|
9254
|
+
this.#prepareEvalExecutionsForDispose(),
|
|
8504
9255
|
);
|
|
8505
|
-
const pythonExecutionsSettled = await this.#prepareEvalExecutionsForDispose();
|
|
8506
9256
|
if (!pythonExecutionsSettled) {
|
|
8507
9257
|
logger.warn(
|
|
8508
9258
|
"Detaching retained Python kernel ownership during dispose while Python execution is still active",
|
|
8509
9259
|
);
|
|
8510
9260
|
}
|
|
8511
|
-
await disposeKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
8512
|
-
await disposeVmContextsByOwner(this.#evalKernelOwnerId);
|
|
9261
|
+
await awaitDisposeStep("Python kernel sessions", disposeKernelSessionsByOwner(this.#evalKernelOwnerId));
|
|
9262
|
+
await awaitDisposeStep("VM contexts", disposeVmContextsByOwner(this.#evalKernelOwnerId));
|
|
8513
9263
|
this.#releasePowerAssertion();
|
|
8514
9264
|
// Disconnect the agent event listener BEFORE closing session resources so a late
|
|
8515
9265
|
// provider/tool message_end cannot append to the closing SessionManager.
|
|
8516
9266
|
this.#disconnectFromAgent();
|
|
8517
|
-
await this.memoryBackend.dispose();
|
|
8518
|
-
if (this.#workspaceTreeService)
|
|
8519
|
-
|
|
9267
|
+
await awaitDisposeStep("memory backend", this.memoryBackend.dispose(), true);
|
|
9268
|
+
if (this.#workspaceTreeService)
|
|
9269
|
+
await awaitDisposeStep("workspace tree", this.#workspaceTreeService.dispose(), true);
|
|
9270
|
+
if (this.#networkPrewarmService)
|
|
9271
|
+
await awaitDisposeStep("network prewarm", this.#networkPrewarmService.dispose(), true);
|
|
8520
9272
|
this.#modelRegistry.authStorage?.releaseCredentialScope(this.credentialSessionId);
|
|
8521
|
-
|
|
9273
|
+
let criticalDisposeError: unknown;
|
|
9274
|
+
try {
|
|
9275
|
+
await this.#closeSessionManagerForDispose();
|
|
9276
|
+
} catch (error) {
|
|
9277
|
+
criticalDisposeError = error;
|
|
9278
|
+
logger.warn("Session manager close failed during dispose; continuing independent cleanup", {
|
|
9279
|
+
error: String(error),
|
|
9280
|
+
});
|
|
9281
|
+
}
|
|
8522
9282
|
this.#closeAllProviderSessions("dispose");
|
|
8523
9283
|
const hindsightState = this.getHindsightSessionState();
|
|
8524
|
-
await hindsightState?.dispose();
|
|
9284
|
+
await awaitDisposeStep("hindsight state", hindsightState?.dispose() ?? Promise.resolve());
|
|
8525
9285
|
this.setHindsightSessionState(undefined);
|
|
8526
9286
|
if (this.#unsubscribeAppendOnly) {
|
|
8527
9287
|
this.#unsubscribeAppendOnly();
|
|
@@ -8529,7 +9289,35 @@ export class AgentSession {
|
|
|
8529
9289
|
}
|
|
8530
9290
|
this.#eventListeners = [];
|
|
8531
9291
|
this.#rebuildEventListenerSnapshot();
|
|
9292
|
+
if (criticalDisposeError !== undefined) {
|
|
9293
|
+
disposeFailures.push({ label: "session manager close", error: criticalDisposeError, critical: true });
|
|
9294
|
+
}
|
|
9295
|
+
if (disposeFailures.length > 0) {
|
|
9296
|
+
throw new AggregateError(
|
|
9297
|
+
disposeFailures.map(failure => failure.error),
|
|
9298
|
+
`Session disposal did not reach a terminal state: ${disposeFailures.map(failure => failure.label).join(", ")}`,
|
|
9299
|
+
);
|
|
9300
|
+
}
|
|
9301
|
+
}
|
|
9302
|
+
|
|
9303
|
+
async #closeSessionManagerForDispose(): Promise<void> {
|
|
9304
|
+
let retryDelayMs = 50;
|
|
9305
|
+
for (;;) {
|
|
9306
|
+
const outcome = await this.sessionManager.closeStrict();
|
|
9307
|
+
if (outcome.kind === "closed") {
|
|
9308
|
+
SessionManager.releaseProcessCwdOwnership(this.sessionManager);
|
|
9309
|
+
return;
|
|
9310
|
+
}
|
|
9311
|
+
if (outcome.kind === "close_unknown") {
|
|
9312
|
+
SessionManager.releaseProcessCwdOwnership(this.sessionManager);
|
|
9313
|
+
throw outcome.error;
|
|
9314
|
+
}
|
|
9315
|
+
logger.warn("Session manager close remains retryable during dispose", { error: outcome.error.message });
|
|
9316
|
+
await Bun.sleep(retryDelayMs);
|
|
9317
|
+
retryDelayMs = Math.min(retryDelayMs * 2, 1_000);
|
|
9318
|
+
}
|
|
8532
9319
|
}
|
|
9320
|
+
|
|
8533
9321
|
/**
|
|
8534
9322
|
* Strict writer close for ACP session delete. On the first attempt it flushes
|
|
8535
9323
|
* pending writes, then returns the certainty-aware close outcome so the caller
|
|
@@ -8663,14 +9451,24 @@ export class AgentSession {
|
|
|
8663
9451
|
await this.#waitForPostPromptTasksBeforeSelectionFence(ignoreSelectionFenceGeneration);
|
|
8664
9452
|
}
|
|
8665
9453
|
await this.#waitForSessionSettlement(ignoreSelectionFenceGeneration);
|
|
9454
|
+
const hasBlockingPostPromptTasks =
|
|
9455
|
+
ignoreSelectionFenceGeneration === undefined
|
|
9456
|
+
? this.#postPromptTasks.size > 0
|
|
9457
|
+
: [...this.#postPromptTasks].some(
|
|
9458
|
+
task =>
|
|
9459
|
+
!this.#postPromptTaskRecoveryExcluded.has(task) &&
|
|
9460
|
+
(this.#postPromptTaskSelectionFenceGenerations.get(task) ?? 0) < ignoreSelectionFenceGeneration,
|
|
9461
|
+
);
|
|
8666
9462
|
if (
|
|
8667
9463
|
!this.agent.state.isStreaming &&
|
|
8668
9464
|
!this.#retryPromise &&
|
|
8669
9465
|
!this.#ttsrResumePromise &&
|
|
8670
|
-
!
|
|
9466
|
+
!hasBlockingPostPromptTasks &&
|
|
8671
9467
|
!this.#isSessionSettlementPending(ignoreSelectionFenceGeneration)
|
|
8672
9468
|
)
|
|
8673
9469
|
return;
|
|
9470
|
+
if (ignoreSelectionFenceGeneration === undefined && hasBlockingPostPromptTasks && this.#postPromptTasksPromise)
|
|
9471
|
+
await this.#postPromptTasksPromise;
|
|
8674
9472
|
}
|
|
8675
9473
|
}
|
|
8676
9474
|
|
|
@@ -8695,6 +9493,42 @@ export class AgentSession {
|
|
|
8695
9493
|
await this.sessionManager.flush();
|
|
8696
9494
|
}
|
|
8697
9495
|
|
|
9496
|
+
/** Test seam: hold prompt admission while bypassing a pending selection fence. */
|
|
9497
|
+
runWithPromptAdmissionForTests(body: () => Promise<void>): Promise<void> {
|
|
9498
|
+
return this.#withSessionAdmission("prompt", async () => body(), undefined, undefined, {
|
|
9499
|
+
bypassSelectionFenceGeneration: this.#selectionFenceGeneration - 1,
|
|
9500
|
+
});
|
|
9501
|
+
}
|
|
9502
|
+
|
|
9503
|
+
queueCoordinatorRuntimeStatePersistForTests(event: AgentSessionEvent, gate: Promise<void>): Promise<void> {
|
|
9504
|
+
this.#agentEventAdmission.set(event, {
|
|
9505
|
+
persistGeneration: this.#coordinatorPersistGeneration,
|
|
9506
|
+
persistBarrier: this.#coordinatorRescopeBarrier,
|
|
9507
|
+
});
|
|
9508
|
+
const task = (async () => {
|
|
9509
|
+
await gate;
|
|
9510
|
+
await this.#queueCoordinatorRuntimeStatePersist(event, true);
|
|
9511
|
+
})();
|
|
9512
|
+
this.#coordinatorEventHandlers.add(task);
|
|
9513
|
+
void task.then(
|
|
9514
|
+
() => this.#coordinatorEventHandlers.delete(task),
|
|
9515
|
+
() => this.#coordinatorEventHandlers.delete(task),
|
|
9516
|
+
);
|
|
9517
|
+
return task;
|
|
9518
|
+
}
|
|
9519
|
+
|
|
9520
|
+
parkAgentEndForCoordinatorPersistForTests(event: Extract<AgentSessionEvent, { type: "agent_end" }>): void {
|
|
9521
|
+
this.#agentEventAdmission.set(event, {
|
|
9522
|
+
persistGeneration: this.#coordinatorPersistGeneration,
|
|
9523
|
+
persistBarrier: this.#coordinatorRescopeBarrier,
|
|
9524
|
+
});
|
|
9525
|
+
this.#pendingAgentEndEmit = event;
|
|
9526
|
+
}
|
|
9527
|
+
|
|
9528
|
+
flushParkedAgentEndForCoordinatorPersistForTests(): void {
|
|
9529
|
+
this.#flushPendingAgentEnd();
|
|
9530
|
+
}
|
|
9531
|
+
|
|
8698
9532
|
async drainAsyncJobDeliveriesForAcp(options?: { timeoutMs?: number }): Promise<boolean> {
|
|
8699
9533
|
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
8700
9534
|
if (!manager) return false;
|
|
@@ -8970,23 +9804,77 @@ export class AgentSession {
|
|
|
8970
9804
|
this.#setGuardedAgentTools(activeTools);
|
|
8971
9805
|
}
|
|
8972
9806
|
|
|
9807
|
+
/**
|
|
9808
|
+
* Subscribe to folds of this session's foreground waits. Backed by the
|
|
9809
|
+
* session-owned manager (or the process-global instance for legacy setups)
|
|
9810
|
+
* so the SDK host can publish one `bash_folded` frame per fold.
|
|
9811
|
+
*/
|
|
9812
|
+
onJobFold(listener: (event: JobFoldEvent) => void): () => void {
|
|
9813
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
9814
|
+
if (!manager) return () => {};
|
|
9815
|
+
const ownerId = this.#agentId;
|
|
9816
|
+
return manager.onFold(event => {
|
|
9817
|
+
if (ownerId && manager.getJob(event.jobId)?.ownerId !== ownerId) return;
|
|
9818
|
+
listener(event);
|
|
9819
|
+
});
|
|
9820
|
+
}
|
|
9821
|
+
|
|
8973
9822
|
/**
|
|
8974
9823
|
* Ask the active managed foreground bash call to return as a background job.
|
|
8975
9824
|
* Returns false when no supported foreground tool is currently backgroundable.
|
|
9825
|
+
* `reason` names the trigger (`chord` for the keybinding, `sdk_control` for
|
|
9826
|
+
* the `bash.background` control) and is recorded on the folded job.
|
|
9827
|
+
*/
|
|
9828
|
+
async requestForegroundBashBackground(reason: FoldReason = "chord", adapter?: FoldAdapter): Promise<boolean> {
|
|
9829
|
+
return (await this.requestForegroundBashBackgroundOutcome(reason, adapter)).status === "folded";
|
|
9830
|
+
}
|
|
9831
|
+
|
|
9832
|
+
/**
|
|
9833
|
+
* Typed variant of {@link requestForegroundBashBackground} for the SDK
|
|
9834
|
+
* `bash.background` control (C52), which must distinguish "this fold moved
|
|
9835
|
+
* the wait" from "an earlier fold already moved it" from "nothing to fold".
|
|
9836
|
+
*
|
|
9837
|
+
* `already_backgrounded` is reported when no wait is foldable but this
|
|
9838
|
+
* session still has a running job that a fold (steer, chord, timer, or a
|
|
9839
|
+
* prior control call) moved out of the foreground: `metadata.foldReason` is
|
|
9840
|
+
* set only by folds, never by `async: true` starts, so it is the exact
|
|
9841
|
+
* evidence that the wait the client is asking about has already left.
|
|
8976
9842
|
*/
|
|
8977
|
-
async
|
|
8978
|
-
|
|
9843
|
+
async requestForegroundBashBackgroundOutcome(
|
|
9844
|
+
reason: FoldReason = "chord",
|
|
9845
|
+
adapter?: FoldAdapter,
|
|
9846
|
+
): Promise<ForegroundFoldOutcome> {
|
|
9847
|
+
if (!this.#foldCoordinator.hasFoldableParticipant()) {
|
|
9848
|
+
return this.#hasRunningFoldedJob() ? { status: "already_backgrounded" } : { status: "no_active_bash" };
|
|
9849
|
+
}
|
|
8979
9850
|
try {
|
|
8980
|
-
const result = await this.#foldCoordinator.requestFold();
|
|
8981
|
-
|
|
9851
|
+
const result = await this.#foldCoordinator.requestFold(adapter, reason);
|
|
9852
|
+
if (result.status === "folded") {
|
|
9853
|
+
if (reason === "steer") {
|
|
9854
|
+
this.emitNotice(
|
|
9855
|
+
"info",
|
|
9856
|
+
`Folded ${result.receipt.label} into background job ${result.receipt.jobId} so your message can be handled now.`,
|
|
9857
|
+
"fold",
|
|
9858
|
+
);
|
|
9859
|
+
}
|
|
9860
|
+
return { status: "folded", jobId: result.receipt.jobId };
|
|
9861
|
+
}
|
|
9862
|
+
if (result.status === "unavailable" && this.#hasRunningFoldedJob()) return { status: "already_backgrounded" };
|
|
9863
|
+
return { status: "not_foldable", reason: result.reason };
|
|
8982
9864
|
} catch (error) {
|
|
8983
9865
|
logger.warn("Foreground fold request failed", {
|
|
8984
9866
|
error: error instanceof Error ? error.message : String(error),
|
|
8985
9867
|
});
|
|
8986
|
-
return
|
|
9868
|
+
return { status: "not_foldable", reason: error instanceof Error ? error.message : String(error) };
|
|
8987
9869
|
}
|
|
8988
9870
|
}
|
|
8989
9871
|
|
|
9872
|
+
#hasRunningFoldedJob(): boolean {
|
|
9873
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
9874
|
+
if (!manager) return false;
|
|
9875
|
+
const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
|
|
9876
|
+
return manager.getRunningJobs(ownerFilter).some(job => job.metadata?.foldReason !== undefined);
|
|
9877
|
+
}
|
|
8990
9878
|
/**
|
|
8991
9879
|
* Get all configured tool names (built-in via --tools or default, plus custom tools).
|
|
8992
9880
|
*/
|
|
@@ -9277,7 +10165,10 @@ export class AgentSession {
|
|
|
9277
10165
|
// The lease keeps writers out for the whole execution, so this can
|
|
9278
10166
|
// only trip if a caller bypassed the lease; surface it rather than
|
|
9279
10167
|
// returning a result computed against a retired cwd.
|
|
9280
|
-
if (
|
|
10168
|
+
if (
|
|
10169
|
+
this.sessionManager.getCwdGeneration() !== admittedGeneration &&
|
|
10170
|
+
!this.sessionManager.consumeNestedCwdMove(admittedGeneration)
|
|
10171
|
+
) {
|
|
9281
10172
|
throw new Error(
|
|
9282
10173
|
"Session working directory changed while this tool executed; retry against the new cwd.",
|
|
9283
10174
|
);
|
|
@@ -10077,8 +10968,8 @@ export class AgentSession {
|
|
|
10077
10968
|
}
|
|
10078
10969
|
|
|
10079
10970
|
/** Current interrupt mode */
|
|
10080
|
-
get
|
|
10081
|
-
return this.agent.
|
|
10971
|
+
get toolInterruptPolicy(): "abort_tools" | "finish_tools" {
|
|
10972
|
+
return this.agent.getToolInterruptPolicy();
|
|
10082
10973
|
}
|
|
10083
10974
|
|
|
10084
10975
|
/** Current session file path, or undefined if sessions are disabled */
|
|
@@ -10165,7 +11056,7 @@ export class AgentSession {
|
|
|
10165
11056
|
thinking: this.#thinkingLevel ?? "off",
|
|
10166
11057
|
steeringMode: this.steeringMode,
|
|
10167
11058
|
followUpMode: this.followUpMode,
|
|
10168
|
-
|
|
11059
|
+
toolInterruptPolicy: this.toolInterruptPolicy,
|
|
10169
11060
|
};
|
|
10170
11061
|
}
|
|
10171
11062
|
|
|
@@ -10924,6 +11815,19 @@ export class AgentSession {
|
|
|
10924
11815
|
* @throws Error if no model selected or no API key available (when not streaming)
|
|
10925
11816
|
*/
|
|
10926
11817
|
async prompt(text: string, options?: PromptOptions): Promise<void> {
|
|
11818
|
+
const releaseStartupPromptWaiter = this.#reserveStartupPromptWaiter();
|
|
11819
|
+
try {
|
|
11820
|
+
await this.#promptInternal(text, options, releaseStartupPromptWaiter);
|
|
11821
|
+
} finally {
|
|
11822
|
+
releaseStartupPromptWaiter();
|
|
11823
|
+
}
|
|
11824
|
+
}
|
|
11825
|
+
|
|
11826
|
+
async #promptInternal(
|
|
11827
|
+
text: string,
|
|
11828
|
+
options: PromptOptions | undefined,
|
|
11829
|
+
releaseStartupPromptWaiter: () => void,
|
|
11830
|
+
): Promise<void> {
|
|
10927
11831
|
const hasUsableImage =
|
|
10928
11832
|
options?.images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
|
|
10929
11833
|
if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
|
|
@@ -10937,7 +11841,12 @@ export class AgentSession {
|
|
|
10937
11841
|
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
10938
11842
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
10939
11843
|
|
|
10940
|
-
if (
|
|
11844
|
+
if (
|
|
11845
|
+
expandPromptTemplates &&
|
|
11846
|
+
text.startsWith("/") &&
|
|
11847
|
+
isNamespacedSkillSlashCommandName(text.slice(1)) &&
|
|
11848
|
+
!options?.images?.length
|
|
11849
|
+
) {
|
|
10941
11850
|
const skillCommands = new Map(this.skills.map(skill => [getSkillSlashCommandName(skill), skill]));
|
|
10942
11851
|
const invocations = parseSkillInvocations(text, skillCommands);
|
|
10943
11852
|
if (invocations.length === 1) {
|
|
@@ -11023,7 +11932,15 @@ export class AgentSession {
|
|
|
11023
11932
|
|
|
11024
11933
|
// If streaming, queue via steer() or followUp() based on option.
|
|
11025
11934
|
// Abort unwind is awaited above so a successor is not busy against leftover in-flight.
|
|
11026
|
-
|
|
11935
|
+
// An explicit streamingBehavior submitted while another prompt still holds
|
|
11936
|
+
// or awaits session admission (agent idle, session not yet live) is queued
|
|
11937
|
+
// as well: entering admission behind that prompt would only get this
|
|
11938
|
+
// message cancelled by the predecessor's preflight reset and lost.
|
|
11939
|
+
const promptAdmissionContended =
|
|
11940
|
+
options?.streamingBehavior !== undefined &&
|
|
11941
|
+
(this.#activeSessionAdmission?.kind === "prompt" ||
|
|
11942
|
+
this.#sessionAdmissionQueue.some(entry => entry.kind === "prompt"));
|
|
11943
|
+
if ((this.#isLiveTurnBusy() || promptAdmissionContended) && !waitedForAbortUnwind) {
|
|
11027
11944
|
if (!options?.streamingBehavior) {
|
|
11028
11945
|
throw new AgentBusyError();
|
|
11029
11946
|
}
|
|
@@ -11033,7 +11950,10 @@ export class AgentSession {
|
|
|
11033
11950
|
claimsGenuineUserIntent,
|
|
11034
11951
|
});
|
|
11035
11952
|
} else {
|
|
11036
|
-
await this.#queueSteer(expandedText, options?.images, {
|
|
11953
|
+
await this.#queueSteer(expandedText, options?.images, {
|
|
11954
|
+
claimsGenuineUserIntent,
|
|
11955
|
+
forceOneAtATime: options.steerQueuePolicy === "sequential",
|
|
11956
|
+
});
|
|
11037
11957
|
}
|
|
11038
11958
|
if (workflowIntentDiff) {
|
|
11039
11959
|
this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
|
|
@@ -11046,6 +11966,35 @@ export class AgentSession {
|
|
|
11046
11966
|
await this.#withSessionAdmission(
|
|
11047
11967
|
"prompt",
|
|
11048
11968
|
async admission => {
|
|
11969
|
+
// A prompt that carries an explicit streamingBehavior and was admitted
|
|
11970
|
+
// behind another prompt (that prompt reset the preflight generation
|
|
11971
|
+
// while this one waited in the admission queue) is a busy submission:
|
|
11972
|
+
// queue it against the now-live turn instead of cancelling it.
|
|
11973
|
+
if (
|
|
11974
|
+
options?.streamingBehavior !== undefined &&
|
|
11975
|
+
this.#isPromptPreflightCancelled(admissionGeneration, admissionSignal) &&
|
|
11976
|
+
options.preflightSignal?.aborted !== true &&
|
|
11977
|
+
!this.#abortUnwind
|
|
11978
|
+
) {
|
|
11979
|
+
admission.release();
|
|
11980
|
+
if (options.streamingBehavior === "followUp") {
|
|
11981
|
+
await this.#queueFollowUp(expandedText, options?.images, {
|
|
11982
|
+
forceOneAtATime: options.followUpQueuePolicy === "sequential",
|
|
11983
|
+
claimsGenuineUserIntent,
|
|
11984
|
+
});
|
|
11985
|
+
} else {
|
|
11986
|
+
await this.#queueSteer(expandedText, options?.images, {
|
|
11987
|
+
claimsGenuineUserIntent,
|
|
11988
|
+
forceOneAtATime: options.steerQueuePolicy === "sequential",
|
|
11989
|
+
});
|
|
11990
|
+
}
|
|
11991
|
+
if (workflowIntentDiff) {
|
|
11992
|
+
this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
|
|
11993
|
+
}
|
|
11994
|
+
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
11995
|
+
else options?.onPreflightAccepted?.();
|
|
11996
|
+
return;
|
|
11997
|
+
}
|
|
11049
11998
|
this.#throwIfPromptPreflightCancelled(admissionGeneration, admissionSignal);
|
|
11050
11999
|
if (workflowIntentDiff) {
|
|
11051
12000
|
this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
|
|
@@ -11069,7 +12018,12 @@ export class AgentSession {
|
|
|
11069
12018
|
attribution: promptAttribution,
|
|
11070
12019
|
timestamp: Date.now(),
|
|
11071
12020
|
}
|
|
11072
|
-
: {
|
|
12021
|
+
: {
|
|
12022
|
+
role: "user" as const,
|
|
12023
|
+
content: userContent,
|
|
12024
|
+
attribution: promptAttribution,
|
|
12025
|
+
timestamp: Date.now(),
|
|
12026
|
+
};
|
|
11073
12027
|
if (deepInterviewUserIntentEpoch !== undefined)
|
|
11074
12028
|
this.#deepInterviewGenuineUserMessageEpochs.set(message, deepInterviewUserIntentEpoch);
|
|
11075
12029
|
await this.refreshGjcSubskillTools();
|
|
@@ -11097,6 +12051,8 @@ export class AgentSession {
|
|
|
11097
12051
|
}
|
|
11098
12052
|
},
|
|
11099
12053
|
options?.preflightSignal,
|
|
12054
|
+
undefined,
|
|
12055
|
+
{ startupPromptWaiterRelease: releaseStartupPromptWaiter },
|
|
11100
12056
|
);
|
|
11101
12057
|
}
|
|
11102
12058
|
|
|
@@ -11213,6 +12169,7 @@ export class AgentSession {
|
|
|
11213
12169
|
| "streamingBehavior"
|
|
11214
12170
|
| "toolChoice"
|
|
11215
12171
|
| "followUpQueuePolicy"
|
|
12172
|
+
| "steerQueuePolicy"
|
|
11216
12173
|
| "onPreflightAccepted"
|
|
11217
12174
|
| "onPreflightAcceptCommit"
|
|
11218
12175
|
| "preflightSignal"
|
|
@@ -11248,6 +12205,7 @@ export class AgentSession {
|
|
|
11248
12205
|
await this.sendCustomMessage(message, {
|
|
11249
12206
|
deliverAs: options.streamingBehavior,
|
|
11250
12207
|
followUpQueuePolicy: options.followUpQueuePolicy,
|
|
12208
|
+
steerQueuePolicy: options.steerQueuePolicy,
|
|
11251
12209
|
});
|
|
11252
12210
|
return;
|
|
11253
12211
|
}
|
|
@@ -11328,6 +12286,7 @@ export class AgentSession {
|
|
|
11328
12286
|
skipPostPromptRecoveryWait?: boolean;
|
|
11329
12287
|
predecessorAgentEndHold?: symbol;
|
|
11330
12288
|
admissionLease?: SessionAdmissionLease;
|
|
12289
|
+
skipInitialSteeringPoll?: boolean;
|
|
11331
12290
|
onRunAccepted?: (handle: AttemptRunHandle) => void;
|
|
11332
12291
|
onFinalPreflight?: (context: { hasPendingNextTurnMessages: boolean }) => Promise<boolean>;
|
|
11333
12292
|
resetRetryReplaySafety?: boolean;
|
|
@@ -11658,6 +12617,7 @@ export class AgentSession {
|
|
|
11658
12617
|
|
|
11659
12618
|
const agentPromptOptions = {
|
|
11660
12619
|
...(options?.toolChoice ? { toolChoice: options.toolChoice } : undefined),
|
|
12620
|
+
...(options?.skipInitialSteeringPoll ? { skipInitialSteeringPoll: true } : {}),
|
|
11661
12621
|
...this.#managedFallbackPromptOptions(),
|
|
11662
12622
|
...(options?.sdkRunToken ? { sdkRunToken: options.sdkRunToken } : {}),
|
|
11663
12623
|
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
@@ -11828,8 +12788,8 @@ export class AgentSession {
|
|
|
11828
12788
|
this.setFollowUpMode(mode);
|
|
11829
12789
|
return true;
|
|
11830
12790
|
}
|
|
11831
|
-
if (kind === "
|
|
11832
|
-
this.
|
|
12791
|
+
if (kind === "tool_interrupt" && (mode === "abort_tools" || mode === "finish_tools")) {
|
|
12792
|
+
this.setToolInterruptPolicy(mode);
|
|
11833
12793
|
return true;
|
|
11834
12794
|
}
|
|
11835
12795
|
return false;
|
|
@@ -12005,6 +12965,7 @@ export class AgentSession {
|
|
|
12005
12965
|
onPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
|
|
12006
12966
|
external?: boolean;
|
|
12007
12967
|
sdkRunToken?: string;
|
|
12968
|
+
forceOneAtATime?: boolean;
|
|
12008
12969
|
},
|
|
12009
12970
|
): Promise<void> {
|
|
12010
12971
|
this.#assertNoHandoffTransition();
|
|
@@ -12013,6 +12974,26 @@ export class AgentSession {
|
|
|
12013
12974
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
12014
12975
|
if (images && images.length > 0) content.push(...images);
|
|
12015
12976
|
const message = { role: "user" as const, content, attribution: "user" as const, timestamp: Date.now() };
|
|
12977
|
+
// Enqueue-time admission: the Agent only accepts a steer into a live,
|
|
12978
|
+
// non-aborted run. Anything else (idle, unwinding after agent_end, another
|
|
12979
|
+
// prompt still in preflight, aborting) has no run to steer, so it is routed
|
|
12980
|
+
// as a sequential follow-up owned by the next turn instead of being parked
|
|
12981
|
+
// in a queue nobody owns.
|
|
12982
|
+
const admission = this.agent.steer(message, options?.forceOneAtATime ? { forceOneAtATime: true } : undefined);
|
|
12983
|
+
if (!admission.admitted) {
|
|
12984
|
+
await this.#queueFollowUp(text, images, {
|
|
12985
|
+
forceOneAtATime: options?.forceOneAtATime,
|
|
12986
|
+
claimsGenuineUserIntent: options?.claimsGenuineUserIntent,
|
|
12987
|
+
onPromoted: options?.onPromoted,
|
|
12988
|
+
sdkRunToken: options?.sdkRunToken,
|
|
12989
|
+
onQueued: message => {
|
|
12990
|
+
if (this.#abortUnwind && !options?.forceOneAtATime) this.#abortUnwindSteerFallbacks.push(message);
|
|
12991
|
+
},
|
|
12992
|
+
});
|
|
12993
|
+
this.#scheduleNonAdmittedSteerContinuation();
|
|
12994
|
+
return;
|
|
12995
|
+
}
|
|
12996
|
+
if (options?.forceOneAtATime) this.#sequentialSteerMessages.add(message);
|
|
12016
12997
|
this.#steeringMessages.push(this.#createQueuedDisplayEntry(displayText, undefined, message));
|
|
12017
12998
|
if (options?.external) {
|
|
12018
12999
|
this.#externalSteerMessages.add(message);
|
|
@@ -12024,22 +13005,42 @@ export class AgentSession {
|
|
|
12024
13005
|
const epoch = this.#claimDeepInterviewUserIntent();
|
|
12025
13006
|
this.#deepInterviewGenuineUserMessageEpochs.set(message, epoch);
|
|
12026
13007
|
}
|
|
12027
|
-
|
|
12028
|
-
|
|
12029
|
-
|
|
12030
|
-
|
|
12031
|
-
|
|
12032
|
-
|
|
12033
|
-
|
|
12034
|
-
|
|
12035
|
-
|
|
12036
|
-
|
|
12037
|
-
|
|
12038
|
-
|
|
12039
|
-
|
|
12040
|
-
|
|
12041
|
-
|
|
12042
|
-
|
|
13008
|
+
}
|
|
13009
|
+
|
|
13010
|
+
/**
|
|
13011
|
+
* A steer that was not admitted into a live run was requeued as a follow-up.
|
|
13012
|
+
* `#queueFollowUp` already schedules delivery when the session is idle; this
|
|
13013
|
+
* covers the unwind window where the session still reports busy only because
|
|
13014
|
+
* a finished prompt is unwinding (deferred agent_end / post-prompt work), so
|
|
13015
|
+
* the follow-up gate refuses while no loop actually owns the queue. A live
|
|
13016
|
+
* loop, a prompt still in preflight, or a drained queue makes the scheduled
|
|
13017
|
+
* continue a no-op.
|
|
13018
|
+
*/
|
|
13019
|
+
#scheduleNonAdmittedSteerContinuation(): void {
|
|
13020
|
+
// `#queueFollowUp` already schedules delivery when the session is idle; this
|
|
13021
|
+
// only covers the window where that gate refuses (a finished prompt is
|
|
13022
|
+
// unwinding) while no loop owns the queue. The resumable-tail requirement
|
|
13023
|
+
// is kept here on purpose: a message queued against a session that never
|
|
13024
|
+
// ran a turn waits for the next explicit prompt, as it always has.
|
|
13025
|
+
if (this.#cancelAndSubmitInProgress || !this.#canAutoContinueForSteer()) return;
|
|
13026
|
+
if (this.#canAutoContinueForFollowUp()) return;
|
|
13027
|
+
this.#scheduleQueuedDelivery();
|
|
13028
|
+
}
|
|
13029
|
+
|
|
13030
|
+
/**
|
|
13031
|
+
* Schedule delivery of whatever is queued. Used by every path that enqueues
|
|
13032
|
+
* without going through `#queueFollowUp` (custom-message steer/follow-up
|
|
13033
|
+
* routing, disown re-routing, held-steering fallback, a run's terminal): the
|
|
13034
|
+
* run that would have polled the queue is gone, so nothing else owns it.
|
|
13035
|
+
*/
|
|
13036
|
+
#scheduleQueuedDelivery(delayMs?: number): void {
|
|
13037
|
+
if (this.#cancelAndSubmitInProgress || !this.#canDeliverQueuedMessages()) return;
|
|
13038
|
+
this.#scheduleAgentContinue({
|
|
13039
|
+
...(delayMs === undefined ? {} : { delayMs }),
|
|
13040
|
+
shouldContinue: () => this.#canDeliverQueuedMessages() && this.agent.hasQueuedMessages(),
|
|
13041
|
+
rescheduleOnBusy: true,
|
|
13042
|
+
continueQueuedOnly: true,
|
|
13043
|
+
});
|
|
12043
13044
|
}
|
|
12044
13045
|
|
|
12045
13046
|
/**
|
|
@@ -12053,6 +13054,7 @@ export class AgentSession {
|
|
|
12053
13054
|
claimsGenuineUserIntent?: boolean;
|
|
12054
13055
|
onPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
|
|
12055
13056
|
sdkRunToken?: string;
|
|
13057
|
+
onQueued?: (message: AgentMessage) => void;
|
|
12056
13058
|
},
|
|
12057
13059
|
): Promise<QueuedFollowUpOwner> {
|
|
12058
13060
|
this.#assertNoHandoffTransition();
|
|
@@ -12062,6 +13064,7 @@ export class AgentSession {
|
|
|
12062
13064
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
12063
13065
|
if (images && images.length > 0) content.push(...images);
|
|
12064
13066
|
const message = { role: "user" as const, content, attribution: "user" as const, timestamp: Date.now() };
|
|
13067
|
+
options?.onQueued?.(message);
|
|
12065
13068
|
// Display entry carries the message identity so positional editing can never
|
|
12066
13069
|
// misaddress a deferred SDK follow-up held outside the Agent live queue.
|
|
12067
13070
|
const displayEntry = this.#createQueuedDisplayEntry(displayText, undefined, message);
|
|
@@ -12145,6 +13148,7 @@ export class AgentSession {
|
|
|
12145
13148
|
* Gate for idle-path follow-up auto-continue. See `#queueFollowUp` for rationale.
|
|
12146
13149
|
*/
|
|
12147
13150
|
#canAutoContinueForFollowUp(): boolean {
|
|
13151
|
+
if (this.#abortUnwind) return false;
|
|
12148
13152
|
if (this.isStreaming) return false;
|
|
12149
13153
|
if (this.isCompacting) return false;
|
|
12150
13154
|
if (this.isBashRunning) return false;
|
|
@@ -12170,6 +13174,24 @@ export class AgentSession {
|
|
|
12170
13174
|
}
|
|
12171
13175
|
}
|
|
12172
13176
|
|
|
13177
|
+
/**
|
|
13178
|
+
* Gate for delivering queued messages through `continueQueuedMessages()`.
|
|
13179
|
+
* Unlike the steer/follow-up auto-continue gates this does NOT require an
|
|
13180
|
+
* assistant tail: a queued-only continuation delivers the queued turn without
|
|
13181
|
+
* replaying the tail, so a tool-result tail (paused fold, aborted turn) is
|
|
13182
|
+
* still deliverable. A live agent loop, a retry, or an in-flight
|
|
13183
|
+
* compaction/bash/eval owns the session and defers delivery.
|
|
13184
|
+
*/
|
|
13185
|
+
#canDeliverQueuedMessages(): boolean {
|
|
13186
|
+
if (this.#abortUnwind) return false;
|
|
13187
|
+
if (this.agent.state.isStreaming) return false;
|
|
13188
|
+
if (this.isRetrying) return false;
|
|
13189
|
+
if (this.isCompacting) return false;
|
|
13190
|
+
if (this.isBashRunning) return false;
|
|
13191
|
+
if (this.isEvalRunning) return false;
|
|
13192
|
+
return true;
|
|
13193
|
+
}
|
|
13194
|
+
|
|
12173
13195
|
/**
|
|
12174
13196
|
* Gate for idle / winding-down steer auto-continue. Unlike the follow-up gate
|
|
12175
13197
|
* this checks `agent.state.isStreaming` (a live agent loop) rather than the
|
|
@@ -12178,6 +13200,7 @@ export class AgentSession {
|
|
|
12178
13200
|
* false here because it polls the steering queue itself.
|
|
12179
13201
|
*/
|
|
12180
13202
|
#canAutoContinueForSteer(): boolean {
|
|
13203
|
+
if (this.#abortUnwind) return false;
|
|
12181
13204
|
if (this.agent.state.isStreaming) return false;
|
|
12182
13205
|
if (this.isRetrying) return false;
|
|
12183
13206
|
const messages = this.agent.state.messages;
|
|
@@ -12185,6 +13208,13 @@ export class AgentSession {
|
|
|
12185
13208
|
return last?.role === "assistant" || last?.role === "bashExecution" || last?.role === "pythonExecution";
|
|
12186
13209
|
}
|
|
12187
13210
|
|
|
13211
|
+
#finalizeAbortUnwindSteerBatch(): void {
|
|
13212
|
+
if (this.agent.getSteeringMode() === "all" && this.#abortUnwindSteerFallbacks.length > 1) {
|
|
13213
|
+
this.agent.markFollowUpBatch(this.#abortUnwindSteerFallbacks);
|
|
13214
|
+
}
|
|
13215
|
+
this.#abortUnwindSteerFallbacks = [];
|
|
13216
|
+
}
|
|
13217
|
+
|
|
12188
13218
|
queueDeferredMessage(message: CustomMessage): void {
|
|
12189
13219
|
this.#queueHiddenNextTurnMessage(message, true, "turn");
|
|
12190
13220
|
}
|
|
@@ -12220,7 +13250,16 @@ export class AgentSession {
|
|
|
12220
13250
|
}
|
|
12221
13251
|
this.#scheduledHiddenNextTurnGeneration = generation;
|
|
12222
13252
|
this.#schedulePostPromptTask(
|
|
12223
|
-
async
|
|
13253
|
+
async signal => {
|
|
13254
|
+
if (signal.aborted || this.#isDisposed || this.#sessionAdmissionClosing) return;
|
|
13255
|
+
if (this.#startupTurnBarrier) {
|
|
13256
|
+
try {
|
|
13257
|
+
await this.#awaitStartupTurnBarrier(signal);
|
|
13258
|
+
} catch {
|
|
13259
|
+
return;
|
|
13260
|
+
}
|
|
13261
|
+
}
|
|
13262
|
+
if (signal.aborted || this.#isDisposed || this.#sessionAdmissionClosing) return;
|
|
12224
13263
|
if (this.#scheduledHiddenNextTurnGeneration === generation) {
|
|
12225
13264
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
12226
13265
|
}
|
|
@@ -12249,8 +13288,12 @@ export class AgentSession {
|
|
|
12249
13288
|
return;
|
|
12250
13289
|
}
|
|
12251
13290
|
try {
|
|
12252
|
-
await this.#promptQueuedHiddenNextTurnMessages();
|
|
13291
|
+
await this.#promptQueuedHiddenNextTurnMessages(signal);
|
|
12253
13292
|
} catch {
|
|
13293
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
|
|
13294
|
+
this.#pendingNextTurnMessages = [];
|
|
13295
|
+
return;
|
|
13296
|
+
}
|
|
12254
13297
|
// Leave the hidden next-turn messages queued for the next explicit prompt.
|
|
12255
13298
|
}
|
|
12256
13299
|
},
|
|
@@ -12265,7 +13308,7 @@ export class AgentSession {
|
|
|
12265
13308
|
);
|
|
12266
13309
|
}
|
|
12267
13310
|
|
|
12268
|
-
async #promptQueuedHiddenNextTurnMessages(): Promise<void> {
|
|
13311
|
+
async #promptQueuedHiddenNextTurnMessages(signal?: AbortSignal): Promise<void> {
|
|
12269
13312
|
if (this.#pendingNextTurnMessages.length === 0) {
|
|
12270
13313
|
return;
|
|
12271
13314
|
}
|
|
@@ -12295,6 +13338,14 @@ export class AgentSession {
|
|
|
12295
13338
|
if (reclassified.length === 0) {
|
|
12296
13339
|
return;
|
|
12297
13340
|
}
|
|
13341
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
|
|
13342
|
+
this.#settleDeliveredOwnedRegistrations(reclassified.map(entry => entry.message));
|
|
13343
|
+
return;
|
|
13344
|
+
}
|
|
13345
|
+
if (signal?.aborted) {
|
|
13346
|
+
this.#pendingNextTurnMessages = [...reclassified, ...this.#pendingNextTurnMessages];
|
|
13347
|
+
return;
|
|
13348
|
+
}
|
|
12298
13349
|
// Allocate a fresh root-turn lineage when the drained batch contains a
|
|
12299
13350
|
// fresh owned envelope OR an EXTERNAL hidden trigger: an external
|
|
12300
13351
|
// nextTurn message has no owned envelope, so without this it would
|
|
@@ -12316,11 +13367,24 @@ export class AgentSession {
|
|
|
12316
13367
|
const textContent = this.#getCustomMessageTextContent(message);
|
|
12317
13368
|
await this.#syncSkillPromptActiveStateSafely(message, true);
|
|
12318
13369
|
try {
|
|
13370
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
|
|
13371
|
+
this.#settleDeliveredOwnedRegistrations(reclassified.map(entry => entry.message));
|
|
13372
|
+
return;
|
|
13373
|
+
}
|
|
13374
|
+
if (signal?.aborted) {
|
|
13375
|
+
this.#pendingNextTurnMessages = [...reclassified, ...this.#pendingNextTurnMessages];
|
|
13376
|
+
return;
|
|
13377
|
+
}
|
|
12319
13378
|
await this.#promptWithMessage(message, textContent, {
|
|
12320
13379
|
prependMessages,
|
|
12321
13380
|
skipPostPromptRecoveryWait: true,
|
|
13381
|
+
preflightSignal: signal,
|
|
12322
13382
|
});
|
|
12323
13383
|
} catch (error) {
|
|
13384
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
|
|
13385
|
+
this.#settleDeliveredOwnedRegistrations(reclassified.map(entry => entry.message));
|
|
13386
|
+
return;
|
|
13387
|
+
}
|
|
12324
13388
|
// Requeue only the SURVIVING reclassified entries: a completion
|
|
12325
13389
|
// denied by scope:"owned" was settled (dropped) above and must
|
|
12326
13390
|
// never reach a later prompt — once its scope and now-unoccupied
|
|
@@ -12347,19 +13411,55 @@ export class AgentSession {
|
|
|
12347
13411
|
// another session's manager (which lacks the same local job id) would
|
|
12348
13412
|
// see job === undefined and remove a still-live registration (review
|
|
12349
13413
|
// thread P1).
|
|
12350
|
-
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
12351
13414
|
for (const message of messages) {
|
|
12352
13415
|
const details = (message as { details?: { ownedCompletions?: OwnedCompletionEnvelope[] } }).details;
|
|
12353
|
-
for (const envelope of details?.ownedCompletions ?? [])
|
|
12354
|
-
|
|
12355
|
-
|
|
12356
|
-
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
13416
|
+
for (const envelope of details?.ownedCompletions ?? []) this.#settleOwnedCompletionEnvelope(envelope);
|
|
13417
|
+
}
|
|
13418
|
+
}
|
|
13419
|
+
|
|
13420
|
+
#settleOwnedCompletionEnvelope(envelope: OwnedCompletionEnvelope): void {
|
|
13421
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
13422
|
+
const job = manager?.getJob(envelope.registration.jobId);
|
|
13423
|
+
const status = job?.generation === envelope.registration.jobGeneration ? job?.status : undefined;
|
|
13424
|
+
// Evicted jobs have no live record (job === undefined); terminal statuses
|
|
13425
|
+
// settle the registration.
|
|
13426
|
+
if (job === undefined || status === "completed" || status === "cancelled" || status === "failed") {
|
|
13427
|
+
unregisterOwnedRegistration(envelope.registration);
|
|
13428
|
+
}
|
|
13429
|
+
}
|
|
13430
|
+
|
|
13431
|
+
/**
|
|
13432
|
+
* Drop every queued yield entry, releasing its retained delivery claim and
|
|
13433
|
+
* settling terminal owned-completion envelopes so neither outlives this
|
|
13434
|
+
* session's disposal. Claims resolve to the manager that actually retained
|
|
13435
|
+
* them: owned entries via their registration endpoint (subagents inherit a
|
|
13436
|
+
* parent's manager, and the process-global instance may belong to a
|
|
13437
|
+
* different concurrent session), ordinary entries only on the session-OWNED
|
|
13438
|
+
* manager. Disposal runs this twice: before the awaited session_shutdown
|
|
13439
|
+
* emit, and again after producer cancellation — a completion enqueued
|
|
13440
|
+
* inside that window would otherwise survive disposal, its claim retained
|
|
13441
|
+
* on a shared manager and interfering with later delivery teardown (Codex
|
|
13442
|
+
* review P2 on #5088).
|
|
13443
|
+
*/
|
|
13444
|
+
#drainTerminalOwnedYieldEntries(): void {
|
|
13445
|
+
this.yieldQueue.clear((kind, entries) => {
|
|
13446
|
+
if (kind !== "async-result") return;
|
|
13447
|
+
const ownedManager = this.#ownedAsyncJobManager;
|
|
13448
|
+
for (const entry of entries) {
|
|
13449
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
13450
|
+
const record = entry as { generation?: unknown; ownedCompletion?: unknown };
|
|
13451
|
+
if (typeof record.generation === "string" && record.generation !== "") {
|
|
13452
|
+
const envelope = isOwnedCompletionEnvelope(record.ownedCompletion) ? record.ownedCompletion : undefined;
|
|
13453
|
+
const endpointId = envelope?.registration.endpointId;
|
|
13454
|
+
const manager =
|
|
13455
|
+
(endpointId !== undefined ? AsyncJobManager.forEndpoint(endpointId) : undefined) ?? ownedManager;
|
|
13456
|
+
manager?.releaseDeliveryClaim(record.generation);
|
|
13457
|
+
}
|
|
13458
|
+
if (isOwnedCompletionEnvelope(record.ownedCompletion)) {
|
|
13459
|
+
this.#settleOwnedCompletionEnvelope(record.ownedCompletion);
|
|
12360
13460
|
}
|
|
12361
13461
|
}
|
|
12362
|
-
}
|
|
13462
|
+
});
|
|
12363
13463
|
}
|
|
12364
13464
|
|
|
12365
13465
|
#getCustomMessageTextContent(message: Pick<CustomMessage, "content">): string {
|
|
@@ -12403,6 +13503,7 @@ export class AgentSession {
|
|
|
12403
13503
|
triggerTurn?: boolean;
|
|
12404
13504
|
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
12405
13505
|
followUpQueuePolicy?: "respect-mode" | "sequential";
|
|
13506
|
+
steerQueuePolicy?: "respect-mode" | "sequential";
|
|
12406
13507
|
/** INTERNAL trusted option: origin of a hidden next-turn message —
|
|
12407
13508
|
* "turn" (produced by the current turn's continuation machinery,
|
|
12408
13509
|
* purged by a terminal abort) or "external" (background producers,
|
|
@@ -12428,6 +13529,53 @@ export class AgentSession {
|
|
|
12428
13529
|
const epoch = preclaimedUserIntentEpoch ?? this.#claimDeepInterviewUserIntent();
|
|
12429
13530
|
this.#deepInterviewGenuineUserMessageEpochs.set(appMessage, epoch);
|
|
12430
13531
|
}
|
|
13532
|
+
// An explicit steer request routes the same way at every session phase: the
|
|
13533
|
+
// Agent admits it only into a live run, and anything else becomes a
|
|
13534
|
+
// sequential follow-up the next turn delivers. Routing this on the session's
|
|
13535
|
+
// streaming flag instead would strand it during the post-prompt unwind (no
|
|
13536
|
+
// loop owns the queue) and append it without a turn while idle.
|
|
13537
|
+
if (options?.deliverAs === "steer") {
|
|
13538
|
+
// A handoff transition owns the session; a background/custom trigger
|
|
13539
|
+
// (cron, monitor, skill) must not steer against the outgoing turn.
|
|
13540
|
+
this.#assertNoHandoffTransition();
|
|
13541
|
+
const sequential = options.steerQueuePolicy === "sequential" ? { forceOneAtATime: true } : undefined;
|
|
13542
|
+
if (this.agent.steer(appMessage, sequential).admitted) {
|
|
13543
|
+
if (sequential) this.#sequentialSteerMessages.add(appMessage);
|
|
13544
|
+
this.#bindCustomDisplayEntry(appMessage, "steer");
|
|
13545
|
+
} else {
|
|
13546
|
+
// No live run to steer, so this becomes next-turn work. Provenance —
|
|
13547
|
+
// never the caller's `triggerTurn` delivery preference — decides what a
|
|
13548
|
+
// terminal abort's retained fence does with it: genuine user input and
|
|
13549
|
+
// an independent external producer are root requests of their own and
|
|
13550
|
+
// survive; a continuation produced by the aborted turn's own machinery
|
|
13551
|
+
// (preview/plan reminder) is dropped with its removal disposition
|
|
13552
|
+
// rather than leaking into an unrelated later turn.
|
|
13553
|
+
const independentOfTurn = appMessage.attribution === "user" || options.origin === "external";
|
|
13554
|
+
if (this.#isTurnContinuationBlocked() && !independentOfTurn) {
|
|
13555
|
+
const displayTag = readPendingDisplayTag(appMessage.details);
|
|
13556
|
+
if (displayTag !== undefined) {
|
|
13557
|
+
this.#steeringMessages = this.#steeringMessages.filter(entry => entry.tag !== displayTag);
|
|
13558
|
+
this.#followUpMessages = this.#followUpMessages.filter(entry => entry.tag !== displayTag);
|
|
13559
|
+
}
|
|
13560
|
+
this.#settleDeliveredOwnedRegistrations([appMessage]);
|
|
13561
|
+
return;
|
|
13562
|
+
}
|
|
13563
|
+
this.agent.followUp(appMessage, sequential);
|
|
13564
|
+
if (this.#abortUnwind && !sequential) this.#abortUnwindSteerFallbacks.push(appMessage);
|
|
13565
|
+
// The chip now describes follow-up work: keep its mode and identity
|
|
13566
|
+
// aligned with where the executable message actually landed.
|
|
13567
|
+
this.#bindCustomDisplayEntry(appMessage, "followUp");
|
|
13568
|
+
if (independentOfTurn) {
|
|
13569
|
+
this.#externalFollowUps.add(appMessage);
|
|
13570
|
+
// Past a retained terminal fence this message is a NEW root: record
|
|
13571
|
+
// the requirement on the message itself so it survives a deferred
|
|
13572
|
+
// delivery and disappears with the message if it is removed.
|
|
13573
|
+
if (this.#isTurnContinuationBlocked()) this.#freshRootLineageRequests.add(appMessage);
|
|
13574
|
+
}
|
|
13575
|
+
this.#scheduleQueuedDelivery();
|
|
13576
|
+
}
|
|
13577
|
+
return;
|
|
13578
|
+
}
|
|
12431
13579
|
if (this.isStreaming) {
|
|
12432
13580
|
// A handoff transition owns the session; a background/custom trigger (cron,
|
|
12433
13581
|
// monitor, skill) must not steer/follow-up/queue against the outgoing turn
|
|
@@ -12438,14 +13586,15 @@ export class AgentSession {
|
|
|
12438
13586
|
return;
|
|
12439
13587
|
}
|
|
12440
13588
|
|
|
12441
|
-
|
|
12442
|
-
|
|
12443
|
-
|
|
12444
|
-
|
|
12445
|
-
|
|
12446
|
-
|
|
12447
|
-
|
|
12448
|
-
|
|
13589
|
+
this.agent.followUp(
|
|
13590
|
+
appMessage,
|
|
13591
|
+
options?.followUpQueuePolicy === "sequential" ? { forceOneAtATime: true } : undefined,
|
|
13592
|
+
);
|
|
13593
|
+
this.#bindCustomDisplayEntry(appMessage, "followUp");
|
|
13594
|
+
// The session can report streaming while no agent loop owns the queue
|
|
13595
|
+
// (post-prompt unwind): without a scheduled delivery a cron/monitor
|
|
13596
|
+
// notification would sit queued until unrelated activity.
|
|
13597
|
+
if (!this.agent.state.isStreaming) this.#scheduleQueuedDelivery();
|
|
12449
13598
|
return;
|
|
12450
13599
|
}
|
|
12451
13600
|
|
|
@@ -12966,8 +14115,37 @@ export class AgentSession {
|
|
|
12966
14115
|
const fromIndex = queue.findIndex(entry => entry.sequence === sequence);
|
|
12967
14116
|
if (fromIndex === -1) return false;
|
|
12968
14117
|
const toIndex = direction === "up" ? fromIndex - 1 : fromIndex + 1;
|
|
14118
|
+
if (toIndex < 0 || toIndex >= queue.length) return false;
|
|
14119
|
+
// Identity-based reorder, for the same reason removal is: a display index is
|
|
14120
|
+
// NOT an Agent queue index. Hidden `display:false` custom messages occupy
|
|
14121
|
+
// executable slots without a chip, so mapping the visible position straight
|
|
14122
|
+
// onto the Agent queue reorders an unrelated message while the UI moves the
|
|
14123
|
+
// chip. Resolve both endpoints from the bound messages instead.
|
|
14124
|
+
const fromMessage = queue[fromIndex]?.message;
|
|
14125
|
+
const toMessage = queue[toIndex]?.message;
|
|
14126
|
+
const executable = mode === "steer" ? this.agent.snapshotSteering() : this.agent.snapshotFollowUp();
|
|
14127
|
+
const fromDeferred = fromMessage !== undefined && this.#deferredSdkFollowUps.includes(fromMessage);
|
|
14128
|
+
const toDeferred = toMessage !== undefined && this.#deferredSdkFollowUps.includes(toMessage);
|
|
14129
|
+
// A deferred SDK follow-up is held OUTSIDE the Agent queue, so its display
|
|
14130
|
+
// row cannot be reordered against live messages: the two rows live in
|
|
14131
|
+
// different backing stores and no single index describes both. Reorder
|
|
14132
|
+
// within the deferred store when both rows are deferred; refuse a
|
|
14133
|
+
// cross-store move rather than silently moving an unrelated message.
|
|
14134
|
+
if (fromDeferred || toDeferred) {
|
|
14135
|
+
if (!fromDeferred || !toDeferred || fromMessage === undefined || toMessage === undefined) return false;
|
|
14136
|
+
const deferredFrom = this.#deferredSdkFollowUps.indexOf(fromMessage);
|
|
14137
|
+
const deferredTo = this.#deferredSdkFollowUps.indexOf(toMessage);
|
|
14138
|
+
if (deferredFrom === -1 || deferredTo === -1) return false;
|
|
14139
|
+
const [moved] = this.#deferredSdkFollowUps.splice(deferredFrom, 1);
|
|
14140
|
+
if (moved === undefined) return false;
|
|
14141
|
+
this.#deferredSdkFollowUps.splice(deferredTo, 0, moved);
|
|
14142
|
+
return this.#moveQueuedDisplayEntry(queue, fromIndex, toIndex);
|
|
14143
|
+
}
|
|
14144
|
+
const agentFrom = fromMessage === undefined ? fromIndex : executable.indexOf(fromMessage);
|
|
14145
|
+
const agentTo = toMessage === undefined ? toIndex : executable.indexOf(toMessage);
|
|
14146
|
+
if (agentFrom === -1 || agentTo === -1) return false;
|
|
12969
14147
|
const agentMoved =
|
|
12970
|
-
mode === "steer" ? this.agent.moveSteer(
|
|
14148
|
+
mode === "steer" ? this.agent.moveSteer(agentFrom, agentTo) : this.agent.moveFollowUp(agentFrom, agentTo);
|
|
12971
14149
|
if (!agentMoved) return false;
|
|
12972
14150
|
return this.#moveQueuedDisplayEntry(queue, fromIndex, toIndex);
|
|
12973
14151
|
}
|
|
@@ -13327,6 +14505,7 @@ export class AgentSession {
|
|
|
13327
14505
|
| "internal";
|
|
13328
14506
|
silent?: boolean;
|
|
13329
14507
|
sdkRunToken?: string;
|
|
14508
|
+
preserveCompaction?: boolean;
|
|
13330
14509
|
}): void {
|
|
13331
14510
|
const abortGoalState = this.getGoalModeState();
|
|
13332
14511
|
this.#suppressNextGoalReminderAfterAbortGoalId =
|
|
@@ -13343,7 +14522,8 @@ export class AgentSession {
|
|
|
13343
14522
|
this.#promptPreflightAbortController.abort();
|
|
13344
14523
|
this.#promptPreflightAbortController = new AbortController();
|
|
13345
14524
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
13346
|
-
this
|
|
14525
|
+
if (options?.preserveCompaction) this.#autoCompactionAbortController?.abort();
|
|
14526
|
+
else this.abortCompaction();
|
|
13347
14527
|
this.abortHandoff();
|
|
13348
14528
|
this.abortBash();
|
|
13349
14529
|
this.abortEval();
|
|
@@ -13399,6 +14579,7 @@ export class AgentSession {
|
|
|
13399
14579
|
| "tool_abort"
|
|
13400
14580
|
| "internal";
|
|
13401
14581
|
silent?: boolean;
|
|
14582
|
+
preserveCompaction?: boolean;
|
|
13402
14583
|
}): Promise<AbortOutcome> {
|
|
13403
14584
|
// Advance the admission epoch SYNCHRONOUSLY, before any await and before the
|
|
13404
14585
|
// shared-unwind branch. A second abort that piggybacks on the first unwind
|
|
@@ -13415,12 +14596,15 @@ export class AgentSession {
|
|
|
13415
14596
|
this.#promptPreflightCancellationGeneration++;
|
|
13416
14597
|
this.#promptPreflightAbortController.abort();
|
|
13417
14598
|
this.#promptPreflightAbortController = new AbortController();
|
|
14599
|
+
this.#drainTerminalOwnedYieldEntries();
|
|
14600
|
+
if (options?.preserveCompaction !== true) this.abortCompaction();
|
|
14601
|
+
const overlappingPostPromptDrain = this.#cancelPostPromptTasks();
|
|
13418
14602
|
// Abort visibility is per-request: a later real abort must not inherit an
|
|
13419
14603
|
// earlier silent abort's suppression and swallow the user-visible notice.
|
|
13420
14604
|
if (options?.silent !== true) this.#silentAbortPending = false;
|
|
13421
14605
|
// Capture the unwind: the field clears once the first abort settles, and
|
|
13422
14606
|
// the awaits below must keep watching THIS unwind, not a successor's.
|
|
13423
|
-
const sharedUnwind = this.#abortUnwind;
|
|
14607
|
+
const sharedUnwind = Promise.all([this.#abortUnwind, overlappingPostPromptDrain]).then(() => {});
|
|
13424
14608
|
if (options?.timeoutMs !== undefined) {
|
|
13425
14609
|
const timeoutMs = Math.max(0, options.timeoutMs);
|
|
13426
14610
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -13451,6 +14635,17 @@ export class AgentSession {
|
|
|
13451
14635
|
this.#abortUnwind = unwind.promise;
|
|
13452
14636
|
try {
|
|
13453
14637
|
this.#abortOptions(options);
|
|
14638
|
+
// Decide what the run's unconsumed steering becomes BEFORE it ends: a
|
|
14639
|
+
// user interrupt (Esc) hands it to a fresh turn; every other cause
|
|
14640
|
+
// (teardown, session switch, compaction, handoff, tool abort) drops it.
|
|
14641
|
+
// A cancel-and-submit owns its snapshot and re-queues explicitly.
|
|
14642
|
+
if (this.#disownedSteeringDisposition === undefined) {
|
|
14643
|
+
this.#disownedSteeringDisposition =
|
|
14644
|
+
(options?.cause ?? "internal") === "user_interrupt" && !this.#cancelAndSubmitInProgress
|
|
14645
|
+
? { kind: "rearm" }
|
|
14646
|
+
: { kind: "drop" };
|
|
14647
|
+
}
|
|
14648
|
+
this.#drainTerminalOwnedYieldEntries();
|
|
13454
14649
|
const postPromptDrain = this.#cancelPostPromptTasks();
|
|
13455
14650
|
const managedLogicalRunId =
|
|
13456
14651
|
this.#defaultFallbackChain().chain.entries.length > 1 ? this.agent.currentManagedLogicalRunId : undefined;
|
|
@@ -13478,15 +14673,18 @@ export class AgentSession {
|
|
|
13478
14673
|
this.agent.requestRunTerminal(managedLogicalRunId, { stopReason: "cancelled" });
|
|
13479
14674
|
this.#flushPendingBackgroundExchanges();
|
|
13480
14675
|
await this.#awaitAbortedTurnTerminal();
|
|
14676
|
+
this.#finalizeAbortUnwindSteerBatch();
|
|
14677
|
+
// Steering disowned by the aborted run was re-routed as follow-ups
|
|
14678
|
+
// on its agent_end; a user interrupt resumes into them promptly.
|
|
13481
14679
|
if (
|
|
13482
14680
|
!this.#cancelAndSubmitInProgress &&
|
|
13483
14681
|
(options?.cause ?? "internal") === "user_interrupt" &&
|
|
13484
|
-
this.agent.
|
|
14682
|
+
this.agent.hasQueuedMessages()
|
|
13485
14683
|
) {
|
|
13486
14684
|
this.#scheduleAgentContinue({
|
|
13487
14685
|
delayMs: 1,
|
|
13488
14686
|
generation: this.#promptGeneration,
|
|
13489
|
-
shouldContinue: () => this.agent.
|
|
14687
|
+
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
13490
14688
|
rescheduleOnBusy: true,
|
|
13491
14689
|
continueQueuedOnly: true,
|
|
13492
14690
|
});
|
|
@@ -13499,6 +14697,10 @@ export class AgentSession {
|
|
|
13499
14697
|
if (this.#toolChoiceQueue.hasInFlight) this.#toolChoiceQueue.reject("aborted");
|
|
13500
14698
|
}
|
|
13501
14699
|
} finally {
|
|
14700
|
+
// No run was live to end (or its terminal already settled): the
|
|
14701
|
+
// disposition must not leak into an unrelated later run's exit.
|
|
14702
|
+
this.#disownedSteeringDisposition = undefined;
|
|
14703
|
+
this.#abortUnwindSteerFallbacks = [];
|
|
13502
14704
|
this.#abortUnwind = undefined;
|
|
13503
14705
|
this.#abortForceRecoveryStarted = false;
|
|
13504
14706
|
unwind.resolve();
|
|
@@ -13518,6 +14720,7 @@ export class AgentSession {
|
|
|
13518
14720
|
| "tool_abort"
|
|
13519
14721
|
| "internal";
|
|
13520
14722
|
silent?: boolean;
|
|
14723
|
+
preserveCompaction?: boolean;
|
|
13521
14724
|
}): Promise<void> {
|
|
13522
14725
|
const outcome = await this.#abortWithOutcome(options);
|
|
13523
14726
|
if (outcome.kind === "error") throw outcome.cause;
|
|
@@ -13638,10 +14841,6 @@ export class AgentSession {
|
|
|
13638
14841
|
}
|
|
13639
14842
|
> {
|
|
13640
14843
|
let registeredScope: { scopeId: string; abortedAttemptEpoch: number; lineageIdHash: string } | undefined;
|
|
13641
|
-
// The purge snapshot is consumed per-admission inside the terminal
|
|
13642
|
-
// block; the rearm below (reachable only when a scope was registered,
|
|
13643
|
-
// i.e. only after the purge ran) reads the same value.
|
|
13644
|
-
let abortSteeringSnapshot: number | undefined;
|
|
13645
14844
|
if (options.terminal) {
|
|
13646
14845
|
// The terminal scope must be registered for the ABORTED turn's
|
|
13647
14846
|
// lineage, never a successor's. A new root turn advances the attempt
|
|
@@ -13696,10 +14895,10 @@ export class AgentSession {
|
|
|
13696
14895
|
};
|
|
13697
14896
|
}
|
|
13698
14897
|
// Terminal abort blocks STEERING continuations of the aborted turn:
|
|
13699
|
-
//
|
|
13700
|
-
//
|
|
13701
|
-
//
|
|
13702
|
-
//
|
|
14898
|
+
// only a client steer admitted AFTER this abort's admission snapshot
|
|
14899
|
+
// survives; everything else was a continuation of the attempt being
|
|
14900
|
+
// terminated (review thread P2). The follow-up queue is preserved —
|
|
14901
|
+
// owned-completion resumes must still deliver.
|
|
13703
14902
|
const turnSnapshotKey = `${lineageIdHash}:${this.#promptGeneration}`;
|
|
13704
14903
|
const queuedTurnSnapshots = this.#terminalAbortSteeringSnapshots.get(turnSnapshotKey);
|
|
13705
14904
|
const snapshotIndex =
|
|
@@ -13709,39 +14908,31 @@ export class AgentSession {
|
|
|
13709
14908
|
const consumedSnapshot =
|
|
13710
14909
|
queuedTurnSnapshots && snapshotIndex >= 0 ? queuedTurnSnapshots.splice(snapshotIndex, 1)[0] : undefined;
|
|
13711
14910
|
if (consumedSnapshot) this.#terminalAbortSteeringSnapshotKeys.delete(consumedSnapshot.token);
|
|
13712
|
-
|
|
14911
|
+
const snapshotSeq: number = consumedSnapshot?.seq ?? this.#steeringAdmissionSeq;
|
|
13713
14912
|
if (queuedTurnSnapshots?.length === 0) {
|
|
13714
14913
|
this.#terminalAbortSteeringSnapshots.delete(turnSnapshotKey);
|
|
13715
14914
|
}
|
|
13716
|
-
const
|
|
13717
|
-
|
|
13718
|
-
|
|
13719
|
-
//
|
|
13720
|
-
//
|
|
13721
|
-
|
|
13722
|
-
|
|
13723
|
-
|
|
13724
|
-
|
|
13725
|
-
|
|
13726
|
-
|
|
13727
|
-
|
|
13728
|
-
|
|
13729
|
-
|
|
13730
|
-
|
|
13731
|
-
|
|
13732
|
-
|
|
13733
|
-
|
|
13734
|
-
|
|
13735
|
-
|
|
13736
|
-
|
|
13737
|
-
const steeringAfterPurge = new Set(this.agent.snapshotSteering());
|
|
13738
|
-
const keptSteeringDisplays: QueuedDisplayEntry[] = [];
|
|
13739
|
-
for (let displayIndex = 0; displayIndex < steeringBeforePurge.length; displayIndex++) {
|
|
13740
|
-
if (steeringAfterPurge.has(steeringBeforePurge[displayIndex]!)) {
|
|
13741
|
-
keptSteeringDisplays.push(this.#steeringMessages[displayIndex]!);
|
|
13742
|
-
}
|
|
13743
|
-
}
|
|
13744
|
-
this.#steeringMessages = keptSteeringDisplays;
|
|
14915
|
+
const preserveSteering = (message: AgentMessage): boolean =>
|
|
14916
|
+
this.#externalSteerMessages.has(message) &&
|
|
14917
|
+
(this.#externalSteerAdmissionSeq.get(message) ?? 0) > snapshotSeq;
|
|
14918
|
+
// The live run disowns its steering on agent_end; the disposition
|
|
14919
|
+
// installed here is what settles it (drop pre-snapshot, re-route
|
|
14920
|
+
// post-snapshot external steers as a fresh turn). Nothing is
|
|
14921
|
+
// removed from the Agent queue by the session itself.
|
|
14922
|
+
// Overlapping terminal aborts of the same run may settle out of order:
|
|
14923
|
+
// the disposition only ever NARROWS. The highest admission snapshot
|
|
14924
|
+
// wins, and a preserve decision must satisfy every terminal abort that
|
|
14925
|
+
// registered, so a later stricter snapshot cannot be widened by an
|
|
14926
|
+
// older one settling afterwards.
|
|
14927
|
+
const previous = this.#disownedSteeringDisposition;
|
|
14928
|
+
this.#disownedSteeringDisposition =
|
|
14929
|
+
previous?.kind === "terminal"
|
|
14930
|
+
? {
|
|
14931
|
+
kind: "terminal",
|
|
14932
|
+
snapshotSeq: Math.max(previous.snapshotSeq, snapshotSeq),
|
|
14933
|
+
preserve: (message: AgentMessage) => previous.preserve(message) && preserveSteering(message),
|
|
14934
|
+
}
|
|
14935
|
+
: { kind: "terminal", preserve: preserveSteering, snapshotSeq };
|
|
13745
14936
|
|
|
13746
14937
|
// Remove ORDINARY follow-ups queued for the aborted turn: the
|
|
13747
14938
|
// aborted loop exits without polling them, so a later unrelated
|
|
@@ -13796,6 +14987,7 @@ export class AgentSession {
|
|
|
13796
14987
|
}
|
|
13797
14988
|
const aborted = this.#runCancellationDomains.abort(handle);
|
|
13798
14989
|
if (!aborted.ok) {
|
|
14990
|
+
this.#disownedSteeringDisposition = undefined;
|
|
13799
14991
|
if (aborted.reason === "quarantined") {
|
|
13800
14992
|
return {
|
|
13801
14993
|
...(await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: 0 })),
|
|
@@ -13812,6 +15004,9 @@ export class AgentSession {
|
|
|
13812
15004
|
if (handle === this.agent.activeResourceRunId) this.agent.abort();
|
|
13813
15005
|
const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
|
|
13814
15006
|
if (proof.status === "unfenced") this.agent.resourceLedger.quarantine(handle);
|
|
15007
|
+
// The run's agent_end (if any) consumed the disposition; a settled or
|
|
15008
|
+
// already-ended run must not leave it for an unrelated later exit.
|
|
15009
|
+
this.#disownedSteeringDisposition = undefined;
|
|
13815
15010
|
// Rearm surviving owned-completion follow-ups once the abort has
|
|
13816
15011
|
// settled: the aborted loop exits before polling the follow-up queue,
|
|
13817
15012
|
// so preserving the envelopes alone leaves them stranded until an
|
|
@@ -13851,19 +15046,10 @@ export class AgentSession {
|
|
|
13851
15046
|
message =>
|
|
13852
15047
|
ownedCompletionResumeAction(message as never) === "fresh" || this.#externalFollowUps.has(message),
|
|
13853
15048
|
);
|
|
13854
|
-
// A
|
|
13855
|
-
//
|
|
13856
|
-
//
|
|
13857
|
-
|
|
13858
|
-
const hasPreservedSteering = (): boolean =>
|
|
13859
|
-
this.agent
|
|
13860
|
-
.snapshotQueues()
|
|
13861
|
-
.steering.some(
|
|
13862
|
-
message =>
|
|
13863
|
-
this.#externalSteerMessages.has(message) &&
|
|
13864
|
-
(this.#externalSteerAdmissionSeq.get(message) ?? 0) > (abortSteeringSnapshot ?? -1),
|
|
13865
|
-
);
|
|
13866
|
-
if (hasPreservedFollowUp() || hasPreservedSteering()) {
|
|
15049
|
+
// A post-snapshot client steer preserved by the run's disown settlement
|
|
15050
|
+
// is already an external follow-up here (an independent root-turn
|
|
15051
|
+
// request), so one predicate covers both (review thread P1).
|
|
15052
|
+
if (hasPreservedFollowUp()) {
|
|
13867
15053
|
// ANY preserved follow-up or steer — an owned-completion resume,
|
|
13868
15054
|
// an independently requested external follow-up, or a
|
|
13869
15055
|
// post-snapshot client steer — needs a fresh lineage before its
|
|
@@ -13878,7 +15064,7 @@ export class AgentSession {
|
|
|
13878
15064
|
this.#scheduleAgentContinue({
|
|
13879
15065
|
delayMs: 1,
|
|
13880
15066
|
generation: this.#promptGeneration,
|
|
13881
|
-
shouldContinue: () => hasPreservedFollowUp()
|
|
15067
|
+
shouldContinue: () => hasPreservedFollowUp(),
|
|
13882
15068
|
// The rearmed continuation must consume the queued steer inside
|
|
13883
15069
|
// the run acceptance (continueQueuedMessages) rather than
|
|
13884
15070
|
// dequeue it later via getSteeringMessages: a terminal abort
|
|
@@ -13966,9 +15152,14 @@ export class AgentSession {
|
|
|
13966
15152
|
];
|
|
13967
15153
|
};
|
|
13968
15154
|
try {
|
|
15155
|
+
// The aborted run disowns its steering on agent_end; hold it here
|
|
15156
|
+
// (rather than re-routing) because this call owns the snapshot and
|
|
15157
|
+
// re-queues it against the NEW turn below.
|
|
15158
|
+
this.#disownedSteeringDisposition = { kind: "held" };
|
|
13969
15159
|
const outcome = this.#cancelAndSubmitAbortOutcomeProviderForTests
|
|
13970
15160
|
? await this.#cancelAndSubmitAbortOutcomeProviderForTests()
|
|
13971
15161
|
: await this.#abortWithOutcome({ cause: "user_interrupt", timeoutMs: 5_000 });
|
|
15162
|
+
this.#disownedSteeringDisposition = undefined;
|
|
13972
15163
|
if (outcome.kind !== "settled") {
|
|
13973
15164
|
restore();
|
|
13974
15165
|
if (outcome.kind === "error") {
|
|
@@ -14010,18 +15201,29 @@ export class AgentSession {
|
|
|
14010
15201
|
followUp: [...heldFollowUp, ...current.followUp],
|
|
14011
15202
|
});
|
|
14012
15203
|
};
|
|
15204
|
+
// Steers of the aborted turn stay STEERS of the new turn: they are
|
|
15205
|
+
// restored once the new run is accepted (below), so the Agent
|
|
15206
|
+
// admits them into a live run instead of re-labelling them as
|
|
15207
|
+
// follow-ups drained turn after turn. Follow-ups keep their queue.
|
|
15208
|
+
const heldSteering = selected ? [] : queueSnapshot.steering;
|
|
15209
|
+
const heldSteeringDisplays = selected ? [] : steeringDisplaySnapshot;
|
|
14013
15210
|
this.agent.restoreQueues({
|
|
14014
15211
|
steering: [...queuedDuringWindow.steering],
|
|
14015
15212
|
followUp: selected
|
|
14016
15213
|
? [...queuedDuringWindow.followUp]
|
|
14017
|
-
: [...queueSnapshot.
|
|
15214
|
+
: [...queueSnapshot.followUp, ...queuedDuringWindow.followUp],
|
|
14018
15215
|
});
|
|
14019
15216
|
this.#steeringMessages = steeringDisplaysDuringWindow;
|
|
14020
|
-
|
|
14021
|
-
|
|
14022
|
-
|
|
14023
|
-
|
|
14024
|
-
|
|
15217
|
+
// With a selected entry the held (unselected) messages are re-queued
|
|
15218
|
+
// as follow-ups by restoreHeldQueue once the new run is accepted;
|
|
15219
|
+
// mirror them in the display in the same order.
|
|
15220
|
+
this.#followUpMessages = selected
|
|
15221
|
+
? [
|
|
15222
|
+
...steeringDisplaySnapshot.filter(entry => entry !== selected.display),
|
|
15223
|
+
...followUpDisplaySnapshot.filter(entry => entry !== selected.display),
|
|
15224
|
+
...followUpDisplaysDuringWindow,
|
|
15225
|
+
]
|
|
15226
|
+
: [...followUpDisplaySnapshot, ...followUpDisplaysDuringWindow];
|
|
14025
15227
|
const message = selectedMessage ?? {
|
|
14026
15228
|
role: "user" as const,
|
|
14027
15229
|
content: [{ type: "text" as const, text }],
|
|
@@ -14050,8 +15252,19 @@ export class AgentSession {
|
|
|
14050
15252
|
admissionLease: admission,
|
|
14051
15253
|
resetRetryReplaySafety: true,
|
|
14052
15254
|
sdkRunToken: selectedSdkRunToken,
|
|
15255
|
+
skipInitialSteeringPoll: heldSteering.length > 0,
|
|
14053
15256
|
onRunAccepted: () => {
|
|
14054
15257
|
runAccepted = true;
|
|
15258
|
+
if (heldSteering.length > 0) {
|
|
15259
|
+
// Re-admit the aborted turn's steers into the replacement run the
|
|
15260
|
+
// moment it is accepted, with the run's INITIAL poll skipped: the
|
|
15261
|
+
// replacement prompt is answered by the opening model call and the
|
|
15262
|
+
// steers are consumed together at its first turn boundary. Doing
|
|
15263
|
+
// this synchronously at acceptance (rather than from an event
|
|
15264
|
+
// listener) keeps the ordering deterministic.
|
|
15265
|
+
this.agent.restoreSteering(heldSteering);
|
|
15266
|
+
this.#steeringMessages = [...heldSteeringDisplays, ...this.#steeringMessages];
|
|
15267
|
+
}
|
|
14055
15268
|
if (selected) this.#fireQueuedPromotionHooks([message], { startsOwnRun: true });
|
|
14056
15269
|
if (selected) {
|
|
14057
15270
|
this.#steeringMessages = this.#steeringMessages.filter(entry => entry !== selected.display);
|
|
@@ -14080,6 +15293,10 @@ export class AgentSession {
|
|
|
14080
15293
|
} finally {
|
|
14081
15294
|
this.#cancelAndSubmitInProgress = false;
|
|
14082
15295
|
this.#cancelAndSubmitPendingNextTurnDrained = false;
|
|
15296
|
+
// Queue work that arrived (or was re-routed) during the atomic window was
|
|
15297
|
+
// deliberately not scheduled while the flag was set; now that it is
|
|
15298
|
+
// cleared, nothing else owns delivery.
|
|
15299
|
+
if (this.agent.hasQueuedMessages()) this.#scheduleQueuedDelivery(1);
|
|
14083
15300
|
}
|
|
14084
15301
|
}
|
|
14085
15302
|
|
|
@@ -15239,6 +16456,10 @@ export class AgentSession {
|
|
|
15239
16456
|
onBeforeMutation?: () => void;
|
|
15240
16457
|
/** Run inside the selection admission after the durable mutation. */
|
|
15241
16458
|
onAfterMutation?: () => void;
|
|
16459
|
+
/** Test seam: pause after idle drain but before mutation admission. */
|
|
16460
|
+
onBeforeMutationAdmissionForTests?: () => Promise<void>;
|
|
16461
|
+
/** Test seam: pause after mutation admission activates, before its closed-fence recheck. */
|
|
16462
|
+
onAfterMutationAdmissionReadyForTests?: () => Promise<void>;
|
|
15242
16463
|
},
|
|
15243
16464
|
): Promise<DefaultModelSelectionResult> {
|
|
15244
16465
|
// Reserve the causal selection fence synchronously, before credential
|
|
@@ -15254,6 +16475,7 @@ export class AgentSession {
|
|
|
15254
16475
|
throw new Error("Default model selection cannot inherit a thinking level");
|
|
15255
16476
|
}
|
|
15256
16477
|
const expectedSessionId = this.sessionId;
|
|
16478
|
+
const selectionTransaction = Symbol("default-model-selection");
|
|
15257
16479
|
const priorSelectionFence = this.#selectionFenceTail;
|
|
15258
16480
|
const selectionFence = Promise.withResolvers<void>();
|
|
15259
16481
|
this.#selectionFenceGeneration += 1;
|
|
@@ -15289,9 +16511,11 @@ export class AgentSession {
|
|
|
15289
16511
|
},
|
|
15290
16512
|
undefined,
|
|
15291
16513
|
undefined,
|
|
15292
|
-
{ allowDuringClosing: true },
|
|
16514
|
+
{ allowDuringClosing: true, selectionTransaction },
|
|
15293
16515
|
);
|
|
16516
|
+
this.#selectionAwaitingMutationTransaction = selectionTransaction;
|
|
15294
16517
|
await this.waitForIdle(selectionFenceGeneration);
|
|
16518
|
+
await options?.onBeforeMutationAdmissionForTests?.();
|
|
15295
16519
|
return await this.#withSessionAdmission(
|
|
15296
16520
|
"selection",
|
|
15297
16521
|
async () => {
|
|
@@ -15373,9 +16597,16 @@ export class AgentSession {
|
|
|
15373
16597
|
},
|
|
15374
16598
|
undefined,
|
|
15375
16599
|
undefined,
|
|
15376
|
-
{
|
|
16600
|
+
{
|
|
16601
|
+
allowDuringClosing: true,
|
|
16602
|
+
closedSelectionTransaction: selectionTransaction,
|
|
16603
|
+
selectionTransaction,
|
|
16604
|
+
onAfterReadyForTests: options?.onAfterMutationAdmissionReadyForTests,
|
|
16605
|
+
},
|
|
15377
16606
|
);
|
|
15378
16607
|
} finally {
|
|
16608
|
+
if (this.#selectionAwaitingMutationTransaction === selectionTransaction)
|
|
16609
|
+
this.#selectionAwaitingMutationTransaction = undefined;
|
|
15379
16610
|
selectionFence.resolve();
|
|
15380
16611
|
this.#pendingSelectionFences -= 1;
|
|
15381
16612
|
if (this.#oldestPendingSelectionFenceGeneration === selectionFenceGeneration) {
|
|
@@ -16041,13 +17272,13 @@ export class AgentSession {
|
|
|
16041
17272
|
}
|
|
16042
17273
|
|
|
16043
17274
|
/**
|
|
16044
|
-
* Set interrupt
|
|
17275
|
+
* Set the tool interrupt policy (whether a steer aborts in-flight tools).
|
|
16045
17276
|
* Saves to settings.
|
|
16046
17277
|
*/
|
|
16047
|
-
|
|
17278
|
+
setToolInterruptPolicy(policy: "abort_tools" | "finish_tools"): void {
|
|
16048
17279
|
this.#assertDurableSettingsWritable();
|
|
16049
|
-
this.agent.
|
|
16050
|
-
this.settings.set("
|
|
17280
|
+
this.agent.setToolInterruptPolicy(policy);
|
|
17281
|
+
this.settings.set("toolInterruptPolicy", policy);
|
|
16051
17282
|
}
|
|
16052
17283
|
|
|
16053
17284
|
// =========================================================================
|
|
@@ -16434,22 +17665,35 @@ export class AgentSession {
|
|
|
16434
17665
|
* @param options Optional callbacks for completion/error handling
|
|
16435
17666
|
*/
|
|
16436
17667
|
async compact(customInstructions?: string, options?: CompactOptions): Promise<CompactionResult> {
|
|
17668
|
+
this.#assertSessionAdmissionOpen();
|
|
16437
17669
|
// Serialize with every other session-identity transition via the shared lease
|
|
16438
17670
|
// (bidirectional mutual exclusion with handoff/new/switch/branch/clear/fork/
|
|
16439
17671
|
// navigateTree). Released in the outer finally below.
|
|
16440
17672
|
this.#beginSessionTransition("compact");
|
|
17673
|
+
const completion = Promise.withResolvers<void>();
|
|
17674
|
+
this.#compactionCompletion = completion.promise;
|
|
16441
17675
|
try {
|
|
16442
17676
|
if (this.#compactionAbortController) {
|
|
16443
17677
|
throw new Error("Compaction already in progress");
|
|
16444
17678
|
}
|
|
16445
|
-
this.#disconnectFromAgent();
|
|
16446
|
-
await this.abort();
|
|
16447
17679
|
const compactionAbortController = new AbortController();
|
|
16448
17680
|
this.#compactionAbortController = compactionAbortController;
|
|
16449
|
-
|
|
16450
|
-
|
|
16451
|
-
|
|
17681
|
+
this.#disconnectFromAgent();
|
|
17682
|
+
try {
|
|
17683
|
+
await this.abort({ cause: "compaction", preserveCompaction: true });
|
|
17684
|
+
} catch (error) {
|
|
17685
|
+
this.#compactionAbortController = undefined;
|
|
17686
|
+
throw error;
|
|
17687
|
+
}
|
|
17688
|
+
await this.#waitForAutoCompactionCompletions();
|
|
16452
17689
|
try {
|
|
17690
|
+
if (compactionAbortController.signal.aborted) {
|
|
17691
|
+
throw new CompactionCancelledError();
|
|
17692
|
+
}
|
|
17693
|
+
// Take this invocation's state snapshot for the summarizer context.
|
|
17694
|
+
const compactionStateSnapshot = await this.#compactionStateSnapshot({
|
|
17695
|
+
trackWorkflowRecoveryProgress: true,
|
|
17696
|
+
});
|
|
16453
17697
|
if (!this.model) {
|
|
16454
17698
|
throw new Error("No model selected");
|
|
16455
17699
|
}
|
|
@@ -16586,9 +17830,11 @@ export class AgentSession {
|
|
|
16586
17830
|
if (this.#compactionAbortController === compactionAbortController) {
|
|
16587
17831
|
this.#compactionAbortController = undefined;
|
|
16588
17832
|
}
|
|
16589
|
-
this.#reconnectToAgent();
|
|
17833
|
+
if (!this.#isDisposed && !this.#sessionAdmissionClosing) this.#reconnectToAgent();
|
|
16590
17834
|
}
|
|
16591
17835
|
} finally {
|
|
17836
|
+
completion.resolve();
|
|
17837
|
+
if (this.#compactionCompletion === completion.promise) this.#compactionCompletion = undefined;
|
|
16592
17838
|
this.#endSessionTransition();
|
|
16593
17839
|
}
|
|
16594
17840
|
}
|
|
@@ -16650,9 +17896,21 @@ export class AgentSession {
|
|
|
16650
17896
|
}
|
|
16651
17897
|
}
|
|
16652
17898
|
|
|
17899
|
+
/**
|
|
17900
|
+
* Wait for every automatic compaction producer currently unwinding. An auto
|
|
17901
|
+
* compaction owns the session history through its post-append rewrite and
|
|
17902
|
+
* `session_compact` delivery, so cancelling its signal is not sufficient to
|
|
17903
|
+
* make that work safe to overlap with a manual compaction.
|
|
17904
|
+
*/
|
|
17905
|
+
async #waitForAutoCompactionCompletions(): Promise<void> {
|
|
17906
|
+
while (this.#autoCompactionCompletions.size > 0) {
|
|
17907
|
+
await Promise.allSettled([...this.#autoCompactionCompletions]);
|
|
17908
|
+
}
|
|
17909
|
+
}
|
|
17910
|
+
|
|
16653
17911
|
/** Trigger idle compaction through the auto-compaction flow (with UI events). */
|
|
16654
17912
|
async runIdleCompaction(): Promise<void> {
|
|
16655
|
-
if (this.isStreaming || this.isCompacting) return;
|
|
17913
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing || this.isStreaming || this.isCompacting) return;
|
|
16656
17914
|
// Do not start idle compaction while a handoff transition owns the session.
|
|
16657
17915
|
if (this.isGeneratingHandoff || this.#handoffTransitionActive) return;
|
|
16658
17916
|
await this.#runAutoCompaction("idle", false, true);
|
|
@@ -18500,7 +19758,27 @@ export class AgentSession {
|
|
|
18500
19758
|
/**
|
|
18501
19759
|
* Internal: Run auto-compaction with events.
|
|
18502
19760
|
*/
|
|
18503
|
-
|
|
19761
|
+
#runAutoCompaction(
|
|
19762
|
+
reason: "overflow" | "threshold" | "idle",
|
|
19763
|
+
willRetry: boolean,
|
|
19764
|
+
deferred = false,
|
|
19765
|
+
options?: {
|
|
19766
|
+
continueAfterMaintenance?: boolean;
|
|
19767
|
+
deferHandoffMaintenance?: boolean;
|
|
19768
|
+
force?: boolean;
|
|
19769
|
+
signal?: AbortSignal;
|
|
19770
|
+
beforeTerminalOverflowNoop?: () => void;
|
|
19771
|
+
resourceRunId?: string;
|
|
19772
|
+
},
|
|
19773
|
+
): Promise<AutoCompactionTerminalStatus> {
|
|
19774
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing) return Promise.resolve({ kind: "skipped" });
|
|
19775
|
+
const completion = this.#runAutoCompactionImpl(reason, willRetry, deferred, options);
|
|
19776
|
+
this.#autoCompactionCompletions.add(completion);
|
|
19777
|
+
void completion.finally(() => this.#autoCompactionCompletions.delete(completion)).catch(() => {});
|
|
19778
|
+
return completion;
|
|
19779
|
+
}
|
|
19780
|
+
|
|
19781
|
+
async #runAutoCompactionImpl(
|
|
18504
19782
|
reason: "overflow" | "threshold" | "idle",
|
|
18505
19783
|
willRetry: boolean,
|
|
18506
19784
|
deferred = false,
|
|
@@ -19493,11 +20771,15 @@ export class AgentSession {
|
|
|
19493
20771
|
}
|
|
19494
20772
|
|
|
19495
20773
|
async #handleManagedAttemptOutcome(outcome: ManagedAttemptOutcome): Promise<ManagedAttemptDecision> {
|
|
20774
|
+
const attemptAbortEpoch = this.#abortAdmissionEpoch;
|
|
20775
|
+
const attemptCancelled = () =>
|
|
20776
|
+
this.#isDisposed || this.#sessionAdmissionClosing || this.#abortAdmissionEpoch !== attemptAbortEpoch;
|
|
19496
20777
|
const activePromptHandle = this.activePromptHandle;
|
|
19497
20778
|
const cancellationSignal = activePromptHandle
|
|
19498
20779
|
? this.#runCancellationDomains.lookup(activePromptHandle)?.signal
|
|
19499
20780
|
: undefined;
|
|
19500
|
-
if (cancellationSignal?.aborted)
|
|
20781
|
+
if (attemptCancelled() || cancellationSignal?.aborted)
|
|
20782
|
+
return { type: "terminal", terminal: { stopReason: "cancelled" } };
|
|
19501
20783
|
if (outcome.type === "run_terminal") {
|
|
19502
20784
|
this.#defaultFallbackChain().resetAttemptBudget();
|
|
19503
20785
|
return { type: "terminal", terminal: { stopReason: outcome.reason } };
|
|
@@ -19533,7 +20815,6 @@ export class AgentSession {
|
|
|
19533
20815
|
const controllerStateBeforeFailure = controller.snapshotRuntimeState();
|
|
19534
20816
|
const advanced = controller.recordEscapedArgumentsFailure(errorMessage, false);
|
|
19535
20817
|
this.#escapedNonAsciiManagedRetries = 0;
|
|
19536
|
-
const abortEpoch = this.#abortAdmissionEpoch;
|
|
19537
20818
|
if (advanced) {
|
|
19538
20819
|
const transitionGeneration = ++this.#fallbackTransitionGeneration;
|
|
19539
20820
|
return {
|
|
@@ -19554,7 +20835,9 @@ export class AgentSession {
|
|
|
19554
20835
|
!ownership.isCurrent() ||
|
|
19555
20836
|
ownership.lease.signal.aborted ||
|
|
19556
20837
|
cancellationSignal?.aborted ||
|
|
19557
|
-
this.#
|
|
20838
|
+
this.#isDisposed ||
|
|
20839
|
+
this.#sessionAdmissionClosing ||
|
|
20840
|
+
this.#abortAdmissionEpoch !== attemptAbortEpoch ||
|
|
19558
20841
|
this.#fallbackTransitionGeneration !== transitionGeneration;
|
|
19559
20842
|
if (continuationCancelled) {
|
|
19560
20843
|
await restoreOwnedTransition();
|
|
@@ -19579,7 +20862,7 @@ export class AgentSession {
|
|
|
19579
20862
|
"escaped_non_ascii",
|
|
19580
20863
|
1,
|
|
19581
20864
|
cancellationSignal,
|
|
19582
|
-
|
|
20865
|
+
attemptAbortEpoch,
|
|
19583
20866
|
controllerStateBeforeFailure,
|
|
19584
20867
|
previousModel,
|
|
19585
20868
|
previousThinkingLevel,
|
|
@@ -19588,7 +20871,9 @@ export class AgentSession {
|
|
|
19588
20871
|
);
|
|
19589
20872
|
if (!switched) {
|
|
19590
20873
|
if (
|
|
19591
|
-
this.#
|
|
20874
|
+
this.#isDisposed ||
|
|
20875
|
+
this.#sessionAdmissionClosing ||
|
|
20876
|
+
this.#abortAdmissionEpoch !== attemptAbortEpoch ||
|
|
19592
20877
|
cancellationSignal?.aborted ||
|
|
19593
20878
|
this.#fallbackTransitionGeneration !== transitionGeneration
|
|
19594
20879
|
) {
|
|
@@ -19608,6 +20893,15 @@ export class AgentSession {
|
|
|
19608
20893
|
});
|
|
19609
20894
|
return;
|
|
19610
20895
|
}
|
|
20896
|
+
if (
|
|
20897
|
+
attemptCancelled() ||
|
|
20898
|
+
!ownership.isCurrent() ||
|
|
20899
|
+
ownership.lease.signal.aborted ||
|
|
20900
|
+
this.#fallbackTransitionGeneration !== transitionGeneration
|
|
20901
|
+
) {
|
|
20902
|
+
await restoreOwnedTransition();
|
|
20903
|
+
return;
|
|
20904
|
+
}
|
|
19611
20905
|
const continuation = this.agent.continue({
|
|
19612
20906
|
...this.#managedFallbackPromptOptions(),
|
|
19613
20907
|
transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
|
|
@@ -19617,7 +20911,9 @@ export class AgentSession {
|
|
|
19617
20911
|
});
|
|
19618
20912
|
if (
|
|
19619
20913
|
cancellationSignal?.aborted ||
|
|
19620
|
-
this.#
|
|
20914
|
+
this.#isDisposed ||
|
|
20915
|
+
this.#sessionAdmissionClosing ||
|
|
20916
|
+
this.#abortAdmissionEpoch !== attemptAbortEpoch ||
|
|
19621
20917
|
this.#fallbackTransitionGeneration !== transitionGeneration
|
|
19622
20918
|
) {
|
|
19623
20919
|
await restoreOwnedTransition();
|
|
@@ -19636,7 +20932,7 @@ export class AgentSession {
|
|
|
19636
20932
|
};
|
|
19637
20933
|
}
|
|
19638
20934
|
controller.advance();
|
|
19639
|
-
if (
|
|
20935
|
+
if (attemptCancelled() || cancellationSignal?.aborted)
|
|
19640
20936
|
return { type: "terminal", terminal: { stopReason: "cancelled" } };
|
|
19641
20937
|
this.#defaultFallbackExhaustedLastTurn = true;
|
|
19642
20938
|
return this.#managedFallbackExhaustionDecision(
|
|
@@ -19648,7 +20944,7 @@ export class AgentSession {
|
|
|
19648
20944
|
return {
|
|
19649
20945
|
type: "retry",
|
|
19650
20946
|
continuation: async ownership => {
|
|
19651
|
-
if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
|
|
20947
|
+
if (attemptCancelled() || !ownership.isCurrent() || ownership.lease.signal.aborted) return;
|
|
19652
20948
|
await this.agent.continue({
|
|
19653
20949
|
...this.#managedFallbackPromptOptions(),
|
|
19654
20950
|
transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
|
|
@@ -19667,7 +20963,7 @@ export class AgentSession {
|
|
|
19667
20963
|
return {
|
|
19668
20964
|
type: "maintenance",
|
|
19669
20965
|
continuation: async ownership => {
|
|
19670
|
-
if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
|
|
20966
|
+
if (attemptCancelled() || !ownership.isCurrent() || ownership.lease.signal.aborted) return;
|
|
19671
20967
|
const resourceRunId = String(ownership.logicalRunId);
|
|
19672
20968
|
const previousLease = this.#postPromptLeases.get(resourceRunId);
|
|
19673
20969
|
this.#postPromptLeases.set(resourceRunId, ownership.lease);
|
|
@@ -19686,7 +20982,13 @@ export class AgentSession {
|
|
|
19686
20982
|
resourceRunId,
|
|
19687
20983
|
ownership.lease.signal,
|
|
19688
20984
|
);
|
|
19689
|
-
if (
|
|
20985
|
+
if (
|
|
20986
|
+
terminalized ||
|
|
20987
|
+
successorScheduled ||
|
|
20988
|
+
attemptCancelled() ||
|
|
20989
|
+
!ownership.isCurrent() ||
|
|
20990
|
+
ownership.lease.signal.aborted
|
|
20991
|
+
)
|
|
19690
20992
|
return;
|
|
19691
20993
|
this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
|
|
19692
20994
|
stopReason: "error",
|
|
@@ -20129,6 +21431,9 @@ export class AgentSession {
|
|
|
20129
21431
|
scope?: AttemptScope,
|
|
20130
21432
|
scopeWasClean = this.#isRetryScopeClean(scope),
|
|
20131
21433
|
): Promise<boolean | ManagedAttemptDecision> {
|
|
21434
|
+
const retryAbortEpoch = this.#abortAdmissionEpoch;
|
|
21435
|
+
const retryCancelled = () =>
|
|
21436
|
+
this.#isDisposed || this.#sessionAdmissionClosing || this.#abortAdmissionEpoch !== retryAbortEpoch;
|
|
20132
21437
|
const controller = this.#defaultFallbackChain();
|
|
20133
21438
|
const managedFallback = controller.chain.entries.length > 1;
|
|
20134
21439
|
const retrySettings = this.settings.getGroup("retry");
|
|
@@ -20140,6 +21445,9 @@ export class AgentSession {
|
|
|
20140
21445
|
const classification = this.#classifyErrorForRetry(message);
|
|
20141
21446
|
const localSnapshot = classification === "local_snapshot";
|
|
20142
21447
|
const localBufferOverflow = classification === "local_buffer_overflow";
|
|
21448
|
+
if (retryCancelled()) {
|
|
21449
|
+
return managedOutcome ? { type: "terminal", terminal: { stopReason: "cancelled" } } : false;
|
|
21450
|
+
}
|
|
20143
21451
|
// A local machinery failure must never stay charged against the provider
|
|
20144
21452
|
// fallback budget, no matter which local exit follows (disabled retry,
|
|
20145
21453
|
// visible-content surface, bounded retry, exhaustion, or the immediate
|
|
@@ -20402,7 +21710,7 @@ export class AgentSession {
|
|
|
20402
21710
|
ownership?.domain.signal ??
|
|
20403
21711
|
(activePromptHandle ? this.#runCancellationDomains.lookup(activePromptHandle)?.signal : undefined);
|
|
20404
21712
|
if (ownership && (!ownership.isCurrent() || cancellationSignal?.aborted)) return;
|
|
20405
|
-
|
|
21713
|
+
if (retryCancelled()) return;
|
|
20406
21714
|
let quotaPoolExhausted = false;
|
|
20407
21715
|
if (managedFallback && !credentialRotated && !providerRetryCeilingReached) {
|
|
20408
21716
|
const mark = await this.#markFailedCredential(trigger);
|
|
@@ -20436,7 +21744,7 @@ export class AgentSession {
|
|
|
20436
21744
|
trigger.class,
|
|
20437
21745
|
attemptsUsed,
|
|
20438
21746
|
cancellationSignal,
|
|
20439
|
-
|
|
21747
|
+
retryAbortEpoch,
|
|
20440
21748
|
controllerStateBeforeAdvance,
|
|
20441
21749
|
previousModel,
|
|
20442
21750
|
previousThinkingLevel,
|
|
@@ -20446,7 +21754,7 @@ export class AgentSession {
|
|
|
20446
21754
|
}
|
|
20447
21755
|
}
|
|
20448
21756
|
if (ownership && (!ownership.isCurrent() || cancellationSignal?.aborted)) {
|
|
20449
|
-
if (
|
|
21757
|
+
if (!retryCancelled()) {
|
|
20450
21758
|
await this.#restoreDefaultFallbackTransition(
|
|
20451
21759
|
controller,
|
|
20452
21760
|
controllerStateBeforeAdvance,
|
|
@@ -20456,7 +21764,7 @@ export class AgentSession {
|
|
|
20456
21764
|
}
|
|
20457
21765
|
return;
|
|
20458
21766
|
}
|
|
20459
|
-
if (
|
|
21767
|
+
if (retryCancelled()) {
|
|
20460
21768
|
controller.resetForNewTurn();
|
|
20461
21769
|
return;
|
|
20462
21770
|
}
|
|
@@ -20585,8 +21893,9 @@ export class AgentSession {
|
|
|
20585
21893
|
|
|
20586
21894
|
if (managedOutcome) {
|
|
20587
21895
|
try {
|
|
21896
|
+
if (retryCancelled() || ownershipCancelled()) return;
|
|
20588
21897
|
await this.#checkEstimatedContextBeforePrompt();
|
|
20589
|
-
if (ownershipCancelled()) {
|
|
21898
|
+
if (retryCancelled() || ownershipCancelled()) {
|
|
20590
21899
|
const attempt = this.#retryAttempt;
|
|
20591
21900
|
this.#retryAttempt = 0;
|
|
20592
21901
|
await this.#emitSessionEvent({
|
|
@@ -20687,6 +21996,7 @@ export class AgentSession {
|
|
|
20687
21996
|
toolChoice?: ToolChoice;
|
|
20688
21997
|
sdkRunToken?: string;
|
|
20689
21998
|
fallbackManaged?: boolean;
|
|
21999
|
+
skipInitialSteeringPoll?: boolean;
|
|
20690
22000
|
onRunAccepted?: (handle: AttemptRunHandle) => void;
|
|
20691
22001
|
},
|
|
20692
22002
|
predecessorAgentEndHold?: symbol,
|