@gajae-code/coding-agent 0.15.4 → 0.15.6
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 +46 -2
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
- package/dist/types/cli/model-presets-cli.d.ts +9 -0
- package/dist/types/cli/update-cli.d.ts +27 -5
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +99 -0
- package/dist/types/config/file-lock.d.ts +49 -2
- package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
- package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
- package/dist/types/config/keybindings.d.ts +3 -2
- package/dist/types/config/model-preset-registry.d.ts +368 -0
- package/dist/types/config/model-profile-activation.d.ts +39 -0
- package/dist/types/config/model-profiles.d.ts +5 -3
- package/dist/types/config/model-registry.d.ts +11 -5
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +46 -0
- package/dist/types/extensibility/extensions/types.d.ts +4 -4
- package/dist/types/extensibility/shared-events.d.ts +2 -0
- package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +6 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/jobs-observer.d.ts +36 -0
- package/dist/types/sdk/broker/broker.d.ts +3 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +8 -1
- package/dist/types/sdk/cli/session-cli.d.ts +42 -1
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/model-pin.d.ts +7 -2
- package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +19 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
- package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +9 -0
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +19 -5
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +4 -3
- package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
- package/dist/types/session/sdk-run-capability.d.ts +2 -0
- package/dist/types/telemetry/control.d.ts +11 -0
- package/dist/types/telemetry/events.d.ts +20 -0
- package/dist/types/telemetry/index.d.ts +3 -0
- package/dist/types/telemetry/transport.d.ts +22 -0
- package/dist/types/tools/bash-interactive.d.ts +23 -0
- package/dist/types/tools/index.d.ts +6 -5
- package/dist/types/tools/path-utils.d.ts +5 -6
- package/dist/types/tools/tmux-self-injection-guard.d.ts +15 -0
- package/dist/types/web/search/providers/base.d.ts +9 -4
- package/dist/types/web/search/providers/xai.d.ts +2 -1
- package/dist/types/web/search/types.d.ts +14 -0
- package/package.json +10 -7
- package/scripts/generate-hotkeys-docs.ts +4 -0
- package/scripts/generate-sdk-operation-inventory.ts +3 -3
- package/src/async/job-manager.ts +671 -39
- package/src/cli/auth-broker-cli.ts +40 -12
- package/src/cli/auth-gateway-cli.ts +273 -97
- package/src/cli/config-cli.ts +2 -2
- package/src/cli/fast-help.ts +3 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/update-cli.ts +142 -6
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +5 -0
- package/src/config/autorouting-tier-map.ts +33 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +775 -59
- package/src/config/internal/model-preset-registry-test-state.ts +28 -0
- package/src/config/internal/model-preset-registry-test-support.ts +11 -0
- package/src/config/keybindings.ts +6 -2
- package/src/config/model-preset-registry.ts +2799 -0
- package/src/config/model-profile-activation.ts +115 -28
- package/src/config/model-profile-contract.ts +4 -1
- package/src/config/model-profiles.ts +56 -14
- package/src/config/model-registry.ts +1028 -123
- package/src/config/settings-schema.ts +11 -0
- package/src/config/settings.ts +6 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/policy.ts +1 -1
- package/src/coordinator-mcp/projection-scan.ts +571 -0
- package/src/coordinator-mcp/server.ts +19 -24
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +4 -2
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
- package/src/extensibility/shared-events.ts +2 -0
- package/src/gjc-runtime/empty-delete-gc.ts +301 -0
- package/src/gjc-runtime/gc-render.ts +29 -0
- package/src/gjc-runtime/gc-runtime.ts +174 -11
- package/src/gjc-runtime/launch-worktree.ts +44 -7
- package/src/gjc-runtime/ralplan-runtime.ts +14 -1
- package/src/gjc-runtime/session-state-lock.ts +1242 -141
- package/src/gjc-runtime/session-state-sidecar.ts +76 -3
- package/src/gjc-runtime/tmux-sessions.ts +145 -48
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +9 -2
- package/src/modes/acp/acp-agent.ts +11 -5
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/status-line/segments.ts +6 -1
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +23 -6
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/prompt-action-autocomplete.ts +26 -47
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/tools/bash.md +2 -2
- package/src/prompts/tools/fold-receipt.md +3 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +17 -2
- package/src/sdk/broker/broker.ts +48 -21
- package/src/sdk/broker/lifecycle.ts +32 -6
- package/src/sdk/bus/chat-daemon-control.ts +2 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +11 -1
- package/src/sdk/bus/kind-aware-reconciliation.ts +46 -8
- package/src/sdk/bus/prompt-reconciliation.ts +33 -6
- package/src/sdk/bus/slack-conversation.ts +5 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +8 -1
- package/src/sdk/cli/session-cli.ts +161 -5
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +3 -0
- package/src/sdk/host/host.ts +7 -0
- package/src/sdk/host/model-pin.ts +185 -25
- package/src/sdk/host/sdk-run-capability.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +1240 -107
- package/src/sdk/host/session-runtime.ts +1029 -115
- package/src/sdk/prompt-deadline-manager.ts +16 -1
- package/src/sdk/protocol/adapter-validation.ts +27 -0
- package/src/sdk/protocol/operation-inventory.generated.json +2 -2
- package/src/sdk/protocol/operation-registry.ts +4 -1
- package/src/sdk/router/session-router.ts +297 -111
- package/src/sdk/session.ts +83 -12
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +887 -95
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +64 -26
- package/src/session/sdk-run-capability-internal.ts +15 -0
- package/src/session/sdk-run-capability.ts +2 -0
- package/src/session/session-manager.ts +29 -10
- package/src/setup/provider-presets.json +1 -1
- package/src/slash-commands/helpers/credential-switch.ts +6 -1
- package/src/task/executor.ts +19 -3
- package/src/telemetry/control.ts +26 -0
- package/src/telemetry/events.ts +142 -0
- package/src/telemetry/index.ts +3 -0
- package/src/telemetry/transport.ts +87 -0
- package/src/tools/bash-interactive.ts +182 -85
- package/src/tools/bash.ts +786 -67
- package/src/tools/index.ts +7 -5
- package/src/tools/path-utils.ts +77 -29
- package/src/tools/tmux-self-injection-guard.ts +359 -0
- package/src/tools/tool-catalog.generated.ts +1 -1
- package/src/web/search/provider.ts +120 -11
- package/src/web/search/providers/anthropic.ts +89 -17
- package/src/web/search/providers/base.ts +9 -4
- package/src/web/search/providers/gemini.ts +87 -15
- package/src/web/search/providers/openai-compatible.ts +23 -8
- package/src/web/search/providers/xai.ts +98 -12
- package/src/web/search/types.ts +16 -0
|
@@ -5,16 +5,30 @@ import * as fs from "node:fs/promises";
|
|
|
5
5
|
import * as path from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { ThinkingLevel } from "@gajae-code/agent-core";
|
|
8
|
-
import type { Api, Model } from "@gajae-code/ai/core";
|
|
8
|
+
import type { Api, ImageContent, Model } from "@gajae-code/ai/core";
|
|
9
9
|
import { logger } from "@gajae-code/utils";
|
|
10
10
|
import { AsyncJobManager } from "../../async";
|
|
11
|
+
import {
|
|
12
|
+
getProxyRoutableProviders,
|
|
13
|
+
inspectProxyProviderId,
|
|
14
|
+
requiresQualifiedModelProfileRoleResolution,
|
|
15
|
+
resolveProxyMode,
|
|
16
|
+
rewriteSelectorForProxy,
|
|
17
|
+
tryResolveProxyProviderId,
|
|
18
|
+
} from "../../config/model-profile-activation";
|
|
11
19
|
import { isModelProfileProviderAvailable, projectModelProfileCatalog } from "../../config/model-profile-contract";
|
|
12
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
deriveModelProfileMappedProviders,
|
|
22
|
+
type ModelProfileDefinition,
|
|
23
|
+
resolveProfileBindings,
|
|
24
|
+
} from "../../config/model-profiles";
|
|
25
|
+
import { isAuthenticated, kNoAuth } from "../../config/model-registry";
|
|
13
26
|
import { resolveModelChainWithAuth, splitSelectorThinkingSuffix } from "../../config/model-resolver";
|
|
14
27
|
import { type ModelSelectorValue, normalizeModelSelectorValue } from "../../config/model-selector-value";
|
|
15
28
|
import { type Settings, validateSettingPatch } from "../../config/settings";
|
|
16
29
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
|
|
17
30
|
import { normalizeGoal } from "../../goals/state";
|
|
31
|
+
import type { SdkRunCapability } from "../../session/sdk-run-capability";
|
|
18
32
|
import {
|
|
19
33
|
boundTerminalRetentionState,
|
|
20
34
|
findOwnedRegistrationsForTurn,
|
|
@@ -26,7 +40,6 @@ import { parseThinkingLevel } from "../../thinking";
|
|
|
26
40
|
import { ensureBroker } from "../broker/ensure";
|
|
27
41
|
import { SessionIndex } from "../broker/session-index";
|
|
28
42
|
import {
|
|
29
|
-
collectAuthenticatedProfileProviders,
|
|
30
43
|
parseSyntheticModelId,
|
|
31
44
|
resolveSyntheticModelSelection,
|
|
32
45
|
SYNTHETIC_PROVIDER_ID,
|
|
@@ -34,8 +47,9 @@ import {
|
|
|
34
47
|
syntheticNamespaceCollision,
|
|
35
48
|
} from "../model-profile-model";
|
|
36
49
|
import { projectQ10Models } from "../models.js";
|
|
37
|
-
import { PromptDeadlineManager } from "../prompt-deadline-manager";
|
|
50
|
+
import { PromptDeadlineManager, type PromptTerminalTransitionEvidence } from "../prompt-deadline-manager";
|
|
38
51
|
import { formatPromptFailureForLocalLog, sanitizePromptFailure } from "../prompt-failure";
|
|
52
|
+
import { validateRequiredPromptText } from "../protocol/adapter-validation";
|
|
39
53
|
import { OPERATIONS } from "../protocol/operation-registry";
|
|
40
54
|
import {
|
|
41
55
|
createKindAwareReconciliation,
|
|
@@ -43,11 +57,13 @@ import {
|
|
|
43
57
|
type KindAwareReconciliation,
|
|
44
58
|
resolveReconciliationSessionFile,
|
|
45
59
|
} from "../reconciliation-extensions";
|
|
60
|
+
import { sanitizeTurnResultContent, type TurnResultContent } from "../turn-result";
|
|
46
61
|
import { type ControlSurface, controlRequestFromFrame, dispatchControl } from "./control";
|
|
47
62
|
import { BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD } from "./control/runtime-gate";
|
|
48
63
|
import { SessionSdkHost, type SessionSdkHostOptions } from "./host";
|
|
49
64
|
import { clearAutoroutingInactive, isAutoroutingInactive, markAutoroutingInactive } from "./internal-autorouting-state";
|
|
50
65
|
import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "./query";
|
|
66
|
+
import { createSdkRunCapability } from "./sdk-run-capability";
|
|
51
67
|
import {
|
|
52
68
|
createSdkCapabilities,
|
|
53
69
|
createSdkSurfacePolicyForContext,
|
|
@@ -87,6 +103,8 @@ const SDK_ONLY_TERMINAL_PUBLICATION_WAIT_MS = 1_000;
|
|
|
87
103
|
* after this bound are abandoned — their durable broker state is the recovery
|
|
88
104
|
* authority, and outcomes are inherently uncertain. */
|
|
89
105
|
const GATE_RESOLUTION_QUIESCENCE_MS = 5_000;
|
|
106
|
+
/** Maximum time a replaced runtime may retain a lifecycle persistence task. */
|
|
107
|
+
const LIFECYCLE_QUIESCENCE_MS = 1_000;
|
|
90
108
|
|
|
91
109
|
class DiffQueryError extends Error {
|
|
92
110
|
constructor(
|
|
@@ -397,6 +415,10 @@ export interface CreateSdkSessionRuntimeOptions {
|
|
|
397
415
|
terminalAbortSeams?: SdkOnlyTerminalAbortSeams;
|
|
398
416
|
/** Callback when a frame is admitted to the runtime (test harness). */
|
|
399
417
|
onFrameAdmitted?: () => void;
|
|
418
|
+
/** Test-only observation of a genuinely timed-out lifecycle drain. */
|
|
419
|
+
onLifecycleDrainTimeoutForTests?: () => void;
|
|
420
|
+
/** Test-only observation of bounded failure-diagnostic deduplication state. */
|
|
421
|
+
onFailureDiagnosticKeyCountForTests?: (count: number) => void;
|
|
400
422
|
}
|
|
401
423
|
|
|
402
424
|
function unavailable(operation: string): () => never {
|
|
@@ -414,6 +436,7 @@ export interface InvocationCorrelation {
|
|
|
414
436
|
|
|
415
437
|
export type InvocationKind = "prompt" | "skill" | "steer";
|
|
416
438
|
type InvocationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed" | "uncertain";
|
|
439
|
+
const EMPTY_PROMPT_FAILURE = { code: "prompt_failed", message: "Prompt submission failed." } as const;
|
|
417
440
|
interface InvocationRecord extends InvocationCorrelation {
|
|
418
441
|
kind: InvocationKind;
|
|
419
442
|
revision: number;
|
|
@@ -434,7 +457,14 @@ export interface InvocationReconciliation {
|
|
|
434
457
|
noteTransition(
|
|
435
458
|
kind: InvocationKind,
|
|
436
459
|
correlation: InvocationCorrelation | undefined,
|
|
437
|
-
frame:
|
|
460
|
+
frame:
|
|
461
|
+
| {
|
|
462
|
+
type: "agent_start" | "agent_end";
|
|
463
|
+
content?: TurnResultContent;
|
|
464
|
+
hasActivity?: boolean;
|
|
465
|
+
outcome?: { kind: "stopped"; reason: "cancelled"; provenance: "client_cancel" };
|
|
466
|
+
}
|
|
467
|
+
| { type: "agent_failed"; error: unknown; content?: TurnResultContent; hasActivity?: boolean },
|
|
438
468
|
): Promise<void>;
|
|
439
469
|
lookup(kind: InvocationKind, selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown;
|
|
440
470
|
lookupResult(kind: InvocationKind, selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown;
|
|
@@ -458,7 +488,13 @@ export interface InvocationReconciliation {
|
|
|
458
488
|
correlation: InvocationCorrelation,
|
|
459
489
|
outcome?: { kind: string; code: string; message: string; provenance?: string },
|
|
460
490
|
arg4?: (() => boolean) | { code: string; message: string },
|
|
491
|
+
// Terminal evidence accepted by finalizeOutcome (exact-head review P1/P2):
|
|
492
|
+
// when an explicit outcome is absent, a content-bearing, activity-bearing,
|
|
493
|
+
// or explicitly stopped finalization is still a successful terminal; only a
|
|
494
|
+
// genuinely empty, no-activity completion fails closed with the sanitized
|
|
495
|
+
// empty-prompt failure. Mirrors the noteTransition evidence predicate.
|
|
461
496
|
arg5?: unknown,
|
|
497
|
+
arg6?: { content?: TurnResultContent; hasActivity?: boolean; outcomeKind?: string },
|
|
462
498
|
): Promise<void>;
|
|
463
499
|
markUncertain(
|
|
464
500
|
kind: InvocationKind,
|
|
@@ -693,7 +729,14 @@ export function createInvocationReconciliation(
|
|
|
693
729
|
// so a real lifecycle event is never swallowed.
|
|
694
730
|
}
|
|
695
731
|
const current = records.get(recordKey);
|
|
696
|
-
if (
|
|
732
|
+
if (
|
|
733
|
+
frame.type === "agent_end" &&
|
|
734
|
+
current === pending.finalizedRecord &&
|
|
735
|
+
(kind !== "prompt" ||
|
|
736
|
+
frame.outcome?.kind === "stopped" ||
|
|
737
|
+
frame.hasActivity === true ||
|
|
738
|
+
frame.content?.text.trim())
|
|
739
|
+
) {
|
|
697
740
|
// A real lifecycle end that arrived during deadline finalization is
|
|
698
741
|
// stronger evidence than the synthetic deadline outcome. Upgrade the
|
|
699
742
|
// durable terminal instead of treating the event as a duplicate.
|
|
@@ -728,7 +771,14 @@ export function createInvocationReconciliation(
|
|
|
728
771
|
}
|
|
729
772
|
}
|
|
730
773
|
if (record.terminalAt !== undefined) {
|
|
731
|
-
if (
|
|
774
|
+
if (
|
|
775
|
+
frame.type === "agent_end" &&
|
|
776
|
+
record.error?.code === "prompt_deadline_exceeded" &&
|
|
777
|
+
(kind !== "prompt" ||
|
|
778
|
+
frame.outcome?.kind === "stopped" ||
|
|
779
|
+
frame.hasActivity === true ||
|
|
780
|
+
frame.content?.text.trim())
|
|
781
|
+
) {
|
|
732
782
|
const upgraded = { ...record, revision: ++mutationRevision, status: "terminal_ok" as const };
|
|
733
783
|
// The synthetic deadline error is superseded evidence; terminal_ok must not
|
|
734
784
|
// surface a deadline failure for a prompt that actually completed.
|
|
@@ -746,7 +796,13 @@ export function createInvocationReconciliation(
|
|
|
746
796
|
// failure reason may arrive on a different delivery path than the one that
|
|
747
797
|
// claimed the terminal. Enrich the settled record instead of dropping it;
|
|
748
798
|
// never resurrect (status/terminalAt untouched), and first reason wins.
|
|
749
|
-
if (frame.type === "agent_failed"
|
|
799
|
+
if (frame.type === "agent_failed") {
|
|
800
|
+
const failure = sanitizePromptFailure(frame.error);
|
|
801
|
+
if (
|
|
802
|
+
record.error !== undefined &&
|
|
803
|
+
!(record.error.code === "agent_failed" && failure.code !== "agent_failed")
|
|
804
|
+
)
|
|
805
|
+
return;
|
|
750
806
|
const next = { ...record, revision: ++mutationRevision };
|
|
751
807
|
logger.error("SDK invocation failed (late)", {
|
|
752
808
|
kind,
|
|
@@ -754,7 +810,7 @@ export function createInvocationReconciliation(
|
|
|
754
810
|
turnId: correlation.turnId,
|
|
755
811
|
error: formatPromptFailureForLocalLog(frame.error),
|
|
756
812
|
});
|
|
757
|
-
next.error =
|
|
813
|
+
next.error = failure;
|
|
758
814
|
records.set(recordKey, next);
|
|
759
815
|
try {
|
|
760
816
|
await persist();
|
|
@@ -779,9 +835,21 @@ export function createInvocationReconciliation(
|
|
|
779
835
|
turnId: correlation.turnId,
|
|
780
836
|
error: formatPromptFailureForLocalLog(frame.error),
|
|
781
837
|
});
|
|
782
|
-
|
|
838
|
+
const failure = sanitizePromptFailure(frame.error);
|
|
839
|
+
if (next.error === undefined || (next.error.code === "agent_failed" && failure.code !== "agent_failed"))
|
|
840
|
+
next.error = failure;
|
|
783
841
|
} else {
|
|
784
|
-
|
|
842
|
+
if (
|
|
843
|
+
kind === "prompt" &&
|
|
844
|
+
next.error === undefined &&
|
|
845
|
+
frame.type === "agent_end" &&
|
|
846
|
+
frame.outcome?.kind !== "stopped" &&
|
|
847
|
+
!frame.content?.text.trim() &&
|
|
848
|
+
!frame.hasActivity
|
|
849
|
+
) {
|
|
850
|
+
next.status = "failed";
|
|
851
|
+
next.error = EMPTY_PROMPT_FAILURE;
|
|
852
|
+
} else next.status = next.error === undefined ? "terminal_ok" : "failed";
|
|
785
853
|
next.terminalAt = Date.now();
|
|
786
854
|
}
|
|
787
855
|
records.set(recordKey, next);
|
|
@@ -854,6 +922,7 @@ export function createInvocationReconciliation(
|
|
|
854
922
|
outcome,
|
|
855
923
|
arg4?: (() => boolean) | { code: string; message: string },
|
|
856
924
|
arg5?: unknown,
|
|
925
|
+
arg6?: { content?: TurnResultContent; hasActivity?: boolean; outcomeKind?: string },
|
|
857
926
|
) {
|
|
858
927
|
// Normalize every supported positional form (exact-head review P1):
|
|
859
928
|
// (…, isCurrent) | (…, recordError) | (…, undefined, recordError) |
|
|
@@ -866,6 +935,14 @@ export function createInvocationReconciliation(
|
|
|
866
935
|
? (arg5 as { code: string; message: string })
|
|
867
936
|
: undefined;
|
|
868
937
|
const recordError = recordErrorCandidate;
|
|
938
|
+
// Terminal evidence may also arrive positionally through arg5 (legacy
|
|
939
|
+
// callers) when it does not carry a recordError shape; normalize it so
|
|
940
|
+
// the empty predicate below sees every evidence channel consistently.
|
|
941
|
+
const legacyEvidenceCandidate =
|
|
942
|
+
typeof arg5 === "object" && arg5 !== null && !("code" in arg5)
|
|
943
|
+
? (arg5 as { content?: TurnResultContent; hasActivity?: boolean; outcomeKind?: string })
|
|
944
|
+
: undefined;
|
|
945
|
+
const evidence = arg6 ?? legacyEvidenceCandidate;
|
|
869
946
|
const recordKey = key(kind, correlation);
|
|
870
947
|
const record = records.get(recordKey);
|
|
871
948
|
if (!record || record.terminalAt !== undefined || record.kind !== kind) return;
|
|
@@ -882,6 +959,19 @@ export function createInvocationReconciliation(
|
|
|
882
959
|
recordError !== undefined
|
|
883
960
|
? { code: recordError.code, message: recordError.message }
|
|
884
961
|
: { code: finalOutcome.code, message: finalOutcome.message };
|
|
962
|
+
} else if (kind === "prompt" && finalOutcome === undefined) {
|
|
963
|
+
// Evidence-based empty predicate (exact-head review P1/P2): the same
|
|
964
|
+
// semantics as the noteTransition agent_end classifier. A
|
|
965
|
+
// content-bearing, activity-bearing, or explicitly stopped
|
|
966
|
+
// finalization without an explicit outcome is a successful terminal;
|
|
967
|
+
// only a genuinely empty, no-activity completion fails closed.
|
|
968
|
+
const terminalText = evidence?.content?.text.trim() ?? "";
|
|
969
|
+
if (terminalText === "" && !evidence?.hasActivity && evidence?.outcomeKind !== "stopped") {
|
|
970
|
+
finalizedRecord.status = "failed";
|
|
971
|
+
finalizedRecord.error = EMPTY_PROMPT_FAILURE;
|
|
972
|
+
} else {
|
|
973
|
+
finalizedRecord.status = "terminal_ok";
|
|
974
|
+
}
|
|
885
975
|
} else {
|
|
886
976
|
finalizedRecord.status = "terminal_ok";
|
|
887
977
|
}
|
|
@@ -1043,21 +1133,50 @@ function createQuerySurface(
|
|
|
1043
1133
|
return undefined;
|
|
1044
1134
|
};
|
|
1045
1135
|
const getProfileCredentialSessionId = () => ctx.credentialSessionId ?? id;
|
|
1136
|
+
const profileSettings = (options.settings ?? ctx.settings) as Pick<Settings, "get"> | undefined;
|
|
1137
|
+
const getProfileAvailableModels = (): Model<Api>[] => {
|
|
1138
|
+
const getAvailableForProfileActivation = ctx.modelRegistry.getAvailableForProfileActivation;
|
|
1139
|
+
return typeof getAvailableForProfileActivation === "function"
|
|
1140
|
+
? getAvailableForProfileActivation.call(ctx.modelRegistry)
|
|
1141
|
+
: ctx.modelRegistry.getAvailable();
|
|
1142
|
+
};
|
|
1046
1143
|
const resolveProfileAvailability = async (
|
|
1047
1144
|
profile: ModelProfileDefinition,
|
|
1048
1145
|
authenticatedProviders: ReadonlySet<string>,
|
|
1049
1146
|
): Promise<{ available: boolean; defaultModel?: Model<Api> }> => {
|
|
1147
|
+
if (profile.source !== "user" && inspectProxyProviderId(profileSettings).status === "invalid")
|
|
1148
|
+
return { available: false };
|
|
1149
|
+
const proxyProvider = profile.source === "user" ? undefined : tryResolveProxyProviderId(profileSettings);
|
|
1150
|
+
const proxyAuthenticated = proxyProvider !== undefined && authenticatedProviders.has(proxyProvider);
|
|
1151
|
+
const profileAuthenticated = new Set(authenticatedProviders);
|
|
1152
|
+
if (proxyAuthenticated) {
|
|
1153
|
+
for (const provider of getProxyRoutableProviders(profile)) profileAuthenticated.add(provider);
|
|
1154
|
+
}
|
|
1050
1155
|
const rewriteSelectorProvider = (selector: string): string => {
|
|
1051
1156
|
const slash = selector.indexOf("/");
|
|
1052
1157
|
if (slash < 0) return selector;
|
|
1053
1158
|
const provider = selector.slice(0, slash);
|
|
1054
|
-
if (
|
|
1159
|
+
if (profileAuthenticated.has(provider)) return selector;
|
|
1055
1160
|
const group = (profile.alternativeProviderGroups ?? []).find(candidates => candidates.includes(provider));
|
|
1056
1161
|
if (!group) return selector;
|
|
1057
|
-
const replacement = group.find(candidate =>
|
|
1162
|
+
const replacement = group.find(candidate => profileAuthenticated.has(candidate));
|
|
1058
1163
|
return replacement ? replacement + selector.slice(slash) : selector;
|
|
1059
1164
|
};
|
|
1060
1165
|
try {
|
|
1166
|
+
const proxyMode = profile.source === "user" ? "fallback" : resolveProxyMode(profileSettings);
|
|
1167
|
+
if (profile.source !== "user") {
|
|
1168
|
+
const configuredProviders = ctx.modelRegistry.getConfiguredProviderIds?.() ?? [];
|
|
1169
|
+
if (proxyProvider !== undefined && !configuredProviders.includes(proxyProvider))
|
|
1170
|
+
return { available: false };
|
|
1171
|
+
}
|
|
1172
|
+
if (profile.source !== "user" && proxyMode === "always") {
|
|
1173
|
+
if (
|
|
1174
|
+
proxyProvider === undefined ||
|
|
1175
|
+
!proxyAuthenticated ||
|
|
1176
|
+
!(ctx.modelRegistry.getConfiguredProviderIds?.() ?? []).includes(proxyProvider)
|
|
1177
|
+
)
|
|
1178
|
+
return { available: false };
|
|
1179
|
+
}
|
|
1061
1180
|
const bindings = resolveProfileBindings(profile);
|
|
1062
1181
|
const assignments: Array<{ value: ModelSelectorValue; isDefault: boolean }> = [];
|
|
1063
1182
|
if (bindings.defaultSelector !== undefined) {
|
|
@@ -1065,23 +1184,44 @@ function createQuerySurface(
|
|
|
1065
1184
|
}
|
|
1066
1185
|
for (const value of Object.values(bindings.modelRoles)) assignments.push({ value, isDefault: false });
|
|
1067
1186
|
for (const value of Object.values(bindings.agentModelOverrides)) assignments.push({ value, isDefault: false });
|
|
1187
|
+
const availableModels = getProfileAvailableModels();
|
|
1188
|
+
const resolutionRegistry = {
|
|
1189
|
+
...ctx.modelRegistry,
|
|
1190
|
+
getAvailable: () => availableModels,
|
|
1191
|
+
getApiKey: (model: Model<Api>, sessionId?: string) =>
|
|
1192
|
+
ctx.modelRegistry.getApiKeyForProvider(model.provider, sessionId, model.baseUrl),
|
|
1193
|
+
resolveCanonicalModel: ctx.modelRegistry.resolveCanonicalModel?.bind(ctx.modelRegistry),
|
|
1194
|
+
getCanonicalVariants: ctx.modelRegistry.getCanonicalVariants?.bind(ctx.modelRegistry),
|
|
1195
|
+
getCanonicalId: ctx.modelRegistry.getCanonicalId?.bind(ctx.modelRegistry),
|
|
1196
|
+
resolveModelByLookupAlias: ctx.modelRegistry.resolveModelByLookupAlias?.bind(ctx.modelRegistry),
|
|
1197
|
+
lookupAliasExists: ctx.modelRegistry.lookupAliasExists?.bind(ctx.modelRegistry),
|
|
1198
|
+
clearCanonicalVariant: ctx.modelRegistry.clearCanonicalVariant?.bind(ctx.modelRegistry),
|
|
1199
|
+
};
|
|
1068
1200
|
let defaultModel: Model<Api> | undefined;
|
|
1069
1201
|
for (const assignment of assignments) {
|
|
1070
|
-
|
|
1202
|
+
let selectors = normalizeModelSelectorValue(assignment.value).map(rewriteSelectorProvider);
|
|
1203
|
+
if (proxyProvider !== undefined && proxyAuthenticated && profile.source !== "user") {
|
|
1204
|
+
selectors = selectors.map(selector =>
|
|
1205
|
+
rewriteSelectorForProxy(
|
|
1206
|
+
selector,
|
|
1207
|
+
proxyProvider,
|
|
1208
|
+
proxyMode,
|
|
1209
|
+
availableModels,
|
|
1210
|
+
new Set(authenticatedProviders),
|
|
1211
|
+
getProxyRoutableProviders(profile),
|
|
1212
|
+
),
|
|
1213
|
+
);
|
|
1214
|
+
}
|
|
1071
1215
|
const hasBareSelector = selectors.some(selector => {
|
|
1072
1216
|
const suffix = splitSelectorThinkingSuffix(selector);
|
|
1073
1217
|
const identity = suffix.thinkingLevel ? suffix.selector : selector;
|
|
1074
1218
|
return !identity.includes("/");
|
|
1075
1219
|
});
|
|
1076
|
-
if (!assignment.isDefault && !hasBareSelector)
|
|
1220
|
+
if (!assignment.isDefault && !requiresQualifiedModelProfileRoleResolution(profile) && !hasBareSelector)
|
|
1221
|
+
continue;
|
|
1077
1222
|
const resolution = await resolveModelChainWithAuth(
|
|
1078
1223
|
selectors,
|
|
1079
|
-
|
|
1080
|
-
...ctx.modelRegistry,
|
|
1081
|
-
getAvailable: () => ctx.modelRegistry.getAvailable(),
|
|
1082
|
-
getApiKey: (model: Model<Api>, sessionId?: string) =>
|
|
1083
|
-
ctx.modelRegistry.getApiKeyForProvider(model.provider, sessionId, model.baseUrl),
|
|
1084
|
-
},
|
|
1224
|
+
resolutionRegistry,
|
|
1085
1225
|
options.settings,
|
|
1086
1226
|
getProfileCredentialSessionId(),
|
|
1087
1227
|
{
|
|
@@ -1099,6 +1239,45 @@ function createQuerySurface(
|
|
|
1099
1239
|
return { available: false };
|
|
1100
1240
|
}
|
|
1101
1241
|
};
|
|
1242
|
+
const collectProfileAuthentication = async (
|
|
1243
|
+
profiles: ReadonlyMap<string, ModelProfileDefinition>,
|
|
1244
|
+
): Promise<Set<string>> => {
|
|
1245
|
+
const providers = new Set<string>();
|
|
1246
|
+
for (const profile of profiles.values()) {
|
|
1247
|
+
for (const provider of profile.requiredProviders) providers.add(provider);
|
|
1248
|
+
for (const group of profile.alternativeProviderGroups ?? []) {
|
|
1249
|
+
for (const provider of group) providers.add(provider);
|
|
1250
|
+
}
|
|
1251
|
+
for (const provider of deriveModelProfileMappedProviders(profile)) providers.add(provider);
|
|
1252
|
+
}
|
|
1253
|
+
const authenticated = new Set<string>();
|
|
1254
|
+
await Promise.all(
|
|
1255
|
+
[...providers].map(async provider => {
|
|
1256
|
+
try {
|
|
1257
|
+
const apiKey = await ctx.modelRegistry.getApiKeyForProvider(provider, getProfileCredentialSessionId());
|
|
1258
|
+
if (apiKey === kNoAuth || isAuthenticated(apiKey)) authenticated.add(provider);
|
|
1259
|
+
} catch {
|
|
1260
|
+
// A provider whose credential state cannot be read is not currently configurable.
|
|
1261
|
+
}
|
|
1262
|
+
}),
|
|
1263
|
+
);
|
|
1264
|
+
const proxyProviders = new Set<string>();
|
|
1265
|
+
for (const profile of profiles.values()) {
|
|
1266
|
+
if (profile.source === "user") continue;
|
|
1267
|
+
const proxyProvider = tryResolveProxyProviderId(profileSettings);
|
|
1268
|
+
if (proxyProvider !== undefined) proxyProviders.add(proxyProvider);
|
|
1269
|
+
}
|
|
1270
|
+
for (const proxyProvider of proxyProviders) {
|
|
1271
|
+
try {
|
|
1272
|
+
const apiKey = await ctx.modelRegistry.getApiKeyForProvider(proxyProvider, getProfileCredentialSessionId());
|
|
1273
|
+
if (apiKey === kNoAuth || isAuthenticated(apiKey)) authenticated.add(proxyProvider);
|
|
1274
|
+
} catch {
|
|
1275
|
+
// Passive availability must degrade to unavailable when proxy credential
|
|
1276
|
+
// refresh/storage fails; explicit activation retains its diagnostics.
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
return authenticated;
|
|
1280
|
+
};
|
|
1102
1281
|
const getDiff = async () => {
|
|
1103
1282
|
try {
|
|
1104
1283
|
const { stdout } = await execFileAsync("git", ["diff", "--no-ext-diff"], {
|
|
@@ -1231,9 +1410,7 @@ function createQuerySurface(
|
|
|
1231
1410
|
if (collision) return degraded();
|
|
1232
1411
|
let authenticatedProviders: ReadonlySet<string>;
|
|
1233
1412
|
try {
|
|
1234
|
-
authenticatedProviders = await
|
|
1235
|
-
ctx.modelRegistry.getApiKeyForProvider(provider, getProfileCredentialSessionId()),
|
|
1236
|
-
);
|
|
1413
|
+
authenticatedProviders = await collectProfileAuthentication(profiles);
|
|
1237
1414
|
} catch {
|
|
1238
1415
|
// Availability join failed: degrade only the synthetic facade,
|
|
1239
1416
|
// retain concrete rows and the active marker readback.
|
|
@@ -1251,7 +1428,12 @@ function createQuerySurface(
|
|
|
1251
1428
|
);
|
|
1252
1429
|
const availableProfileIds = new Set<string>();
|
|
1253
1430
|
for (const [name, profile] of profiles) {
|
|
1254
|
-
if (
|
|
1431
|
+
if (profile.source !== "user" && inspectProxyProviderId(profileSettings).status === "invalid") continue;
|
|
1432
|
+
const profileAuthenticated = new Set(authenticatedProviders);
|
|
1433
|
+
const proxyProvider = profile.source === "user" ? undefined : tryResolveProxyProviderId(profileSettings);
|
|
1434
|
+
if (proxyProvider !== undefined && profileAuthenticated.has(proxyProvider))
|
|
1435
|
+
for (const provider of getProxyRoutableProviders(profile)) profileAuthenticated.add(provider);
|
|
1436
|
+
if (!isModelProfileProviderAvailable(profile, profileAuthenticated)) continue;
|
|
1255
1437
|
if (!fullyResolvedProfiles.has(name)) continue;
|
|
1256
1438
|
// A profile with a default mapping is selectable only when its
|
|
1257
1439
|
// default chain actually resolves to an authenticated model:
|
|
@@ -1322,18 +1504,19 @@ function createQuerySurface(
|
|
|
1322
1504
|
(options.steerStatusLookup ?? (value => reconciliation.lookup("steer", value)))(selector),
|
|
1323
1505
|
getModelProfiles: async () => {
|
|
1324
1506
|
const profiles = ctx.modelRegistry.getModelProfiles();
|
|
1325
|
-
const authenticatedProviders = await
|
|
1326
|
-
ctx.modelRegistry.getApiKeyForProvider(provider, getProfileCredentialSessionId()),
|
|
1327
|
-
);
|
|
1507
|
+
const authenticatedProviders = await collectProfileAuthentication(profiles);
|
|
1328
1508
|
return (await Promise.all(
|
|
1329
|
-
projectModelProfileCatalog(profiles, ctx.modelRegistry.getError()).map(async item =>
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1509
|
+
projectModelProfileCatalog(profiles, ctx.modelRegistry.getError()).map(async item => {
|
|
1510
|
+
const profile = profiles.get(item.id)!;
|
|
1511
|
+
const profileAuthenticated = new Set(authenticatedProviders);
|
|
1512
|
+
const proxyProvider = profile.source === "user" ? undefined : tryResolveProxyProviderId(profileSettings);
|
|
1513
|
+
if (proxyProvider !== undefined && profileAuthenticated.has(proxyProvider))
|
|
1514
|
+
for (const provider of getProxyRoutableProviders(profile)) profileAuthenticated.add(provider);
|
|
1515
|
+
const available =
|
|
1516
|
+
isModelProfileProviderAvailable(profile, profileAuthenticated) &&
|
|
1517
|
+
(await resolveProfileAvailability(profile, authenticatedProviders)).available;
|
|
1518
|
+
return { ...item, available };
|
|
1519
|
+
}),
|
|
1337
1520
|
)) as unknown[];
|
|
1338
1521
|
},
|
|
1339
1522
|
installedQueries: policy.installedQueries,
|
|
@@ -1507,6 +1690,134 @@ function rejectionRecoveryIntent(error: unknown): { code: string; message: strin
|
|
|
1507
1690
|
return sanitizePromptFailure(error);
|
|
1508
1691
|
}
|
|
1509
1692
|
|
|
1693
|
+
/**
|
|
1694
|
+
* A provider can finish a stream with an assistant error message rather than
|
|
1695
|
+
* rejecting the prompt promise. The SDK extension receives the final agent_end
|
|
1696
|
+
* after retry/fallback policy has settled, so this is the safe boundary at which
|
|
1697
|
+
* to publish the additive failure diagnostic before terminal reconciliation.
|
|
1698
|
+
*/
|
|
1699
|
+
function providerFailureFromAgentEnd(event: unknown): { code: string; message: string } | undefined {
|
|
1700
|
+
try {
|
|
1701
|
+
if (!event || typeof event !== "object") return undefined;
|
|
1702
|
+
const messages = (event as { messages?: unknown }).messages;
|
|
1703
|
+
if (!Array.isArray(messages)) return undefined;
|
|
1704
|
+
const lastAssistant = [...messages]
|
|
1705
|
+
.reverse()
|
|
1706
|
+
.find(
|
|
1707
|
+
message => message && typeof message === "object" && (message as { role?: unknown }).role === "assistant",
|
|
1708
|
+
);
|
|
1709
|
+
if (!lastAssistant || typeof lastAssistant !== "object") return undefined;
|
|
1710
|
+
const assistant = lastAssistant as {
|
|
1711
|
+
stopReason?: unknown;
|
|
1712
|
+
errorMessage?: unknown;
|
|
1713
|
+
errorKind?: unknown;
|
|
1714
|
+
errorStatus?: unknown;
|
|
1715
|
+
transportFailure?: { status?: unknown };
|
|
1716
|
+
};
|
|
1717
|
+
let stopReason: unknown;
|
|
1718
|
+
try {
|
|
1719
|
+
stopReason = assistant.stopReason;
|
|
1720
|
+
} catch {
|
|
1721
|
+
return undefined;
|
|
1722
|
+
}
|
|
1723
|
+
if (stopReason !== "error") return undefined;
|
|
1724
|
+
let errorKind: unknown;
|
|
1725
|
+
try {
|
|
1726
|
+
errorKind = assistant.errorKind;
|
|
1727
|
+
} catch {
|
|
1728
|
+
return undefined;
|
|
1729
|
+
}
|
|
1730
|
+
if (errorKind === "local_snapshot_failure" || errorKind === "local_buffer_overflow") return undefined;
|
|
1731
|
+
let errorMessage: unknown;
|
|
1732
|
+
try {
|
|
1733
|
+
errorMessage = assistant.errorMessage;
|
|
1734
|
+
} catch {
|
|
1735
|
+
return undefined;
|
|
1736
|
+
}
|
|
1737
|
+
// Agent-local malformed-tool and composer policy circuit breakers also use
|
|
1738
|
+
// stopReason=error, but they are not provider failures and must not be
|
|
1739
|
+
// reported as provider_rejected to SDK callers.
|
|
1740
|
+
if (
|
|
1741
|
+
typeof errorMessage === "string" &&
|
|
1742
|
+
(errorMessage.includes("Composer bash policy blocked repository file I/O again") ||
|
|
1743
|
+
errorMessage.includes("consecutive turns of malformed tool calls"))
|
|
1744
|
+
)
|
|
1745
|
+
return undefined;
|
|
1746
|
+
let status: number | undefined;
|
|
1747
|
+
try {
|
|
1748
|
+
const errorStatus = assistant.errorStatus;
|
|
1749
|
+
if (typeof errorStatus === "number" && Number.isSafeInteger(errorStatus)) status = errorStatus;
|
|
1750
|
+
} catch {
|
|
1751
|
+
// A malformed errorStatus is equivalent to a statusless provider error.
|
|
1752
|
+
}
|
|
1753
|
+
if (status === undefined) {
|
|
1754
|
+
try {
|
|
1755
|
+
const transportStatus = assistant.transportFailure?.status;
|
|
1756
|
+
if (typeof transportStatus === "number" && Number.isSafeInteger(transportStatus)) status = transportStatus;
|
|
1757
|
+
} catch {
|
|
1758
|
+
// A throwing transport metadata accessor is also statusless.
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
if (status === 402 || status === 429)
|
|
1762
|
+
return { code: `provider_http_${status}`, message: "Prompt submission failed." };
|
|
1763
|
+
if (status !== undefined) return { code: "provider_rejected", message: "Prompt submission failed." };
|
|
1764
|
+
return { code: "provider_rejected", message: "Prompt submission failed." };
|
|
1765
|
+
} catch {
|
|
1766
|
+
// Provider metadata is untrusted: an SDK/provider adapter may expose a
|
|
1767
|
+
// throwing getter while the lifecycle boundary still needs to terminalize.
|
|
1768
|
+
return undefined;
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
interface PromptTerminalEvidence {
|
|
1773
|
+
content?: TurnResultContent;
|
|
1774
|
+
hasActivity: boolean;
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
function promptTerminalEvidenceFromAgentEnd(event: unknown): PromptTerminalEvidence {
|
|
1778
|
+
try {
|
|
1779
|
+
if (!event || typeof event !== "object") return { hasActivity: false };
|
|
1780
|
+
const messages = (event as { messages?: unknown }).messages;
|
|
1781
|
+
if (!Array.isArray(messages)) return { hasActivity: false };
|
|
1782
|
+
const assistant = [...messages]
|
|
1783
|
+
.reverse()
|
|
1784
|
+
.find(
|
|
1785
|
+
message => message && typeof message === "object" && (message as { role?: unknown }).role === "assistant",
|
|
1786
|
+
);
|
|
1787
|
+
if (!assistant || typeof assistant !== "object") return { hasActivity: false };
|
|
1788
|
+
const content = (assistant as { content?: unknown }).content;
|
|
1789
|
+
if (typeof content === "string") {
|
|
1790
|
+
const bounded = sanitizeTurnResultContent(content);
|
|
1791
|
+
return { content: bounded, hasActivity: content.trim().length > 0 };
|
|
1792
|
+
}
|
|
1793
|
+
if (Array.isArray(content)) {
|
|
1794
|
+
const hasActivity = content.some(block => {
|
|
1795
|
+
if (block === null || typeof block !== "object") return false;
|
|
1796
|
+
const type = (block as { type?: unknown }).type;
|
|
1797
|
+
if (type === "text") {
|
|
1798
|
+
const text = (block as { text?: unknown }).text;
|
|
1799
|
+
return typeof text === "string" && text.trim().length > 0;
|
|
1800
|
+
}
|
|
1801
|
+
return typeof type === "string" && type.length > 0;
|
|
1802
|
+
});
|
|
1803
|
+
const text = content
|
|
1804
|
+
.filter(
|
|
1805
|
+
(block): block is { type: "text"; text: string } =>
|
|
1806
|
+
block !== null &&
|
|
1807
|
+
typeof block === "object" &&
|
|
1808
|
+
(block as { type?: unknown }).type === "text" &&
|
|
1809
|
+
typeof (block as { text?: unknown }).text === "string",
|
|
1810
|
+
)
|
|
1811
|
+
.map(block => block.text)
|
|
1812
|
+
.join("");
|
|
1813
|
+
return { content: sanitizeTurnResultContent(text), hasActivity };
|
|
1814
|
+
}
|
|
1815
|
+
return { content: sanitizeTurnResultContent(""), hasActivity: false };
|
|
1816
|
+
} catch {
|
|
1817
|
+
return { hasActivity: false };
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1510
1821
|
function createControlSurface(
|
|
1511
1822
|
ctx: ExtensionContext,
|
|
1512
1823
|
api: ExtensionAPI,
|
|
@@ -1516,12 +1827,15 @@ function createControlSurface(
|
|
|
1516
1827
|
correlation: InvocationCorrelation,
|
|
1517
1828
|
connectionId: string | undefined,
|
|
1518
1829
|
startsOwnTurn: boolean,
|
|
1830
|
+
sdkRunToken: string,
|
|
1519
1831
|
) => void,
|
|
1832
|
+
armPromptDeadline: (correlation: InvocationCorrelation) => void,
|
|
1520
1833
|
steerReconciliation: KindAwareReconciliation,
|
|
1521
1834
|
onPromotedTurn?: (
|
|
1522
1835
|
kind: InvocationKind,
|
|
1523
1836
|
correlation: InvocationCorrelation,
|
|
1524
1837
|
connectionId: string | undefined,
|
|
1838
|
+
sdkRunToken: string,
|
|
1525
1839
|
promotion?: { startsOwnRun?: boolean; removed?: boolean },
|
|
1526
1840
|
) => void,
|
|
1527
1841
|
policy?: SdkSurfacePolicy,
|
|
@@ -1543,6 +1857,36 @@ function createControlSurface(
|
|
|
1543
1857
|
canResolveGate: () => boolean = () => true,
|
|
1544
1858
|
trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
|
|
1545
1859
|
): ControlSurface {
|
|
1860
|
+
const normalizePromptImages = (value: unknown): ImageContent[] => {
|
|
1861
|
+
if (!Array.isArray(value)) return [];
|
|
1862
|
+
const normalized: ImageContent[] = [];
|
|
1863
|
+
for (const candidate of value) {
|
|
1864
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
|
|
1865
|
+
const image = candidate as Record<string, unknown>;
|
|
1866
|
+
if (typeof image.data !== "string" || image.data.trim().length === 0) continue;
|
|
1867
|
+
if (image.mimeType !== undefined && typeof image.mimeType !== "string") continue;
|
|
1868
|
+
normalized.push({
|
|
1869
|
+
type: "image",
|
|
1870
|
+
data: image.data,
|
|
1871
|
+
mimeType: typeof image.mimeType === "string" && image.mimeType.length > 0 ? image.mimeType : "image/png",
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
return normalized;
|
|
1875
|
+
};
|
|
1876
|
+
type InternalSendOptions = NonNullable<Parameters<ExtensionAPI["sendUserMessage"]>[1]> & {
|
|
1877
|
+
sdkRunCapability?: SdkRunCapability;
|
|
1878
|
+
};
|
|
1879
|
+
type InternalSdkApi = Omit<ExtensionAPI, "sendUserMessage"> & {
|
|
1880
|
+
sendUserMessage: (
|
|
1881
|
+
content: Parameters<ExtensionAPI["sendUserMessage"]>[0],
|
|
1882
|
+
options?: InternalSendOptions,
|
|
1883
|
+
) => Promise<void>;
|
|
1884
|
+
};
|
|
1885
|
+
const internalApi = api as InternalSdkApi;
|
|
1886
|
+
const sendSdkUserMessage = (
|
|
1887
|
+
content: Parameters<ExtensionAPI["sendUserMessage"]>[0],
|
|
1888
|
+
options?: Record<string, unknown>,
|
|
1889
|
+
): Promise<void> => internalApi.sendUserMessage(content, options);
|
|
1546
1890
|
const surfacePolicy =
|
|
1547
1891
|
policy ?? createSdkSurfacePolicyForContext(ctx, hasSdkWorkflowGateCapability(ctx.workflowGate));
|
|
1548
1892
|
const typed = (operation: string, input: Record<string, unknown> = {}) =>
|
|
@@ -1627,6 +1971,7 @@ function createControlSurface(
|
|
|
1627
1971
|
kind: InvocationKind,
|
|
1628
1972
|
clientRef: string | undefined,
|
|
1629
1973
|
run: (options: {
|
|
1974
|
+
sdkRunCapability: SdkRunCapability;
|
|
1630
1975
|
onPreflightAccepted: () => void;
|
|
1631
1976
|
onPreflightAcceptCommit: () => Promise<void>;
|
|
1632
1977
|
/** Internal disposition before a queued submission is actually consumed. */
|
|
@@ -1646,6 +1991,8 @@ function createControlSurface(
|
|
|
1646
1991
|
const retainedClientRef = normalizeClientRef(clientRef);
|
|
1647
1992
|
reconciliation.admit(kind, retainedClientRef);
|
|
1648
1993
|
const correlation = newCorrelation();
|
|
1994
|
+
const sdkRunToken = `${correlation.commandId}:${correlation.turnId}`;
|
|
1995
|
+
const sdkRunCapability = createSdkRunCapability(sdkRunToken);
|
|
1649
1996
|
const preflight = Promise.withResolvers<void>();
|
|
1650
1997
|
let accepted = false;
|
|
1651
1998
|
let settled = false;
|
|
@@ -1669,10 +2016,11 @@ function createControlSurface(
|
|
|
1669
2016
|
await reconciliation.noteAccepted(kind, correlation, retainedClientRef);
|
|
1670
2017
|
accepted = true;
|
|
1671
2018
|
settled = true;
|
|
2019
|
+
if (kind === "prompt" && startsOwnTurn) armPromptDeadline(correlation);
|
|
1672
2020
|
// The accepted submission does NOT own the active turn until its run
|
|
1673
2021
|
// actually STARTS: the connection is carried on the pending entry and
|
|
1674
2022
|
// associated at agent_start instead (review thread P1).
|
|
1675
|
-
onAccepted(kind, correlation, requesterConnectionId, startsOwnTurn);
|
|
2023
|
+
onAccepted(kind, correlation, requesterConnectionId, startsOwnTurn, sdkRunToken);
|
|
1676
2024
|
preflight.resolve();
|
|
1677
2025
|
} catch (error) {
|
|
1678
2026
|
settled = true;
|
|
@@ -1704,6 +2052,7 @@ function createControlSurface(
|
|
|
1704
2052
|
const submission = Promise.resolve(
|
|
1705
2053
|
run({
|
|
1706
2054
|
onPreflightAccepted: () => void accept().catch(() => undefined),
|
|
2055
|
+
sdkRunCapability,
|
|
1707
2056
|
onPreflightAcceptCommit: accept,
|
|
1708
2057
|
onDispatchDisposition: promotion => {
|
|
1709
2058
|
promotionStartsOwnRun = promotion.startsOwnRun;
|
|
@@ -1724,7 +2073,7 @@ function createControlSurface(
|
|
|
1724
2073
|
// terminal-abort that turn (review threads P1/P2).
|
|
1725
2074
|
onQueuedPromoted: (promotion?: { startsOwnRun?: boolean; removed?: boolean }) => {
|
|
1726
2075
|
promotionStartsOwnRun = promotion?.startsOwnRun;
|
|
1727
|
-
onPromotedTurn?.(kind, correlation, requesterConnectionId, promotion);
|
|
2076
|
+
onPromotedTurn?.(kind, correlation, requesterConnectionId, sdkRunToken, promotion);
|
|
1728
2077
|
},
|
|
1729
2078
|
queuedAtDispatch,
|
|
1730
2079
|
}),
|
|
@@ -1768,7 +2117,7 @@ function createControlSurface(
|
|
|
1768
2117
|
});
|
|
1769
2118
|
retirePendingOwner?.(kind, correlation, "always");
|
|
1770
2119
|
} catch (transitionError) {
|
|
1771
|
-
if (kind === "prompt") {
|
|
2120
|
+
if (kind === "prompt" || kind === "skill") {
|
|
1772
2121
|
retirePendingOwner?.(kind, correlation, "recover-terminal");
|
|
1773
2122
|
return;
|
|
1774
2123
|
}
|
|
@@ -1830,7 +2179,7 @@ function createControlSurface(
|
|
|
1830
2179
|
try {
|
|
1831
2180
|
await reconciliation.noteTransition(kind, correlation, { type: "agent_failed", error });
|
|
1832
2181
|
} catch {
|
|
1833
|
-
if (kind === "prompt") {
|
|
2182
|
+
if (kind === "prompt" || kind === "skill") {
|
|
1834
2183
|
retirePendingOwner?.(
|
|
1835
2184
|
kind,
|
|
1836
2185
|
correlation,
|
|
@@ -1863,7 +2212,7 @@ function createControlSurface(
|
|
|
1863
2212
|
try {
|
|
1864
2213
|
await reconciliation.noteTransition(kind, correlation, { type: "agent_end" });
|
|
1865
2214
|
} catch (transitionError) {
|
|
1866
|
-
if (kind === "prompt") {
|
|
2215
|
+
if (kind === "prompt" || kind === "skill") {
|
|
1867
2216
|
retirePendingOwner?.(
|
|
1868
2217
|
kind,
|
|
1869
2218
|
correlation,
|
|
@@ -2783,48 +3132,77 @@ function createControlSurface(
|
|
|
2783
3132
|
}
|
|
2784
3133
|
};
|
|
2785
3134
|
return {
|
|
2786
|
-
prompt: async (text, images, clientRef) =>
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
3135
|
+
prompt: async (text, images, clientRef) => {
|
|
3136
|
+
const invalid = validateRequiredPromptText("turn.prompt", { text, images });
|
|
3137
|
+
if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
|
|
3138
|
+
return await submit("prompt", clientRef, ({ queuedAtDispatch, sdkRunCapability, ...options }) =>
|
|
3139
|
+
sendSdkUserMessage(
|
|
3140
|
+
typeof images === "undefined"
|
|
3141
|
+
? text
|
|
3142
|
+
: ([{ type: "text", text }, ...normalizePromptImages(images)] as [
|
|
3143
|
+
{ type: "text"; text: string },
|
|
3144
|
+
...ImageContent[],
|
|
3145
|
+
]),
|
|
3146
|
+
{
|
|
3147
|
+
...options,
|
|
3148
|
+
sdkRunCapability,
|
|
3149
|
+
// ACP terminal settlement is owned by the correlated agent_end
|
|
3150
|
+
// publication. Post-prompt recovery may include independent
|
|
3151
|
+
// subagent work and must not hold that client-facing boundary.
|
|
3152
|
+
...(queuedAtDispatch ? { queuedAtDispatch: true } : {}),
|
|
3153
|
+
},
|
|
2791
3154
|
),
|
|
2792
|
-
)
|
|
3155
|
+
);
|
|
3156
|
+
},
|
|
2793
3157
|
steer: async (text, clientRef) => {
|
|
3158
|
+
const invalid = validateRequiredPromptText("turn.steer", { text });
|
|
3159
|
+
if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
|
|
2794
3160
|
const retainedClientRef = normalizeClientRef(clientRef);
|
|
2795
3161
|
if (retainedClientRef === undefined) {
|
|
2796
3162
|
const correlation = newCorrelation();
|
|
2797
|
-
await
|
|
3163
|
+
await sendSdkUserMessage(text, { deliverAs: "steer" });
|
|
2798
3164
|
return { accepted: true, ...correlation };
|
|
2799
3165
|
}
|
|
2800
3166
|
const durable = steerReconciliation;
|
|
2801
3167
|
const reservation = await durable.reserveSteer(retainedClientRef, text);
|
|
2802
3168
|
if (reservation.replay) return { accepted: reservation.result.status === "accepted", ...reservation.result };
|
|
2803
3169
|
try {
|
|
2804
|
-
await
|
|
3170
|
+
await sendSdkUserMessage(text, { deliverAs: "steer" });
|
|
2805
3171
|
return { accepted: true, ...(await durable.settleSteer(retainedClientRef, "accepted")) };
|
|
2806
3172
|
} catch (error) {
|
|
2807
3173
|
return { accepted: false, ...(await durable.settleSteer(retainedClientRef, "rejected", error)) };
|
|
2808
3174
|
}
|
|
2809
3175
|
},
|
|
2810
|
-
followUp: async text =>
|
|
2811
|
-
|
|
3176
|
+
followUp: async text => {
|
|
3177
|
+
const invalid = validateRequiredPromptText("turn.follow_up", { text });
|
|
3178
|
+
if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
|
|
3179
|
+
return await submit(
|
|
2812
3180
|
"prompt",
|
|
2813
3181
|
undefined,
|
|
2814
|
-
|
|
3182
|
+
({ sdkRunCapability, ...options }) =>
|
|
3183
|
+
sendSdkUserMessage(text, {
|
|
3184
|
+
...options,
|
|
3185
|
+
sdkRunCapability,
|
|
3186
|
+
deliverAs: "followUp",
|
|
3187
|
+
}),
|
|
2815
3188
|
undefined,
|
|
2816
3189
|
false,
|
|
2817
3190
|
// Follow-ups never start inline; ownership correlates at promotion.
|
|
2818
3191
|
true,
|
|
2819
|
-
)
|
|
3192
|
+
);
|
|
3193
|
+
},
|
|
2820
3194
|
abort: async () => {
|
|
2821
3195
|
await Promise.resolve(ctx.abort()).catch(() => undefined);
|
|
2822
3196
|
return { aborted: true };
|
|
2823
3197
|
},
|
|
2824
3198
|
abortTerminal: terminalAbort,
|
|
2825
3199
|
abortAndPrompt: async text => {
|
|
3200
|
+
const invalid = validateRequiredPromptText("turn.abort_and_prompt", { text });
|
|
3201
|
+
if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
|
|
2826
3202
|
await ctx.abort();
|
|
2827
|
-
return await submit("prompt", undefined,
|
|
3203
|
+
return await submit("prompt", undefined, ({ sdkRunCapability, ...options }) =>
|
|
3204
|
+
sendSdkUserMessage(text, { ...options, sdkRunCapability }),
|
|
3205
|
+
);
|
|
2828
3206
|
},
|
|
2829
3207
|
answerAsk: unavailable("ask.answer"),
|
|
2830
3208
|
answerGate: async (id, response, expectedSessionId, idempotencyKey) =>
|
|
@@ -2851,9 +3229,10 @@ function createControlSurface(
|
|
|
2851
3229
|
return await submit(
|
|
2852
3230
|
"skill",
|
|
2853
3231
|
clientRef,
|
|
2854
|
-
options =>
|
|
3232
|
+
({ sdkRunCapability, ...options }) =>
|
|
2855
3233
|
ctx.invokeSkill!(name, args, {
|
|
2856
3234
|
...options,
|
|
3235
|
+
sdkRunCapability,
|
|
2857
3236
|
onSkillPrepared: meta => {
|
|
2858
3237
|
prepared = meta;
|
|
2859
3238
|
},
|
|
@@ -3033,10 +3412,28 @@ export function registerSdkOnlyNotificationCommand(api: ExtensionAPI): void {
|
|
|
3033
3412
|
});
|
|
3034
3413
|
}
|
|
3035
3414
|
|
|
3415
|
+
function quiescingFrame(frame: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
3416
|
+
const error = { code: "session_quiescing", message: "The session endpoint is being replaced." };
|
|
3417
|
+
const id = typeof frame.id === "string" ? frame.id : "";
|
|
3418
|
+
if (frame.type === "control_request") return { type: "control_response", id, ok: false, error };
|
|
3419
|
+
if (frame.type === "query_request") return { type: "query_response", id, ok: false, error };
|
|
3420
|
+
if (frame.type === "event_replay") return { type: "event_replay_result", id, ok: false, error };
|
|
3421
|
+
if (frame.type === "session_activate")
|
|
3422
|
+
return { type: "session_activate_result", id, ok: false, status: "authority_unavailable", error };
|
|
3423
|
+
if (frame.type === "register_provider") return { type: "register_provider_result", id, ok: false, error };
|
|
3424
|
+
if (frame.type === "reverse_response")
|
|
3425
|
+
return { type: "transport_error", id, code: error.code, message: error.message };
|
|
3426
|
+
if (frame.type === "provider_heartbeat" || frame.type === "lease_release")
|
|
3427
|
+
return { type: "transport_error", code: "session_quiescing", message: error.message };
|
|
3428
|
+
return undefined;
|
|
3429
|
+
}
|
|
3430
|
+
|
|
3036
3431
|
/** Install a complete SDK host for a session when notifications are inactive. */
|
|
3037
3432
|
export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: CreateSdkSessionRuntimeOptions): void {
|
|
3038
3433
|
let active:
|
|
3039
3434
|
| {
|
|
3435
|
+
sessionId: string;
|
|
3436
|
+
sessionIdentity: string;
|
|
3040
3437
|
runtime: SessionSdkSessionRuntime;
|
|
3041
3438
|
revisions: RevisionStore;
|
|
3042
3439
|
cursors: CursorRegistry;
|
|
@@ -3047,8 +3444,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3047
3444
|
kind: InvocationKind;
|
|
3048
3445
|
correlation: InvocationCorrelation;
|
|
3049
3446
|
connectionId: string | undefined;
|
|
3447
|
+
sdkRunToken: string;
|
|
3050
3448
|
}>;
|
|
3051
3449
|
registerBroker: () => Promise<void>;
|
|
3450
|
+
quiesceInput: () => void;
|
|
3052
3451
|
fenceGateResolutions: () => void;
|
|
3053
3452
|
waitForGateResolutionQuiescence: () => Promise<void>;
|
|
3054
3453
|
activeInvocation?: { kind: InvocationKind; correlation: InvocationCorrelation };
|
|
@@ -3060,18 +3459,163 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3060
3459
|
kind: InvocationKind;
|
|
3061
3460
|
correlation: InvocationCorrelation;
|
|
3062
3461
|
connectionId: string | undefined;
|
|
3462
|
+
sdkRunToken: string;
|
|
3063
3463
|
}>;
|
|
3064
3464
|
attachedInvocations: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
|
|
3065
3465
|
}>;
|
|
3066
3466
|
disposeGate?: () => void;
|
|
3067
3467
|
lifecycleActive: boolean;
|
|
3068
3468
|
lifecycleEpoch: number;
|
|
3469
|
+
failureDiagnosticKeys: Set<string>;
|
|
3470
|
+
failureDiagnosticCodes: Map<string, string>;
|
|
3471
|
+
lifecycleTasks: Set<Promise<void>>;
|
|
3069
3472
|
/** Failure reasons whose durable agent_failed write failed; the
|
|
3070
3473
|
* subsequent agent_end must re-record them before terminalizing or
|
|
3071
3474
|
* the record classifies terminal_ok (exact-head review P1). */
|
|
3072
3475
|
unrecordedFailureReasons?: Map<string, { code: string; message: string }>;
|
|
3073
3476
|
}
|
|
3074
3477
|
| undefined;
|
|
3478
|
+
type RuntimeState = NonNullable<typeof active>;
|
|
3479
|
+
type LifecycleBatch = RuntimeState["openLifecycleBatches"][number];
|
|
3480
|
+
type LifecycleOwner = { state: RuntimeState; sessionId: string; batch?: LifecycleBatch };
|
|
3481
|
+
const retiredLifecycleOwners = new Map<string, RuntimeState[]>();
|
|
3482
|
+
const retiredLifecycleOwnerTimers = new Map<RuntimeState, ReturnType<typeof setTimeout>>();
|
|
3483
|
+
const skillRecoveryControllers = new Map<string, AbortController>();
|
|
3484
|
+
const skillTerminalRecoveryControllers = new Map<string, AbortController>();
|
|
3485
|
+
const ambiguousLifecycleIdentities = new Set<string>();
|
|
3486
|
+
const lifecycleRunOwners = new Map<
|
|
3487
|
+
string,
|
|
3488
|
+
{ state: RuntimeState; batch?: LifecycleBatch; correlationKey?: string }
|
|
3489
|
+
>();
|
|
3490
|
+
const lifecycleCorrelationKey = (correlation: InvocationCorrelation): string =>
|
|
3491
|
+
`${correlation.commandId}:${correlation.turnId}`;
|
|
3492
|
+
const sessionIdentityForContext = (ctx: ExtensionContext): string | undefined => {
|
|
3493
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
3494
|
+
const sessionFile = ctx.sessionManager.getSessionFile?.();
|
|
3495
|
+
if (sessionFile) return `${sessionId}\u0000${sessionFile}`;
|
|
3496
|
+
const stateRoot = path.join(ctx.cwd, ".gjc", "state");
|
|
3497
|
+
return `${sessionId}\u0000${resolveReconciliationSessionFile(undefined, stateRoot, sessionId)}`;
|
|
3498
|
+
};
|
|
3499
|
+
const lifecycleStateForContext = (
|
|
3500
|
+
ctx: ExtensionContext,
|
|
3501
|
+
type: "agent_start" | "agent_end" | "agent_failed" = "agent_start",
|
|
3502
|
+
): RuntimeState | undefined => {
|
|
3503
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
3504
|
+
const sessionIdentity = sessionIdentityForContext(ctx);
|
|
3505
|
+
if (!sessionIdentity) return undefined;
|
|
3506
|
+
const retired = (retiredLifecycleOwners.get(sessionId) ?? []).filter(
|
|
3507
|
+
owner => owner.sessionIdentity === sessionIdentity,
|
|
3508
|
+
);
|
|
3509
|
+
if (type !== "agent_start") {
|
|
3510
|
+
if (ambiguousLifecycleIdentities.has(sessionIdentity)) return undefined;
|
|
3511
|
+
if (retired.length > 1) {
|
|
3512
|
+
ambiguousLifecycleIdentities.add(sessionIdentity);
|
|
3513
|
+
return undefined;
|
|
3514
|
+
}
|
|
3515
|
+
const delayedOwner = retired.find(owner => owner.openLifecycleBatches.length > 0);
|
|
3516
|
+
if (delayedOwner) {
|
|
3517
|
+
if (active?.sessionIdentity === sessionIdentity && active.lifecycleActive) {
|
|
3518
|
+
ambiguousLifecycleIdentities.add(sessionIdentity);
|
|
3519
|
+
return undefined;
|
|
3520
|
+
}
|
|
3521
|
+
return delayedOwner;
|
|
3522
|
+
}
|
|
3523
|
+
}
|
|
3524
|
+
if (active?.sessionIdentity === sessionIdentity) return active;
|
|
3525
|
+
return retired[0];
|
|
3526
|
+
};
|
|
3527
|
+
const lifecycleStateForEvent = (
|
|
3528
|
+
ctx: ExtensionContext,
|
|
3529
|
+
type: "agent_start" | "agent_end" | "agent_failed",
|
|
3530
|
+
sdkRunToken: unknown,
|
|
3531
|
+
): RuntimeState | undefined =>
|
|
3532
|
+
typeof sdkRunToken === "string" && sdkRunToken.length > 0
|
|
3533
|
+
? lifecycleRunOwners.get(sdkRunToken)?.state
|
|
3534
|
+
: lifecycleStateForContext(ctx, type);
|
|
3535
|
+
const removeRetiredLifecycleOwner = (owner: RuntimeState): void => {
|
|
3536
|
+
const timer = retiredLifecycleOwnerTimers.get(owner);
|
|
3537
|
+
if (timer !== undefined) {
|
|
3538
|
+
clearTimeout(timer);
|
|
3539
|
+
retiredLifecycleOwnerTimers.delete(owner);
|
|
3540
|
+
}
|
|
3541
|
+
const owners = retiredLifecycleOwners.get(owner.sessionId);
|
|
3542
|
+
if (!owners) {
|
|
3543
|
+
ambiguousLifecycleIdentities.delete(owner.sessionIdentity);
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
const remaining = owners.filter(candidate => candidate !== owner);
|
|
3547
|
+
if (remaining.length === 0) retiredLifecycleOwners.delete(owner.sessionId);
|
|
3548
|
+
else retiredLifecycleOwners.set(owner.sessionId, remaining);
|
|
3549
|
+
const remainingSameIdentity = remaining.filter(candidate => candidate.sessionIdentity === owner.sessionIdentity);
|
|
3550
|
+
if (remainingSameIdentity.length < 2) ambiguousLifecycleIdentities.delete(owner.sessionIdentity);
|
|
3551
|
+
// Expiry is the final lifecycle-reference boundary. Do not retain
|
|
3552
|
+
// diagnostic suppression or failed-write recovery state after the owner is
|
|
3553
|
+
// no longer eligible to receive a delayed event.
|
|
3554
|
+
owner.failureDiagnosticKeys.clear();
|
|
3555
|
+
owner.failureDiagnosticCodes.clear();
|
|
3556
|
+
owner.unrecordedFailureReasons?.clear();
|
|
3557
|
+
for (const [token, binding] of lifecycleRunOwners) if (binding.state === owner) lifecycleRunOwners.delete(token);
|
|
3558
|
+
};
|
|
3559
|
+
const maybeRetireLifecycleOwner = (owner: RuntimeState): void => {
|
|
3560
|
+
if (
|
|
3561
|
+
owner.pending.length > 0 ||
|
|
3562
|
+
owner.openLifecycleBatches.length > 0 ||
|
|
3563
|
+
(owner.attachedInvocations?.length ?? 0) > 0 ||
|
|
3564
|
+
(owner.drainedInvocations?.length ?? 0) > 0 ||
|
|
3565
|
+
owner.lifecycleTasks.size > 0
|
|
3566
|
+
)
|
|
3567
|
+
return;
|
|
3568
|
+
removeRetiredLifecycleOwner(owner);
|
|
3569
|
+
};
|
|
3570
|
+
const removeLifecycleTokenAliases = (owner: RuntimeState, correlation: InvocationCorrelation): void => {
|
|
3571
|
+
const target = lifecycleCorrelationKey(correlation);
|
|
3572
|
+
for (const [token, binding] of lifecycleRunOwners) {
|
|
3573
|
+
if (binding.state === owner && binding.correlationKey === target) lifecycleRunOwners.delete(token);
|
|
3574
|
+
}
|
|
3575
|
+
};
|
|
3576
|
+
const removeLifecycleReferences = (owner: RuntimeState, correlation: InvocationCorrelation): void => {
|
|
3577
|
+
const target = lifecycleCorrelationKey(correlation);
|
|
3578
|
+
const retainedPending = owner.pending.filter(entry => lifecycleCorrelationKey(entry.correlation) !== target);
|
|
3579
|
+
owner.pending.splice(0, owner.pending.length, ...retainedPending);
|
|
3580
|
+
for (const batch of owner.openLifecycleBatches) {
|
|
3581
|
+
batch.invocations = batch.invocations.filter(entry => lifecycleCorrelationKey(entry.correlation) !== target);
|
|
3582
|
+
batch.attachedInvocations = batch.attachedInvocations.filter(
|
|
3583
|
+
entry => lifecycleCorrelationKey(entry.correlation) !== target,
|
|
3584
|
+
);
|
|
3585
|
+
}
|
|
3586
|
+
owner.openLifecycleBatches = owner.openLifecycleBatches.filter(
|
|
3587
|
+
batch => batch.invocations.length > 0 || batch.attachedInvocations.length > 0,
|
|
3588
|
+
);
|
|
3589
|
+
owner.drainedInvocations = owner.drainedInvocations?.filter(
|
|
3590
|
+
entry => lifecycleCorrelationKey(entry.correlation) !== target,
|
|
3591
|
+
);
|
|
3592
|
+
owner.attachedInvocations = owner.attachedInvocations?.filter(
|
|
3593
|
+
entry => lifecycleCorrelationKey(entry.correlation) !== target,
|
|
3594
|
+
);
|
|
3595
|
+
if (owner.activeInvocation && lifecycleCorrelationKey(owner.activeInvocation.correlation) === target) {
|
|
3596
|
+
const replacement =
|
|
3597
|
+
owner.openLifecycleBatches.flatMap(batch => batch.invocations)[0] ?? owner.attachedInvocations?.[0];
|
|
3598
|
+
owner.activeInvocation = replacement
|
|
3599
|
+
? { kind: replacement.kind, correlation: replacement.correlation }
|
|
3600
|
+
: undefined;
|
|
3601
|
+
}
|
|
3602
|
+
removeLifecycleTokenAliases(owner, correlation);
|
|
3603
|
+
if (owner === active) {
|
|
3604
|
+
const owners = new Set<string>();
|
|
3605
|
+
const activeBatch = owner.activeInvocation
|
|
3606
|
+
? owner.openLifecycleBatches.find(batch =>
|
|
3607
|
+
batch.invocations.some(
|
|
3608
|
+
entry =>
|
|
3609
|
+
lifecycleCorrelationKey(entry.correlation) ===
|
|
3610
|
+
lifecycleCorrelationKey(owner.activeInvocation!.correlation),
|
|
3611
|
+
),
|
|
3612
|
+
)
|
|
3613
|
+
: undefined;
|
|
3614
|
+
for (const entry of activeBatch?.invocations ?? [])
|
|
3615
|
+
if (entry.connectionId !== undefined) owners.add(entry.connectionId);
|
|
3616
|
+
activePromptOwnerHolder.connectionIds = owners.size > 0 ? owners : undefined;
|
|
3617
|
+
}
|
|
3618
|
+
};
|
|
3075
3619
|
// Shared with the control surface's terminal abort: the correlated
|
|
3076
3620
|
// agent_end publication capture (AC 19). terminalAbort installs a fresh
|
|
3077
3621
|
// resolver before settling the abort; emitLifecycle resolves it with the
|
|
@@ -3089,15 +3633,31 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3089
3633
|
// turn it did not submit, and never set for agent-initiated turns (review
|
|
3090
3634
|
// thread P1).
|
|
3091
3635
|
const activePromptOwnerHolder: { connectionIds?: Set<string>; lifecycleEpoch?: number } = {};
|
|
3636
|
+
let nextLifecycleEpoch = 0;
|
|
3092
3637
|
const skillTerminalRecoveryKeys = new Set<string>();
|
|
3638
|
+
const trackLifecycle = (handler: () => Promise<void>, owner: RuntimeState | undefined): Promise<void> => {
|
|
3639
|
+
if (!owner) return Promise.resolve();
|
|
3640
|
+
let task: Promise<void>;
|
|
3641
|
+
task = handler().finally(() => {
|
|
3642
|
+
owner.lifecycleTasks.delete(task);
|
|
3643
|
+
maybeRetireLifecycleOwner(owner);
|
|
3644
|
+
});
|
|
3645
|
+
owner.lifecycleTasks.add(task);
|
|
3646
|
+
return task;
|
|
3647
|
+
};
|
|
3093
3648
|
const emitLifecycle = async (
|
|
3094
3649
|
type: "agent_start" | "agent_end" | "agent_failed",
|
|
3095
3650
|
ctx: ExtensionContext,
|
|
3096
3651
|
failureCause?: unknown,
|
|
3097
3652
|
maintenanceOutcome?: string,
|
|
3653
|
+
lifecycleOwner?: LifecycleOwner,
|
|
3654
|
+
terminalContent?: TurnResultContent,
|
|
3655
|
+
terminalHasActivity = false,
|
|
3656
|
+
terminalOutcome?: { kind: "stopped"; reason: "cancelled"; provenance: "client_cancel" },
|
|
3098
3657
|
): Promise<void> => {
|
|
3099
|
-
const current = active;
|
|
3658
|
+
const current = lifecycleOwner?.state ?? active;
|
|
3100
3659
|
if (!current) return;
|
|
3660
|
+
const failureBatch = lifecycleOwner?.batch;
|
|
3101
3661
|
const adoptLifecycleBatch = (
|
|
3102
3662
|
batch:
|
|
3103
3663
|
| Array<{
|
|
@@ -3111,7 +3671,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3111
3671
|
current.activeInvocation = undefined;
|
|
3112
3672
|
current.drainedInvocations = undefined;
|
|
3113
3673
|
current.attachedInvocations = undefined;
|
|
3114
|
-
activePromptOwnerHolder.connectionIds = undefined;
|
|
3674
|
+
if (current === active) activePromptOwnerHolder.connectionIds = undefined;
|
|
3115
3675
|
return;
|
|
3116
3676
|
}
|
|
3117
3677
|
current.activeInvocation = batch[0];
|
|
@@ -3119,19 +3679,20 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3119
3679
|
current.attachedInvocations = undefined;
|
|
3120
3680
|
const owners = new Set<string>();
|
|
3121
3681
|
for (const entry of batch) if (entry.connectionId !== undefined) owners.add(entry.connectionId);
|
|
3122
|
-
activePromptOwnerHolder.connectionIds = owners;
|
|
3682
|
+
if (current === active) activePromptOwnerHolder.connectionIds = owners;
|
|
3123
3683
|
};
|
|
3124
3684
|
let transitions: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }> = [];
|
|
3125
3685
|
if (type === "agent_failed") {
|
|
3126
|
-
const activeInvocation = current.activeInvocation;
|
|
3686
|
+
const activeInvocation = failureBatch?.invocations[0] ?? current.activeInvocation;
|
|
3127
3687
|
const activeBatch = activeInvocation
|
|
3128
|
-
?
|
|
3688
|
+
? (failureBatch ??
|
|
3689
|
+
current.openLifecycleBatches.find(batch =>
|
|
3129
3690
|
batch.invocations.some(
|
|
3130
3691
|
entry =>
|
|
3131
3692
|
entry.correlation.commandId === activeInvocation.correlation.commandId &&
|
|
3132
3693
|
entry.correlation.turnId === activeInvocation.correlation.turnId,
|
|
3133
3694
|
),
|
|
3134
|
-
)
|
|
3695
|
+
))
|
|
3135
3696
|
: undefined;
|
|
3136
3697
|
const fallback =
|
|
3137
3698
|
activeBatch?.invocations ??
|
|
@@ -3152,8 +3713,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3152
3713
|
);
|
|
3153
3714
|
transitions = [...fallback, ...attached].map(({ kind, correlation }) => ({ kind, correlation }));
|
|
3154
3715
|
} else if (type === "agent_start") {
|
|
3155
|
-
current.lifecycleEpoch
|
|
3156
|
-
activePromptOwnerHolder.lifecycleEpoch = current.lifecycleEpoch;
|
|
3716
|
+
current.lifecycleEpoch = ++nextLifecycleEpoch;
|
|
3717
|
+
if (current === active) activePromptOwnerHolder.lifecycleEpoch = current.lifecycleEpoch;
|
|
3157
3718
|
// Mark lifecycle active even when the drain is empty: a monitor/cron
|
|
3158
3719
|
// run started by the session has no SDK pending entry but is still a
|
|
3159
3720
|
// real active run that later in-run promotions must attach to instead
|
|
@@ -3180,6 +3741,11 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3180
3741
|
if (current.activeInvocation?.kind === "prompt") {
|
|
3181
3742
|
current.deadlineManager.onAccepted(current.activeInvocation.correlation);
|
|
3182
3743
|
}
|
|
3744
|
+
} else {
|
|
3745
|
+
// An empty drain is an agent-initiated successor run. Clear the
|
|
3746
|
+
// predecessor's SDK owner and active invocation so abort ownership and
|
|
3747
|
+
// tool-progress renewal cannot leak into the successor.
|
|
3748
|
+
adoptLifecycleBatch(undefined);
|
|
3183
3749
|
}
|
|
3184
3750
|
transitions = drained.map(({ kind, correlation }) => ({ kind, correlation }));
|
|
3185
3751
|
} else {
|
|
@@ -3211,8 +3777,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3211
3777
|
// may already have advanced to. Publication proof and race retirement must
|
|
3212
3778
|
// use the same batch identity as reconciliation.
|
|
3213
3779
|
const eventLifecycleEpoch =
|
|
3214
|
-
type === "agent_end"
|
|
3215
|
-
?
|
|
3780
|
+
(type === "agent_failed" || type === "agent_end") && failureBatch
|
|
3781
|
+
? failureBatch.epoch
|
|
3216
3782
|
: current.lifecycleEpoch;
|
|
3217
3783
|
const resolveTerminalPublicationWaiters = (observed: boolean): void => {
|
|
3218
3784
|
const waiters = terminalPublicationCapture.waiters;
|
|
@@ -3223,17 +3789,19 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3223
3789
|
for (const waiter of matched) waiter.resolve(observed);
|
|
3224
3790
|
};
|
|
3225
3791
|
const retireEndedLifecycleBatch = (): void => {
|
|
3226
|
-
const ended = current.openLifecycleBatches[0];
|
|
3792
|
+
const ended = failureBatch ?? current.openLifecycleBatches[0];
|
|
3227
3793
|
if (!ended) return;
|
|
3228
3794
|
const transitionKeys = new Set(
|
|
3229
3795
|
transitions.map(({ correlation }) => `${correlation.commandId}:${correlation.turnId}`),
|
|
3230
3796
|
);
|
|
3231
3797
|
if (
|
|
3232
|
-
ended.invocations.some(({ correlation }) =>
|
|
3798
|
+
!ended.invocations.some(({ correlation }) =>
|
|
3233
3799
|
transitionKeys.has(`${correlation.commandId}:${correlation.turnId}`),
|
|
3234
3800
|
)
|
|
3235
3801
|
)
|
|
3236
|
-
|
|
3802
|
+
return;
|
|
3803
|
+
const index = current.openLifecycleBatches.indexOf(ended);
|
|
3804
|
+
if (index >= 0) current.openLifecycleBatches.splice(index, 1);
|
|
3237
3805
|
};
|
|
3238
3806
|
const scheduleSkillTerminalRecovery = (invocation: {
|
|
3239
3807
|
kind: InvocationKind;
|
|
@@ -3243,6 +3811,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3243
3811
|
const recoveryKey = `${invocation.correlation.commandId}:${invocation.correlation.turnId}`;
|
|
3244
3812
|
if (skillTerminalRecoveryKeys.has(recoveryKey)) return;
|
|
3245
3813
|
skillTerminalRecoveryKeys.add(recoveryKey);
|
|
3814
|
+
const controller = new AbortController();
|
|
3815
|
+
skillTerminalRecoveryControllers.set(recoveryKey, controller);
|
|
3246
3816
|
const attempt = async (): Promise<void> => {
|
|
3247
3817
|
try {
|
|
3248
3818
|
const unrecorded = current.unrecordedFailureReasons?.get(recoveryKey);
|
|
@@ -3255,6 +3825,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3255
3825
|
}
|
|
3256
3826
|
await current.reconciliation.noteTransition("skill", invocation.correlation, { type: "agent_end" });
|
|
3257
3827
|
skillTerminalRecoveryKeys.delete(recoveryKey);
|
|
3828
|
+
current.failureDiagnosticKeys.delete(recoveryKey);
|
|
3258
3829
|
for (const batch of current.openLifecycleBatches) {
|
|
3259
3830
|
batch.invocations = batch.invocations.filter(
|
|
3260
3831
|
entry =>
|
|
@@ -3277,16 +3848,33 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3277
3848
|
adoptLifecycleBatch(current.openLifecycleBatches[0]?.invocations);
|
|
3278
3849
|
if (current.openLifecycleBatches.length === 0) current.lifecycleActive = false;
|
|
3279
3850
|
} catch (error) {
|
|
3851
|
+
if (controller.signal.aborted) {
|
|
3852
|
+
skillTerminalRecoveryKeys.delete(recoveryKey);
|
|
3853
|
+
return;
|
|
3854
|
+
}
|
|
3280
3855
|
logger.error("SDK skill lifecycle terminal recovery retrying", {
|
|
3281
3856
|
commandId: invocation.correlation.commandId,
|
|
3282
3857
|
turnId: invocation.correlation.turnId,
|
|
3283
3858
|
error: sanitizePromptFailure(error),
|
|
3284
3859
|
});
|
|
3285
|
-
|
|
3860
|
+
const wait = Promise.withResolvers<void>();
|
|
3861
|
+
const timer = setTimeout(wait.resolve, 1_000);
|
|
3862
|
+
timer.unref();
|
|
3863
|
+
const onAbort = () => {
|
|
3864
|
+
clearTimeout(timer);
|
|
3865
|
+
wait.resolve();
|
|
3866
|
+
};
|
|
3867
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
3868
|
+
await wait.promise;
|
|
3869
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
3870
|
+
if (controller.signal.aborted) {
|
|
3871
|
+
skillTerminalRecoveryKeys.delete(recoveryKey);
|
|
3872
|
+
return;
|
|
3873
|
+
}
|
|
3286
3874
|
return attempt();
|
|
3287
3875
|
}
|
|
3288
3876
|
};
|
|
3289
|
-
void attempt();
|
|
3877
|
+
void attempt().finally(() => skillTerminalRecoveryControllers.delete(recoveryKey));
|
|
3290
3878
|
};
|
|
3291
3879
|
if (type === "agent_end" && maintenanceOutcome !== undefined && maintenanceOutcome !== "aborted") {
|
|
3292
3880
|
try {
|
|
@@ -3296,6 +3884,20 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3296
3884
|
}
|
|
3297
3885
|
return;
|
|
3298
3886
|
}
|
|
3887
|
+
const correlationKey = (correlation: InvocationCorrelation): string =>
|
|
3888
|
+
`${correlation.commandId}:${correlation.turnId}`;
|
|
3889
|
+
if (type === "agent_failed") {
|
|
3890
|
+
transitions = transitions.filter(
|
|
3891
|
+
invocation => !current.failureDiagnosticKeys.has(correlationKey(invocation.correlation)),
|
|
3892
|
+
);
|
|
3893
|
+
const diagnosticCode = sanitizePromptFailure(
|
|
3894
|
+
failureCause ?? Object.assign(new Error("agent run failed"), { code: "agent_failed" }),
|
|
3895
|
+
).code;
|
|
3896
|
+
for (const invocation of transitions)
|
|
3897
|
+
current.failureDiagnosticKeys.add(correlationKey(invocation.correlation));
|
|
3898
|
+
for (const invocation of transitions)
|
|
3899
|
+
current.failureDiagnosticCodes.set(correlationKey(invocation.correlation), diagnosticCode);
|
|
3900
|
+
}
|
|
3299
3901
|
// Observe whether the lifecycle publication actually landed: a terminal
|
|
3300
3902
|
// abort awaits this result so its durable row only claims
|
|
3301
3903
|
// terminalPublished when the correlated agent_end event reached the
|
|
@@ -3309,7 +3911,17 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3309
3911
|
const reasonKey = `${invocation.correlation.commandId}:${invocation.correlation.turnId}`;
|
|
3310
3912
|
const unrecorded = type === "agent_end" ? current.unrecordedFailureReasons?.get(reasonKey) : undefined;
|
|
3311
3913
|
if (type === "agent_end" && invocation.kind === "prompt")
|
|
3312
|
-
current.deadlineManager.noteTerminalTransition(
|
|
3914
|
+
current.deadlineManager.noteTerminalTransition(
|
|
3915
|
+
invocation.correlation,
|
|
3916
|
+
unrecorded,
|
|
3917
|
+
terminalContent !== undefined || terminalHasActivity || terminalOutcome !== undefined
|
|
3918
|
+
? ({
|
|
3919
|
+
content: terminalContent,
|
|
3920
|
+
hasActivity: terminalHasActivity,
|
|
3921
|
+
outcome: terminalOutcome,
|
|
3922
|
+
} satisfies PromptTerminalTransitionEvidence)
|
|
3923
|
+
: undefined,
|
|
3924
|
+
);
|
|
3313
3925
|
if (type === "agent_end") {
|
|
3314
3926
|
// Compound recovery (exact-head review P1): if this run's
|
|
3315
3927
|
// agent_failed write failed, re-record the sanitized reason
|
|
@@ -3333,7 +3945,16 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3333
3945
|
error:
|
|
3334
3946
|
failureCause ?? Object.assign(new Error("agent run failed"), { code: "agent_failed" }),
|
|
3335
3947
|
}
|
|
3336
|
-
: {
|
|
3948
|
+
: {
|
|
3949
|
+
type,
|
|
3950
|
+
...(type === "agent_end" && terminalContent !== undefined
|
|
3951
|
+
? { content: terminalContent }
|
|
3952
|
+
: {}),
|
|
3953
|
+
...(type === "agent_end" && terminalHasActivity ? { hasActivity: true } : {}),
|
|
3954
|
+
...(type === "agent_end" && terminalOutcome !== undefined
|
|
3955
|
+
? { outcome: terminalOutcome }
|
|
3956
|
+
: {}),
|
|
3957
|
+
};
|
|
3337
3958
|
await current.reconciliation.noteTransition(invocation.kind, invocation.correlation, frame as never);
|
|
3338
3959
|
if ((type as string) === "agent_end") {
|
|
3339
3960
|
if (invocation.kind === "prompt") current.deadlineManager.clear(invocation.correlation);
|
|
@@ -3356,7 +3977,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3356
3977
|
}
|
|
3357
3978
|
}
|
|
3358
3979
|
}
|
|
3359
|
-
const sessionId = ctx.sessionManager.getSessionId();
|
|
3980
|
+
const sessionId = lifecycleOwner?.sessionId ?? ctx.sessionManager.getSessionId();
|
|
3360
3981
|
if (type === "agent_failed") {
|
|
3361
3982
|
// Failure is a correlated diagnostic, not the terminal lifecycle boundary.
|
|
3362
3983
|
// Publish one safe frame per invocation so clients can attribute a shared
|
|
@@ -3395,6 +4016,16 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3395
4016
|
// and the next agent_end is paired with stale ownership.
|
|
3396
4017
|
for (const invocation of failedTransitions) scheduleSkillTerminalRecovery(invocation);
|
|
3397
4018
|
retireEndedLifecycleBatch();
|
|
4019
|
+
const failedKeys = new Set(failedTransitions.map(({ correlation }) => correlationKey(correlation)));
|
|
4020
|
+
for (const invocation of transitions) {
|
|
4021
|
+
if (!failedKeys.has(correlationKey(invocation.correlation)))
|
|
4022
|
+
removeLifecycleReferences(current, invocation.correlation);
|
|
4023
|
+
if (!failedKeys.has(correlationKey(invocation.correlation)))
|
|
4024
|
+
current.failureDiagnosticKeys.delete(correlationKey(invocation.correlation));
|
|
4025
|
+
if (!failedKeys.has(correlationKey(invocation.correlation)))
|
|
4026
|
+
current.failureDiagnosticCodes.delete(correlationKey(invocation.correlation));
|
|
4027
|
+
}
|
|
4028
|
+
options.onFailureDiagnosticKeyCountForTests?.(current.failureDiagnosticKeys.size);
|
|
3398
4029
|
resolveTerminalPublicationWaiters(observed);
|
|
3399
4030
|
return;
|
|
3400
4031
|
}
|
|
@@ -3410,7 +4041,17 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3410
4041
|
resolveTerminalPublicationWaiters(observed);
|
|
3411
4042
|
return;
|
|
3412
4043
|
}
|
|
3413
|
-
|
|
4044
|
+
const ended = failureBatch ?? current.openLifecycleBatches[0];
|
|
4045
|
+
if (ended) {
|
|
4046
|
+
const index = current.openLifecycleBatches.indexOf(ended);
|
|
4047
|
+
if (index >= 0) current.openLifecycleBatches.splice(index, 1);
|
|
4048
|
+
}
|
|
4049
|
+
for (const invocation of transitions) removeLifecycleReferences(current, invocation.correlation);
|
|
4050
|
+
for (const invocation of transitions)
|
|
4051
|
+
current.failureDiagnosticKeys.delete(correlationKey(invocation.correlation));
|
|
4052
|
+
for (const invocation of transitions)
|
|
4053
|
+
current.failureDiagnosticCodes.delete(correlationKey(invocation.correlation));
|
|
4054
|
+
options.onFailureDiagnosticKeyCountForTests?.(current.failureDiagnosticKeys.size);
|
|
3414
4055
|
for (const invocation of transitions)
|
|
3415
4056
|
if (invocation.kind === "prompt") current.deadlineManager.clear(invocation.correlation);
|
|
3416
4057
|
adoptLifecycleBatch(current.openLifecycleBatches[0]?.invocations);
|
|
@@ -3422,33 +4063,130 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3422
4063
|
resolveTerminalPublicationWaiters(observed);
|
|
3423
4064
|
}
|
|
3424
4065
|
};
|
|
3425
|
-
api.on("agent_start",
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
4066
|
+
api.on("agent_start", (event, ctx) => {
|
|
4067
|
+
const owner = lifecycleStateForEvent(ctx, "agent_start", event.sdkRunToken);
|
|
4068
|
+
return trackLifecycle(async () => {
|
|
4069
|
+
await emitLifecycle(
|
|
4070
|
+
"agent_start",
|
|
4071
|
+
ctx,
|
|
4072
|
+
undefined,
|
|
4073
|
+
undefined,
|
|
4074
|
+
owner ? { state: owner, sessionId: owner.sessionId } : undefined,
|
|
4075
|
+
);
|
|
4076
|
+
if (owner && typeof event.sdkRunToken === "string" && event.sdkRunToken.length > 0) {
|
|
4077
|
+
const batch = owner.openLifecycleBatches.at(-1);
|
|
4078
|
+
const entry = batch?.invocations.find(item => item.sdkRunToken === event.sdkRunToken);
|
|
4079
|
+
const previous = lifecycleRunOwners.get(event.sdkRunToken);
|
|
4080
|
+
lifecycleRunOwners.set(event.sdkRunToken, {
|
|
4081
|
+
state: owner,
|
|
4082
|
+
batch,
|
|
4083
|
+
...(previous?.correlationKey !== undefined
|
|
4084
|
+
? { correlationKey: previous.correlationKey }
|
|
4085
|
+
: entry
|
|
4086
|
+
? { correlationKey: lifecycleCorrelationKey(entry.correlation) }
|
|
4087
|
+
: {}),
|
|
4088
|
+
});
|
|
4089
|
+
}
|
|
4090
|
+
}, owner);
|
|
4091
|
+
});
|
|
4092
|
+
api.on("agent_end", (event, ctx) => {
|
|
4093
|
+
const tokenBinding =
|
|
4094
|
+
typeof event.sdkRunToken === "string" ? lifecycleRunOwners.get(event.sdkRunToken) : undefined;
|
|
4095
|
+
const owner = tokenBinding?.state ?? lifecycleStateForEvent(ctx, "agent_end", event.sdkRunToken);
|
|
4096
|
+
// Capture the oldest unmatched batch synchronously. A successor may start
|
|
4097
|
+
// while the failed diagnostic persists; that must not retarget the
|
|
4098
|
+
// predecessor's reason or terminal boundary to the successor invocation.
|
|
4099
|
+
const endedBatch = tokenBinding?.batch ?? owner?.openLifecycleBatches[0];
|
|
4100
|
+
const lifecycleOwner = owner
|
|
4101
|
+
? {
|
|
4102
|
+
state: owner,
|
|
4103
|
+
sessionId: owner.sessionId,
|
|
4104
|
+
...(endedBatch ? { batch: endedBatch } : {}),
|
|
4105
|
+
}
|
|
4106
|
+
: undefined;
|
|
4107
|
+
const currentInvocation = endedBatch?.invocations[0] ?? owner?.activeInvocation;
|
|
4108
|
+
const failure = providerFailureFromAgentEnd(event);
|
|
4109
|
+
const failureCandidates = endedBatch
|
|
4110
|
+
? [...endedBatch.invocations, ...endedBatch.attachedInvocations]
|
|
4111
|
+
: currentInvocation
|
|
4112
|
+
? [currentInvocation]
|
|
4113
|
+
: [];
|
|
4114
|
+
const genericFailureKeys = failureCandidates
|
|
4115
|
+
.map(({ correlation }) => lifecycleCorrelationKey(correlation))
|
|
4116
|
+
.filter(key => owner?.failureDiagnosticCodes.get(key) === "agent_failed");
|
|
4117
|
+
const hasExistingFailure = failureCandidates.some(({ correlation }) =>
|
|
4118
|
+
owner?.failureDiagnosticKeys.has(lifecycleCorrelationKey(correlation)),
|
|
4119
|
+
);
|
|
4120
|
+
const failureAlreadyPublished = failure === undefined || (hasExistingFailure && genericFailureKeys.length === 0);
|
|
4121
|
+
const terminalEvidence = promptTerminalEvidenceFromAgentEnd(event);
|
|
4122
|
+
const terminalOutcome =
|
|
4123
|
+
event.stopReason === "cancelled"
|
|
4124
|
+
? ({ kind: "stopped", reason: "cancelled", provenance: "client_cancel" } as const)
|
|
4125
|
+
: undefined;
|
|
4126
|
+
return trackLifecycle(async () => {
|
|
4127
|
+
if (failure && !failureAlreadyPublished) {
|
|
4128
|
+
for (const key of genericFailureKeys) owner?.failureDiagnosticKeys.delete(key);
|
|
4129
|
+
await emitLifecycle("agent_failed", ctx, failure, undefined, lifecycleOwner);
|
|
4130
|
+
}
|
|
3429
4131
|
await emitLifecycle(
|
|
3430
4132
|
"agent_end",
|
|
3431
4133
|
ctx,
|
|
3432
4134
|
undefined,
|
|
3433
4135
|
event.stopReason === "maintenance" ? event.maintenanceOutcome : undefined,
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
4136
|
+
lifecycleOwner,
|
|
4137
|
+
terminalEvidence.content,
|
|
4138
|
+
terminalEvidence.hasActivity,
|
|
4139
|
+
terminalOutcome,
|
|
4140
|
+
);
|
|
4141
|
+
}, owner).finally(() => {
|
|
4142
|
+
if (typeof event.sdkRunToken === "string" && lifecycleRunOwners.get(event.sdkRunToken)?.state === owner)
|
|
4143
|
+
lifecycleRunOwners.delete(event.sdkRunToken);
|
|
4144
|
+
});
|
|
4145
|
+
});
|
|
4146
|
+
api.on("agent_failed", (event, ctx) => {
|
|
4147
|
+
const tokenBinding =
|
|
4148
|
+
typeof event.sdkRunToken === "string" ? lifecycleRunOwners.get(event.sdkRunToken) : undefined;
|
|
4149
|
+
const owner = tokenBinding?.state ?? lifecycleStateForEvent(ctx, "agent_failed", event.sdkRunToken);
|
|
4150
|
+
const failedBatch = tokenBinding?.batch ?? owner?.openLifecycleBatches[0];
|
|
4151
|
+
return trackLifecycle(
|
|
4152
|
+
async () =>
|
|
4153
|
+
emitLifecycle(
|
|
4154
|
+
"agent_failed",
|
|
4155
|
+
ctx,
|
|
4156
|
+
event.error,
|
|
4157
|
+
undefined,
|
|
4158
|
+
owner
|
|
4159
|
+
? {
|
|
4160
|
+
state: owner,
|
|
4161
|
+
sessionId: owner.sessionId,
|
|
4162
|
+
...(failedBatch ? { batch: failedBatch } : {}),
|
|
4163
|
+
}
|
|
4164
|
+
: undefined,
|
|
4165
|
+
),
|
|
4166
|
+
owner,
|
|
4167
|
+
);
|
|
4168
|
+
});
|
|
3437
4169
|
api.on("turn_start", async (_event, ctx) => {
|
|
3438
|
-
const current =
|
|
4170
|
+
const current = lifecycleStateForContext(ctx, "agent_start");
|
|
3439
4171
|
if (!current) return;
|
|
3440
4172
|
await current.registerBroker();
|
|
3441
4173
|
current.runtime.emitEvent({ type: "turn_start", sessionId: ctx.sessionManager.getSessionId() });
|
|
3442
4174
|
});
|
|
3443
4175
|
api.on("turn_end", (_event, ctx) =>
|
|
3444
|
-
|
|
4176
|
+
lifecycleStateForContext(ctx, "agent_end")?.runtime.emitEvent({
|
|
4177
|
+
type: "turn_end",
|
|
4178
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
4179
|
+
}),
|
|
3445
4180
|
);
|
|
3446
4181
|
// Tool activity renews the deadline of EVERY prompt correlation attached to
|
|
3447
4182
|
// the active run — the root invocation plus any in-run consumed follow-ups
|
|
3448
4183
|
// 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
|
-
|
|
4184
|
+
// false-fire prompt_deadline_exceeded during a long shared run.
|
|
4185
|
+
const renewAttributableProgress = (eventType: string, ctx: ExtensionContext): void => {
|
|
4186
|
+
// Tool events do not carry an SDK run token. Prefer the lifecycle-active
|
|
4187
|
+
// runtime for the current session so a retained predecessor cannot make a
|
|
4188
|
+
// live replacement look ambiguous and suppress its lease renewal.
|
|
4189
|
+
const current = lifecycleStateForContext(ctx, "agent_start");
|
|
3452
4190
|
if (!current) return;
|
|
3453
4191
|
// Renew only invocations adopted by the CURRENT run: the live lifecycle
|
|
3454
4192
|
// batch plus in-run attachments. drainedInvocations may still hold
|
|
@@ -3488,12 +4226,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3488
4226
|
}
|
|
3489
4227
|
};
|
|
3490
4228
|
api.on("tool_execution_start", async (_event, ctx) => {
|
|
3491
|
-
renewAttributableProgress("tool_execution_start");
|
|
3492
|
-
void ctx;
|
|
4229
|
+
renewAttributableProgress("tool_execution_start", ctx);
|
|
3493
4230
|
});
|
|
3494
4231
|
api.on("tool_execution_end", async (_event, ctx) => {
|
|
3495
|
-
renewAttributableProgress("tool_execution_end");
|
|
3496
|
-
void ctx;
|
|
4232
|
+
renewAttributableProgress("tool_execution_end", ctx);
|
|
3497
4233
|
});
|
|
3498
4234
|
const errorCode = (error: unknown): string | undefined =>
|
|
3499
4235
|
typeof error === "object" &&
|
|
@@ -3513,6 +4249,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3513
4249
|
const sessionFile =
|
|
3514
4250
|
(typeof ctx.sessionManager.getSessionFile === "function" ? ctx.sessionManager.getSessionFile() : undefined) ??
|
|
3515
4251
|
resolveReconciliationSessionFile(undefined, stateRoot, sessionId);
|
|
4252
|
+
const sessionIdentity = `${sessionId}\u0000${sessionFile}`;
|
|
3516
4253
|
const reconciliationStore =
|
|
3517
4254
|
options.terminalAbortSeams?.getReconciliationStore?.() ??
|
|
3518
4255
|
createReconciliationStore({ sessionFile, sessionId });
|
|
@@ -3534,18 +4271,17 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3534
4271
|
return typeof v === "number" && Number.isFinite(v) ? v : 21_600_000;
|
|
3535
4272
|
},
|
|
3536
4273
|
onExpired: correlation => {
|
|
3537
|
-
const
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
);
|
|
3542
|
-
if (idx >= 0) pending.splice(idx, 1);
|
|
4274
|
+
const owner = lifecycleOwnerHolder.state;
|
|
4275
|
+
if (!owner) return;
|
|
4276
|
+
removeLifecycleReferences(owner, correlation);
|
|
4277
|
+
maybeRetireLifecycleOwner(owner);
|
|
3543
4278
|
},
|
|
3544
4279
|
});
|
|
3545
4280
|
const pending: Array<{
|
|
3546
4281
|
kind: InvocationKind;
|
|
3547
4282
|
correlation: InvocationCorrelation;
|
|
3548
4283
|
connectionId: string | undefined;
|
|
4284
|
+
sdkRunToken: string;
|
|
3549
4285
|
}> = [];
|
|
3550
4286
|
for (const { correlation, acceptedAt, deadlineMaxAt } of reconciliation.listDeadlineRecoveryPendingPrompts())
|
|
3551
4287
|
deadlineManager.recoverPending(correlation, acceptedAt, deadlineMaxAt);
|
|
@@ -3555,6 +4291,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3555
4291
|
kind: InvocationKind;
|
|
3556
4292
|
correlation: InvocationCorrelation;
|
|
3557
4293
|
connectionId: string | undefined;
|
|
4294
|
+
sdkRunToken: string;
|
|
3558
4295
|
}>;
|
|
3559
4296
|
attachedInvocations: Array<{ kind: InvocationKind; correlation: InvocationCorrelation }>;
|
|
3560
4297
|
}> = [];
|
|
@@ -3584,6 +4321,60 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3584
4321
|
settings: options.settings,
|
|
3585
4322
|
});
|
|
3586
4323
|
const queryHandlers = new QueryHandlers(surfaceFactory.query, sessionId, revisions, cursors);
|
|
4324
|
+
const inputGate = { quiescing: false };
|
|
4325
|
+
const lifecycleOwnerHolder: { state?: RuntimeState; quiescing: boolean } = { quiescing: false };
|
|
4326
|
+
const skillRecoveryTasks = new Map<string, Promise<void>>();
|
|
4327
|
+
const scheduleSkillRecovery = (
|
|
4328
|
+
correlation: InvocationCorrelation,
|
|
4329
|
+
failureIntent?: { code: string; message: string },
|
|
4330
|
+
): void => {
|
|
4331
|
+
const key = lifecycleCorrelationKey(correlation);
|
|
4332
|
+
if (skillRecoveryTasks.has(key)) return;
|
|
4333
|
+
const controller = new AbortController();
|
|
4334
|
+
skillRecoveryControllers.set(key, controller);
|
|
4335
|
+
const intent = failureIntent;
|
|
4336
|
+
const task = (async (): Promise<void> => {
|
|
4337
|
+
let failureRecorded = intent === undefined;
|
|
4338
|
+
for (;;) {
|
|
4339
|
+
if (controller.signal.aborted) return;
|
|
4340
|
+
try {
|
|
4341
|
+
if (!failureRecorded) {
|
|
4342
|
+
await reconciliation.noteTransition("skill", correlation, {
|
|
4343
|
+
type: "agent_failed",
|
|
4344
|
+
error: Object.assign(new Error(intent?.message ?? "skill invocation failed"), {
|
|
4345
|
+
code: intent?.code ?? "skill_failed",
|
|
4346
|
+
}),
|
|
4347
|
+
} as never);
|
|
4348
|
+
failureRecorded = true;
|
|
4349
|
+
}
|
|
4350
|
+
await reconciliation.noteTransition("skill", correlation, { type: "agent_end" });
|
|
4351
|
+
return;
|
|
4352
|
+
} catch (error) {
|
|
4353
|
+
if (controller.signal.aborted) return;
|
|
4354
|
+
logger.error("SDK skill lifecycle recovery retrying", {
|
|
4355
|
+
commandId: correlation.commandId,
|
|
4356
|
+
turnId: correlation.turnId,
|
|
4357
|
+
error: sanitizePromptFailure(error),
|
|
4358
|
+
});
|
|
4359
|
+
const wait = Promise.withResolvers<void>();
|
|
4360
|
+
const timer = setTimeout(wait.resolve, 1_000);
|
|
4361
|
+
timer.unref();
|
|
4362
|
+
const onAbort = () => {
|
|
4363
|
+
clearTimeout(timer);
|
|
4364
|
+
wait.resolve();
|
|
4365
|
+
};
|
|
4366
|
+
controller.signal.addEventListener("abort", onAbort, { once: true });
|
|
4367
|
+
await wait.promise;
|
|
4368
|
+
controller.signal.removeEventListener("abort", onAbort);
|
|
4369
|
+
}
|
|
4370
|
+
}
|
|
4371
|
+
})();
|
|
4372
|
+
skillRecoveryTasks.set(key, task);
|
|
4373
|
+
void task.finally(() => {
|
|
4374
|
+
skillRecoveryTasks.delete(key);
|
|
4375
|
+
skillRecoveryControllers.delete(key);
|
|
4376
|
+
});
|
|
4377
|
+
};
|
|
3587
4378
|
let runtime: SessionSdkSessionRuntime;
|
|
3588
4379
|
// Durable-first bounded terminalization for accepted submissions that leave
|
|
3589
4380
|
// their queue or race a run WITHOUT consumption (exact-head review: clearing
|
|
@@ -3597,6 +4388,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3597
4388
|
correlation: InvocationCorrelation,
|
|
3598
4389
|
error: { code: string; message: string },
|
|
3599
4390
|
): void => {
|
|
4391
|
+
if (lifecycleOwnerHolder.state) removeLifecycleTokenAliases(lifecycleOwnerHolder.state, correlation);
|
|
3600
4392
|
const attempt = async (remaining: number): Promise<void> => {
|
|
3601
4393
|
try {
|
|
3602
4394
|
// 1. Record the failure reason durably FIRST. If this write fails,
|
|
@@ -3608,10 +4400,12 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3608
4400
|
} as never);
|
|
3609
4401
|
} catch (reasonError) {
|
|
3610
4402
|
if (remaining <= 1) {
|
|
3611
|
-
//
|
|
3612
|
-
//
|
|
3613
|
-
|
|
3614
|
-
|
|
4403
|
+
// Prompts use the deadline lease; skills have no prompt lease and
|
|
4404
|
+
// must use their kind-aware durable retry owner instead.
|
|
4405
|
+
if (kind === "skill")
|
|
4406
|
+
scheduleSkillRecovery(correlation, { code: error.code, message: error.message });
|
|
4407
|
+
else
|
|
4408
|
+
deadlineManager.noteTerminalTransition(correlation, { code: error.code, message: error.message });
|
|
3615
4409
|
logger.error("SDK abandoned submission failed to record its failure reason", {
|
|
3616
4410
|
kind,
|
|
3617
4411
|
commandId: correlation.commandId,
|
|
@@ -3630,8 +4424,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3630
4424
|
// 3. Release recovery ownership ONLY after durable terminalization.
|
|
3631
4425
|
deadlineManager.clear(correlation);
|
|
3632
4426
|
} catch (transitionError) {
|
|
3633
|
-
// Keep
|
|
3634
|
-
|
|
4427
|
+
// Keep prompt recovery leased; skill recovery has no prompt lease.
|
|
4428
|
+
if (kind === "skill") scheduleSkillRecovery(correlation);
|
|
4429
|
+
else deadlineManager.noteTerminalTransition(correlation, { code: error.code, message: error.message });
|
|
3635
4430
|
logger.error("SDK abandoned submission failed to terminalize", {
|
|
3636
4431
|
kind,
|
|
3637
4432
|
commandId: correlation.commandId,
|
|
@@ -3647,13 +4442,26 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3647
4442
|
ctx,
|
|
3648
4443
|
api,
|
|
3649
4444
|
reconciliation,
|
|
3650
|
-
(kind, correlation, connectionId, startsOwnTurn) => {
|
|
4445
|
+
(kind, correlation, connectionId, startsOwnTurn, sdkRunToken) => {
|
|
4446
|
+
const owner = lifecycleOwnerHolder.state;
|
|
4447
|
+
if (lifecycleOwnerHolder.quiescing) {
|
|
4448
|
+
terminalizeAbandonedSubmission(kind, correlation, {
|
|
4449
|
+
code: "session_quiescing",
|
|
4450
|
+
message: "Session endpoint was replaced before the invocation started.",
|
|
4451
|
+
});
|
|
4452
|
+
return;
|
|
4453
|
+
}
|
|
4454
|
+
if (sdkRunToken && owner)
|
|
4455
|
+
lifecycleRunOwners.set(sdkRunToken, {
|
|
4456
|
+
state: owner,
|
|
4457
|
+
correlationKey: lifecycleCorrelationKey(correlation),
|
|
4458
|
+
});
|
|
3651
4459
|
// Only submissions that start their OWN turn get a pending entry: a
|
|
3652
4460
|
// steering-queued submission consumed inside the current run never
|
|
3653
4461
|
// emits the agent_start that would consume the entry, so leaving it
|
|
3654
4462
|
// queued would assign its stale connection as owner of a later
|
|
3655
4463
|
// agent-initiated turn (review thread P1).
|
|
3656
|
-
if (startsOwnTurn) pending.push({ kind, correlation, connectionId });
|
|
4464
|
+
if (startsOwnTurn) pending.push({ kind, correlation, connectionId, sdkRunToken });
|
|
3657
4465
|
// Anchor the zero-progress deadline at durable acceptance, not at
|
|
3658
4466
|
// agent_start (#4668): an accepted own-turn prompt that wedges
|
|
3659
4467
|
// between acceptance and run start (provider/credential/compaction
|
|
@@ -3663,10 +4471,26 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3663
4471
|
// promotion/agent_start instead, so a prompt waiting behind a
|
|
3664
4472
|
// legitimately long turn never false-fires. The agent_start
|
|
3665
4473
|
// re-entry in emitLifecycle is a no-op for an existing lease.
|
|
3666
|
-
if (startsOwnTurn && kind === "prompt") deadlineManager.onAccepted(correlation);
|
|
3667
4474
|
},
|
|
4475
|
+
correlation => deadlineManager.onAccepted(correlation),
|
|
3668
4476
|
steerReconciliation,
|
|
3669
|
-
(kind, correlation, connectionId, promotion) => {
|
|
4477
|
+
(kind, correlation, connectionId, sdkRunToken, promotion) => {
|
|
4478
|
+
const bindPromotedToken = (batch?: LifecycleBatch): void => {
|
|
4479
|
+
const owner = lifecycleOwnerHolder.state;
|
|
4480
|
+
if (sdkRunToken && owner)
|
|
4481
|
+
lifecycleRunOwners.set(sdkRunToken, {
|
|
4482
|
+
state: owner,
|
|
4483
|
+
batch,
|
|
4484
|
+
correlationKey: lifecycleCorrelationKey(correlation),
|
|
4485
|
+
});
|
|
4486
|
+
};
|
|
4487
|
+
if (lifecycleOwnerHolder.quiescing) {
|
|
4488
|
+
terminalizeAbandonedSubmission(kind, correlation, {
|
|
4489
|
+
code: "session_quiescing",
|
|
4490
|
+
message: "Session endpoint was replaced before the invocation started.",
|
|
4491
|
+
});
|
|
4492
|
+
return;
|
|
4493
|
+
}
|
|
3670
4494
|
// Lease at the ACTUAL promotion boundary (#4668 review): a promoted
|
|
3671
4495
|
// submission that wedges before its run's agent_start must still
|
|
3672
4496
|
// terminalize boundedly instead of remaining accepted forever.
|
|
@@ -3696,7 +4520,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3696
4520
|
// starts with an empty pending queue at its agent_start; create the
|
|
3697
4521
|
// entry at promotion so the submitting connection owns that turn
|
|
3698
4522
|
// (review thread P2).
|
|
3699
|
-
pending.push({ kind, correlation, connectionId });
|
|
4523
|
+
pending.push({ kind, correlation, connectionId, sdkRunToken });
|
|
3700
4524
|
return;
|
|
3701
4525
|
}
|
|
3702
4526
|
// Consumed INSIDE the currently running turn: no new agent_start
|
|
@@ -3759,6 +4583,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3759
4583
|
)
|
|
3760
4584
|
)
|
|
3761
4585
|
activeBatch.attachedInvocations.push({ kind, correlation });
|
|
4586
|
+
bindPromotedToken(activeBatch);
|
|
3762
4587
|
return;
|
|
3763
4588
|
}
|
|
3764
4589
|
// No in-flight run visible and this is an in-run consumption
|
|
@@ -3794,8 +4619,14 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3794
4619
|
// the matching correlation so a later agent-initiated turn never
|
|
3795
4620
|
// inherits the failed submission's connection as owner (review
|
|
3796
4621
|
// thread P1).
|
|
3797
|
-
|
|
3798
|
-
|
|
4622
|
+
if (!lifecycleOwnerHolder.quiescing) {
|
|
4623
|
+
const index = pending.findIndex(entry => entry.correlation === correlation);
|
|
4624
|
+
if (index >= 0) {
|
|
4625
|
+
const [removed] = pending.splice(index, 1);
|
|
4626
|
+
if (removed && lifecycleRunOwners.get(removed.sdkRunToken)?.state === lifecycleOwnerHolder.state)
|
|
4627
|
+
lifecycleRunOwners.delete(removed.sdkRunToken);
|
|
4628
|
+
}
|
|
4629
|
+
}
|
|
3799
4630
|
// Release the acceptance-anchored lease ONLY when the durable row is
|
|
3800
4631
|
// already terminal (exact-head review #4): clearing it before the
|
|
3801
4632
|
// settlement writes land strands the row accepted with no recovery
|
|
@@ -3808,6 +4639,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3808
4639
|
return;
|
|
3809
4640
|
}
|
|
3810
4641
|
if (leaseRelease === "recover-failure" && failureIntent !== undefined) {
|
|
4642
|
+
if (kind === "skill") {
|
|
4643
|
+
scheduleSkillRecovery(correlation, failureIntent);
|
|
4644
|
+
return;
|
|
4645
|
+
}
|
|
3811
4646
|
// Compound failure-plus-terminal recovery: the settlement's durable
|
|
3812
4647
|
// writes failed, so the lease stays armed as the recovery owner and its
|
|
3813
4648
|
// replay re-records the failure reason before agent_end (exact-head
|
|
@@ -3816,6 +4651,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3816
4651
|
return;
|
|
3817
4652
|
}
|
|
3818
4653
|
if (leaseRelease === "recover-terminal") {
|
|
4654
|
+
if (kind === "skill") {
|
|
4655
|
+
scheduleSkillRecovery(correlation);
|
|
4656
|
+
return;
|
|
4657
|
+
}
|
|
3819
4658
|
deadlineManager.noteTerminalTransition(correlation);
|
|
3820
4659
|
return;
|
|
3821
4660
|
}
|
|
@@ -3892,6 +4731,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3892
4731
|
};
|
|
3893
4732
|
runtime = new SessionSdkSessionRuntime({
|
|
3894
4733
|
transport,
|
|
4734
|
+
onFrameAdmission: (_connectionId, frame) =>
|
|
4735
|
+
inputGate.quiescing ? quiescingFrame(frame as Record<string, unknown>) : undefined,
|
|
3895
4736
|
control: async (connectionId, frame) => {
|
|
3896
4737
|
options.onFrameAdmitted?.();
|
|
3897
4738
|
const request = controlRequestFromFrame(frame as Record<string, unknown>);
|
|
@@ -4028,7 +4869,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4028
4869
|
logger.warn(`sdk broker registration unavailable: ${String(error)}`);
|
|
4029
4870
|
}
|
|
4030
4871
|
};
|
|
4031
|
-
|
|
4872
|
+
const runtimeOwner: RuntimeState = {
|
|
4873
|
+
sessionId,
|
|
4874
|
+
sessionIdentity,
|
|
4032
4875
|
runtime,
|
|
4033
4876
|
revisions,
|
|
4034
4877
|
cursors,
|
|
@@ -4038,6 +4881,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4038
4881
|
pending,
|
|
4039
4882
|
openLifecycleBatches,
|
|
4040
4883
|
registerBroker,
|
|
4884
|
+
quiesceInput: () => {
|
|
4885
|
+
inputGate.quiescing = true;
|
|
4886
|
+
lifecycleOwnerHolder.quiescing = true;
|
|
4887
|
+
},
|
|
4041
4888
|
fenceGateResolutions: () => {
|
|
4042
4889
|
acceptingGateResolutions = false;
|
|
4043
4890
|
},
|
|
@@ -4045,7 +4892,12 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4045
4892
|
disposeGate,
|
|
4046
4893
|
lifecycleActive: false,
|
|
4047
4894
|
lifecycleEpoch: 0,
|
|
4895
|
+
failureDiagnosticKeys: new Set(),
|
|
4896
|
+
failureDiagnosticCodes: new Map(),
|
|
4897
|
+
lifecycleTasks: new Set(),
|
|
4048
4898
|
};
|
|
4899
|
+
lifecycleOwnerHolder.state = runtimeOwner;
|
|
4900
|
+
active = runtimeOwner;
|
|
4049
4901
|
try {
|
|
4050
4902
|
await runtime.start();
|
|
4051
4903
|
await registerBroker();
|
|
@@ -4059,7 +4911,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4059
4911
|
code: errorCode(cleanupError),
|
|
4060
4912
|
error: String(cleanupError),
|
|
4061
4913
|
});
|
|
4062
|
-
|
|
4914
|
+
const failedRuntimeOwner: RuntimeState = {
|
|
4915
|
+
sessionId,
|
|
4916
|
+
sessionIdentity,
|
|
4063
4917
|
runtime,
|
|
4064
4918
|
revisions,
|
|
4065
4919
|
cursors,
|
|
@@ -4069,6 +4923,10 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4069
4923
|
pending,
|
|
4070
4924
|
openLifecycleBatches,
|
|
4071
4925
|
registerBroker,
|
|
4926
|
+
quiesceInput: () => {
|
|
4927
|
+
inputGate.quiescing = true;
|
|
4928
|
+
lifecycleOwnerHolder.quiescing = true;
|
|
4929
|
+
},
|
|
4072
4930
|
fenceGateResolutions: () => {
|
|
4073
4931
|
acceptingGateResolutions = false;
|
|
4074
4932
|
},
|
|
@@ -4076,7 +4934,12 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4076
4934
|
disposeGate,
|
|
4077
4935
|
lifecycleActive: false,
|
|
4078
4936
|
lifecycleEpoch: 0,
|
|
4937
|
+
failureDiagnosticKeys: new Set(),
|
|
4938
|
+
failureDiagnosticCodes: new Map(),
|
|
4939
|
+
lifecycleTasks: new Set(),
|
|
4079
4940
|
};
|
|
4941
|
+
lifecycleOwnerHolder.state = failedRuntimeOwner;
|
|
4942
|
+
active = failedRuntimeOwner;
|
|
4080
4943
|
throw new AggregateError([error, cleanupError], "SDK runtime startup failed and cleanup failed.");
|
|
4081
4944
|
}
|
|
4082
4945
|
cursors.close();
|
|
@@ -4084,19 +4947,70 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4084
4947
|
throw error;
|
|
4085
4948
|
}
|
|
4086
4949
|
};
|
|
4087
|
-
const stopActive = async (): Promise<void> => {
|
|
4950
|
+
const stopActive = async (cancelSkillRecovery = false): Promise<void> => {
|
|
4088
4951
|
const current = active;
|
|
4089
4952
|
if (!current) return;
|
|
4090
|
-
|
|
4953
|
+
if (cancelSkillRecovery) {
|
|
4954
|
+
for (const controller of skillRecoveryControllers.values()) controller.abort();
|
|
4955
|
+
for (const controller of skillTerminalRecoveryControllers.values()) controller.abort();
|
|
4956
|
+
}
|
|
4957
|
+
activePromptOwnerHolder.connectionIds = undefined;
|
|
4958
|
+
activePromptOwnerHolder.lifecycleEpoch = undefined;
|
|
4959
|
+
current.quiesceInput();
|
|
4091
4960
|
current.fenceGateResolutions();
|
|
4092
4961
|
try {
|
|
4093
4962
|
await current.waitForGateResolutionQuiescence();
|
|
4963
|
+
if (current.lifecycleTasks.size > 0) {
|
|
4964
|
+
const lifecycleDrain = Promise.all([...current.lifecycleTasks]);
|
|
4965
|
+
const timeout = Promise.withResolvers<void>();
|
|
4966
|
+
const timer = setTimeout(timeout.resolve, LIFECYCLE_QUIESCENCE_MS);
|
|
4967
|
+
const drained = Promise.race([lifecycleDrain, timeout.promise]);
|
|
4968
|
+
await drained;
|
|
4969
|
+
clearTimeout(timer);
|
|
4970
|
+
if (current.lifecycleTasks.size > 0) {
|
|
4971
|
+
logger.warn("SDK runtime lifecycle drain timed out; durable lifecycle recovery remains authoritative.");
|
|
4972
|
+
options.onLifecycleDrainTimeoutForTests?.();
|
|
4973
|
+
}
|
|
4974
|
+
}
|
|
4094
4975
|
active = undefined;
|
|
4976
|
+
const retainsLifecycleWork =
|
|
4977
|
+
current.pending.length > 0 ||
|
|
4978
|
+
current.openLifecycleBatches.length > 0 ||
|
|
4979
|
+
(current.attachedInvocations?.length ?? 0) > 0 ||
|
|
4980
|
+
(current.drainedInvocations?.length ?? 0) > 0 ||
|
|
4981
|
+
current.lifecycleTasks.size > 0;
|
|
4982
|
+
if (retainsLifecycleWork) {
|
|
4983
|
+
const owners = retiredLifecycleOwners.get(current.sessionId) ?? [];
|
|
4984
|
+
if (!owners.includes(current)) owners.push(current);
|
|
4985
|
+
retiredLifecycleOwners.set(current.sessionId, owners);
|
|
4986
|
+
const retryCleanup = (): void => {
|
|
4987
|
+
retiredLifecycleOwnerTimers.delete(current);
|
|
4988
|
+
if (
|
|
4989
|
+
current.pending.length > 0 ||
|
|
4990
|
+
current.openLifecycleBatches.length > 0 ||
|
|
4991
|
+
(current.attachedInvocations?.length ?? 0) > 0 ||
|
|
4992
|
+
(current.drainedInvocations?.length ?? 0) > 0 ||
|
|
4993
|
+
current.lifecycleTasks.size > 0
|
|
4994
|
+
) {
|
|
4995
|
+
const retry = setTimeout(retryCleanup, LIFECYCLE_QUIESCENCE_MS);
|
|
4996
|
+
retry.unref();
|
|
4997
|
+
retiredLifecycleOwnerTimers.set(current, retry);
|
|
4998
|
+
return;
|
|
4999
|
+
}
|
|
5000
|
+
removeRetiredLifecycleOwner(current);
|
|
5001
|
+
};
|
|
5002
|
+
const timer = setTimeout(retryCleanup, LIFECYCLE_QUIESCENCE_MS);
|
|
5003
|
+
timer.unref();
|
|
5004
|
+
retiredLifecycleOwnerTimers.set(current, timer);
|
|
5005
|
+
} else current.deadlineManager.clearAll();
|
|
4095
5006
|
current.disposeGate?.();
|
|
4096
5007
|
await current.runtime.stop();
|
|
4097
5008
|
} catch (error) {
|
|
5009
|
+
// Keep the immutable owner available for a retry when transport teardown
|
|
5010
|
+
// fails after quiescing. Clearing `active` before stop prevents a second
|
|
5011
|
+
// shutdown from retrying the failed endpoint removal.
|
|
5012
|
+
if (active === undefined) active = current;
|
|
4098
5013
|
logger.error("sdk runtime stop failed", { code: errorCode(error), error: String(error) });
|
|
4099
|
-
active = current;
|
|
4100
5014
|
throw error;
|
|
4101
5015
|
}
|
|
4102
5016
|
current.cursors.close();
|
|
@@ -4114,6 +5028,6 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4114
5028
|
await startRuntime(ctx);
|
|
4115
5029
|
});
|
|
4116
5030
|
api.on("session_shutdown", async () => {
|
|
4117
|
-
await stopActive();
|
|
5031
|
+
await stopActive(true);
|
|
4118
5032
|
});
|
|
4119
5033
|
}
|