@gajae-code/coding-agent 0.16.0 → 0.16.3
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 +52 -1
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/update-cli.d.ts +1 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/model-registry.d.ts +16 -0
- package/dist/types/config/settings-schema.d.ts +17 -7
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/sdk/broker/broker.d.ts +2 -1
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +14 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +17 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +3 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/rows.d.ts +15 -2
- package/dist/types/sdk/cli/session-cli.d.ts +2 -1
- package/dist/types/sdk/host/events.d.ts +2 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +18 -17
- package/dist/types/sdk/lifecycle-session.d.ts +1 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +4 -0
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +47 -7
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +14 -2
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +12 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/index.d.ts +4 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +12 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +7 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/update-cli.ts +63 -12
- package/src/commands/sdk.ts +22 -7
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/model-preset-registry.ts +177 -18
- package/src/config/model-registry.ts +268 -42
- package/src/config/settings-schema.ts +19 -7
- package/src/config/settings.ts +46 -15
- package/src/coordinator-mcp/projection-scan.ts +6 -6
- package/src/coordinator-mcp/server.ts +21 -1
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/session-state-lock.ts +156 -32
- package/src/gjc-runtime/session-state-sidecar.ts +10 -17
- package/src/gjc-runtime/tmux-owner-isolation.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +11 -11
- package/src/main.ts +4 -1
- package/src/modes/acp/acp-agent.ts +34 -19
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +2 -2
- package/src/modes/controllers/selector-controller.ts +10 -3
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +6 -1
- package/src/sdk/broker/broker.ts +10 -1
- package/src/sdk/broker/ensure.ts +76 -3
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +254 -34
- package/src/sdk/broker/spawn-substrate.ts +45 -2
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +17 -0
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +118 -24
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +33 -3
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/rows.ts +75 -7
- package/src/sdk/cli/session-cli.ts +68 -35
- package/src/sdk/host/control/dispatch.ts +2 -2
- package/src/sdk/host/events.ts +26 -2
- package/src/sdk/host/host.ts +6 -1
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +497 -31
- package/src/sdk/host/session-runtime.ts +387 -54
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle-session.ts +7 -2
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +54 -3
- package/src/sdk/protocol/operation-registry.ts +1 -1
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +210 -94
- package/src/sdk/session.ts +59 -41
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1033 -260
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/session-manager.ts +517 -77
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +158 -33
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/index.ts +4 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +86 -1
|
@@ -201,6 +201,8 @@ import {
|
|
|
201
201
|
type AsyncJobDeliveryState,
|
|
202
202
|
AsyncJobManager,
|
|
203
203
|
asyncJobEndpointId as deriveAsyncJobEndpointId,
|
|
204
|
+
type FoldReason,
|
|
205
|
+
type JobFoldEvent,
|
|
204
206
|
type OwnerSubagentShutdownLease,
|
|
205
207
|
} from "../async";
|
|
206
208
|
import { reset as resetCapabilities } from "../capability";
|
|
@@ -296,6 +298,7 @@ import type { SessionSwitchEvent } from "../extensibility/shared-events";
|
|
|
296
298
|
import {
|
|
297
299
|
buildSkillPromptMessage,
|
|
298
300
|
getSkillSlashCommandName,
|
|
301
|
+
isNamespacedSkillSlashCommandName,
|
|
299
302
|
parseSkillInvocations,
|
|
300
303
|
type Skill,
|
|
301
304
|
type SkillWarning,
|
|
@@ -311,11 +314,14 @@ import {
|
|
|
311
314
|
import {
|
|
312
315
|
assertNonEmptyGjcSessionId,
|
|
313
316
|
modeStatePath as sessionModeStatePath,
|
|
317
|
+
sessionRuntimeDir,
|
|
314
318
|
sessionStateDir,
|
|
315
319
|
} from "../gjc-runtime/session-layout";
|
|
320
|
+
import { sessionStateLockFailureFields, shouldWarnPersistFailure } from "../gjc-runtime/session-state-lock";
|
|
316
321
|
import {
|
|
317
322
|
type CoordinatorToolObservation,
|
|
318
323
|
clearCoordinatorRuntimeStateRescope,
|
|
324
|
+
GJC_COORDINATOR_SESSION_STATE_FILE_ENV,
|
|
319
325
|
hasCoordinatorRuntimeStateRescopeJournal,
|
|
320
326
|
markCoordinatorRuntimeStateRescopePublishing,
|
|
321
327
|
ownerTerminalContextFromEnvironment,
|
|
@@ -453,7 +459,13 @@ import {
|
|
|
453
459
|
} from "./contribution-prep";
|
|
454
460
|
import { canonicalCoordinatorToolLabel } from "./coordinator-tool-label";
|
|
455
461
|
import { pruneStaleFileMentions } from "./file-mention-pruning";
|
|
456
|
-
import {
|
|
462
|
+
import {
|
|
463
|
+
describeFoldReceipt,
|
|
464
|
+
FOLD_WAKE_MERGE_WINDOW_MS,
|
|
465
|
+
type FoldAdapter,
|
|
466
|
+
FoldCoordinator,
|
|
467
|
+
type ForegroundFoldOutcome,
|
|
468
|
+
} from "./fold-coordinator";
|
|
457
469
|
import type { MemoryGuardRestoreResult } from "./memory-guard-checkpoint-participant";
|
|
458
470
|
import {
|
|
459
471
|
type BashExecutionMessage,
|
|
@@ -1062,6 +1074,8 @@ export interface PromptOptions {
|
|
|
1062
1074
|
streamingBehavior?: "steer" | "followUp";
|
|
1063
1075
|
/** When set to "sequential", this follow-up is delivered one prompt at a time even if followUpMode is "all". */
|
|
1064
1076
|
followUpQueuePolicy?: "respect-mode" | "sequential";
|
|
1077
|
+
/** When set to "sequential", this steer is delivered on its own even if steeringMode is "all". */
|
|
1078
|
+
steerQueuePolicy?: "respect-mode" | "sequential";
|
|
1065
1079
|
/** Optional tool choice override for the next LLM call. */
|
|
1066
1080
|
toolChoice?: ToolChoice;
|
|
1067
1081
|
/** Send as developer/system message instead of user. Providers that support it use the developer role; others fall back to user. */
|
|
@@ -1095,6 +1109,7 @@ type InternalCustomMessageOptions = Pick<
|
|
|
1095
1109
|
| "streamingBehavior"
|
|
1096
1110
|
| "toolChoice"
|
|
1097
1111
|
| "followUpQueuePolicy"
|
|
1112
|
+
| "steerQueuePolicy"
|
|
1098
1113
|
| "onPreflightAccepted"
|
|
1099
1114
|
| "onPreflightAcceptCommit"
|
|
1100
1115
|
| "preflightSignal"
|
|
@@ -1562,6 +1577,21 @@ interface EphemeralTurnResult {
|
|
|
1562
1577
|
*/
|
|
1563
1578
|
const SIGNAL_TEARDOWN_TIMEOUT_MS = 5_000;
|
|
1564
1579
|
|
|
1580
|
+
export class SessionDisposalIncompleteError extends Error {
|
|
1581
|
+
constructor(label?: string) {
|
|
1582
|
+
super(
|
|
1583
|
+
label
|
|
1584
|
+
? `Session disposal exceeded its bounded caller deadline while waiting for ${label}.`
|
|
1585
|
+
: "Session disposal exceeded its bounded caller deadline.",
|
|
1586
|
+
);
|
|
1587
|
+
this.name = "SessionDisposalIncompleteError";
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
export function isSessionDisposalIncompleteError(error: unknown): error is SessionDisposalIncompleteError {
|
|
1592
|
+
return error instanceof Error && error.name === "SessionDisposalIncompleteError";
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1565
1595
|
const AGENT_BASH_ARTIFACT_SAVE_DIAGNOSTIC_MAX_BYTES = 256;
|
|
1566
1596
|
|
|
1567
1597
|
function boundAgentBashArtifactSaveDiagnostic(error: unknown): string {
|
|
@@ -2012,6 +2042,7 @@ function sanitizePostPublicationError(error: string | undefined): string | undef
|
|
|
2012
2042
|
|
|
2013
2043
|
export class WorkerIntegrationRequestScheduler {
|
|
2014
2044
|
#inFlight: Promise<WorkerIntegrationOutcome> | undefined = undefined;
|
|
2045
|
+
#ownedRequests = new Set<Promise<void>>();
|
|
2015
2046
|
#pending = false;
|
|
2016
2047
|
|
|
2017
2048
|
constructor(
|
|
@@ -2039,6 +2070,12 @@ export class WorkerIntegrationRequestScheduler {
|
|
|
2039
2070
|
return outcome;
|
|
2040
2071
|
}
|
|
2041
2072
|
|
|
2073
|
+
async joinOwnedRequests(): Promise<void> {
|
|
2074
|
+
while (this.#ownedRequests.size > 0) {
|
|
2075
|
+
await Promise.allSettled([...this.#ownedRequests]);
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2042
2079
|
#start(): void {
|
|
2043
2080
|
this.#pending = false;
|
|
2044
2081
|
const controller = new AbortController();
|
|
@@ -2048,6 +2085,8 @@ export class WorkerIntegrationRequestScheduler {
|
|
|
2048
2085
|
} catch {
|
|
2049
2086
|
request = Promise.reject(new Error("Worker integration request failed before dispatch."));
|
|
2050
2087
|
}
|
|
2088
|
+
this.#ownedRequests.add(request);
|
|
2089
|
+
void request.finally(() => this.#ownedRequests.delete(request)).catch(() => {});
|
|
2051
2090
|
let timeout: ReturnType<typeof setTimeout> | undefined;
|
|
2052
2091
|
const deadline = new Promise<WorkerIntegrationOutcome>(resolve => {
|
|
2053
2092
|
timeout = setTimeout(() => {
|
|
@@ -2283,6 +2322,7 @@ type SessionAdmissionEntry = {
|
|
|
2283
2322
|
settled: PromiseWithResolvers<void>;
|
|
2284
2323
|
released: boolean;
|
|
2285
2324
|
selectionFenceGeneration: number;
|
|
2325
|
+
selectionTransaction?: symbol;
|
|
2286
2326
|
continuationCapability?: symbol;
|
|
2287
2327
|
};
|
|
2288
2328
|
|
|
@@ -2352,6 +2392,12 @@ interface CanonicalMessageAdmission {
|
|
|
2352
2392
|
release: () => void;
|
|
2353
2393
|
}
|
|
2354
2394
|
|
|
2395
|
+
type CoordinatorRuntimeStatePersistContext = {
|
|
2396
|
+
sessionId: string;
|
|
2397
|
+
cwd: string;
|
|
2398
|
+
sessionFile: string | undefined;
|
|
2399
|
+
stateFile: string | null;
|
|
2400
|
+
};
|
|
2355
2401
|
export class AgentSession {
|
|
2356
2402
|
#provisionalStreamingToolCallIds = new Set<string>();
|
|
2357
2403
|
readonly agent: Agent;
|
|
@@ -2399,6 +2445,8 @@ export class AgentSession {
|
|
|
2399
2445
|
#selectionFenceGenerationContext = new AsyncLocalStorage<number>();
|
|
2400
2446
|
#selectionFenceTail: Promise<void> = Promise.resolve();
|
|
2401
2447
|
#pendingSelectionFences = 0;
|
|
2448
|
+
#selectionAwaitingMutationTransaction: symbol | undefined;
|
|
2449
|
+
#authorizedClosedSelectionTransaction: symbol | undefined;
|
|
2402
2450
|
#selectionFenceDeferredContinuations = new Map<number, number>();
|
|
2403
2451
|
#scopedSettlementWaiters = new Set<() => void>();
|
|
2404
2452
|
#oldestPendingSelectionFenceGeneration = 0;
|
|
@@ -2498,6 +2546,10 @@ export class AgentSession {
|
|
|
2498
2546
|
// continuation of the aborted attempt that the contract requires to be
|
|
2499
2547
|
// blocked (review thread P1).
|
|
2500
2548
|
readonly #externalSteerMessages = new WeakSet<AgentMessage>();
|
|
2549
|
+
/** Steers explicitly requested as one-per-poll; preserve this policy if a run disowns them into follow-ups. */
|
|
2550
|
+
readonly #sequentialSteerMessages = new WeakSet<AgentMessage>();
|
|
2551
|
+
/** Non-admitted steers collected while one abort unwind owns delivery. */
|
|
2552
|
+
#abortUnwindSteerFallbacks: AgentMessage[] = [];
|
|
2501
2553
|
/** Per-message SDK requester-ownership correlation for queued client steers:
|
|
2502
2554
|
* fired exactly once when the steer's run accepts it — via the idle
|
|
2503
2555
|
* auto-continue or the terminal-abort rearm (review thread P1). */
|
|
@@ -2543,6 +2595,157 @@ export class AgentSession {
|
|
|
2543
2595
|
this.#deepInterviewGenuineUserMessageEpochs.delete(message);
|
|
2544
2596
|
}
|
|
2545
2597
|
}
|
|
2598
|
+
|
|
2599
|
+
/**
|
|
2600
|
+
* Queued messages that are independent ROOT requests: they must not run under
|
|
2601
|
+
* a terminally fenced lineage, so a fresh identity is minted when one of them
|
|
2602
|
+
* is actually delivered. Tracked per message rather than as a session latch,
|
|
2603
|
+
* so removing the message (queue edit, producer purge, queue clear) removes
|
|
2604
|
+
* its requirement too and no unrelated continuation inherits it. The mint
|
|
2605
|
+
* happens at the last synchronous point before `agent.continue*` entry —
|
|
2606
|
+
* never at enqueue, which would race the ending run's event settlement, and
|
|
2607
|
+
* never lost when a busy owner defers delivery.
|
|
2608
|
+
*/
|
|
2609
|
+
#freshRootLineageRequests = new WeakSet<AgentMessage>();
|
|
2610
|
+
|
|
2611
|
+
/**
|
|
2612
|
+
* Bind a tag-only display chip (registered by `enqueueCustomMessageDisplay`
|
|
2613
|
+
* before dispatch) to the executable message that was actually queued, and
|
|
2614
|
+
* move it to the list matching how the message was queued.
|
|
2615
|
+
*
|
|
2616
|
+
* Without the binding the chip keeps no identity link, so edit/remove/pop fall
|
|
2617
|
+
* back to POSITIONAL addressing against the Agent queue — and a hidden
|
|
2618
|
+
* `display:false` custom message occupying an earlier queue slot makes that
|
|
2619
|
+
* index point at the wrong message. Migrating the entry also keeps the chip's
|
|
2620
|
+
* mode honest when a requested steer is rerouted to a follow-up.
|
|
2621
|
+
*/
|
|
2622
|
+
#bindCustomDisplayEntry(message: AgentMessage, mode: "steer" | "followUp"): void {
|
|
2623
|
+
const tag = message.role === "custom" ? readPendingDisplayTag(message.details) : undefined;
|
|
2624
|
+
if (tag === undefined) return;
|
|
2625
|
+
const from = mode === "steer" ? this.#followUpMessages : this.#steeringMessages;
|
|
2626
|
+
const to = mode === "steer" ? this.#steeringMessages : this.#followUpMessages;
|
|
2627
|
+
const already = to.find(entry => entry.tag === tag);
|
|
2628
|
+
if (already) {
|
|
2629
|
+
already.message = message;
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
const index = from.findIndex(entry => entry.tag === tag);
|
|
2633
|
+
if (index === -1) return;
|
|
2634
|
+
const [entry] = from.splice(index, 1);
|
|
2635
|
+
if (!entry) return;
|
|
2636
|
+
entry.message = message;
|
|
2637
|
+
to.push(entry);
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
/** True when a queued message still carries an unfulfilled fresh-root requirement. */
|
|
2641
|
+
#hasQueuedFreshRootRequest(): boolean {
|
|
2642
|
+
const { steering, followUp } = this.agent.snapshotQueues();
|
|
2643
|
+
return [...steering, ...followUp].some(message => this.#freshRootLineageRequests.has(message));
|
|
2644
|
+
}
|
|
2645
|
+
|
|
2646
|
+
/**
|
|
2647
|
+
* Disposition of steering the current run leaves unconsumed when it exits.
|
|
2648
|
+
* Set by the abort paths before the run ends and consumed exactly once by
|
|
2649
|
+
* `#settleDisownedSteering` on the run's `agent_end`. Absent means the run
|
|
2650
|
+
* ended on its own (completed/error/pause): steering it never polled is
|
|
2651
|
+
* re-routed as sequential follow-ups owned by the next turn.
|
|
2652
|
+
*/
|
|
2653
|
+
#disownedSteeringDisposition:
|
|
2654
|
+
| { kind: "rearm" }
|
|
2655
|
+
| { kind: "terminal"; preserve: (message: AgentMessage) => boolean; snapshotSeq: number }
|
|
2656
|
+
| { kind: "drop" }
|
|
2657
|
+
| { kind: "held" }
|
|
2658
|
+
| undefined;
|
|
2659
|
+
|
|
2660
|
+
/**
|
|
2661
|
+
* The single owner of steering that a run admitted but never consumed. The
|
|
2662
|
+
* Agent already cleared its queue; this decides once, per message, whether
|
|
2663
|
+
* it is delivered as a follow-up of a fresh turn (user interrupt, natural
|
|
2664
|
+
* end, or a post-snapshot external steer surviving a terminal abort) or
|
|
2665
|
+
* dropped with its removal disposition (terminal abort, teardown), and keeps
|
|
2666
|
+
* the display list aligned with that decision.
|
|
2667
|
+
*/
|
|
2668
|
+
#settleDisownedSteering(event: Extract<AgentEvent, { type: "agent_end" }>): void {
|
|
2669
|
+
const disowned = event.disownedSteering ?? [];
|
|
2670
|
+
const disposition = this.#disownedSteeringDisposition;
|
|
2671
|
+
this.#disownedSteeringDisposition = undefined;
|
|
2672
|
+
// A cancel-and-submit snapshotted the queues before aborting and re-queues
|
|
2673
|
+
// them itself once the abort settles; the display list stays as-is.
|
|
2674
|
+
if (disposition?.kind === "held") return;
|
|
2675
|
+
const dropped: AgentMessage[] = [];
|
|
2676
|
+
const rearmed: AgentMessage[] = [];
|
|
2677
|
+
for (const message of disowned) {
|
|
2678
|
+
const keep =
|
|
2679
|
+
disposition === undefined || disposition.kind === "rearm"
|
|
2680
|
+
? true
|
|
2681
|
+
: disposition.kind === "terminal"
|
|
2682
|
+
? disposition.preserve(message)
|
|
2683
|
+
: false;
|
|
2684
|
+
(keep ? rearmed : dropped).push(message);
|
|
2685
|
+
}
|
|
2686
|
+
const droppedSet = new Set(dropped);
|
|
2687
|
+
const rearmedSet = new Set(rearmed);
|
|
2688
|
+
const rearmedDisplays: QueuedDisplayEntry[] = [];
|
|
2689
|
+
this.#steeringMessages = this.#steeringMessages.filter(entry => {
|
|
2690
|
+
if (entry.message !== undefined && droppedSet.has(entry.message)) return false;
|
|
2691
|
+
if (entry.message !== undefined && rearmedSet.has(entry.message)) {
|
|
2692
|
+
rearmedDisplays.push(entry);
|
|
2693
|
+
return false;
|
|
2694
|
+
}
|
|
2695
|
+
// Tag-only display entries (custom skill chips) belong to whichever
|
|
2696
|
+
// custom message carries their tag.
|
|
2697
|
+
if (entry.tag !== undefined) {
|
|
2698
|
+
const owner = disowned.find(
|
|
2699
|
+
message => message.role === "custom" && readPendingDisplayTag(message.details) === entry.tag,
|
|
2700
|
+
);
|
|
2701
|
+
if (owner && droppedSet.has(owner)) return false;
|
|
2702
|
+
if (owner && rearmedSet.has(owner)) {
|
|
2703
|
+
rearmedDisplays.push(entry);
|
|
2704
|
+
return false;
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
return true;
|
|
2708
|
+
});
|
|
2709
|
+
// Dropped steering leaves without consumption: terminalize its accepted
|
|
2710
|
+
// SDK submissions boundedly (#4668 review P1).
|
|
2711
|
+
this.#fireQueuedRemovalHooks(dropped);
|
|
2712
|
+
if (rearmed.length === 0) return;
|
|
2713
|
+
// Re-route as sequential follow-ups of the NEXT turn, ahead of any
|
|
2714
|
+
// follow-up queued after them, preserving submission order. Marking
|
|
2715
|
+
// each as sequential first, then restoring ahead of the existing queue,
|
|
2716
|
+
// keeps the existing follow-ups' identity and per-message marks intact.
|
|
2717
|
+
let batch: AgentMessage[] = [];
|
|
2718
|
+
const flushBatch = () => {
|
|
2719
|
+
if (batch.length > 1) this.agent.markFollowUpBatch(batch);
|
|
2720
|
+
batch = [];
|
|
2721
|
+
};
|
|
2722
|
+
for (const message of rearmed) {
|
|
2723
|
+
if (this.#sequentialSteerMessages.has(message)) {
|
|
2724
|
+
flushBatch();
|
|
2725
|
+
this.agent.markFollowUpSequential(message);
|
|
2726
|
+
} else if (this.agent.getSteeringMode() === "all") {
|
|
2727
|
+
batch.push(message);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
flushBatch();
|
|
2731
|
+
this.agent.restoreFollowUp(rearmed);
|
|
2732
|
+
this.#followUpMessages = [...rearmedDisplays, ...this.#followUpMessages];
|
|
2733
|
+
for (const message of rearmed) {
|
|
2734
|
+
// External SDK steers keep their promotion hook: it now fires at the
|
|
2735
|
+
// follow-up promotion boundary with startsOwnRun: true.
|
|
2736
|
+
const steerHook = this.#steerPromotionHooks.get(message);
|
|
2737
|
+
if (steerHook) {
|
|
2738
|
+
this.#steerPromotionHooks.delete(message);
|
|
2739
|
+
this.#followUpPromotionHooks.set(message, steerHook);
|
|
2740
|
+
}
|
|
2741
|
+
this.#externalFollowUps.add(message);
|
|
2742
|
+
}
|
|
2743
|
+
// Nothing else owns delivery of a rearm that did not come from an abort
|
|
2744
|
+
// (a natural end, or a fold whose wake turn must carry the steer): the
|
|
2745
|
+
// abort paths schedule their own continuation and this one is a no-op
|
|
2746
|
+
// once the queue is drained.
|
|
2747
|
+
this.#scheduleQueuedDelivery();
|
|
2748
|
+
}
|
|
2546
2749
|
#fireQueuedPromotionHooks(messages: readonly AgentMessage[], promotion?: { startsOwnRun?: boolean }): void {
|
|
2547
2750
|
for (const message of messages) {
|
|
2548
2751
|
const steerHook = this.#steerPromotionHooks.get(message);
|
|
@@ -2633,6 +2836,8 @@ export class AgentSession {
|
|
|
2633
2836
|
|
|
2634
2837
|
// Compaction state
|
|
2635
2838
|
#compactionAbortController: AbortController | undefined = undefined;
|
|
2839
|
+
#compactionCompletion: Promise<void> | undefined;
|
|
2840
|
+
#autoCompactionCompletions = new Set<Promise<AutoCompactionTerminalStatus>>();
|
|
2636
2841
|
#autoCompactionAbortController: AbortController | undefined = undefined;
|
|
2637
2842
|
|
|
2638
2843
|
/** Invocation-scoped EventStream drain barriers owned by active maintenance calls. */
|
|
@@ -2876,10 +3081,18 @@ export class AgentSession {
|
|
|
2876
3081
|
#providerCacheSessionId: string | undefined;
|
|
2877
3082
|
readonly #asyncJobProviderSessionId: string | undefined;
|
|
2878
3083
|
#isDisposed = false;
|
|
2879
|
-
#
|
|
3084
|
+
#disposeRunPromise: Promise<void> | undefined;
|
|
3085
|
+
#disposeCallerPromise: Promise<void> | undefined;
|
|
3086
|
+
#disposeCompleted = false;
|
|
3087
|
+
#disposeTerminalError: unknown;
|
|
2880
3088
|
readonly #disposeAbortController = new AbortController();
|
|
2881
3089
|
#disposeAdmissionClosed: Promise<void> | undefined;
|
|
2882
3090
|
#disposePostPromptDrain: Promise<void> | undefined;
|
|
3091
|
+
#disposeDeadline = 0;
|
|
3092
|
+
#disposeDeadlineTimer: NodeJS.Timeout | undefined;
|
|
3093
|
+
#disposeDeadlineExpired: PromiseWithResolvers<void> | undefined;
|
|
3094
|
+
#disposeTimeoutMs = SIGNAL_TEARDOWN_TIMEOUT_MS;
|
|
3095
|
+
#disposeActiveStepLabel: string | undefined;
|
|
2883
3096
|
readonly #toolSessionCleanups = new Set<() => Promise<void> | void>();
|
|
2884
3097
|
readonly #toolSessionTransitionCleanups = new Set<() => Promise<void> | void>();
|
|
2885
3098
|
readonly #deferredOwnerShutdownFinalizations = new Set<Promise<void>>();
|
|
@@ -3315,8 +3528,17 @@ export class AgentSession {
|
|
|
3315
3528
|
}
|
|
3316
3529
|
|
|
3317
3530
|
#activateNextSessionAdmission(): void {
|
|
3318
|
-
if (this.#activeSessionAdmission
|
|
3319
|
-
|
|
3531
|
+
if (this.#activeSessionAdmission) return;
|
|
3532
|
+
let next: SessionAdmissionEntry | undefined;
|
|
3533
|
+
if (this.#sessionAdmissionClosed) {
|
|
3534
|
+
const index = this.#sessionAdmissionQueue.findIndex(
|
|
3535
|
+
entry => entry.selectionTransaction === this.#authorizedClosedSelectionTransaction,
|
|
3536
|
+
);
|
|
3537
|
+
if (index < 0) return;
|
|
3538
|
+
[next] = this.#sessionAdmissionQueue.splice(index, 1);
|
|
3539
|
+
} else {
|
|
3540
|
+
next = this.#sessionAdmissionQueue.shift();
|
|
3541
|
+
}
|
|
3320
3542
|
if (!next) return;
|
|
3321
3543
|
this.#activeSessionAdmission = next;
|
|
3322
3544
|
next.ready.resolve();
|
|
@@ -3398,6 +3620,9 @@ export class AgentSession {
|
|
|
3398
3620
|
continuationAdmission?: ScheduledContinuationAdmission,
|
|
3399
3621
|
options?: {
|
|
3400
3622
|
allowDuringClosing?: boolean;
|
|
3623
|
+
closedSelectionTransaction?: symbol;
|
|
3624
|
+
selectionTransaction?: symbol;
|
|
3625
|
+
onAfterReadyForTests?: () => Promise<void>;
|
|
3401
3626
|
bypassSelectionFenceGeneration?: number;
|
|
3402
3627
|
allowPromptContinuationReentry?: boolean;
|
|
3403
3628
|
idleDelivery?: boolean;
|
|
@@ -3442,8 +3667,11 @@ export class AgentSession {
|
|
|
3442
3667
|
) {
|
|
3443
3668
|
await awaitPromptInvocationPreflight(this.#selectionFenceTail, signal);
|
|
3444
3669
|
}
|
|
3670
|
+
const closedSelectionAuthorized = (): boolean =>
|
|
3671
|
+
options?.closedSelectionTransaction !== undefined &&
|
|
3672
|
+
options.closedSelectionTransaction === this.#authorizedClosedSelectionTransaction;
|
|
3445
3673
|
if (
|
|
3446
|
-
this.#sessionAdmissionClosed ||
|
|
3674
|
+
(this.#sessionAdmissionClosed && !closedSelectionAuthorized()) ||
|
|
3447
3675
|
((this.#sessionAdmissionClosing || this.#isDisposed) && options?.allowDuringClosing !== true)
|
|
3448
3676
|
)
|
|
3449
3677
|
throw this.#sessionAdmissionBusyError();
|
|
@@ -3464,6 +3692,7 @@ export class AgentSession {
|
|
|
3464
3692
|
settled: Promise.withResolvers<void>(),
|
|
3465
3693
|
released: false,
|
|
3466
3694
|
selectionFenceGeneration: this.#selectionFenceGeneration,
|
|
3695
|
+
...(options?.selectionTransaction ? { selectionTransaction: options.selectionTransaction } : {}),
|
|
3467
3696
|
...(kind === "prompt" ? { continuationCapability: Symbol("scheduled-continuation") } : {}),
|
|
3468
3697
|
};
|
|
3469
3698
|
const releaseEntry = () => {
|
|
@@ -3483,39 +3712,33 @@ export class AgentSession {
|
|
|
3483
3712
|
releaseEntry();
|
|
3484
3713
|
throw error;
|
|
3485
3714
|
}
|
|
3486
|
-
if (
|
|
3487
|
-
this.#sessionAdmissionClosed ||
|
|
3488
|
-
((this.#sessionAdmissionClosing || this.#isDisposed) && options?.allowDuringClosing !== true)
|
|
3489
|
-
) {
|
|
3490
|
-
entry.released = true;
|
|
3491
|
-
entry.settled.resolve();
|
|
3492
|
-
if (this.#activeSessionAdmission === entry) this.#activeSessionAdmission = undefined;
|
|
3493
|
-
this.#activateNextSessionAdmission();
|
|
3494
|
-
throw this.#sessionAdmissionBusyError();
|
|
3495
|
-
}
|
|
3496
|
-
// Re-check the handoff fence after activation: a prompt queued before the
|
|
3497
|
-
// transition began must not start once the fence is up.
|
|
3498
|
-
if (kind === "prompt" && this.#handoffTransitionActive) {
|
|
3499
|
-
entry.released = true;
|
|
3500
|
-
entry.settled.resolve();
|
|
3501
|
-
if (this.#activeSessionAdmission === entry) this.#activeSessionAdmission = undefined;
|
|
3502
|
-
this.#activateNextSessionAdmission();
|
|
3503
|
-
throw Object.assign(new AgentBusyError("Cannot start a turn while a handoff is in progress."), {
|
|
3504
|
-
code: "busy",
|
|
3505
|
-
});
|
|
3506
|
-
}
|
|
3507
|
-
|
|
3508
|
-
const release = () => {
|
|
3509
|
-
releaseEntry();
|
|
3510
|
-
};
|
|
3511
3715
|
try {
|
|
3716
|
+
await options?.onAfterReadyForTests?.();
|
|
3717
|
+
if (
|
|
3718
|
+
entry.released ||
|
|
3719
|
+
(this.#sessionAdmissionClosed && !closedSelectionAuthorized()) ||
|
|
3720
|
+
((this.#sessionAdmissionClosing || this.#isDisposed) && options?.allowDuringClosing !== true)
|
|
3721
|
+
) {
|
|
3722
|
+
throw this.#sessionAdmissionBusyError();
|
|
3723
|
+
}
|
|
3724
|
+
// Re-check the handoff fence after activation: a prompt queued before the
|
|
3725
|
+
// transition began must not start once the fence is up.
|
|
3726
|
+
if (kind === "prompt" && this.#handoffTransitionActive) {
|
|
3727
|
+
throw Object.assign(new AgentBusyError("Cannot start a turn while a handoff is in progress."), {
|
|
3728
|
+
code: "busy",
|
|
3729
|
+
});
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
const release = () => {
|
|
3733
|
+
releaseEntry();
|
|
3734
|
+
};
|
|
3512
3735
|
return await this.#sessionAdmissionContext.run(entry, () => body({ release }));
|
|
3513
3736
|
} finally {
|
|
3514
|
-
|
|
3737
|
+
releaseEntry();
|
|
3515
3738
|
}
|
|
3516
3739
|
}
|
|
3517
3740
|
|
|
3518
|
-
async #closeSessionAdmission(): Promise<void> {
|
|
3741
|
+
async #closeSessionAdmission(options?: { waitForActive?: boolean }): Promise<void> {
|
|
3519
3742
|
this.#sessionAdmissionClosing = true;
|
|
3520
3743
|
const active = this.#activeSessionAdmission;
|
|
3521
3744
|
if (active?.kind === "prompt") {
|
|
@@ -3523,6 +3746,29 @@ export class AgentSession {
|
|
|
3523
3746
|
this.#promptPreflightCancellationGeneration++;
|
|
3524
3747
|
this.#promptPreflightAbortController.abort();
|
|
3525
3748
|
}
|
|
3749
|
+
if (options?.waitForActive === false) {
|
|
3750
|
+
this.#authorizedClosedSelectionTransaction =
|
|
3751
|
+
active?.kind === "prompt"
|
|
3752
|
+
? undefined
|
|
3753
|
+
: (active?.selectionTransaction ?? this.#selectionAwaitingMutationTransaction);
|
|
3754
|
+
this.#sessionAdmissionClosed = true;
|
|
3755
|
+
const queued = this.#sessionAdmissionQueue.splice(0);
|
|
3756
|
+
for (const entry of queued) {
|
|
3757
|
+
entry.released = true;
|
|
3758
|
+
entry.ready.resolve();
|
|
3759
|
+
entry.settled.resolve();
|
|
3760
|
+
}
|
|
3761
|
+
// Only an active prompt may outlive bounded disposal. A selection mutates
|
|
3762
|
+
// session/model state and must finish (or observe the closed fence) before
|
|
3763
|
+
// teardown closes those resources; its own finally resolves the fence tail.
|
|
3764
|
+
if (active?.kind === "selection") await active.settled.promise;
|
|
3765
|
+
// An abort-ignoring prompt may have queued selection fences behind it, so
|
|
3766
|
+
// that one case must bypass the fence. Otherwise the fence covers a
|
|
3767
|
+
// selection between its validation and mutation admissions as well as one
|
|
3768
|
+
// currently admitted.
|
|
3769
|
+
if (active?.kind !== "prompt") await this.#selectionFenceTail;
|
|
3770
|
+
return;
|
|
3771
|
+
}
|
|
3526
3772
|
if (active) await active.settled.promise;
|
|
3527
3773
|
await this.#selectionFenceTail;
|
|
3528
3774
|
this.#sessionAdmissionClosed = true;
|
|
@@ -4027,11 +4273,8 @@ export class AgentSession {
|
|
|
4027
4273
|
lease: RunResourceProducerLease | undefined,
|
|
4028
4274
|
workerIntegrationSettled: boolean,
|
|
4029
4275
|
): Promise<void> {
|
|
4030
|
-
const publicationContext =
|
|
4031
|
-
|
|
4032
|
-
cwd: this.sessionManager.getCwd(),
|
|
4033
|
-
sessionFile: this.sessionManager.getSessionFile(),
|
|
4034
|
-
};
|
|
4276
|
+
const publicationContext = this.#captureCoordinatorRuntimeStatePersistContext();
|
|
4277
|
+
|
|
4035
4278
|
const publicationScope = (pending as AgentSessionEvent & { scope?: AttemptScopeRef }).scope as
|
|
4036
4279
|
| AttemptScope
|
|
4037
4280
|
| undefined;
|
|
@@ -4102,7 +4345,6 @@ export class AgentSession {
|
|
|
4102
4345
|
error: String(error),
|
|
4103
4346
|
},
|
|
4104
4347
|
);
|
|
4105
|
-
logger.warn("Failed to persist terminal coordinator runtime state", { error: String(error) });
|
|
4106
4348
|
},
|
|
4107
4349
|
);
|
|
4108
4350
|
extensionDelivery = this.#queueExtensionEvent(
|
|
@@ -4122,9 +4364,7 @@ export class AgentSession {
|
|
|
4122
4364
|
),
|
|
4123
4365
|
);
|
|
4124
4366
|
await extensionDelivery;
|
|
4125
|
-
void terminalPersistence
|
|
4126
|
-
logger.warn("Terminal persistence continued after SDK publication", { error }),
|
|
4127
|
-
);
|
|
4367
|
+
void terminalPersistence;
|
|
4128
4368
|
} else {
|
|
4129
4369
|
// Non-SDK/public publication has already awaited worker integration above.
|
|
4130
4370
|
}
|
|
@@ -5376,21 +5616,26 @@ export class AgentSession {
|
|
|
5376
5616
|
|
|
5377
5617
|
async #runToolSessionTransitionCleanups(): Promise<void> {
|
|
5378
5618
|
const cleanups = Array.from(this.#toolSessionTransitionCleanups);
|
|
5379
|
-
this.#toolSessionTransitionCleanups.clear();
|
|
5380
5619
|
const results = await Promise.allSettled(cleanups.map(async cleanup => await cleanup()));
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5620
|
+
const failures: unknown[] = [];
|
|
5621
|
+
for (let index = 0; index < results.length; index++) {
|
|
5622
|
+
const result = results[index]!;
|
|
5623
|
+
if (result.status === "fulfilled") this.#toolSessionTransitionCleanups.delete(cleanups[index]!);
|
|
5624
|
+
else failures.push(result.reason);
|
|
5384
5625
|
}
|
|
5626
|
+
if (failures.length > 0) throw new AggregateError(failures, "Tool session transition cleanup failed.");
|
|
5385
5627
|
}
|
|
5386
5628
|
|
|
5387
5629
|
async #runToolSessionCleanups(): Promise<void> {
|
|
5388
5630
|
const cleanups = Array.from(this.#toolSessionCleanups);
|
|
5389
|
-
this.#toolSessionCleanups.clear();
|
|
5390
5631
|
const results = await Promise.allSettled(cleanups.map(async cleanup => await cleanup()));
|
|
5391
|
-
|
|
5392
|
-
|
|
5632
|
+
const failures: unknown[] = [];
|
|
5633
|
+
for (let index = 0; index < results.length; index++) {
|
|
5634
|
+
const result = results[index]!;
|
|
5635
|
+
if (result.status === "fulfilled") this.#toolSessionCleanups.delete(cleanups[index]!);
|
|
5636
|
+
else failures.push(result.reason);
|
|
5393
5637
|
}
|
|
5638
|
+
if (failures.length > 0) throw new AggregateError(failures, "Tool session cleanup failed.");
|
|
5394
5639
|
}
|
|
5395
5640
|
|
|
5396
5641
|
getAsyncJobSnapshot(options?: { recentLimit?: number }): AsyncJobSnapshot | null {
|
|
@@ -5663,6 +5908,10 @@ export class AgentSession {
|
|
|
5663
5908
|
// First statement of the listener: the observation must precede every claim,
|
|
5664
5909
|
// reservation, and async hop this handler performs.
|
|
5665
5910
|
this.#observeCoordinatorToolEvent(event);
|
|
5911
|
+
// The run that owned the steering queue is over. Decide ONCE, synchronously
|
|
5912
|
+
// and before any ownership gate, what happens to steering it never consumed
|
|
5913
|
+
// — a late terminal of a superseded run must still settle its own steering.
|
|
5914
|
+
if (event.type === "agent_end" && event.disownedSteering?.length) this.#settleDisownedSteering(event);
|
|
5666
5915
|
// Reserve canonical message order synchronously. Agent listeners are not
|
|
5667
5916
|
// awaited, so a tool-result spill may yield while a later continuation
|
|
5668
5917
|
// otherwise overtakes it in persisted/display context.
|
|
@@ -5781,7 +6030,7 @@ export class AgentSession {
|
|
|
5781
6030
|
#coordinatorPersistQueue: Promise<void> = Promise.resolve();
|
|
5782
6031
|
|
|
5783
6032
|
#recordPostPublicationOutcome(
|
|
5784
|
-
context:
|
|
6033
|
+
context: CoordinatorRuntimeStatePersistContext,
|
|
5785
6034
|
correlationId: string | undefined,
|
|
5786
6035
|
kind: "worker_integration" | "terminal_persistence",
|
|
5787
6036
|
outcome: { status: "completed" | "failed" | "timed_out"; error?: string },
|
|
@@ -5801,19 +6050,12 @@ export class AgentSession {
|
|
|
5801
6050
|
const persist = async () => {
|
|
5802
6051
|
if (barrier) {
|
|
5803
6052
|
await barrier;
|
|
5804
|
-
return await persistCoordinatorWorkerIntegrationOutcome(
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
{
|
|
5811
|
-
...outcome,
|
|
5812
|
-
kind,
|
|
5813
|
-
correlationId,
|
|
5814
|
-
error: sanitizePostPublicationError(outcome.error),
|
|
5815
|
-
},
|
|
5816
|
-
);
|
|
6053
|
+
return await persistCoordinatorWorkerIntegrationOutcome(context, {
|
|
6054
|
+
...outcome,
|
|
6055
|
+
kind,
|
|
6056
|
+
correlationId,
|
|
6057
|
+
error: sanitizePostPublicationError(outcome.error),
|
|
6058
|
+
});
|
|
5817
6059
|
}
|
|
5818
6060
|
return generation === this.#coordinatorPersistGeneration
|
|
5819
6061
|
? await persistCoordinatorWorkerIntegrationOutcome(context, {
|
|
@@ -5830,10 +6072,30 @@ export class AgentSession {
|
|
|
5830
6072
|
if (barrier) this.#trackReleasedBarrierPersist(queued);
|
|
5831
6073
|
else this.#trackUnbarrieredCoordinatorPersist(queued);
|
|
5832
6074
|
void queued.catch(error =>
|
|
5833
|
-
|
|
6075
|
+
this.#warnPersistFailure(
|
|
6076
|
+
"Failed to persist terminal reconciliation outcome",
|
|
6077
|
+
error,
|
|
6078
|
+
context.stateFile,
|
|
6079
|
+
context.sessionId,
|
|
6080
|
+
),
|
|
5834
6081
|
);
|
|
5835
6082
|
}
|
|
5836
6083
|
|
|
6084
|
+
#captureCoordinatorRuntimeStatePersistContext(): CoordinatorRuntimeStatePersistContext {
|
|
6085
|
+
const context = {
|
|
6086
|
+
sessionId: this.sessionId,
|
|
6087
|
+
cwd: this.sessionManager.getCwd(),
|
|
6088
|
+
sessionFile: this.sessionManager.getSessionFile(),
|
|
6089
|
+
};
|
|
6090
|
+
const explicitStateFile = process.env[GJC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
|
|
6091
|
+
const stateFile =
|
|
6092
|
+
explicitStateFile ||
|
|
6093
|
+
(context.sessionId.trim()
|
|
6094
|
+
? path.join(sessionRuntimeDir(context.cwd, context.sessionId), "runtime-state.json")
|
|
6095
|
+
: null);
|
|
6096
|
+
return { ...context, stateFile };
|
|
6097
|
+
}
|
|
6098
|
+
|
|
5837
6099
|
/**
|
|
5838
6100
|
* Reserve this event's place in the sidecar write order.
|
|
5839
6101
|
*
|
|
@@ -5854,26 +6116,14 @@ export class AgentSession {
|
|
|
5854
6116
|
if (barrier) {
|
|
5855
6117
|
const run = async () => {
|
|
5856
6118
|
await barrier;
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
{
|
|
5860
|
-
sessionId: this.sessionId,
|
|
5861
|
-
cwd: this.sessionManager.getCwd(),
|
|
5862
|
-
sessionFile: this.sessionManager.getSessionFile(),
|
|
5863
|
-
},
|
|
5864
|
-
observation,
|
|
5865
|
-
propagateFailure,
|
|
5866
|
-
);
|
|
6119
|
+
const context = this.#captureCoordinatorRuntimeStatePersistContext();
|
|
6120
|
+
await this.#persistRuntimeStateInBackground(event, context, observation, propagateFailure);
|
|
5867
6121
|
};
|
|
5868
6122
|
const queued = barrier.then(() => this.#appendCoordinatorPersist(run));
|
|
5869
6123
|
this.#trackReleasedBarrierPersist(queued);
|
|
5870
6124
|
return queued;
|
|
5871
6125
|
}
|
|
5872
|
-
const context =
|
|
5873
|
-
sessionId: this.sessionId,
|
|
5874
|
-
cwd: this.sessionManager.getCwd(),
|
|
5875
|
-
sessionFile: this.sessionManager.getSessionFile(),
|
|
5876
|
-
};
|
|
6126
|
+
const context = this.#captureCoordinatorRuntimeStatePersistContext();
|
|
5877
6127
|
const generation = admission?.persistGeneration ?? this.#coordinatorPersistGeneration;
|
|
5878
6128
|
const run = () =>
|
|
5879
6129
|
generation === this.#coordinatorPersistGeneration
|
|
@@ -5886,18 +6136,41 @@ export class AgentSession {
|
|
|
5886
6136
|
|
|
5887
6137
|
async #persistRuntimeStateInBackground(
|
|
5888
6138
|
event: AgentSessionEvent,
|
|
5889
|
-
context:
|
|
6139
|
+
context: CoordinatorRuntimeStatePersistContext,
|
|
5890
6140
|
observation: CoordinatorToolObservation | undefined,
|
|
5891
6141
|
propagateFailure: boolean,
|
|
5892
6142
|
): Promise<void> {
|
|
5893
6143
|
try {
|
|
5894
6144
|
await persistCoordinatorRuntimeStateFromEvent(event, context, observation);
|
|
5895
6145
|
} catch (error) {
|
|
5896
|
-
|
|
6146
|
+
this.#warnPersistFailure(
|
|
6147
|
+
"Failed to persist coordinator runtime state",
|
|
6148
|
+
error,
|
|
6149
|
+
context.stateFile,
|
|
6150
|
+
context.sessionId,
|
|
6151
|
+
{ event: event.type },
|
|
6152
|
+
);
|
|
5897
6153
|
if (propagateFailure) throw error;
|
|
5898
6154
|
}
|
|
5899
6155
|
}
|
|
5900
6156
|
|
|
6157
|
+
#warnPersistFailure(
|
|
6158
|
+
message: string,
|
|
6159
|
+
error: unknown,
|
|
6160
|
+
stateFile: string | null,
|
|
6161
|
+
sessionId: string,
|
|
6162
|
+
extra: Record<string, unknown> = {},
|
|
6163
|
+
): void {
|
|
6164
|
+
const fields = {
|
|
6165
|
+
...extra,
|
|
6166
|
+
...sessionStateLockFailureFields(error),
|
|
6167
|
+
...(stateFile === null ? {} : { stateFile }),
|
|
6168
|
+
};
|
|
6169
|
+
const key = stateFile ?? `session:${sessionId}`;
|
|
6170
|
+
if (shouldWarnPersistFailure(key, error)) logger.warn(message, fields);
|
|
6171
|
+
else logger.debug(message, { ...fields, suppressed: true });
|
|
6172
|
+
}
|
|
6173
|
+
|
|
5901
6174
|
async #emitSessionEvent(event: AgentSessionEvent, eventLease?: RunResourceProducerLease): Promise<void> {
|
|
5902
6175
|
const attemptScope = (event as AgentSessionEvent & { scope?: AttemptScope }).scope;
|
|
5903
6176
|
if (event.type === "turn_start") {
|
|
@@ -5955,9 +6228,7 @@ export class AgentSession {
|
|
|
5955
6228
|
// Do not let a lock-hostile sidecar suppress the terminal event itself.
|
|
5956
6229
|
const terminalPersistence = this.#queueCoordinatorRuntimeStatePersist(event);
|
|
5957
6230
|
this.#emit(event);
|
|
5958
|
-
void terminalPersistence
|
|
5959
|
-
logger.warn("Failed to persist terminal coordinator runtime state", { error: String(error) });
|
|
5960
|
-
});
|
|
6231
|
+
void terminalPersistence;
|
|
5961
6232
|
await this.#emitExtensionEvent(event);
|
|
5962
6233
|
return;
|
|
5963
6234
|
}
|
|
@@ -7161,10 +7432,14 @@ export class AgentSession {
|
|
|
7161
7432
|
skip("queue_drained");
|
|
7162
7433
|
return;
|
|
7163
7434
|
}
|
|
7164
|
-
//
|
|
7165
|
-
//
|
|
7166
|
-
//
|
|
7167
|
-
//
|
|
7435
|
+
// Final synchronous boundary before agent.continue* entry; no await
|
|
7436
|
+
// intervenes between here and method entry. A same-turn continuation
|
|
7437
|
+
// of a terminally aborted turn is denied here, while an independent
|
|
7438
|
+
// root request queued past that fence mints its fresh identity here —
|
|
7439
|
+
// so a cancelled or emptied continuation never leaves the session on
|
|
7440
|
+
// an identity that escapes the fence. Owned-completion deliveries are
|
|
7441
|
+
// NOT affected (they use followUp).
|
|
7442
|
+
if (this.#hasQueuedFreshRootRequest()) this.#resumeFromOwnedCompletion();
|
|
7168
7443
|
if (this.#isTurnContinuationBlocked()) {
|
|
7169
7444
|
skip("terminal_turn");
|
|
7170
7445
|
return;
|
|
@@ -8712,26 +8987,86 @@ export class AgentSession {
|
|
|
8712
8987
|
*/
|
|
8713
8988
|
dispose(): Promise<void> {
|
|
8714
8989
|
this.#evalExecutionDisposing = true;
|
|
8715
|
-
if (this.#
|
|
8716
|
-
|
|
8717
|
-
this.#
|
|
8718
|
-
this.#
|
|
8719
|
-
|
|
8720
|
-
|
|
8721
|
-
|
|
8722
|
-
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
|
|
8727
|
-
|
|
8728
|
-
|
|
8729
|
-
|
|
8730
|
-
|
|
8731
|
-
|
|
8732
|
-
|
|
8733
|
-
|
|
8734
|
-
|
|
8990
|
+
if (this.#disposeCompleted) return Promise.resolve();
|
|
8991
|
+
if (this.#disposeTerminalError !== undefined) return Promise.reject(this.#disposeTerminalError);
|
|
8992
|
+
if (this.#disposeCallerPromise) return this.#disposeCallerPromise;
|
|
8993
|
+
if (!this.#disposeRunPromise) {
|
|
8994
|
+
this.#disposeDeadline = Date.now() + this.#disposeTimeoutMs;
|
|
8995
|
+
this.#disposeDeadlineExpired = Promise.withResolvers<void>();
|
|
8996
|
+
this.#disposeDeadlineTimer = setTimeout(() => this.#disposeDeadlineExpired?.resolve(), this.#disposeTimeoutMs);
|
|
8997
|
+
this.#disposeDeadlineTimer.unref?.();
|
|
8998
|
+
this.#abortAdmissionEpoch++;
|
|
8999
|
+
this.#isDisposed = true;
|
|
9000
|
+
// Invalidate every coordinator event admitted before disposal. Handlers may
|
|
9001
|
+
// still unwind, but their captured generation can no longer enqueue a write.
|
|
9002
|
+
this.#coordinatorPersistGeneration += 1;
|
|
9003
|
+
this.#disposeAbortController.abort();
|
|
9004
|
+
// Disposal owns a bounded Agent abort below. Waiting for the active prompt's
|
|
9005
|
+
// admission here would put that unbounded prompt ahead of the abort budget.
|
|
9006
|
+
this.#disposeAdmissionClosed = this.#closeSessionAdmission({ waitForActive: false });
|
|
9007
|
+
this.#disposePostPromptDrain = this.#cancelPostPromptTasks();
|
|
9008
|
+
this.#settleDeliveredOwnedRegistrations(this.#pendingNextTurnMessages.map(entry => entry.message));
|
|
9009
|
+
this.#pendingNextTurnMessages = [];
|
|
9010
|
+
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
9011
|
+
this.#abortActiveMidRunBarriers();
|
|
9012
|
+
this.abortCompaction();
|
|
9013
|
+
this.abortRetry();
|
|
9014
|
+
this.#quarantineAgentRunResources();
|
|
9015
|
+
this.agent.abort();
|
|
9016
|
+
this.agent.setMainAttemptScopeObserver(undefined);
|
|
9017
|
+
this.#disconnectFromAgent();
|
|
9018
|
+
this.#disposeRunPromise = this.#dispose()
|
|
9019
|
+
.then(
|
|
9020
|
+
() => {
|
|
9021
|
+
this.#disposeCompleted = true;
|
|
9022
|
+
},
|
|
9023
|
+
error => {
|
|
9024
|
+
this.#disposeTerminalError = error;
|
|
9025
|
+
throw error;
|
|
9026
|
+
},
|
|
9027
|
+
)
|
|
9028
|
+
.finally(() => {
|
|
9029
|
+
if (this.#disposeDeadlineTimer) clearTimeout(this.#disposeDeadlineTimer);
|
|
9030
|
+
this.#disposeDeadlineTimer = undefined;
|
|
9031
|
+
});
|
|
9032
|
+
void this.#disposeRunPromise.catch(() => {});
|
|
9033
|
+
}
|
|
9034
|
+
const teardown = this.#disposeRunPromise;
|
|
9035
|
+
const deadlineExpired = this.#disposeDeadlineExpired;
|
|
9036
|
+
if (!deadlineExpired || Date.now() >= this.#disposeDeadline) {
|
|
9037
|
+
return Promise.reject(new SessionDisposalIncompleteError(this.#disposeActiveStepLabel));
|
|
9038
|
+
}
|
|
9039
|
+
const boundedTeardown = Promise.race([
|
|
9040
|
+
teardown,
|
|
9041
|
+
deadlineExpired.promise.then(() => {
|
|
9042
|
+
throw new SessionDisposalIncompleteError(this.#disposeActiveStepLabel);
|
|
9043
|
+
}),
|
|
9044
|
+
]);
|
|
9045
|
+
this.#disposeCallerPromise = boundedTeardown;
|
|
9046
|
+
void boundedTeardown
|
|
9047
|
+
.finally(() => {
|
|
9048
|
+
if (this.#disposeCallerPromise === boundedTeardown) this.#disposeCallerPromise = undefined;
|
|
9049
|
+
})
|
|
9050
|
+
.catch(() => {});
|
|
9051
|
+
return boundedTeardown;
|
|
9052
|
+
}
|
|
9053
|
+
|
|
9054
|
+
/** Join the retained teardown owner without allocating another public deadline. */
|
|
9055
|
+
awaitDisposeCompletion(): Promise<void> {
|
|
9056
|
+
return this.#disposeRunPromise ?? this.dispose();
|
|
9057
|
+
}
|
|
9058
|
+
|
|
9059
|
+
setDisposeTimeoutForTests(timeoutMs: number): void {
|
|
9060
|
+
if (this.#disposeRunPromise) throw new Error("Cannot change the disposal timeout after disposal has started.");
|
|
9061
|
+
this.#disposeTimeoutMs = Math.max(0, timeoutMs);
|
|
9062
|
+
}
|
|
9063
|
+
|
|
9064
|
+
trackPostPromptTaskForTests(task: Promise<void>): void {
|
|
9065
|
+
this.#trackPostPromptTask(task, this.#selectionFenceGeneration);
|
|
9066
|
+
}
|
|
9067
|
+
|
|
9068
|
+
requestWorkerIntegrationForTests(): void {
|
|
9069
|
+
this.#requestWorkerIntegrationAttempt();
|
|
8735
9070
|
}
|
|
8736
9071
|
|
|
8737
9072
|
/** Cancel the active logical run domain, including managed continuations detached from Agent's active attempt. */
|
|
@@ -8741,9 +9076,33 @@ export class AgentSession {
|
|
|
8741
9076
|
}
|
|
8742
9077
|
|
|
8743
9078
|
async #dispose(): Promise<void> {
|
|
9079
|
+
const disposeFailures: Array<{ label: string; error: unknown; critical: boolean }> = [];
|
|
9080
|
+
const awaitDisposeStep = async <T>(
|
|
9081
|
+
label: string,
|
|
9082
|
+
operation: Promise<T>,
|
|
9083
|
+
critical = false,
|
|
9084
|
+
): Promise<T | undefined> => {
|
|
9085
|
+
this.#disposeActiveStepLabel = label;
|
|
9086
|
+
try {
|
|
9087
|
+
return await operation;
|
|
9088
|
+
} catch (error) {
|
|
9089
|
+
disposeFailures.push({ label, error, critical });
|
|
9090
|
+
logger.warn("Session dispose step failed", { label, error: String(error) });
|
|
9091
|
+
return undefined;
|
|
9092
|
+
} finally {
|
|
9093
|
+
if (this.#disposeActiveStepLabel === label) this.#disposeActiveStepLabel = undefined;
|
|
9094
|
+
}
|
|
9095
|
+
};
|
|
9096
|
+
const drainCoordinatorEventHandlers = async (): Promise<void> => {
|
|
9097
|
+
while (this.#coordinatorEventHandlers.size > 0) {
|
|
9098
|
+
await Promise.allSettled([...this.#coordinatorEventHandlers]);
|
|
9099
|
+
}
|
|
9100
|
+
};
|
|
8744
9101
|
const admissionClosed = this.#disposeAdmissionClosed ?? this.#closeSessionAdmission();
|
|
8745
|
-
await admissionClosed;
|
|
8746
|
-
await this.sessionManager.closeCwdMoveAdmission();
|
|
9102
|
+
await awaitDisposeStep("session admission close", admissionClosed);
|
|
9103
|
+
await awaitDisposeStep("cwd move admission close", this.sessionManager.closeCwdMoveAdmission());
|
|
9104
|
+
await awaitDisposeStep("admitted cwd transition", this.sessionManager.joinCwdTransition(), true);
|
|
9105
|
+
await awaitDisposeStep("active cwd readers", this.sessionManager.joinCwdReaders(), true);
|
|
8747
9106
|
this.#isDisposed = true;
|
|
8748
9107
|
// Reject new direct Python starts as soon as disposal begins (synchronously,
|
|
8749
9108
|
// before any await) so callers cannot race a start against teardown.
|
|
@@ -8761,60 +9120,71 @@ export class AgentSession {
|
|
|
8761
9120
|
// R2-5: join any in-flight mid-run maintenance invocation before teardown so the
|
|
8762
9121
|
// abort-aware maintenance promise (already aborted above) settles and cannot touch
|
|
8763
9122
|
// torn-down state afterward.
|
|
8764
|
-
await this.#waitForActiveMidRunMaintenance();
|
|
9123
|
+
await awaitDisposeStep("mid-run maintenance", this.#waitForActiveMidRunMaintenance());
|
|
8765
9124
|
// R2-5: give the aborted Agent run a bounded chance to settle, then force-
|
|
8766
9125
|
// invalidate its run id so an abort-ignoring provider/tool cannot emit a late
|
|
8767
9126
|
// message_end after teardown. Mirrors AgentSession.abort({ timeoutMs }).
|
|
9127
|
+
const idleBudgetMs = Math.max(0, Math.min(2_000, this.#disposeDeadline - Date.now()));
|
|
8768
9128
|
const disposeIdleSettled = await Promise.race([
|
|
8769
9129
|
this.agent.waitForIdle().then(
|
|
8770
9130
|
() => true,
|
|
8771
9131
|
() => true,
|
|
8772
9132
|
),
|
|
8773
|
-
Bun.sleep(
|
|
9133
|
+
Bun.sleep(idleBudgetMs).then(() => false),
|
|
8774
9134
|
]);
|
|
8775
9135
|
if (!disposeIdleSettled) {
|
|
8776
|
-
|
|
8777
|
-
this.agent.forceAbort("Session disposed", this.#activeLogicalRunId);
|
|
8778
|
-
} catch {
|
|
8779
|
-
// AttemptScope handle may not be registered for sessions
|
|
8780
|
-
// that don't participate in the facility (e.g. test mocks).
|
|
8781
|
-
this.agent.forceAbort("Session disposed");
|
|
8782
|
-
}
|
|
9136
|
+
this.#forceSessionRecovery();
|
|
8783
9137
|
}
|
|
8784
|
-
await this.#
|
|
8785
|
-
await
|
|
8786
|
-
|
|
9138
|
+
await awaitDisposeStep("active compaction", this.#compactionCompletion ?? Promise.resolve(), true);
|
|
9139
|
+
await awaitDisposeStep(
|
|
9140
|
+
"active auto-compaction",
|
|
9141
|
+
Promise.allSettled([...this.#autoCompactionCompletions]).then(() => {}),
|
|
9142
|
+
true,
|
|
9143
|
+
);
|
|
9144
|
+
await awaitDisposeStep("agent end publication", this.#agentEndPublicationPromise);
|
|
9145
|
+
await awaitDisposeStep("queued extension events", this.#queuedExtensionEvents);
|
|
9146
|
+
await awaitDisposeStep("agent end handling", this.#agentEndHandlingPromise);
|
|
9147
|
+
await awaitDisposeStep("coordinator event handlers", drainCoordinatorEventHandlers(), true);
|
|
8787
9148
|
// Drain the sidecar write order for the same reason the two queues above are
|
|
8788
9149
|
// drained: each entry writes under the native identity-bound state-file lock, so a
|
|
8789
9150
|
// a still-queued write would run after the session that owns it is gone — releasing
|
|
8790
9151
|
// a lock whose owner no longer exists, and under `bun test --isolate` calling into
|
|
8791
9152
|
// the addon after the runtime tore down the context it was scheduled in. The chain
|
|
8792
9153
|
// is already failure-absorbing, so this only waits.
|
|
8793
|
-
await this.#coordinatorPersistQueue;
|
|
9154
|
+
await awaitDisposeStep("coordinator persistence", this.#coordinatorPersistQueue, true);
|
|
8794
9155
|
// Terminal publication records its reconciliation from a promise reaction, which
|
|
8795
9156
|
// can append after the queue snapshot above was awaited. Drain the tracked writes
|
|
8796
9157
|
// again so disposal cannot return while that late reconciliation still owns a lock.
|
|
8797
|
-
await this.#drainUnbarrieredCoordinatorPersists();
|
|
9158
|
+
await awaitDisposeStep("unbarriered coordinator persistence", this.#drainUnbarrieredCoordinatorPersists(), true);
|
|
8798
9159
|
this.#pendingBackgroundExchanges = [];
|
|
8799
9160
|
this.#drainTerminalOwnedYieldEntries();
|
|
8800
9161
|
|
|
8801
9162
|
this.agent.setOnBeforeYield(undefined);
|
|
8802
9163
|
try {
|
|
8803
9164
|
if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
|
|
8804
|
-
await
|
|
9165
|
+
await awaitDisposeStep(
|
|
9166
|
+
"session shutdown handlers",
|
|
9167
|
+
this.#extensionRunner.emit({ type: "session_shutdown" }),
|
|
9168
|
+
);
|
|
8805
9169
|
}
|
|
8806
9170
|
} catch (error) {
|
|
9171
|
+
if (isSessionDisposalIncompleteError(error)) throw error;
|
|
8807
9172
|
logger.warn("Failed to emit session_shutdown event", { error: String(error) });
|
|
8808
9173
|
}
|
|
8809
9174
|
this.#workflowGateEmitter?.fence?.();
|
|
8810
9175
|
this.#workflowGateEmitter = undefined;
|
|
8811
9176
|
this.#notifyWorkflowGateEmitterChanged(this.sessionId, undefined);
|
|
8812
|
-
await this.#flushWorkerIntegrationAttempt();
|
|
9177
|
+
await awaitDisposeStep("worker integration", this.#flushWorkerIntegrationAttempt(), true);
|
|
9178
|
+
await awaitDisposeStep(
|
|
9179
|
+
"worker integration ownership",
|
|
9180
|
+
this.#workerIntegrationScheduler?.joinOwnedRequests() ?? Promise.resolve(),
|
|
9181
|
+
true,
|
|
9182
|
+
);
|
|
8813
9183
|
// Worker integration completion can enqueue terminal reconciliation while the
|
|
8814
9184
|
// 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());
|
|
9185
|
+
await awaitDisposeStep("post-worker coordinator persistence", this.#coordinatorPersistQueue, true);
|
|
9186
|
+
await awaitDisposeStep("post-worker unbarriered persistence", this.#drainUnbarrieredCoordinatorPersists(), true);
|
|
9187
|
+
await awaitDisposeStep("post-prompt tasks", this.#disposePostPromptDrain ?? this.#cancelPostPromptTasks(), true);
|
|
8818
9188
|
// Cancel jobs this agent registered so a subagent's teardown doesn't
|
|
8819
9189
|
// leak its background bash/task work into the parent's manager. Only
|
|
8820
9190
|
// the session that owns the manager goes on to dispose it (which itself
|
|
@@ -8829,32 +9199,37 @@ export class AgentSession {
|
|
|
8829
9199
|
// — a shared manager would otherwise keep the claim past this session's
|
|
8830
9200
|
// disposal.
|
|
8831
9201
|
this.#drainTerminalOwnedYieldEntries();
|
|
8832
|
-
await Promise.allSettled(this.#deferredOwnerShutdownFinalizations);
|
|
9202
|
+
await awaitDisposeStep("deferred owner shutdown", Promise.allSettled(this.#deferredOwnerShutdownFinalizations));
|
|
8833
9203
|
const ownedAsyncManager = this.#ownedAsyncJobManager;
|
|
8834
9204
|
if (ownedAsyncManager && this.#disposeAsyncJobManager) {
|
|
8835
|
-
const drained = await ownedAsyncManager.dispose({ timeoutMs: 3_000 });
|
|
9205
|
+
const drained = await awaitDisposeStep("async job manager", ownedAsyncManager.dispose({ timeoutMs: 3_000 }));
|
|
8836
9206
|
const deliveryState = ownedAsyncManager.getDeliveryState();
|
|
8837
9207
|
if (drained === false && deliveryState) {
|
|
8838
9208
|
logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
|
|
8839
9209
|
}
|
|
8840
|
-
if (
|
|
8841
|
-
|
|
9210
|
+
if (drained === false) {
|
|
9211
|
+
await awaitDisposeStep(
|
|
9212
|
+
"async job manager retained runners",
|
|
9213
|
+
ownedAsyncManager.awaitRetainedDisposalCompletion(),
|
|
9214
|
+
true,
|
|
9215
|
+
);
|
|
8842
9216
|
}
|
|
9217
|
+
if (AsyncJobManager.instance() === ownedAsyncManager) AsyncJobManager.setInstance(undefined);
|
|
8843
9218
|
}
|
|
8844
|
-
await this.#runToolSessionTransitionCleanups();
|
|
8845
|
-
await this.#runToolSessionCleanups();
|
|
9219
|
+
await awaitDisposeStep("tool session transition cleanups", this.#runToolSessionTransitionCleanups(), true);
|
|
9220
|
+
await awaitDisposeStep("tool session cleanups", this.#runToolSessionCleanups(), true);
|
|
8846
9221
|
// Only disconnect the MCP manager THIS session owns (top-level sessions that
|
|
8847
9222
|
// connected plugin-bundle MCP servers). Subagents and callers that merely
|
|
8848
9223
|
// observe the process-global manager must never tear down a manager they do
|
|
8849
9224
|
// not own. Mirrors the ownedAsyncJobManager rule above.
|
|
8850
9225
|
const ownedMcpManager = this.#ownedMcpManager;
|
|
8851
9226
|
if (ownedMcpManager) {
|
|
8852
|
-
await ownedMcpManager.releaseLeases();
|
|
9227
|
+
await awaitDisposeStep("MCP lease release", ownedMcpManager.releaseLeases());
|
|
8853
9228
|
if (MCPManager.instance() === ownedMcpManager) {
|
|
8854
9229
|
MCPManager.setInstance(undefined);
|
|
8855
9230
|
}
|
|
8856
9231
|
}
|
|
8857
|
-
await shutdownAllLspClients();
|
|
9232
|
+
await awaitDisposeStep("LSP shutdown", shutdownAllLspClients());
|
|
8858
9233
|
// F13: release only THIS session's browser tabs on dispose (kill:false → remote
|
|
8859
9234
|
// browsers disconnect, headless close gracefully). Scoped by the session id the
|
|
8860
9235
|
// browser tool tagged tabs with, so other live sessions' tabs are untouched.
|
|
@@ -8873,29 +9248,40 @@ export class AgentSession {
|
|
|
8873
9248
|
this.#unregisterMovePublicationListener = undefined;
|
|
8874
9249
|
this.#unregisterAfterMoveListener?.();
|
|
8875
9250
|
this.#unregisterAfterMoveListener = undefined;
|
|
8876
|
-
await releaseTabsForOwner(this.sessionManager.getSessionId())
|
|
8877
|
-
|
|
9251
|
+
await awaitDisposeStep("browser tab release", releaseTabsForOwner(this.sessionManager.getSessionId()), true);
|
|
9252
|
+
const pythonExecutionsSettled = await awaitDisposeStep(
|
|
9253
|
+
"Python execution preparation",
|
|
9254
|
+
this.#prepareEvalExecutionsForDispose(),
|
|
8878
9255
|
);
|
|
8879
|
-
const pythonExecutionsSettled = await this.#prepareEvalExecutionsForDispose();
|
|
8880
9256
|
if (!pythonExecutionsSettled) {
|
|
8881
9257
|
logger.warn(
|
|
8882
9258
|
"Detaching retained Python kernel ownership during dispose while Python execution is still active",
|
|
8883
9259
|
);
|
|
8884
9260
|
}
|
|
8885
|
-
await disposeKernelSessionsByOwner(this.#evalKernelOwnerId);
|
|
8886
|
-
await disposeVmContextsByOwner(this.#evalKernelOwnerId);
|
|
9261
|
+
await awaitDisposeStep("Python kernel sessions", disposeKernelSessionsByOwner(this.#evalKernelOwnerId));
|
|
9262
|
+
await awaitDisposeStep("VM contexts", disposeVmContextsByOwner(this.#evalKernelOwnerId));
|
|
8887
9263
|
this.#releasePowerAssertion();
|
|
8888
9264
|
// Disconnect the agent event listener BEFORE closing session resources so a late
|
|
8889
9265
|
// provider/tool message_end cannot append to the closing SessionManager.
|
|
8890
9266
|
this.#disconnectFromAgent();
|
|
8891
|
-
await this.memoryBackend.dispose();
|
|
8892
|
-
if (this.#workspaceTreeService)
|
|
8893
|
-
|
|
9267
|
+
await awaitDisposeStep("memory backend", this.memoryBackend.dispose(), true);
|
|
9268
|
+
if (this.#workspaceTreeService)
|
|
9269
|
+
await awaitDisposeStep("workspace tree", this.#workspaceTreeService.dispose(), true);
|
|
9270
|
+
if (this.#networkPrewarmService)
|
|
9271
|
+
await awaitDisposeStep("network prewarm", this.#networkPrewarmService.dispose(), true);
|
|
8894
9272
|
this.#modelRegistry.authStorage?.releaseCredentialScope(this.credentialSessionId);
|
|
8895
|
-
|
|
9273
|
+
let criticalDisposeError: unknown;
|
|
9274
|
+
try {
|
|
9275
|
+
await this.#closeSessionManagerForDispose();
|
|
9276
|
+
} catch (error) {
|
|
9277
|
+
criticalDisposeError = error;
|
|
9278
|
+
logger.warn("Session manager close failed during dispose; continuing independent cleanup", {
|
|
9279
|
+
error: String(error),
|
|
9280
|
+
});
|
|
9281
|
+
}
|
|
8896
9282
|
this.#closeAllProviderSessions("dispose");
|
|
8897
9283
|
const hindsightState = this.getHindsightSessionState();
|
|
8898
|
-
await hindsightState?.dispose();
|
|
9284
|
+
await awaitDisposeStep("hindsight state", hindsightState?.dispose() ?? Promise.resolve());
|
|
8899
9285
|
this.setHindsightSessionState(undefined);
|
|
8900
9286
|
if (this.#unsubscribeAppendOnly) {
|
|
8901
9287
|
this.#unsubscribeAppendOnly();
|
|
@@ -8903,7 +9289,35 @@ export class AgentSession {
|
|
|
8903
9289
|
}
|
|
8904
9290
|
this.#eventListeners = [];
|
|
8905
9291
|
this.#rebuildEventListenerSnapshot();
|
|
9292
|
+
if (criticalDisposeError !== undefined) {
|
|
9293
|
+
disposeFailures.push({ label: "session manager close", error: criticalDisposeError, critical: true });
|
|
9294
|
+
}
|
|
9295
|
+
if (disposeFailures.length > 0) {
|
|
9296
|
+
throw new AggregateError(
|
|
9297
|
+
disposeFailures.map(failure => failure.error),
|
|
9298
|
+
`Session disposal did not reach a terminal state: ${disposeFailures.map(failure => failure.label).join(", ")}`,
|
|
9299
|
+
);
|
|
9300
|
+
}
|
|
9301
|
+
}
|
|
9302
|
+
|
|
9303
|
+
async #closeSessionManagerForDispose(): Promise<void> {
|
|
9304
|
+
let retryDelayMs = 50;
|
|
9305
|
+
for (;;) {
|
|
9306
|
+
const outcome = await this.sessionManager.closeStrict();
|
|
9307
|
+
if (outcome.kind === "closed") {
|
|
9308
|
+
SessionManager.releaseProcessCwdOwnership(this.sessionManager);
|
|
9309
|
+
return;
|
|
9310
|
+
}
|
|
9311
|
+
if (outcome.kind === "close_unknown") {
|
|
9312
|
+
SessionManager.releaseProcessCwdOwnership(this.sessionManager);
|
|
9313
|
+
throw outcome.error;
|
|
9314
|
+
}
|
|
9315
|
+
logger.warn("Session manager close remains retryable during dispose", { error: outcome.error.message });
|
|
9316
|
+
await Bun.sleep(retryDelayMs);
|
|
9317
|
+
retryDelayMs = Math.min(retryDelayMs * 2, 1_000);
|
|
9318
|
+
}
|
|
8906
9319
|
}
|
|
9320
|
+
|
|
8907
9321
|
/**
|
|
8908
9322
|
* Strict writer close for ACP session delete. On the first attempt it flushes
|
|
8909
9323
|
* pending writes, then returns the certainty-aware close outcome so the caller
|
|
@@ -9079,6 +9493,13 @@ export class AgentSession {
|
|
|
9079
9493
|
await this.sessionManager.flush();
|
|
9080
9494
|
}
|
|
9081
9495
|
|
|
9496
|
+
/** Test seam: hold prompt admission while bypassing a pending selection fence. */
|
|
9497
|
+
runWithPromptAdmissionForTests(body: () => Promise<void>): Promise<void> {
|
|
9498
|
+
return this.#withSessionAdmission("prompt", async () => body(), undefined, undefined, {
|
|
9499
|
+
bypassSelectionFenceGeneration: this.#selectionFenceGeneration - 1,
|
|
9500
|
+
});
|
|
9501
|
+
}
|
|
9502
|
+
|
|
9082
9503
|
queueCoordinatorRuntimeStatePersistForTests(event: AgentSessionEvent, gate: Promise<void>): Promise<void> {
|
|
9083
9504
|
this.#agentEventAdmission.set(event, {
|
|
9084
9505
|
persistGeneration: this.#coordinatorPersistGeneration,
|
|
@@ -9383,23 +9804,77 @@ export class AgentSession {
|
|
|
9383
9804
|
this.#setGuardedAgentTools(activeTools);
|
|
9384
9805
|
}
|
|
9385
9806
|
|
|
9807
|
+
/**
|
|
9808
|
+
* Subscribe to folds of this session's foreground waits. Backed by the
|
|
9809
|
+
* session-owned manager (or the process-global instance for legacy setups)
|
|
9810
|
+
* so the SDK host can publish one `bash_folded` frame per fold.
|
|
9811
|
+
*/
|
|
9812
|
+
onJobFold(listener: (event: JobFoldEvent) => void): () => void {
|
|
9813
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
9814
|
+
if (!manager) return () => {};
|
|
9815
|
+
const ownerId = this.#agentId;
|
|
9816
|
+
return manager.onFold(event => {
|
|
9817
|
+
if (ownerId && manager.getJob(event.jobId)?.ownerId !== ownerId) return;
|
|
9818
|
+
listener(event);
|
|
9819
|
+
});
|
|
9820
|
+
}
|
|
9821
|
+
|
|
9386
9822
|
/**
|
|
9387
9823
|
* Ask the active managed foreground bash call to return as a background job.
|
|
9388
9824
|
* Returns false when no supported foreground tool is currently backgroundable.
|
|
9825
|
+
* `reason` names the trigger (`chord` for the keybinding, `sdk_control` for
|
|
9826
|
+
* the `bash.background` control) and is recorded on the folded job.
|
|
9827
|
+
*/
|
|
9828
|
+
async requestForegroundBashBackground(reason: FoldReason = "chord", adapter?: FoldAdapter): Promise<boolean> {
|
|
9829
|
+
return (await this.requestForegroundBashBackgroundOutcome(reason, adapter)).status === "folded";
|
|
9830
|
+
}
|
|
9831
|
+
|
|
9832
|
+
/**
|
|
9833
|
+
* Typed variant of {@link requestForegroundBashBackground} for the SDK
|
|
9834
|
+
* `bash.background` control (C52), which must distinguish "this fold moved
|
|
9835
|
+
* the wait" from "an earlier fold already moved it" from "nothing to fold".
|
|
9836
|
+
*
|
|
9837
|
+
* `already_backgrounded` is reported when no wait is foldable but this
|
|
9838
|
+
* session still has a running job that a fold (steer, chord, timer, or a
|
|
9839
|
+
* prior control call) moved out of the foreground: `metadata.foldReason` is
|
|
9840
|
+
* set only by folds, never by `async: true` starts, so it is the exact
|
|
9841
|
+
* evidence that the wait the client is asking about has already left.
|
|
9389
9842
|
*/
|
|
9390
|
-
async
|
|
9391
|
-
|
|
9843
|
+
async requestForegroundBashBackgroundOutcome(
|
|
9844
|
+
reason: FoldReason = "chord",
|
|
9845
|
+
adapter?: FoldAdapter,
|
|
9846
|
+
): Promise<ForegroundFoldOutcome> {
|
|
9847
|
+
if (!this.#foldCoordinator.hasFoldableParticipant()) {
|
|
9848
|
+
return this.#hasRunningFoldedJob() ? { status: "already_backgrounded" } : { status: "no_active_bash" };
|
|
9849
|
+
}
|
|
9392
9850
|
try {
|
|
9393
|
-
const result = await this.#foldCoordinator.requestFold();
|
|
9394
|
-
|
|
9851
|
+
const result = await this.#foldCoordinator.requestFold(adapter, reason);
|
|
9852
|
+
if (result.status === "folded") {
|
|
9853
|
+
if (reason === "steer") {
|
|
9854
|
+
this.emitNotice(
|
|
9855
|
+
"info",
|
|
9856
|
+
`Folded ${result.receipt.label} into background job ${result.receipt.jobId} so your message can be handled now.`,
|
|
9857
|
+
"fold",
|
|
9858
|
+
);
|
|
9859
|
+
}
|
|
9860
|
+
return { status: "folded", jobId: result.receipt.jobId };
|
|
9861
|
+
}
|
|
9862
|
+
if (result.status === "unavailable" && this.#hasRunningFoldedJob()) return { status: "already_backgrounded" };
|
|
9863
|
+
return { status: "not_foldable", reason: result.reason };
|
|
9395
9864
|
} catch (error) {
|
|
9396
9865
|
logger.warn("Foreground fold request failed", {
|
|
9397
9866
|
error: error instanceof Error ? error.message : String(error),
|
|
9398
9867
|
});
|
|
9399
|
-
return
|
|
9868
|
+
return { status: "not_foldable", reason: error instanceof Error ? error.message : String(error) };
|
|
9400
9869
|
}
|
|
9401
9870
|
}
|
|
9402
9871
|
|
|
9872
|
+
#hasRunningFoldedJob(): boolean {
|
|
9873
|
+
const manager = this.#ownedAsyncJobManager ?? AsyncJobManager.instance();
|
|
9874
|
+
if (!manager) return false;
|
|
9875
|
+
const ownerFilter = this.#agentId ? { ownerId: this.#agentId } : undefined;
|
|
9876
|
+
return manager.getRunningJobs(ownerFilter).some(job => job.metadata?.foldReason !== undefined);
|
|
9877
|
+
}
|
|
9403
9878
|
/**
|
|
9404
9879
|
* Get all configured tool names (built-in via --tools or default, plus custom tools).
|
|
9405
9880
|
*/
|
|
@@ -9690,7 +10165,10 @@ export class AgentSession {
|
|
|
9690
10165
|
// The lease keeps writers out for the whole execution, so this can
|
|
9691
10166
|
// only trip if a caller bypassed the lease; surface it rather than
|
|
9692
10167
|
// returning a result computed against a retired cwd.
|
|
9693
|
-
if (
|
|
10168
|
+
if (
|
|
10169
|
+
this.sessionManager.getCwdGeneration() !== admittedGeneration &&
|
|
10170
|
+
!this.sessionManager.consumeNestedCwdMove(admittedGeneration)
|
|
10171
|
+
) {
|
|
9694
10172
|
throw new Error(
|
|
9695
10173
|
"Session working directory changed while this tool executed; retry against the new cwd.",
|
|
9696
10174
|
);
|
|
@@ -10490,8 +10968,8 @@ export class AgentSession {
|
|
|
10490
10968
|
}
|
|
10491
10969
|
|
|
10492
10970
|
/** Current interrupt mode */
|
|
10493
|
-
get
|
|
10494
|
-
return this.agent.
|
|
10971
|
+
get toolInterruptPolicy(): "abort_tools" | "finish_tools" {
|
|
10972
|
+
return this.agent.getToolInterruptPolicy();
|
|
10495
10973
|
}
|
|
10496
10974
|
|
|
10497
10975
|
/** Current session file path, or undefined if sessions are disabled */
|
|
@@ -10578,7 +11056,7 @@ export class AgentSession {
|
|
|
10578
11056
|
thinking: this.#thinkingLevel ?? "off",
|
|
10579
11057
|
steeringMode: this.steeringMode,
|
|
10580
11058
|
followUpMode: this.followUpMode,
|
|
10581
|
-
|
|
11059
|
+
toolInterruptPolicy: this.toolInterruptPolicy,
|
|
10582
11060
|
};
|
|
10583
11061
|
}
|
|
10584
11062
|
|
|
@@ -11363,7 +11841,12 @@ export class AgentSession {
|
|
|
11363
11841
|
if (owner && !owner.released) throw this.#sessionAdmissionBusyError();
|
|
11364
11842
|
const expandPromptTemplates = options?.expandPromptTemplates ?? true;
|
|
11365
11843
|
|
|
11366
|
-
if (
|
|
11844
|
+
if (
|
|
11845
|
+
expandPromptTemplates &&
|
|
11846
|
+
text.startsWith("/") &&
|
|
11847
|
+
isNamespacedSkillSlashCommandName(text.slice(1)) &&
|
|
11848
|
+
!options?.images?.length
|
|
11849
|
+
) {
|
|
11367
11850
|
const skillCommands = new Map(this.skills.map(skill => [getSkillSlashCommandName(skill), skill]));
|
|
11368
11851
|
const invocations = parseSkillInvocations(text, skillCommands);
|
|
11369
11852
|
if (invocations.length === 1) {
|
|
@@ -11449,7 +11932,15 @@ export class AgentSession {
|
|
|
11449
11932
|
|
|
11450
11933
|
// If streaming, queue via steer() or followUp() based on option.
|
|
11451
11934
|
// Abort unwind is awaited above so a successor is not busy against leftover in-flight.
|
|
11452
|
-
|
|
11935
|
+
// An explicit streamingBehavior submitted while another prompt still holds
|
|
11936
|
+
// or awaits session admission (agent idle, session not yet live) is queued
|
|
11937
|
+
// as well: entering admission behind that prompt would only get this
|
|
11938
|
+
// message cancelled by the predecessor's preflight reset and lost.
|
|
11939
|
+
const promptAdmissionContended =
|
|
11940
|
+
options?.streamingBehavior !== undefined &&
|
|
11941
|
+
(this.#activeSessionAdmission?.kind === "prompt" ||
|
|
11942
|
+
this.#sessionAdmissionQueue.some(entry => entry.kind === "prompt"));
|
|
11943
|
+
if ((this.#isLiveTurnBusy() || promptAdmissionContended) && !waitedForAbortUnwind) {
|
|
11453
11944
|
if (!options?.streamingBehavior) {
|
|
11454
11945
|
throw new AgentBusyError();
|
|
11455
11946
|
}
|
|
@@ -11459,7 +11950,10 @@ export class AgentSession {
|
|
|
11459
11950
|
claimsGenuineUserIntent,
|
|
11460
11951
|
});
|
|
11461
11952
|
} else {
|
|
11462
|
-
await this.#queueSteer(expandedText, options?.images, {
|
|
11953
|
+
await this.#queueSteer(expandedText, options?.images, {
|
|
11954
|
+
claimsGenuineUserIntent,
|
|
11955
|
+
forceOneAtATime: options.steerQueuePolicy === "sequential",
|
|
11956
|
+
});
|
|
11463
11957
|
}
|
|
11464
11958
|
if (workflowIntentDiff) {
|
|
11465
11959
|
this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
|
|
@@ -11472,6 +11966,35 @@ export class AgentSession {
|
|
|
11472
11966
|
await this.#withSessionAdmission(
|
|
11473
11967
|
"prompt",
|
|
11474
11968
|
async admission => {
|
|
11969
|
+
// A prompt that carries an explicit streamingBehavior and was admitted
|
|
11970
|
+
// behind another prompt (that prompt reset the preflight generation
|
|
11971
|
+
// while this one waited in the admission queue) is a busy submission:
|
|
11972
|
+
// queue it against the now-live turn instead of cancelling it.
|
|
11973
|
+
if (
|
|
11974
|
+
options?.streamingBehavior !== undefined &&
|
|
11975
|
+
this.#isPromptPreflightCancelled(admissionGeneration, admissionSignal) &&
|
|
11976
|
+
options.preflightSignal?.aborted !== true &&
|
|
11977
|
+
!this.#abortUnwind
|
|
11978
|
+
) {
|
|
11979
|
+
admission.release();
|
|
11980
|
+
if (options.streamingBehavior === "followUp") {
|
|
11981
|
+
await this.#queueFollowUp(expandedText, options?.images, {
|
|
11982
|
+
forceOneAtATime: options.followUpQueuePolicy === "sequential",
|
|
11983
|
+
claimsGenuineUserIntent,
|
|
11984
|
+
});
|
|
11985
|
+
} else {
|
|
11986
|
+
await this.#queueSteer(expandedText, options?.images, {
|
|
11987
|
+
claimsGenuineUserIntent,
|
|
11988
|
+
forceOneAtATime: options.steerQueuePolicy === "sequential",
|
|
11989
|
+
});
|
|
11990
|
+
}
|
|
11991
|
+
if (workflowIntentDiff) {
|
|
11992
|
+
this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
|
|
11993
|
+
}
|
|
11994
|
+
if (options?.onPreflightAcceptCommit) await options.onPreflightAcceptCommit();
|
|
11995
|
+
else options?.onPreflightAccepted?.();
|
|
11996
|
+
return;
|
|
11997
|
+
}
|
|
11475
11998
|
this.#throwIfPromptPreflightCancelled(admissionGeneration, admissionSignal);
|
|
11476
11999
|
if (workflowIntentDiff) {
|
|
11477
12000
|
this.sessionManager.appendCustomEntry(WORKFLOW_INTENT_DIFF_CUSTOM_TYPE, workflowIntentDiff);
|
|
@@ -11646,6 +12169,7 @@ export class AgentSession {
|
|
|
11646
12169
|
| "streamingBehavior"
|
|
11647
12170
|
| "toolChoice"
|
|
11648
12171
|
| "followUpQueuePolicy"
|
|
12172
|
+
| "steerQueuePolicy"
|
|
11649
12173
|
| "onPreflightAccepted"
|
|
11650
12174
|
| "onPreflightAcceptCommit"
|
|
11651
12175
|
| "preflightSignal"
|
|
@@ -11681,6 +12205,7 @@ export class AgentSession {
|
|
|
11681
12205
|
await this.sendCustomMessage(message, {
|
|
11682
12206
|
deliverAs: options.streamingBehavior,
|
|
11683
12207
|
followUpQueuePolicy: options.followUpQueuePolicy,
|
|
12208
|
+
steerQueuePolicy: options.steerQueuePolicy,
|
|
11684
12209
|
});
|
|
11685
12210
|
return;
|
|
11686
12211
|
}
|
|
@@ -11761,6 +12286,7 @@ export class AgentSession {
|
|
|
11761
12286
|
skipPostPromptRecoveryWait?: boolean;
|
|
11762
12287
|
predecessorAgentEndHold?: symbol;
|
|
11763
12288
|
admissionLease?: SessionAdmissionLease;
|
|
12289
|
+
skipInitialSteeringPoll?: boolean;
|
|
11764
12290
|
onRunAccepted?: (handle: AttemptRunHandle) => void;
|
|
11765
12291
|
onFinalPreflight?: (context: { hasPendingNextTurnMessages: boolean }) => Promise<boolean>;
|
|
11766
12292
|
resetRetryReplaySafety?: boolean;
|
|
@@ -12091,6 +12617,7 @@ export class AgentSession {
|
|
|
12091
12617
|
|
|
12092
12618
|
const agentPromptOptions = {
|
|
12093
12619
|
...(options?.toolChoice ? { toolChoice: options.toolChoice } : undefined),
|
|
12620
|
+
...(options?.skipInitialSteeringPoll ? { skipInitialSteeringPoll: true } : {}),
|
|
12094
12621
|
...this.#managedFallbackPromptOptions(),
|
|
12095
12622
|
...(options?.sdkRunToken ? { sdkRunToken: options.sdkRunToken } : {}),
|
|
12096
12623
|
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
@@ -12261,8 +12788,8 @@ export class AgentSession {
|
|
|
12261
12788
|
this.setFollowUpMode(mode);
|
|
12262
12789
|
return true;
|
|
12263
12790
|
}
|
|
12264
|
-
if (kind === "
|
|
12265
|
-
this.
|
|
12791
|
+
if (kind === "tool_interrupt" && (mode === "abort_tools" || mode === "finish_tools")) {
|
|
12792
|
+
this.setToolInterruptPolicy(mode);
|
|
12266
12793
|
return true;
|
|
12267
12794
|
}
|
|
12268
12795
|
return false;
|
|
@@ -12438,6 +12965,7 @@ export class AgentSession {
|
|
|
12438
12965
|
onPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
|
|
12439
12966
|
external?: boolean;
|
|
12440
12967
|
sdkRunToken?: string;
|
|
12968
|
+
forceOneAtATime?: boolean;
|
|
12441
12969
|
},
|
|
12442
12970
|
): Promise<void> {
|
|
12443
12971
|
this.#assertNoHandoffTransition();
|
|
@@ -12446,6 +12974,26 @@ export class AgentSession {
|
|
|
12446
12974
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
12447
12975
|
if (images && images.length > 0) content.push(...images);
|
|
12448
12976
|
const message = { role: "user" as const, content, attribution: "user" as const, timestamp: Date.now() };
|
|
12977
|
+
// Enqueue-time admission: the Agent only accepts a steer into a live,
|
|
12978
|
+
// non-aborted run. Anything else (idle, unwinding after agent_end, another
|
|
12979
|
+
// prompt still in preflight, aborting) has no run to steer, so it is routed
|
|
12980
|
+
// as a sequential follow-up owned by the next turn instead of being parked
|
|
12981
|
+
// in a queue nobody owns.
|
|
12982
|
+
const admission = this.agent.steer(message, options?.forceOneAtATime ? { forceOneAtATime: true } : undefined);
|
|
12983
|
+
if (!admission.admitted) {
|
|
12984
|
+
await this.#queueFollowUp(text, images, {
|
|
12985
|
+
forceOneAtATime: options?.forceOneAtATime,
|
|
12986
|
+
claimsGenuineUserIntent: options?.claimsGenuineUserIntent,
|
|
12987
|
+
onPromoted: options?.onPromoted,
|
|
12988
|
+
sdkRunToken: options?.sdkRunToken,
|
|
12989
|
+
onQueued: message => {
|
|
12990
|
+
if (this.#abortUnwind && !options?.forceOneAtATime) this.#abortUnwindSteerFallbacks.push(message);
|
|
12991
|
+
},
|
|
12992
|
+
});
|
|
12993
|
+
this.#scheduleNonAdmittedSteerContinuation();
|
|
12994
|
+
return;
|
|
12995
|
+
}
|
|
12996
|
+
if (options?.forceOneAtATime) this.#sequentialSteerMessages.add(message);
|
|
12449
12997
|
this.#steeringMessages.push(this.#createQueuedDisplayEntry(displayText, undefined, message));
|
|
12450
12998
|
if (options?.external) {
|
|
12451
12999
|
this.#externalSteerMessages.add(message);
|
|
@@ -12457,22 +13005,42 @@ export class AgentSession {
|
|
|
12457
13005
|
const epoch = this.#claimDeepInterviewUserIntent();
|
|
12458
13006
|
this.#deepInterviewGenuineUserMessageEpochs.set(message, epoch);
|
|
12459
13007
|
}
|
|
12460
|
-
|
|
12461
|
-
|
|
12462
|
-
|
|
12463
|
-
|
|
12464
|
-
|
|
12465
|
-
|
|
12466
|
-
|
|
12467
|
-
|
|
12468
|
-
|
|
12469
|
-
|
|
12470
|
-
|
|
12471
|
-
|
|
12472
|
-
|
|
12473
|
-
|
|
12474
|
-
|
|
12475
|
-
|
|
13008
|
+
}
|
|
13009
|
+
|
|
13010
|
+
/**
|
|
13011
|
+
* A steer that was not admitted into a live run was requeued as a follow-up.
|
|
13012
|
+
* `#queueFollowUp` already schedules delivery when the session is idle; this
|
|
13013
|
+
* covers the unwind window where the session still reports busy only because
|
|
13014
|
+
* a finished prompt is unwinding (deferred agent_end / post-prompt work), so
|
|
13015
|
+
* the follow-up gate refuses while no loop actually owns the queue. A live
|
|
13016
|
+
* loop, a prompt still in preflight, or a drained queue makes the scheduled
|
|
13017
|
+
* continue a no-op.
|
|
13018
|
+
*/
|
|
13019
|
+
#scheduleNonAdmittedSteerContinuation(): void {
|
|
13020
|
+
// `#queueFollowUp` already schedules delivery when the session is idle; this
|
|
13021
|
+
// only covers the window where that gate refuses (a finished prompt is
|
|
13022
|
+
// unwinding) while no loop owns the queue. The resumable-tail requirement
|
|
13023
|
+
// is kept here on purpose: a message queued against a session that never
|
|
13024
|
+
// ran a turn waits for the next explicit prompt, as it always has.
|
|
13025
|
+
if (this.#cancelAndSubmitInProgress || !this.#canAutoContinueForSteer()) return;
|
|
13026
|
+
if (this.#canAutoContinueForFollowUp()) return;
|
|
13027
|
+
this.#scheduleQueuedDelivery();
|
|
13028
|
+
}
|
|
13029
|
+
|
|
13030
|
+
/**
|
|
13031
|
+
* Schedule delivery of whatever is queued. Used by every path that enqueues
|
|
13032
|
+
* without going through `#queueFollowUp` (custom-message steer/follow-up
|
|
13033
|
+
* routing, disown re-routing, held-steering fallback, a run's terminal): the
|
|
13034
|
+
* run that would have polled the queue is gone, so nothing else owns it.
|
|
13035
|
+
*/
|
|
13036
|
+
#scheduleQueuedDelivery(delayMs?: number): void {
|
|
13037
|
+
if (this.#cancelAndSubmitInProgress || !this.#canDeliverQueuedMessages()) return;
|
|
13038
|
+
this.#scheduleAgentContinue({
|
|
13039
|
+
...(delayMs === undefined ? {} : { delayMs }),
|
|
13040
|
+
shouldContinue: () => this.#canDeliverQueuedMessages() && this.agent.hasQueuedMessages(),
|
|
13041
|
+
rescheduleOnBusy: true,
|
|
13042
|
+
continueQueuedOnly: true,
|
|
13043
|
+
});
|
|
12476
13044
|
}
|
|
12477
13045
|
|
|
12478
13046
|
/**
|
|
@@ -12486,6 +13054,7 @@ export class AgentSession {
|
|
|
12486
13054
|
claimsGenuineUserIntent?: boolean;
|
|
12487
13055
|
onPromoted?: (promotion: { startsOwnRun?: boolean; removed?: boolean }) => void;
|
|
12488
13056
|
sdkRunToken?: string;
|
|
13057
|
+
onQueued?: (message: AgentMessage) => void;
|
|
12489
13058
|
},
|
|
12490
13059
|
): Promise<QueuedFollowUpOwner> {
|
|
12491
13060
|
this.#assertNoHandoffTransition();
|
|
@@ -12495,6 +13064,7 @@ export class AgentSession {
|
|
|
12495
13064
|
const content: (TextContent | ImageContent)[] = [{ type: "text", text }];
|
|
12496
13065
|
if (images && images.length > 0) content.push(...images);
|
|
12497
13066
|
const message = { role: "user" as const, content, attribution: "user" as const, timestamp: Date.now() };
|
|
13067
|
+
options?.onQueued?.(message);
|
|
12498
13068
|
// Display entry carries the message identity so positional editing can never
|
|
12499
13069
|
// misaddress a deferred SDK follow-up held outside the Agent live queue.
|
|
12500
13070
|
const displayEntry = this.#createQueuedDisplayEntry(displayText, undefined, message);
|
|
@@ -12578,6 +13148,7 @@ export class AgentSession {
|
|
|
12578
13148
|
* Gate for idle-path follow-up auto-continue. See `#queueFollowUp` for rationale.
|
|
12579
13149
|
*/
|
|
12580
13150
|
#canAutoContinueForFollowUp(): boolean {
|
|
13151
|
+
if (this.#abortUnwind) return false;
|
|
12581
13152
|
if (this.isStreaming) return false;
|
|
12582
13153
|
if (this.isCompacting) return false;
|
|
12583
13154
|
if (this.isBashRunning) return false;
|
|
@@ -12603,6 +13174,24 @@ export class AgentSession {
|
|
|
12603
13174
|
}
|
|
12604
13175
|
}
|
|
12605
13176
|
|
|
13177
|
+
/**
|
|
13178
|
+
* Gate for delivering queued messages through `continueQueuedMessages()`.
|
|
13179
|
+
* Unlike the steer/follow-up auto-continue gates this does NOT require an
|
|
13180
|
+
* assistant tail: a queued-only continuation delivers the queued turn without
|
|
13181
|
+
* replaying the tail, so a tool-result tail (paused fold, aborted turn) is
|
|
13182
|
+
* still deliverable. A live agent loop, a retry, or an in-flight
|
|
13183
|
+
* compaction/bash/eval owns the session and defers delivery.
|
|
13184
|
+
*/
|
|
13185
|
+
#canDeliverQueuedMessages(): boolean {
|
|
13186
|
+
if (this.#abortUnwind) return false;
|
|
13187
|
+
if (this.agent.state.isStreaming) return false;
|
|
13188
|
+
if (this.isRetrying) return false;
|
|
13189
|
+
if (this.isCompacting) return false;
|
|
13190
|
+
if (this.isBashRunning) return false;
|
|
13191
|
+
if (this.isEvalRunning) return false;
|
|
13192
|
+
return true;
|
|
13193
|
+
}
|
|
13194
|
+
|
|
12606
13195
|
/**
|
|
12607
13196
|
* Gate for idle / winding-down steer auto-continue. Unlike the follow-up gate
|
|
12608
13197
|
* this checks `agent.state.isStreaming` (a live agent loop) rather than the
|
|
@@ -12611,6 +13200,7 @@ export class AgentSession {
|
|
|
12611
13200
|
* false here because it polls the steering queue itself.
|
|
12612
13201
|
*/
|
|
12613
13202
|
#canAutoContinueForSteer(): boolean {
|
|
13203
|
+
if (this.#abortUnwind) return false;
|
|
12614
13204
|
if (this.agent.state.isStreaming) return false;
|
|
12615
13205
|
if (this.isRetrying) return false;
|
|
12616
13206
|
const messages = this.agent.state.messages;
|
|
@@ -12618,6 +13208,13 @@ export class AgentSession {
|
|
|
12618
13208
|
return last?.role === "assistant" || last?.role === "bashExecution" || last?.role === "pythonExecution";
|
|
12619
13209
|
}
|
|
12620
13210
|
|
|
13211
|
+
#finalizeAbortUnwindSteerBatch(): void {
|
|
13212
|
+
if (this.agent.getSteeringMode() === "all" && this.#abortUnwindSteerFallbacks.length > 1) {
|
|
13213
|
+
this.agent.markFollowUpBatch(this.#abortUnwindSteerFallbacks);
|
|
13214
|
+
}
|
|
13215
|
+
this.#abortUnwindSteerFallbacks = [];
|
|
13216
|
+
}
|
|
13217
|
+
|
|
12621
13218
|
queueDeferredMessage(message: CustomMessage): void {
|
|
12622
13219
|
this.#queueHiddenNextTurnMessage(message, true, "turn");
|
|
12623
13220
|
}
|
|
@@ -12906,6 +13503,7 @@ export class AgentSession {
|
|
|
12906
13503
|
triggerTurn?: boolean;
|
|
12907
13504
|
deliverAs?: "steer" | "followUp" | "nextTurn";
|
|
12908
13505
|
followUpQueuePolicy?: "respect-mode" | "sequential";
|
|
13506
|
+
steerQueuePolicy?: "respect-mode" | "sequential";
|
|
12909
13507
|
/** INTERNAL trusted option: origin of a hidden next-turn message —
|
|
12910
13508
|
* "turn" (produced by the current turn's continuation machinery,
|
|
12911
13509
|
* purged by a terminal abort) or "external" (background producers,
|
|
@@ -12931,6 +13529,53 @@ export class AgentSession {
|
|
|
12931
13529
|
const epoch = preclaimedUserIntentEpoch ?? this.#claimDeepInterviewUserIntent();
|
|
12932
13530
|
this.#deepInterviewGenuineUserMessageEpochs.set(appMessage, epoch);
|
|
12933
13531
|
}
|
|
13532
|
+
// An explicit steer request routes the same way at every session phase: the
|
|
13533
|
+
// Agent admits it only into a live run, and anything else becomes a
|
|
13534
|
+
// sequential follow-up the next turn delivers. Routing this on the session's
|
|
13535
|
+
// streaming flag instead would strand it during the post-prompt unwind (no
|
|
13536
|
+
// loop owns the queue) and append it without a turn while idle.
|
|
13537
|
+
if (options?.deliverAs === "steer") {
|
|
13538
|
+
// A handoff transition owns the session; a background/custom trigger
|
|
13539
|
+
// (cron, monitor, skill) must not steer against the outgoing turn.
|
|
13540
|
+
this.#assertNoHandoffTransition();
|
|
13541
|
+
const sequential = options.steerQueuePolicy === "sequential" ? { forceOneAtATime: true } : undefined;
|
|
13542
|
+
if (this.agent.steer(appMessage, sequential).admitted) {
|
|
13543
|
+
if (sequential) this.#sequentialSteerMessages.add(appMessage);
|
|
13544
|
+
this.#bindCustomDisplayEntry(appMessage, "steer");
|
|
13545
|
+
} else {
|
|
13546
|
+
// No live run to steer, so this becomes next-turn work. Provenance —
|
|
13547
|
+
// never the caller's `triggerTurn` delivery preference — decides what a
|
|
13548
|
+
// terminal abort's retained fence does with it: genuine user input and
|
|
13549
|
+
// an independent external producer are root requests of their own and
|
|
13550
|
+
// survive; a continuation produced by the aborted turn's own machinery
|
|
13551
|
+
// (preview/plan reminder) is dropped with its removal disposition
|
|
13552
|
+
// rather than leaking into an unrelated later turn.
|
|
13553
|
+
const independentOfTurn = appMessage.attribution === "user" || options.origin === "external";
|
|
13554
|
+
if (this.#isTurnContinuationBlocked() && !independentOfTurn) {
|
|
13555
|
+
const displayTag = readPendingDisplayTag(appMessage.details);
|
|
13556
|
+
if (displayTag !== undefined) {
|
|
13557
|
+
this.#steeringMessages = this.#steeringMessages.filter(entry => entry.tag !== displayTag);
|
|
13558
|
+
this.#followUpMessages = this.#followUpMessages.filter(entry => entry.tag !== displayTag);
|
|
13559
|
+
}
|
|
13560
|
+
this.#settleDeliveredOwnedRegistrations([appMessage]);
|
|
13561
|
+
return;
|
|
13562
|
+
}
|
|
13563
|
+
this.agent.followUp(appMessage, sequential);
|
|
13564
|
+
if (this.#abortUnwind && !sequential) this.#abortUnwindSteerFallbacks.push(appMessage);
|
|
13565
|
+
// The chip now describes follow-up work: keep its mode and identity
|
|
13566
|
+
// aligned with where the executable message actually landed.
|
|
13567
|
+
this.#bindCustomDisplayEntry(appMessage, "followUp");
|
|
13568
|
+
if (independentOfTurn) {
|
|
13569
|
+
this.#externalFollowUps.add(appMessage);
|
|
13570
|
+
// Past a retained terminal fence this message is a NEW root: record
|
|
13571
|
+
// the requirement on the message itself so it survives a deferred
|
|
13572
|
+
// delivery and disappears with the message if it is removed.
|
|
13573
|
+
if (this.#isTurnContinuationBlocked()) this.#freshRootLineageRequests.add(appMessage);
|
|
13574
|
+
}
|
|
13575
|
+
this.#scheduleQueuedDelivery();
|
|
13576
|
+
}
|
|
13577
|
+
return;
|
|
13578
|
+
}
|
|
12934
13579
|
if (this.isStreaming) {
|
|
12935
13580
|
// A handoff transition owns the session; a background/custom trigger (cron,
|
|
12936
13581
|
// monitor, skill) must not steer/follow-up/queue against the outgoing turn
|
|
@@ -12941,14 +13586,15 @@ export class AgentSession {
|
|
|
12941
13586
|
return;
|
|
12942
13587
|
}
|
|
12943
13588
|
|
|
12944
|
-
|
|
12945
|
-
|
|
12946
|
-
|
|
12947
|
-
|
|
12948
|
-
|
|
12949
|
-
|
|
12950
|
-
|
|
12951
|
-
|
|
13589
|
+
this.agent.followUp(
|
|
13590
|
+
appMessage,
|
|
13591
|
+
options?.followUpQueuePolicy === "sequential" ? { forceOneAtATime: true } : undefined,
|
|
13592
|
+
);
|
|
13593
|
+
this.#bindCustomDisplayEntry(appMessage, "followUp");
|
|
13594
|
+
// The session can report streaming while no agent loop owns the queue
|
|
13595
|
+
// (post-prompt unwind): without a scheduled delivery a cron/monitor
|
|
13596
|
+
// notification would sit queued until unrelated activity.
|
|
13597
|
+
if (!this.agent.state.isStreaming) this.#scheduleQueuedDelivery();
|
|
12952
13598
|
return;
|
|
12953
13599
|
}
|
|
12954
13600
|
|
|
@@ -13469,8 +14115,37 @@ export class AgentSession {
|
|
|
13469
14115
|
const fromIndex = queue.findIndex(entry => entry.sequence === sequence);
|
|
13470
14116
|
if (fromIndex === -1) return false;
|
|
13471
14117
|
const toIndex = direction === "up" ? fromIndex - 1 : fromIndex + 1;
|
|
14118
|
+
if (toIndex < 0 || toIndex >= queue.length) return false;
|
|
14119
|
+
// Identity-based reorder, for the same reason removal is: a display index is
|
|
14120
|
+
// NOT an Agent queue index. Hidden `display:false` custom messages occupy
|
|
14121
|
+
// executable slots without a chip, so mapping the visible position straight
|
|
14122
|
+
// onto the Agent queue reorders an unrelated message while the UI moves the
|
|
14123
|
+
// chip. Resolve both endpoints from the bound messages instead.
|
|
14124
|
+
const fromMessage = queue[fromIndex]?.message;
|
|
14125
|
+
const toMessage = queue[toIndex]?.message;
|
|
14126
|
+
const executable = mode === "steer" ? this.agent.snapshotSteering() : this.agent.snapshotFollowUp();
|
|
14127
|
+
const fromDeferred = fromMessage !== undefined && this.#deferredSdkFollowUps.includes(fromMessage);
|
|
14128
|
+
const toDeferred = toMessage !== undefined && this.#deferredSdkFollowUps.includes(toMessage);
|
|
14129
|
+
// A deferred SDK follow-up is held OUTSIDE the Agent queue, so its display
|
|
14130
|
+
// row cannot be reordered against live messages: the two rows live in
|
|
14131
|
+
// different backing stores and no single index describes both. Reorder
|
|
14132
|
+
// within the deferred store when both rows are deferred; refuse a
|
|
14133
|
+
// cross-store move rather than silently moving an unrelated message.
|
|
14134
|
+
if (fromDeferred || toDeferred) {
|
|
14135
|
+
if (!fromDeferred || !toDeferred || fromMessage === undefined || toMessage === undefined) return false;
|
|
14136
|
+
const deferredFrom = this.#deferredSdkFollowUps.indexOf(fromMessage);
|
|
14137
|
+
const deferredTo = this.#deferredSdkFollowUps.indexOf(toMessage);
|
|
14138
|
+
if (deferredFrom === -1 || deferredTo === -1) return false;
|
|
14139
|
+
const [moved] = this.#deferredSdkFollowUps.splice(deferredFrom, 1);
|
|
14140
|
+
if (moved === undefined) return false;
|
|
14141
|
+
this.#deferredSdkFollowUps.splice(deferredTo, 0, moved);
|
|
14142
|
+
return this.#moveQueuedDisplayEntry(queue, fromIndex, toIndex);
|
|
14143
|
+
}
|
|
14144
|
+
const agentFrom = fromMessage === undefined ? fromIndex : executable.indexOf(fromMessage);
|
|
14145
|
+
const agentTo = toMessage === undefined ? toIndex : executable.indexOf(toMessage);
|
|
14146
|
+
if (agentFrom === -1 || agentTo === -1) return false;
|
|
13472
14147
|
const agentMoved =
|
|
13473
|
-
mode === "steer" ? this.agent.moveSteer(
|
|
14148
|
+
mode === "steer" ? this.agent.moveSteer(agentFrom, agentTo) : this.agent.moveFollowUp(agentFrom, agentTo);
|
|
13474
14149
|
if (!agentMoved) return false;
|
|
13475
14150
|
return this.#moveQueuedDisplayEntry(queue, fromIndex, toIndex);
|
|
13476
14151
|
}
|
|
@@ -13830,6 +14505,7 @@ export class AgentSession {
|
|
|
13830
14505
|
| "internal";
|
|
13831
14506
|
silent?: boolean;
|
|
13832
14507
|
sdkRunToken?: string;
|
|
14508
|
+
preserveCompaction?: boolean;
|
|
13833
14509
|
}): void {
|
|
13834
14510
|
const abortGoalState = this.getGoalModeState();
|
|
13835
14511
|
this.#suppressNextGoalReminderAfterAbortGoalId =
|
|
@@ -13846,7 +14522,8 @@ export class AgentSession {
|
|
|
13846
14522
|
this.#promptPreflightAbortController.abort();
|
|
13847
14523
|
this.#promptPreflightAbortController = new AbortController();
|
|
13848
14524
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
13849
|
-
this
|
|
14525
|
+
if (options?.preserveCompaction) this.#autoCompactionAbortController?.abort();
|
|
14526
|
+
else this.abortCompaction();
|
|
13850
14527
|
this.abortHandoff();
|
|
13851
14528
|
this.abortBash();
|
|
13852
14529
|
this.abortEval();
|
|
@@ -13902,6 +14579,7 @@ export class AgentSession {
|
|
|
13902
14579
|
| "tool_abort"
|
|
13903
14580
|
| "internal";
|
|
13904
14581
|
silent?: boolean;
|
|
14582
|
+
preserveCompaction?: boolean;
|
|
13905
14583
|
}): Promise<AbortOutcome> {
|
|
13906
14584
|
// Advance the admission epoch SYNCHRONOUSLY, before any await and before the
|
|
13907
14585
|
// shared-unwind branch. A second abort that piggybacks on the first unwind
|
|
@@ -13919,6 +14597,7 @@ export class AgentSession {
|
|
|
13919
14597
|
this.#promptPreflightAbortController.abort();
|
|
13920
14598
|
this.#promptPreflightAbortController = new AbortController();
|
|
13921
14599
|
this.#drainTerminalOwnedYieldEntries();
|
|
14600
|
+
if (options?.preserveCompaction !== true) this.abortCompaction();
|
|
13922
14601
|
const overlappingPostPromptDrain = this.#cancelPostPromptTasks();
|
|
13923
14602
|
// Abort visibility is per-request: a later real abort must not inherit an
|
|
13924
14603
|
// earlier silent abort's suppression and swallow the user-visible notice.
|
|
@@ -13956,6 +14635,16 @@ export class AgentSession {
|
|
|
13956
14635
|
this.#abortUnwind = unwind.promise;
|
|
13957
14636
|
try {
|
|
13958
14637
|
this.#abortOptions(options);
|
|
14638
|
+
// Decide what the run's unconsumed steering becomes BEFORE it ends: a
|
|
14639
|
+
// user interrupt (Esc) hands it to a fresh turn; every other cause
|
|
14640
|
+
// (teardown, session switch, compaction, handoff, tool abort) drops it.
|
|
14641
|
+
// A cancel-and-submit owns its snapshot and re-queues explicitly.
|
|
14642
|
+
if (this.#disownedSteeringDisposition === undefined) {
|
|
14643
|
+
this.#disownedSteeringDisposition =
|
|
14644
|
+
(options?.cause ?? "internal") === "user_interrupt" && !this.#cancelAndSubmitInProgress
|
|
14645
|
+
? { kind: "rearm" }
|
|
14646
|
+
: { kind: "drop" };
|
|
14647
|
+
}
|
|
13959
14648
|
this.#drainTerminalOwnedYieldEntries();
|
|
13960
14649
|
const postPromptDrain = this.#cancelPostPromptTasks();
|
|
13961
14650
|
const managedLogicalRunId =
|
|
@@ -13984,15 +14673,18 @@ export class AgentSession {
|
|
|
13984
14673
|
this.agent.requestRunTerminal(managedLogicalRunId, { stopReason: "cancelled" });
|
|
13985
14674
|
this.#flushPendingBackgroundExchanges();
|
|
13986
14675
|
await this.#awaitAbortedTurnTerminal();
|
|
14676
|
+
this.#finalizeAbortUnwindSteerBatch();
|
|
14677
|
+
// Steering disowned by the aborted run was re-routed as follow-ups
|
|
14678
|
+
// on its agent_end; a user interrupt resumes into them promptly.
|
|
13987
14679
|
if (
|
|
13988
14680
|
!this.#cancelAndSubmitInProgress &&
|
|
13989
14681
|
(options?.cause ?? "internal") === "user_interrupt" &&
|
|
13990
|
-
this.agent.
|
|
14682
|
+
this.agent.hasQueuedMessages()
|
|
13991
14683
|
) {
|
|
13992
14684
|
this.#scheduleAgentContinue({
|
|
13993
14685
|
delayMs: 1,
|
|
13994
14686
|
generation: this.#promptGeneration,
|
|
13995
|
-
shouldContinue: () => this.agent.
|
|
14687
|
+
shouldContinue: () => this.agent.hasQueuedMessages(),
|
|
13996
14688
|
rescheduleOnBusy: true,
|
|
13997
14689
|
continueQueuedOnly: true,
|
|
13998
14690
|
});
|
|
@@ -14005,6 +14697,10 @@ export class AgentSession {
|
|
|
14005
14697
|
if (this.#toolChoiceQueue.hasInFlight) this.#toolChoiceQueue.reject("aborted");
|
|
14006
14698
|
}
|
|
14007
14699
|
} finally {
|
|
14700
|
+
// No run was live to end (or its terminal already settled): the
|
|
14701
|
+
// disposition must not leak into an unrelated later run's exit.
|
|
14702
|
+
this.#disownedSteeringDisposition = undefined;
|
|
14703
|
+
this.#abortUnwindSteerFallbacks = [];
|
|
14008
14704
|
this.#abortUnwind = undefined;
|
|
14009
14705
|
this.#abortForceRecoveryStarted = false;
|
|
14010
14706
|
unwind.resolve();
|
|
@@ -14024,6 +14720,7 @@ export class AgentSession {
|
|
|
14024
14720
|
| "tool_abort"
|
|
14025
14721
|
| "internal";
|
|
14026
14722
|
silent?: boolean;
|
|
14723
|
+
preserveCompaction?: boolean;
|
|
14027
14724
|
}): Promise<void> {
|
|
14028
14725
|
const outcome = await this.#abortWithOutcome(options);
|
|
14029
14726
|
if (outcome.kind === "error") throw outcome.cause;
|
|
@@ -14144,10 +14841,6 @@ export class AgentSession {
|
|
|
14144
14841
|
}
|
|
14145
14842
|
> {
|
|
14146
14843
|
let registeredScope: { scopeId: string; abortedAttemptEpoch: number; lineageIdHash: string } | undefined;
|
|
14147
|
-
// The purge snapshot is consumed per-admission inside the terminal
|
|
14148
|
-
// block; the rearm below (reachable only when a scope was registered,
|
|
14149
|
-
// i.e. only after the purge ran) reads the same value.
|
|
14150
|
-
let abortSteeringSnapshot: number | undefined;
|
|
14151
14844
|
if (options.terminal) {
|
|
14152
14845
|
// The terminal scope must be registered for the ABORTED turn's
|
|
14153
14846
|
// lineage, never a successor's. A new root turn advances the attempt
|
|
@@ -14202,10 +14895,10 @@ export class AgentSession {
|
|
|
14202
14895
|
};
|
|
14203
14896
|
}
|
|
14204
14897
|
// Terminal abort blocks STEERING continuations of the aborted turn:
|
|
14205
|
-
//
|
|
14206
|
-
//
|
|
14207
|
-
//
|
|
14208
|
-
//
|
|
14898
|
+
// only a client steer admitted AFTER this abort's admission snapshot
|
|
14899
|
+
// survives; everything else was a continuation of the attempt being
|
|
14900
|
+
// terminated (review thread P2). The follow-up queue is preserved —
|
|
14901
|
+
// owned-completion resumes must still deliver.
|
|
14209
14902
|
const turnSnapshotKey = `${lineageIdHash}:${this.#promptGeneration}`;
|
|
14210
14903
|
const queuedTurnSnapshots = this.#terminalAbortSteeringSnapshots.get(turnSnapshotKey);
|
|
14211
14904
|
const snapshotIndex =
|
|
@@ -14215,39 +14908,31 @@ export class AgentSession {
|
|
|
14215
14908
|
const consumedSnapshot =
|
|
14216
14909
|
queuedTurnSnapshots && snapshotIndex >= 0 ? queuedTurnSnapshots.splice(snapshotIndex, 1)[0] : undefined;
|
|
14217
14910
|
if (consumedSnapshot) this.#terminalAbortSteeringSnapshotKeys.delete(consumedSnapshot.token);
|
|
14218
|
-
|
|
14911
|
+
const snapshotSeq: number = consumedSnapshot?.seq ?? this.#steeringAdmissionSeq;
|
|
14219
14912
|
if (queuedTurnSnapshots?.length === 0) {
|
|
14220
14913
|
this.#terminalAbortSteeringSnapshots.delete(turnSnapshotKey);
|
|
14221
14914
|
}
|
|
14222
|
-
const
|
|
14223
|
-
|
|
14224
|
-
|
|
14225
|
-
//
|
|
14226
|
-
//
|
|
14227
|
-
|
|
14228
|
-
|
|
14229
|
-
|
|
14230
|
-
|
|
14231
|
-
|
|
14232
|
-
|
|
14233
|
-
|
|
14234
|
-
|
|
14235
|
-
|
|
14236
|
-
|
|
14237
|
-
|
|
14238
|
-
|
|
14239
|
-
|
|
14240
|
-
|
|
14241
|
-
|
|
14242
|
-
|
|
14243
|
-
const steeringAfterPurge = new Set(this.agent.snapshotSteering());
|
|
14244
|
-
const keptSteeringDisplays: QueuedDisplayEntry[] = [];
|
|
14245
|
-
for (let displayIndex = 0; displayIndex < steeringBeforePurge.length; displayIndex++) {
|
|
14246
|
-
if (steeringAfterPurge.has(steeringBeforePurge[displayIndex]!)) {
|
|
14247
|
-
keptSteeringDisplays.push(this.#steeringMessages[displayIndex]!);
|
|
14248
|
-
}
|
|
14249
|
-
}
|
|
14250
|
-
this.#steeringMessages = keptSteeringDisplays;
|
|
14915
|
+
const preserveSteering = (message: AgentMessage): boolean =>
|
|
14916
|
+
this.#externalSteerMessages.has(message) &&
|
|
14917
|
+
(this.#externalSteerAdmissionSeq.get(message) ?? 0) > snapshotSeq;
|
|
14918
|
+
// The live run disowns its steering on agent_end; the disposition
|
|
14919
|
+
// installed here is what settles it (drop pre-snapshot, re-route
|
|
14920
|
+
// post-snapshot external steers as a fresh turn). Nothing is
|
|
14921
|
+
// removed from the Agent queue by the session itself.
|
|
14922
|
+
// Overlapping terminal aborts of the same run may settle out of order:
|
|
14923
|
+
// the disposition only ever NARROWS. The highest admission snapshot
|
|
14924
|
+
// wins, and a preserve decision must satisfy every terminal abort that
|
|
14925
|
+
// registered, so a later stricter snapshot cannot be widened by an
|
|
14926
|
+
// older one settling afterwards.
|
|
14927
|
+
const previous = this.#disownedSteeringDisposition;
|
|
14928
|
+
this.#disownedSteeringDisposition =
|
|
14929
|
+
previous?.kind === "terminal"
|
|
14930
|
+
? {
|
|
14931
|
+
kind: "terminal",
|
|
14932
|
+
snapshotSeq: Math.max(previous.snapshotSeq, snapshotSeq),
|
|
14933
|
+
preserve: (message: AgentMessage) => previous.preserve(message) && preserveSteering(message),
|
|
14934
|
+
}
|
|
14935
|
+
: { kind: "terminal", preserve: preserveSteering, snapshotSeq };
|
|
14251
14936
|
|
|
14252
14937
|
// Remove ORDINARY follow-ups queued for the aborted turn: the
|
|
14253
14938
|
// aborted loop exits without polling them, so a later unrelated
|
|
@@ -14302,6 +14987,7 @@ export class AgentSession {
|
|
|
14302
14987
|
}
|
|
14303
14988
|
const aborted = this.#runCancellationDomains.abort(handle);
|
|
14304
14989
|
if (!aborted.ok) {
|
|
14990
|
+
this.#disownedSteeringDisposition = undefined;
|
|
14305
14991
|
if (aborted.reason === "quarantined") {
|
|
14306
14992
|
return {
|
|
14307
14993
|
...(await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: 0 })),
|
|
@@ -14318,6 +15004,9 @@ export class AgentSession {
|
|
|
14318
15004
|
if (handle === this.agent.activeResourceRunId) this.agent.abort();
|
|
14319
15005
|
const proof = await this.agent.resourceLedger.waitForSettlement(handle, { graceMs: options.graceMs });
|
|
14320
15006
|
if (proof.status === "unfenced") this.agent.resourceLedger.quarantine(handle);
|
|
15007
|
+
// The run's agent_end (if any) consumed the disposition; a settled or
|
|
15008
|
+
// already-ended run must not leave it for an unrelated later exit.
|
|
15009
|
+
this.#disownedSteeringDisposition = undefined;
|
|
14321
15010
|
// Rearm surviving owned-completion follow-ups once the abort has
|
|
14322
15011
|
// settled: the aborted loop exits before polling the follow-up queue,
|
|
14323
15012
|
// so preserving the envelopes alone leaves them stranded until an
|
|
@@ -14357,19 +15046,10 @@ export class AgentSession {
|
|
|
14357
15046
|
message =>
|
|
14358
15047
|
ownedCompletionResumeAction(message as never) === "fresh" || this.#externalFollowUps.has(message),
|
|
14359
15048
|
);
|
|
14360
|
-
// A
|
|
14361
|
-
//
|
|
14362
|
-
//
|
|
14363
|
-
|
|
14364
|
-
const hasPreservedSteering = (): boolean =>
|
|
14365
|
-
this.agent
|
|
14366
|
-
.snapshotQueues()
|
|
14367
|
-
.steering.some(
|
|
14368
|
-
message =>
|
|
14369
|
-
this.#externalSteerMessages.has(message) &&
|
|
14370
|
-
(this.#externalSteerAdmissionSeq.get(message) ?? 0) > (abortSteeringSnapshot ?? -1),
|
|
14371
|
-
);
|
|
14372
|
-
if (hasPreservedFollowUp() || hasPreservedSteering()) {
|
|
15049
|
+
// A post-snapshot client steer preserved by the run's disown settlement
|
|
15050
|
+
// is already an external follow-up here (an independent root-turn
|
|
15051
|
+
// request), so one predicate covers both (review thread P1).
|
|
15052
|
+
if (hasPreservedFollowUp()) {
|
|
14373
15053
|
// ANY preserved follow-up or steer — an owned-completion resume,
|
|
14374
15054
|
// an independently requested external follow-up, or a
|
|
14375
15055
|
// post-snapshot client steer — needs a fresh lineage before its
|
|
@@ -14384,7 +15064,7 @@ export class AgentSession {
|
|
|
14384
15064
|
this.#scheduleAgentContinue({
|
|
14385
15065
|
delayMs: 1,
|
|
14386
15066
|
generation: this.#promptGeneration,
|
|
14387
|
-
shouldContinue: () => hasPreservedFollowUp()
|
|
15067
|
+
shouldContinue: () => hasPreservedFollowUp(),
|
|
14388
15068
|
// The rearmed continuation must consume the queued steer inside
|
|
14389
15069
|
// the run acceptance (continueQueuedMessages) rather than
|
|
14390
15070
|
// dequeue it later via getSteeringMessages: a terminal abort
|
|
@@ -14472,9 +15152,14 @@ export class AgentSession {
|
|
|
14472
15152
|
];
|
|
14473
15153
|
};
|
|
14474
15154
|
try {
|
|
15155
|
+
// The aborted run disowns its steering on agent_end; hold it here
|
|
15156
|
+
// (rather than re-routing) because this call owns the snapshot and
|
|
15157
|
+
// re-queues it against the NEW turn below.
|
|
15158
|
+
this.#disownedSteeringDisposition = { kind: "held" };
|
|
14475
15159
|
const outcome = this.#cancelAndSubmitAbortOutcomeProviderForTests
|
|
14476
15160
|
? await this.#cancelAndSubmitAbortOutcomeProviderForTests()
|
|
14477
15161
|
: await this.#abortWithOutcome({ cause: "user_interrupt", timeoutMs: 5_000 });
|
|
15162
|
+
this.#disownedSteeringDisposition = undefined;
|
|
14478
15163
|
if (outcome.kind !== "settled") {
|
|
14479
15164
|
restore();
|
|
14480
15165
|
if (outcome.kind === "error") {
|
|
@@ -14516,18 +15201,29 @@ export class AgentSession {
|
|
|
14516
15201
|
followUp: [...heldFollowUp, ...current.followUp],
|
|
14517
15202
|
});
|
|
14518
15203
|
};
|
|
15204
|
+
// Steers of the aborted turn stay STEERS of the new turn: they are
|
|
15205
|
+
// restored once the new run is accepted (below), so the Agent
|
|
15206
|
+
// admits them into a live run instead of re-labelling them as
|
|
15207
|
+
// follow-ups drained turn after turn. Follow-ups keep their queue.
|
|
15208
|
+
const heldSteering = selected ? [] : queueSnapshot.steering;
|
|
15209
|
+
const heldSteeringDisplays = selected ? [] : steeringDisplaySnapshot;
|
|
14519
15210
|
this.agent.restoreQueues({
|
|
14520
15211
|
steering: [...queuedDuringWindow.steering],
|
|
14521
15212
|
followUp: selected
|
|
14522
15213
|
? [...queuedDuringWindow.followUp]
|
|
14523
|
-
: [...queueSnapshot.
|
|
15214
|
+
: [...queueSnapshot.followUp, ...queuedDuringWindow.followUp],
|
|
14524
15215
|
});
|
|
14525
15216
|
this.#steeringMessages = steeringDisplaysDuringWindow;
|
|
14526
|
-
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
|
|
14530
|
-
|
|
15217
|
+
// With a selected entry the held (unselected) messages are re-queued
|
|
15218
|
+
// as follow-ups by restoreHeldQueue once the new run is accepted;
|
|
15219
|
+
// mirror them in the display in the same order.
|
|
15220
|
+
this.#followUpMessages = selected
|
|
15221
|
+
? [
|
|
15222
|
+
...steeringDisplaySnapshot.filter(entry => entry !== selected.display),
|
|
15223
|
+
...followUpDisplaySnapshot.filter(entry => entry !== selected.display),
|
|
15224
|
+
...followUpDisplaysDuringWindow,
|
|
15225
|
+
]
|
|
15226
|
+
: [...followUpDisplaySnapshot, ...followUpDisplaysDuringWindow];
|
|
14531
15227
|
const message = selectedMessage ?? {
|
|
14532
15228
|
role: "user" as const,
|
|
14533
15229
|
content: [{ type: "text" as const, text }],
|
|
@@ -14556,8 +15252,19 @@ export class AgentSession {
|
|
|
14556
15252
|
admissionLease: admission,
|
|
14557
15253
|
resetRetryReplaySafety: true,
|
|
14558
15254
|
sdkRunToken: selectedSdkRunToken,
|
|
15255
|
+
skipInitialSteeringPoll: heldSteering.length > 0,
|
|
14559
15256
|
onRunAccepted: () => {
|
|
14560
15257
|
runAccepted = true;
|
|
15258
|
+
if (heldSteering.length > 0) {
|
|
15259
|
+
// Re-admit the aborted turn's steers into the replacement run the
|
|
15260
|
+
// moment it is accepted, with the run's INITIAL poll skipped: the
|
|
15261
|
+
// replacement prompt is answered by the opening model call and the
|
|
15262
|
+
// steers are consumed together at its first turn boundary. Doing
|
|
15263
|
+
// this synchronously at acceptance (rather than from an event
|
|
15264
|
+
// listener) keeps the ordering deterministic.
|
|
15265
|
+
this.agent.restoreSteering(heldSteering);
|
|
15266
|
+
this.#steeringMessages = [...heldSteeringDisplays, ...this.#steeringMessages];
|
|
15267
|
+
}
|
|
14561
15268
|
if (selected) this.#fireQueuedPromotionHooks([message], { startsOwnRun: true });
|
|
14562
15269
|
if (selected) {
|
|
14563
15270
|
this.#steeringMessages = this.#steeringMessages.filter(entry => entry !== selected.display);
|
|
@@ -14586,6 +15293,10 @@ export class AgentSession {
|
|
|
14586
15293
|
} finally {
|
|
14587
15294
|
this.#cancelAndSubmitInProgress = false;
|
|
14588
15295
|
this.#cancelAndSubmitPendingNextTurnDrained = false;
|
|
15296
|
+
// Queue work that arrived (or was re-routed) during the atomic window was
|
|
15297
|
+
// deliberately not scheduled while the flag was set; now that it is
|
|
15298
|
+
// cleared, nothing else owns delivery.
|
|
15299
|
+
if (this.agent.hasQueuedMessages()) this.#scheduleQueuedDelivery(1);
|
|
14589
15300
|
}
|
|
14590
15301
|
}
|
|
14591
15302
|
|
|
@@ -15745,6 +16456,10 @@ export class AgentSession {
|
|
|
15745
16456
|
onBeforeMutation?: () => void;
|
|
15746
16457
|
/** Run inside the selection admission after the durable mutation. */
|
|
15747
16458
|
onAfterMutation?: () => void;
|
|
16459
|
+
/** Test seam: pause after idle drain but before mutation admission. */
|
|
16460
|
+
onBeforeMutationAdmissionForTests?: () => Promise<void>;
|
|
16461
|
+
/** Test seam: pause after mutation admission activates, before its closed-fence recheck. */
|
|
16462
|
+
onAfterMutationAdmissionReadyForTests?: () => Promise<void>;
|
|
15748
16463
|
},
|
|
15749
16464
|
): Promise<DefaultModelSelectionResult> {
|
|
15750
16465
|
// Reserve the causal selection fence synchronously, before credential
|
|
@@ -15760,6 +16475,7 @@ export class AgentSession {
|
|
|
15760
16475
|
throw new Error("Default model selection cannot inherit a thinking level");
|
|
15761
16476
|
}
|
|
15762
16477
|
const expectedSessionId = this.sessionId;
|
|
16478
|
+
const selectionTransaction = Symbol("default-model-selection");
|
|
15763
16479
|
const priorSelectionFence = this.#selectionFenceTail;
|
|
15764
16480
|
const selectionFence = Promise.withResolvers<void>();
|
|
15765
16481
|
this.#selectionFenceGeneration += 1;
|
|
@@ -15795,9 +16511,11 @@ export class AgentSession {
|
|
|
15795
16511
|
},
|
|
15796
16512
|
undefined,
|
|
15797
16513
|
undefined,
|
|
15798
|
-
{ allowDuringClosing: true },
|
|
16514
|
+
{ allowDuringClosing: true, selectionTransaction },
|
|
15799
16515
|
);
|
|
16516
|
+
this.#selectionAwaitingMutationTransaction = selectionTransaction;
|
|
15800
16517
|
await this.waitForIdle(selectionFenceGeneration);
|
|
16518
|
+
await options?.onBeforeMutationAdmissionForTests?.();
|
|
15801
16519
|
return await this.#withSessionAdmission(
|
|
15802
16520
|
"selection",
|
|
15803
16521
|
async () => {
|
|
@@ -15879,9 +16597,16 @@ export class AgentSession {
|
|
|
15879
16597
|
},
|
|
15880
16598
|
undefined,
|
|
15881
16599
|
undefined,
|
|
15882
|
-
{
|
|
16600
|
+
{
|
|
16601
|
+
allowDuringClosing: true,
|
|
16602
|
+
closedSelectionTransaction: selectionTransaction,
|
|
16603
|
+
selectionTransaction,
|
|
16604
|
+
onAfterReadyForTests: options?.onAfterMutationAdmissionReadyForTests,
|
|
16605
|
+
},
|
|
15883
16606
|
);
|
|
15884
16607
|
} finally {
|
|
16608
|
+
if (this.#selectionAwaitingMutationTransaction === selectionTransaction)
|
|
16609
|
+
this.#selectionAwaitingMutationTransaction = undefined;
|
|
15885
16610
|
selectionFence.resolve();
|
|
15886
16611
|
this.#pendingSelectionFences -= 1;
|
|
15887
16612
|
if (this.#oldestPendingSelectionFenceGeneration === selectionFenceGeneration) {
|
|
@@ -16547,13 +17272,13 @@ export class AgentSession {
|
|
|
16547
17272
|
}
|
|
16548
17273
|
|
|
16549
17274
|
/**
|
|
16550
|
-
* Set interrupt
|
|
17275
|
+
* Set the tool interrupt policy (whether a steer aborts in-flight tools).
|
|
16551
17276
|
* Saves to settings.
|
|
16552
17277
|
*/
|
|
16553
|
-
|
|
17278
|
+
setToolInterruptPolicy(policy: "abort_tools" | "finish_tools"): void {
|
|
16554
17279
|
this.#assertDurableSettingsWritable();
|
|
16555
|
-
this.agent.
|
|
16556
|
-
this.settings.set("
|
|
17280
|
+
this.agent.setToolInterruptPolicy(policy);
|
|
17281
|
+
this.settings.set("toolInterruptPolicy", policy);
|
|
16557
17282
|
}
|
|
16558
17283
|
|
|
16559
17284
|
// =========================================================================
|
|
@@ -16940,22 +17665,35 @@ export class AgentSession {
|
|
|
16940
17665
|
* @param options Optional callbacks for completion/error handling
|
|
16941
17666
|
*/
|
|
16942
17667
|
async compact(customInstructions?: string, options?: CompactOptions): Promise<CompactionResult> {
|
|
17668
|
+
this.#assertSessionAdmissionOpen();
|
|
16943
17669
|
// Serialize with every other session-identity transition via the shared lease
|
|
16944
17670
|
// (bidirectional mutual exclusion with handoff/new/switch/branch/clear/fork/
|
|
16945
17671
|
// navigateTree). Released in the outer finally below.
|
|
16946
17672
|
this.#beginSessionTransition("compact");
|
|
17673
|
+
const completion = Promise.withResolvers<void>();
|
|
17674
|
+
this.#compactionCompletion = completion.promise;
|
|
16947
17675
|
try {
|
|
16948
17676
|
if (this.#compactionAbortController) {
|
|
16949
17677
|
throw new Error("Compaction already in progress");
|
|
16950
17678
|
}
|
|
16951
|
-
this.#disconnectFromAgent();
|
|
16952
|
-
await this.abort();
|
|
16953
17679
|
const compactionAbortController = new AbortController();
|
|
16954
17680
|
this.#compactionAbortController = compactionAbortController;
|
|
16955
|
-
|
|
16956
|
-
const compactionStateSnapshot = await this.#compactionStateSnapshot({ trackWorkflowRecoveryProgress: true });
|
|
16957
|
-
|
|
17681
|
+
this.#disconnectFromAgent();
|
|
16958
17682
|
try {
|
|
17683
|
+
await this.abort({ cause: "compaction", preserveCompaction: true });
|
|
17684
|
+
} catch (error) {
|
|
17685
|
+
this.#compactionAbortController = undefined;
|
|
17686
|
+
throw error;
|
|
17687
|
+
}
|
|
17688
|
+
await this.#waitForAutoCompactionCompletions();
|
|
17689
|
+
try {
|
|
17690
|
+
if (compactionAbortController.signal.aborted) {
|
|
17691
|
+
throw new CompactionCancelledError();
|
|
17692
|
+
}
|
|
17693
|
+
// Take this invocation's state snapshot for the summarizer context.
|
|
17694
|
+
const compactionStateSnapshot = await this.#compactionStateSnapshot({
|
|
17695
|
+
trackWorkflowRecoveryProgress: true,
|
|
17696
|
+
});
|
|
16959
17697
|
if (!this.model) {
|
|
16960
17698
|
throw new Error("No model selected");
|
|
16961
17699
|
}
|
|
@@ -17092,9 +17830,11 @@ export class AgentSession {
|
|
|
17092
17830
|
if (this.#compactionAbortController === compactionAbortController) {
|
|
17093
17831
|
this.#compactionAbortController = undefined;
|
|
17094
17832
|
}
|
|
17095
|
-
this.#reconnectToAgent();
|
|
17833
|
+
if (!this.#isDisposed && !this.#sessionAdmissionClosing) this.#reconnectToAgent();
|
|
17096
17834
|
}
|
|
17097
17835
|
} finally {
|
|
17836
|
+
completion.resolve();
|
|
17837
|
+
if (this.#compactionCompletion === completion.promise) this.#compactionCompletion = undefined;
|
|
17098
17838
|
this.#endSessionTransition();
|
|
17099
17839
|
}
|
|
17100
17840
|
}
|
|
@@ -17156,9 +17896,21 @@ export class AgentSession {
|
|
|
17156
17896
|
}
|
|
17157
17897
|
}
|
|
17158
17898
|
|
|
17899
|
+
/**
|
|
17900
|
+
* Wait for every automatic compaction producer currently unwinding. An auto
|
|
17901
|
+
* compaction owns the session history through its post-append rewrite and
|
|
17902
|
+
* `session_compact` delivery, so cancelling its signal is not sufficient to
|
|
17903
|
+
* make that work safe to overlap with a manual compaction.
|
|
17904
|
+
*/
|
|
17905
|
+
async #waitForAutoCompactionCompletions(): Promise<void> {
|
|
17906
|
+
while (this.#autoCompactionCompletions.size > 0) {
|
|
17907
|
+
await Promise.allSettled([...this.#autoCompactionCompletions]);
|
|
17908
|
+
}
|
|
17909
|
+
}
|
|
17910
|
+
|
|
17159
17911
|
/** Trigger idle compaction through the auto-compaction flow (with UI events). */
|
|
17160
17912
|
async runIdleCompaction(): Promise<void> {
|
|
17161
|
-
if (this.isStreaming || this.isCompacting) return;
|
|
17913
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing || this.isStreaming || this.isCompacting) return;
|
|
17162
17914
|
// Do not start idle compaction while a handoff transition owns the session.
|
|
17163
17915
|
if (this.isGeneratingHandoff || this.#handoffTransitionActive) return;
|
|
17164
17916
|
await this.#runAutoCompaction("idle", false, true);
|
|
@@ -19006,7 +19758,27 @@ export class AgentSession {
|
|
|
19006
19758
|
/**
|
|
19007
19759
|
* Internal: Run auto-compaction with events.
|
|
19008
19760
|
*/
|
|
19009
|
-
|
|
19761
|
+
#runAutoCompaction(
|
|
19762
|
+
reason: "overflow" | "threshold" | "idle",
|
|
19763
|
+
willRetry: boolean,
|
|
19764
|
+
deferred = false,
|
|
19765
|
+
options?: {
|
|
19766
|
+
continueAfterMaintenance?: boolean;
|
|
19767
|
+
deferHandoffMaintenance?: boolean;
|
|
19768
|
+
force?: boolean;
|
|
19769
|
+
signal?: AbortSignal;
|
|
19770
|
+
beforeTerminalOverflowNoop?: () => void;
|
|
19771
|
+
resourceRunId?: string;
|
|
19772
|
+
},
|
|
19773
|
+
): Promise<AutoCompactionTerminalStatus> {
|
|
19774
|
+
if (this.#isDisposed || this.#sessionAdmissionClosing) return Promise.resolve({ kind: "skipped" });
|
|
19775
|
+
const completion = this.#runAutoCompactionImpl(reason, willRetry, deferred, options);
|
|
19776
|
+
this.#autoCompactionCompletions.add(completion);
|
|
19777
|
+
void completion.finally(() => this.#autoCompactionCompletions.delete(completion)).catch(() => {});
|
|
19778
|
+
return completion;
|
|
19779
|
+
}
|
|
19780
|
+
|
|
19781
|
+
async #runAutoCompactionImpl(
|
|
19010
19782
|
reason: "overflow" | "threshold" | "idle",
|
|
19011
19783
|
willRetry: boolean,
|
|
19012
19784
|
deferred = false,
|
|
@@ -21224,6 +21996,7 @@ export class AgentSession {
|
|
|
21224
21996
|
toolChoice?: ToolChoice;
|
|
21225
21997
|
sdkRunToken?: string;
|
|
21226
21998
|
fallbackManaged?: boolean;
|
|
21999
|
+
skipInitialSteeringPoll?: boolean;
|
|
21227
22000
|
onRunAccepted?: (handle: AttemptRunHandle) => void;
|
|
21228
22001
|
},
|
|
21229
22002
|
predecessorAgentEndHold?: symbol,
|