@gajae-code/coding-agent 0.15.4 → 0.15.5
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 +20 -1
- package/dist/types/cli/update-cli.d.ts +25 -5
- package/dist/types/config/autorouting-tier-map.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/cli/session-cli.d.ts +42 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
- package/dist/types/tools/path-utils.d.ts +5 -6
- package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
- package/package.json +7 -7
- package/src/cli/update-cli.ts +102 -1
- package/src/commands/sdk.ts +5 -0
- package/src/config/autorouting-tier-map.ts +1 -0
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +10 -4
- package/src/coordinator-mcp/policy.ts +1 -1
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/launch-worktree.ts +44 -7
- package/src/gjc-runtime/ralplan-runtime.ts +14 -1
- package/src/gjc-runtime/tmux-sessions.ts +145 -48
- package/src/internal-urls/docs-index.generated.ts +7 -7
- package/src/modes/prompt-action-autocomplete.ts +26 -47
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/kind-aware-reconciliation.ts +10 -2
- package/src/sdk/bus/prompt-reconciliation.ts +5 -2
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/cli/session-cli.ts +149 -4
- package/src/sdk/host/host.ts +7 -0
- package/src/sdk/host/session-runtime.test.ts +821 -22
- package/src/sdk/host/session-runtime.ts +669 -67
- package/src/sdk/router/session-router.ts +69 -15
- package/src/session/agent-session.ts +232 -15
- package/src/session/internal/managed-session-storage.ts +64 -26
- package/src/session/session-manager.ts +26 -8
- package/src/tools/bash.ts +14 -0
- package/src/tools/path-utils.ts +77 -29
- package/src/tools/tmux-self-injection-guard.ts +359 -0
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
isSessionAuthorityEligible,
|
|
9
9
|
type SessionIndex,
|
|
10
10
|
} from "../broker/session-index";
|
|
11
|
-
import { lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
11
|
+
import { cancellableSleep, lifecycleRequestTimeoutMs } from "../broker/startup-budget";
|
|
12
12
|
import { SdkClient, SdkClientError, type SdkDispatchContext, type SdkDispatchHandler } from "../client/client";
|
|
13
13
|
import { readSdkBrokerDiscovery, readSdkSessionEndpoint, type SdkSessionEndpoint } from "../client/discovery";
|
|
14
14
|
import {
|
|
@@ -295,6 +295,8 @@ type AttachedSession = {
|
|
|
295
295
|
readonly capability: SessionAttachment;
|
|
296
296
|
readonly notificationSubscription: NotificationSubscription;
|
|
297
297
|
notificationCancelled: boolean;
|
|
298
|
+
/** Consecutive refused notification publications; reset by the next delivered frame. */
|
|
299
|
+
notificationFailures: number;
|
|
298
300
|
readonly notificationCursor: { generation: number; seq: number };
|
|
299
301
|
published: boolean;
|
|
300
302
|
initializingPublication: boolean;
|
|
@@ -342,6 +344,9 @@ const STARTUP_ATTACH_BUDGET_MS = 5_000;
|
|
|
342
344
|
const STARTUP_STRAGGLER_LOG_LIMIT = 8;
|
|
343
345
|
const ATTACH_CONNECT_TIMEOUT_MS = 10_000;
|
|
344
346
|
const NOTIFICATION_WORK_TIMEOUT_MS = 5_000;
|
|
347
|
+
// Core shutdown waits only for Router reconciliation and client close; provider
|
|
348
|
+
// notification work is bounded separately by NOTIFICATION_WORK_TIMEOUT_MS.
|
|
349
|
+
const ROUTER_SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
345
350
|
/**
|
|
346
351
|
* Idle liveness-sweep cadence (#4689). When the session index is unchanged and
|
|
347
352
|
* no adoption is pending, the 2s reconcile tick is only a change-stamp check;
|
|
@@ -352,6 +357,17 @@ const NOTIFICATION_WORK_TIMEOUT_MS = 5_000;
|
|
|
352
357
|
*/
|
|
353
358
|
const SESSION_ROUTER_IDLE_SWEEP_MS = 30_000;
|
|
354
359
|
const NOTIFICATION_WORK_TIMEOUT = Symbol("notification_work_timeout");
|
|
360
|
+
/**
|
|
361
|
+
* Consecutive local notification-dispatch failures tolerated before the
|
|
362
|
+
* subscription is conceded. A provider that refuses ONE publication is not a
|
|
363
|
+
* dead subscription: it has already settled that publication as rejected on its
|
|
364
|
+
* own side, and the next frame normally succeeds. Cancelling on the first
|
|
365
|
+
* rejection latched `notificationCancelled` for the life of the
|
|
366
|
+
* `AttachedSession`, which only a new attachment clears -- a still-running
|
|
367
|
+
* session never gets one, so its mirroring stayed dead until the session was
|
|
368
|
+
* restarted. Mirrors {@link DELIVERY_ATTEMPT_LIMIT} on the ordered path.
|
|
369
|
+
*/
|
|
370
|
+
const NOTIFICATION_LOCAL_FAILURE_LIMIT = 5;
|
|
355
371
|
/**
|
|
356
372
|
* Client-message types the native session server authorizes with the
|
|
357
373
|
* per-session endpoint token (`tokens_match` in crates/gjc-sdk server.rs).
|
|
@@ -552,10 +568,16 @@ export class SessionRouter {
|
|
|
552
568
|
// initial replay delivered. Past that budget the pass continues in the
|
|
553
569
|
// background instead of owning the caller's deadline.
|
|
554
570
|
const initialPass = this.#serialReconcile(runEpoch, false, true);
|
|
571
|
+
// Cancelled as soon as the race settles. Promise.race leaves the loser pending,
|
|
572
|
+
// and a pending Bun.sleep keeps the event loop alive, so a startup that settled
|
|
573
|
+
// in milliseconds still owed the rest of the budget before the process could exit.
|
|
574
|
+
const startupCutoff = new AbortController();
|
|
555
575
|
const settled = await Promise.race([
|
|
556
576
|
initialPass.then(() => true),
|
|
557
|
-
|
|
558
|
-
|
|
577
|
+
cancellableSleep(this.#deps.startupAttachBudgetMs ?? STARTUP_ATTACH_BUDGET_MS, startupCutoff.signal).then(
|
|
578
|
+
() => false,
|
|
579
|
+
),
|
|
580
|
+
]).finally(() => startupCutoff.abort());
|
|
559
581
|
if (!settled) {
|
|
560
582
|
// Name the sessions the budget was spent on. Without this the lapse is
|
|
561
583
|
// silent and the only symptom is a slow startup somewhere else, which
|
|
@@ -586,6 +608,10 @@ export class SessionRouter {
|
|
|
586
608
|
() => this.#schedule(this.#serialReconcile(runEpoch, true)),
|
|
587
609
|
2_000,
|
|
588
610
|
);
|
|
611
|
+
// Reconciliation is background upkeep and must not be the reason a host process
|
|
612
|
+
// stays alive. stop() still clears it on the normal path; this only keeps a
|
|
613
|
+
// caller that never stops the Router from hanging at exit.
|
|
614
|
+
timer.unref?.();
|
|
589
615
|
this.#stopTimer = () => (this.#deps.clearInterval ?? clearInterval)(timer);
|
|
590
616
|
} catch (error) {
|
|
591
617
|
if (this.#running(runEpoch)) await this.stop();
|
|
@@ -695,13 +721,18 @@ export class SessionRouter {
|
|
|
695
721
|
// Provider notification work is detached and bounded independently; core
|
|
696
722
|
// shutdown waits only for Router reconciliation and client close.
|
|
697
723
|
const pending = Promise.allSettled([this.#reconcileTail, ...shutdownTasks]);
|
|
724
|
+
// Cancelled once the race settles, so a completed shutdown does not hold the
|
|
725
|
+
// event loop open for the remainder of the timeout.
|
|
726
|
+
const shutdownCutoff = new AbortController();
|
|
698
727
|
const outcome = await Promise.race([
|
|
699
728
|
pending.then(results => ({ kind: "settled" as const, results })),
|
|
700
|
-
|
|
701
|
-
|
|
729
|
+
cancellableSleep(ROUTER_SHUTDOWN_TIMEOUT_MS, shutdownCutoff.signal).then(() => ({
|
|
730
|
+
kind: "timeout" as const,
|
|
731
|
+
})),
|
|
732
|
+
]).finally(() => shutdownCutoff.abort());
|
|
702
733
|
if (outcome.kind === "timeout") {
|
|
703
734
|
logger.warn(
|
|
704
|
-
|
|
735
|
+
`SessionRouter shutdown exceeded ${ROUTER_SHUTDOWN_TIMEOUT_MS}ms; authority is revoked and cleanup continues in background.`,
|
|
705
736
|
);
|
|
706
737
|
return;
|
|
707
738
|
}
|
|
@@ -1548,6 +1579,7 @@ export class SessionRouter {
|
|
|
1548
1579
|
capability,
|
|
1549
1580
|
notificationSubscription,
|
|
1550
1581
|
notificationCancelled: false,
|
|
1582
|
+
notificationFailures: 0,
|
|
1551
1583
|
notificationCursor,
|
|
1552
1584
|
published: false,
|
|
1553
1585
|
publication,
|
|
@@ -1780,10 +1812,20 @@ export class SessionRouter {
|
|
|
1780
1812
|
}
|
|
1781
1813
|
|
|
1782
1814
|
async #boundedNotificationWork<T>(work: () => Promise<T> | T): Promise<T | typeof NOTIFICATION_WORK_TIMEOUT> {
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1815
|
+
// The dominant cost of a one-shot `gjc sdk` command: notification work that
|
|
1816
|
+
// finished in milliseconds left this timeout pending, and a pending Bun.sleep
|
|
1817
|
+
// keeps the event loop alive, so the process wrote its response and then idled
|
|
1818
|
+
// for the full budget before exiting.
|
|
1819
|
+
const cutoff = new AbortController();
|
|
1820
|
+
const timeout: Promise<typeof NOTIFICATION_WORK_TIMEOUT> = cancellableSleep(
|
|
1821
|
+
NOTIFICATION_WORK_TIMEOUT_MS,
|
|
1822
|
+
cutoff.signal,
|
|
1823
|
+
).then(() => NOTIFICATION_WORK_TIMEOUT);
|
|
1824
|
+
try {
|
|
1825
|
+
return await Promise.race([Promise.resolve().then(work), timeout]);
|
|
1826
|
+
} finally {
|
|
1827
|
+
cutoff.abort();
|
|
1828
|
+
}
|
|
1787
1829
|
}
|
|
1788
1830
|
|
|
1789
1831
|
#detachNotification(
|
|
@@ -1827,15 +1869,25 @@ export class SessionRouter {
|
|
|
1827
1869
|
this.#detachNotification(attached, "cancelled");
|
|
1828
1870
|
return;
|
|
1829
1871
|
}
|
|
1872
|
+
// A delivered frame ends the consecutive-failure run.
|
|
1873
|
+
attached.notificationFailures = 0;
|
|
1830
1874
|
if (frame.seq !== undefined)
|
|
1831
1875
|
attached.notificationSubscription.advanceCursor(frame.generation ?? attached.generation, frame.seq);
|
|
1832
1876
|
},
|
|
1833
1877
|
(error: unknown) => {
|
|
1834
|
-
|
|
1878
|
+
// Concede only after a bounded consecutive run: a single refused
|
|
1879
|
+
// publication drops that frame, it does not end the subscription.
|
|
1880
|
+
attached.notificationFailures += 1;
|
|
1881
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1882
|
+
if (attached.notificationFailures >= NOTIFICATION_LOCAL_FAILURE_LIMIT) {
|
|
1883
|
+
this.#detachNotification(attached, "cancelled");
|
|
1884
|
+
logger.warn(
|
|
1885
|
+
`SDK notification subscription ${attached.notificationSubscription.subscriptionId} failed locally ${attached.notificationFailures} times in a row; cancelling: ${detail}`,
|
|
1886
|
+
);
|
|
1887
|
+
return;
|
|
1888
|
+
}
|
|
1835
1889
|
logger.warn(
|
|
1836
|
-
`SDK notification subscription ${attached.notificationSubscription.subscriptionId}
|
|
1837
|
-
error instanceof Error ? error.message : String(error)
|
|
1838
|
-
}`,
|
|
1890
|
+
`SDK notification subscription ${attached.notificationSubscription.subscriptionId} refused one frame (${attached.notificationFailures}/${NOTIFICATION_LOCAL_FAILURE_LIMIT}); subscription retained: ${detail}`,
|
|
1839
1891
|
);
|
|
1840
1892
|
},
|
|
1841
1893
|
);
|
|
@@ -2092,7 +2144,9 @@ export class SessionRouter {
|
|
|
2092
2144
|
this.#failBarrier(attached, "replay went unanswered");
|
|
2093
2145
|
return;
|
|
2094
2146
|
}
|
|
2095
|
-
|
|
2147
|
+
// Awaited backoff rather than a raced timeout, but it must still not sleep
|
|
2148
|
+
// through a shutdown: cancelling on stop lets the process exit promptly.
|
|
2149
|
+
await cancellableSleep(REPLAY_RETRY_BACKOFF_MS * 2 ** attempt, this.#stopController.signal);
|
|
2096
2150
|
if (attached.barrier.held !== held || !this.#attachmentLive(attached)) return;
|
|
2097
2151
|
}
|
|
2098
2152
|
}
|
|
@@ -124,6 +124,7 @@ import {
|
|
|
124
124
|
EMPTY_RESPONSE_PROVIDER_CODE,
|
|
125
125
|
type FallbackAttemptToken,
|
|
126
126
|
type FallbackTriggerClass,
|
|
127
|
+
SERVER_OVERLOADED_PROVIDER_CODE,
|
|
127
128
|
STREAM_FIRST_EVENT_TIMEOUT_PROVIDER_CODE,
|
|
128
129
|
} from "@gajae-code/ai/utils/fallback-transport";
|
|
129
130
|
import { AttemptRecordStore } from "./attempt-record-store";
|
|
@@ -1047,6 +1048,8 @@ export interface PromptOptions {
|
|
|
1047
1048
|
onSkillPrepared?: (meta: { name: string; path: string; lineCount?: number; cleanedArgs?: string }) => void;
|
|
1048
1049
|
/** Optional invocation-scoped cancellation fence used before an accepted skill starts execution. */
|
|
1049
1050
|
preflightSignal?: AbortSignal;
|
|
1051
|
+
/** Internal SDK lifecycle owner token. */
|
|
1052
|
+
sdkRunToken?: string;
|
|
1050
1053
|
}
|
|
1051
1054
|
|
|
1052
1055
|
function promptPreflightCancelledError(): Error {
|
|
@@ -1297,11 +1300,74 @@ function isBareDefaultMessageOnlyFirstEventTimeout(message: AssistantMessage): b
|
|
|
1297
1300
|
BARE_DEFAULT_WATCHDOG_ERROR.test(message.errorMessage ?? ""))
|
|
1298
1301
|
);
|
|
1299
1302
|
}
|
|
1303
|
+
/**
|
|
1304
|
+
* True when the attempt's structured evidence is exactly the provider's typed
|
|
1305
|
+
* capacity-overload code and nothing else: no status, no retained retry header,
|
|
1306
|
+
* no second typed code, no error kind, and none of the transport's other
|
|
1307
|
+
* observations (request size, first-event timing, endpoint class, provider retry
|
|
1308
|
+
* ceiling). The code arrives statuslessly (a Codex error event, an HTTP 200
|
|
1309
|
+
* terminal Responses envelope), so these facts ARE the overload rather than
|
|
1310
|
+
* evidence that the attempt did observable work; any additional fact means the
|
|
1311
|
+
* attempt is no longer provably that bare capacity rejection. The code is
|
|
1312
|
+
* compared case-sensitively, matching the parser and the transport gate.
|
|
1313
|
+
*/
|
|
1314
|
+
function isExactTypedOverloadFacts(message: AssistantMessage): boolean {
|
|
1315
|
+
if (message.errorKind !== undefined || message.errorStatus !== undefined) return false;
|
|
1316
|
+
const facts = message.transportFailure;
|
|
1317
|
+
if (!facts) return false;
|
|
1318
|
+
return (
|
|
1319
|
+
facts.providerCode === SERVER_OVERLOADED_PROVIDER_CODE &&
|
|
1320
|
+
facts.status === undefined &&
|
|
1321
|
+
facts.anthropicErrorType === undefined &&
|
|
1322
|
+
(facts.openaiErrorCode === undefined || facts.openaiErrorCode === SERVER_OVERLOADED_PROVIDER_CODE) &&
|
|
1323
|
+
(facts.headers === undefined || Object.keys(facts.headers).length === 0) &&
|
|
1324
|
+
facts.requestBytes === undefined &&
|
|
1325
|
+
facts.firstEventElapsedMs === undefined &&
|
|
1326
|
+
facts.firstEventTimeoutMs === undefined &&
|
|
1327
|
+
facts.endpointClass === undefined &&
|
|
1328
|
+
facts.retryMaxAttempts === undefined
|
|
1329
|
+
);
|
|
1330
|
+
}
|
|
1331
|
+
/**
|
|
1332
|
+
* True when transport facts are exactly the provider's statusless typed
|
|
1333
|
+
* capacity-overload code (the provider code verbatim and, when present, the
|
|
1334
|
+
* OpenAI code verbatim, with no status). The agent loop must not treat the new
|
|
1335
|
+
* facts as managed transaction authority, and managed session fallback must
|
|
1336
|
+
* not gain retry/advance authority from them, so this predicate keeps both
|
|
1337
|
+
* guards reading only typed facts, never error prose.
|
|
1338
|
+
*/
|
|
1339
|
+
function isStatuslessTypedOverloadFacts(facts: TransportFailureFacts | undefined): boolean {
|
|
1340
|
+
if (!facts) return false;
|
|
1341
|
+
return (
|
|
1342
|
+
facts.status === undefined &&
|
|
1343
|
+
facts.providerCode === SERVER_OVERLOADED_PROVIDER_CODE &&
|
|
1344
|
+
(facts.openaiErrorCode === undefined || facts.openaiErrorCode === SERVER_OVERLOADED_PROVIDER_CODE)
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1300
1348
|
function isBareDefaultCodexOverload(message: AssistantMessage): boolean {
|
|
1301
1349
|
return (
|
|
1302
1350
|
message.api === "openai-codex-responses" &&
|
|
1303
1351
|
BARE_DEFAULT_CODEX_OVERLOAD_ERROR.test(message.errorMessage ?? "") &&
|
|
1304
|
-
!hasBareDefaultRetryDisqualifyingFacts(message) &&
|
|
1352
|
+
(isExactTypedOverloadFacts(message) || !hasBareDefaultRetryDisqualifyingFacts(message)) &&
|
|
1353
|
+
!assistantMessageHasVisibleOrToolContent(message)
|
|
1354
|
+
);
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* True when a generic OpenAI Responses turn failed with exactly OpenAI's typed
|
|
1359
|
+
* capacity-overload code. Unlike the Codex and Anthropic admissions this reads
|
|
1360
|
+
* no prose at all: the terminal Responses envelope carries the code in typed
|
|
1361
|
+
* transport facts, so the code plus a content-free attempt is the whole
|
|
1362
|
+
* admission. Both `openaiErrorCode` and the derived `providerCode` must be the
|
|
1363
|
+
* exact code, so a different transport's overload facts cannot enter through
|
|
1364
|
+
* this path.
|
|
1365
|
+
*/
|
|
1366
|
+
function isBareDefaultOpenAIResponsesOverload(message: AssistantMessage): boolean {
|
|
1367
|
+
return (
|
|
1368
|
+
message.api === "openai-responses" &&
|
|
1369
|
+
message.transportFailure?.openaiErrorCode === SERVER_OVERLOADED_PROVIDER_CODE &&
|
|
1370
|
+
isExactTypedOverloadFacts(message) &&
|
|
1305
1371
|
!assistantMessageHasVisibleOrToolContent(message)
|
|
1306
1372
|
);
|
|
1307
1373
|
}
|
|
@@ -2365,6 +2431,8 @@ export class AgentSession {
|
|
|
2365
2431
|
this.#followUpPromotionHooks.delete(message);
|
|
2366
2432
|
followUpHook({ startsOwnRun: true, removed: true });
|
|
2367
2433
|
}
|
|
2434
|
+
this.#sdkRunTokensByQueuedMessage.delete(message);
|
|
2435
|
+
this.#deepInterviewGenuineUserMessageEpochs.delete(message);
|
|
2368
2436
|
}
|
|
2369
2437
|
}
|
|
2370
2438
|
#fireQueuedPromotionHooks(messages: readonly AgentMessage[], promotion?: { startsOwnRun?: boolean }): void {
|
|
@@ -2384,6 +2452,29 @@ export class AgentSession {
|
|
|
2384
2452
|
}
|
|
2385
2453
|
}
|
|
2386
2454
|
}
|
|
2455
|
+
#queuedMessagesForSessionTransition(): AgentMessage[] {
|
|
2456
|
+
return [
|
|
2457
|
+
...new Set([
|
|
2458
|
+
...this.agent.snapshotSteering(),
|
|
2459
|
+
...this.agent.snapshotFollowUp(),
|
|
2460
|
+
...this.#deferredSdkFollowUps,
|
|
2461
|
+
]),
|
|
2462
|
+
];
|
|
2463
|
+
}
|
|
2464
|
+
/** Drop queued SDK work when the session identity is replaced. The old
|
|
2465
|
+
* promotion hooks belong to the predecessor runtime; retaining the message
|
|
2466
|
+
* would let it execute later under the successor without an owner. */
|
|
2467
|
+
#terminalizeQueuedSdkWorkForSessionTransition(messages: readonly AgentMessage[]): void {
|
|
2468
|
+
if (messages.length === 0) return;
|
|
2469
|
+
this.#fireQueuedRemovalHooks(messages);
|
|
2470
|
+
for (const message of messages) this.#sdkRunTokensByQueuedMessage.delete(message);
|
|
2471
|
+
this.#deferredSdkFollowUps = this.#deferredSdkFollowUps.filter(message => !messages.includes(message));
|
|
2472
|
+
}
|
|
2473
|
+
#resetActiveSdkRunOwnership(): void {
|
|
2474
|
+
this.#activeSdkRunToken = undefined;
|
|
2475
|
+
this.#activeAttemptScope = undefined;
|
|
2476
|
+
this.#activeLogicalRunId = undefined;
|
|
2477
|
+
}
|
|
2387
2478
|
/** Messages queued to be included with the next user prompt as context ("asides"). */
|
|
2388
2479
|
#pendingNextTurnMessages: Array<{ message: CustomMessage; origin: "turn" | "external" }> = [];
|
|
2389
2480
|
#scheduledHiddenNextTurnGeneration: number | undefined = undefined;
|
|
@@ -2568,6 +2659,8 @@ export class AgentSession {
|
|
|
2568
2659
|
/** SDK follow-up ownership by queued message and the attempt that dequeues it. */
|
|
2569
2660
|
#sdkRunTokensByQueuedMessage = new WeakMap<AgentMessage, string>();
|
|
2570
2661
|
#sdkRunTokensByAttemptScope = new WeakMap<AttemptScope, string>();
|
|
2662
|
+
#activeSdkRunToken: string | undefined;
|
|
2663
|
+
#activeAttemptScope: AttemptScope | undefined;
|
|
2571
2664
|
#attemptAuthority!: AttemptScopeAuthority;
|
|
2572
2665
|
#attemptRecordStore!: AttemptRecordStore;
|
|
2573
2666
|
#activeLogicalRunId: AttemptRunHandle["logicalRunId"] | undefined;
|
|
@@ -2576,6 +2669,7 @@ export class AgentSession {
|
|
|
2576
2669
|
// previously recorded run id rather than throwing inside the callback.
|
|
2577
2670
|
if (!handle) return;
|
|
2578
2671
|
this.#activeLogicalRunId = handle.logicalRunId;
|
|
2672
|
+
this.#activeAttemptScope = handle.scope;
|
|
2579
2673
|
}
|
|
2580
2674
|
|
|
2581
2675
|
#turnIndex = 0;
|
|
@@ -3975,7 +4069,20 @@ export class AgentSession {
|
|
|
3975
4069
|
// when the batch is drained by a continuation the message did not
|
|
3976
4070
|
// schedule (a skipped continuation must never discard the correlation
|
|
3977
4071
|
// of work that is still consumed; review thread P2).
|
|
4072
|
+
const consumedSdkRunToken =
|
|
4073
|
+
promotion.startsOwnRun === true
|
|
4074
|
+
? [...messages, ...dropped]
|
|
4075
|
+
.map(message => this.#sdkRunTokensByQueuedMessage.get(message))
|
|
4076
|
+
.find((token): token is string => token !== undefined)
|
|
4077
|
+
: undefined;
|
|
3978
4078
|
this.#fireQueuedPromotionHooks(messages, promotion);
|
|
4079
|
+
if (consumedSdkRunToken !== undefined) this.#activeSdkRunToken = consumedSdkRunToken;
|
|
4080
|
+
// Dropped or in-run follow-ups have no later own-run acceptance callback;
|
|
4081
|
+
// delete those tokens now. Own-run messages stay mapped until
|
|
4082
|
+
// #scheduleAgentContinue.onRunAccepted binds the new attempt scope.
|
|
4083
|
+
for (const message of dropped) this.#sdkRunTokensByQueuedMessage.delete(message);
|
|
4084
|
+
if (promotion.startsOwnRun !== true)
|
|
4085
|
+
for (const message of messages) this.#sdkRunTokensByQueuedMessage.delete(message);
|
|
3979
4086
|
};
|
|
3980
4087
|
// Steering consumed mid-run never starts its own run: fire the stored
|
|
3981
4088
|
// promotion hook at the REAL dequeue boundary so the SDK attaches the
|
|
@@ -6368,18 +6475,23 @@ export class AgentSession {
|
|
|
6368
6475
|
// agent_start (review P1); their queued messages are in-run
|
|
6369
6476
|
// consumptions, not own-run promotions.
|
|
6370
6477
|
const startsOwn = options?.maintenanceContinuation !== true;
|
|
6478
|
+
const consumedSdkRunToken = startsOwn
|
|
6479
|
+
? acceptance.consumedQueuedMessages
|
|
6480
|
+
.map(message => this.#sdkRunTokensByQueuedMessage.get(message))
|
|
6481
|
+
.find((token): token is string => token !== undefined)
|
|
6482
|
+
: undefined;
|
|
6371
6483
|
this.#fireQueuedPromotionHooks(acceptance.consumedQueuedMessages, {
|
|
6372
6484
|
startsOwnRun: startsOwn,
|
|
6373
6485
|
});
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6377
|
-
this.#sdkRunTokensByAttemptScope.set(handle.scope, sdkRunToken);
|
|
6378
|
-
break;
|
|
6379
|
-
}
|
|
6380
|
-
}
|
|
6486
|
+
if (consumedSdkRunToken !== undefined)
|
|
6487
|
+
this.#sdkRunTokensByAttemptScope.set(handle.scope, consumedSdkRunToken);
|
|
6488
|
+
if (startsOwn) this.#activeSdkRunToken = consumedSdkRunToken;
|
|
6381
6489
|
this.#acceptRunHandle(handle);
|
|
6382
6490
|
options?.onRunAccepted?.(handle);
|
|
6491
|
+
// Keep the queued token available through the acceptance callback;
|
|
6492
|
+
// SDK follow-up owners bind it to the new attempt scope there.
|
|
6493
|
+
for (const message of acceptance.consumedQueuedMessages)
|
|
6494
|
+
this.#sdkRunTokensByQueuedMessage.delete(message);
|
|
6383
6495
|
settleLease();
|
|
6384
6496
|
releasePredecessor();
|
|
6385
6497
|
if (startsQueuedSuccessor) {
|
|
@@ -7441,12 +7553,30 @@ export class AgentSession {
|
|
|
7441
7553
|
await this.#flushWorkerIntegrationForAgentEnd();
|
|
7442
7554
|
}
|
|
7443
7555
|
const deliveryScope = scope ?? (event as AgentSessionEvent & { scope?: AttemptScopeRef }).scope;
|
|
7556
|
+
const eventToken = (event as AgentSessionEvent & { sdkRunToken?: unknown }).sdkRunToken;
|
|
7557
|
+
const sdkRunToken =
|
|
7558
|
+
typeof eventToken === "string"
|
|
7559
|
+
? eventToken
|
|
7560
|
+
: deliveryScope
|
|
7561
|
+
? this.#sdkRunTokensByAttemptScope.get(deliveryScope as AttemptScope)
|
|
7562
|
+
: this.#activeSdkRunToken;
|
|
7444
7563
|
const isTerminalAgentEnd =
|
|
7445
7564
|
event.type === "agent_end" && !(event.stopReason === "maintenance" && event.maintenanceOutcome !== "aborted");
|
|
7446
7565
|
const finishAttempt = () => {
|
|
7447
7566
|
if (!isTerminalAgentEnd) return;
|
|
7448
|
-
|
|
7449
|
-
|
|
7567
|
+
const isActiveAttempt =
|
|
7568
|
+
deliveryScope === undefined ||
|
|
7569
|
+
this.#activeAttemptScope === undefined ||
|
|
7570
|
+
deliveryScope === this.#activeAttemptScope;
|
|
7571
|
+
if (deliveryScope) {
|
|
7572
|
+
this.#attemptRecordStore.retire(deliveryScope as AttemptScope);
|
|
7573
|
+
this.#sdkRunTokensByAttemptScope.delete(deliveryScope as AttemptScope);
|
|
7574
|
+
}
|
|
7575
|
+
if (isActiveAttempt && sdkRunToken === this.#activeSdkRunToken) this.#activeSdkRunToken = undefined;
|
|
7576
|
+
if (isActiveAttempt) {
|
|
7577
|
+
this.#activeAttemptScope = undefined;
|
|
7578
|
+
this.#activeLogicalRunId = undefined;
|
|
7579
|
+
}
|
|
7450
7580
|
};
|
|
7451
7581
|
if (!this.#extensionRunner) {
|
|
7452
7582
|
finishAttempt();
|
|
@@ -7459,9 +7589,7 @@ export class AgentSession {
|
|
|
7459
7589
|
await this.#extensionRunner.emit(
|
|
7460
7590
|
{
|
|
7461
7591
|
type: "agent_start",
|
|
7462
|
-
...(
|
|
7463
|
-
? { sdkRunToken: this.#sdkRunTokensByAttemptScope.get(deliveryScope as AttemptScope) }
|
|
7464
|
-
: {}),
|
|
7592
|
+
...(sdkRunToken ? { sdkRunToken } : {}),
|
|
7465
7593
|
},
|
|
7466
7594
|
undefined,
|
|
7467
7595
|
deliveryScope,
|
|
@@ -7472,6 +7600,7 @@ export class AgentSession {
|
|
|
7472
7600
|
type: "agent_failed",
|
|
7473
7601
|
error: sanitizePromptFailure(event.error),
|
|
7474
7602
|
scope: event.scope,
|
|
7603
|
+
...(sdkRunToken ? { sdkRunToken } : {}),
|
|
7475
7604
|
},
|
|
7476
7605
|
undefined,
|
|
7477
7606
|
deliveryScope,
|
|
@@ -7483,6 +7612,7 @@ export class AgentSession {
|
|
|
7483
7612
|
messages: event.messages,
|
|
7484
7613
|
stopReason: event.stopReason,
|
|
7485
7614
|
maintenanceOutcome: event.maintenanceOutcome,
|
|
7615
|
+
...(sdkRunToken ? { sdkRunToken } : {}),
|
|
7486
7616
|
},
|
|
7487
7617
|
undefined,
|
|
7488
7618
|
deliveryScope,
|
|
@@ -9571,7 +9701,7 @@ export class AgentSession {
|
|
|
9571
9701
|
args = "",
|
|
9572
9702
|
options?: Pick<
|
|
9573
9703
|
PromptOptions,
|
|
9574
|
-
"onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared" | "preflightSignal"
|
|
9704
|
+
"onPreflightAccepted" | "onPreflightAcceptCommit" | "onSkillPrepared" | "preflightSignal" | "sdkRunToken"
|
|
9575
9705
|
>,
|
|
9576
9706
|
): Promise<{ name: string; path: string; args?: string; lineCount?: number }> {
|
|
9577
9707
|
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
@@ -10330,6 +10460,7 @@ export class AgentSession {
|
|
|
10330
10460
|
? { onPreflightAcceptCommit: options.onPreflightAcceptCommit }
|
|
10331
10461
|
: {}),
|
|
10332
10462
|
...(options.preflightSignal ? { preflightSignal: options.preflightSignal } : {}),
|
|
10463
|
+
...(options.sdkRunToken ? { sdkRunToken: options.sdkRunToken } : {}),
|
|
10333
10464
|
}
|
|
10334
10465
|
: undefined,
|
|
10335
10466
|
);
|
|
@@ -10583,6 +10714,7 @@ export class AgentSession {
|
|
|
10583
10714
|
| "onPreflightAccepted"
|
|
10584
10715
|
| "onPreflightAcceptCommit"
|
|
10585
10716
|
| "preflightSignal"
|
|
10717
|
+
| "sdkRunToken"
|
|
10586
10718
|
>,
|
|
10587
10719
|
): Promise<void> {
|
|
10588
10720
|
if (options?.preflightSignal?.aborted) throw promptPreflightCancelledError();
|
|
@@ -10684,6 +10816,7 @@ export class AgentSession {
|
|
|
10684
10816
|
| "onPreflightAccepted"
|
|
10685
10817
|
| "onPreflightAcceptCommit"
|
|
10686
10818
|
| "preflightSignal"
|
|
10819
|
+
| "sdkRunToken"
|
|
10687
10820
|
> & {
|
|
10688
10821
|
prependMessages?: AgentMessage[];
|
|
10689
10822
|
skipPostPromptRecoveryWait?: boolean;
|
|
@@ -11017,8 +11150,13 @@ export class AgentSession {
|
|
|
11017
11150
|
const agentPromptOptions = {
|
|
11018
11151
|
...(options?.toolChoice ? { toolChoice: options.toolChoice } : undefined),
|
|
11019
11152
|
...this.#managedFallbackPromptOptions(),
|
|
11153
|
+
...(options?.sdkRunToken ? { sdkRunToken: options.sdkRunToken } : {}),
|
|
11020
11154
|
onRunAccepted: (handle: AttemptRunHandle) => {
|
|
11021
11155
|
this.#acceptRunHandle(handle);
|
|
11156
|
+
if (options?.sdkRunToken) {
|
|
11157
|
+
this.#sdkRunTokensByAttemptScope.set(handle.scope, options.sdkRunToken);
|
|
11158
|
+
this.#activeSdkRunToken = options.sdkRunToken;
|
|
11159
|
+
}
|
|
11022
11160
|
options?.onRunAccepted?.(handle);
|
|
11023
11161
|
options?.admissionLease?.release();
|
|
11024
11162
|
// R3.3: the accepted-run wrapper is the exact acceptance boundary —
|
|
@@ -12110,6 +12248,7 @@ export class AgentSession {
|
|
|
12110
12248
|
await this.prompt(text, {
|
|
12111
12249
|
expandPromptTemplates: false,
|
|
12112
12250
|
images,
|
|
12251
|
+
sdkRunToken: options?.sdkRunToken,
|
|
12113
12252
|
onPreflightAccepted: () => {
|
|
12114
12253
|
options?.onPreflightAccepted?.();
|
|
12115
12254
|
fireQueuedPromotion();
|
|
@@ -13461,6 +13600,8 @@ export class AgentSession {
|
|
|
13461
13600
|
this.#cancelOwnAsyncJobs();
|
|
13462
13601
|
this.#closeAllProviderSessions("new session");
|
|
13463
13602
|
this.#rebindProviderSessionState(new Map());
|
|
13603
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition(this.#queuedMessagesForSessionTransition());
|
|
13604
|
+
this.#resetActiveSdkRunOwnership();
|
|
13464
13605
|
this.agent.reset();
|
|
13465
13606
|
if (!options?.drop) await this.sessionManager.flush();
|
|
13466
13607
|
const noLeasePreviousSessionIdentity = this.sessionManager.getSessionId();
|
|
@@ -13558,6 +13699,8 @@ export class AgentSession {
|
|
|
13558
13699
|
this.#disconnectFromAgent();
|
|
13559
13700
|
this.#closeAllProviderSessions("new session");
|
|
13560
13701
|
this.#rebindProviderSessionState(new Map());
|
|
13702
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition(this.#queuedMessagesForSessionTransition());
|
|
13703
|
+
this.#resetActiveSdkRunOwnership();
|
|
13561
13704
|
this.agent.reset();
|
|
13562
13705
|
this.setTodoPhases([]);
|
|
13563
13706
|
this.#syncAgentSessionId();
|
|
@@ -13680,6 +13823,8 @@ export class AgentSession {
|
|
|
13680
13823
|
this.yieldQueue.clear();
|
|
13681
13824
|
this.#pendingBackgroundExchanges = [];
|
|
13682
13825
|
this.#closeAllProviderSessions("context clear");
|
|
13826
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition(this.#queuedMessagesForSessionTransition());
|
|
13827
|
+
this.#resetActiveSdkRunOwnership();
|
|
13683
13828
|
this.agent.reset();
|
|
13684
13829
|
await this.sessionManager.flush();
|
|
13685
13830
|
this.sessionManager.appendContextClearEntry({ sessionId });
|
|
@@ -16104,6 +16249,7 @@ export class AgentSession {
|
|
|
16104
16249
|
const rollbackPendingNextTurnMessages = [...this.#pendingNextTurnMessages];
|
|
16105
16250
|
const rollbackScheduledHiddenNextTurnGeneration = this.#scheduledHiddenNextTurnGeneration;
|
|
16106
16251
|
const rollbackTodoReminderCount = this.#todoReminderCount;
|
|
16252
|
+
const rollbackDeferredSdkFollowUps = [...this.#deferredSdkFollowUps];
|
|
16107
16253
|
// Snapshot the agent's executable queues so a rollback restores queued
|
|
16108
16254
|
// user work that agent.reset() would otherwise clear.
|
|
16109
16255
|
const rollbackAgentSteeringQueue = this.agent.snapshotSteering();
|
|
@@ -16144,6 +16290,12 @@ export class AgentSession {
|
|
|
16144
16290
|
this.#rekeyJobManagerForSessionIdentity(rollbackSessionState.sessionId, rollbackSessionState.sessionFile);
|
|
16145
16291
|
committed = true;
|
|
16146
16292
|
await this.#runToolSessionTransitionCleanups();
|
|
16293
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition([
|
|
16294
|
+
...rollbackAgentSteeringQueue,
|
|
16295
|
+
...rollbackAgentFollowUpQueue,
|
|
16296
|
+
...rollbackDeferredSdkFollowUps,
|
|
16297
|
+
]);
|
|
16298
|
+
this.#resetActiveSdkRunOwnership();
|
|
16147
16299
|
this.agent.reset();
|
|
16148
16300
|
this.#syncAgentSessionId();
|
|
16149
16301
|
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
@@ -16227,6 +16379,7 @@ export class AgentSession {
|
|
|
16227
16379
|
this.#pendingNextTurnMessages = rollbackPendingNextTurnMessages;
|
|
16228
16380
|
this.#scheduledHiddenNextTurnGeneration = rollbackScheduledHiddenNextTurnGeneration;
|
|
16229
16381
|
this.#todoReminderCount = rollbackTodoReminderCount;
|
|
16382
|
+
this.#deferredSdkFollowUps = rollbackDeferredSdkFollowUps;
|
|
16230
16383
|
this.#syncTodoPhasesFromBranch();
|
|
16231
16384
|
// Exact-discard only the staged successor; predecessor state was never adopted.
|
|
16232
16385
|
const rollbackError = prepared
|
|
@@ -18995,6 +19148,18 @@ export class AgentSession {
|
|
|
18995
19148
|
},
|
|
18996
19149
|
};
|
|
18997
19150
|
}
|
|
19151
|
+
// Issue #5018 preserves managed behavior for the typed statusless
|
|
19152
|
+
// Responses overload: before the code survived transport, this failure
|
|
19153
|
+
// reached the session as an ordinary committed error, so the chain never
|
|
19154
|
+
// discarded or advanced on it. Route it to the exhaustion decision
|
|
19155
|
+
// directly, before the retryable path can classify its new facts.
|
|
19156
|
+
if (isStatuslessTypedOverloadFacts(outcome.failure.transportFailure)) {
|
|
19157
|
+
this.#defaultFallbackChain().resetAttemptBudget();
|
|
19158
|
+
return this.#managedFallbackExhaustionDecision(
|
|
19159
|
+
outcome.failure.message,
|
|
19160
|
+
outcome.failure.message.errorMessage || "Model fallback attempt failed",
|
|
19161
|
+
);
|
|
19162
|
+
}
|
|
18998
19163
|
return this.#handleRetryableError(
|
|
18999
19164
|
outcome.failure.message,
|
|
19000
19165
|
true,
|
|
@@ -19453,7 +19618,10 @@ export class AgentSession {
|
|
|
19453
19618
|
}
|
|
19454
19619
|
const firstEventTimeout = classification === "first_event_timeout";
|
|
19455
19620
|
const emptyResponse = classification === "empty_response";
|
|
19456
|
-
const canReplayProviderOverload =
|
|
19621
|
+
const canReplayProviderOverload =
|
|
19622
|
+
isBareDefaultCodexOverload(message) ||
|
|
19623
|
+
isBareDefaultAnthropicOverload(message) ||
|
|
19624
|
+
isBareDefaultOpenAIResponsesOverload(message);
|
|
19457
19625
|
const reportedRetryMaxAttempts = transportFailure?.retryMaxAttempts;
|
|
19458
19626
|
if (reportedRetryMaxAttempts !== undefined) {
|
|
19459
19627
|
this.#providerRetryMaxAttempts = Math.min(
|
|
@@ -19496,6 +19664,17 @@ export class AgentSession {
|
|
|
19496
19664
|
const trigger:
|
|
19497
19665
|
| { class: FallbackTriggerClass; retryAfterMs?: number; authDisposition?: AuthDisposition }
|
|
19498
19666
|
| undefined = this.#fallbackTriggerFor(message, !managedFallback, transportFailure);
|
|
19667
|
+
// OpenAI's typed statusless capacity-overload code (issue #5018) must not
|
|
19668
|
+
// gain managed-chain retry/advance authority from its new facts. Before
|
|
19669
|
+
// the code survived transport, this failure reached the session as an
|
|
19670
|
+
// ordinary committed error and surfaced immediately, so mirror that
|
|
19671
|
+
// behavior with the existing exhaustion decision.
|
|
19672
|
+
if (managedFallback && isStatuslessTypedOverloadFacts(transportFailure)) {
|
|
19673
|
+
return this.#managedFallbackExhaustionDecision(
|
|
19674
|
+
message,
|
|
19675
|
+
message.errorMessage || "Model fallback attempt failed",
|
|
19676
|
+
);
|
|
19677
|
+
}
|
|
19499
19678
|
if (!trigger) {
|
|
19500
19679
|
return managedOutcome
|
|
19501
19680
|
? this.#managedFallbackExhaustionDecision(message, message.errorMessage || "Model fallback attempt failed")
|
|
@@ -19934,6 +20113,7 @@ export class AgentSession {
|
|
|
19934
20113
|
preSubmit: PreSubmitBuilder,
|
|
19935
20114
|
options?: {
|
|
19936
20115
|
toolChoice?: ToolChoice;
|
|
20116
|
+
sdkRunToken?: string;
|
|
19937
20117
|
fallbackManaged?: boolean;
|
|
19938
20118
|
onRunAccepted?: (handle: AttemptRunHandle) => void;
|
|
19939
20119
|
},
|
|
@@ -21233,6 +21413,10 @@ export class AgentSession {
|
|
|
21233
21413
|
const previousSystemPrompt = this.agent.state.systemPrompt;
|
|
21234
21414
|
const previousAgentSteeringQueue = this.agent.snapshotSteering();
|
|
21235
21415
|
const previousAgentFollowUpQueue = this.agent.snapshotFollowUp();
|
|
21416
|
+
const previousDeferredSdkFollowUps = [...this.#deferredSdkFollowUps];
|
|
21417
|
+
const previousActiveSdkRunToken = this.#activeSdkRunToken;
|
|
21418
|
+
const previousActiveAttemptScope = this.#activeAttemptScope;
|
|
21419
|
+
const previousActiveLogicalRunId = this.#activeLogicalRunId;
|
|
21236
21420
|
|
|
21237
21421
|
this.#steeringMessages = [];
|
|
21238
21422
|
this.#followUpMessages = [];
|
|
@@ -21280,6 +21464,7 @@ export class AgentSession {
|
|
|
21280
21464
|
// The target session is loaded and MCP selections are restored: discard
|
|
21281
21465
|
// pre-switch delivery queues before completing the restored agent state.
|
|
21282
21466
|
this.agent.clearAllQueues();
|
|
21467
|
+
this.#resetActiveSdkRunOwnership();
|
|
21283
21468
|
|
|
21284
21469
|
if (historyRewriteReason) {
|
|
21285
21470
|
this.agent.replaceMessages(sessionContext.messages, {
|
|
@@ -21464,6 +21649,12 @@ export class AgentSession {
|
|
|
21464
21649
|
...(options?.transition ? { transition: options.transition } : {}),
|
|
21465
21650
|
});
|
|
21466
21651
|
}
|
|
21652
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition([
|
|
21653
|
+
...previousAgentSteeringQueue,
|
|
21654
|
+
...previousAgentFollowUpQueue,
|
|
21655
|
+
...previousDeferredSdkFollowUps,
|
|
21656
|
+
]);
|
|
21657
|
+
this.#deferredSdkFollowUps = [];
|
|
21467
21658
|
return true;
|
|
21468
21659
|
} catch (error) {
|
|
21469
21660
|
if (transitionCleanupCommitted) throw error;
|
|
@@ -21529,6 +21720,10 @@ export class AgentSession {
|
|
|
21529
21720
|
this.#followUpMessages = previousFollowUpMessages;
|
|
21530
21721
|
this.#pendingNextTurnMessages = previousPendingNextTurnMessages;
|
|
21531
21722
|
this.#scheduledHiddenNextTurnGeneration = previousScheduledHiddenNextTurnGeneration;
|
|
21723
|
+
this.#deferredSdkFollowUps = previousDeferredSdkFollowUps;
|
|
21724
|
+
this.#activeSdkRunToken = previousActiveSdkRunToken;
|
|
21725
|
+
this.#activeAttemptScope = previousActiveAttemptScope;
|
|
21726
|
+
this.#activeLogicalRunId = previousActiveLogicalRunId;
|
|
21532
21727
|
this.agent.clearAllQueues();
|
|
21533
21728
|
this.agent.restoreSteering(previousAgentSteeringQueue);
|
|
21534
21729
|
this.agent.restoreFollowUp(previousAgentFollowUpQueue);
|
|
@@ -21595,6 +21790,9 @@ export class AgentSession {
|
|
|
21595
21790
|
}
|
|
21596
21791
|
|
|
21597
21792
|
const selectedText = this.#extractUserMessageText(selectedEntry.message.content);
|
|
21793
|
+
const previousAgentSteeringQueue = this.agent.snapshotSteering();
|
|
21794
|
+
const previousAgentFollowUpQueue = this.agent.snapshotFollowUp();
|
|
21795
|
+
const previousDeferredSdkFollowUps = [...this.#deferredSdkFollowUps];
|
|
21598
21796
|
|
|
21599
21797
|
let skipConversationRestore = false;
|
|
21600
21798
|
|
|
@@ -21630,6 +21828,16 @@ export class AgentSession {
|
|
|
21630
21828
|
}
|
|
21631
21829
|
this.#pendingNextTurnMessages = [];
|
|
21632
21830
|
this.#scheduledHiddenNextTurnGeneration = undefined;
|
|
21831
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition([
|
|
21832
|
+
...previousAgentSteeringQueue,
|
|
21833
|
+
...previousAgentFollowUpQueue,
|
|
21834
|
+
...previousDeferredSdkFollowUps,
|
|
21835
|
+
]);
|
|
21836
|
+
this.#deferredSdkFollowUps = [];
|
|
21837
|
+
this.#resetActiveSdkRunOwnership();
|
|
21838
|
+
this.agent.clearAllQueues();
|
|
21839
|
+
this.#steeringMessages = [];
|
|
21840
|
+
this.#followUpMessages = [];
|
|
21633
21841
|
|
|
21634
21842
|
this.#syncTodoPhasesFromBranch();
|
|
21635
21843
|
this.#syncAgentSessionId();
|
|
@@ -21835,6 +22043,15 @@ export class AgentSession {
|
|
|
21835
22043
|
this.sessionManager.branch(newLeafId);
|
|
21836
22044
|
}
|
|
21837
22045
|
|
|
22046
|
+
// The history rewrite is now committed. Drop predecessor-owned queued SDK
|
|
22047
|
+
// work at this boundary; cancelled or failed preparation above preserves it.
|
|
22048
|
+
const queuedSdkWork = this.#queuedMessagesForSessionTransition();
|
|
22049
|
+
this.#terminalizeQueuedSdkWorkForSessionTransition(queuedSdkWork);
|
|
22050
|
+
this.#deferredSdkFollowUps = [];
|
|
22051
|
+
this.agent.clearAllQueues();
|
|
22052
|
+
this.#steeringMessages = [];
|
|
22053
|
+
this.#followUpMessages = [];
|
|
22054
|
+
|
|
21838
22055
|
// Update agent state through the canonical filtered display context so legacy
|
|
21839
22056
|
// request-scoped entries cannot re-enter live history after tree navigation.
|
|
21840
22057
|
const displayContext = this.buildDisplaySessionContext();
|