@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
|
@@ -87,6 +87,8 @@ const SDK_ONLY_TERMINAL_PUBLICATION_WAIT_MS = 1_000;
|
|
|
87
87
|
* after this bound are abandoned — their durable broker state is the recovery
|
|
88
88
|
* authority, and outcomes are inherently uncertain. */
|
|
89
89
|
const GATE_RESOLUTION_QUIESCENCE_MS = 5_000;
|
|
90
|
+
/** Maximum time a replaced runtime may retain a lifecycle persistence task. */
|
|
91
|
+
const LIFECYCLE_QUIESCENCE_MS = 1_000;
|
|
90
92
|
|
|
91
93
|
class DiffQueryError extends Error {
|
|
92
94
|
constructor(
|
|
@@ -397,6 +399,10 @@ export interface CreateSdkSessionRuntimeOptions {
|
|
|
397
399
|
terminalAbortSeams?: SdkOnlyTerminalAbortSeams;
|
|
398
400
|
/** Callback when a frame is admitted to the runtime (test harness). */
|
|
399
401
|
onFrameAdmitted?: () => void;
|
|
402
|
+
/** Test-only observation of a genuinely timed-out lifecycle drain. */
|
|
403
|
+
onLifecycleDrainTimeoutForTests?: () => void;
|
|
404
|
+
/** Test-only observation of bounded failure-diagnostic deduplication state. */
|
|
405
|
+
onFailureDiagnosticKeyCountForTests?: (count: number) => void;
|
|
400
406
|
}
|
|
401
407
|
|
|
402
408
|
function unavailable(operation: string): () => never {
|
|
@@ -746,7 +752,13 @@ export function createInvocationReconciliation(
|
|
|
746
752
|
// failure reason may arrive on a different delivery path than the one that
|
|
747
753
|
// claimed the terminal. Enrich the settled record instead of dropping it;
|
|
748
754
|
// never resurrect (status/terminalAt untouched), and first reason wins.
|
|
749
|
-
if (frame.type === "agent_failed"
|
|
755
|
+
if (frame.type === "agent_failed") {
|
|
756
|
+
const failure = sanitizePromptFailure(frame.error);
|
|
757
|
+
if (
|
|
758
|
+
record.error !== undefined &&
|
|
759
|
+
!(record.error.code === "agent_failed" && failure.code !== "agent_failed")
|
|
760
|
+
)
|
|
761
|
+
return;
|
|
750
762
|
const next = { ...record, revision: ++mutationRevision };
|
|
751
763
|
logger.error("SDK invocation failed (late)", {
|
|
752
764
|
kind,
|
|
@@ -754,7 +766,7 @@ export function createInvocationReconciliation(
|
|
|
754
766
|
turnId: correlation.turnId,
|
|
755
767
|
error: formatPromptFailureForLocalLog(frame.error),
|
|
756
768
|
});
|
|
757
|
-
next.error =
|
|
769
|
+
next.error = failure;
|
|
758
770
|
records.set(recordKey, next);
|
|
759
771
|
try {
|
|
760
772
|
await persist();
|
|
@@ -779,7 +791,9 @@ export function createInvocationReconciliation(
|
|
|
779
791
|
turnId: correlation.turnId,
|
|
780
792
|
error: formatPromptFailureForLocalLog(frame.error),
|
|
781
793
|
});
|
|
782
|
-
|
|
794
|
+
const failure = sanitizePromptFailure(frame.error);
|
|
795
|
+
if (next.error === undefined || (next.error.code === "agent_failed" && failure.code !== "agent_failed"))
|
|
796
|
+
next.error = failure;
|
|
783
797
|
} else {
|
|
784
798
|
next.status = next.error === undefined ? "terminal_ok" : "failed";
|
|
785
799
|
next.terminalAt = Date.now();
|
|
@@ -1507,6 +1521,85 @@ function rejectionRecoveryIntent(error: unknown): { code: string; message: strin
|
|
|
1507
1521
|
return sanitizePromptFailure(error);
|
|
1508
1522
|
}
|
|
1509
1523
|
|
|
1524
|
+
/**
|
|
1525
|
+
* A provider can finish a stream with an assistant error message rather than
|
|
1526
|
+
* rejecting the prompt promise. The SDK extension receives the final agent_end
|
|
1527
|
+
* after retry/fallback policy has settled, so this is the safe boundary at which
|
|
1528
|
+
* to publish the additive failure diagnostic before terminal reconciliation.
|
|
1529
|
+
*/
|
|
1530
|
+
function providerFailureFromAgentEnd(event: unknown): { code: string; message: string } | undefined {
|
|
1531
|
+
try {
|
|
1532
|
+
if (!event || typeof event !== "object") return undefined;
|
|
1533
|
+
const messages = (event as { messages?: unknown }).messages;
|
|
1534
|
+
if (!Array.isArray(messages)) return undefined;
|
|
1535
|
+
const lastAssistant = [...messages]
|
|
1536
|
+
.reverse()
|
|
1537
|
+
.find(
|
|
1538
|
+
message => message && typeof message === "object" && (message as { role?: unknown }).role === "assistant",
|
|
1539
|
+
);
|
|
1540
|
+
if (!lastAssistant || typeof lastAssistant !== "object") return undefined;
|
|
1541
|
+
const assistant = lastAssistant as {
|
|
1542
|
+
stopReason?: unknown;
|
|
1543
|
+
errorMessage?: unknown;
|
|
1544
|
+
errorKind?: unknown;
|
|
1545
|
+
errorStatus?: unknown;
|
|
1546
|
+
transportFailure?: { status?: unknown };
|
|
1547
|
+
};
|
|
1548
|
+
let stopReason: unknown;
|
|
1549
|
+
try {
|
|
1550
|
+
stopReason = assistant.stopReason;
|
|
1551
|
+
} catch {
|
|
1552
|
+
return undefined;
|
|
1553
|
+
}
|
|
1554
|
+
if (stopReason !== "error") return undefined;
|
|
1555
|
+
let errorKind: unknown;
|
|
1556
|
+
try {
|
|
1557
|
+
errorKind = assistant.errorKind;
|
|
1558
|
+
} catch {
|
|
1559
|
+
return undefined;
|
|
1560
|
+
}
|
|
1561
|
+
if (errorKind === "local_snapshot_failure" || errorKind === "local_buffer_overflow") return undefined;
|
|
1562
|
+
let errorMessage: unknown;
|
|
1563
|
+
try {
|
|
1564
|
+
errorMessage = assistant.errorMessage;
|
|
1565
|
+
} catch {
|
|
1566
|
+
return undefined;
|
|
1567
|
+
}
|
|
1568
|
+
// Agent-local malformed-tool and composer policy circuit breakers also use
|
|
1569
|
+
// stopReason=error, but they are not provider failures and must not be
|
|
1570
|
+
// reported as provider_rejected to SDK callers.
|
|
1571
|
+
if (
|
|
1572
|
+
typeof errorMessage === "string" &&
|
|
1573
|
+
(errorMessage.includes("Composer bash policy blocked repository file I/O again") ||
|
|
1574
|
+
errorMessage.includes("consecutive turns of malformed tool calls"))
|
|
1575
|
+
)
|
|
1576
|
+
return undefined;
|
|
1577
|
+
let status: number | undefined;
|
|
1578
|
+
try {
|
|
1579
|
+
const errorStatus = assistant.errorStatus;
|
|
1580
|
+
if (typeof errorStatus === "number" && Number.isSafeInteger(errorStatus)) status = errorStatus;
|
|
1581
|
+
} catch {
|
|
1582
|
+
// A malformed errorStatus is equivalent to a statusless provider error.
|
|
1583
|
+
}
|
|
1584
|
+
if (status === undefined) {
|
|
1585
|
+
try {
|
|
1586
|
+
const transportStatus = assistant.transportFailure?.status;
|
|
1587
|
+
if (typeof transportStatus === "number" && Number.isSafeInteger(transportStatus)) status = transportStatus;
|
|
1588
|
+
} catch {
|
|
1589
|
+
// A throwing transport metadata accessor is also statusless.
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
if (status === 402 || status === 429)
|
|
1593
|
+
return { code: `provider_http_${status}`, message: "Prompt submission failed." };
|
|
1594
|
+
if (status !== undefined) return { code: "provider_rejected", message: "Prompt submission failed." };
|
|
1595
|
+
return { code: "provider_rejected", message: "Prompt submission failed." };
|
|
1596
|
+
} catch {
|
|
1597
|
+
// Provider metadata is untrusted: an SDK/provider adapter may expose a
|
|
1598
|
+
// throwing getter while the lifecycle boundary still needs to terminalize.
|
|
1599
|
+
return undefined;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1510
1603
|
function createControlSurface(
|
|
1511
1604
|
ctx: ExtensionContext,
|
|
1512
1605
|
api: ExtensionAPI,
|
|
@@ -1516,12 +1609,14 @@ function createControlSurface(
|
|
|
1516
1609
|
correlation: InvocationCorrelation,
|
|
1517
1610
|
connectionId: string | undefined,
|
|
1518
1611
|
startsOwnTurn: boolean,
|
|
1612
|
+
sdkRunToken: string,
|
|
1519
1613
|
) => void,
|
|
1520
1614
|
steerReconciliation: KindAwareReconciliation,
|
|
1521
1615
|
onPromotedTurn?: (
|
|
1522
1616
|
kind: InvocationKind,
|
|
1523
1617
|
correlation: InvocationCorrelation,
|
|
1524
1618
|
connectionId: string | undefined,
|
|
1619
|
+
sdkRunToken: string,
|
|
1525
1620
|
promotion?: { startsOwnRun?: boolean; removed?: boolean },
|
|
1526
1621
|
) => void,
|
|
1527
1622
|
policy?: SdkSurfacePolicy,
|
|
@@ -1627,6 +1722,7 @@ function createControlSurface(
|
|
|
1627
1722
|
kind: InvocationKind,
|
|
1628
1723
|
clientRef: string | undefined,
|
|
1629
1724
|
run: (options: {
|
|
1725
|
+
sdkRunToken: string;
|
|
1630
1726
|
onPreflightAccepted: () => void;
|
|
1631
1727
|
onPreflightAcceptCommit: () => Promise<void>;
|
|
1632
1728
|
/** Internal disposition before a queued submission is actually consumed. */
|
|
@@ -1646,6 +1742,7 @@ function createControlSurface(
|
|
|
1646
1742
|
const retainedClientRef = normalizeClientRef(clientRef);
|
|
1647
1743
|
reconciliation.admit(kind, retainedClientRef);
|
|
1648
1744
|
const correlation = newCorrelation();
|
|
1745
|
+
const sdkRunToken = `${correlation.commandId}:${correlation.turnId}`;
|
|
1649
1746
|
const preflight = Promise.withResolvers<void>();
|
|
1650
1747
|
let accepted = false;
|
|
1651
1748
|
let settled = false;
|
|
@@ -1672,7 +1769,7 @@ function createControlSurface(
|
|
|
1672
1769
|
// The accepted submission does NOT own the active turn until its run
|
|
1673
1770
|
// actually STARTS: the connection is carried on the pending entry and
|
|
1674
1771
|
// associated at agent_start instead (review thread P1).
|
|
1675
|
-
onAccepted(kind, correlation, requesterConnectionId, startsOwnTurn);
|
|
1772
|
+
onAccepted(kind, correlation, requesterConnectionId, startsOwnTurn, sdkRunToken);
|
|
1676
1773
|
preflight.resolve();
|
|
1677
1774
|
} catch (error) {
|
|
1678
1775
|
settled = true;
|
|
@@ -1704,6 +1801,7 @@ function createControlSurface(
|
|
|
1704
1801
|
const submission = Promise.resolve(
|
|
1705
1802
|
run({
|
|
1706
1803
|
onPreflightAccepted: () => void accept().catch(() => undefined),
|
|
1804
|
+
sdkRunToken,
|
|
1707
1805
|
onPreflightAcceptCommit: accept,
|
|
1708
1806
|
onDispatchDisposition: promotion => {
|
|
1709
1807
|
promotionStartsOwnRun = promotion.startsOwnRun;
|
|
@@ -1724,7 +1822,7 @@ function createControlSurface(
|
|
|
1724
1822
|
// terminal-abort that turn (review threads P1/P2).
|
|
1725
1823
|
onQueuedPromoted: (promotion?: { startsOwnRun?: boolean; removed?: boolean }) => {
|
|
1726
1824
|
promotionStartsOwnRun = promotion?.startsOwnRun;
|
|
1727
|
-
onPromotedTurn?.(kind, correlation, requesterConnectionId, promotion);
|
|
1825
|
+
onPromotedTurn?.(kind, correlation, requesterConnectionId, sdkRunToken, promotion);
|
|
1728
1826
|
},
|
|
1729
1827
|
queuedAtDispatch,
|
|
1730
1828
|
}),
|
|
@@ -1768,7 +1866,7 @@ function createControlSurface(
|
|
|
1768
1866
|
});
|
|
1769
1867
|
retirePendingOwner?.(kind, correlation, "always");
|
|
1770
1868
|
} catch (transitionError) {
|
|
1771
|
-
if (kind === "prompt") {
|
|
1869
|
+
if (kind === "prompt" || kind === "skill") {
|
|
1772
1870
|
retirePendingOwner?.(kind, correlation, "recover-terminal");
|
|
1773
1871
|
return;
|
|
1774
1872
|
}
|
|
@@ -1830,7 +1928,7 @@ function createControlSurface(
|
|
|
1830
1928
|
try {
|
|
1831
1929
|
await reconciliation.noteTransition(kind, correlation, { type: "agent_failed", error });
|
|
1832
1930
|
} catch {
|
|
1833
|
-
if (kind === "prompt") {
|
|
1931
|
+
if (kind === "prompt" || kind === "skill") {
|
|
1834
1932
|
retirePendingOwner?.(
|
|
1835
1933
|
kind,
|
|
1836
1934
|
correlation,
|
|
@@ -1863,7 +1961,7 @@ function createControlSurface(
|
|
|
1863
1961
|
try {
|
|
1864
1962
|
await reconciliation.noteTransition(kind, correlation, { type: "agent_end" });
|
|
1865
1963
|
} catch (transitionError) {
|
|
1866
|
-
if (kind === "prompt") {
|
|
1964
|
+
if (kind === "prompt" || kind === "skill") {
|
|
1867
1965
|
retirePendingOwner?.(
|
|
1868
1966
|
kind,
|
|
1869
1967
|
correlation,
|
|
@@ -2784,10 +2882,14 @@ function createControlSurface(
|
|
|
2784
2882
|
};
|
|
2785
2883
|
return {
|
|
2786
2884
|
prompt: async (text, images, clientRef) =>
|
|
2787
|
-
submit("prompt", clientRef, ({ queuedAtDispatch, ...options }) =>
|
|
2885
|
+
submit("prompt", clientRef, ({ queuedAtDispatch, sdkRunToken, ...options }) =>
|
|
2788
2886
|
api.sendUserMessage(
|
|
2789
2887
|
typeof images === "undefined" ? text : ([{ type: "text", text }, ...(images as never[])] as never),
|
|
2790
|
-
|
|
2888
|
+
{
|
|
2889
|
+
...options,
|
|
2890
|
+
sdkRunToken,
|
|
2891
|
+
...(queuedAtDispatch ? { queuedAtDispatch: true } : {}),
|
|
2892
|
+
},
|
|
2791
2893
|
),
|
|
2792
2894
|
),
|
|
2793
2895
|
steer: async (text, clientRef) => {
|
|
@@ -3033,10 +3135,28 @@ export function registerSdkOnlyNotificationCommand(api: ExtensionAPI): void {
|
|
|
3033
3135
|
});
|
|
3034
3136
|
}
|
|
3035
3137
|
|
|
3138
|
+
function quiescingFrame(frame: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
3139
|
+
const error = { code: "session_quiescing", message: "The session endpoint is being replaced." };
|
|
3140
|
+
const id = typeof frame.id === "string" ? frame.id : "";
|
|
3141
|
+
if (frame.type === "control_request") return { type: "control_response", id, ok: false, error };
|
|
3142
|
+
if (frame.type === "query_request") return { type: "query_response", id, ok: false, error };
|
|
3143
|
+
if (frame.type === "event_replay") return { type: "event_replay_result", id, ok: false, error };
|
|
3144
|
+
if (frame.type === "session_activate")
|
|
3145
|
+
return { type: "session_activate_result", id, ok: false, status: "authority_unavailable", error };
|
|
3146
|
+
if (frame.type === "register_provider") return { type: "register_provider_result", id, ok: false, error };
|
|
3147
|
+
if (frame.type === "reverse_response")
|
|
3148
|
+
return { type: "transport_error", id, code: error.code, message: error.message };
|
|
3149
|
+
if (frame.type === "provider_heartbeat" || frame.type === "lease_release")
|
|
3150
|
+
return { type: "transport_error", code: "session_quiescing", message: error.message };
|
|
3151
|
+
return undefined;
|
|
3152
|
+
}
|
|
3153
|
+
|
|
3036
3154
|
/** Install a complete SDK host for a session when notifications are inactive. */
|
|
3037
3155
|
export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: CreateSdkSessionRuntimeOptions): void {
|
|
3038
3156
|
let active:
|
|
3039
3157
|
| {
|
|
3158
|
+
sessionId: string;
|
|
3159
|
+
sessionIdentity: string;
|
|
3040
3160
|
runtime: SessionSdkSessionRuntime;
|
|
3041
3161
|
revisions: RevisionStore;
|
|
3042
3162
|
cursors: CursorRegistry;
|
|
@@ -3047,8 +3167,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3047
3167
|
kind: InvocationKind;
|
|
3048
3168
|
correlation: InvocationCorrelation;
|
|
3049
3169
|
connectionId: string | undefined;
|
|
3170
|
+
sdkRunToken: string;
|
|
3050
3171
|
}>;
|
|
3051
3172
|
registerBroker: () => Promise<void>;
|
|
3173
|
+
quiesceInput: () => void;
|
|
3052
3174
|
fenceGateResolutions: () => void;
|
|
3053
3175
|
waitForGateResolutionQuiescence: () => Promise<void>;
|
|
3054
3176
|
activeInvocation?: { kind: InvocationKind; correlation: InvocationCorrelation };
|
|
@@ -3060,18 +3182,163 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3060
3182
|
kind: InvocationKind;
|
|
3061
3183
|
correlation: InvocationCorrelation;
|
|
3062
3184
|
connectionId: string | undefined;
|
|
3185
|
+
sdkRunToken: string;
|
|
3063
3186
|
}>;
|
|
3064
3187
|
attachedInvocations: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
|
|
3065
3188
|
}>;
|
|
3066
3189
|
disposeGate?: () => void;
|
|
3067
3190
|
lifecycleActive: boolean;
|
|
3068
3191
|
lifecycleEpoch: number;
|
|
3192
|
+
failureDiagnosticKeys: Set<string>;
|
|
3193
|
+
failureDiagnosticCodes: Map<string, string>;
|
|
3194
|
+
lifecycleTasks: Set<Promise<void>>;
|
|
3069
3195
|
/** Failure reasons whose durable agent_failed write failed; the
|
|
3070
3196
|
* subsequent agent_end must re-record them before terminalizing or
|
|
3071
3197
|
* the record classifies terminal_ok (exact-head review P1). */
|
|
3072
3198
|
unrecordedFailureReasons?: Map<string, { code: string; message: string }>;
|
|
3073
3199
|
}
|
|
3074
3200
|
| undefined;
|
|
3201
|
+
type RuntimeState = NonNullable<typeof active>;
|
|
3202
|
+
type LifecycleBatch = RuntimeState["openLifecycleBatches"][number];
|
|
3203
|
+
type LifecycleOwner = { state: RuntimeState; sessionId: string; batch?: LifecycleBatch };
|
|
3204
|
+
const retiredLifecycleOwners = new Map<string, RuntimeState[]>();
|
|
3205
|
+
const retiredLifecycleOwnerTimers = new Map<RuntimeState, ReturnType<typeof setTimeout>>();
|
|
3206
|
+
const skillRecoveryControllers = new Map<string, AbortController>();
|
|
3207
|
+
const skillTerminalRecoveryControllers = new Map<string, AbortController>();
|
|
3208
|
+
const ambiguousLifecycleIdentities = new Set<string>();
|
|
3209
|
+
const lifecycleRunOwners = new Map<
|
|
3210
|
+
string,
|
|
3211
|
+
{ state: RuntimeState; batch?: LifecycleBatch; correlationKey?: string }
|
|
3212
|
+
>();
|
|
3213
|
+
const lifecycleCorrelationKey = (correlation: InvocationCorrelation): string =>
|
|
3214
|
+
`${correlation.commandId}:${correlation.turnId}`;
|
|
3215
|
+
const sessionIdentityForContext = (ctx: ExtensionContext): string | undefined => {
|
|
3216
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
3217
|
+
const sessionFile = ctx.sessionManager.getSessionFile?.();
|
|
3218
|
+
if (sessionFile) return `${sessionId}\u0000${sessionFile}`;
|
|
3219
|
+
const stateRoot = path.join(ctx.cwd, ".gjc", "state");
|
|
3220
|
+
return `${sessionId}\u0000${resolveReconciliationSessionFile(undefined, stateRoot, sessionId)}`;
|
|
3221
|
+
};
|
|
3222
|
+
const lifecycleStateForContext = (
|
|
3223
|
+
ctx: ExtensionContext,
|
|
3224
|
+
type: "agent_start" | "agent_end" | "agent_failed" = "agent_start",
|
|
3225
|
+
): RuntimeState | undefined => {
|
|
3226
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
3227
|
+
const sessionIdentity = sessionIdentityForContext(ctx);
|
|
3228
|
+
if (!sessionIdentity) return undefined;
|
|
3229
|
+
const retired = (retiredLifecycleOwners.get(sessionId) ?? []).filter(
|
|
3230
|
+
owner => owner.sessionIdentity === sessionIdentity,
|
|
3231
|
+
);
|
|
3232
|
+
if (type !== "agent_start") {
|
|
3233
|
+
if (ambiguousLifecycleIdentities.has(sessionIdentity)) return undefined;
|
|
3234
|
+
if (retired.length > 1) {
|
|
3235
|
+
ambiguousLifecycleIdentities.add(sessionIdentity);
|
|
3236
|
+
return undefined;
|
|
3237
|
+
}
|
|
3238
|
+
const delayedOwner = retired.find(owner => owner.openLifecycleBatches.length > 0);
|
|
3239
|
+
if (delayedOwner) {
|
|
3240
|
+
if (active?.sessionIdentity === sessionIdentity && active.lifecycleActive) {
|
|
3241
|
+
ambiguousLifecycleIdentities.add(sessionIdentity);
|
|
3242
|
+
return undefined;
|
|
3243
|
+
}
|
|
3244
|
+
return delayedOwner;
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
if (active?.sessionIdentity === sessionIdentity) return active;
|
|
3248
|
+
return retired[0];
|
|
3249
|
+
};
|
|
3250
|
+
const lifecycleStateForEvent = (
|
|
3251
|
+
ctx: ExtensionContext,
|
|
3252
|
+
type: "agent_start" | "agent_end" | "agent_failed",
|
|
3253
|
+
sdkRunToken: unknown,
|
|
3254
|
+
): RuntimeState | undefined =>
|
|
3255
|
+
typeof sdkRunToken === "string" && sdkRunToken.length > 0
|
|
3256
|
+
? lifecycleRunOwners.get(sdkRunToken)?.state
|
|
3257
|
+
: lifecycleStateForContext(ctx, type);
|
|
3258
|
+
const removeRetiredLifecycleOwner = (owner: RuntimeState): void => {
|
|
3259
|
+
const timer = retiredLifecycleOwnerTimers.get(owner);
|
|
3260
|
+
if (timer !== undefined) {
|
|
3261
|
+
clearTimeout(timer);
|
|
3262
|
+
retiredLifecycleOwnerTimers.delete(owner);
|
|
3263
|
+
}
|
|
3264
|
+
const owners = retiredLifecycleOwners.get(owner.sessionId);
|
|
3265
|
+
if (!owners) {
|
|
3266
|
+
ambiguousLifecycleIdentities.delete(owner.sessionIdentity);
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
const remaining = owners.filter(candidate => candidate !== owner);
|
|
3270
|
+
if (remaining.length === 0) retiredLifecycleOwners.delete(owner.sessionId);
|
|
3271
|
+
else retiredLifecycleOwners.set(owner.sessionId, remaining);
|
|
3272
|
+
const remainingSameIdentity = remaining.filter(candidate => candidate.sessionIdentity === owner.sessionIdentity);
|
|
3273
|
+
if (remainingSameIdentity.length < 2) ambiguousLifecycleIdentities.delete(owner.sessionIdentity);
|
|
3274
|
+
// Expiry is the final lifecycle-reference boundary. Do not retain
|
|
3275
|
+
// diagnostic suppression or failed-write recovery state after the owner is
|
|
3276
|
+
// no longer eligible to receive a delayed event.
|
|
3277
|
+
owner.failureDiagnosticKeys.clear();
|
|
3278
|
+
owner.failureDiagnosticCodes.clear();
|
|
3279
|
+
owner.unrecordedFailureReasons?.clear();
|
|
3280
|
+
for (const [token, binding] of lifecycleRunOwners) if (binding.state === owner) lifecycleRunOwners.delete(token);
|
|
3281
|
+
};
|
|
3282
|
+
const maybeRetireLifecycleOwner = (owner: RuntimeState): void => {
|
|
3283
|
+
if (
|
|
3284
|
+
owner.pending.length > 0 ||
|
|
3285
|
+
owner.openLifecycleBatches.length > 0 ||
|
|
3286
|
+
(owner.attachedInvocations?.length ?? 0) > 0 ||
|
|
3287
|
+
(owner.drainedInvocations?.length ?? 0) > 0 ||
|
|
3288
|
+
owner.lifecycleTasks.size > 0
|
|
3289
|
+
)
|
|
3290
|
+
return;
|
|
3291
|
+
removeRetiredLifecycleOwner(owner);
|
|
3292
|
+
};
|
|
3293
|
+
const removeLifecycleTokenAliases = (owner: RuntimeState, correlation: InvocationCorrelation): void => {
|
|
3294
|
+
const target = lifecycleCorrelationKey(correlation);
|
|
3295
|
+
for (const [token, binding] of lifecycleRunOwners) {
|
|
3296
|
+
if (binding.state === owner && binding.correlationKey === target) lifecycleRunOwners.delete(token);
|
|
3297
|
+
}
|
|
3298
|
+
};
|
|
3299
|
+
const removeLifecycleReferences = (owner: RuntimeState, correlation: InvocationCorrelation): void => {
|
|
3300
|
+
const target = lifecycleCorrelationKey(correlation);
|
|
3301
|
+
const retainedPending = owner.pending.filter(entry => lifecycleCorrelationKey(entry.correlation) !== target);
|
|
3302
|
+
owner.pending.splice(0, owner.pending.length, ...retainedPending);
|
|
3303
|
+
for (const batch of owner.openLifecycleBatches) {
|
|
3304
|
+
batch.invocations = batch.invocations.filter(entry => lifecycleCorrelationKey(entry.correlation) !== target);
|
|
3305
|
+
batch.attachedInvocations = batch.attachedInvocations.filter(
|
|
3306
|
+
entry => lifecycleCorrelationKey(entry.correlation) !== target,
|
|
3307
|
+
);
|
|
3308
|
+
}
|
|
3309
|
+
owner.openLifecycleBatches = owner.openLifecycleBatches.filter(
|
|
3310
|
+
batch => batch.invocations.length > 0 || batch.attachedInvocations.length > 0,
|
|
3311
|
+
);
|
|
3312
|
+
owner.drainedInvocations = owner.drainedInvocations?.filter(
|
|
3313
|
+
entry => lifecycleCorrelationKey(entry.correlation) !== target,
|
|
3314
|
+
);
|
|
3315
|
+
owner.attachedInvocations = owner.attachedInvocations?.filter(
|
|
3316
|
+
entry => lifecycleCorrelationKey(entry.correlation) !== target,
|
|
3317
|
+
);
|
|
3318
|
+
if (owner.activeInvocation && lifecycleCorrelationKey(owner.activeInvocation.correlation) === target) {
|
|
3319
|
+
const replacement =
|
|
3320
|
+
owner.openLifecycleBatches.flatMap(batch => batch.invocations)[0] ?? owner.attachedInvocations?.[0];
|
|
3321
|
+
owner.activeInvocation = replacement
|
|
3322
|
+
? { kind: replacement.kind, correlation: replacement.correlation }
|
|
3323
|
+
: undefined;
|
|
3324
|
+
}
|
|
3325
|
+
removeLifecycleTokenAliases(owner, correlation);
|
|
3326
|
+
if (owner === active) {
|
|
3327
|
+
const owners = new Set<string>();
|
|
3328
|
+
const activeBatch = owner.activeInvocation
|
|
3329
|
+
? owner.openLifecycleBatches.find(batch =>
|
|
3330
|
+
batch.invocations.some(
|
|
3331
|
+
entry =>
|
|
3332
|
+
lifecycleCorrelationKey(entry.correlation) ===
|
|
3333
|
+
lifecycleCorrelationKey(owner.activeInvocation!.correlation),
|
|
3334
|
+
),
|
|
3335
|
+
)
|
|
3336
|
+
: undefined;
|
|
3337
|
+
for (const entry of activeBatch?.invocations ?? [])
|
|
3338
|
+
if (entry.connectionId !== undefined) owners.add(entry.connectionId);
|
|
3339
|
+
activePromptOwnerHolder.connectionIds = owners.size > 0 ? owners : undefined;
|
|
3340
|
+
}
|
|
3341
|
+
};
|
|
3075
3342
|
// Shared with the control surface's terminal abort: the correlated
|
|
3076
3343
|
// agent_end publication capture (AC 19). terminalAbort installs a fresh
|
|
3077
3344
|
// resolver before settling the abort; emitLifecycle resolves it with the
|
|
@@ -3089,15 +3356,28 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3089
3356
|
// turn it did not submit, and never set for agent-initiated turns (review
|
|
3090
3357
|
// thread P1).
|
|
3091
3358
|
const activePromptOwnerHolder: { connectionIds?: Set<string>; lifecycleEpoch?: number } = {};
|
|
3359
|
+
let nextLifecycleEpoch = 0;
|
|
3092
3360
|
const skillTerminalRecoveryKeys = new Set<string>();
|
|
3361
|
+
const trackLifecycle = (handler: () => Promise<void>, owner: RuntimeState | undefined): Promise<void> => {
|
|
3362
|
+
if (!owner) return Promise.resolve();
|
|
3363
|
+
let task: Promise<void>;
|
|
3364
|
+
task = handler().finally(() => {
|
|
3365
|
+
owner.lifecycleTasks.delete(task);
|
|
3366
|
+
maybeRetireLifecycleOwner(owner);
|
|
3367
|
+
});
|
|
3368
|
+
owner.lifecycleTasks.add(task);
|
|
3369
|
+
return task;
|
|
3370
|
+
};
|
|
3093
3371
|
const emitLifecycle = async (
|
|
3094
3372
|
type: "agent_start" | "agent_end" | "agent_failed",
|
|
3095
3373
|
ctx: ExtensionContext,
|
|
3096
3374
|
failureCause?: unknown,
|
|
3097
3375
|
maintenanceOutcome?: string,
|
|
3376
|
+
lifecycleOwner?: LifecycleOwner,
|
|
3098
3377
|
): Promise<void> => {
|
|
3099
|
-
const current = active;
|
|
3378
|
+
const current = lifecycleOwner?.state ?? active;
|
|
3100
3379
|
if (!current) return;
|
|
3380
|
+
const failureBatch = lifecycleOwner?.batch;
|
|
3101
3381
|
const adoptLifecycleBatch = (
|
|
3102
3382
|
batch:
|
|
3103
3383
|
| Array<{
|
|
@@ -3111,7 +3391,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3111
3391
|
current.activeInvocation = undefined;
|
|
3112
3392
|
current.drainedInvocations = undefined;
|
|
3113
3393
|
current.attachedInvocations = undefined;
|
|
3114
|
-
activePromptOwnerHolder.connectionIds = undefined;
|
|
3394
|
+
if (current === active) activePromptOwnerHolder.connectionIds = undefined;
|
|
3115
3395
|
return;
|
|
3116
3396
|
}
|
|
3117
3397
|
current.activeInvocation = batch[0];
|
|
@@ -3119,19 +3399,20 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3119
3399
|
current.attachedInvocations = undefined;
|
|
3120
3400
|
const owners = new Set<string>();
|
|
3121
3401
|
for (const entry of batch) if (entry.connectionId !== undefined) owners.add(entry.connectionId);
|
|
3122
|
-
activePromptOwnerHolder.connectionIds = owners;
|
|
3402
|
+
if (current === active) activePromptOwnerHolder.connectionIds = owners;
|
|
3123
3403
|
};
|
|
3124
3404
|
let transitions: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }> = [];
|
|
3125
3405
|
if (type === "agent_failed") {
|
|
3126
|
-
const activeInvocation = current.activeInvocation;
|
|
3406
|
+
const activeInvocation = failureBatch?.invocations[0] ?? current.activeInvocation;
|
|
3127
3407
|
const activeBatch = activeInvocation
|
|
3128
|
-
?
|
|
3408
|
+
? (failureBatch ??
|
|
3409
|
+
current.openLifecycleBatches.find(batch =>
|
|
3129
3410
|
batch.invocations.some(
|
|
3130
3411
|
entry =>
|
|
3131
3412
|
entry.correlation.commandId === activeInvocation.correlation.commandId &&
|
|
3132
3413
|
entry.correlation.turnId === activeInvocation.correlation.turnId,
|
|
3133
3414
|
),
|
|
3134
|
-
)
|
|
3415
|
+
))
|
|
3135
3416
|
: undefined;
|
|
3136
3417
|
const fallback =
|
|
3137
3418
|
activeBatch?.invocations ??
|
|
@@ -3152,8 +3433,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3152
3433
|
);
|
|
3153
3434
|
transitions = [...fallback, ...attached].map(({ kind, correlation }) => ({ kind, correlation }));
|
|
3154
3435
|
} else if (type === "agent_start") {
|
|
3155
|
-
current.lifecycleEpoch
|
|
3156
|
-
activePromptOwnerHolder.lifecycleEpoch = current.lifecycleEpoch;
|
|
3436
|
+
current.lifecycleEpoch = ++nextLifecycleEpoch;
|
|
3437
|
+
if (current === active) activePromptOwnerHolder.lifecycleEpoch = current.lifecycleEpoch;
|
|
3157
3438
|
// Mark lifecycle active even when the drain is empty: a monitor/cron
|
|
3158
3439
|
// run started by the session has no SDK pending entry but is still a
|
|
3159
3440
|
// real active run that later in-run promotions must attach to instead
|
|
@@ -3211,8 +3492,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3211
3492
|
// may already have advanced to. Publication proof and race retirement must
|
|
3212
3493
|
// use the same batch identity as reconciliation.
|
|
3213
3494
|
const eventLifecycleEpoch =
|
|
3214
|
-
type === "agent_end"
|
|
3215
|
-
?
|
|
3495
|
+
(type === "agent_failed" || type === "agent_end") && failureBatch
|
|
3496
|
+
? failureBatch.epoch
|
|
3216
3497
|
: current.lifecycleEpoch;
|
|
3217
3498
|
const resolveTerminalPublicationWaiters = (observed: boolean): void => {
|
|
3218
3499
|
const waiters = terminalPublicationCapture.waiters;
|
|
@@ -3223,17 +3504,19 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3223
3504
|
for (const waiter of matched) waiter.resolve(observed);
|
|
3224
3505
|
};
|
|
3225
3506
|
const retireEndedLifecycleBatch = (): void => {
|
|
3226
|
-
const ended = current.openLifecycleBatches[0];
|
|
3507
|
+
const ended = failureBatch ?? current.openLifecycleBatches[0];
|
|
3227
3508
|
if (!ended) return;
|
|
3228
3509
|
const transitionKeys = new Set(
|
|
3229
3510
|
transitions.map(({ correlation }) => `${correlation.commandId}:${correlation.turnId}`),
|
|
3230
3511
|
);
|
|
3231
3512
|
if (
|
|
3232
|
-
ended.invocations.some(({ correlation }) =>
|
|
3513
|
+
!ended.invocations.some(({ correlation }) =>
|
|
3233
3514
|
transitionKeys.has(`${correlation.commandId}:${correlation.turnId}`),
|
|
3234
3515
|
)
|
|
3235
3516
|
)
|
|
3236
|
-
|
|
3517
|
+
return;
|
|
3518
|
+
const index = current.openLifecycleBatches.indexOf(ended);
|
|
3519
|
+
if (index >= 0) current.openLifecycleBatches.splice(index, 1);
|
|
3237
3520
|
};
|
|
3238
3521
|
const scheduleSkillTerminalRecovery = (invocation: {
|
|
3239
3522
|
kind: InvocationKind;
|
|
@@ -3243,6 +3526,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3243
3526
|
const recoveryKey = `${invocation.correlation.commandId}:${invocation.correlation.turnId}`;
|
|
3244
3527
|
if (skillTerminalRecoveryKeys.has(recoveryKey)) return;
|
|
3245
3528
|
skillTerminalRecoveryKeys.add(recoveryKey);
|
|
3529
|
+
const controller = new AbortController();
|
|
3530
|
+
skillTerminalRecoveryControllers.set(recoveryKey, controller);
|
|
3246
3531
|
const attempt = async (): Promise<void> => {
|
|
3247
3532
|
try {
|
|
3248
3533
|
const unrecorded = current.unrecordedFailureReasons?.get(recoveryKey);
|
|
@@ -3255,6 +3540,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3255
3540
|
}
|
|
3256
3541
|
await current.reconciliation.noteTransition("skill", invocation.correlation, { type: "agent_end" });
|
|
3257
3542
|
skillTerminalRecoveryKeys.delete(recoveryKey);
|
|
3543
|
+
current.failureDiagnosticKeys.delete(recoveryKey);
|
|
3258
3544
|
for (const batch of current.openLifecycleBatches) {
|
|
3259
3545
|
batch.invocations = batch.invocations.filter(
|
|
3260
3546
|
entry =>
|
|
@@ -3277,16 +3563,33 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3277
3563
|
adoptLifecycleBatch(current.openLifecycleBatches[0]?.invocations);
|
|
3278
3564
|
if (current.openLifecycleBatches.length === 0) current.lifecycleActive = false;
|
|
3279
3565
|
} catch (error) {
|
|
3566
|
+
if (controller.signal.aborted) {
|
|
3567
|
+
skillTerminalRecoveryKeys.delete(recoveryKey);
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3280
3570
|
logger.error("SDK skill lifecycle terminal recovery retrying", {
|
|
3281
3571
|
commandId: invocation.correlation.commandId,
|
|
3282
3572
|
turnId: invocation.correlation.turnId,
|
|
3283
3573
|
error: sanitizePromptFailure(error),
|
|
3284
3574
|
});
|
|
3285
|
-
|
|
3575
|
+
const wait = Promise.withResolvers<void>();
|
|
3576
|
+
const timer = setTimeout(wait.resolve, 1_000);
|
|
3577
|
+
timer.unref();
|
|
3578
|
+
const onAbort = () => {
|
|
3579
|
+
clearTimeout(timer);
|
|
3580
|
+
wait.resolve();
|
|
3581
|
+
};
|
|
3582
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
3583
|
+
await wait.promise;
|
|
3584
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
3585
|
+
if (controller.signal.aborted) {
|
|
3586
|
+
skillTerminalRecoveryKeys.delete(recoveryKey);
|
|
3587
|
+
return;
|
|
3588
|
+
}
|
|
3286
3589
|
return attempt();
|
|
3287
3590
|
}
|
|
3288
3591
|
};
|
|
3289
|
-
void attempt();
|
|
3592
|
+
void attempt().finally(() => skillTerminalRecoveryControllers.delete(recoveryKey));
|
|
3290
3593
|
};
|
|
3291
3594
|
if (type === "agent_end" && maintenanceOutcome !== undefined && maintenanceOutcome !== "aborted") {
|
|
3292
3595
|
try {
|
|
@@ -3296,6 +3599,20 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3296
3599
|
}
|
|
3297
3600
|
return;
|
|
3298
3601
|
}
|
|
3602
|
+
const correlationKey = (correlation: InvocationCorrelation): string =>
|
|
3603
|
+
`${correlation.commandId}:${correlation.turnId}`;
|
|
3604
|
+
if (type === "agent_failed") {
|
|
3605
|
+
transitions = transitions.filter(
|
|
3606
|
+
invocation => !current.failureDiagnosticKeys.has(correlationKey(invocation.correlation)),
|
|
3607
|
+
);
|
|
3608
|
+
const diagnosticCode = sanitizePromptFailure(
|
|
3609
|
+
failureCause ?? Object.assign(new Error("agent run failed"), { code: "agent_failed" }),
|
|
3610
|
+
).code;
|
|
3611
|
+
for (const invocation of transitions)
|
|
3612
|
+
current.failureDiagnosticKeys.add(correlationKey(invocation.correlation));
|
|
3613
|
+
for (const invocation of transitions)
|
|
3614
|
+
current.failureDiagnosticCodes.set(correlationKey(invocation.correlation), diagnosticCode);
|
|
3615
|
+
}
|
|
3299
3616
|
// Observe whether the lifecycle publication actually landed: a terminal
|
|
3300
3617
|
// abort awaits this result so its durable row only claims
|
|
3301
3618
|
// terminalPublished when the correlated agent_end event reached the
|
|
@@ -3356,7 +3673,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3356
3673
|
}
|
|
3357
3674
|
}
|
|
3358
3675
|
}
|
|
3359
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
3676
|
+
const sessionId = lifecycleOwner?.sessionId ?? ctx.sessionManager.getSessionId();
|
|
3360
3677
|
if (type === "agent_failed") {
|
|
3361
3678
|
// Failure is a correlated diagnostic, not the terminal lifecycle boundary.
|
|
3362
3679
|
// Publish one safe frame per invocation so clients can attribute a shared
|
|
@@ -3395,6 +3712,16 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3395
3712
|
// and the next agent_end is paired with stale ownership.
|
|
3396
3713
|
for (const invocation of failedTransitions) scheduleSkillTerminalRecovery(invocation);
|
|
3397
3714
|
retireEndedLifecycleBatch();
|
|
3715
|
+
const failedKeys = new Set(failedTransitions.map(({ correlation }) => correlationKey(correlation)));
|
|
3716
|
+
for (const invocation of transitions) {
|
|
3717
|
+
if (!failedKeys.has(correlationKey(invocation.correlation)))
|
|
3718
|
+
removeLifecycleReferences(current, invocation.correlation);
|
|
3719
|
+
if (!failedKeys.has(correlationKey(invocation.correlation)))
|
|
3720
|
+
current.failureDiagnosticKeys.delete(correlationKey(invocation.correlation));
|
|
3721
|
+
if (!failedKeys.has(correlationKey(invocation.correlation)))
|
|
3722
|
+
current.failureDiagnosticCodes.delete(correlationKey(invocation.correlation));
|
|
3723
|
+
}
|
|
3724
|
+
options.onFailureDiagnosticKeyCountForTests?.(current.failureDiagnosticKeys.size);
|
|
3398
3725
|
resolveTerminalPublicationWaiters(observed);
|
|
3399
3726
|
return;
|
|
3400
3727
|
}
|
|
@@ -3410,7 +3737,17 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3410
3737
|
resolveTerminalPublicationWaiters(observed);
|
|
3411
3738
|
return;
|
|
3412
3739
|
}
|
|
3413
|
-
|
|
3740
|
+
const ended = failureBatch ?? current.openLifecycleBatches[0];
|
|
3741
|
+
if (ended) {
|
|
3742
|
+
const index = current.openLifecycleBatches.indexOf(ended);
|
|
3743
|
+
if (index >= 0) current.openLifecycleBatches.splice(index, 1);
|
|
3744
|
+
}
|
|
3745
|
+
for (const invocation of transitions) removeLifecycleReferences(current, invocation.correlation);
|
|
3746
|
+
for (const invocation of transitions)
|
|
3747
|
+
current.failureDiagnosticKeys.delete(correlationKey(invocation.correlation));
|
|
3748
|
+
for (const invocation of transitions)
|
|
3749
|
+
current.failureDiagnosticCodes.delete(correlationKey(invocation.correlation));
|
|
3750
|
+
options.onFailureDiagnosticKeyCountForTests?.(current.failureDiagnosticKeys.size);
|
|
3414
3751
|
for (const invocation of transitions)
|
|
3415
3752
|
if (invocation.kind === "prompt") current.deadlineManager.clear(invocation.correlation);
|
|
3416
3753
|
adoptLifecycleBatch(current.openLifecycleBatches[0]?.invocations);
|
|
@@ -3422,33 +3759,122 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3422
3759
|
resolveTerminalPublicationWaiters(observed);
|
|
3423
3760
|
}
|
|
3424
3761
|
};
|
|
3425
|
-
api.on("agent_start",
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3762
|
+
api.on("agent_start", (event, ctx) => {
|
|
3763
|
+
const owner = lifecycleStateForEvent(ctx, "agent_start", event.sdkRunToken);
|
|
3764
|
+
return trackLifecycle(async () => {
|
|
3765
|
+
await emitLifecycle(
|
|
3766
|
+
"agent_start",
|
|
3767
|
+
ctx,
|
|
3768
|
+
undefined,
|
|
3769
|
+
undefined,
|
|
3770
|
+
owner ? { state: owner, sessionId: owner.sessionId } : undefined,
|
|
3771
|
+
);
|
|
3772
|
+
if (owner && typeof event.sdkRunToken === "string" && event.sdkRunToken.length > 0) {
|
|
3773
|
+
const batch = owner.openLifecycleBatches.at(-1);
|
|
3774
|
+
const entry = batch?.invocations.find(item => item.sdkRunToken === event.sdkRunToken);
|
|
3775
|
+
const previous = lifecycleRunOwners.get(event.sdkRunToken);
|
|
3776
|
+
lifecycleRunOwners.set(event.sdkRunToken, {
|
|
3777
|
+
state: owner,
|
|
3778
|
+
batch,
|
|
3779
|
+
...(previous?.correlationKey !== undefined
|
|
3780
|
+
? { correlationKey: previous.correlationKey }
|
|
3781
|
+
: entry
|
|
3782
|
+
? { correlationKey: lifecycleCorrelationKey(entry.correlation) }
|
|
3783
|
+
: {}),
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
}, owner);
|
|
3787
|
+
});
|
|
3788
|
+
api.on("agent_end", (event, ctx) => {
|
|
3789
|
+
const tokenBinding =
|
|
3790
|
+
typeof event.sdkRunToken === "string" ? lifecycleRunOwners.get(event.sdkRunToken) : undefined;
|
|
3791
|
+
const owner = tokenBinding?.state ?? lifecycleStateForEvent(ctx, "agent_end", event.sdkRunToken);
|
|
3792
|
+
// Capture the oldest unmatched batch synchronously. A successor may start
|
|
3793
|
+
// while the failed diagnostic persists; that must not retarget the
|
|
3794
|
+
// predecessor's reason or terminal boundary to the successor invocation.
|
|
3795
|
+
const endedBatch = tokenBinding?.batch ?? owner?.openLifecycleBatches[0];
|
|
3796
|
+
const lifecycleOwner = owner
|
|
3797
|
+
? {
|
|
3798
|
+
state: owner,
|
|
3799
|
+
sessionId: owner.sessionId,
|
|
3800
|
+
...(endedBatch ? { batch: endedBatch } : {}),
|
|
3801
|
+
}
|
|
3802
|
+
: undefined;
|
|
3803
|
+
const currentInvocation = endedBatch?.invocations[0] ?? owner?.activeInvocation;
|
|
3804
|
+
const failure = providerFailureFromAgentEnd(event);
|
|
3805
|
+
const failureCandidates = endedBatch
|
|
3806
|
+
? [...endedBatch.invocations, ...endedBatch.attachedInvocations]
|
|
3807
|
+
: currentInvocation
|
|
3808
|
+
? [currentInvocation]
|
|
3809
|
+
: [];
|
|
3810
|
+
const genericFailureKeys = failureCandidates
|
|
3811
|
+
.map(({ correlation }) => lifecycleCorrelationKey(correlation))
|
|
3812
|
+
.filter(key => owner?.failureDiagnosticCodes.get(key) === "agent_failed");
|
|
3813
|
+
const hasExistingFailure = failureCandidates.some(({ correlation }) =>
|
|
3814
|
+
owner?.failureDiagnosticKeys.has(lifecycleCorrelationKey(correlation)),
|
|
3815
|
+
);
|
|
3816
|
+
const failureAlreadyPublished = failure === undefined || (hasExistingFailure && genericFailureKeys.length === 0);
|
|
3817
|
+
return trackLifecycle(async () => {
|
|
3818
|
+
if (failure && !failureAlreadyPublished) {
|
|
3819
|
+
for (const key of genericFailureKeys) owner?.failureDiagnosticKeys.delete(key);
|
|
3820
|
+
await emitLifecycle("agent_failed", ctx, failure, undefined, lifecycleOwner);
|
|
3821
|
+
}
|
|
3429
3822
|
await emitLifecycle(
|
|
3430
3823
|
"agent_end",
|
|
3431
3824
|
ctx,
|
|
3432
3825
|
undefined,
|
|
3433
3826
|
event.stopReason === "maintenance" ? event.maintenanceOutcome : undefined,
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3827
|
+
lifecycleOwner,
|
|
3828
|
+
);
|
|
3829
|
+
}, owner).finally(() => {
|
|
3830
|
+
if (typeof event.sdkRunToken === "string" && lifecycleRunOwners.get(event.sdkRunToken)?.state === owner)
|
|
3831
|
+
lifecycleRunOwners.delete(event.sdkRunToken);
|
|
3832
|
+
});
|
|
3833
|
+
});
|
|
3834
|
+
api.on("agent_failed", (event, ctx) => {
|
|
3835
|
+
const tokenBinding =
|
|
3836
|
+
typeof event.sdkRunToken === "string" ? lifecycleRunOwners.get(event.sdkRunToken) : undefined;
|
|
3837
|
+
const owner = tokenBinding?.state ?? lifecycleStateForEvent(ctx, "agent_failed", event.sdkRunToken);
|
|
3838
|
+
const failedBatch = tokenBinding?.batch ?? owner?.openLifecycleBatches[0];
|
|
3839
|
+
return trackLifecycle(
|
|
3840
|
+
async () =>
|
|
3841
|
+
emitLifecycle(
|
|
3842
|
+
"agent_failed",
|
|
3843
|
+
ctx,
|
|
3844
|
+
event.error,
|
|
3845
|
+
undefined,
|
|
3846
|
+
owner
|
|
3847
|
+
? {
|
|
3848
|
+
state: owner,
|
|
3849
|
+
sessionId: owner.sessionId,
|
|
3850
|
+
...(failedBatch ? { batch: failedBatch } : {}),
|
|
3851
|
+
}
|
|
3852
|
+
: undefined,
|
|
3853
|
+
),
|
|
3854
|
+
owner,
|
|
3855
|
+
);
|
|
3856
|
+
});
|
|
3437
3857
|
api.on("turn_start", async (_event, ctx) => {
|
|
3438
|
-
const current =
|
|
3858
|
+
const current = lifecycleStateForContext(ctx, "agent_start");
|
|
3439
3859
|
if (!current) return;
|
|
3440
3860
|
await current.registerBroker();
|
|
3441
3861
|
current.runtime.emitEvent({ type: "turn_start", sessionId: ctx.sessionManager.getSessionId() });
|
|
3442
3862
|
});
|
|
3443
3863
|
api.on("turn_end", (_event, ctx) =>
|
|
3444
|
-
|
|
3864
|
+
lifecycleStateForContext(ctx, "agent_end")?.runtime.emitEvent({
|
|
3865
|
+
type: "turn_end",
|
|
3866
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
3867
|
+
}),
|
|
3445
3868
|
);
|
|
3446
3869
|
// Tool activity renews the deadline of EVERY prompt correlation attached to
|
|
3447
3870
|
// the active run — the root invocation plus any in-run consumed follow-ups
|
|
3448
3871
|
// sharing it — not only the head, or an attached correlation would
|
|
3449
|
-
// false-fire prompt_deadline_exceeded during a long shared run
|
|
3450
|
-
const renewAttributableProgress = (eventType: string): void => {
|
|
3451
|
-
|
|
3872
|
+
// false-fire prompt_deadline_exceeded during a long shared run.
|
|
3873
|
+
const renewAttributableProgress = (eventType: string, ctx: ExtensionContext): void => {
|
|
3874
|
+
// Tool events do not carry an SDK run token. Prefer the lifecycle-active
|
|
3875
|
+
// runtime for the current session so a retained predecessor cannot make a
|
|
3876
|
+
// live replacement look ambiguous and suppress its lease renewal.
|
|
3877
|
+
const current = lifecycleStateForContext(ctx, "agent_start");
|
|
3452
3878
|
if (!current) return;
|
|
3453
3879
|
// Renew only invocations adopted by the CURRENT run: the live lifecycle
|
|
3454
3880
|
// batch plus in-run attachments. drainedInvocations may still hold
|
|
@@ -3488,12 +3914,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3488
3914
|
}
|
|
3489
3915
|
};
|
|
3490
3916
|
api.on("tool_execution_start", async (_event, ctx) => {
|
|
3491
|
-
renewAttributableProgress("tool_execution_start");
|
|
3492
|
-
void ctx;
|
|
3917
|
+
renewAttributableProgress("tool_execution_start", ctx);
|
|
3493
3918
|
});
|
|
3494
3919
|
api.on("tool_execution_end", async (_event, ctx) => {
|
|
3495
|
-
renewAttributableProgress("tool_execution_end");
|
|
3496
|
-
void ctx;
|
|
3920
|
+
renewAttributableProgress("tool_execution_end", ctx);
|
|
3497
3921
|
});
|
|
3498
3922
|
const errorCode = (error: unknown): string | undefined =>
|
|
3499
3923
|
typeof error === "object" &&
|
|
@@ -3513,6 +3937,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3513
3937
|
const sessionFile =
|
|
3514
3938
|
(typeof ctx.sessionManager.getSessionFile === "function" ? ctx.sessionManager.getSessionFile() : undefined) ??
|
|
3515
3939
|
resolveReconciliationSessionFile(undefined, stateRoot, sessionId);
|
|
3940
|
+
const sessionIdentity = `${sessionId}\u0000${sessionFile}`;
|
|
3516
3941
|
const reconciliationStore =
|
|
3517
3942
|
options.terminalAbortSeams?.getReconciliationStore?.() ??
|
|
3518
3943
|
createReconciliationStore({ sessionFile, sessionId });
|
|
@@ -3534,18 +3959,17 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3534
3959
|
return typeof v === "number" && Number.isFinite(v) ? v : 21_600_000;
|
|
3535
3960
|
},
|
|
3536
3961
|
onExpired: correlation => {
|
|
3537
|
-
const
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
);
|
|
3542
|
-
if (idx >= 0) pending.splice(idx, 1);
|
|
3962
|
+
const owner = lifecycleOwnerHolder.state;
|
|
3963
|
+
if (!owner) return;
|
|
3964
|
+
removeLifecycleReferences(owner, correlation);
|
|
3965
|
+
maybeRetireLifecycleOwner(owner);
|
|
3543
3966
|
},
|
|
3544
3967
|
});
|
|
3545
3968
|
const pending: Array<{
|
|
3546
3969
|
kind: InvocationKind;
|
|
3547
3970
|
correlation: InvocationCorrelation;
|
|
3548
3971
|
connectionId: string | undefined;
|
|
3972
|
+
sdkRunToken: string;
|
|
3549
3973
|
}> = [];
|
|
3550
3974
|
for (const { correlation, acceptedAt, deadlineMaxAt } of reconciliation.listDeadlineRecoveryPendingPrompts())
|
|
3551
3975
|
deadlineManager.recoverPending(correlation, acceptedAt, deadlineMaxAt);
|
|
@@ -3555,6 +3979,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3555
3979
|
kind: InvocationKind;
|
|
3556
3980
|
correlation: InvocationCorrelation;
|
|
3557
3981
|
connectionId: string | undefined;
|
|
3982
|
+
sdkRunToken: string;
|
|
3558
3983
|
}>;
|
|
3559
3984
|
attachedInvocations: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
|
|
3560
3985
|
}> = [];
|
|
@@ -3584,6 +4009,60 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3584
4009
|
settings: options.settings,
|
|
3585
4010
|
});
|
|
3586
4011
|
const queryHandlers = new QueryHandlers(surfaceFactory.query, sessionId, revisions, cursors);
|
|
4012
|
+
const inputGate = { quiescing: false };
|
|
4013
|
+
const lifecycleOwnerHolder: { state?: RuntimeState; quiescing: boolean } = { quiescing: false };
|
|
4014
|
+
const skillRecoveryTasks = new Map<string, Promise<void>>();
|
|
4015
|
+
const scheduleSkillRecovery = (
|
|
4016
|
+
correlation: InvocationCorrelation,
|
|
4017
|
+
failureIntent?: { code: string; message: string },
|
|
4018
|
+
): void => {
|
|
4019
|
+
const key = lifecycleCorrelationKey(correlation);
|
|
4020
|
+
if (skillRecoveryTasks.has(key)) return;
|
|
4021
|
+
const controller = new AbortController();
|
|
4022
|
+
skillRecoveryControllers.set(key, controller);
|
|
4023
|
+
const intent = failureIntent;
|
|
4024
|
+
const task = (async (): Promise<void> => {
|
|
4025
|
+
let failureRecorded = intent === undefined;
|
|
4026
|
+
for (;;) {
|
|
4027
|
+
if (controller.signal.aborted) return;
|
|
4028
|
+
try {
|
|
4029
|
+
if (!failureRecorded) {
|
|
4030
|
+
await reconciliation.noteTransition("skill", correlation, {
|
|
4031
|
+
type: "agent_failed",
|
|
4032
|
+
error: Object.assign(new Error(intent?.message ?? "skill invocation failed"), {
|
|
4033
|
+
code: intent?.code ?? "skill_failed",
|
|
4034
|
+
}),
|
|
4035
|
+
} as never);
|
|
4036
|
+
failureRecorded = true;
|
|
4037
|
+
}
|
|
4038
|
+
await reconciliation.noteTransition("skill", correlation, { type: "agent_end" });
|
|
4039
|
+
return;
|
|
4040
|
+
} catch (error) {
|
|
4041
|
+
if (controller.signal.aborted) return;
|
|
4042
|
+
logger.error("SDK skill lifecycle recovery retrying", {
|
|
4043
|
+
commandId: correlation.commandId,
|
|
4044
|
+
turnId: correlation.turnId,
|
|
4045
|
+
error: sanitizePromptFailure(error),
|
|
4046
|
+
});
|
|
4047
|
+
const wait = Promise.withResolvers<void>();
|
|
4048
|
+
const timer = setTimeout(wait.resolve, 1_000);
|
|
4049
|
+
timer.unref();
|
|
4050
|
+
const onAbort = () => {
|
|
4051
|
+
clearTimeout(timer);
|
|
4052
|
+
wait.resolve();
|
|
4053
|
+
};
|
|
4054
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
4055
|
+
await wait.promise;
|
|
4056
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
4057
|
+
}
|
|
4058
|
+
}
|
|
4059
|
+
})();
|
|
4060
|
+
skillRecoveryTasks.set(key, task);
|
|
4061
|
+
void task.finally(() => {
|
|
4062
|
+
skillRecoveryTasks.delete(key);
|
|
4063
|
+
skillRecoveryControllers.delete(key);
|
|
4064
|
+
});
|
|
4065
|
+
};
|
|
3587
4066
|
let runtime: SessionSdkSessionRuntime;
|
|
3588
4067
|
// Durable-first bounded terminalization for accepted submissions that leave
|
|
3589
4068
|
// their queue or race a run WITHOUT consumption (exact-head review: clearing
|
|
@@ -3597,6 +4076,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3597
4076
|
correlation: InvocationCorrelation,
|
|
3598
4077
|
error: { code: string; message: string },
|
|
3599
4078
|
): void => {
|
|
4079
|
+
if (lifecycleOwnerHolder.state) removeLifecycleTokenAliases(lifecycleOwnerHolder.state, correlation);
|
|
3600
4080
|
const attempt = async (remaining: number): Promise<void> => {
|
|
3601
4081
|
try {
|
|
3602
4082
|
// 1. Record the failure reason durably FIRST. If this write fails,
|
|
@@ -3608,10 +4088,12 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3608
4088
|
} as never);
|
|
3609
4089
|
} catch (reasonError) {
|
|
3610
4090
|
if (remaining <= 1) {
|
|
3611
|
-
//
|
|
3612
|
-
//
|
|
3613
|
-
|
|
3614
|
-
|
|
4091
|
+
// Prompts use the deadline lease; skills have no prompt lease and
|
|
4092
|
+
// must use their kind-aware durable retry owner instead.
|
|
4093
|
+
if (kind === "skill")
|
|
4094
|
+
scheduleSkillRecovery(correlation, { code: error.code, message: error.message });
|
|
4095
|
+
else
|
|
4096
|
+
deadlineManager.noteTerminalTransition(correlation, { code: error.code, message: error.message });
|
|
3615
4097
|
logger.error("SDK abandoned submission failed to record its failure reason", {
|
|
3616
4098
|
kind,
|
|
3617
4099
|
commandId: correlation.commandId,
|
|
@@ -3630,8 +4112,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3630
4112
|
// 3. Release recovery ownership ONLY after durable terminalization.
|
|
3631
4113
|
deadlineManager.clear(correlation);
|
|
3632
4114
|
} catch (transitionError) {
|
|
3633
|
-
// Keep
|
|
3634
|
-
|
|
4115
|
+
// Keep prompt recovery leased; skill recovery has no prompt lease.
|
|
4116
|
+
if (kind === "skill") scheduleSkillRecovery(correlation);
|
|
4117
|
+
else deadlineManager.noteTerminalTransition(correlation, { code: error.code, message: error.message });
|
|
3635
4118
|
logger.error("SDK abandoned submission failed to terminalize", {
|
|
3636
4119
|
kind,
|
|
3637
4120
|
commandId: correlation.commandId,
|
|
@@ -3647,13 +4130,26 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3647
4130
|
ctx,
|
|
3648
4131
|
api,
|
|
3649
4132
|
reconciliation,
|
|
3650
|
-
(kind, correlation, connectionId, startsOwnTurn) => {
|
|
4133
|
+
(kind, correlation, connectionId, startsOwnTurn, sdkRunToken) => {
|
|
4134
|
+
const owner = lifecycleOwnerHolder.state;
|
|
4135
|
+
if (lifecycleOwnerHolder.quiescing) {
|
|
4136
|
+
terminalizeAbandonedSubmission(kind, correlation, {
|
|
4137
|
+
code: "session_quiescing",
|
|
4138
|
+
message: "Session endpoint was replaced before the invocation started.",
|
|
4139
|
+
});
|
|
4140
|
+
return;
|
|
4141
|
+
}
|
|
4142
|
+
if (sdkRunToken && owner)
|
|
4143
|
+
lifecycleRunOwners.set(sdkRunToken, {
|
|
4144
|
+
state: owner,
|
|
4145
|
+
correlationKey: lifecycleCorrelationKey(correlation),
|
|
4146
|
+
});
|
|
3651
4147
|
// Only submissions that start their OWN turn get a pending entry: a
|
|
3652
4148
|
// steering-queued submission consumed inside the current run never
|
|
3653
4149
|
// emits the agent_start that would consume the entry, so leaving it
|
|
3654
4150
|
// queued would assign its stale connection as owner of a later
|
|
3655
4151
|
// agent-initiated turn (review thread P1).
|
|
3656
|
-
if (startsOwnTurn) pending.push({ kind, correlation, connectionId });
|
|
4152
|
+
if (startsOwnTurn) pending.push({ kind, correlation, connectionId, sdkRunToken });
|
|
3657
4153
|
// Anchor the zero-progress deadline at durable acceptance, not at
|
|
3658
4154
|
// agent_start (#4668): an accepted own-turn prompt that wedges
|
|
3659
4155
|
// between acceptance and run start (provider/credential/compaction
|
|
@@ -3666,7 +4162,23 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3666
4162
|
if (startsOwnTurn && kind === "prompt") deadlineManager.onAccepted(correlation);
|
|
3667
4163
|
},
|
|
3668
4164
|
steerReconciliation,
|
|
3669
|
-
(kind, correlation, connectionId, promotion) => {
|
|
4165
|
+
(kind, correlation, connectionId, sdkRunToken, promotion) => {
|
|
4166
|
+
const bindPromotedToken = (batch?: LifecycleBatch): void => {
|
|
4167
|
+
const owner = lifecycleOwnerHolder.state;
|
|
4168
|
+
if (sdkRunToken && owner)
|
|
4169
|
+
lifecycleRunOwners.set(sdkRunToken, {
|
|
4170
|
+
state: owner,
|
|
4171
|
+
batch,
|
|
4172
|
+
correlationKey: lifecycleCorrelationKey(correlation),
|
|
4173
|
+
});
|
|
4174
|
+
};
|
|
4175
|
+
if (lifecycleOwnerHolder.quiescing) {
|
|
4176
|
+
terminalizeAbandonedSubmission(kind, correlation, {
|
|
4177
|
+
code: "session_quiescing",
|
|
4178
|
+
message: "Session endpoint was replaced before the invocation started.",
|
|
4179
|
+
});
|
|
4180
|
+
return;
|
|
4181
|
+
}
|
|
3670
4182
|
// Lease at the ACTUAL promotion boundary (#4668 review): a promoted
|
|
3671
4183
|
// submission that wedges before its run's agent_start must still
|
|
3672
4184
|
// terminalize boundedly instead of remaining accepted forever.
|
|
@@ -3696,7 +4208,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3696
4208
|
// starts with an empty pending queue at its agent_start; create the
|
|
3697
4209
|
// entry at promotion so the submitting connection owns that turn
|
|
3698
4210
|
// (review thread P2).
|
|
3699
|
-
pending.push({ kind, correlation, connectionId });
|
|
4211
|
+
pending.push({ kind, correlation, connectionId, sdkRunToken });
|
|
3700
4212
|
return;
|
|
3701
4213
|
}
|
|
3702
4214
|
// Consumed INSIDE the currently running turn: no new agent_start
|
|
@@ -3759,6 +4271,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3759
4271
|
)
|
|
3760
4272
|
)
|
|
3761
4273
|
activeBatch.attachedInvocations.push({ kind, correlation });
|
|
4274
|
+
bindPromotedToken(activeBatch);
|
|
3762
4275
|
return;
|
|
3763
4276
|
}
|
|
3764
4277
|
// No in-flight run visible and this is an in-run consumption
|
|
@@ -3794,8 +4307,14 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3794
4307
|
// the matching correlation so a later agent-initiated turn never
|
|
3795
4308
|
// inherits the failed submission's connection as owner (review
|
|
3796
4309
|
// thread P1).
|
|
3797
|
-
|
|
3798
|
-
|
|
4310
|
+
if (!lifecycleOwnerHolder.quiescing) {
|
|
4311
|
+
const index = pending.findIndex(entry => entry.correlation === correlation);
|
|
4312
|
+
if (index >= 0) {
|
|
4313
|
+
const [removed] = pending.splice(index, 1);
|
|
4314
|
+
if (removed && lifecycleRunOwners.get(removed.sdkRunToken)?.state === lifecycleOwnerHolder.state)
|
|
4315
|
+
lifecycleRunOwners.delete(removed.sdkRunToken);
|
|
4316
|
+
}
|
|
4317
|
+
}
|
|
3799
4318
|
// Release the acceptance-anchored lease ONLY when the durable row is
|
|
3800
4319
|
// already terminal (exact-head review #4): clearing it before the
|
|
3801
4320
|
// settlement writes land strands the row accepted with no recovery
|
|
@@ -3808,6 +4327,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3808
4327
|
return;
|
|
3809
4328
|
}
|
|
3810
4329
|
if (leaseRelease === "recover-failure" && failureIntent !== undefined) {
|
|
4330
|
+
if (kind === "skill") {
|
|
4331
|
+
scheduleSkillRecovery(correlation, failureIntent);
|
|
4332
|
+
return;
|
|
4333
|
+
}
|
|
3811
4334
|
// Compound failure-plus-terminal recovery: the settlement's durable
|
|
3812
4335
|
// writes failed, so the lease stays armed as the recovery owner and its
|
|
3813
4336
|
// replay re-records the failure reason before agent_end (exact-head
|
|
@@ -3816,6 +4339,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3816
4339
|
return;
|
|
3817
4340
|
}
|
|
3818
4341
|
if (leaseRelease === "recover-terminal") {
|
|
4342
|
+
if (kind === "skill") {
|
|
4343
|
+
scheduleSkillRecovery(correlation);
|
|
4344
|
+
return;
|
|
4345
|
+
}
|
|
3819
4346
|
deadlineManager.noteTerminalTransition(correlation);
|
|
3820
4347
|
return;
|
|
3821
4348
|
}
|
|
@@ -3892,6 +4419,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3892
4419
|
};
|
|
3893
4420
|
runtime = new SessionSdkSessionRuntime({
|
|
3894
4421
|
transport,
|
|
4422
|
+
onFrameAdmission: (_connectionId, frame) =>
|
|
4423
|
+
inputGate.quiescing ? quiescingFrame(frame as Record<string, unknown>) : undefined,
|
|
3895
4424
|
control: async (connectionId, frame) => {
|
|
3896
4425
|
options.onFrameAdmitted?.();
|
|
3897
4426
|
const request = controlRequestFromFrame(frame as Record<string, unknown>);
|
|
@@ -4028,7 +4557,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4028
4557
|
logger.warn(`sdk broker registration unavailable: ${String(error)}`);
|
|
4029
4558
|
}
|
|
4030
4559
|
};
|
|
4031
|
-
|
|
4560
|
+
const runtimeOwner: RuntimeState = {
|
|
4561
|
+
sessionId,
|
|
4562
|
+
sessionIdentity,
|
|
4032
4563
|
runtime,
|
|
4033
4564
|
revisions,
|
|
4034
4565
|
cursors,
|
|
@@ -4038,6 +4569,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4038
4569
|
pending,
|
|
4039
4570
|
openLifecycleBatches,
|
|
4040
4571
|
registerBroker,
|
|
4572
|
+
quiesceInput: () => {
|
|
4573
|
+
inputGate.quiescing = true;
|
|
4574
|
+
lifecycleOwnerHolder.quiescing = true;
|
|
4575
|
+
},
|
|
4041
4576
|
fenceGateResolutions: () => {
|
|
4042
4577
|
acceptingGateResolutions = false;
|
|
4043
4578
|
},
|
|
@@ -4045,7 +4580,12 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4045
4580
|
disposeGate,
|
|
4046
4581
|
lifecycleActive: false,
|
|
4047
4582
|
lifecycleEpoch: 0,
|
|
4583
|
+
failureDiagnosticKeys: new Set(),
|
|
4584
|
+
failureDiagnosticCodes: new Map(),
|
|
4585
|
+
lifecycleTasks: new Set(),
|
|
4048
4586
|
};
|
|
4587
|
+
lifecycleOwnerHolder.state = runtimeOwner;
|
|
4588
|
+
active = runtimeOwner;
|
|
4049
4589
|
try {
|
|
4050
4590
|
await runtime.start();
|
|
4051
4591
|
await registerBroker();
|
|
@@ -4059,7 +4599,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4059
4599
|
code: errorCode(cleanupError),
|
|
4060
4600
|
error: String(cleanupError),
|
|
4061
4601
|
});
|
|
4062
|
-
|
|
4602
|
+
const failedRuntimeOwner: RuntimeState = {
|
|
4603
|
+
sessionId,
|
|
4604
|
+
sessionIdentity,
|
|
4063
4605
|
runtime,
|
|
4064
4606
|
revisions,
|
|
4065
4607
|
cursors,
|
|
@@ -4069,6 +4611,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4069
4611
|
pending,
|
|
4070
4612
|
openLifecycleBatches,
|
|
4071
4613
|
registerBroker,
|
|
4614
|
+
quiesceInput: () => {
|
|
4615
|
+
inputGate.quiescing = true;
|
|
4616
|
+
lifecycleOwnerHolder.quiescing = true;
|
|
4617
|
+
},
|
|
4072
4618
|
fenceGateResolutions: () => {
|
|
4073
4619
|
acceptingGateResolutions = false;
|
|
4074
4620
|
},
|
|
@@ -4076,7 +4622,12 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4076
4622
|
disposeGate,
|
|
4077
4623
|
lifecycleActive: false,
|
|
4078
4624
|
lifecycleEpoch: 0,
|
|
4625
|
+
failureDiagnosticKeys: new Set(),
|
|
4626
|
+
failureDiagnosticCodes: new Map(),
|
|
4627
|
+
lifecycleTasks: new Set(),
|
|
4079
4628
|
};
|
|
4629
|
+
lifecycleOwnerHolder.state = failedRuntimeOwner;
|
|
4630
|
+
active = failedRuntimeOwner;
|
|
4080
4631
|
throw new AggregateError([error, cleanupError], "SDK runtime startup failed and cleanup failed.");
|
|
4081
4632
|
}
|
|
4082
4633
|
cursors.close();
|
|
@@ -4084,19 +4635,70 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4084
4635
|
throw error;
|
|
4085
4636
|
}
|
|
4086
4637
|
};
|
|
4087
|
-
const stopActive = async (): Promise<void> => {
|
|
4638
|
+
const stopActive = async (cancelSkillRecovery = false): Promise<void> => {
|
|
4088
4639
|
const current = active;
|
|
4089
4640
|
if (!current) return;
|
|
4090
|
-
|
|
4641
|
+
if (cancelSkillRecovery) {
|
|
4642
|
+
for (const controller of skillRecoveryControllers.values()) controller.abort();
|
|
4643
|
+
for (const controller of skillTerminalRecoveryControllers.values()) controller.abort();
|
|
4644
|
+
}
|
|
4645
|
+
activePromptOwnerHolder.connectionIds = undefined;
|
|
4646
|
+
activePromptOwnerHolder.lifecycleEpoch = undefined;
|
|
4647
|
+
current.quiesceInput();
|
|
4091
4648
|
current.fenceGateResolutions();
|
|
4092
4649
|
try {
|
|
4093
4650
|
await current.waitForGateResolutionQuiescence();
|
|
4651
|
+
if (current.lifecycleTasks.size > 0) {
|
|
4652
|
+
const lifecycleDrain = Promise.all([...current.lifecycleTasks]);
|
|
4653
|
+
const timeout = Promise.withResolvers<void>();
|
|
4654
|
+
const timer = setTimeout(timeout.resolve, LIFECYCLE_QUIESCENCE_MS);
|
|
4655
|
+
const drained = Promise.race([lifecycleDrain, timeout.promise]);
|
|
4656
|
+
await drained;
|
|
4657
|
+
clearTimeout(timer);
|
|
4658
|
+
if (current.lifecycleTasks.size > 0) {
|
|
4659
|
+
logger.warn("SDK runtime lifecycle drain timed out; durable lifecycle recovery remains authoritative.");
|
|
4660
|
+
options.onLifecycleDrainTimeoutForTests?.();
|
|
4661
|
+
}
|
|
4662
|
+
}
|
|
4094
4663
|
active = undefined;
|
|
4664
|
+
const retainsLifecycleWork =
|
|
4665
|
+
current.pending.length > 0 ||
|
|
4666
|
+
current.openLifecycleBatches.length > 0 ||
|
|
4667
|
+
(current.attachedInvocations?.length ?? 0) > 0 ||
|
|
4668
|
+
(current.drainedInvocations?.length ?? 0) > 0 ||
|
|
4669
|
+
current.lifecycleTasks.size > 0;
|
|
4670
|
+
if (retainsLifecycleWork) {
|
|
4671
|
+
const owners = retiredLifecycleOwners.get(current.sessionId) ?? [];
|
|
4672
|
+
if (!owners.includes(current)) owners.push(current);
|
|
4673
|
+
retiredLifecycleOwners.set(current.sessionId, owners);
|
|
4674
|
+
const retryCleanup = (): void => {
|
|
4675
|
+
retiredLifecycleOwnerTimers.delete(current);
|
|
4676
|
+
if (
|
|
4677
|
+
current.pending.length > 0 ||
|
|
4678
|
+
current.openLifecycleBatches.length > 0 ||
|
|
4679
|
+
(current.attachedInvocations?.length ?? 0) > 0 ||
|
|
4680
|
+
(current.drainedInvocations?.length ?? 0) > 0 ||
|
|
4681
|
+
current.lifecycleTasks.size > 0
|
|
4682
|
+
) {
|
|
4683
|
+
const retry = setTimeout(retryCleanup, LIFECYCLE_QUIESCENCE_MS);
|
|
4684
|
+
retry.unref();
|
|
4685
|
+
retiredLifecycleOwnerTimers.set(current, retry);
|
|
4686
|
+
return;
|
|
4687
|
+
}
|
|
4688
|
+
removeRetiredLifecycleOwner(current);
|
|
4689
|
+
};
|
|
4690
|
+
const timer = setTimeout(retryCleanup, LIFECYCLE_QUIESCENCE_MS);
|
|
4691
|
+
timer.unref();
|
|
4692
|
+
retiredLifecycleOwnerTimers.set(current, timer);
|
|
4693
|
+
} else current.deadlineManager.clearAll();
|
|
4095
4694
|
current.disposeGate?.();
|
|
4096
4695
|
await current.runtime.stop();
|
|
4097
4696
|
} catch (error) {
|
|
4697
|
+
// Keep the immutable owner available for a retry when transport teardown
|
|
4698
|
+
// fails after quiescing. Clearing `active` before stop prevents a second
|
|
4699
|
+
// shutdown from retrying the failed endpoint removal.
|
|
4700
|
+
if (active === undefined) active = current;
|
|
4098
4701
|
logger.error("sdk runtime stop failed", { code: errorCode(error), error: String(error) });
|
|
4099
|
-
active = current;
|
|
4100
4702
|
throw error;
|
|
4101
4703
|
}
|
|
4102
4704
|
current.cursors.close();
|
|
@@ -4114,6 +4716,6 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4114
4716
|
await startRuntime(ctx);
|
|
4115
4717
|
});
|
|
4116
4718
|
api.on("session_shutdown", async () => {
|
|
4117
|
-
await stopActive();
|
|
4719
|
+
await stopActive(true);
|
|
4118
4720
|
});
|
|
4119
4721
|
}
|