@gajae-code/coding-agent 0.15.5 → 0.16.0
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 +85 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +120 -0
- package/dist/types/config/file-lock.d.ts +54 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +17 -5
- package/dist/types/config/settings-schema.d.ts +19 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -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/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- 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/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- 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/jobs-observer.d.ts +36 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +80 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -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/state-version.d.ts +4 -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/index.d.ts +6 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- 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 +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- 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/host.d.ts +10 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +67 -1
- 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 +2 -0
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +24 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -5
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +9 -3
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +671 -39
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +4 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +117 -16
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/launch.ts +86 -18
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +44 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +890 -99
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2795 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1044 -122
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +19 -1
- package/src/config/settings.ts +11 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +592 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +111 -42
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +1321 -147
- package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +66 -4
- 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 +616 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/oauth-selector.ts +6 -1
- 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 +29 -10
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +51 -6
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +1671 -34
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +217 -46
- 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 +548 -0
- package/src/sdk/broker/state-version.ts +26 -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 +46 -5
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- 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 +11 -8
- package/src/sdk/cli/session-cli.ts +276 -6
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +101 -37
- 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/host.ts +27 -2
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +581 -43
- package/src/sdk/host/session-runtime.ts +655 -101
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +53 -2
- package/src/sdk/protocol/operation-registry.ts +17 -4
- package/src/sdk/router/session-router.ts +294 -154
- package/src/sdk/session.ts +440 -167
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +1292 -180
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +342 -28
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +1268 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +1028 -109
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +11 -5
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -117,6 +117,7 @@ import {
|
|
|
117
117
|
modelsAreEqual,
|
|
118
118
|
streamSimple,
|
|
119
119
|
} from "@gajae-code/ai/core";
|
|
120
|
+
import { normalizeAnthropicBaseUrl } from "@gajae-code/ai/providers/anthropic";
|
|
120
121
|
import {
|
|
121
122
|
type AuthDisposition,
|
|
122
123
|
beginAttempt,
|
|
@@ -181,8 +182,10 @@ export interface ForkContextSeedOptions {
|
|
|
181
182
|
}
|
|
182
183
|
|
|
183
184
|
import type { AuthCredentialSelector } from "@gajae-code/ai/core";
|
|
185
|
+
import { isFoundryEnabled } from "@gajae-code/ai/utils/foundry";
|
|
184
186
|
import type { MacOSPowerAssertion } from "@gajae-code/natives";
|
|
185
187
|
import {
|
|
188
|
+
$pickCredentialEnv,
|
|
186
189
|
extractRetryHint,
|
|
187
190
|
hasFsCode,
|
|
188
191
|
isEacces,
|
|
@@ -312,9 +315,16 @@ import {
|
|
|
312
315
|
} from "../gjc-runtime/session-layout";
|
|
313
316
|
import {
|
|
314
317
|
type CoordinatorToolObservation,
|
|
318
|
+
clearCoordinatorRuntimeStateRescope,
|
|
319
|
+
hasCoordinatorRuntimeStateRescopeJournal,
|
|
320
|
+
markCoordinatorRuntimeStateRescopePublishing,
|
|
315
321
|
ownerTerminalContextFromEnvironment,
|
|
316
322
|
persistCoordinatorRuntimeStateFromEvent,
|
|
323
|
+
persistCoordinatorWorkerIntegrationOutcome,
|
|
324
|
+
prepareCoordinatorRuntimeStateRescope,
|
|
325
|
+
recoverCoordinatorRuntimeStateRescope,
|
|
317
326
|
registerCoordinatorRuntimeStateFinalizer,
|
|
327
|
+
relocateCoordinatorRuntimeStateForRescope,
|
|
318
328
|
UNPROVEN_TOOL_LABEL,
|
|
319
329
|
} from "../gjc-runtime/session-state-sidecar";
|
|
320
330
|
import {
|
|
@@ -443,6 +453,7 @@ import {
|
|
|
443
453
|
} from "./contribution-prep";
|
|
444
454
|
import { canonicalCoordinatorToolLabel } from "./coordinator-tool-label";
|
|
445
455
|
import { pruneStaleFileMentions } from "./file-mention-pruning";
|
|
456
|
+
import { describeFoldReceipt, FOLD_WAKE_MERGE_WINDOW_MS, type FoldAdapter, FoldCoordinator } from "./fold-coordinator";
|
|
446
457
|
import type { MemoryGuardRestoreResult } from "./memory-guard-checkpoint-participant";
|
|
447
458
|
import {
|
|
448
459
|
type BashExecutionMessage,
|
|
@@ -457,6 +468,7 @@ import {
|
|
|
457
468
|
SKILL_PROMPT_MESSAGE_TYPE,
|
|
458
469
|
} from "./messages";
|
|
459
470
|
import { isLegacyProviderSafetyStopMessage } from "./provider-safety-stop";
|
|
471
|
+
import { readSdkRunCapability } from "./sdk-run-capability";
|
|
460
472
|
import { formatSessionDumpText } from "./session-dump-format";
|
|
461
473
|
import type {
|
|
462
474
|
BranchSummaryEntry,
|
|
@@ -471,7 +483,6 @@ import type {
|
|
|
471
483
|
SessionManagerCloseOutcome,
|
|
472
484
|
SessionMemoryStats,
|
|
473
485
|
} from "./session-manager";
|
|
474
|
-
|
|
475
486
|
import {
|
|
476
487
|
createReadonlySessionManager,
|
|
477
488
|
getLatestCompactionEntry,
|
|
@@ -488,6 +499,8 @@ import {
|
|
|
488
499
|
bindToolLineage,
|
|
489
500
|
classifyOwnedEnvelope,
|
|
490
501
|
isOwnedCompletionEnvelope,
|
|
502
|
+
isOwnedCompletionEnvelopeAllowed,
|
|
503
|
+
lookupOwnedRegistration,
|
|
491
504
|
lookupTerminalScope,
|
|
492
505
|
mintTurnLineageIdHash,
|
|
493
506
|
type OwnedCompletionEnvelope,
|
|
@@ -496,11 +509,35 @@ import {
|
|
|
496
509
|
settleToolLineageRegistrationWindow,
|
|
497
510
|
unregisterOwnedRegistration,
|
|
498
511
|
} from "./terminal-abort";
|
|
499
|
-
|
|
500
512
|
import { ToolChoiceQueue } from "./tool-choice-queue";
|
|
501
513
|
import { pruneSupersededMaintenanceReminders, pruneSupersededVolatileProjectContext } from "./volatile-context-pruning";
|
|
502
514
|
import { YieldQueue } from "./yield-queue";
|
|
503
515
|
|
|
516
|
+
const ASYNC_INLINE_RESULT_MAX_CHARS = 12_000;
|
|
517
|
+
const ASYNC_PREVIEW_MAX_CHARS = 4_000;
|
|
518
|
+
|
|
519
|
+
async function formatParkedAsyncResult(
|
|
520
|
+
sessionManager: SessionManager,
|
|
521
|
+
result: string,
|
|
522
|
+
allowArtifact = true,
|
|
523
|
+
): Promise<string> {
|
|
524
|
+
if (result.length <= ASYNC_INLINE_RESULT_MAX_CHARS) return result;
|
|
525
|
+
const preview = `${result.slice(0, ASYNC_PREVIEW_MAX_CHARS)}\n\n[Output truncated. Showing first ${ASYNC_PREVIEW_MAX_CHARS.toLocaleString()} characters.]`;
|
|
526
|
+
try {
|
|
527
|
+
if (!allowArtifact) return preview;
|
|
528
|
+
const { path: artifactPath, id: artifactId } = await sessionManager.allocateArtifactPath("async");
|
|
529
|
+
if (artifactPath && artifactId) {
|
|
530
|
+
await Bun.write(artifactPath, result);
|
|
531
|
+
return `${preview}\nFull output: artifact://${artifactId}`;
|
|
532
|
+
}
|
|
533
|
+
} catch (error) {
|
|
534
|
+
logger.warn("Failed to persist parked async follow-up artifact", {
|
|
535
|
+
error: error instanceof Error ? error.message : String(error),
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
return preview;
|
|
539
|
+
}
|
|
540
|
+
|
|
504
541
|
/**
|
|
505
542
|
* #4560: structured workflow recovery projection from canonical durable
|
|
506
543
|
* Ralplan/Ultragoal state, consumed by the compaction summary context and
|
|
@@ -1048,10 +1085,23 @@ export interface PromptOptions {
|
|
|
1048
1085
|
onSkillPrepared?: (meta: { name: string; path: string; lineCount?: number; cleanedArgs?: string }) => void;
|
|
1049
1086
|
/** Optional invocation-scoped cancellation fence used before an accepted skill starts execution. */
|
|
1050
1087
|
preflightSignal?: AbortSignal;
|
|
1051
|
-
/** Internal
|
|
1052
|
-
|
|
1088
|
+
/** Internal host capability; ordinary package callers cannot construct the branded value. */
|
|
1089
|
+
sdkRunCapability?: unknown;
|
|
1053
1090
|
}
|
|
1054
1091
|
|
|
1092
|
+
type InternalPromptOptions = PromptOptions & { sdkRunToken?: string };
|
|
1093
|
+
type InternalCustomMessageOptions = Pick<
|
|
1094
|
+
PromptOptions,
|
|
1095
|
+
| "streamingBehavior"
|
|
1096
|
+
| "toolChoice"
|
|
1097
|
+
| "followUpQueuePolicy"
|
|
1098
|
+
| "onPreflightAccepted"
|
|
1099
|
+
| "onPreflightAcceptCommit"
|
|
1100
|
+
| "preflightSignal"
|
|
1101
|
+
> & {
|
|
1102
|
+
sdkRunToken?: string;
|
|
1103
|
+
};
|
|
1104
|
+
|
|
1055
1105
|
function promptPreflightCancelledError(): Error {
|
|
1056
1106
|
const error = Object.assign(new Error("Prompt preflight was cancelled before execution."), { code: "busy" });
|
|
1057
1107
|
error.name = "PromptPreflightCancelledError";
|
|
@@ -1626,14 +1676,44 @@ function buildSessionMetadata(
|
|
|
1626
1676
|
provider: string,
|
|
1627
1677
|
authStorage: AuthStorage | undefined,
|
|
1628
1678
|
credentialSessionId = sessionId,
|
|
1679
|
+
model?: Model,
|
|
1680
|
+
owner?: object,
|
|
1629
1681
|
): Record<string, unknown> {
|
|
1630
1682
|
const userId: Record<string, string> = { session_id: sessionId };
|
|
1631
1683
|
// Only look up account_uuid when the request is going to Anthropic. Injecting
|
|
1632
1684
|
// a Anthropic model OAuth account_uuid into requests bound for other providers (including
|
|
1633
1685
|
// Anthropic-format-compatible proxies like cloudflare-ai-gateway or gitlab-duo)
|
|
1634
1686
|
// would leak the user's Anthropic identity to unrelated third-party APIs.
|
|
1635
|
-
|
|
1636
|
-
|
|
1687
|
+
const effectiveBaseUrl =
|
|
1688
|
+
model?.provider === "anthropic"
|
|
1689
|
+
? (normalizeAnthropicBaseUrl(
|
|
1690
|
+
(isFoundryEnabled() ? $pickCredentialEnv("FOUNDRY_BASE_URL") : undefined) ?? model.baseUrl,
|
|
1691
|
+
) ?? "https://api.anthropic.com")
|
|
1692
|
+
: undefined;
|
|
1693
|
+
let officialAnthropicEndpoint = false;
|
|
1694
|
+
if (provider === "anthropic" && model?.api === "anthropic-messages") {
|
|
1695
|
+
try {
|
|
1696
|
+
const candidate = effectiveBaseUrl ?? "https://api.anthropic.com";
|
|
1697
|
+
const url = new URL(candidate);
|
|
1698
|
+
const authority = candidate.match(/^[a-z][a-z\d+.-]*:\/\/([^/?#]*)/iu)?.[1];
|
|
1699
|
+
officialAnthropicEndpoint =
|
|
1700
|
+
url.protocol === "https:" &&
|
|
1701
|
+
url.hostname === "api.anthropic.com" &&
|
|
1702
|
+
authority?.toLowerCase() === "api.anthropic.com" &&
|
|
1703
|
+
!url.username &&
|
|
1704
|
+
!url.password &&
|
|
1705
|
+
!url.search &&
|
|
1706
|
+
!url.hash &&
|
|
1707
|
+
(url.pathname === "/" || url.pathname === "/v1" || url.pathname === "/v1/");
|
|
1708
|
+
} catch {
|
|
1709
|
+
officialAnthropicEndpoint = false;
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
// Without the registry owner, do not query shared auth storage: an unscoped
|
|
1713
|
+
// lookup can attribute a registry-scoped API-key request to another OAuth
|
|
1714
|
+
// account. Compatibility facades therefore retain session_id-only metadata.
|
|
1715
|
+
if (officialAnthropicEndpoint && owner) {
|
|
1716
|
+
const accountUuid = authStorage?.getOAuthAccountId("anthropic", credentialSessionId, { owner });
|
|
1637
1717
|
if (typeof accountUuid === "string" && accountUuid.length > 0) {
|
|
1638
1718
|
userId.account_uuid = accountUuid;
|
|
1639
1719
|
// Derive device_id from account_uuid so the payload matches the real CC
|
|
@@ -1914,9 +1994,24 @@ export type BeforeAgentStartContributor = (event: {
|
|
|
1914
1994
|
}) => Promise<BeforeAgentStartInternalMessage | undefined>;
|
|
1915
1995
|
|
|
1916
1996
|
const AGENT_END_WORKER_INTEGRATION_TIMEOUT_MS = 5_000;
|
|
1997
|
+
const POST_PUBLICATION_ERROR_MAX_BYTES = 512;
|
|
1998
|
+
|
|
1999
|
+
export type WorkerIntegrationOutcome =
|
|
2000
|
+
| { status: "completed" }
|
|
2001
|
+
| { status: "failed"; error: string }
|
|
2002
|
+
| { status: "timed_out" };
|
|
2003
|
+
|
|
2004
|
+
function sanitizePostPublicationError(error: string | undefined): string | undefined {
|
|
2005
|
+
if (error === undefined) return undefined;
|
|
2006
|
+
const normalized = error
|
|
2007
|
+
.replace(/[\u0000-\u001f\u007f]/gu, " ")
|
|
2008
|
+
.replace(/\s+/gu, " ")
|
|
2009
|
+
.trim();
|
|
2010
|
+
return truncateHeadBytes(normalized || "unknown worker integration error", POST_PUBLICATION_ERROR_MAX_BYTES).text;
|
|
2011
|
+
}
|
|
1917
2012
|
|
|
1918
2013
|
export class WorkerIntegrationRequestScheduler {
|
|
1919
|
-
#inFlight: Promise<
|
|
2014
|
+
#inFlight: Promise<WorkerIntegrationOutcome> | undefined = undefined;
|
|
1920
2015
|
#pending = false;
|
|
1921
2016
|
|
|
1922
2017
|
constructor(
|
|
@@ -1933,9 +2028,15 @@ export class WorkerIntegrationRequestScheduler {
|
|
|
1933
2028
|
}
|
|
1934
2029
|
|
|
1935
2030
|
async flush(): Promise<void> {
|
|
2031
|
+
await this.flushWithOutcome();
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
async flushWithOutcome(): Promise<WorkerIntegrationOutcome> {
|
|
2035
|
+
let outcome: WorkerIntegrationOutcome = { status: "completed" };
|
|
1936
2036
|
while (this.#inFlight) {
|
|
1937
|
-
await this.#inFlight;
|
|
2037
|
+
outcome = await this.#inFlight;
|
|
1938
2038
|
}
|
|
2039
|
+
return outcome;
|
|
1939
2040
|
}
|
|
1940
2041
|
|
|
1941
2042
|
#start(): void {
|
|
@@ -1945,16 +2046,22 @@ export class WorkerIntegrationRequestScheduler {
|
|
|
1945
2046
|
try {
|
|
1946
2047
|
request = this.request(controller.signal);
|
|
1947
2048
|
} catch {
|
|
1948
|
-
request = Promise.
|
|
2049
|
+
request = Promise.reject(new Error("Worker integration request failed before dispatch."));
|
|
1949
2050
|
}
|
|
1950
2051
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
1951
|
-
const deadline = new Promise<
|
|
2052
|
+
const deadline = new Promise<WorkerIntegrationOutcome>(resolve => {
|
|
1952
2053
|
timeout = setTimeout(() => {
|
|
1953
2054
|
controller.abort(new Error("Worker integration request timed out"));
|
|
1954
|
-
resolve();
|
|
2055
|
+
resolve({ status: "timed_out" });
|
|
1955
2056
|
}, this.timeoutMs);
|
|
1956
2057
|
});
|
|
1957
|
-
this.#inFlight = Promise.race([
|
|
2058
|
+
this.#inFlight = Promise.race([
|
|
2059
|
+
request.then(
|
|
2060
|
+
() => ({ status: "completed" as const }),
|
|
2061
|
+
error => ({ status: "failed" as const, error: String(error) }),
|
|
2062
|
+
),
|
|
2063
|
+
deadline,
|
|
2064
|
+
]).finally(() => {
|
|
1958
2065
|
if (timeout) clearTimeout(timeout);
|
|
1959
2066
|
controller.abort();
|
|
1960
2067
|
this.#inFlight = undefined;
|
|
@@ -2285,6 +2392,7 @@ export class AgentSession {
|
|
|
2285
2392
|
#preProfileModel: Model | undefined;
|
|
2286
2393
|
#sessionAdmissionQueue: SessionAdmissionEntry[] = [];
|
|
2287
2394
|
#activeSessionAdmission: SessionAdmissionEntry | undefined;
|
|
2395
|
+
#startupPromptWaiters = new Set<Promise<void>>();
|
|
2288
2396
|
#sessionAdmissionClosing = false;
|
|
2289
2397
|
#sessionAdmissionClosed = false;
|
|
2290
2398
|
#sessionAdmissionContext = new AsyncLocalStorage<SessionAdmissionEntry>();
|
|
@@ -2475,6 +2583,22 @@ export class AgentSession {
|
|
|
2475
2583
|
this.#activeAttemptScope = undefined;
|
|
2476
2584
|
this.#activeLogicalRunId = undefined;
|
|
2477
2585
|
}
|
|
2586
|
+
#acceptSdkAttemptRun(handle: AttemptRunHandle, sdkRunToken?: string): void {
|
|
2587
|
+
const predecessorScope = this.#activeAttemptScope;
|
|
2588
|
+
const predecessorSdkRunToken =
|
|
2589
|
+
predecessorScope === undefined ? undefined : this.#sdkRunTokensByAttemptScope.get(predecessorScope);
|
|
2590
|
+
const carryRecoverySkip =
|
|
2591
|
+
sdkRunToken !== undefined &&
|
|
2592
|
+
predecessorSdkRunToken === sdkRunToken &&
|
|
2593
|
+
predecessorScope !== undefined &&
|
|
2594
|
+
this.#skipPostPromptRecoveryWaitByAttemptScope.delete(predecessorScope);
|
|
2595
|
+
this.#acceptRunHandle(handle);
|
|
2596
|
+
if (sdkRunToken !== undefined) {
|
|
2597
|
+
this.#activeSdkRunToken = sdkRunToken;
|
|
2598
|
+
this.#sdkRunTokensByAttemptScope.set(handle.scope, sdkRunToken);
|
|
2599
|
+
}
|
|
2600
|
+
if (carryRecoverySkip) this.#skipPostPromptRecoveryWaitByAttemptScope.add(handle.scope);
|
|
2601
|
+
}
|
|
2478
2602
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
2479
2603
|
#pendingNextTurnMessages: Array<{ message: CustomMessage; origin: "turn" | "external" }> = [];
|
|
2480
2604
|
#scheduledHiddenNextTurnGeneration: number | undefined = undefined;
|
|
@@ -2608,7 +2732,88 @@ export class AgentSession {
|
|
|
2608
2732
|
onPersisted?: () => void;
|
|
2609
2733
|
appendedToAgent: boolean;
|
|
2610
2734
|
}> = [];
|
|
2611
|
-
|
|
2735
|
+
/**
|
|
2736
|
+
* Set by a fold and consumed once by the pause checkpoint, so a fold ends its
|
|
2737
|
+
* own turn without pausing any later turn.
|
|
2738
|
+
*/
|
|
2739
|
+
#foldStopRequested = false;
|
|
2740
|
+
/**
|
|
2741
|
+
* The single linearization point for folding a foreground wait. Arrow deps
|
|
2742
|
+
* capture `this` but are only invoked after construction.
|
|
2743
|
+
*/
|
|
2744
|
+
readonly #foldCoordinator = new FoldCoordinator({
|
|
2745
|
+
hasActiveTurn: () => this.activePromptHandle !== undefined,
|
|
2746
|
+
armSteeringFence: () => {
|
|
2747
|
+
this.agent.setSteeringAdmissionFence(() => true);
|
|
2748
|
+
return () => this.agent.setSteeringAdmissionFence(undefined);
|
|
2749
|
+
},
|
|
2750
|
+
requestStop: () => {
|
|
2751
|
+
this.#foldStopRequested = true;
|
|
2752
|
+
},
|
|
2753
|
+
captureRemainingIntent: () => this.#captureRemainingIntentForFold(),
|
|
2754
|
+
// A parked completion replayed after the fold must schedule its own wake.
|
|
2755
|
+
// This mirrors exactly what the sdk delivery seam does for a live receipt
|
|
2756
|
+
// delivery, so the parked path cannot rely on an unrelated idle rearm.
|
|
2757
|
+
deliverParked: (job, disposition) => {
|
|
2758
|
+
const endpointId = this.#ownedAsyncJobManager
|
|
2759
|
+
? AsyncJobManager.endpointIdOf(this.#ownedAsyncJobManager)
|
|
2760
|
+
: undefined;
|
|
2761
|
+
const registration = lookupOwnedRegistration(job.id, job.generation, endpointId);
|
|
2762
|
+
if (this.#ownedAsyncJobManager?.isDeliverySuppressed(job.id, job.generation)) {
|
|
2763
|
+
this.#ownedAsyncJobManager.clearParkedDelivery(job.generation);
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2766
|
+
if (this.#foldCoordinator.claimCompletionNotice(job)) {
|
|
2767
|
+
this.emitNotice(
|
|
2768
|
+
"info",
|
|
2769
|
+
`Folded job ${disposition.receipt.jobId} (${disposition.receipt.label}) finished.`,
|
|
2770
|
+
"fold",
|
|
2771
|
+
);
|
|
2772
|
+
}
|
|
2773
|
+
const ownedCompletion = registration
|
|
2774
|
+
? {
|
|
2775
|
+
lineageIdHash: registration.lineageIdHash,
|
|
2776
|
+
promptAttemptEpoch: registration.promptAttemptEpoch,
|
|
2777
|
+
registration,
|
|
2778
|
+
}
|
|
2779
|
+
: undefined;
|
|
2780
|
+
const allowArtifact = ownedCompletion === undefined || isOwnedCompletionEnvelopeAllowed(ownedCompletion);
|
|
2781
|
+
void formatParkedAsyncResult(this.sessionManager, disposition.text, allowArtifact)
|
|
2782
|
+
.then(formattedResult => {
|
|
2783
|
+
const manager = this.#ownedAsyncJobManager;
|
|
2784
|
+
if (manager?.isDeliverySuppressed(job.id, job.generation)) {
|
|
2785
|
+
manager.clearParkedDelivery(job.generation);
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
if (!this.#foldCoordinator.claimCompletionDelivery(job)) {
|
|
2789
|
+
manager?.clearParkedDelivery(job.generation);
|
|
2790
|
+
return;
|
|
2791
|
+
}
|
|
2792
|
+
try {
|
|
2793
|
+
manager?.retainDeliveryClaim(job);
|
|
2794
|
+
if (manager?.isDeliverySuppressed(job.id, job.generation)) {
|
|
2795
|
+
manager.releaseDeliveryClaim(job.generation);
|
|
2796
|
+
manager.clearParkedDelivery(job.generation);
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
this.yieldQueue.enqueue("async-result", {
|
|
2800
|
+
jobId: disposition.receipt.jobId,
|
|
2801
|
+
generation: disposition.receipt.jobGeneration,
|
|
2802
|
+
result: `${formattedResult}\n\n${describeFoldReceipt(disposition.receipt)}`,
|
|
2803
|
+
job,
|
|
2804
|
+
durationMs: undefined,
|
|
2805
|
+
ownedCompletion,
|
|
2806
|
+
});
|
|
2807
|
+
} finally {
|
|
2808
|
+
manager?.clearParkedDelivery(job.generation);
|
|
2809
|
+
}
|
|
2810
|
+
})
|
|
2811
|
+
.catch(error => {
|
|
2812
|
+
this.#ownedAsyncJobManager?.clearParkedDelivery(job.generation);
|
|
2813
|
+
logger.warn("Parked folded delivery formatting failed", { error: String(error) });
|
|
2814
|
+
});
|
|
2815
|
+
},
|
|
2816
|
+
});
|
|
2612
2817
|
|
|
2613
2818
|
// Python execution state
|
|
2614
2819
|
#evalAbortControllers = new Set<AbortController>();
|
|
@@ -2616,6 +2821,27 @@ export class AgentSession {
|
|
|
2616
2821
|
/** Idempotent unregister handle for this session's resource-GC registration. */
|
|
2617
2822
|
#unregisterResourceGc?: () => void;
|
|
2618
2823
|
#unregisterRuntimeStateFinalizer?: () => void;
|
|
2824
|
+
#unregisterBeforeMoveListener?: () => void;
|
|
2825
|
+
#unregisterMoveAbortListener?: () => void;
|
|
2826
|
+
#unregisterMovePublicationListener?: () => void;
|
|
2827
|
+
#unregisterAfterMoveListener?: () => void;
|
|
2828
|
+
/**
|
|
2829
|
+
* (Re)register the postmortem finalizer bound to the CURRENT cwd/session file. Called at
|
|
2830
|
+
* construction and after every committed rescope, so an unexpected exit writes terminal
|
|
2831
|
+
* state to the session's live location rather than the abandoned launch root.
|
|
2832
|
+
*/
|
|
2833
|
+
#registerRuntimeStateFinalizer(): void {
|
|
2834
|
+
this.#unregisterRuntimeStateFinalizer?.();
|
|
2835
|
+
const currentContext = () => ({
|
|
2836
|
+
sessionId: this.sessionId,
|
|
2837
|
+
cwd: this.sessionManager.getCwd(),
|
|
2838
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
2839
|
+
});
|
|
2840
|
+
this.#unregisterRuntimeStateFinalizer = registerCoordinatorRuntimeStateFinalizer(
|
|
2841
|
+
currentContext(),
|
|
2842
|
+
currentContext,
|
|
2843
|
+
);
|
|
2844
|
+
}
|
|
2619
2845
|
#unregisterSessionMemorySettings?: () => void;
|
|
2620
2846
|
/**
|
|
2621
2847
|
* AsyncJobManager owned by this session (top-level only). Subagents leave
|
|
@@ -2625,6 +2851,7 @@ export class AgentSession {
|
|
|
2625
2851
|
readonly #disposeAsyncJobManager: boolean;
|
|
2626
2852
|
#ownedMcpManager: MCPManager | undefined;
|
|
2627
2853
|
#startupTurnBarrier: Promise<void> | undefined;
|
|
2854
|
+
#startupTurnBarrierPending = false;
|
|
2628
2855
|
#pendingPythonMessages: Array<{
|
|
2629
2856
|
message: PythonExecutionMessage;
|
|
2630
2857
|
onPersisted?: () => void;
|
|
@@ -2650,6 +2877,9 @@ export class AgentSession {
|
|
|
2650
2877
|
readonly #asyncJobProviderSessionId: string | undefined;
|
|
2651
2878
|
#isDisposed = false;
|
|
2652
2879
|
#disposePromise: Promise<void> | undefined;
|
|
2880
|
+
readonly #disposeAbortController = new AbortController();
|
|
2881
|
+
#disposeAdmissionClosed: Promise<void> | undefined;
|
|
2882
|
+
#disposePostPromptDrain: Promise<void> | undefined;
|
|
2653
2883
|
readonly #toolSessionCleanups = new Set<() => Promise<void> | void>();
|
|
2654
2884
|
readonly #toolSessionTransitionCleanups = new Set<() => Promise<void> | void>();
|
|
2655
2885
|
readonly #deferredOwnerShutdownFinalizations = new Set<Promise<void>>();
|
|
@@ -2658,6 +2888,7 @@ export class AgentSession {
|
|
|
2658
2888
|
#extensionRunner: ExtensionRunner | undefined = undefined;
|
|
2659
2889
|
/** SDK follow-up ownership by queued message and the attempt that dequeues it. */
|
|
2660
2890
|
#sdkRunTokensByQueuedMessage = new WeakMap<AgentMessage, string>();
|
|
2891
|
+
#skipPostPromptRecoveryWaitByAttemptScope = new WeakSet<AttemptScope>();
|
|
2661
2892
|
#sdkRunTokensByAttemptScope = new WeakMap<AttemptScope, string>();
|
|
2662
2893
|
#activeSdkRunToken: string | undefined;
|
|
2663
2894
|
#activeAttemptScope: AttemptScope | undefined;
|
|
@@ -2820,6 +3051,7 @@ export class AgentSession {
|
|
|
2820
3051
|
#cancelAndSubmitAbortOutcomeProviderForTests: (() => Promise<AbortOutcome>) | undefined = undefined;
|
|
2821
3052
|
#postPromptTasks = new Set<Promise<void>>();
|
|
2822
3053
|
#postPromptTaskSelectionFenceGenerations = new Map<Promise<void>, number>();
|
|
3054
|
+
#postPromptTaskRecoveryExcluded = new Set<Promise<void>>();
|
|
2823
3055
|
#postPromptTasksPromise: Promise<void> | undefined = undefined;
|
|
2824
3056
|
#postPromptTasksResolve: (() => void) | undefined = undefined;
|
|
2825
3057
|
#postPromptTasksAbortController = new AbortController();
|
|
@@ -3006,6 +3238,66 @@ export class AgentSession {
|
|
|
3006
3238
|
* orchestrator does not hold it), so there is no self-deadlock.
|
|
3007
3239
|
*/
|
|
3008
3240
|
#sessionTransitionKind: string | undefined;
|
|
3241
|
+
#coordinatorPersistGeneration = 0;
|
|
3242
|
+
#coordinatorRescopeBarrier: Promise<void> | undefined;
|
|
3243
|
+
#releaseCoordinatorRescopeBarrier: (() => void) | undefined;
|
|
3244
|
+
#coordinatorRescopeMoveId: string | undefined;
|
|
3245
|
+
#coordinatorEventHandlers = new Set<Promise<void>>();
|
|
3246
|
+
#coordinatorUnbarrieredPersists = new Set<Promise<void>>();
|
|
3247
|
+
#coordinatorReleasedBarrierPersists = new Set<Promise<void>>();
|
|
3248
|
+
|
|
3249
|
+
#beginCoordinatorRescopeBarrier(): {
|
|
3250
|
+
eventHandlers: Promise<void>[];
|
|
3251
|
+
releasedBarrierPersists: Promise<void>[];
|
|
3252
|
+
} {
|
|
3253
|
+
if (this.#coordinatorRescopeBarrier) throw new Error("Coordinator rescope barrier is already active.");
|
|
3254
|
+
const releasedBarrierPersists = [...this.#coordinatorReleasedBarrierPersists];
|
|
3255
|
+
const barrier = Promise.withResolvers<void>();
|
|
3256
|
+
this.#coordinatorRescopeBarrier = barrier.promise;
|
|
3257
|
+
this.#releaseCoordinatorRescopeBarrier = barrier.resolve;
|
|
3258
|
+
this.#bindPendingAgentEndToRescopeBarrier();
|
|
3259
|
+
return { eventHandlers: [...this.#coordinatorEventHandlers], releasedBarrierPersists };
|
|
3260
|
+
}
|
|
3261
|
+
|
|
3262
|
+
#trackReleasedBarrierPersist(persist: Promise<void>): void {
|
|
3263
|
+
this.#coordinatorReleasedBarrierPersists.add(persist);
|
|
3264
|
+
void persist.then(
|
|
3265
|
+
() => this.#coordinatorReleasedBarrierPersists.delete(persist),
|
|
3266
|
+
() => this.#coordinatorReleasedBarrierPersists.delete(persist),
|
|
3267
|
+
);
|
|
3268
|
+
}
|
|
3269
|
+
|
|
3270
|
+
#bindPendingAgentEndToRescopeBarrier(): void {
|
|
3271
|
+
if (!this.#coordinatorRescopeBarrier || !this.#pendingAgentEndEmit) return;
|
|
3272
|
+
const admission = this.#agentEventAdmission.get(this.#pendingAgentEndEmit);
|
|
3273
|
+
if (admission) admission.persistBarrier = this.#coordinatorRescopeBarrier;
|
|
3274
|
+
}
|
|
3275
|
+
|
|
3276
|
+
#appendCoordinatorPersist(run: () => Promise<void>): Promise<void> {
|
|
3277
|
+
const queued = this.#coordinatorPersistQueue.then(run, run);
|
|
3278
|
+
this.#coordinatorPersistQueue = queued.catch(() => {});
|
|
3279
|
+
return queued;
|
|
3280
|
+
}
|
|
3281
|
+
|
|
3282
|
+
#trackUnbarrieredCoordinatorPersist(persist: Promise<void>): void {
|
|
3283
|
+
this.#coordinatorUnbarrieredPersists.add(persist);
|
|
3284
|
+
void persist.then(
|
|
3285
|
+
() => this.#coordinatorUnbarrieredPersists.delete(persist),
|
|
3286
|
+
() => this.#coordinatorUnbarrieredPersists.delete(persist),
|
|
3287
|
+
);
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
async #drainUnbarrieredCoordinatorPersists(): Promise<void> {
|
|
3291
|
+
while (this.#coordinatorUnbarrieredPersists.size > 0) {
|
|
3292
|
+
await Promise.allSettled([...this.#coordinatorUnbarrieredPersists]);
|
|
3293
|
+
}
|
|
3294
|
+
}
|
|
3295
|
+
|
|
3296
|
+
#endCoordinatorRescopeBarrier(): void {
|
|
3297
|
+
this.#releaseCoordinatorRescopeBarrier?.();
|
|
3298
|
+
this.#releaseCoordinatorRescopeBarrier = undefined;
|
|
3299
|
+
this.#coordinatorRescopeBarrier = undefined;
|
|
3300
|
+
}
|
|
3009
3301
|
|
|
3010
3302
|
#beginSessionTransition(kind: string): void {
|
|
3011
3303
|
if (this.#sessionTransitionKind !== undefined) {
|
|
@@ -3015,6 +3307,7 @@ export class AgentSession {
|
|
|
3015
3307
|
);
|
|
3016
3308
|
}
|
|
3017
3309
|
this.#sessionTransitionKind = kind;
|
|
3310
|
+
this.#coordinatorPersistGeneration += 1;
|
|
3018
3311
|
}
|
|
3019
3312
|
|
|
3020
3313
|
#endSessionTransition(): void {
|
|
@@ -3041,16 +3334,61 @@ export class AgentSession {
|
|
|
3041
3334
|
return { entry, capability: entry.continuationCapability };
|
|
3042
3335
|
}
|
|
3043
3336
|
|
|
3044
|
-
|
|
3045
|
-
|
|
3337
|
+
#setStartupTurnBarrier(barrier: Promise<void> | undefined): void {
|
|
3338
|
+
this.#startupTurnBarrier = barrier;
|
|
3339
|
+
this.#startupTurnBarrierPending = barrier !== undefined;
|
|
3046
3340
|
if (!barrier) return;
|
|
3047
|
-
|
|
3048
|
-
|
|
3341
|
+
void barrier.then(
|
|
3342
|
+
() => {
|
|
3343
|
+
if (this.#startupTurnBarrier === barrier) this.#startupTurnBarrierPending = false;
|
|
3344
|
+
},
|
|
3345
|
+
() => {
|
|
3346
|
+
if (this.#startupTurnBarrier === barrier) this.#startupTurnBarrierPending = false;
|
|
3347
|
+
},
|
|
3348
|
+
);
|
|
3349
|
+
}
|
|
3350
|
+
async #awaitStartupTurnBarrier(signal?: AbortSignal): Promise<void> {
|
|
3351
|
+
while (true) {
|
|
3352
|
+
const barrier = this.#startupTurnBarrier;
|
|
3353
|
+
if (!barrier) return;
|
|
3354
|
+
await awaitPromptInvocationPreflight(barrier, signal);
|
|
3355
|
+
// Cross one task boundary before clearing readiness. Promise reaction
|
|
3356
|
+
// chains fully drain before timers, so every extension published by the
|
|
3357
|
+
// captured barrier's settlement becomes visible without an arbitrary
|
|
3358
|
+
// microtask-depth cap.
|
|
3359
|
+
const publicationTurn = Promise.withResolvers<void>();
|
|
3360
|
+
const timer = setTimeout(publicationTurn.resolve, 0);
|
|
3361
|
+
try {
|
|
3362
|
+
await awaitPromptInvocationPreflight(publicationTurn.promise, signal);
|
|
3363
|
+
} finally {
|
|
3364
|
+
clearTimeout(timer);
|
|
3365
|
+
}
|
|
3366
|
+
if (this.#startupTurnBarrier !== barrier) continue;
|
|
3367
|
+
this.#setStartupTurnBarrier(undefined);
|
|
3368
|
+
return;
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
async #awaitStartupPromptWaiters(signal?: AbortSignal): Promise<void> {
|
|
3372
|
+
while (this.#startupPromptWaiters.size > 0) {
|
|
3373
|
+
await awaitPromptInvocationPreflight(Promise.all([...this.#startupPromptWaiters]), signal);
|
|
3374
|
+
await Promise.resolve();
|
|
3375
|
+
}
|
|
3376
|
+
}
|
|
3377
|
+
#reserveStartupPromptWaiter(): () => void {
|
|
3378
|
+
const waiter = Promise.withResolvers<void>();
|
|
3379
|
+
this.#startupPromptWaiters.add(waiter.promise);
|
|
3380
|
+
let released = false;
|
|
3381
|
+
return () => {
|
|
3382
|
+
if (released) return;
|
|
3383
|
+
released = true;
|
|
3384
|
+
this.#startupPromptWaiters.delete(waiter.promise);
|
|
3385
|
+
waiter.resolve();
|
|
3386
|
+
};
|
|
3049
3387
|
}
|
|
3050
3388
|
extendStartupTurnBarrier(barrier: Promise<void>): void {
|
|
3051
3389
|
const current = this.#startupTurnBarrier;
|
|
3052
|
-
this.#
|
|
3053
|
-
void this.#startupTurnBarrier
|
|
3390
|
+
this.#setStartupTurnBarrier(current ? Promise.all([current, barrier]).then(() => {}) : barrier);
|
|
3391
|
+
void this.#startupTurnBarrier?.catch(() => {});
|
|
3054
3392
|
}
|
|
3055
3393
|
|
|
3056
3394
|
async #withSessionAdmission<T>(
|
|
@@ -3062,6 +3400,8 @@ export class AgentSession {
|
|
|
3062
3400
|
allowDuringClosing?: boolean;
|
|
3063
3401
|
bypassSelectionFenceGeneration?: number;
|
|
3064
3402
|
allowPromptContinuationReentry?: boolean;
|
|
3403
|
+
idleDelivery?: boolean;
|
|
3404
|
+
startupPromptWaiterRelease?: () => void;
|
|
3065
3405
|
},
|
|
3066
3406
|
): Promise<T> {
|
|
3067
3407
|
const owner = this.#sessionAdmissionContext.getStore();
|
|
@@ -3076,7 +3416,21 @@ export class AgentSession {
|
|
|
3076
3416
|
}
|
|
3077
3417
|
throw this.#sessionAdmissionBusyError();
|
|
3078
3418
|
}
|
|
3079
|
-
|
|
3419
|
+
const releaseStartupPromptWaiter =
|
|
3420
|
+
kind === "prompt" && options?.idleDelivery !== true
|
|
3421
|
+
? (options?.startupPromptWaiterRelease ?? this.#reserveStartupPromptWaiter())
|
|
3422
|
+
: undefined;
|
|
3423
|
+
if (kind === "prompt") {
|
|
3424
|
+
const startupSignal = signal
|
|
3425
|
+
? AbortSignal.any([signal, this.#disposeAbortController.signal])
|
|
3426
|
+
: this.#disposeAbortController.signal;
|
|
3427
|
+
try {
|
|
3428
|
+
await awaitPromptInvocationPreflight(this.#awaitStartupTurnBarrier(startupSignal), startupSignal);
|
|
3429
|
+
} finally {
|
|
3430
|
+
releaseStartupPromptWaiter?.();
|
|
3431
|
+
}
|
|
3432
|
+
if (options?.idleDelivery === true) await this.#awaitStartupPromptWaiters(startupSignal);
|
|
3433
|
+
}
|
|
3080
3434
|
const bypassesSelectionFence =
|
|
3081
3435
|
options?.bypassSelectionFenceGeneration !== undefined &&
|
|
3082
3436
|
options.bypassSelectionFenceGeneration < this.#selectionFenceGeneration;
|
|
@@ -3614,7 +3968,7 @@ export class AgentSession {
|
|
|
3614
3968
|
// Deferrals behind still-pending selection fences are excluded here: they
|
|
3615
3969
|
// cannot settle before their fence resolves, and the fence owner may be
|
|
3616
3970
|
// waiting on this very settlement (#4519).
|
|
3617
|
-
if (!predecessorPromptStillInFlight) {
|
|
3971
|
+
if (promptWait !== "publication" && !predecessorPromptStillInFlight) {
|
|
3618
3972
|
await this.#waitForSessionSettlement(this.#turnSettlementDeferralFloor());
|
|
3619
3973
|
}
|
|
3620
3974
|
if (flushError) throw flushError;
|
|
@@ -3656,28 +4010,50 @@ export class AgentSession {
|
|
|
3656
4010
|
this.#pendingAgentEndEmit = undefined;
|
|
3657
4011
|
const lease = this.#deferredAgentEndLeases.get(pending);
|
|
3658
4012
|
if (lease) this.#deferredAgentEndLeases.delete(pending);
|
|
4013
|
+
this.#startAgentEndPublication(pending, lease);
|
|
4014
|
+
}
|
|
4015
|
+
#startAgentEndPublication(pending: AgentSessionEvent, lease?: RunResourceProducerLease): void {
|
|
3659
4016
|
this.#agentEndPublicationInFlight++;
|
|
3660
|
-
|
|
4017
|
+
const pendingScope = (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope as
|
|
4018
|
+
| AttemptScope
|
|
4019
|
+
| undefined;
|
|
4020
|
+
const sdkTerminal = pendingScope !== undefined && this.#sdkRunTokensByAttemptScope.has(pendingScope);
|
|
4021
|
+
this.#agentEndPublicationPromise = this.#publishDeferredAgentEnd(pending, lease, sdkTerminal);
|
|
3661
4022
|
void this.#agentEndPublicationPromise;
|
|
3662
4023
|
}
|
|
3663
4024
|
|
|
3664
4025
|
async #publishDeferredAgentEnd(
|
|
3665
4026
|
pending: AgentSessionEvent,
|
|
3666
4027
|
lease: RunResourceProducerLease | undefined,
|
|
4028
|
+
workerIntegrationSettled: boolean,
|
|
3667
4029
|
): Promise<void> {
|
|
4030
|
+
const publicationContext = {
|
|
4031
|
+
sessionId: this.sessionId,
|
|
4032
|
+
cwd: this.sessionManager.getCwd(),
|
|
4033
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
4034
|
+
};
|
|
4035
|
+
const publicationScope = (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope as
|
|
4036
|
+
| AttemptScope
|
|
4037
|
+
| undefined;
|
|
4038
|
+
const publicationCorrelationId = publicationScope
|
|
4039
|
+
? this.#sdkRunTokensByAttemptScope.get(publicationScope)
|
|
4040
|
+
: undefined;
|
|
3668
4041
|
let extensionDelivery: Promise<void> | undefined;
|
|
3669
4042
|
const releaseLease = () => {
|
|
3670
4043
|
if (!lease) return;
|
|
4044
|
+
this.#deferredAgentEndLeases.delete(pending);
|
|
3671
4045
|
if (this.#postPromptLeases.get(lease.resourceRunId) === lease) {
|
|
3672
4046
|
this.#postPromptLeases.delete(lease.resourceRunId);
|
|
3673
4047
|
}
|
|
3674
4048
|
lease.closeDiscovery();
|
|
3675
4049
|
};
|
|
3676
4050
|
const publish = async () => {
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
4051
|
+
let workerIntegration: Promise<WorkerIntegrationOutcome> | undefined;
|
|
4052
|
+
let workerIntegrationOutcome: WorkerIntegrationOutcome | undefined;
|
|
4053
|
+
if (!workerIntegrationSettled) {
|
|
4054
|
+
workerIntegration = this.#flushWorkerIntegrationForAgentEnd();
|
|
4055
|
+
workerIntegrationOutcome = await workerIntegration;
|
|
4056
|
+
}
|
|
3681
4057
|
// Reserve persistence before notifying synchronous subscribers: a subscriber
|
|
3682
4058
|
// may start a successor prompt from agent_end, whose running state must
|
|
3683
4059
|
// serialize after this terminal boundary rather than be overwritten by it.
|
|
@@ -3686,16 +4062,72 @@ export class AgentSession {
|
|
|
3686
4062
|
// behind this terminal transition, but do not make the interactive terminal
|
|
3687
4063
|
// wait for the filesystem-backed lock/write to settle. On slow or lock-hostile
|
|
3688
4064
|
// mounts such as WSL drvfs, waiting here leaves the foreground activity loader
|
|
3689
|
-
// and busy input state visible after the model has already finished.
|
|
3690
|
-
|
|
4065
|
+
// and busy input state visible after the model has already finished. The queue
|
|
4066
|
+
// preserves transaction order; terminal publication is the user-visible
|
|
4067
|
+
// authority and must not be suppressed by a secondary persistence failure.
|
|
4068
|
+
const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(pending, true);
|
|
3691
4069
|
this.#emit(pending);
|
|
3692
|
-
|
|
4070
|
+
void terminalPersistence.then(
|
|
4071
|
+
() => {
|
|
4072
|
+
if (workerIntegrationOutcome) {
|
|
4073
|
+
this.#recordPostPublicationOutcome(
|
|
4074
|
+
publicationContext,
|
|
4075
|
+
publicationCorrelationId,
|
|
4076
|
+
"worker_integration",
|
|
4077
|
+
workerIntegrationOutcome,
|
|
4078
|
+
);
|
|
4079
|
+
}
|
|
4080
|
+
this.#recordPostPublicationOutcome(
|
|
4081
|
+
publicationContext,
|
|
4082
|
+
publicationCorrelationId,
|
|
4083
|
+
"terminal_persistence",
|
|
4084
|
+
{ status: "completed" },
|
|
4085
|
+
);
|
|
4086
|
+
},
|
|
4087
|
+
error => {
|
|
4088
|
+
if (workerIntegrationOutcome) {
|
|
4089
|
+
this.#recordPostPublicationOutcome(
|
|
4090
|
+
publicationContext,
|
|
4091
|
+
publicationCorrelationId,
|
|
4092
|
+
"worker_integration",
|
|
4093
|
+
workerIntegrationOutcome,
|
|
4094
|
+
);
|
|
4095
|
+
}
|
|
4096
|
+
this.#recordPostPublicationOutcome(
|
|
4097
|
+
publicationContext,
|
|
4098
|
+
publicationCorrelationId,
|
|
4099
|
+
"terminal_persistence",
|
|
4100
|
+
{
|
|
4101
|
+
status: "failed",
|
|
4102
|
+
error: String(error),
|
|
4103
|
+
},
|
|
4104
|
+
);
|
|
4105
|
+
logger.warn("Failed to persist terminal coordinator runtime state", { error: String(error) });
|
|
4106
|
+
},
|
|
4107
|
+
);
|
|
3693
4108
|
extensionDelivery = this.#queueExtensionEvent(
|
|
3694
4109
|
pending,
|
|
3695
4110
|
undefined,
|
|
3696
4111
|
true,
|
|
3697
4112
|
(pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope,
|
|
3698
4113
|
);
|
|
4114
|
+
if (workerIntegrationSettled) {
|
|
4115
|
+
workerIntegration = this.#flushWorkerIntegrationForAgentEnd();
|
|
4116
|
+
void workerIntegration.then(outcome =>
|
|
4117
|
+
this.#recordPostPublicationOutcome(
|
|
4118
|
+
publicationContext,
|
|
4119
|
+
publicationCorrelationId,
|
|
4120
|
+
"worker_integration",
|
|
4121
|
+
outcome,
|
|
4122
|
+
),
|
|
4123
|
+
);
|
|
4124
|
+
await extensionDelivery;
|
|
4125
|
+
void terminalPersistence.catch(error =>
|
|
4126
|
+
logger.warn("Terminal persistence continued after SDK publication", { error }),
|
|
4127
|
+
);
|
|
4128
|
+
} else {
|
|
4129
|
+
// Non-SDK/public publication has already awaited worker integration above.
|
|
4130
|
+
}
|
|
3699
4131
|
};
|
|
3700
4132
|
try {
|
|
3701
4133
|
if (lease) await this.#runResourceLeaseContext.run(lease, publish);
|
|
@@ -3760,10 +4192,96 @@ export class AgentSession {
|
|
|
3760
4192
|
cwd: () => this.sessionManager.getCwd(),
|
|
3761
4193
|
});
|
|
3762
4194
|
}
|
|
3763
|
-
this.#
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
4195
|
+
this.#registerRuntimeStateFinalizer();
|
|
4196
|
+
this.#unregisterBeforeMoveListener = this.sessionManager.registerBeforeMoveListener(async move => {
|
|
4197
|
+
const admittedBeforeBarrier = this.#beginCoordinatorRescopeBarrier();
|
|
4198
|
+
await Promise.allSettled([
|
|
4199
|
+
...admittedBeforeBarrier.eventHandlers,
|
|
4200
|
+
...admittedBeforeBarrier.releasedBarrierPersists,
|
|
4201
|
+
]);
|
|
4202
|
+
this.#bindPendingAgentEndToRescopeBarrier();
|
|
4203
|
+
await this.#drainUnbarrieredCoordinatorPersists();
|
|
4204
|
+
this.#coordinatorPersistGeneration += 1;
|
|
4205
|
+
const moveId = crypto.randomUUID();
|
|
4206
|
+
this.#coordinatorRescopeMoveId = moveId;
|
|
4207
|
+
await prepareCoordinatorRuntimeStateRescope({
|
|
4208
|
+
moveId,
|
|
4209
|
+
sessionId: this.sessionId,
|
|
4210
|
+
previousCwd: move.previousCwd,
|
|
4211
|
+
newCwd: move.newCwd,
|
|
4212
|
+
previousCwdIdentity: move.previousCwdIdentity,
|
|
4213
|
+
newCwdIdentity: move.newCwdIdentity,
|
|
4214
|
+
previousSessionFile: move.previousSessionFile ?? null,
|
|
4215
|
+
newSessionFile: move.newSessionFile ?? null,
|
|
4216
|
+
});
|
|
4217
|
+
});
|
|
4218
|
+
this.#unregisterMoveAbortListener = this.sessionManager.registerMoveAbortListener(async move => {
|
|
4219
|
+
const moveId = this.#coordinatorRescopeMoveId;
|
|
4220
|
+
try {
|
|
4221
|
+
if (moveId && !move.preserveRecoveryJournal) {
|
|
4222
|
+
await clearCoordinatorRuntimeStateRescope(
|
|
4223
|
+
{
|
|
4224
|
+
sessionId: this.sessionId,
|
|
4225
|
+
cwd: move.newCwd,
|
|
4226
|
+
sessionFile: move.newSessionFile ?? null,
|
|
4227
|
+
},
|
|
4228
|
+
moveId,
|
|
4229
|
+
move.previousCwd,
|
|
4230
|
+
);
|
|
4231
|
+
}
|
|
4232
|
+
} finally {
|
|
4233
|
+
this.#coordinatorRescopeMoveId = undefined;
|
|
4234
|
+
this.#endCoordinatorRescopeBarrier();
|
|
4235
|
+
}
|
|
4236
|
+
});
|
|
4237
|
+
this.#unregisterMovePublicationListener = this.sessionManager.registerMovePublicationListener(async move => {
|
|
4238
|
+
const moveId = this.#coordinatorRescopeMoveId;
|
|
4239
|
+
if (!moveId) throw new Error("Coordinator rescope publication has no prepared move identity.");
|
|
4240
|
+
await markCoordinatorRuntimeStateRescopePublishing(
|
|
4241
|
+
{
|
|
4242
|
+
sessionId: this.sessionId,
|
|
4243
|
+
cwd: move.newCwd,
|
|
4244
|
+
sessionFile: move.newSessionFile ?? null,
|
|
4245
|
+
},
|
|
4246
|
+
move.previousCwd,
|
|
4247
|
+
moveId,
|
|
4248
|
+
);
|
|
4249
|
+
});
|
|
4250
|
+
// Every committed rescope (`move_session`, `/move`, SDK/ACP `session.cwd.move`) funnels
|
|
4251
|
+
// through SessionManager.moveTo, so one after-move listener covers all surfaces: it
|
|
4252
|
+
// relocates the coordinator-shared runtime state to the new cwd (otherwise the identity
|
|
4253
|
+
// fence refuses every later persist) and rebinds the postmortem finalizer, which
|
|
4254
|
+
// otherwise keeps writing terminal state to the launch root's cwd/session file.
|
|
4255
|
+
this.#unregisterAfterMoveListener = this.sessionManager.registerAfterMoveListener(async move => {
|
|
4256
|
+
let completed = false;
|
|
4257
|
+
try {
|
|
4258
|
+
const relocated = await relocateCoordinatorRuntimeStateForRescope(
|
|
4259
|
+
{
|
|
4260
|
+
sessionId: this.sessionId,
|
|
4261
|
+
cwd: move.newCwd,
|
|
4262
|
+
sessionFile: this.sessionManager.getSessionFile() ?? null,
|
|
4263
|
+
previousSessionFile: move.previousSessionFile ?? null,
|
|
4264
|
+
},
|
|
4265
|
+
move.previousCwd,
|
|
4266
|
+
);
|
|
4267
|
+
if (!relocated) throw new Error("Coordinator runtime state rescope was refused.");
|
|
4268
|
+
await clearCoordinatorRuntimeStateRescope(
|
|
4269
|
+
{
|
|
4270
|
+
sessionId: this.sessionId,
|
|
4271
|
+
cwd: move.newCwd,
|
|
4272
|
+
sessionFile: this.sessionManager.getSessionFile() ?? null,
|
|
4273
|
+
},
|
|
4274
|
+
this.#coordinatorRescopeMoveId,
|
|
4275
|
+
move.previousCwd,
|
|
4276
|
+
);
|
|
4277
|
+
completed = true;
|
|
4278
|
+
} finally {
|
|
4279
|
+
this.#registerRuntimeStateFinalizer();
|
|
4280
|
+
if (completed) {
|
|
4281
|
+
this.#coordinatorRescopeMoveId = undefined;
|
|
4282
|
+
this.#endCoordinatorRescopeBarrier();
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
3767
4285
|
});
|
|
3768
4286
|
// Power assertions are taken per turn (see #beginInFlight); nothing acquired here.
|
|
3769
4287
|
this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
|
|
@@ -3772,7 +4290,19 @@ export class AgentSession {
|
|
|
3772
4290
|
this.#disposeAsyncJobManager = config.disposeAsyncJobManager ?? true;
|
|
3773
4291
|
this.#retainedMemorySampler = config.retainedMemorySampler;
|
|
3774
4292
|
this.#ownedMcpManager = config.ownedMcpManager;
|
|
3775
|
-
this.#
|
|
4293
|
+
this.#setStartupTurnBarrier(config.startupTurnBarrier);
|
|
4294
|
+
// Only arm the recovery barrier when a rescope journal is actually pending. Arming it
|
|
4295
|
+
// unconditionally flips #startupTurnBarrier from absent to a (resolved) promise for
|
|
4296
|
+
// every session, which makes barrier-gated post-turn work (e.g. the hidden-next-turn
|
|
4297
|
+
// drain) take an extra scheduler hop and reorder against terminal-abort requeue.
|
|
4298
|
+
const rescopeRecoveryContext = {
|
|
4299
|
+
sessionId: this.sessionId,
|
|
4300
|
+
cwd: this.sessionManager.getCwd(),
|
|
4301
|
+
sessionFile: this.sessionManager.getSessionFile() ?? null,
|
|
4302
|
+
};
|
|
4303
|
+
if (hasCoordinatorRuntimeStateRescopeJournal(rescopeRecoveryContext)) {
|
|
4304
|
+
this.extendStartupTurnBarrier(recoverCoordinatorRuntimeStateRescope(rescopeRecoveryContext));
|
|
4305
|
+
}
|
|
3776
4306
|
this.#scopedModels = config.scopedModels ?? [];
|
|
3777
4307
|
this.#thinkingLevel = config.thinkingLevel;
|
|
3778
4308
|
this.#promptTemplates = config.promptTemplates ?? [];
|
|
@@ -3860,7 +4390,7 @@ export class AgentSession {
|
|
|
3860
4390
|
// agent.prompt directly, allocates right before admission.
|
|
3861
4391
|
this.agent.followUp(message);
|
|
3862
4392
|
},
|
|
3863
|
-
injectIdle: async messages => {
|
|
4393
|
+
injectIdle: async (messages, signal) => {
|
|
3864
4394
|
// Mandated boundary comment (corrected turn semantics): same origin
|
|
3865
4395
|
// split as the streaming injector — an allowed owned-completion
|
|
3866
4396
|
// delivery starts a fresh turn attempt/lineage and is not a
|
|
@@ -3874,27 +4404,43 @@ export class AgentSession {
|
|
|
3874
4404
|
if (dropped.length > 0) this.#settleDeliveredOwnedRegistrations(dropped);
|
|
3875
4405
|
const first = survivors[0];
|
|
3876
4406
|
if (!first) return;
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
// completion). Route the survivors through followUp — the
|
|
3884
|
-
// streaming injector's path — which allocates the fresh resume
|
|
3885
|
-
// lineage at actual admission (review thread P1).
|
|
3886
|
-
if (this.isStreaming) {
|
|
3887
|
-
for (const message of survivors) this.agent.followUp(message);
|
|
3888
|
-
return;
|
|
3889
|
-
}
|
|
3890
|
-
if (survivors.some(message => ownedCompletionResumeAction(message) === "fresh"))
|
|
3891
|
-
this.#resumeFromOwnedCompletion();
|
|
4407
|
+
const settleIfDisposing = (): boolean => {
|
|
4408
|
+
if (!this.#isDisposed && !this.#sessionAdmissionClosing && !this.#disposeAbortController.signal.aborted)
|
|
4409
|
+
return false;
|
|
4410
|
+
this.#settleDeliveredOwnedRegistrations(survivors);
|
|
4411
|
+
return true;
|
|
4412
|
+
};
|
|
3892
4413
|
try {
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
4414
|
+
await this.#withSessionAdmission(
|
|
4415
|
+
"prompt",
|
|
4416
|
+
async () => {
|
|
4417
|
+
if (settleIfDisposing()) return;
|
|
4418
|
+
if (this.isStreaming) {
|
|
4419
|
+
await awaitPromptInvocationPreflight(this.agent.waitForIdle(), signal);
|
|
4420
|
+
if (settleIfDisposing()) return;
|
|
4421
|
+
}
|
|
4422
|
+
if (survivors.some(message => ownedCompletionResumeAction(message) === "fresh"))
|
|
4423
|
+
this.#resumeFromOwnedCompletion();
|
|
4424
|
+
if (survivors.length === 1) {
|
|
4425
|
+
await this.agent.prompt(first, {
|
|
4426
|
+
...this.#managedFallbackPromptOptions(),
|
|
4427
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
4428
|
+
if (handle) this.#acceptSdkAttemptRun(handle);
|
|
4429
|
+
},
|
|
4430
|
+
});
|
|
4431
|
+
} else {
|
|
4432
|
+
await this.agent.prompt(survivors, {
|
|
4433
|
+
...this.#managedFallbackPromptOptions(),
|
|
4434
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
4435
|
+
if (handle) this.#acceptSdkAttemptRun(handle);
|
|
4436
|
+
},
|
|
4437
|
+
});
|
|
4438
|
+
}
|
|
4439
|
+
},
|
|
4440
|
+
signal,
|
|
4441
|
+
undefined,
|
|
4442
|
+
{ idleDelivery: true },
|
|
4443
|
+
);
|
|
3898
4444
|
} finally {
|
|
3899
4445
|
// The owned completions were delivered OR the prompt attempt
|
|
3900
4446
|
// failed (e.g. provider rejection): either way the yield
|
|
@@ -3906,16 +4452,40 @@ export class AgentSession {
|
|
|
3906
4452
|
this.#settleDeliveredOwnedRegistrations(survivors);
|
|
3907
4453
|
}
|
|
3908
4454
|
},
|
|
3909
|
-
scheduleIdleFlush: run => {
|
|
4455
|
+
scheduleIdleFlush: (run, onSkip) => {
|
|
4456
|
+
// The startup barrier already gates injectIdle, so begin waiting on a
|
|
4457
|
+
// pending barrier immediately. Once readiness has settled, ordinary
|
|
4458
|
+
// idle wakes retain the fixed merge window.
|
|
4459
|
+
const delayMs = this.#startupTurnBarrierPending ? 0 : FOLD_WAKE_MERGE_WINDOW_MS;
|
|
3910
4460
|
this.#schedulePostPromptTask(
|
|
3911
|
-
async
|
|
3912
|
-
await run();
|
|
4461
|
+
async signal => {
|
|
4462
|
+
await run(signal);
|
|
3913
4463
|
},
|
|
3914
|
-
{ delayMs:
|
|
4464
|
+
{ delayMs, onSkip, excludeFromPostPromptRecovery: true },
|
|
3915
4465
|
);
|
|
3916
4466
|
},
|
|
4467
|
+
getIdleFlushSignal: () => this.#postPromptTasksAbortController.signal,
|
|
3917
4468
|
});
|
|
3918
4469
|
this.agent.setOnBeforeYield(() => this.yieldQueue.flush("streaming"));
|
|
4470
|
+
// Stop-after-result, never abort: a fold arms this once and the loop ends the
|
|
4471
|
+
// turn at its next checkpoint. Consuming the flag here keeps the pause scoped
|
|
4472
|
+
// to the folded turn instead of pausing everything that follows. The
|
|
4473
|
+
// config-provided checkpoint (subagent requestPause, RLM research) is OR-ed
|
|
4474
|
+
// in, never replaced.
|
|
4475
|
+
// sdk/session.ts threads the config's shouldPause into new Agent, so the
|
|
4476
|
+
// Agent's current checkpoint IS the config-provided one (subagent
|
|
4477
|
+
// requestPause, RLM research). Capture and OR it rather than replace it.
|
|
4478
|
+
const configuredShouldPause = this.agent.shouldPause;
|
|
4479
|
+
this.agent.setShouldPause(() => {
|
|
4480
|
+
if (configuredShouldPause?.() === true) return true;
|
|
4481
|
+
if (!this.#foldStopRequested) return false;
|
|
4482
|
+
this.#foldStopRequested = false;
|
|
4483
|
+
// The folded turn has now actually stopped: steering admission must be
|
|
4484
|
+
// restored so later turns drain their queues normally (the fence stays
|
|
4485
|
+
// armed from the fold until exactly this point).
|
|
4486
|
+
this.agent.setSteeringAdmissionFence(undefined);
|
|
4487
|
+
return true;
|
|
4488
|
+
});
|
|
3919
4489
|
this.agent.setMaintainContext((context, lifecycle) =>
|
|
3920
4490
|
this.#trackMidRunMaintenance(
|
|
3921
4491
|
this.awaitPendingContextTransformations().then(() => this.#runMidRunMaintenance(context, lifecycle)),
|
|
@@ -4054,6 +4624,18 @@ export class AgentSession {
|
|
|
4054
4624
|
if (messages.some(message => ownedCompletionResumeAction(message) === "fresh")) {
|
|
4055
4625
|
this.#resumeFromOwnedCompletion();
|
|
4056
4626
|
}
|
|
4627
|
+
const consumedSdkRunTokenForCurrentRun = messages
|
|
4628
|
+
.map(message => this.#sdkRunTokensByQueuedMessage.get(message))
|
|
4629
|
+
.find((token): token is string => token !== undefined);
|
|
4630
|
+
if (
|
|
4631
|
+
consumedSdkRunTokenForCurrentRun !== undefined &&
|
|
4632
|
+
this.#activeAttemptScope !== undefined &&
|
|
4633
|
+
promotion.startsOwnRun !== true
|
|
4634
|
+
) {
|
|
4635
|
+
this.#sdkRunTokensByAttemptScope.set(this.#activeAttemptScope, consumedSdkRunTokenForCurrentRun);
|
|
4636
|
+
this.#skipPostPromptRecoveryWaitByAttemptScope.add(this.#activeAttemptScope);
|
|
4637
|
+
this.#activeSdkRunToken = consumedSdkRunTokenForCurrentRun;
|
|
4638
|
+
}
|
|
4057
4639
|
// Monitor task-notifications now carry the owned-completion envelope
|
|
4058
4640
|
// (see tools/monitor.ts), so the general drop + fresh-admission paths
|
|
4059
4641
|
// above cover them exactly like async results — no monitor-specific
|
|
@@ -4089,6 +4671,18 @@ export class AgentSession {
|
|
|
4089
4671
|
// submitter to the in-flight run instead of parking the correlation for
|
|
4090
4672
|
// an unrelated later agent_start (#4668).
|
|
4091
4673
|
this.agent.onSteeringConsumed = (messages, promotion = { startsOwnRun: false }) => {
|
|
4674
|
+
const consumedSdkRunToken = messages
|
|
4675
|
+
.map(message => this.#sdkRunTokensByQueuedMessage.get(message))
|
|
4676
|
+
.find((token): token is string => token !== undefined);
|
|
4677
|
+
if (
|
|
4678
|
+
consumedSdkRunToken !== undefined &&
|
|
4679
|
+
this.#activeAttemptScope !== undefined &&
|
|
4680
|
+
promotion.startsOwnRun !== true
|
|
4681
|
+
) {
|
|
4682
|
+
this.#sdkRunTokensByAttemptScope.set(this.#activeAttemptScope, consumedSdkRunToken);
|
|
4683
|
+
this.#skipPostPromptRecoveryWaitByAttemptScope.add(this.#activeAttemptScope);
|
|
4684
|
+
this.#activeSdkRunToken = consumedSdkRunToken;
|
|
4685
|
+
}
|
|
4092
4686
|
this.#fireQueuedPromotionHooks(messages, promotion);
|
|
4093
4687
|
};
|
|
4094
4688
|
this.agent.providerSessionState = this.#providerSessionState;
|
|
@@ -5001,6 +5595,10 @@ export class AgentSession {
|
|
|
5001
5595
|
* seen.
|
|
5002
5596
|
*/
|
|
5003
5597
|
#coordinatorToolObservations = new WeakMap<object, CoordinatorToolObservation>();
|
|
5598
|
+
#agentEventAdmission = new WeakMap<
|
|
5599
|
+
object,
|
|
5600
|
+
{ scope?: AttemptScope; sdkRunToken?: string; persistGeneration: number; persistBarrier?: Promise<void> }
|
|
5601
|
+
>();
|
|
5004
5602
|
|
|
5005
5603
|
/**
|
|
5006
5604
|
* Capture what is true at the SYNCHRONOUS agent-event boundary, before any async work.
|
|
@@ -5056,6 +5654,12 @@ export class AgentSession {
|
|
|
5056
5654
|
}
|
|
5057
5655
|
|
|
5058
5656
|
#trackAgentEvent = (event: AgentEvent): Promise<void> => {
|
|
5657
|
+
this.#agentEventAdmission.set(event, {
|
|
5658
|
+
scope: this.#activeAttemptScope,
|
|
5659
|
+
sdkRunToken: this.#activeSdkRunToken,
|
|
5660
|
+
persistGeneration: this.#coordinatorPersistGeneration,
|
|
5661
|
+
persistBarrier: this.#coordinatorRescopeBarrier,
|
|
5662
|
+
});
|
|
5059
5663
|
// First statement of the listener: the observation must precede every claim,
|
|
5060
5664
|
// reservation, and async hop this handler performs.
|
|
5061
5665
|
this.#observeCoordinatorToolEvent(event);
|
|
@@ -5125,10 +5729,10 @@ export class AgentSession {
|
|
|
5125
5729
|
this.#postPromptLeases.set(eventLease.resourceRunId, eventLease);
|
|
5126
5730
|
if (eventLease) {
|
|
5127
5731
|
await this.#runResourceLeaseContext.run(eventLease, () =>
|
|
5128
|
-
this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission),
|
|
5732
|
+
this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission, eventLease),
|
|
5129
5733
|
);
|
|
5130
5734
|
} else {
|
|
5131
|
-
await this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission);
|
|
5735
|
+
await this.#handleAgentEvent(event, activePromptHandle, canonicalAdmission, undefined);
|
|
5132
5736
|
}
|
|
5133
5737
|
} catch (error) {
|
|
5134
5738
|
logger.warn("Agent event handler failed", { event: event.type, error: String(error) });
|
|
@@ -5136,11 +5740,19 @@ export class AgentSession {
|
|
|
5136
5740
|
canonicalAdmission?.release();
|
|
5137
5741
|
if (eventLease) {
|
|
5138
5742
|
const pendingAgentEnd =
|
|
5139
|
-
event.type === "agent_end" &&
|
|
5743
|
+
event.type === "agent_end" &&
|
|
5744
|
+
!maintenanceCheckpoint &&
|
|
5745
|
+
(this.#pendingAgentEndEmit === event || this.#deferredAgentEndLeases.has(event))
|
|
5140
5746
|
? this.#pendingAgentEndEmit
|
|
5141
5747
|
: undefined;
|
|
5142
5748
|
if (pendingAgentEnd) {
|
|
5143
5749
|
this.#deferredAgentEndLeases.set(pendingAgentEnd, eventLease);
|
|
5750
|
+
} else if (
|
|
5751
|
+
event.type === "agent_end" &&
|
|
5752
|
+
!maintenanceCheckpoint &&
|
|
5753
|
+
this.#deferredAgentEndLeases.has(event)
|
|
5754
|
+
) {
|
|
5755
|
+
// Fast publication took ownership of the lease before the handler unwound.
|
|
5144
5756
|
} else {
|
|
5145
5757
|
if (event.type === "agent_end" && this.#postPromptLeases.get(eventLease.resourceRunId) === eventLease)
|
|
5146
5758
|
this.#postPromptLeases.delete(eventLease.resourceRunId);
|
|
@@ -5156,6 +5768,11 @@ export class AgentSession {
|
|
|
5156
5768
|
this.#resolveSessionSettlement();
|
|
5157
5769
|
}
|
|
5158
5770
|
})();
|
|
5771
|
+
this.#coordinatorEventHandlers.add(handler);
|
|
5772
|
+
void handler.then(
|
|
5773
|
+
() => this.#coordinatorEventHandlers.delete(handler),
|
|
5774
|
+
() => this.#coordinatorEventHandlers.delete(handler),
|
|
5775
|
+
);
|
|
5159
5776
|
if (eventLease) eventLease.track("post_prompt", "agent-session-event", handler);
|
|
5160
5777
|
return handler;
|
|
5161
5778
|
};
|
|
@@ -5163,46 +5780,125 @@ export class AgentSession {
|
|
|
5163
5780
|
/** Serializes sidecar writes in publication order, independent of write latency. */
|
|
5164
5781
|
#coordinatorPersistQueue: Promise<void> = Promise.resolve();
|
|
5165
5782
|
|
|
5783
|
+
#recordPostPublicationOutcome(
|
|
5784
|
+
context: { sessionId: string; cwd: string; sessionFile: string | undefined },
|
|
5785
|
+
correlationId: string | undefined,
|
|
5786
|
+
kind: "worker_integration" | "terminal_persistence",
|
|
5787
|
+
outcome: { status: "completed" | "failed" | "timed_out"; error?: string },
|
|
5788
|
+
): void {
|
|
5789
|
+
if (outcome.status !== "completed") {
|
|
5790
|
+
const correlation = correlationId ?? context.sessionId;
|
|
5791
|
+
const safeError = sanitizePostPublicationError(outcome.error);
|
|
5792
|
+
this.#emit({
|
|
5793
|
+
type: "notice",
|
|
5794
|
+
level: "error",
|
|
5795
|
+
source: kind,
|
|
5796
|
+
message: `${kind} ${outcome.status} for ${correlation}${safeError ? `: ${safeError}` : ""}`,
|
|
5797
|
+
});
|
|
5798
|
+
}
|
|
5799
|
+
const generation = this.#coordinatorPersistGeneration;
|
|
5800
|
+
const barrier = this.#coordinatorRescopeBarrier;
|
|
5801
|
+
const persist = async () => {
|
|
5802
|
+
if (barrier) {
|
|
5803
|
+
await barrier;
|
|
5804
|
+
return await persistCoordinatorWorkerIntegrationOutcome(
|
|
5805
|
+
{
|
|
5806
|
+
sessionId: this.sessionId,
|
|
5807
|
+
cwd: this.sessionManager.getCwd(),
|
|
5808
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
5809
|
+
},
|
|
5810
|
+
{
|
|
5811
|
+
...outcome,
|
|
5812
|
+
kind,
|
|
5813
|
+
correlationId,
|
|
5814
|
+
error: sanitizePostPublicationError(outcome.error),
|
|
5815
|
+
},
|
|
5816
|
+
);
|
|
5817
|
+
}
|
|
5818
|
+
return generation === this.#coordinatorPersistGeneration
|
|
5819
|
+
? await persistCoordinatorWorkerIntegrationOutcome(context, {
|
|
5820
|
+
...outcome,
|
|
5821
|
+
kind,
|
|
5822
|
+
correlationId,
|
|
5823
|
+
error: sanitizePostPublicationError(outcome.error),
|
|
5824
|
+
})
|
|
5825
|
+
: undefined;
|
|
5826
|
+
};
|
|
5827
|
+
const queued = barrier
|
|
5828
|
+
? barrier.then(() => this.#appendCoordinatorPersist(persist))
|
|
5829
|
+
: this.#appendCoordinatorPersist(persist);
|
|
5830
|
+
if (barrier) this.#trackReleasedBarrierPersist(queued);
|
|
5831
|
+
else this.#trackUnbarrieredCoordinatorPersist(queued);
|
|
5832
|
+
void queued.catch(error =>
|
|
5833
|
+
logger.warn("Failed to persist terminal reconciliation outcome", { error: String(error) }),
|
|
5834
|
+
);
|
|
5835
|
+
}
|
|
5836
|
+
|
|
5166
5837
|
/**
|
|
5167
5838
|
* Reserve this event's place in the sidecar write order.
|
|
5168
5839
|
*
|
|
5169
5840
|
* Called immediately BEFORE local delivery so a synchronous subscriber that re-enters
|
|
5170
5841
|
* the session cannot get its own, later event persisted first. The returned promise is
|
|
5171
|
-
* only
|
|
5842
|
+
* awaited only by callers that explicitly require a durable handoff; interactive
|
|
5843
|
+
* terminal delivery remains independent of this secondary sink.
|
|
5172
5844
|
*
|
|
5173
5845
|
* A pairing-only tool event takes no place in that order at all — neither its start nor
|
|
5174
5846
|
* its end. It describes a call that was never dispatched, and this file is read as the
|
|
5175
5847
|
* answer to "what is this session doing right now".
|
|
5176
5848
|
*/
|
|
5177
|
-
#queueCoordinatorRuntimeStatePersist(event: AgentSessionEvent): Promise<void> {
|
|
5849
|
+
#queueCoordinatorRuntimeStatePersist(event: AgentSessionEvent, propagateFailure = false): Promise<void> {
|
|
5178
5850
|
if (isNonDispatchedToolEvent(event)) return Promise.resolve();
|
|
5179
5851
|
const observation = this.#coordinatorToolObservations.get(event);
|
|
5180
|
-
const
|
|
5181
|
-
const
|
|
5182
|
-
|
|
5852
|
+
const admission = this.#agentEventAdmission.get(event);
|
|
5853
|
+
const barrier = admission?.persistBarrier;
|
|
5854
|
+
if (barrier) {
|
|
5855
|
+
const run = async () => {
|
|
5856
|
+
await barrier;
|
|
5857
|
+
await this.#persistRuntimeStateInBackground(
|
|
5858
|
+
event,
|
|
5859
|
+
{
|
|
5860
|
+
sessionId: this.sessionId,
|
|
5861
|
+
cwd: this.sessionManager.getCwd(),
|
|
5862
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
5863
|
+
},
|
|
5864
|
+
observation,
|
|
5865
|
+
propagateFailure,
|
|
5866
|
+
);
|
|
5867
|
+
};
|
|
5868
|
+
const queued = barrier.then(() => this.#appendCoordinatorPersist(run));
|
|
5869
|
+
this.#trackReleasedBarrierPersist(queued);
|
|
5870
|
+
return queued;
|
|
5871
|
+
}
|
|
5872
|
+
const context = {
|
|
5873
|
+
sessionId: this.sessionId,
|
|
5874
|
+
cwd: this.sessionManager.getCwd(),
|
|
5875
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
5876
|
+
};
|
|
5877
|
+
const generation = admission?.persistGeneration ?? this.#coordinatorPersistGeneration;
|
|
5878
|
+
const run = () =>
|
|
5879
|
+
generation === this.#coordinatorPersistGeneration
|
|
5880
|
+
? this.#persistRuntimeStateInBackground(event, context, observation, propagateFailure)
|
|
5881
|
+
: Promise.resolve();
|
|
5882
|
+
const queued = this.#appendCoordinatorPersist(run);
|
|
5883
|
+
this.#trackUnbarrieredCoordinatorPersist(queued);
|
|
5183
5884
|
return queued;
|
|
5184
5885
|
}
|
|
5185
5886
|
|
|
5186
5887
|
async #persistRuntimeStateInBackground(
|
|
5187
5888
|
event: AgentSessionEvent,
|
|
5889
|
+
context: { sessionId: string; cwd: string; sessionFile: string | undefined },
|
|
5188
5890
|
observation: CoordinatorToolObservation | undefined,
|
|
5891
|
+
propagateFailure: boolean,
|
|
5189
5892
|
): Promise<void> {
|
|
5190
5893
|
try {
|
|
5191
|
-
await persistCoordinatorRuntimeStateFromEvent(
|
|
5192
|
-
|
|
5193
|
-
{
|
|
5194
|
-
sessionId: this.sessionId,
|
|
5195
|
-
cwd: this.sessionManager.getCwd(),
|
|
5196
|
-
sessionFile: this.sessionManager.getSessionFile(),
|
|
5197
|
-
},
|
|
5198
|
-
observation,
|
|
5199
|
-
);
|
|
5200
|
-
} catch {
|
|
5894
|
+
await persistCoordinatorRuntimeStateFromEvent(event, context, observation);
|
|
5895
|
+
} catch (error) {
|
|
5201
5896
|
logger.warn("Failed to persist coordinator runtime state", { event: event.type });
|
|
5897
|
+
if (propagateFailure) throw error;
|
|
5202
5898
|
}
|
|
5203
5899
|
}
|
|
5204
5900
|
|
|
5205
|
-
async #emitSessionEvent(event: AgentSessionEvent): Promise<void> {
|
|
5901
|
+
async #emitSessionEvent(event: AgentSessionEvent, eventLease?: RunResourceProducerLease): Promise<void> {
|
|
5206
5902
|
const attemptScope = (event as AgentSessionEvent & { scope?: AttemptScope }).scope;
|
|
5207
5903
|
if (event.type === "turn_start") {
|
|
5208
5904
|
this.#extensionTurnGeneration++;
|
|
@@ -5236,6 +5932,18 @@ export class AgentSession {
|
|
|
5236
5932
|
// have unwound. Subscribers treat this event as the ready signal; flushing it
|
|
5237
5933
|
// from abort while either barrier is active permits a successor to race the
|
|
5238
5934
|
// prior prompt's cleanup.
|
|
5935
|
+
const sdkTerminal =
|
|
5936
|
+
event.type === "agent_end" && attemptScope !== undefined && this.#sdkRunTokensByAttemptScope.has(attemptScope);
|
|
5937
|
+
if (
|
|
5938
|
+
event.type === "agent_end" &&
|
|
5939
|
+
sdkTerminal &&
|
|
5940
|
+
this.#pendingAgentEndContinuationHolds.size === 0 &&
|
|
5941
|
+
this.#pendingAgentEndEmit === undefined
|
|
5942
|
+
) {
|
|
5943
|
+
if (eventLease) this.#deferredAgentEndLeases.set(event, eventLease);
|
|
5944
|
+
this.#startAgentEndPublication(event, eventLease);
|
|
5945
|
+
return;
|
|
5946
|
+
}
|
|
5239
5947
|
if (event.type === "agent_end" && (this.#livePromptsInFlight() > 0 || this.#agentEventHandlersInFlight > 0)) {
|
|
5240
5948
|
this.#pendingAgentEndEmit = event;
|
|
5241
5949
|
return;
|
|
@@ -5244,8 +5952,12 @@ export class AgentSession {
|
|
|
5244
5952
|
if (event.type === "agent_end") {
|
|
5245
5953
|
// Start the durable terminal write before synchronous subscribers can
|
|
5246
5954
|
// re-enter prompt(), so a successor's running transition serializes after it.
|
|
5247
|
-
|
|
5955
|
+
// Do not let a lock-hostile sidecar suppress the terminal event itself.
|
|
5956
|
+
const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(event);
|
|
5248
5957
|
this.#emit(event);
|
|
5958
|
+
void terminalPersistence.catch(error => {
|
|
5959
|
+
logger.warn("Failed to persist terminal coordinator runtime state", { error: String(error) });
|
|
5960
|
+
});
|
|
5249
5961
|
await this.#emitExtensionEvent(event);
|
|
5250
5962
|
return;
|
|
5251
5963
|
}
|
|
@@ -5341,6 +6053,7 @@ export class AgentSession {
|
|
|
5341
6053
|
event: AgentEvent,
|
|
5342
6054
|
activePromptHandle?: string,
|
|
5343
6055
|
canonicalAdmission?: CanonicalMessageAdmission,
|
|
6056
|
+
eventLease?: RunResourceProducerLease,
|
|
5344
6057
|
): Promise<void> => {
|
|
5345
6058
|
const attemptScope = (event as AgentEvent & { scope?: AttemptScope }).scope;
|
|
5346
6059
|
|
|
@@ -5680,7 +6393,7 @@ export class AgentSession {
|
|
|
5680
6393
|
});
|
|
5681
6394
|
}
|
|
5682
6395
|
|
|
5683
|
-
await this.#emitSessionEvent(displayEvent);
|
|
6396
|
+
await this.#emitSessionEvent(displayEvent, eventLease);
|
|
5684
6397
|
if (event.type === "message_end" && event.message.role === "assistant") {
|
|
5685
6398
|
this.#assistantAttemptScopes.set(event.message, {
|
|
5686
6399
|
scope: attemptScope,
|
|
@@ -6030,6 +6743,7 @@ export class AgentSession {
|
|
|
6030
6743
|
skipCompactionCheck: true,
|
|
6031
6744
|
resourceRunId: activePromptHandle,
|
|
6032
6745
|
maintenanceContinuation: true,
|
|
6746
|
+
sdkRunToken: attemptScope ? this.#sdkRunTokensByAttemptScope.get(attemptScope) : undefined,
|
|
6033
6747
|
});
|
|
6034
6748
|
}
|
|
6035
6749
|
return;
|
|
@@ -6126,9 +6840,11 @@ export class AgentSession {
|
|
|
6126
6840
|
});
|
|
6127
6841
|
}
|
|
6128
6842
|
this.#resolveRetry();
|
|
6843
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing) return;
|
|
6129
6844
|
|
|
6130
6845
|
const compactionTask = this.#schedulePostPromptTask(
|
|
6131
6846
|
async () => {
|
|
6847
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing) return;
|
|
6132
6848
|
await this.#checkCompaction(msg, true, undefined, activePromptHandle);
|
|
6133
6849
|
},
|
|
6134
6850
|
{ resourceRunId: activePromptHandle },
|
|
@@ -6201,9 +6917,11 @@ export class AgentSession {
|
|
|
6201
6917
|
selectionFenceGeneration: number,
|
|
6202
6918
|
lease?: RunResourceProducerLease,
|
|
6203
6919
|
leaseTask: Promise<void> = task,
|
|
6920
|
+
excludeFromRecovery = false,
|
|
6204
6921
|
): void {
|
|
6205
6922
|
this.#postPromptTasks.add(task);
|
|
6206
6923
|
this.#postPromptTaskSelectionFenceGenerations.set(task, selectionFenceGeneration);
|
|
6924
|
+
if (excludeFromRecovery) this.#postPromptTaskRecoveryExcluded.add(task);
|
|
6207
6925
|
this.#ensurePostPromptTasksPromise();
|
|
6208
6926
|
if (lease) {
|
|
6209
6927
|
lease.track("post_prompt", "agent-session", leaseTask);
|
|
@@ -6214,6 +6932,7 @@ export class AgentSession {
|
|
|
6214
6932
|
.finally(() => {
|
|
6215
6933
|
this.#postPromptTasks.delete(task);
|
|
6216
6934
|
this.#postPromptTaskSelectionFenceGenerations.delete(task);
|
|
6935
|
+
this.#postPromptTaskRecoveryExcluded.delete(task);
|
|
6217
6936
|
if (this.#postPromptTasks.size === 0) this.#resolvePostPromptTasks();
|
|
6218
6937
|
});
|
|
6219
6938
|
}
|
|
@@ -6227,6 +6946,7 @@ export class AgentSession {
|
|
|
6227
6946
|
resourceRunId?: string;
|
|
6228
6947
|
leaseTask?: Promise<void>;
|
|
6229
6948
|
selectionFenceGeneration?: number;
|
|
6949
|
+
excludeFromPostPromptRecovery?: boolean;
|
|
6230
6950
|
},
|
|
6231
6951
|
): Promise<void> {
|
|
6232
6952
|
const selectionFenceGeneration =
|
|
@@ -6253,7 +6973,11 @@ export class AgentSession {
|
|
|
6253
6973
|
options?.onSkip?.();
|
|
6254
6974
|
return Promise.resolve();
|
|
6255
6975
|
}
|
|
6256
|
-
const
|
|
6976
|
+
const taskSignal = reservation?.ok ? reservation.lease.signal : this.#postPromptTasksAbortController.signal;
|
|
6977
|
+
// Disposal is a session-wide terminal fence. Compose it with the selected
|
|
6978
|
+
// logical-resource signal so stale leases from earlier runs cannot keep a
|
|
6979
|
+
// post-prompt task alive after the current run was quarantined.
|
|
6980
|
+
const signal = AbortSignal.any([taskSignal, this.#disposeAbortController.signal]);
|
|
6257
6981
|
const runScheduled = async () => {
|
|
6258
6982
|
if (delayMs > 0) {
|
|
6259
6983
|
try {
|
|
@@ -6281,6 +7005,7 @@ export class AgentSession {
|
|
|
6281
7005
|
selectionFenceGeneration,
|
|
6282
7006
|
reservation?.ok ? reservation.lease : undefined,
|
|
6283
7007
|
options?.leaseTask,
|
|
7008
|
+
options?.excludeFromPostPromptRecovery,
|
|
6284
7009
|
);
|
|
6285
7010
|
return scheduled;
|
|
6286
7011
|
}
|
|
@@ -6317,8 +7042,11 @@ export class AgentSession {
|
|
|
6317
7042
|
predecessorAgentEndHold?: symbol;
|
|
6318
7043
|
/** Internal predecessor terminal event sequestered while waiting behind selection. */
|
|
6319
7044
|
deferredPredecessorAgentEnd?: AgentSessionEvent;
|
|
7045
|
+
/** Internal causal SDK owner captured when this continuation was scheduled. */
|
|
7046
|
+
sdkRunToken?: string;
|
|
6320
7047
|
}): Promise<void> {
|
|
6321
7048
|
const continuationAdmission = this.#captureScheduledContinuationAdmission();
|
|
7049
|
+
const scheduledSdkRunToken = options?.sdkRunToken;
|
|
6322
7050
|
const selectionFenceGeneration =
|
|
6323
7051
|
options?.selectionFenceGeneration ??
|
|
6324
7052
|
this.#selectionFenceGenerationContext.getStore() ??
|
|
@@ -6340,6 +7068,7 @@ export class AgentSession {
|
|
|
6340
7068
|
generation: deferredPromptGeneration,
|
|
6341
7069
|
selectionFenceGeneration,
|
|
6342
7070
|
deferredPredecessorAgentEnd,
|
|
7071
|
+
sdkRunToken: scheduledSdkRunToken,
|
|
6343
7072
|
});
|
|
6344
7073
|
} finally {
|
|
6345
7074
|
// The recursive call synchronously re-reserved its settlement
|
|
@@ -6410,7 +7139,7 @@ export class AgentSession {
|
|
|
6410
7139
|
settleLease();
|
|
6411
7140
|
return;
|
|
6412
7141
|
}
|
|
6413
|
-
await this.#awaitStartupTurnBarrier();
|
|
7142
|
+
await this.#awaitStartupTurnBarrier(scheduledSignal);
|
|
6414
7143
|
if (!canContinue()) {
|
|
6415
7144
|
settleLease();
|
|
6416
7145
|
return;
|
|
@@ -6475,18 +7204,18 @@ export class AgentSession {
|
|
|
6475
7204
|
// agent_start (review P1); their queued messages are in-run
|
|
6476
7205
|
// consumptions, not own-run promotions.
|
|
6477
7206
|
const startsOwn = options?.maintenanceContinuation !== true;
|
|
6478
|
-
const
|
|
6479
|
-
?
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
7207
|
+
const inheritedSdkRunToken = options?.continueQueuedOnly
|
|
7208
|
+
? undefined
|
|
7209
|
+
: (options?.sdkRunToken ?? scheduledSdkRunToken);
|
|
7210
|
+
const consumedSdkRunToken = acceptance.consumedQueuedMessages
|
|
7211
|
+
.map(message => this.#sdkRunTokensByQueuedMessage.get(message))
|
|
7212
|
+
.find((token): token is string => token !== undefined);
|
|
7213
|
+
const sdkRunToken = consumedSdkRunToken ?? inheritedSdkRunToken;
|
|
6483
7214
|
this.#fireQueuedPromotionHooks(acceptance.consumedQueuedMessages, {
|
|
6484
7215
|
startsOwnRun: startsOwn,
|
|
6485
7216
|
});
|
|
6486
|
-
if (
|
|
6487
|
-
|
|
6488
|
-
if (startsOwn) this.#activeSdkRunToken = consumedSdkRunToken;
|
|
6489
|
-
this.#acceptRunHandle(handle);
|
|
7217
|
+
if (startsOwn) this.#activeSdkRunToken = sdkRunToken;
|
|
7218
|
+
this.#acceptSdkAttemptRun(handle, sdkRunToken);
|
|
6490
7219
|
options?.onRunAccepted?.(handle);
|
|
6491
7220
|
// Keep the queued token available through the acceptance callback;
|
|
6492
7221
|
// SDK follow-up owners bind it to the new attempt scope there.
|
|
@@ -6649,8 +7378,12 @@ export class AgentSession {
|
|
|
6649
7378
|
resourceRunId?: string,
|
|
6650
7379
|
deferredSelectionFenceGeneration?: number,
|
|
6651
7380
|
deferredPredecessorAgentEnd?: AgentSessionEvent,
|
|
7381
|
+
sdkRunToken?: string,
|
|
6652
7382
|
): void {
|
|
6653
7383
|
const scheduledGeneration = generation;
|
|
7384
|
+
const scheduledSdkRunToken =
|
|
7385
|
+
sdkRunToken ??
|
|
7386
|
+
(this.#activeAttemptScope ? this.#sdkRunTokensByAttemptScope.get(this.#activeAttemptScope) : undefined);
|
|
6654
7387
|
const continuationAuthorized = async (
|
|
6655
7388
|
signal: AbortSignal,
|
|
6656
7389
|
hasPendingNextTurnMessages = false,
|
|
@@ -6703,6 +7436,7 @@ export class AgentSession {
|
|
|
6703
7436
|
resourceRunId,
|
|
6704
7437
|
selectionFenceGeneration,
|
|
6705
7438
|
predecessorAgentEnd,
|
|
7439
|
+
scheduledSdkRunToken,
|
|
6706
7440
|
);
|
|
6707
7441
|
} finally {
|
|
6708
7442
|
this.#endSelectionFenceDeferralTracking(selectionFenceGeneration);
|
|
@@ -6750,6 +7484,7 @@ export class AgentSession {
|
|
|
6750
7484
|
promptText,
|
|
6751
7485
|
{
|
|
6752
7486
|
skipPostPromptRecoveryWait: true,
|
|
7487
|
+
sdkRunToken: scheduledSdkRunToken,
|
|
6753
7488
|
skipCompactionCheck: true,
|
|
6754
7489
|
predecessorAgentEndHold,
|
|
6755
7490
|
onFinalPreflight: ({ hasPendingNextTurnMessages }) =>
|
|
@@ -6808,6 +7543,7 @@ export class AgentSession {
|
|
|
6808
7543
|
this.#postPromptTasksAbortController = new AbortController();
|
|
6809
7544
|
this.#postPromptTasks.clear();
|
|
6810
7545
|
this.#postPromptTaskSelectionFenceGenerations.clear();
|
|
7546
|
+
this.#postPromptTaskRecoveryExcluded.clear();
|
|
6811
7547
|
this.#releaseDeferredAgentEndContinuations();
|
|
6812
7548
|
this.#resolveTtsrResume();
|
|
6813
7549
|
this.#resolvePostPromptTasks();
|
|
@@ -6829,8 +7565,11 @@ export class AgentSession {
|
|
|
6829
7565
|
await this.#ttsrResumePromise;
|
|
6830
7566
|
continue;
|
|
6831
7567
|
}
|
|
6832
|
-
|
|
6833
|
-
|
|
7568
|
+
const recoveryTasks = [...this.#postPromptTasks].filter(
|
|
7569
|
+
task => !this.#postPromptTaskRecoveryExcluded.has(task),
|
|
7570
|
+
);
|
|
7571
|
+
if (recoveryTasks.length > 0) {
|
|
7572
|
+
await Promise.allSettled(recoveryTasks);
|
|
6834
7573
|
continue;
|
|
6835
7574
|
}
|
|
6836
7575
|
// Tracked post-prompt tasks cover deferred continuations scheduled from
|
|
@@ -6852,7 +7591,9 @@ export class AgentSession {
|
|
|
6852
7591
|
*/
|
|
6853
7592
|
async #waitForPostPromptTasksBeforeSelectionFence(selectionFenceGeneration: number): Promise<void> {
|
|
6854
7593
|
const precedingTasks = [...this.#postPromptTasks].filter(
|
|
6855
|
-
task =>
|
|
7594
|
+
task =>
|
|
7595
|
+
!this.#postPromptTaskRecoveryExcluded.has(task) &&
|
|
7596
|
+
(this.#postPromptTaskSelectionFenceGenerations.get(task) ?? 0) < selectionFenceGeneration,
|
|
6856
7597
|
);
|
|
6857
7598
|
if (precedingTasks.length > 0) await Promise.allSettled(precedingTasks);
|
|
6858
7599
|
}
|
|
@@ -7527,16 +8268,16 @@ export class AgentSession {
|
|
|
7527
8268
|
this.#workerIntegrationScheduler?.enqueue();
|
|
7528
8269
|
}
|
|
7529
8270
|
|
|
7530
|
-
async #flushWorkerIntegrationAttempt(): Promise<
|
|
7531
|
-
await this.#workerIntegrationScheduler?.
|
|
8271
|
+
async #flushWorkerIntegrationAttempt(): Promise<WorkerIntegrationOutcome> {
|
|
8272
|
+
return (await this.#workerIntegrationScheduler?.flushWithOutcome()) ?? { status: "completed" };
|
|
7532
8273
|
}
|
|
7533
8274
|
|
|
7534
|
-
async #flushWorkerIntegrationForAgentEnd(): Promise<
|
|
8275
|
+
async #flushWorkerIntegrationForAgentEnd(): Promise<WorkerIntegrationOutcome> {
|
|
7535
8276
|
if (!this.#workerIntegrationRequestedForTurn) {
|
|
7536
8277
|
this.#requestWorkerIntegrationAttempt();
|
|
7537
8278
|
}
|
|
7538
8279
|
try {
|
|
7539
|
-
await this.#flushWorkerIntegrationAttempt();
|
|
8280
|
+
return await this.#flushWorkerIntegrationAttempt();
|
|
7540
8281
|
} finally {
|
|
7541
8282
|
this.#workerIntegrationRequestedForTurn = false;
|
|
7542
8283
|
}
|
|
@@ -7549,6 +8290,9 @@ export class AgentSession {
|
|
|
7549
8290
|
workerIntegrationSettled = false,
|
|
7550
8291
|
scope?: AttemptScopeRef,
|
|
7551
8292
|
): Promise<void> {
|
|
8293
|
+
const admission = this.#agentEventAdmission.get(event);
|
|
8294
|
+
const activeAttemptScopeAtEvent = admission === undefined ? this.#activeAttemptScope : admission.scope;
|
|
8295
|
+
const activeSdkRunTokenAtEvent = admission === undefined ? this.#activeSdkRunToken : admission.sdkRunToken;
|
|
7552
8296
|
if (event.type === "agent_end" && !workerIntegrationSettled) {
|
|
7553
8297
|
await this.#flushWorkerIntegrationForAgentEnd();
|
|
7554
8298
|
}
|
|
@@ -7559,20 +8303,36 @@ export class AgentSession {
|
|
|
7559
8303
|
? eventToken
|
|
7560
8304
|
: deliveryScope
|
|
7561
8305
|
? this.#sdkRunTokensByAttemptScope.get(deliveryScope as AttemptScope)
|
|
7562
|
-
:
|
|
8306
|
+
: undefined;
|
|
7563
8307
|
const isTerminalAgentEnd =
|
|
7564
8308
|
event.type === "agent_end" && !(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted");
|
|
7565
8309
|
const finishAttempt = () => {
|
|
7566
8310
|
if (!isTerminalAgentEnd) return;
|
|
8311
|
+
// A merged wake batch whose flush fired while still streaming cleared its
|
|
8312
|
+
// pending flag without rescheduling. Returning to idle is exactly when it
|
|
8313
|
+
// becomes deliverable again, so rearm here rather than waiting for an
|
|
8314
|
+
// unrelated enqueue.
|
|
8315
|
+
this.yieldQueue.rearmIdle();
|
|
7567
8316
|
const isActiveAttempt =
|
|
7568
|
-
deliveryScope === undefined
|
|
7569
|
-
|
|
7570
|
-
|
|
8317
|
+
deliveryScope === undefined
|
|
8318
|
+
? this.#activeAttemptScope === activeAttemptScopeAtEvent &&
|
|
8319
|
+
this.#activeSdkRunToken === activeSdkRunTokenAtEvent
|
|
8320
|
+
: deliveryScope === activeAttemptScopeAtEvent &&
|
|
8321
|
+
deliveryScope === this.#activeAttemptScope &&
|
|
8322
|
+
this.#activeSdkRunToken === activeSdkRunTokenAtEvent;
|
|
8323
|
+
if (this.#foldStopRequested) {
|
|
8324
|
+
this.#foldStopRequested = false;
|
|
8325
|
+
this.agent.setSteeringAdmissionFence(undefined);
|
|
8326
|
+
}
|
|
7571
8327
|
if (deliveryScope) {
|
|
7572
8328
|
this.#attemptRecordStore.retire(deliveryScope as AttemptScope);
|
|
7573
|
-
|
|
8329
|
+
if (
|
|
8330
|
+
activeSdkRunTokenAtEvent !== undefined &&
|
|
8331
|
+
this.#sdkRunTokensByAttemptScope.get(deliveryScope as AttemptScope) === activeSdkRunTokenAtEvent
|
|
8332
|
+
)
|
|
8333
|
+
this.#sdkRunTokensByAttemptScope.delete(deliveryScope as AttemptScope);
|
|
7574
8334
|
}
|
|
7575
|
-
if (isActiveAttempt
|
|
8335
|
+
if (isActiveAttempt) this.#activeSdkRunToken = undefined;
|
|
7576
8336
|
if (isActiveAttempt) {
|
|
7577
8337
|
this.#activeAttemptScope = undefined;
|
|
7578
8338
|
this.#activeLogicalRunId = undefined;
|
|
@@ -7840,8 +8600,18 @@ export class AgentSession {
|
|
|
7840
8600
|
const sid = this.#providerSessionId ?? sessionId ?? this.sessionManager.getSessionId();
|
|
7841
8601
|
this.agent.sessionId = sid;
|
|
7842
8602
|
this.agent.providerSessionId = this.#providerCacheSessionId ?? sid;
|
|
7843
|
-
this.agent.setMetadataResolver(
|
|
7844
|
-
buildSessionMetadata(
|
|
8603
|
+
this.agent.setMetadataResolver(context =>
|
|
8604
|
+
buildSessionMetadata(
|
|
8605
|
+
sid,
|
|
8606
|
+
context.provider,
|
|
8607
|
+
this.#modelRegistry.authStorage,
|
|
8608
|
+
this.credentialSessionId,
|
|
8609
|
+
context.model ?? this.model,
|
|
8610
|
+
// Optional call: the metadata resolver runs on every prompt, and `owner` is an
|
|
8611
|
+
// optional `buildSessionMetadata` argument. The auth-credential paths keep the
|
|
8612
|
+
// strict call because they genuinely require a real registry owner.
|
|
8613
|
+
this.#modelRegistry.getAuthStorageOwner?.(),
|
|
8614
|
+
),
|
|
7845
8615
|
);
|
|
7846
8616
|
}
|
|
7847
8617
|
|
|
@@ -7945,13 +8715,35 @@ export class AgentSession {
|
|
|
7945
8715
|
if (this.#disposePromise) return this.#disposePromise;
|
|
7946
8716
|
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
7947
8717
|
this.#disposePromise = promise;
|
|
8718
|
+
this.#abortAdmissionEpoch++;
|
|
8719
|
+
this.#isDisposed = true;
|
|
8720
|
+
this.#disposeAbortController.abort();
|
|
8721
|
+
this.#disposeAdmissionClosed = this.#closeSessionAdmission();
|
|
8722
|
+
this.#disposePostPromptDrain = this.#cancelPostPromptTasks();
|
|
8723
|
+
this.#settleDeliveredOwnedRegistrations(this.#pendingNextTurnMessages.map(entry => entry.message));
|
|
8724
|
+
this.#pendingNextTurnMessages = [];
|
|
8725
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
8726
|
+
this.#abortActiveMidRunBarriers();
|
|
8727
|
+
this.abortCompaction();
|
|
8728
|
+
this.abortRetry();
|
|
8729
|
+
this.#quarantineAgentRunResources();
|
|
8730
|
+
this.agent.abort();
|
|
8731
|
+
this.agent.setMainAttemptScopeObserver(undefined);
|
|
8732
|
+
this.#disconnectFromAgent();
|
|
7948
8733
|
void this.#dispose().then(resolve, reject);
|
|
7949
8734
|
return promise;
|
|
7950
8735
|
}
|
|
7951
8736
|
|
|
8737
|
+
/** Cancel the active logical run domain, including managed continuations detached from Agent's active attempt. */
|
|
8738
|
+
#quarantineAgentRunResources(): void {
|
|
8739
|
+
const resourceRunId = this.agent.currentManagedLogicalRunId ?? this.agent.activeResourceRunId;
|
|
8740
|
+
if (resourceRunId !== undefined) this.agent.resourceLedger.quarantine(String(resourceRunId));
|
|
8741
|
+
}
|
|
8742
|
+
|
|
7952
8743
|
async #dispose(): Promise<void> {
|
|
7953
|
-
|
|
7954
|
-
|
|
8744
|
+
const admissionClosed = this.#disposeAdmissionClosed ?? this.#closeSessionAdmission();
|
|
8745
|
+
await admissionClosed;
|
|
8746
|
+
await this.sessionManager.closeCwdMoveAdmission();
|
|
7955
8747
|
this.#isDisposed = true;
|
|
7956
8748
|
// Reject new direct Python starts as soon as disposal begins (synchronously,
|
|
7957
8749
|
// before any await) so callers cannot race a start against teardown.
|
|
@@ -7989,18 +8781,22 @@ export class AgentSession {
|
|
|
7989
8781
|
this.agent.forceAbort("Session disposed");
|
|
7990
8782
|
}
|
|
7991
8783
|
}
|
|
7992
|
-
await admissionClosed;
|
|
7993
8784
|
await this.#agentEndPublicationPromise;
|
|
7994
8785
|
await this.#queuedExtensionEvents;
|
|
8786
|
+
await this.#agentEndHandlingPromise;
|
|
7995
8787
|
// Drain the sidecar write order for the same reason the two queues above are
|
|
7996
8788
|
// drained: each entry writes under the native identity-bound state-file lock, so a
|
|
7997
|
-
// still-queued write would run after the session that owns it is gone — releasing
|
|
8789
|
+
// a still-queued write would run after the session that owns it is gone — releasing
|
|
7998
8790
|
// a lock whose owner no longer exists, and under `bun test --isolate` calling into
|
|
7999
8791
|
// the addon after the runtime tore down the context it was scheduled in. The chain
|
|
8000
8792
|
// is already failure-absorbing, so this only waits.
|
|
8001
8793
|
await this.#coordinatorPersistQueue;
|
|
8794
|
+
// Terminal publication records its reconciliation from a promise reaction, which
|
|
8795
|
+
// can append after the queue snapshot above was awaited. Drain the tracked writes
|
|
8796
|
+
// again so disposal cannot return while that late reconciliation still owns a lock.
|
|
8797
|
+
await this.#drainUnbarrieredCoordinatorPersists();
|
|
8002
8798
|
this.#pendingBackgroundExchanges = [];
|
|
8003
|
-
this
|
|
8799
|
+
this.#drainTerminalOwnedYieldEntries();
|
|
8004
8800
|
|
|
8005
8801
|
this.agent.setOnBeforeYield(undefined);
|
|
8006
8802
|
try {
|
|
@@ -8014,12 +8810,25 @@ export class AgentSession {
|
|
|
8014
8810
|
this.#workflowGateEmitter = undefined;
|
|
8015
8811
|
this.#notifyWorkflowGateEmitterChanged(this.sessionId, undefined);
|
|
8016
8812
|
await this.#flushWorkerIntegrationAttempt();
|
|
8017
|
-
|
|
8813
|
+
// Worker integration completion can enqueue terminal reconciliation while the
|
|
8814
|
+
// flush above is pending; drain that late producer before disposal resolves.
|
|
8815
|
+
await this.#coordinatorPersistQueue;
|
|
8816
|
+
await this.#drainUnbarrieredCoordinatorPersists();
|
|
8817
|
+
await (this.#disposePostPromptDrain ?? this.#cancelPostPromptTasks());
|
|
8018
8818
|
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
8019
8819
|
// leak its background bash/task work into the parent's manager. Only
|
|
8020
8820
|
// the session that owns the manager goes on to dispose it (which itself
|
|
8021
8821
|
// nukes any leftover jobs and pending deliveries).
|
|
8022
8822
|
this.#cancelOwnAsyncJobs();
|
|
8823
|
+
// Final drain (Codex review P2 on #5088): an async-job completion that
|
|
8824
|
+
// enqueued during the awaited session_shutdown window above survived the
|
|
8825
|
+
// first drain with a retained delivery claim, and the already-aborted
|
|
8826
|
+
// disposal signal makes its idle-flush task exit before YieldQueue.flush
|
|
8827
|
+
// can run. Producers are now cancelled, so drain once more to settle any
|
|
8828
|
+
// straggler envelope and release its claim before owned-manager teardown
|
|
8829
|
+
// — a shared manager would otherwise keep the claim past this session's
|
|
8830
|
+
// disposal.
|
|
8831
|
+
this.#drainTerminalOwnedYieldEntries();
|
|
8023
8832
|
await Promise.allSettled(this.#deferredOwnerShutdownFinalizations);
|
|
8024
8833
|
const ownedAsyncManager = this.#ownedAsyncJobManager;
|
|
8025
8834
|
if (ownedAsyncManager && this.#disposeAsyncJobManager) {
|
|
@@ -8056,6 +8865,14 @@ export class AgentSession {
|
|
|
8056
8865
|
this.#unregisterSessionMemorySettings = undefined;
|
|
8057
8866
|
if (ownerTerminalContextFromEnvironment() === null) this.#unregisterRuntimeStateFinalizer?.();
|
|
8058
8867
|
this.#unregisterRuntimeStateFinalizer = undefined;
|
|
8868
|
+
this.#unregisterBeforeMoveListener?.();
|
|
8869
|
+
this.#unregisterBeforeMoveListener = undefined;
|
|
8870
|
+
this.#unregisterMoveAbortListener?.();
|
|
8871
|
+
this.#unregisterMoveAbortListener = undefined;
|
|
8872
|
+
this.#unregisterMovePublicationListener?.();
|
|
8873
|
+
this.#unregisterMovePublicationListener = undefined;
|
|
8874
|
+
this.#unregisterAfterMoveListener?.();
|
|
8875
|
+
this.#unregisterAfterMoveListener = undefined;
|
|
8059
8876
|
await releaseTabsForOwner(this.sessionManager.getSessionId()).catch((error: unknown) =>
|
|
8060
8877
|
logger.warn("session dispose: releaseTabsForOwner failed", { error }),
|
|
8061
8878
|
);
|
|
@@ -8220,14 +9037,24 @@ export class AgentSession {
|
|
|
8220
9037
|
await this.#waitForPostPromptTasksBeforeSelectionFence(ignoreSelectionFenceGeneration);
|
|
8221
9038
|
}
|
|
8222
9039
|
await this.#waitForSessionSettlement(ignoreSelectionFenceGeneration);
|
|
9040
|
+
const hasBlockingPostPromptTasks =
|
|
9041
|
+
ignoreSelectionFenceGeneration === undefined
|
|
9042
|
+
? this.#postPromptTasks.size > 0
|
|
9043
|
+
: [...this.#postPromptTasks].some(
|
|
9044
|
+
task =>
|
|
9045
|
+
!this.#postPromptTaskRecoveryExcluded.has(task) &&
|
|
9046
|
+
(this.#postPromptTaskSelectionFenceGenerations.get(task) ?? 0) < ignoreSelectionFenceGeneration,
|
|
9047
|
+
);
|
|
8223
9048
|
if (
|
|
8224
9049
|
!this.agent.state.isStreaming &&
|
|
8225
9050
|
!this.#retryPromise &&
|
|
8226
9051
|
!this.#ttsrResumePromise &&
|
|
8227
|
-
!
|
|
9052
|
+
!hasBlockingPostPromptTasks &&
|
|
8228
9053
|
!this.#isSessionSettlementPending(ignoreSelectionFenceGeneration)
|
|
8229
9054
|
)
|
|
8230
9055
|
return;
|
|
9056
|
+
if (ignoreSelectionFenceGeneration === undefined && hasBlockingPostPromptTasks && this.#postPromptTasksPromise)
|
|
9057
|
+
await this.#postPromptTasksPromise;
|
|
8231
9058
|
}
|
|
8232
9059
|
}
|
|
8233
9060
|
|
|
@@ -8252,6 +9079,35 @@ export class AgentSession {
|
|
|
8252
9079
|
await this.sessionManager.flush();
|
|
8253
9080
|
}
|
|
8254
9081
|
|
|
9082
|
+
queueCoordinatorRuntimeStatePersistForTests(event: AgentSessionEvent, gate: Promise<void>): Promise<void> {
|
|
9083
|
+
this.#agentEventAdmission.set(event, {
|
|
9084
|
+
persistGeneration: this.#coordinatorPersistGeneration,
|
|
9085
|
+
persistBarrier: this.#coordinatorRescopeBarrier,
|
|
9086
|
+
});
|
|
9087
|
+
const task = (async () => {
|
|
9088
|
+
await gate;
|
|
9089
|
+
await this.#queueCoordinatorRuntimeStatePersist(event, true);
|
|
9090
|
+
})();
|
|
9091
|
+
this.#coordinatorEventHandlers.add(task);
|
|
9092
|
+
void task.then(
|
|
9093
|
+
() => this.#coordinatorEventHandlers.delete(task),
|
|
9094
|
+
() => this.#coordinatorEventHandlers.delete(task),
|
|
9095
|
+
);
|
|
9096
|
+
return task;
|
|
9097
|
+
}
|
|
9098
|
+
|
|
9099
|
+
parkAgentEndForCoordinatorPersistForTests(event: Extract<AgentSessionEvent, { type: "agent_end" }>): void {
|
|
9100
|
+
this.#agentEventAdmission.set(event, {
|
|
9101
|
+
persistGeneration: this.#coordinatorPersistGeneration,
|
|
9102
|
+
persistBarrier: this.#coordinatorRescopeBarrier,
|
|
9103
|
+
});
|
|
9104
|
+
this.#pendingAgentEndEmit = event;
|
|
9105
|
+
}
|
|
9106
|
+
|
|
9107
|
+
flushParkedAgentEndForCoordinatorPersistForTests(): void {
|
|
9108
|
+
this.#flushPendingAgentEnd();
|
|
9109
|
+
}
|
|
9110
|
+
|
|
8255
9111
|
async drainAsyncJobDeliveriesForAcp(options?: { timeoutMs?: number }): Promise<boolean> {
|
|
8256
9112
|
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
8257
9113
|
if (!manager) return false;
|
|
@@ -8454,13 +9310,39 @@ export class AgentSession {
|
|
|
8454
9310
|
* handler, so Ctrl+B-style folding fails closed instead of aborting or
|
|
8455
9311
|
* shell-suspending arbitrary work.
|
|
8456
9312
|
*/
|
|
8457
|
-
|
|
8458
|
-
this.#
|
|
8459
|
-
|
|
8460
|
-
|
|
8461
|
-
|
|
8462
|
-
|
|
8463
|
-
|
|
9313
|
+
registerForegroundFoldParticipant(adapter: FoldAdapter): () => void {
|
|
9314
|
+
return this.#foldCoordinator.registerParticipant(adapter);
|
|
9315
|
+
}
|
|
9316
|
+
|
|
9317
|
+
/** The session-owned fold coordinator, for delivery-side receipt lookup. */
|
|
9318
|
+
get foldCoordinator(): FoldCoordinator {
|
|
9319
|
+
return this.#foldCoordinator;
|
|
9320
|
+
}
|
|
9321
|
+
|
|
9322
|
+
/**
|
|
9323
|
+
* Best-effort capture of what the interrupted turn still intended to do, so
|
|
9324
|
+
* the wake turn can finish the original task instead of only reporting output.
|
|
9325
|
+
*/
|
|
9326
|
+
#captureRemainingIntentForFold(): string | undefined {
|
|
9327
|
+
const messages = this.agent.state.messages;
|
|
9328
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
9329
|
+
const message = messages[index];
|
|
9330
|
+
if (message?.role !== "user") continue;
|
|
9331
|
+
const content = message.content;
|
|
9332
|
+
const text =
|
|
9333
|
+
typeof content === "string"
|
|
9334
|
+
? content
|
|
9335
|
+
: Array.isArray(content)
|
|
9336
|
+
? content
|
|
9337
|
+
.map(block => (typeof block === "object" && block && "text" in block ? String(block.text) : ""))
|
|
9338
|
+
.filter(Boolean)
|
|
9339
|
+
.join("\n")
|
|
9340
|
+
: "";
|
|
9341
|
+
const trimmed = text.trim();
|
|
9342
|
+
if (trimmed.length === 0) return undefined;
|
|
9343
|
+
return trimmed.length > 2000 ? `${trimmed.slice(0, 2000)}…` : trimmed;
|
|
9344
|
+
}
|
|
9345
|
+
return undefined;
|
|
8464
9346
|
}
|
|
8465
9347
|
|
|
8466
9348
|
/**
|
|
@@ -8469,7 +9351,7 @@ export class AgentSession {
|
|
|
8469
9351
|
* no fold target exists.
|
|
8470
9352
|
*/
|
|
8471
9353
|
hasForegroundBashBackgroundRequestHandler(): boolean {
|
|
8472
|
-
return this.#
|
|
9354
|
+
return this.#foldCoordinator.hasFoldableParticipant();
|
|
8473
9355
|
}
|
|
8474
9356
|
|
|
8475
9357
|
/** Set the SDK permission policy used by guarded ACP tool execution. */
|
|
@@ -8505,11 +9387,17 @@ export class AgentSession {
|
|
|
8505
9387
|
* Ask the active managed foreground bash call to return as a background job.
|
|
8506
9388
|
* Returns false when no supported foreground tool is currently backgroundable.
|
|
8507
9389
|
*/
|
|
8508
|
-
requestForegroundBashBackground(): boolean {
|
|
8509
|
-
|
|
8510
|
-
|
|
8511
|
-
|
|
8512
|
-
|
|
9390
|
+
async requestForegroundBashBackground(): Promise<boolean> {
|
|
9391
|
+
if (!this.#foldCoordinator.hasFoldableParticipant()) return false;
|
|
9392
|
+
try {
|
|
9393
|
+
const result = await this.#foldCoordinator.requestFold();
|
|
9394
|
+
return result.status === "folded";
|
|
9395
|
+
} catch (error) {
|
|
9396
|
+
logger.warn("Foreground fold request failed", {
|
|
9397
|
+
error: error instanceof Error ? error.message : String(error),
|
|
9398
|
+
});
|
|
9399
|
+
return false;
|
|
9400
|
+
}
|
|
8513
9401
|
}
|
|
8514
9402
|
|
|
8515
9403
|
/**
|
|
@@ -9450,6 +10338,7 @@ export class AgentSession {
|
|
|
9450
10338
|
if (this.#sessionAdmissionClosed || this.#isDisposed) throw this.#sessionAdmissionBusyError();
|
|
9451
10339
|
const inFlightPrompt = this.#beginInFlight();
|
|
9452
10340
|
let hindsightRecall: string | undefined;
|
|
10341
|
+
let continuationSdkRunToken: string | undefined;
|
|
9453
10342
|
try {
|
|
9454
10343
|
const volatileProjectContextMessage = await this.#buildVolatileProjectContextMessage();
|
|
9455
10344
|
this.agent.appendMessage(volatileProjectContextMessage);
|
|
@@ -9485,14 +10374,18 @@ export class AgentSession {
|
|
|
9485
10374
|
await this.agent.continue({
|
|
9486
10375
|
...this.#managedFallbackPromptOptions(),
|
|
9487
10376
|
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
9488
|
-
this.#
|
|
10377
|
+
continuationSdkRunToken = this.#activeSdkRunToken;
|
|
10378
|
+
this.#acceptSdkAttemptRun(handle, continuationSdkRunToken);
|
|
9489
10379
|
if (hindsightRecall) hindsightState?.markRecallSnippetInjected(hindsightRecall);
|
|
9490
10380
|
},
|
|
9491
10381
|
});
|
|
9492
|
-
await this.#waitForPostPromptRecovery();
|
|
10382
|
+
if (continuationSdkRunToken === undefined) await this.#waitForPostPromptRecovery();
|
|
9493
10383
|
} finally {
|
|
9494
10384
|
this.#removeEphemeralCustomMessages();
|
|
9495
|
-
await this.#settleEndedInFlight(
|
|
10385
|
+
await this.#settleEndedInFlight(
|
|
10386
|
+
inFlightPrompt,
|
|
10387
|
+
continuationSdkRunToken === undefined ? "full" : "publication",
|
|
10388
|
+
);
|
|
9496
10389
|
}
|
|
9497
10390
|
}
|
|
9498
10391
|
|
|
@@ -9620,8 +10513,9 @@ export class AgentSession {
|
|
|
9620
10513
|
async setCredentialPin(provider: string, selector: AuthCredentialSelector): Promise<void> {
|
|
9621
10514
|
const scopeId = this.credentialSessionId;
|
|
9622
10515
|
const authStorage = this.#modelRegistry.authStorage;
|
|
9623
|
-
const
|
|
9624
|
-
authStorage.
|
|
10516
|
+
const authStorageOwner = this.#modelRegistry.getAuthStorageOwner();
|
|
10517
|
+
const target = authStorage.resolveOAuthPinTarget(provider, selector, authStorageOwner);
|
|
10518
|
+
authStorage.setSessionCredentialSelector(scopeId, provider, target.canonicalSelector, authStorageOwner);
|
|
9625
10519
|
if (target.canonicalSelector.kind === "id" && !this.#credentialStoreIdentity) return;
|
|
9626
10520
|
this.sessionManager.appendCustomEntry("auth-credential-pin", {
|
|
9627
10521
|
v: 1,
|
|
@@ -9701,9 +10595,13 @@ export class AgentSession {
|
|
|
9701
10595
|
args = "",
|
|
9702
10596
|
options?: Pick<
|
|
9703
10597
|
PromptOptions,
|
|
9704
|
-
"onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared" | "preflightSignal" | "
|
|
10598
|
+
"onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared" | "preflightSignal" | "sdkRunCapability"
|
|
9705
10599
|
>,
|
|
9706
10600
|
): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
|
|
10601
|
+
const internalOptions = {
|
|
10602
|
+
...options,
|
|
10603
|
+
sdkRunToken: readSdkRunCapability(options?.sdkRunCapability),
|
|
10604
|
+
};
|
|
9707
10605
|
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
9708
10606
|
const skillName = name.trim();
|
|
9709
10607
|
if (!skillName) throw Object.assign(new Error("skill.invoke requires a skill name."), { code: "invalid_input" });
|
|
@@ -9749,7 +10647,7 @@ export class AgentSession {
|
|
|
9749
10647
|
lineCount: built.details.lineCount,
|
|
9750
10648
|
cleanedArgs: activation.cleanedArgs || undefined,
|
|
9751
10649
|
});
|
|
9752
|
-
await this.promptCustomMessage(skillPromptMessage,
|
|
10650
|
+
await this.promptCustomMessage(skillPromptMessage, internalOptions as InternalCustomMessageOptions);
|
|
9753
10651
|
return {
|
|
9754
10652
|
name: skill.name,
|
|
9755
10653
|
path: skill.filePath,
|
|
@@ -10439,6 +11337,27 @@ export class AgentSession {
|
|
|
10439
11337
|
* @throws Error if no model selected or no API key available (when not streaming)
|
|
10440
11338
|
*/
|
|
10441
11339
|
async prompt(text: string, options?: PromptOptions): Promise<void> {
|
|
11340
|
+
const releaseStartupPromptWaiter = this.#reserveStartupPromptWaiter();
|
|
11341
|
+
try {
|
|
11342
|
+
await this.#promptInternal(text, options, releaseStartupPromptWaiter);
|
|
11343
|
+
} finally {
|
|
11344
|
+
releaseStartupPromptWaiter();
|
|
11345
|
+
}
|
|
11346
|
+
}
|
|
11347
|
+
|
|
11348
|
+
async #promptInternal(
|
|
11349
|
+
text: string,
|
|
11350
|
+
options: PromptOptions | undefined,
|
|
11351
|
+
releaseStartupPromptWaiter: () => void,
|
|
11352
|
+
): Promise<void> {
|
|
11353
|
+
const hasUsableImage =
|
|
11354
|
+
options?.images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
|
|
11355
|
+
if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
|
|
11356
|
+
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
11357
|
+
const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
|
|
11358
|
+
const internalOptions: InternalPromptOptions | undefined = options
|
|
11359
|
+
? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) }
|
|
11360
|
+
: undefined;
|
|
10442
11361
|
this.#assertRecoveryHydrationPromoted();
|
|
10443
11362
|
const owner = this.#sessionAdmissionContext.getStore();
|
|
10444
11363
|
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
@@ -10453,14 +11372,19 @@ export class AgentSession {
|
|
|
10453
11372
|
await this.invokeSkill(
|
|
10454
11373
|
invocation.skill.name,
|
|
10455
11374
|
invocation.args,
|
|
10456
|
-
options?.onPreflightAccepted ||
|
|
11375
|
+
options?.onPreflightAccepted ||
|
|
11376
|
+
options?.onPreflightAcceptCommit ||
|
|
11377
|
+
options?.preflightSignal ||
|
|
11378
|
+
internalOptions?.sdkRunToken
|
|
10457
11379
|
? {
|
|
10458
|
-
...(options
|
|
10459
|
-
|
|
11380
|
+
...(options?.onPreflightAccepted
|
|
11381
|
+
? { onPreflightAccepted: options.onPreflightAccepted }
|
|
11382
|
+
: {}),
|
|
11383
|
+
...(options?.onPreflightAcceptCommit
|
|
10460
11384
|
? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
|
|
10461
11385
|
: {}),
|
|
10462
|
-
...(options
|
|
10463
|
-
...(options
|
|
11386
|
+
...(options?.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
|
|
11387
|
+
...(options?.sdkRunCapability ? { sdkRunCapability: options.sdkRunCapability } : {}),
|
|
10464
11388
|
}
|
|
10465
11389
|
: undefined,
|
|
10466
11390
|
);
|
|
@@ -10494,6 +11418,10 @@ export class AgentSession {
|
|
|
10494
11418
|
|
|
10495
11419
|
// Expand file-based prompt templates if requested
|
|
10496
11420
|
const expandedText = expandPromptTemplates ? expandPromptTemplate(text, [...this.#promptTemplates]) : text;
|
|
11421
|
+
const expandedHasUsableImage =
|
|
11422
|
+
options?.images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
|
|
11423
|
+
if (expandedText.trim().length === 0 && !expandedHasUsableImage)
|
|
11424
|
+
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
10497
11425
|
assertImagePlaceholdersHavePayload(expandedText, options?.images);
|
|
10498
11426
|
const workflowIntentDiff = options?.synthetic ? null : buildWorkflowIntentDiff(expandedText);
|
|
10499
11427
|
const claimsGenuineUserIntent = !options?.synthetic && options?.attribution !== "agent";
|
|
@@ -10567,7 +11495,12 @@ export class AgentSession {
|
|
|
10567
11495
|
attribution: promptAttribution,
|
|
10568
11496
|
timestamp: Date.now(),
|
|
10569
11497
|
}
|
|
10570
|
-
: {
|
|
11498
|
+
: {
|
|
11499
|
+
role: "user" as const,
|
|
11500
|
+
content: userContent,
|
|
11501
|
+
attribution: promptAttribution,
|
|
11502
|
+
timestamp: Date.now(),
|
|
11503
|
+
};
|
|
10571
11504
|
if (deepInterviewUserIntentEpoch !== undefined)
|
|
10572
11505
|
this.#deepInterviewGenuineUserMessageEpochs.set(message, deepInterviewUserIntentEpoch);
|
|
10573
11506
|
await this.refreshGjcSubskillTools();
|
|
@@ -10595,6 +11528,8 @@ export class AgentSession {
|
|
|
10595
11528
|
}
|
|
10596
11529
|
},
|
|
10597
11530
|
options?.preflightSignal,
|
|
11531
|
+
undefined,
|
|
11532
|
+
{ startupPromptWaiterRelease: releaseStartupPromptWaiter },
|
|
10598
11533
|
);
|
|
10599
11534
|
}
|
|
10600
11535
|
|
|
@@ -10714,9 +11649,13 @@ export class AgentSession {
|
|
|
10714
11649
|
| "onPreflightAccepted"
|
|
10715
11650
|
| "onPreflightAcceptCommit"
|
|
10716
11651
|
| "preflightSignal"
|
|
10717
|
-
| "
|
|
11652
|
+
| "sdkRunCapability"
|
|
10718
11653
|
>,
|
|
10719
11654
|
): Promise<void> {
|
|
11655
|
+
const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
|
|
11656
|
+
const internalOptions: InternalCustomMessageOptions | undefined = options
|
|
11657
|
+
? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) }
|
|
11658
|
+
: undefined;
|
|
10720
11659
|
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
10721
11660
|
const textContent =
|
|
10722
11661
|
typeof message.content === "string"
|
|
@@ -10785,7 +11724,7 @@ export class AgentSession {
|
|
|
10785
11724
|
};
|
|
10786
11725
|
try {
|
|
10787
11726
|
await this.#promptWithMessage(customMessage, textContent, {
|
|
10788
|
-
...
|
|
11727
|
+
...internalOptions,
|
|
10789
11728
|
onPreflightAccepted: undefined,
|
|
10790
11729
|
onPreflightAcceptCommit: commitAcceptance,
|
|
10791
11730
|
admissionLease: admission,
|
|
@@ -10816,8 +11755,8 @@ export class AgentSession {
|
|
|
10816
11755
|
| "onPreflightAccepted"
|
|
10817
11756
|
| "onPreflightAcceptCommit"
|
|
10818
11757
|
| "preflightSignal"
|
|
10819
|
-
| "sdkRunToken"
|
|
10820
11758
|
> & {
|
|
11759
|
+
sdkRunToken?: string;
|
|
10821
11760
|
prependMessages?: AgentMessage[];
|
|
10822
11761
|
skipPostPromptRecoveryWait?: boolean;
|
|
10823
11762
|
predecessorAgentEndHold?: symbol;
|
|
@@ -10884,6 +11823,9 @@ export class AgentSession {
|
|
|
10884
11823
|
const rosterClaim = this.#claimIrcRosterCandidate();
|
|
10885
11824
|
let hasPendingNextTurnMessages = false;
|
|
10886
11825
|
let pendingNextTurnMessageCount = 0;
|
|
11826
|
+
let skipPostPromptRecoveryWait =
|
|
11827
|
+
options?.skipPostPromptRecoveryWait === true || options?.sdkRunToken !== undefined;
|
|
11828
|
+
let promptAttemptScope: AttemptScope | undefined;
|
|
10887
11829
|
let hindsightRecall: string | undefined;
|
|
10888
11830
|
try {
|
|
10889
11831
|
this.#throwIfPromptPreflightCancelled(generation, preflightSignal);
|
|
@@ -11152,9 +12094,10 @@ export class AgentSession {
|
|
|
11152
12094
|
...this.#managedFallbackPromptOptions(),
|
|
11153
12095
|
...(options?.sdkRunToken ? { sdkRunToken: options.sdkRunToken } : {}),
|
|
11154
12096
|
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
11155
|
-
|
|
11156
|
-
if (options?.sdkRunToken)
|
|
11157
|
-
|
|
12097
|
+
promptAttemptScope = handle?.scope;
|
|
12098
|
+
if (handle) this.#acceptSdkAttemptRun(handle, options?.sdkRunToken);
|
|
12099
|
+
else this.#acceptRunHandle(handle);
|
|
12100
|
+
if (handle && options?.sdkRunToken) {
|
|
11158
12101
|
this.#activeSdkRunToken = options.sdkRunToken;
|
|
11159
12102
|
}
|
|
11160
12103
|
options?.onRunAccepted?.(handle);
|
|
@@ -11189,6 +12132,20 @@ export class AgentSession {
|
|
|
11189
12132
|
options?.onPreflightAccepted?.();
|
|
11190
12133
|
},
|
|
11191
12134
|
});
|
|
12135
|
+
const activeTerminalScope = this.#activeAttemptScope;
|
|
12136
|
+
const terminalAttemptScope =
|
|
12137
|
+
activeTerminalScope !== undefined &&
|
|
12138
|
+
(this.#skipPostPromptRecoveryWaitByAttemptScope.has(activeTerminalScope) ||
|
|
12139
|
+
(options?.sdkRunToken !== undefined &&
|
|
12140
|
+
this.#sdkRunTokensByAttemptScope.get(activeTerminalScope) === options.sdkRunToken))
|
|
12141
|
+
? activeTerminalScope
|
|
12142
|
+
: promptAttemptScope;
|
|
12143
|
+
if (
|
|
12144
|
+
terminalAttemptScope !== undefined &&
|
|
12145
|
+
this.#skipPostPromptRecoveryWaitByAttemptScope.delete(terminalAttemptScope)
|
|
12146
|
+
) {
|
|
12147
|
+
skipPostPromptRecoveryWait = true;
|
|
12148
|
+
}
|
|
11192
12149
|
const terminalAssistant = this.#findLastAssistantMessage();
|
|
11193
12150
|
if (
|
|
11194
12151
|
rosterClaim &&
|
|
@@ -11198,7 +12155,7 @@ export class AgentSession {
|
|
|
11198
12155
|
) {
|
|
11199
12156
|
this.#commitIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
11200
12157
|
}
|
|
11201
|
-
if (!
|
|
12158
|
+
if (!skipPostPromptRecoveryWait) {
|
|
11202
12159
|
await this.#waitForPostPromptRecovery();
|
|
11203
12160
|
}
|
|
11204
12161
|
} catch (error) {
|
|
@@ -11223,7 +12180,7 @@ export class AgentSession {
|
|
|
11223
12180
|
this.#releaseIrcRosterClaim(rosterClaim.token, rosterClaim.epoch);
|
|
11224
12181
|
}
|
|
11225
12182
|
this.#releaseDeferredAgentEndContinuation(predecessorAgentEndHold);
|
|
11226
|
-
await this.#settleEndedInFlight(inFlightPrompt,
|
|
12183
|
+
await this.#settleEndedInFlight(inFlightPrompt, skipPostPromptRecoveryWait ? "publication" : "full");
|
|
11227
12184
|
}
|
|
11228
12185
|
}
|
|
11229
12186
|
|
|
@@ -11427,12 +12384,18 @@ export class AgentSession {
|
|
|
11427
12384
|
* Queue a steering message to interrupt the agent mid-run.
|
|
11428
12385
|
*/
|
|
11429
12386
|
async steer(text: string, images?: ImageContent[]): Promise<void> {
|
|
12387
|
+
const hasUsableImage =
|
|
12388
|
+
images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
|
|
12389
|
+
if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
|
|
12390
|
+
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
11430
12391
|
this.#assertRecoveryHydrationPromoted();
|
|
11431
12392
|
if (text.startsWith("/")) {
|
|
11432
12393
|
this.#throwIfExtensionCommand(text);
|
|
11433
12394
|
}
|
|
11434
12395
|
|
|
11435
12396
|
const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
|
|
12397
|
+
if (expandedText.trim().length === 0 && !hasUsableImage)
|
|
12398
|
+
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
11436
12399
|
assertImagePlaceholdersHavePayload(expandedText, images);
|
|
11437
12400
|
await this.#queueSteer(expandedText, images, { claimsGenuineUserIntent: true });
|
|
11438
12401
|
}
|
|
@@ -11445,12 +12408,18 @@ export class AgentSession {
|
|
|
11445
12408
|
images?: ImageContent[],
|
|
11446
12409
|
options?: Pick<PromptOptions, "followUpQueuePolicy">,
|
|
11447
12410
|
): Promise<void> {
|
|
12411
|
+
const hasUsableImage =
|
|
12412
|
+
images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
|
|
12413
|
+
if (typeof text !== "string" || (text.trim().length === 0 && !hasUsableImage))
|
|
12414
|
+
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
11448
12415
|
this.#assertRecoveryHydrationPromoted();
|
|
11449
12416
|
if (text.startsWith("/")) {
|
|
11450
12417
|
this.#throwIfExtensionCommand(text);
|
|
11451
12418
|
}
|
|
11452
12419
|
|
|
11453
12420
|
const expandedText = expandPromptTemplate(text, [...this.#promptTemplates]);
|
|
12421
|
+
if (expandedText.trim().length === 0 && !hasUsableImage)
|
|
12422
|
+
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
11454
12423
|
assertImagePlaceholdersHavePayload(expandedText, images);
|
|
11455
12424
|
await this.#queueFollowUp(expandedText, images, {
|
|
11456
12425
|
forceOneAtATime: options?.followUpQueuePolicy === "sequential",
|
|
@@ -11468,6 +12437,7 @@ export class AgentSession {
|
|
|
11468
12437
|
claimsGenuineUserIntent?: boolean;
|
|
11469
12438
|
onPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
|
|
11470
12439
|
external?: boolean;
|
|
12440
|
+
sdkRunToken?: string;
|
|
11471
12441
|
},
|
|
11472
12442
|
): Promise<void> {
|
|
11473
12443
|
this.#assertNoHandoffTransition();
|
|
@@ -11482,6 +12452,7 @@ export class AgentSession {
|
|
|
11482
12452
|
this.#externalSteerAdmissionSeq.set(message, ++this.#steeringAdmissionSeq);
|
|
11483
12453
|
if (options.onPromoted) this.#steerPromotionHooks.set(message, options.onPromoted);
|
|
11484
12454
|
}
|
|
12455
|
+
if (options?.sdkRunToken) this.#sdkRunTokensByQueuedMessage.set(message, options.sdkRunToken);
|
|
11485
12456
|
if (options?.claimsGenuineUserIntent) {
|
|
11486
12457
|
const epoch = this.#claimDeepInterviewUserIntent();
|
|
11487
12458
|
this.#deepInterviewGenuineUserMessageEpochs.set(message, epoch);
|
|
@@ -11682,7 +12653,16 @@ export class AgentSession {
|
|
|
11682
12653
|
}
|
|
11683
12654
|
this.#scheduledHiddenNextTurnGeneration = generation;
|
|
11684
12655
|
this.#schedulePostPromptTask(
|
|
11685
|
-
async
|
|
12656
|
+
async signal => {
|
|
12657
|
+
if (signal.aborted || this.#isDisposed || this.#sessionAdmissionClosing) return;
|
|
12658
|
+
if (this.#startupTurnBarrier) {
|
|
12659
|
+
try {
|
|
12660
|
+
await this.#awaitStartupTurnBarrier(signal);
|
|
12661
|
+
} catch {
|
|
12662
|
+
return;
|
|
12663
|
+
}
|
|
12664
|
+
}
|
|
12665
|
+
if (signal.aborted || this.#isDisposed || this.#sessionAdmissionClosing) return;
|
|
11686
12666
|
if (this.#scheduledHiddenNextTurnGeneration === generation) {
|
|
11687
12667
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
11688
12668
|
}
|
|
@@ -11711,8 +12691,12 @@ export class AgentSession {
|
|
|
11711
12691
|
return;
|
|
11712
12692
|
}
|
|
11713
12693
|
try {
|
|
11714
|
-
await this.#promptQueuedHiddenNextTurnMessages();
|
|
12694
|
+
await this.#promptQueuedHiddenNextTurnMessages(signal);
|
|
11715
12695
|
} catch {
|
|
12696
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
|
|
12697
|
+
this.#pendingNextTurnMessages = [];
|
|
12698
|
+
return;
|
|
12699
|
+
}
|
|
11716
12700
|
// Leave the hidden next-turn messages queued for the next explicit prompt.
|
|
11717
12701
|
}
|
|
11718
12702
|
},
|
|
@@ -11727,7 +12711,7 @@ export class AgentSession {
|
|
|
11727
12711
|
);
|
|
11728
12712
|
}
|
|
11729
12713
|
|
|
11730
|
-
async #promptQueuedHiddenNextTurnMessages(): Promise<void> {
|
|
12714
|
+
async #promptQueuedHiddenNextTurnMessages(signal?: AbortSignal): Promise<void> {
|
|
11731
12715
|
if (this.#pendingNextTurnMessages.length === 0) {
|
|
11732
12716
|
return;
|
|
11733
12717
|
}
|
|
@@ -11757,6 +12741,14 @@ export class AgentSession {
|
|
|
11757
12741
|
if (reclassified.length === 0) {
|
|
11758
12742
|
return;
|
|
11759
12743
|
}
|
|
12744
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
|
|
12745
|
+
this.#settleDeliveredOwnedRegistrations(reclassified.map(entry => entry.message));
|
|
12746
|
+
return;
|
|
12747
|
+
}
|
|
12748
|
+
if (signal?.aborted) {
|
|
12749
|
+
this.#pendingNextTurnMessages = [...reclassified, ...this.#pendingNextTurnMessages];
|
|
12750
|
+
return;
|
|
12751
|
+
}
|
|
11760
12752
|
// Allocate a fresh root-turn lineage when the drained batch contains a
|
|
11761
12753
|
// fresh owned envelope OR an EXTERNAL hidden trigger: an external
|
|
11762
12754
|
// nextTurn message has no owned envelope, so without this it would
|
|
@@ -11778,11 +12770,24 @@ export class AgentSession {
|
|
|
11778
12770
|
const textContent = this.#getCustomMessageTextContent(message);
|
|
11779
12771
|
await this.#syncSkillPromptActiveStateSafely(message, true);
|
|
11780
12772
|
try {
|
|
12773
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
|
|
12774
|
+
this.#settleDeliveredOwnedRegistrations(reclassified.map(entry => entry.message));
|
|
12775
|
+
return;
|
|
12776
|
+
}
|
|
12777
|
+
if (signal?.aborted) {
|
|
12778
|
+
this.#pendingNextTurnMessages = [...reclassified, ...this.#pendingNextTurnMessages];
|
|
12779
|
+
return;
|
|
12780
|
+
}
|
|
11781
12781
|
await this.#promptWithMessage(message, textContent, {
|
|
11782
12782
|
prependMessages,
|
|
11783
12783
|
skipPostPromptRecoveryWait: true,
|
|
12784
|
+
preflightSignal: signal,
|
|
11784
12785
|
});
|
|
11785
12786
|
} catch (error) {
|
|
12787
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing || this.#disposeAbortController.signal.aborted) {
|
|
12788
|
+
this.#settleDeliveredOwnedRegistrations(reclassified.map(entry => entry.message));
|
|
12789
|
+
return;
|
|
12790
|
+
}
|
|
11786
12791
|
// Requeue only the SURVIVING reclassified entries: a completion
|
|
11787
12792
|
// denied by scope:"owned" was settled (dropped) above and must
|
|
11788
12793
|
// never reach a later prompt — once its scope and now-unoccupied
|
|
@@ -11809,19 +12814,55 @@ export class AgentSession {
|
|
|
11809
12814
|
// another session's manager (which lacks the same local job id) would
|
|
11810
12815
|
// see job === undefined and remove a still-live registration (review
|
|
11811
12816
|
// thread P1).
|
|
11812
|
-
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
11813
12817
|
for (const message of messages) {
|
|
11814
12818
|
const details = (message as { details?: { ownedCompletions?: OwnedCompletionEnvelope[] } }).details;
|
|
11815
|
-
for (const envelope of details?.ownedCompletions ?? [])
|
|
11816
|
-
|
|
11817
|
-
|
|
11818
|
-
|
|
11819
|
-
|
|
11820
|
-
|
|
11821
|
-
|
|
12819
|
+
for (const envelope of details?.ownedCompletions ?? []) this.#settleOwnedCompletionEnvelope(envelope);
|
|
12820
|
+
}
|
|
12821
|
+
}
|
|
12822
|
+
|
|
12823
|
+
#settleOwnedCompletionEnvelope(envelope: OwnedCompletionEnvelope): void {
|
|
12824
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
12825
|
+
const job = manager?.getJob(envelope.registration.jobId);
|
|
12826
|
+
const status = job?.generation === envelope.registration.jobGeneration ? job?.status : undefined;
|
|
12827
|
+
// Evicted jobs have no live record (job === undefined); terminal statuses
|
|
12828
|
+
// settle the registration.
|
|
12829
|
+
if (job === undefined || status === "completed" || status === "cancelled" || status === "failed") {
|
|
12830
|
+
unregisterOwnedRegistration(envelope.registration);
|
|
12831
|
+
}
|
|
12832
|
+
}
|
|
12833
|
+
|
|
12834
|
+
/**
|
|
12835
|
+
* Drop every queued yield entry, releasing its retained delivery claim and
|
|
12836
|
+
* settling terminal owned-completion envelopes so neither outlives this
|
|
12837
|
+
* session's disposal. Claims resolve to the manager that actually retained
|
|
12838
|
+
* them: owned entries via their registration endpoint (subagents inherit a
|
|
12839
|
+
* parent's manager, and the process-global instance may belong to a
|
|
12840
|
+
* different concurrent session), ordinary entries only on the session-OWNED
|
|
12841
|
+
* manager. Disposal runs this twice: before the awaited session_shutdown
|
|
12842
|
+
* emit, and again after producer cancellation — a completion enqueued
|
|
12843
|
+
* inside that window would otherwise survive disposal, its claim retained
|
|
12844
|
+
* on a shared manager and interfering with later delivery teardown (Codex
|
|
12845
|
+
* review P2 on #5088).
|
|
12846
|
+
*/
|
|
12847
|
+
#drainTerminalOwnedYieldEntries(): void {
|
|
12848
|
+
this.yieldQueue.clear((kind, entries) => {
|
|
12849
|
+
if (kind !== "async-result") return;
|
|
12850
|
+
const ownedManager = this.#ownedAsyncJobManager;
|
|
12851
|
+
for (const entry of entries) {
|
|
12852
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
12853
|
+
const record = entry as { generation?: unknown; ownedCompletion?: unknown };
|
|
12854
|
+
if (typeof record.generation === "string" && record.generation !== "") {
|
|
12855
|
+
const envelope = isOwnedCompletionEnvelope(record.ownedCompletion) ? record.ownedCompletion : undefined;
|
|
12856
|
+
const endpointId = envelope?.registration.endpointId;
|
|
12857
|
+
const manager =
|
|
12858
|
+
(endpointId !== undefined ? AsyncJobManager.forEndpoint(endpointId) : undefined) ?? ownedManager;
|
|
12859
|
+
manager?.releaseDeliveryClaim(record.generation);
|
|
12860
|
+
}
|
|
12861
|
+
if (isOwnedCompletionEnvelope(record.ownedCompletion)) {
|
|
12862
|
+
this.#settleOwnedCompletionEnvelope(record.ownedCompletion);
|
|
11822
12863
|
}
|
|
11823
12864
|
}
|
|
11824
|
-
}
|
|
12865
|
+
});
|
|
11825
12866
|
}
|
|
11826
12867
|
|
|
11827
12868
|
#getCustomMessageTextContent(message: Pick<CustomMessage, "content">): string {
|
|
@@ -12064,9 +13105,11 @@ export class AgentSession {
|
|
|
12064
13105
|
/** Internal dispatch disposition used before actual queue consumption. */
|
|
12065
13106
|
onDispatchDisposition?: (promotion: { startsOwnRun: boolean }) => void;
|
|
12066
13107
|
preflightSignal?: AbortSignal;
|
|
12067
|
-
|
|
13108
|
+
sdkRunCapability?: unknown;
|
|
12068
13109
|
},
|
|
12069
13110
|
): Promise<void> {
|
|
13111
|
+
const sdkRunToken = readSdkRunCapability(options?.sdkRunCapability);
|
|
13112
|
+
const internalOptions = options ? { ...options, ...(sdkRunToken ? { sdkRunToken } : {}) } : undefined;
|
|
12070
13113
|
this.#assertRecoveryHydrationPromoted();
|
|
12071
13114
|
const owner = this.#sessionAdmissionContext.getStore();
|
|
12072
13115
|
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
@@ -12087,7 +13130,15 @@ export class AgentSession {
|
|
|
12087
13130
|
const textParts: string[] = [];
|
|
12088
13131
|
images = [];
|
|
12089
13132
|
for (const part of content) {
|
|
13133
|
+
if (!part || typeof part !== "object" || Array.isArray(part))
|
|
13134
|
+
throw Object.assign(new Error("sendUserMessage content blocks must be objects."), {
|
|
13135
|
+
code: "invalid_input",
|
|
13136
|
+
});
|
|
12090
13137
|
if (part.type === "text") {
|
|
13138
|
+
if (typeof part.text !== "string")
|
|
13139
|
+
throw Object.assign(new Error("sendUserMessage text blocks must contain string text."), {
|
|
13140
|
+
code: "invalid_input",
|
|
13141
|
+
});
|
|
12091
13142
|
textParts.push(part.text);
|
|
12092
13143
|
} else {
|
|
12093
13144
|
images.push(part);
|
|
@@ -12096,6 +13147,10 @@ export class AgentSession {
|
|
|
12096
13147
|
text = textParts.join("\n");
|
|
12097
13148
|
if (images.length === 0) images = undefined;
|
|
12098
13149
|
}
|
|
13150
|
+
const hasUsableImage =
|
|
13151
|
+
images?.some(image => typeof image?.data === "string" && image.data.trim().length > 0) === true;
|
|
13152
|
+
if (text.trim().length === 0 && !hasUsableImage)
|
|
13153
|
+
throw Object.assign(new Error("Prompt must not be empty."), { code: "invalid_input" });
|
|
12099
13154
|
|
|
12100
13155
|
let admissionSignal = options?.preflightSignal
|
|
12101
13156
|
? AbortSignal.any([this.#promptPreflightAbortController.signal, options.preflightSignal])
|
|
@@ -12189,7 +13244,7 @@ export class AgentSession {
|
|
|
12189
13244
|
claimsGenuineUserIntent: true,
|
|
12190
13245
|
forceOneAtATime: Boolean(options?.preflightSignal || options?.queuedAtDispatch),
|
|
12191
13246
|
onPromoted: options?.onQueuedPromoted,
|
|
12192
|
-
sdkRunToken:
|
|
13247
|
+
sdkRunToken: internalOptions?.sdkRunToken,
|
|
12193
13248
|
});
|
|
12194
13249
|
const cancelQueuedFollowUp = () => queuedFollowUp.cancel();
|
|
12195
13250
|
options?.preflightSignal?.addEventListener("abort", cancelQueuedFollowUp, { once: true });
|
|
@@ -12204,6 +13259,7 @@ export class AgentSession {
|
|
|
12204
13259
|
claimsGenuineUserIntent: true,
|
|
12205
13260
|
onPromoted: options?.onQueuedPromoted,
|
|
12206
13261
|
external: true,
|
|
13262
|
+
sdkRunToken: internalOptions?.sdkRunToken,
|
|
12207
13263
|
});
|
|
12208
13264
|
options?.onPreflightAccepted?.();
|
|
12209
13265
|
return;
|
|
@@ -12223,6 +13279,7 @@ export class AgentSession {
|
|
|
12223
13279
|
claimsGenuineUserIntent: true,
|
|
12224
13280
|
onPromoted: options?.onQueuedPromoted,
|
|
12225
13281
|
external: true,
|
|
13282
|
+
sdkRunToken: internalOptions?.sdkRunToken,
|
|
12226
13283
|
});
|
|
12227
13284
|
// Dispatch-race disposition (#4668 review P1): the SDK snapshot-decided
|
|
12228
13285
|
// this submission starts its own turn (idle at dispatch), but the
|
|
@@ -12248,7 +13305,7 @@ export class AgentSession {
|
|
|
12248
13305
|
await this.prompt(text, {
|
|
12249
13306
|
expandPromptTemplates: false,
|
|
12250
13307
|
images,
|
|
12251
|
-
sdkRunToken:
|
|
13308
|
+
sdkRunToken: internalOptions?.sdkRunToken,
|
|
12252
13309
|
onPreflightAccepted: () => {
|
|
12253
13310
|
options?.onPreflightAccepted?.();
|
|
12254
13311
|
fireQueuedPromotion();
|
|
@@ -12263,7 +13320,7 @@ export class AgentSession {
|
|
|
12263
13320
|
}
|
|
12264
13321
|
: undefined,
|
|
12265
13322
|
preflightSignal: options?.preflightSignal,
|
|
12266
|
-
});
|
|
13323
|
+
} as InternalPromptOptions);
|
|
12267
13324
|
} finally {
|
|
12268
13325
|
releaseFollowUpReservation();
|
|
12269
13326
|
}
|
|
@@ -12772,6 +13829,7 @@ export class AgentSession {
|
|
|
12772
13829
|
| "tool_abort"
|
|
12773
13830
|
| "internal";
|
|
12774
13831
|
silent?: boolean;
|
|
13832
|
+
sdkRunToken?: string;
|
|
12775
13833
|
}): void {
|
|
12776
13834
|
const abortGoalState = this.getGoalModeState();
|
|
12777
13835
|
this.#suppressNextGoalReminderAfterAbortGoalId =
|
|
@@ -12860,12 +13918,14 @@ export class AgentSession {
|
|
|
12860
13918
|
this.#promptPreflightCancellationGeneration++;
|
|
12861
13919
|
this.#promptPreflightAbortController.abort();
|
|
12862
13920
|
this.#promptPreflightAbortController = new AbortController();
|
|
13921
|
+
this.#drainTerminalOwnedYieldEntries();
|
|
13922
|
+
const overlappingPostPromptDrain = this.#cancelPostPromptTasks();
|
|
12863
13923
|
// Abort visibility is per-request: a later real abort must not inherit an
|
|
12864
13924
|
// earlier silent abort's suppression and swallow the user-visible notice.
|
|
12865
13925
|
if (options?.silent !== true) this.#silentAbortPending = false;
|
|
12866
13926
|
// Capture the unwind: the field clears once the first abort settles, and
|
|
12867
13927
|
// the awaits below must keep watching THIS unwind, not a successor's.
|
|
12868
|
-
const sharedUnwind = this.#abortUnwind;
|
|
13928
|
+
const sharedUnwind = Promise.all([this.#abortUnwind, overlappingPostPromptDrain]).then(() => {});
|
|
12869
13929
|
if (options?.timeoutMs !== undefined) {
|
|
12870
13930
|
const timeoutMs = Math.max(0, options.timeoutMs);
|
|
12871
13931
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -12896,6 +13956,7 @@ export class AgentSession {
|
|
|
12896
13956
|
this.#abortUnwind = unwind.promise;
|
|
12897
13957
|
try {
|
|
12898
13958
|
this.#abortOptions(options);
|
|
13959
|
+
this.#drainTerminalOwnedYieldEntries();
|
|
12899
13960
|
const postPromptDrain = this.#cancelPostPromptTasks();
|
|
12900
13961
|
const managedLogicalRunId =
|
|
12901
13962
|
this.#defaultFallbackChain().chain.entries.length > 1 ? this.agent.currentManagedLogicalRunId : undefined;
|
|
@@ -13473,6 +14534,9 @@ export class AgentSession {
|
|
|
13473
14534
|
attribution: "user" as const,
|
|
13474
14535
|
timestamp: Date.now(),
|
|
13475
14536
|
};
|
|
14537
|
+
const selectedSdkRunToken = selectedMessage
|
|
14538
|
+
? this.#sdkRunTokensByQueuedMessage.get(selectedMessage)
|
|
14539
|
+
: undefined;
|
|
13476
14540
|
const messageText =
|
|
13477
14541
|
message.role === "custom"
|
|
13478
14542
|
? this.#getCustomMessageTextContent(message)
|
|
@@ -13491,6 +14555,7 @@ export class AgentSession {
|
|
|
13491
14555
|
await this.#promptWithMessage(message, messageText, {
|
|
13492
14556
|
admissionLease: admission,
|
|
13493
14557
|
resetRetryReplaySafety: true,
|
|
14558
|
+
sdkRunToken: selectedSdkRunToken,
|
|
13494
14559
|
onRunAccepted: () => {
|
|
13495
14560
|
runAccepted = true;
|
|
13496
14561
|
if (selected) this.#fireQueuedPromotionHooks([message], { startsOwnRun: true });
|
|
@@ -18934,11 +19999,15 @@ export class AgentSession {
|
|
|
18934
19999
|
}
|
|
18935
20000
|
|
|
18936
20001
|
async #handleManagedAttemptOutcome(outcome: ManagedAttemptOutcome): Promise<ManagedAttemptDecision> {
|
|
20002
|
+
const attemptAbortEpoch = this.#abortAdmissionEpoch;
|
|
20003
|
+
const attemptCancelled = () =>
|
|
20004
|
+
this.#isDisposed || this.#sessionAdmissionClosing || this.#abortAdmissionEpoch !== attemptAbortEpoch;
|
|
18937
20005
|
const activePromptHandle = this.activePromptHandle;
|
|
18938
20006
|
const cancellationSignal = activePromptHandle
|
|
18939
20007
|
? this.#runCancellationDomains.lookup(activePromptHandle)?.signal
|
|
18940
20008
|
: undefined;
|
|
18941
|
-
if (cancellationSignal?.aborted)
|
|
20009
|
+
if (attemptCancelled() || cancellationSignal?.aborted)
|
|
20010
|
+
return { type: "terminal", terminal: { stopReason: "cancelled" } };
|
|
18942
20011
|
if (outcome.type === "run_terminal") {
|
|
18943
20012
|
this.#defaultFallbackChain().resetAttemptBudget();
|
|
18944
20013
|
return { type: "terminal", terminal: { stopReason: outcome.reason } };
|
|
@@ -18974,7 +20043,6 @@ export class AgentSession {
|
|
|
18974
20043
|
const controllerStateBeforeFailure = controller.snapshotRuntimeState();
|
|
18975
20044
|
const advanced = controller.recordEscapedArgumentsFailure(errorMessage, false);
|
|
18976
20045
|
this.#escapedNonAsciiManagedRetries = 0;
|
|
18977
|
-
const abortEpoch = this.#abortAdmissionEpoch;
|
|
18978
20046
|
if (advanced) {
|
|
18979
20047
|
const transitionGeneration = ++this.#fallbackTransitionGeneration;
|
|
18980
20048
|
return {
|
|
@@ -18995,7 +20063,9 @@ export class AgentSession {
|
|
|
18995
20063
|
!ownership.isCurrent() ||
|
|
18996
20064
|
ownership.lease.signal.aborted ||
|
|
18997
20065
|
cancellationSignal?.aborted ||
|
|
18998
|
-
this.#
|
|
20066
|
+
this.#isDisposed ||
|
|
20067
|
+
this.#sessionAdmissionClosing ||
|
|
20068
|
+
this.#abortAdmissionEpoch !== attemptAbortEpoch ||
|
|
18999
20069
|
this.#fallbackTransitionGeneration !== transitionGeneration;
|
|
19000
20070
|
if (continuationCancelled) {
|
|
19001
20071
|
await restoreOwnedTransition();
|
|
@@ -19020,7 +20090,7 @@ export class AgentSession {
|
|
|
19020
20090
|
"escaped_non_ascii",
|
|
19021
20091
|
1,
|
|
19022
20092
|
cancellationSignal,
|
|
19023
|
-
|
|
20093
|
+
attemptAbortEpoch,
|
|
19024
20094
|
controllerStateBeforeFailure,
|
|
19025
20095
|
previousModel,
|
|
19026
20096
|
previousThinkingLevel,
|
|
@@ -19029,7 +20099,9 @@ export class AgentSession {
|
|
|
19029
20099
|
);
|
|
19030
20100
|
if (!switched) {
|
|
19031
20101
|
if (
|
|
19032
|
-
this.#
|
|
20102
|
+
this.#isDisposed ||
|
|
20103
|
+
this.#sessionAdmissionClosing ||
|
|
20104
|
+
this.#abortAdmissionEpoch !== attemptAbortEpoch ||
|
|
19033
20105
|
cancellationSignal?.aborted ||
|
|
19034
20106
|
this.#fallbackTransitionGeneration !== transitionGeneration
|
|
19035
20107
|
) {
|
|
@@ -19049,13 +20121,27 @@ export class AgentSession {
|
|
|
19049
20121
|
});
|
|
19050
20122
|
return;
|
|
19051
20123
|
}
|
|
20124
|
+
if (
|
|
20125
|
+
attemptCancelled() ||
|
|
20126
|
+
!ownership.isCurrent() ||
|
|
20127
|
+
ownership.lease.signal.aborted ||
|
|
20128
|
+
this.#fallbackTransitionGeneration !== transitionGeneration
|
|
20129
|
+
) {
|
|
20130
|
+
await restoreOwnedTransition();
|
|
20131
|
+
return;
|
|
20132
|
+
}
|
|
19052
20133
|
const continuation = this.agent.continue({
|
|
19053
20134
|
...this.#managedFallbackPromptOptions(),
|
|
19054
20135
|
transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
|
|
20136
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
20137
|
+
this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
|
|
20138
|
+
},
|
|
19055
20139
|
});
|
|
19056
20140
|
if (
|
|
19057
20141
|
cancellationSignal?.aborted ||
|
|
19058
|
-
this.#
|
|
20142
|
+
this.#isDisposed ||
|
|
20143
|
+
this.#sessionAdmissionClosing ||
|
|
20144
|
+
this.#abortAdmissionEpoch !== attemptAbortEpoch ||
|
|
19059
20145
|
this.#fallbackTransitionGeneration !== transitionGeneration
|
|
19060
20146
|
) {
|
|
19061
20147
|
await restoreOwnedTransition();
|
|
@@ -19074,7 +20160,7 @@ export class AgentSession {
|
|
|
19074
20160
|
};
|
|
19075
20161
|
}
|
|
19076
20162
|
controller.advance();
|
|
19077
|
-
if (
|
|
20163
|
+
if (attemptCancelled() || cancellationSignal?.aborted)
|
|
19078
20164
|
return { type: "terminal", terminal: { stopReason: "cancelled" } };
|
|
19079
20165
|
this.#defaultFallbackExhaustedLastTurn = true;
|
|
19080
20166
|
return this.#managedFallbackExhaustionDecision(
|
|
@@ -19086,10 +20172,13 @@ export class AgentSession {
|
|
|
19086
20172
|
return {
|
|
19087
20173
|
type: "retry",
|
|
19088
20174
|
continuation: async ownership => {
|
|
19089
|
-
if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
|
|
20175
|
+
if (attemptCancelled() || !ownership.isCurrent() || ownership.lease.signal.aborted) return;
|
|
19090
20176
|
await this.agent.continue({
|
|
19091
20177
|
...this.#managedFallbackPromptOptions(),
|
|
19092
20178
|
transientRecoveryMessage: this.#escapedNonAsciiRecoveryMessage(),
|
|
20179
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
20180
|
+
this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
|
|
20181
|
+
},
|
|
19093
20182
|
});
|
|
19094
20183
|
},
|
|
19095
20184
|
};
|
|
@@ -19102,7 +20191,7 @@ export class AgentSession {
|
|
|
19102
20191
|
return {
|
|
19103
20192
|
type: "maintenance",
|
|
19104
20193
|
continuation: async ownership => {
|
|
19105
|
-
if (!ownership.isCurrent() || ownership.lease.signal.aborted) return;
|
|
20194
|
+
if (attemptCancelled() || !ownership.isCurrent() || ownership.lease.signal.aborted) return;
|
|
19106
20195
|
const resourceRunId = String(ownership.logicalRunId);
|
|
19107
20196
|
const previousLease = this.#postPromptLeases.get(resourceRunId);
|
|
19108
20197
|
this.#postPromptLeases.set(resourceRunId, ownership.lease);
|
|
@@ -19121,7 +20210,13 @@ export class AgentSession {
|
|
|
19121
20210
|
resourceRunId,
|
|
19122
20211
|
ownership.lease.signal,
|
|
19123
20212
|
);
|
|
19124
|
-
if (
|
|
20213
|
+
if (
|
|
20214
|
+
terminalized ||
|
|
20215
|
+
successorScheduled ||
|
|
20216
|
+
attemptCancelled() ||
|
|
20217
|
+
!ownership.isCurrent() ||
|
|
20218
|
+
ownership.lease.signal.aborted
|
|
20219
|
+
)
|
|
19125
20220
|
return;
|
|
19126
20221
|
this.agent.requestRunTerminal(ownership.handle.logicalRunId, {
|
|
19127
20222
|
stopReason: "error",
|
|
@@ -19519,11 +20614,13 @@ export class AgentSession {
|
|
|
19519
20614
|
if (!isAuthenticated(activeApiKey)) return "unchanged";
|
|
19520
20615
|
remaining = await authStorage.invalidateCredentialMatching(provider, activeApiKey, {
|
|
19521
20616
|
sessionId: credentialSessionId,
|
|
20617
|
+
owner: this.#modelRegistry.getAuthStorageOwner(),
|
|
19522
20618
|
});
|
|
19523
20619
|
if (!remaining) return "unchanged";
|
|
19524
20620
|
} else {
|
|
19525
20621
|
remaining = await authStorage.markUsageLimitReached(provider, credentialSessionId, {
|
|
19526
20622
|
retryAfterMs: trigger.retryAfterMs,
|
|
20623
|
+
owner: this.#modelRegistry.getAuthStorageOwner(),
|
|
19527
20624
|
});
|
|
19528
20625
|
}
|
|
19529
20626
|
|
|
@@ -19562,6 +20659,9 @@ export class AgentSession {
|
|
|
19562
20659
|
scope?: AttemptScope,
|
|
19563
20660
|
scopeWasClean = this.#isRetryScopeClean(scope),
|
|
19564
20661
|
): Promise<boolean | ManagedAttemptDecision> {
|
|
20662
|
+
const retryAbortEpoch = this.#abortAdmissionEpoch;
|
|
20663
|
+
const retryCancelled = () =>
|
|
20664
|
+
this.#isDisposed || this.#sessionAdmissionClosing || this.#abortAdmissionEpoch !== retryAbortEpoch;
|
|
19565
20665
|
const controller = this.#defaultFallbackChain();
|
|
19566
20666
|
const managedFallback = controller.chain.entries.length > 1;
|
|
19567
20667
|
const retrySettings = this.settings.getGroup("retry");
|
|
@@ -19573,6 +20673,9 @@ export class AgentSession {
|
|
|
19573
20673
|
const classification = this.#classifyErrorForRetry(message);
|
|
19574
20674
|
const localSnapshot = classification === "local_snapshot";
|
|
19575
20675
|
const localBufferOverflow = classification === "local_buffer_overflow";
|
|
20676
|
+
if (retryCancelled()) {
|
|
20677
|
+
return managedOutcome ? { type: "terminal", terminal: { stopReason: "cancelled" } } : false;
|
|
20678
|
+
}
|
|
19576
20679
|
// A local machinery failure must never stay charged against the provider
|
|
19577
20680
|
// fallback budget, no matter which local exit follows (disabled retry,
|
|
19578
20681
|
// visible-content surface, bounded retry, exhaustion, or the immediate
|
|
@@ -19835,7 +20938,7 @@ export class AgentSession {
|
|
|
19835
20938
|
ownership?.domain.signal ??
|
|
19836
20939
|
(activePromptHandle ? this.#runCancellationDomains.lookup(activePromptHandle)?.signal : undefined);
|
|
19837
20940
|
if (ownership && (!ownership.isCurrent() || cancellationSignal?.aborted)) return;
|
|
19838
|
-
|
|
20941
|
+
if (retryCancelled()) return;
|
|
19839
20942
|
let quotaPoolExhausted = false;
|
|
19840
20943
|
if (managedFallback && !credentialRotated && !providerRetryCeilingReached) {
|
|
19841
20944
|
const mark = await this.#markFailedCredential(trigger);
|
|
@@ -19869,7 +20972,7 @@ export class AgentSession {
|
|
|
19869
20972
|
trigger.class,
|
|
19870
20973
|
attemptsUsed,
|
|
19871
20974
|
cancellationSignal,
|
|
19872
|
-
|
|
20975
|
+
retryAbortEpoch,
|
|
19873
20976
|
controllerStateBeforeAdvance,
|
|
19874
20977
|
previousModel,
|
|
19875
20978
|
previousThinkingLevel,
|
|
@@ -19879,7 +20982,7 @@ export class AgentSession {
|
|
|
19879
20982
|
}
|
|
19880
20983
|
}
|
|
19881
20984
|
if (ownership && (!ownership.isCurrent() || cancellationSignal?.aborted)) {
|
|
19882
|
-
if (
|
|
20985
|
+
if (!retryCancelled()) {
|
|
19883
20986
|
await this.#restoreDefaultFallbackTransition(
|
|
19884
20987
|
controller,
|
|
19885
20988
|
controllerStateBeforeAdvance,
|
|
@@ -19889,7 +20992,7 @@ export class AgentSession {
|
|
|
19889
20992
|
}
|
|
19890
20993
|
return;
|
|
19891
20994
|
}
|
|
19892
|
-
if (
|
|
20995
|
+
if (retryCancelled()) {
|
|
19893
20996
|
controller.resetForNewTurn();
|
|
19894
20997
|
return;
|
|
19895
20998
|
}
|
|
@@ -20018,8 +21121,9 @@ export class AgentSession {
|
|
|
20018
21121
|
|
|
20019
21122
|
if (managedOutcome) {
|
|
20020
21123
|
try {
|
|
21124
|
+
if (retryCancelled() || ownershipCancelled()) return;
|
|
20021
21125
|
await this.#checkEstimatedContextBeforePrompt();
|
|
20022
|
-
if (ownershipCancelled()) {
|
|
21126
|
+
if (retryCancelled() || ownershipCancelled()) {
|
|
20023
21127
|
const attempt = this.#retryAttempt;
|
|
20024
21128
|
this.#retryAttempt = 0;
|
|
20025
21129
|
await this.#emitSessionEvent({
|
|
@@ -20031,7 +21135,12 @@ export class AgentSession {
|
|
|
20031
21135
|
this.#resolveRetry();
|
|
20032
21136
|
return;
|
|
20033
21137
|
}
|
|
20034
|
-
await this.agent.continue(
|
|
21138
|
+
await this.agent.continue({
|
|
21139
|
+
...this.#managedFallbackPromptOptions(),
|
|
21140
|
+
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
21141
|
+
this.#acceptSdkAttemptRun(handle, this.#activeSdkRunToken);
|
|
21142
|
+
},
|
|
21143
|
+
});
|
|
20035
21144
|
return;
|
|
20036
21145
|
} catch (error) {
|
|
20037
21146
|
const attempt = this.#retryAttempt;
|
|
@@ -21068,6 +22177,9 @@ export class AgentSession {
|
|
|
21068
22177
|
model.provider,
|
|
21069
22178
|
this.#modelRegistry.authStorage,
|
|
21070
22179
|
this.credentialSessionId,
|
|
22180
|
+
model,
|
|
22181
|
+
// Optional for the same reason as the main metadata resolver above.
|
|
22182
|
+
this.#modelRegistry.getAuthStorageOwner?.(),
|
|
21071
22183
|
),
|
|
21072
22184
|
reasoning: toReasoningEffort(this.thinkingLevel),
|
|
21073
22185
|
hideThinkingSummary: this.agent.hideThinkingSummary,
|