@gajae-code/coding-agent 0.15.6 → 0.16.1
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 +99 -0
- package/dist/types/async/job-manager.d.ts +32 -1
- package/dist/types/browser-backend.d.ts +12 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +14 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/sdk.d.ts +2 -0
- package/dist/types/config/autorouting-tier-map.d.ts +24 -0
- package/dist/types/config/billing-path.d.ts +25 -0
- package/dist/types/config/config-schema-version.d.ts +13 -1
- package/dist/types/config/file-lock.d.ts +5 -0
- package/dist/types/config/model-registry.d.ts +22 -0
- package/dist/types/config/settings-schema.d.ts +27 -8
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +8 -3
- package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
- package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/master-mode/context.d.ts +21 -0
- package/dist/types/master-mode/first-request.d.ts +34 -0
- package/dist/types/master-mode/snapshot.d.ts +32 -0
- package/dist/types/modes/acp/acp-agent.d.ts +21 -21
- package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/model-selector.d.ts +4 -0
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- package/dist/types/modes/components/session-selector.d.ts +1 -1
- package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
- package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
- package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
- package/dist/types/modes/components/status-line/segments.d.ts +10 -0
- package/dist/types/modes/interactive-mode.d.ts +16 -2
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/theme/theme.d.ts +2 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +79 -2
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/ensure.d.ts +3 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
- package/dist/types/sdk/broker/master-capability.d.ts +10 -0
- package/dist/types/sdk/broker/session-index.d.ts +40 -11
- package/dist/types/sdk/broker/session-scope.d.ts +72 -0
- package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
- package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
- package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
- package/dist/types/sdk/broker/state-version.d.ts +21 -1
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
- package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/index.d.ts +9 -1
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
- package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/cli/index.d.ts +1 -0
- package/dist/types/sdk/cli/master-cli.d.ts +63 -0
- package/dist/types/sdk/cli/rows.d.ts +12 -5
- package/dist/types/sdk/cli/session-cli.d.ts +20 -3
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +2 -0
- package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
- package/dist/types/sdk/host/events.d.ts +1 -0
- package/dist/types/sdk/host/host.d.ts +10 -0
- package/dist/types/sdk/host/query/cursor.d.ts +4 -2
- package/dist/types/sdk/host/query/handlers.d.ts +1 -0
- package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +70 -17
- package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
- package/dist/types/sdk/lifecycle/service.d.ts +15 -3
- package/dist/types/sdk/lifecycle-session.d.ts +3 -1
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
- package/dist/types/sdk/receipt-state.d.ts +2 -0
- package/dist/types/sdk/router/session-router.d.ts +6 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/sdk/turn-result.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +52 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +29 -2
- package/dist/types/session/session-manager.d.ts +79 -2
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
- package/dist/types/tools/ask.d.ts +1 -0
- package/dist/types/tools/bash.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +10 -0
- package/dist/types/tools/browser/registry.d.ts +8 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -10
- package/dist/types/tools/job.d.ts +3 -1
- package/dist/types/utils/external-editor.d.ts +7 -0
- package/examples/extensions/README.md +1 -1
- package/examples/extensions/pirate.ts +7 -4
- package/package.json +7 -7
- package/scripts/generate-sdk-operation-inventory.ts +13 -1
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +107 -9
- package/src/browser-backend.ts +29 -0
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/plugin-cli.ts +29 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +140 -23
- package/src/commands/launch.ts +86 -18
- package/src/commands/sdk.ts +135 -28
- package/src/config/autorouting-tier-map.ts +12 -0
- package/src/config/billing-path.ts +42 -0
- package/src/config/config-schema-version.ts +18 -1
- package/src/config/file-lock.ts +158 -83
- package/src/config/model-preset-registry.ts +178 -23
- package/src/config/model-profiles.ts +5 -5
- package/src/config/model-registry.ts +291 -42
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +27 -8
- package/src/config/settings.ts +51 -15
- package/src/coordinator-mcp/projection-scan.ts +27 -6
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +115 -21
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
- package/src/extensibility/extensions/runner.ts +27 -0
- package/src/extensibility/extensions/types.ts +8 -3
- package/src/extensibility/gjc-plugins/installer.ts +51 -11
- package/src/extensibility/runtime-skill-discovery.ts +110 -3
- package/src/extensibility/skills.ts +22 -12
- package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
- package/src/gjc-runtime/launch-worktree.ts +285 -4
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +213 -14
- package/src/gjc-runtime/session-state-sidecar.ts +938 -39
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/internal-urls/docs-index.generated.ts +15 -15
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/main.ts +61 -3
- package/src/master-mode/context.ts +50 -0
- package/src/master-mode/first-request.ts +65 -0
- package/src/master-mode/snapshot.ts +205 -0
- package/src/modes/acp/acp-agent.ts +610 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/model-selector.ts +76 -8
- package/src/modes/components/oauth-selector.ts +6 -1
- package/src/modes/components/session-selector.ts +23 -15
- package/src/modes/components/sessions-dashboard.ts +8 -5
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/status-line/model-name.ts +53 -0
- package/src/modes/components/status-line/priority-row.ts +107 -0
- package/src/modes/components/status-line/segments.ts +23 -9
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/command-controller.ts +1 -1
- package/src/modes/controllers/event-controller.ts +30 -4
- package/src/modes/controllers/extension-ui-controller.ts +13 -13
- package/src/modes/controllers/input-controller.ts +30 -2
- package/src/modes/controllers/selector-controller.ts +200 -98
- package/src/modes/interactive-mode.ts +36 -12
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/prompt-action-autocomplete.ts +30 -4
- package/src/modes/runtime-init.ts +12 -9
- package/src/modes/theme/theme.ts +5 -0
- package/src/modes/types.ts +2 -0
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/ui-helpers.ts +22 -3
- package/src/prompts/agent-fragments/browser-aside.md +22 -0
- package/src/prompts/system/master-mode.md +28 -0
- package/src/prompts/tools/ask.md +1 -1
- package/src/prompts/tools/bash.md +1 -0
- package/src/prompts/tools/job.md +1 -1
- package/src/sdk/acp/adapter.ts +5 -0
- package/src/sdk/broker/broker.ts +1646 -27
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +81 -3
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle-ledger.ts +6 -0
- package/src/sdk/broker/lifecycle.ts +440 -75
- package/src/sdk/broker/master-capability.ts +232 -0
- package/src/sdk/broker/session-index.ts +121 -17
- package/src/sdk/broker/session-scope.ts +272 -0
- package/src/sdk/broker/spawn-authority.ts +1031 -0
- package/src/sdk/broker/spawn-substrate.ts +591 -0
- package/src/sdk/broker/startup-budget.ts +144 -2
- package/src/sdk/broker/state-version.ts +43 -14
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/broker/worktree-occupancy.ts +90 -0
- package/src/sdk/bus/chat-daemon-control.ts +5 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
- package/src/sdk/bus/discord-daemon.ts +50 -10
- package/src/sdk/bus/index.ts +84 -7
- package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
- package/src/sdk/bus/lifecycle-commands.ts +4 -0
- package/src/sdk/bus/notification-service.ts +22 -5
- package/src/sdk/bus/prompt-reconciliation.ts +7 -2
- package/src/sdk/bus/reconciliation-store.ts +55 -31
- package/src/sdk/bus/slack-conversation.ts +1 -1
- package/src/sdk/bus/slack-daemon.ts +72 -38
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/cli/index.ts +1 -0
- package/src/sdk/cli/master-cli.ts +259 -0
- package/src/sdk/cli/rows.ts +41 -15
- package/src/sdk/cli/session-cli.ts +297 -26
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/host/control/dispatch.ts +101 -40
- package/src/sdk/host/control/index.ts +2 -0
- package/src/sdk/host/control/operations.ts +2 -0
- package/src/sdk/host/control/runtime-gate.ts +203 -11
- package/src/sdk/host/events.ts +1 -0
- package/src/sdk/host/host.ts +29 -2
- package/src/sdk/host/query/cursor.ts +52 -7
- package/src/sdk/host/query/handlers.ts +33 -12
- package/src/sdk/host/query/revision-store.ts +12 -0
- package/src/sdk/host/session-runtime.test.ts +673 -14
- package/src/sdk/host/session-runtime.ts +674 -103
- package/src/sdk/host/surface-policy.ts +3 -3
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +17 -4
- package/src/sdk/prompt-deadline-manager.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +105 -3
- package/src/sdk/protocol/operation-registry.ts +14 -4
- package/src/sdk/receipt-state.ts +10 -0
- package/src/sdk/router/session-router.ts +219 -118
- package/src/sdk/session.ts +416 -195
- package/src/sdk/startup-capability.ts +1 -0
- package/src/sdk/turn-result.ts +22 -0
- package/src/session/agent-session.ts +1628 -318
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +40 -3
- package/src/session/internal/managed-session-storage.ts +153 -78
- package/src/session/session-manager.ts +850 -97
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- package/src/slash-commands/builtin-registry.ts +41 -0
- package/src/task/executor.ts +9 -4
- package/src/telemetry/events.ts +1138 -12
- package/src/tools/ask-contract.test.ts +64 -0
- package/src/tools/ask-contract.ts +36 -1
- package/src/tools/ask.ts +10 -8
- package/src/tools/bash-interactive.ts +11 -2
- package/src/tools/bash.ts +412 -75
- package/src/tools/browser/launch.ts +37 -0
- package/src/tools/browser/registry.ts +99 -9
- package/src/tools/cron.ts +6 -4
- package/src/tools/descriptors.test.ts +15 -0
- package/src/tools/descriptors.ts +5 -1
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +10 -6
- package/src/tools/job.ts +4 -1
- package/src/tools/monitor.ts +12 -9
- package/src/tools/resolve.ts +30 -10
- package/src/tools/tool-catalog.generated.ts +2 -2
- package/src/utils/external-editor.ts +72 -1
|
@@ -37,8 +37,15 @@ import {
|
|
|
37
37
|
settleOwnedWork,
|
|
38
38
|
} from "../../session/terminal-abort";
|
|
39
39
|
import { parseThinkingLevel } from "../../thinking";
|
|
40
|
+
import { readEndpointFile } from "../broker/endpoint-authority";
|
|
40
41
|
import { ensureBroker } from "../broker/ensure";
|
|
41
|
-
import {
|
|
42
|
+
import { processIncarnation } from "../broker/process-incarnation";
|
|
43
|
+
import {
|
|
44
|
+
type MasterRoleAttestationV2,
|
|
45
|
+
resolveSessionLocator,
|
|
46
|
+
SessionIndex,
|
|
47
|
+
type SessionLocatorV2,
|
|
48
|
+
} from "../broker/session-index";
|
|
42
49
|
import {
|
|
43
50
|
parseSyntheticModelId,
|
|
44
51
|
resolveSyntheticModelSelection,
|
|
@@ -49,17 +56,28 @@ import {
|
|
|
49
56
|
import { projectQ10Models } from "../models.js";
|
|
50
57
|
import { PromptDeadlineManager, type PromptTerminalTransitionEvidence } from "../prompt-deadline-manager";
|
|
51
58
|
import { formatPromptFailureForLocalLog, sanitizePromptFailure } from "../prompt-failure";
|
|
59
|
+
import type { SdkPromptTerminalOutcome } from "../prompt-status";
|
|
52
60
|
import { validateRequiredPromptText } from "../protocol/adapter-validation";
|
|
53
61
|
import { OPERATIONS } from "../protocol/operation-registry";
|
|
62
|
+
import { type ReceiptState, reduceReceiptState } from "../receipt-state";
|
|
54
63
|
import {
|
|
55
64
|
createKindAwareReconciliation,
|
|
56
65
|
createReconciliationStore,
|
|
57
66
|
type KindAwareReconciliation,
|
|
58
67
|
resolveReconciliationSessionFile,
|
|
59
68
|
} from "../reconciliation-extensions";
|
|
60
|
-
import {
|
|
61
|
-
|
|
62
|
-
|
|
69
|
+
import {
|
|
70
|
+
reduceTurnResultContent,
|
|
71
|
+
reportableTurnResultContent,
|
|
72
|
+
sanitizeTurnResultContent,
|
|
73
|
+
type TurnResultContent,
|
|
74
|
+
} from "../turn-result";
|
|
75
|
+
import { type ControlSurface, controlRequestFromFrame, dispatchControl, terminalAbortIdentity } from "./control";
|
|
76
|
+
import {
|
|
77
|
+
BROKER_RUNTIME_ABORT_CAPABILITY_FIELD,
|
|
78
|
+
BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD,
|
|
79
|
+
hasBrokerRuntimeAbortCapability,
|
|
80
|
+
} from "./control/runtime-gate";
|
|
63
81
|
import { SessionSdkHost, type SessionSdkHostOptions } from "./host";
|
|
64
82
|
import { clearAutoroutingInactive, isAutoroutingInactive, markAutoroutingInactive } from "./internal-autorouting-state";
|
|
65
83
|
import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "./query";
|
|
@@ -103,6 +121,65 @@ const SDK_ONLY_TERMINAL_PUBLICATION_WAIT_MS = 1_000;
|
|
|
103
121
|
* after this bound are abandoned — their durable broker state is the recovery
|
|
104
122
|
* authority, and outcomes are inherently uncertain. */
|
|
105
123
|
const GATE_RESOLUTION_QUIESCENCE_MS = 5_000;
|
|
124
|
+
/** Master verification fields are ephemeral protocol values, not unbounded input. */
|
|
125
|
+
const MASTER_AUTH_VALUE_MAX_LENGTH = 512;
|
|
126
|
+
const MASTER_AUTH_VALUE_PATTERN = /^[A-Za-z0-9_-]+$/u;
|
|
127
|
+
/** Keep replay protection finite even when an authenticated client floods nonces. */
|
|
128
|
+
const MASTER_NONCE_REPLAY_MAX_ENTRIES = 1_024;
|
|
129
|
+
/** Verification exchanges complete in seconds; retain replay evidence for a bounded window. */
|
|
130
|
+
const MASTER_NONCE_REPLAY_TTL_MS = 5 * 60_000;
|
|
131
|
+
|
|
132
|
+
export type MasterCapabilityReplayState = Map<string, number>;
|
|
133
|
+
|
|
134
|
+
export function verifyMasterCapabilityFrame(input: {
|
|
135
|
+
frame: { nonce: unknown; attestationEpoch: unknown; capability: unknown };
|
|
136
|
+
expectedCapability: string | undefined;
|
|
137
|
+
expectedEpoch: string | undefined;
|
|
138
|
+
replay: MasterCapabilityReplayState;
|
|
139
|
+
now?: number;
|
|
140
|
+
}): { ok: boolean; nonce: string; attestationEpoch: string } {
|
|
141
|
+
const now = input.now ?? Date.now();
|
|
142
|
+
for (const [consumedNonce, expiresAt] of input.replay) {
|
|
143
|
+
if (expiresAt <= now) input.replay.delete(consumedNonce);
|
|
144
|
+
}
|
|
145
|
+
const bounded = (value: unknown): value is string =>
|
|
146
|
+
typeof value === "string" &&
|
|
147
|
+
value.length > 0 &&
|
|
148
|
+
value.length <= MASTER_AUTH_VALUE_MAX_LENGTH &&
|
|
149
|
+
MASTER_AUTH_VALUE_PATTERN.test(value);
|
|
150
|
+
const nonce = bounded(input.frame.nonce) ? input.frame.nonce : "";
|
|
151
|
+
const epoch = bounded(input.frame.attestationEpoch) ? input.frame.attestationEpoch : "";
|
|
152
|
+
const capability = bounded(input.frame.capability) ? input.frame.capability : "";
|
|
153
|
+
const expectedCapability = bounded(input.expectedCapability) ? input.expectedCapability : "";
|
|
154
|
+
const expectedEpoch = bounded(input.expectedEpoch) ? input.expectedEpoch : "";
|
|
155
|
+
const authenticatedFields =
|
|
156
|
+
bounded(input.frame.nonce) &&
|
|
157
|
+
bounded(input.frame.attestationEpoch) &&
|
|
158
|
+
bounded(input.frame.capability) &&
|
|
159
|
+
bounded(input.expectedCapability) &&
|
|
160
|
+
bounded(input.expectedEpoch) &&
|
|
161
|
+
epoch === expectedEpoch;
|
|
162
|
+
const expectedBytes = Buffer.from(expectedCapability);
|
|
163
|
+
const capabilityBytes = Buffer.from(capability);
|
|
164
|
+
const capabilityMatches =
|
|
165
|
+
authenticatedFields &&
|
|
166
|
+
expectedBytes.length === capabilityBytes.length &&
|
|
167
|
+
crypto.timingSafeEqual(expectedBytes, capabilityBytes);
|
|
168
|
+
const reusable = capabilityMatches && !input.replay.has(nonce);
|
|
169
|
+
if (reusable) {
|
|
170
|
+
if (input.replay.size >= MASTER_NONCE_REPLAY_MAX_ENTRIES) {
|
|
171
|
+
const oldest = input.replay.keys().next().value;
|
|
172
|
+
if (typeof oldest === "string") input.replay.delete(oldest);
|
|
173
|
+
}
|
|
174
|
+
input.replay.set(nonce, now + MASTER_NONCE_REPLAY_TTL_MS);
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
ok: reusable,
|
|
178
|
+
nonce,
|
|
179
|
+
attestationEpoch: authenticatedFields ? epoch : "",
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
106
183
|
/** Maximum time a replaced runtime may retain a lifecycle persistence task. */
|
|
107
184
|
const LIFECYCLE_QUIESCENCE_MS = 1_000;
|
|
108
185
|
|
|
@@ -142,6 +219,8 @@ export interface SessionSdkRuntimeOptions
|
|
|
142
219
|
settings?: Settings;
|
|
143
220
|
/** Mutable shadow of patched config values merged into query readback. */
|
|
144
221
|
configOverrides?: Map<string, unknown>;
|
|
222
|
+
/** Stable master lineage identity retained across session switches/branches. */
|
|
223
|
+
masterOwnerSessionId?: string;
|
|
145
224
|
}
|
|
146
225
|
|
|
147
226
|
export interface SdkOnlyInvocationRecord extends InvocationCorrelation {
|
|
@@ -152,6 +231,8 @@ export interface SdkOnlyInvocationRecord extends InvocationCorrelation {
|
|
|
152
231
|
startedAt?: number;
|
|
153
232
|
terminalAt?: number;
|
|
154
233
|
error?: { code: string; message: string };
|
|
234
|
+
content?: TurnResultContent;
|
|
235
|
+
receiptState?: Exclude<ReceiptState, "absent">;
|
|
155
236
|
outcome?: unknown;
|
|
156
237
|
pendingOutcome?: unknown;
|
|
157
238
|
skillName?: string;
|
|
@@ -400,6 +481,8 @@ export interface CreateSdkSessionRuntimeOptions {
|
|
|
400
481
|
agentDir: string;
|
|
401
482
|
/** Lifecycle-owned sessions require broker publication before they become usable. */
|
|
402
483
|
brokerRegistrationRequired?: boolean;
|
|
484
|
+
/** Trusted broker-issued lifecycle marker bound to lifecycle host index events. */
|
|
485
|
+
lifecycleRequestId?: string;
|
|
403
486
|
createTransport(input: {
|
|
404
487
|
sessionId: string;
|
|
405
488
|
stateRoot: string;
|
|
@@ -411,6 +494,12 @@ export interface CreateSdkSessionRuntimeOptions {
|
|
|
411
494
|
onSdkRequest?: SessionSdkHostOptions["onRequest"];
|
|
412
495
|
/** Mutable shadow of patched config values merged into query readback. */
|
|
413
496
|
configOverrides?: Map<string, unknown>;
|
|
497
|
+
/** In-memory master capability for private broker verification only. */
|
|
498
|
+
masterCapability?: string;
|
|
499
|
+
/** Opaque direct-role epoch this effective host may adopt. */
|
|
500
|
+
masterAttestationEpoch?: string;
|
|
501
|
+
/** Stable master lineage identity retained across session switches/branches. */
|
|
502
|
+
masterOwnerSessionId?: string;
|
|
414
503
|
/** Private session-owned terminal-abort capabilities; never exposed on ExtensionContext. */
|
|
415
504
|
terminalAbortSeams?: SdkOnlyTerminalAbortSeams;
|
|
416
505
|
/** Callback when a frame is admitted to the runtime (test harness). */
|
|
@@ -419,6 +508,8 @@ export interface CreateSdkSessionRuntimeOptions {
|
|
|
419
508
|
onLifecycleDrainTimeoutForTests?: () => void;
|
|
420
509
|
/** Test-only observation of bounded failure-diagnostic deduplication state. */
|
|
421
510
|
onFailureDiagnosticKeyCountForTests?: (count: number) => void;
|
|
511
|
+
/** Test-only observation after a resolved invocation completion is reconciled. */
|
|
512
|
+
onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void;
|
|
422
513
|
}
|
|
423
514
|
|
|
424
515
|
function unavailable(operation: string): () => never {
|
|
@@ -436,7 +527,67 @@ export interface InvocationCorrelation {
|
|
|
436
527
|
|
|
437
528
|
export type InvocationKind = "prompt" | "skill" | "steer";
|
|
438
529
|
type InvocationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed" | "uncertain";
|
|
530
|
+
type InvocationOutcome = SdkPromptTerminalOutcome;
|
|
531
|
+
type InvocationOutcomeInput = {
|
|
532
|
+
kind: string;
|
|
533
|
+
reason?: unknown;
|
|
534
|
+
code?: unknown;
|
|
535
|
+
message?: unknown;
|
|
536
|
+
provenance?: unknown;
|
|
537
|
+
};
|
|
439
538
|
const EMPTY_PROMPT_FAILURE = { code: "prompt_failed", message: "Prompt submission failed." } as const;
|
|
539
|
+
const PRESERVED_PROVIDER_FAILURE_CODES = new Set([
|
|
540
|
+
"provider_down",
|
|
541
|
+
"provider_unavailable",
|
|
542
|
+
"provider_rejected",
|
|
543
|
+
"upstream_stream_interrupted",
|
|
544
|
+
"argument_validation",
|
|
545
|
+
"execution",
|
|
546
|
+
"upstream_error",
|
|
547
|
+
"transport_reset",
|
|
548
|
+
]);
|
|
549
|
+
const isPreservedProviderFailure = (code: string | undefined): boolean =>
|
|
550
|
+
code !== undefined && (PRESERVED_PROVIDER_FAILURE_CODES.has(code) || code.startsWith("provider_http_"));
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Durable reconciliation accepts only the bounded outcome vocabulary from
|
|
554
|
+
* reconciliation-store.ts. Provider diagnostics keep their specific safe code
|
|
555
|
+
* in `error`, while the durable outcome is normalized to the public failure
|
|
556
|
+
* classifier so a failed terminal can never be quarantined on reload.
|
|
557
|
+
*/
|
|
558
|
+
function canonicalFailedOutcome(
|
|
559
|
+
failure?: { code?: unknown; message?: unknown },
|
|
560
|
+
provenance: "agent_failed" | "deadline" = "agent_failed",
|
|
561
|
+
): InvocationOutcome {
|
|
562
|
+
const deadline = provenance === "deadline" || failure?.code === "prompt_deadline_exceeded";
|
|
563
|
+
return {
|
|
564
|
+
kind: "failed",
|
|
565
|
+
code: deadline ? "prompt_deadline_exceeded" : "prompt_failed",
|
|
566
|
+
message: deadline ? "Prompt deadline exceeded." : EMPTY_PROMPT_FAILURE.message,
|
|
567
|
+
provenance: deadline ? "deadline" : "agent_failed",
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function canonicalTerminalOutcome(
|
|
572
|
+
outcome: unknown,
|
|
573
|
+
failure?: { code?: unknown; message?: unknown },
|
|
574
|
+
): InvocationOutcome | undefined {
|
|
575
|
+
if (outcome && typeof outcome === "object") {
|
|
576
|
+
const candidate = outcome as { kind?: unknown; reason?: unknown; provenance?: unknown; code?: unknown };
|
|
577
|
+
if (candidate.kind === "stopped") {
|
|
578
|
+
if (candidate.reason === "cancelled" && candidate.provenance === "client_cancel")
|
|
579
|
+
return { kind: "stopped", reason: "cancelled", provenance: "client_cancel" };
|
|
580
|
+
return undefined;
|
|
581
|
+
}
|
|
582
|
+
if (candidate.kind === "failed")
|
|
583
|
+
return canonicalFailedOutcome(
|
|
584
|
+
{ code: candidate.code, message: (outcome as { message?: unknown }).message },
|
|
585
|
+
candidate.provenance === "deadline" ? "deadline" : "agent_failed",
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
if (failure !== undefined) return canonicalFailedOutcome(failure);
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
440
591
|
interface InvocationRecord extends InvocationCorrelation {
|
|
441
592
|
kind: InvocationKind;
|
|
442
593
|
revision: number;
|
|
@@ -447,6 +598,9 @@ interface InvocationRecord extends InvocationCorrelation {
|
|
|
447
598
|
startedAt?: number;
|
|
448
599
|
terminalAt?: number;
|
|
449
600
|
error?: { code: string; message: string };
|
|
601
|
+
content?: TurnResultContent;
|
|
602
|
+
receiptState?: Exclude<ReceiptState, "absent">;
|
|
603
|
+
outcome?: unknown;
|
|
450
604
|
}
|
|
451
605
|
export interface InvocationReconciliation {
|
|
452
606
|
/** Shared v2 reconciliation owner; present for durable terminal admission. */
|
|
@@ -462,7 +616,7 @@ export interface InvocationReconciliation {
|
|
|
462
616
|
type: "agent_start" | "agent_end";
|
|
463
617
|
content?: TurnResultContent;
|
|
464
618
|
hasActivity?: boolean;
|
|
465
|
-
outcome?:
|
|
619
|
+
outcome?: InvocationOutcome;
|
|
466
620
|
}
|
|
467
621
|
| { type: "agent_failed"; error: unknown; content?: TurnResultContent; hasActivity?: boolean },
|
|
468
622
|
): Promise<void>;
|
|
@@ -477,8 +631,8 @@ export interface InvocationReconciliation {
|
|
|
477
631
|
claimPendingOutcome(
|
|
478
632
|
kind: InvocationKind,
|
|
479
633
|
correlation: InvocationCorrelation,
|
|
480
|
-
outcome:
|
|
481
|
-
): Promise<
|
|
634
|
+
outcome: InvocationOutcomeInput,
|
|
635
|
+
): Promise<InvocationOutcome>;
|
|
482
636
|
// Positional compatibility (exact-head review P1): the 4th argument is either
|
|
483
637
|
// the generation-fence isCurrent callback or the legacy recordError object;
|
|
484
638
|
// both shapes are normalized in the implementation so a legacy object can
|
|
@@ -486,7 +640,7 @@ export interface InvocationReconciliation {
|
|
|
486
640
|
finalizeOutcome(
|
|
487
641
|
kind: InvocationKind,
|
|
488
642
|
correlation: InvocationCorrelation,
|
|
489
|
-
outcome?:
|
|
643
|
+
outcome?: InvocationOutcomeInput,
|
|
490
644
|
arg4?: (() => boolean) | { code: string; message: string },
|
|
491
645
|
// Terminal evidence accepted by finalizeOutcome (exact-head review P1/P2):
|
|
492
646
|
// when an explicit outcome is absent, a content-bearing, activity-bearing,
|
|
@@ -729,25 +883,71 @@ export function createInvocationReconciliation(
|
|
|
729
883
|
// so a real lifecycle event is never swallowed.
|
|
730
884
|
}
|
|
731
885
|
const current = records.get(recordKey);
|
|
886
|
+
const incomingOutcome = frame.type === "agent_end" ? canonicalTerminalOutcome(frame.outcome) : undefined;
|
|
887
|
+
const providerFailure =
|
|
888
|
+
current?.error !== undefined && current.error.code !== "prompt_deadline_exceeded";
|
|
889
|
+
if (
|
|
890
|
+
current !== undefined &&
|
|
891
|
+
frame.type === "agent_end" &&
|
|
892
|
+
incomingOutcome?.kind === "failed" &&
|
|
893
|
+
incomingOutcome.provenance !== "deadline"
|
|
894
|
+
) {
|
|
895
|
+
const content = reduceTurnResultContent(current.content, frame.content);
|
|
896
|
+
const failed = {
|
|
897
|
+
...current,
|
|
898
|
+
revision: ++mutationRevision,
|
|
899
|
+
status: "failed" as const,
|
|
900
|
+
outcome: incomingOutcome,
|
|
901
|
+
error:
|
|
902
|
+
current.error?.code !== "prompt_deadline_exceeded" && current.error !== undefined
|
|
903
|
+
? current.error
|
|
904
|
+
: { code: incomingOutcome.code, message: incomingOutcome.message },
|
|
905
|
+
content,
|
|
906
|
+
receiptState: reduceReceiptState(
|
|
907
|
+
current.receiptState,
|
|
908
|
+
reportableTurnResultContent(content) ? "present" : undefined,
|
|
909
|
+
),
|
|
910
|
+
};
|
|
911
|
+
delete (failed as unknown as { pendingOutcome?: unknown }).pendingOutcome;
|
|
912
|
+
delete (failed as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
|
|
913
|
+
records.set(recordKey, failed);
|
|
914
|
+
try {
|
|
915
|
+
await persist();
|
|
916
|
+
} catch (error) {
|
|
917
|
+
if (records.get(recordKey) === failed) records.set(recordKey, current);
|
|
918
|
+
throw error;
|
|
919
|
+
}
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
732
922
|
if (
|
|
733
923
|
frame.type === "agent_end" &&
|
|
734
924
|
current === pending.finalizedRecord &&
|
|
925
|
+
incomingOutcome?.kind !== "failed" &&
|
|
926
|
+
!providerFailure &&
|
|
735
927
|
(kind !== "prompt" ||
|
|
736
|
-
|
|
928
|
+
incomingOutcome?.kind === "stopped" ||
|
|
737
929
|
frame.hasActivity === true ||
|
|
738
930
|
frame.content?.text.trim())
|
|
739
931
|
) {
|
|
740
932
|
// A real lifecycle end that arrived during deadline finalization is
|
|
741
933
|
// stronger evidence than the synthetic deadline outcome. Upgrade the
|
|
742
|
-
// durable terminal instead of treating
|
|
934
|
+
// durable terminal instead of treating it as a duplicate.
|
|
935
|
+
const content = reduceTurnResultContent(current.content, frame.content);
|
|
743
936
|
const upgraded = {
|
|
744
937
|
...current,
|
|
745
938
|
revision: ++mutationRevision,
|
|
746
939
|
status: "terminal_ok" as const,
|
|
940
|
+
content,
|
|
941
|
+
...(incomingOutcome?.kind === "stopped" ? { outcome: incomingOutcome } : {}),
|
|
942
|
+
receiptState: reduceReceiptState(
|
|
943
|
+
current.receiptState,
|
|
944
|
+
reportableTurnResultContent(content) ? "present" : undefined,
|
|
945
|
+
),
|
|
747
946
|
};
|
|
748
947
|
// Drop the superseded synthetic deadline error so the surfaced payload is
|
|
749
948
|
// not the contradictory {status: terminal_ok, error: prompt_deadline_exceeded}.
|
|
750
949
|
delete (upgraded as Partial<InvocationRecord>).error;
|
|
950
|
+
if (incomingOutcome?.kind !== "stopped") delete (upgraded as Partial<InvocationRecord>).outcome;
|
|
751
951
|
records.set(recordKey, upgraded);
|
|
752
952
|
try {
|
|
753
953
|
await persist();
|
|
@@ -771,18 +971,115 @@ export function createInvocationReconciliation(
|
|
|
771
971
|
}
|
|
772
972
|
}
|
|
773
973
|
if (record.terminalAt !== undefined) {
|
|
974
|
+
if (frame.type === "agent_end") {
|
|
975
|
+
const incomingOutcome = canonicalTerminalOutcome(frame.outcome);
|
|
976
|
+
if (incomingOutcome?.kind === "failed" && incomingOutcome.provenance !== "deadline") {
|
|
977
|
+
const content = reduceTurnResultContent(record.content, frame.content);
|
|
978
|
+
const failed = {
|
|
979
|
+
...record,
|
|
980
|
+
revision: ++mutationRevision,
|
|
981
|
+
status: "failed" as const,
|
|
982
|
+
outcome: incomingOutcome,
|
|
983
|
+
error:
|
|
984
|
+
record.error?.code !== "prompt_deadline_exceeded" && record.error !== undefined
|
|
985
|
+
? record.error
|
|
986
|
+
: { code: incomingOutcome.code, message: incomingOutcome.message },
|
|
987
|
+
content,
|
|
988
|
+
receiptState: reduceReceiptState(
|
|
989
|
+
record.receiptState,
|
|
990
|
+
reportableTurnResultContent(content) ? "present" : undefined,
|
|
991
|
+
),
|
|
992
|
+
};
|
|
993
|
+
delete (failed as unknown as { pendingOutcome?: unknown }).pendingOutcome;
|
|
994
|
+
delete (failed as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
|
|
995
|
+
records.set(recordKey, failed);
|
|
996
|
+
try {
|
|
997
|
+
await persist();
|
|
998
|
+
} catch (error) {
|
|
999
|
+
if (records.get(recordKey) === failed) records.set(recordKey, record);
|
|
1000
|
+
throw error;
|
|
1001
|
+
}
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
if (frame.type === "agent_failed" && record.error?.code === "prompt_deadline_exceeded") {
|
|
1006
|
+
const failure = sanitizePromptFailure(frame.error);
|
|
1007
|
+
const next = {
|
|
1008
|
+
...record,
|
|
1009
|
+
revision: ++mutationRevision,
|
|
1010
|
+
status: "failed" as const,
|
|
1011
|
+
error: failure,
|
|
1012
|
+
outcome: canonicalFailedOutcome(failure),
|
|
1013
|
+
};
|
|
1014
|
+
delete (next as unknown as { pendingOutcome?: unknown }).pendingOutcome;
|
|
1015
|
+
delete (next as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
|
|
1016
|
+
records.set(recordKey, next);
|
|
1017
|
+
try {
|
|
1018
|
+
await persist();
|
|
1019
|
+
} catch (error) {
|
|
1020
|
+
if (records.get(recordKey) === next) records.set(recordKey, record);
|
|
1021
|
+
throw error;
|
|
1022
|
+
}
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
if (frame.type === "agent_end" && record.error?.code !== "prompt_deadline_exceeded") {
|
|
1026
|
+
const content = reduceTurnResultContent(record.content, frame.content);
|
|
1027
|
+
const receiptState = reduceReceiptState(
|
|
1028
|
+
record.receiptState,
|
|
1029
|
+
reportableTurnResultContent(content) ? "present" : undefined,
|
|
1030
|
+
);
|
|
1031
|
+
let outcome = record.outcome;
|
|
1032
|
+
const incomingOutcome = canonicalTerminalOutcome(frame.outcome);
|
|
1033
|
+
if (outcome === undefined) {
|
|
1034
|
+
if (record.status === "failed" && record.error !== undefined)
|
|
1035
|
+
outcome = canonicalFailedOutcome(record.error);
|
|
1036
|
+
else if (record.status === "terminal_ok" && incomingOutcome?.kind === "stopped")
|
|
1037
|
+
outcome = incomingOutcome;
|
|
1038
|
+
}
|
|
1039
|
+
if (content !== record.content || receiptState !== record.receiptState || outcome !== record.outcome) {
|
|
1040
|
+
const enriched = {
|
|
1041
|
+
...record,
|
|
1042
|
+
revision: ++mutationRevision,
|
|
1043
|
+
content,
|
|
1044
|
+
receiptState,
|
|
1045
|
+
...(outcome === undefined ? {} : { outcome }),
|
|
1046
|
+
};
|
|
1047
|
+
records.set(recordKey, enriched);
|
|
1048
|
+
try {
|
|
1049
|
+
await persist();
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
if (records.get(recordKey) === enriched) records.set(recordKey, record);
|
|
1052
|
+
throw error;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
774
1057
|
if (
|
|
775
1058
|
frame.type === "agent_end" &&
|
|
776
1059
|
record.error?.code === "prompt_deadline_exceeded" &&
|
|
1060
|
+
canonicalTerminalOutcome(frame.outcome)?.kind !== "failed" &&
|
|
777
1061
|
(kind !== "prompt" ||
|
|
778
|
-
frame.outcome?.kind === "stopped" ||
|
|
1062
|
+
canonicalTerminalOutcome(frame.outcome)?.kind === "stopped" ||
|
|
779
1063
|
frame.hasActivity === true ||
|
|
780
1064
|
frame.content?.text.trim())
|
|
781
1065
|
) {
|
|
782
|
-
const
|
|
1066
|
+
const content = reduceTurnResultContent(record.content, frame.content);
|
|
1067
|
+
const incomingOutcome = canonicalTerminalOutcome(frame.outcome);
|
|
1068
|
+
const upgraded = {
|
|
1069
|
+
...record,
|
|
1070
|
+
revision: ++mutationRevision,
|
|
1071
|
+
status: "terminal_ok" as const,
|
|
1072
|
+
content,
|
|
1073
|
+
...(incomingOutcome?.kind === "stopped" ? { outcome: incomingOutcome } : {}),
|
|
1074
|
+
receiptState: reduceReceiptState(
|
|
1075
|
+
record.receiptState,
|
|
1076
|
+
reportableTurnResultContent(content) ? "present" : undefined,
|
|
1077
|
+
),
|
|
1078
|
+
};
|
|
783
1079
|
// The synthetic deadline error is superseded evidence; terminal_ok must not
|
|
784
1080
|
// surface a deadline failure for a prompt that actually completed.
|
|
785
1081
|
delete (upgraded as Partial<InvocationRecord>).error;
|
|
1082
|
+
if (incomingOutcome?.kind !== "stopped") delete (upgraded as Partial<InvocationRecord>).outcome;
|
|
786
1083
|
records.set(recordKey, upgraded);
|
|
787
1084
|
try {
|
|
788
1085
|
await persist();
|
|
@@ -839,19 +1136,43 @@ export function createInvocationReconciliation(
|
|
|
839
1136
|
if (next.error === undefined || (next.error.code === "agent_failed" && failure.code !== "agent_failed"))
|
|
840
1137
|
next.error = failure;
|
|
841
1138
|
} else {
|
|
1139
|
+
const pendingOutcome = (next as unknown as { pendingOutcome?: unknown }).pendingOutcome;
|
|
1140
|
+
delete (next as unknown as { pendingOutcome?: unknown }).pendingOutcome;
|
|
1141
|
+
delete (next as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
|
|
1142
|
+
next.content = reduceTurnResultContent(next.content, frame.content);
|
|
1143
|
+
const incomingOutcome = canonicalTerminalOutcome(frame.outcome) ?? canonicalTerminalOutcome(pendingOutcome);
|
|
842
1144
|
if (
|
|
1145
|
+
incomingOutcome?.kind === "stopped" &&
|
|
1146
|
+
(next.error === undefined || next.error.code === "prompt_deadline_exceeded")
|
|
1147
|
+
) {
|
|
1148
|
+
next.outcome = incomingOutcome;
|
|
1149
|
+
if (next.error?.code === "prompt_deadline_exceeded") delete next.error;
|
|
1150
|
+
next.status = "terminal_ok";
|
|
1151
|
+
} else if (incomingOutcome?.kind === "failed") {
|
|
1152
|
+
next.outcome = incomingOutcome;
|
|
1153
|
+
if (next.error === undefined)
|
|
1154
|
+
next.error = { code: incomingOutcome.code, message: incomingOutcome.message };
|
|
1155
|
+
next.status = "failed";
|
|
1156
|
+
} else if (next.error !== undefined) {
|
|
1157
|
+
next.outcome = canonicalFailedOutcome(next.error);
|
|
1158
|
+
next.status = "failed";
|
|
1159
|
+
} else if (
|
|
843
1160
|
kind === "prompt" &&
|
|
844
|
-
next.error === undefined &&
|
|
845
1161
|
frame.type === "agent_end" &&
|
|
846
|
-
frame.outcome?.kind !== "stopped" &&
|
|
847
1162
|
!frame.content?.text.trim() &&
|
|
848
1163
|
!frame.hasActivity
|
|
849
1164
|
) {
|
|
850
1165
|
next.status = "failed";
|
|
851
1166
|
next.error = EMPTY_PROMPT_FAILURE;
|
|
852
|
-
|
|
1167
|
+
next.outcome = canonicalFailedOutcome(EMPTY_PROMPT_FAILURE);
|
|
1168
|
+
} else next.status = "terminal_ok";
|
|
1169
|
+
next.receiptState = reduceReceiptState(
|
|
1170
|
+
next.receiptState,
|
|
1171
|
+
reportableTurnResultContent(next.content) ? "present" : "missing",
|
|
1172
|
+
);
|
|
853
1173
|
next.terminalAt = Date.now();
|
|
854
1174
|
}
|
|
1175
|
+
|
|
855
1176
|
records.set(recordKey, next);
|
|
856
1177
|
try {
|
|
857
1178
|
await persist();
|
|
@@ -862,20 +1183,24 @@ export function createInvocationReconciliation(
|
|
|
862
1183
|
},
|
|
863
1184
|
lookup(kind, selector) {
|
|
864
1185
|
const record = find(kind, selector);
|
|
865
|
-
if (!record) return { status: "unknown" };
|
|
1186
|
+
if (!record) return { status: "unknown", receiptState: "unknown" };
|
|
866
1187
|
const identity = {
|
|
867
1188
|
commandId: record.commandId,
|
|
868
1189
|
turnId: record.turnId,
|
|
869
1190
|
...(record.clientRef === undefined ? {} : { clientRef: record.clientRef }),
|
|
870
1191
|
acceptedAt: record.acceptedAt,
|
|
871
1192
|
};
|
|
872
|
-
if (record.status === "accepted") return { status: "accepted", ...identity };
|
|
873
|
-
if (record.status === "in_flight")
|
|
1193
|
+
if (record.status === "accepted") return { status: "accepted", receiptState: "absent", ...identity };
|
|
1194
|
+
if (record.status === "in_flight")
|
|
1195
|
+
return { status: "in_flight", receiptState: "absent", ...identity, startedAt: record.startedAt };
|
|
874
1196
|
return {
|
|
875
1197
|
status: record.status,
|
|
876
1198
|
...identity,
|
|
877
1199
|
...(record.startedAt === undefined ? {} : { startedAt: record.startedAt }),
|
|
878
1200
|
terminalAt: record.terminalAt,
|
|
1201
|
+
receiptState: record.receiptState ?? "unknown",
|
|
1202
|
+
...(record.outcome === undefined ? {} : { outcome: record.outcome }),
|
|
1203
|
+
...(reportableTurnResultContent(record.content) ? { content: record.content } : {}),
|
|
879
1204
|
...(record.error === undefined ? {} : { error: record.error }),
|
|
880
1205
|
};
|
|
881
1206
|
},
|
|
@@ -902,11 +1227,13 @@ export function createInvocationReconciliation(
|
|
|
902
1227
|
hydrate,
|
|
903
1228
|
async claimPendingOutcome(kind, correlation, outcome) {
|
|
904
1229
|
const record = records.get(key(kind, correlation));
|
|
905
|
-
|
|
1230
|
+
const normalized = canonicalTerminalOutcome(outcome);
|
|
1231
|
+
if (normalized === undefined) throw new Error("Invalid terminal outcome.");
|
|
1232
|
+
if (!record || record.terminalAt !== undefined || record.kind !== kind) return normalized;
|
|
906
1233
|
const pending = (record as unknown as { pendingOutcome?: unknown }).pendingOutcome;
|
|
907
|
-
if (pending !== undefined) return pending;
|
|
1234
|
+
if (pending !== undefined) return canonicalTerminalOutcome(pending) ?? normalized;
|
|
908
1235
|
const next = { ...record, revision: ++mutationRevision } as InvocationRecord & { pendingOutcome?: unknown };
|
|
909
|
-
next.pendingOutcome =
|
|
1236
|
+
next.pendingOutcome = normalized;
|
|
910
1237
|
records.set(key(kind, correlation), next);
|
|
911
1238
|
try {
|
|
912
1239
|
await persist();
|
|
@@ -914,7 +1241,7 @@ export function createInvocationReconciliation(
|
|
|
914
1241
|
if (records.get(key(kind, correlation)) === next) records.set(key(kind, correlation), record);
|
|
915
1242
|
throw error;
|
|
916
1243
|
}
|
|
917
|
-
return
|
|
1244
|
+
return normalized;
|
|
918
1245
|
},
|
|
919
1246
|
async finalizeOutcome(
|
|
920
1247
|
kind,
|
|
@@ -946,36 +1273,64 @@ export function createInvocationReconciliation(
|
|
|
946
1273
|
const recordKey = key(kind, correlation);
|
|
947
1274
|
const record = records.get(recordKey);
|
|
948
1275
|
if (!record || record.terminalAt !== undefined || record.kind !== kind) return;
|
|
949
|
-
const
|
|
950
|
-
(record as unknown as { pendingOutcome?:
|
|
951
|
-
|
|
1276
|
+
const requestedOutcome = canonicalTerminalOutcome(
|
|
1277
|
+
outcome ?? (record as unknown as { pendingOutcome?: unknown }).pendingOutcome,
|
|
1278
|
+
);
|
|
1279
|
+
const priorFailure = record.error;
|
|
1280
|
+
// A provider diagnostic recorded before a deadline finalization is stronger
|
|
1281
|
+
// than the synthetic deadline claim. Preserve it as the terminal failure
|
|
1282
|
+
// intent instead of allowing a later agent_end to look successful.
|
|
1283
|
+
const finalOutcome =
|
|
1284
|
+
priorFailure !== undefined && priorFailure.code !== "prompt_deadline_exceeded"
|
|
1285
|
+
? canonicalFailedOutcome(priorFailure)
|
|
1286
|
+
: requestedOutcome?.kind === "stopped"
|
|
1287
|
+
? requestedOutcome
|
|
1288
|
+
: priorFailure !== undefined &&
|
|
1289
|
+
(requestedOutcome === undefined ||
|
|
1290
|
+
(requestedOutcome.kind === "failed" &&
|
|
1291
|
+
(requestedOutcome.code === "prompt_deadline_exceeded" ||
|
|
1292
|
+
priorFailure.code !== "prompt_deadline_exceeded")))
|
|
1293
|
+
? canonicalFailedOutcome(priorFailure)
|
|
1294
|
+
: requestedOutcome;
|
|
952
1295
|
const previousRecord = { ...record };
|
|
953
1296
|
const finalizedRecord: InvocationRecord = { ...record, revision: ++mutationRevision, terminalAt: Date.now() };
|
|
954
|
-
|
|
1297
|
+
finalizedRecord.content = reduceTurnResultContent(finalizedRecord.content, evidence?.content);
|
|
1298
|
+
let resolvedOutcome = finalOutcome;
|
|
1299
|
+
if (resolvedOutcome?.kind === "stopped") {
|
|
1300
|
+
// Stopped is a successful terminal boundary: it must never coexist with
|
|
1301
|
+
// an earlier diagnostic failure.
|
|
1302
|
+
finalizedRecord.status = "terminal_ok";
|
|
1303
|
+
delete finalizedRecord.error;
|
|
1304
|
+
} else if (resolvedOutcome?.kind === "failed") {
|
|
955
1305
|
finalizedRecord.status = "failed";
|
|
956
|
-
// Legacy positional recordError overrides the recorded cause; the
|
|
957
|
-
// outcome's own code/message is the default (exact-head review P1).
|
|
958
1306
|
finalizedRecord.error =
|
|
959
1307
|
recordError !== undefined
|
|
960
1308
|
? { code: recordError.code, message: recordError.message }
|
|
961
|
-
:
|
|
962
|
-
|
|
1309
|
+
: priorFailure !== undefined &&
|
|
1310
|
+
(resolvedOutcome.code === "prompt_deadline_exceeded" ||
|
|
1311
|
+
priorFailure.code !== "prompt_deadline_exceeded")
|
|
1312
|
+
? priorFailure
|
|
1313
|
+
: { code: resolvedOutcome.code, message: resolvedOutcome.message };
|
|
1314
|
+
} else if (kind === "prompt") {
|
|
963
1315
|
// Evidence-based empty predicate (exact-head review P1/P2): the same
|
|
964
1316
|
// semantics as the noteTransition agent_end classifier. A
|
|
965
|
-
// content-bearing
|
|
966
|
-
// finalization without an explicit outcome is a successful terminal;
|
|
1317
|
+
// content-bearing or activity-bearing finalization is successful;
|
|
967
1318
|
// only a genuinely empty, no-activity completion fails closed.
|
|
968
1319
|
const terminalText = evidence?.content?.text.trim() ?? "";
|
|
969
1320
|
if (terminalText === "" && !evidence?.hasActivity && evidence?.outcomeKind !== "stopped") {
|
|
1321
|
+
resolvedOutcome = canonicalFailedOutcome(EMPTY_PROMPT_FAILURE);
|
|
970
1322
|
finalizedRecord.status = "failed";
|
|
971
1323
|
finalizedRecord.error = EMPTY_PROMPT_FAILURE;
|
|
972
|
-
} else
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
finalizedRecord.
|
|
977
|
-
|
|
978
|
-
(
|
|
1324
|
+
} else finalizedRecord.status = "terminal_ok";
|
|
1325
|
+
} else finalizedRecord.status = "terminal_ok";
|
|
1326
|
+
finalizedRecord.receiptState = reduceReceiptState(
|
|
1327
|
+
finalizedRecord.receiptState,
|
|
1328
|
+
reportableTurnResultContent(finalizedRecord.content) ? "present" : "unknown",
|
|
1329
|
+
);
|
|
1330
|
+
if (resolvedOutcome === undefined) delete finalizedRecord.outcome;
|
|
1331
|
+
else finalizedRecord.outcome = resolvedOutcome;
|
|
1332
|
+
delete (finalizedRecord as unknown as Record<string, unknown>).pendingOutcome;
|
|
1333
|
+
delete (finalizedRecord as unknown as Record<string, unknown>).pendingReceiptState;
|
|
979
1334
|
if (isCurrent !== undefined && !isCurrent()) return;
|
|
980
1335
|
const commit = Promise.withResolvers<void>();
|
|
981
1336
|
// This promise is an internal coordination signal for lifecycle transitions
|
|
@@ -1025,8 +1380,13 @@ export function createInvocationReconciliation(
|
|
|
1025
1380
|
};
|
|
1026
1381
|
if (isCurrent !== undefined && !isCurrent()) return;
|
|
1027
1382
|
delete next.terminalAt;
|
|
1028
|
-
|
|
1029
|
-
|
|
1383
|
+
if (next.error?.code === "prompt_deadline_exceeded" || !isPreservedProviderFailure(next.error?.code))
|
|
1384
|
+
delete next.error;
|
|
1385
|
+
delete next.receiptState;
|
|
1386
|
+
delete (next as unknown as { outcome?: unknown; pendingOutcome?: unknown; pendingReceiptState?: unknown })
|
|
1387
|
+
.outcome;
|
|
1388
|
+
delete (next as unknown as { pendingOutcome?: unknown }).pendingOutcome;
|
|
1389
|
+
delete (next as unknown as { pendingReceiptState?: unknown }).pendingReceiptState;
|
|
1030
1390
|
(next as unknown as { deadlineRecoveryPending?: boolean }).deadlineRecoveryPending = true;
|
|
1031
1391
|
if (deadlineMaxAt !== undefined) (next as unknown as { deadlineMaxAt?: number }).deadlineMaxAt = deadlineMaxAt;
|
|
1032
1392
|
records.set(recordKey, next);
|
|
@@ -1065,6 +1425,7 @@ export interface SdkSurfaceFactoryOptions {
|
|
|
1065
1425
|
}) => unknown;
|
|
1066
1426
|
steerStatusLookup?: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
|
|
1067
1427
|
hostTools?: boolean | (() => boolean);
|
|
1428
|
+
getRuntimeHost?: () => SessionSdkHost | undefined;
|
|
1068
1429
|
}
|
|
1069
1430
|
|
|
1070
1431
|
/** Shared policy, capability, and query-surface factory for every SDK transport. */
|
|
@@ -1094,6 +1455,7 @@ function createQuerySurface(
|
|
|
1094
1455
|
}) => unknown;
|
|
1095
1456
|
steerStatusLookup?: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
|
|
1096
1457
|
hostTools?: boolean | (() => boolean);
|
|
1458
|
+
getRuntimeHost?: () => SessionSdkHost | undefined;
|
|
1097
1459
|
} = {},
|
|
1098
1460
|
): SessionSurface {
|
|
1099
1461
|
const policy =
|
|
@@ -1117,18 +1479,15 @@ function createQuerySurface(
|
|
|
1117
1479
|
kind: ctx.sessionMetadata?.kind ?? "main",
|
|
1118
1480
|
});
|
|
1119
1481
|
const lastAssistant = () => {
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
if (
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
.
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
)
|
|
1130
|
-
.map(block => block.text)
|
|
1131
|
-
.join("");
|
|
1482
|
+
const transcript =
|
|
1483
|
+
typeof (ctx as Partial<ExtensionContext>).getTranscript === "function" ? ctx.getTranscript() : [];
|
|
1484
|
+
for (const entry of transcript.toReversed()) {
|
|
1485
|
+
if (entry.role !== "assistant") continue;
|
|
1486
|
+
const text =
|
|
1487
|
+
typeof entry.body === "string"
|
|
1488
|
+
? entry.body
|
|
1489
|
+
: entry.content?.flatMap(block => (block.type === "text" ? [block.text] : [])).join("\n");
|
|
1490
|
+
if (text !== undefined && text.trim().length > 0) return text;
|
|
1132
1491
|
}
|
|
1133
1492
|
return undefined;
|
|
1134
1493
|
};
|
|
@@ -1500,6 +1859,28 @@ function createQuerySurface(
|
|
|
1500
1859
|
turnId?: string;
|
|
1501
1860
|
clientRef?: string;
|
|
1502
1861
|
}) => (options.turnResultLookup ?? (value => reconciliation.lookupResult(value.kind, value)))(selector),
|
|
1862
|
+
getCheckpointSnapshot: () => {
|
|
1863
|
+
const host = options.getRuntimeHost?.();
|
|
1864
|
+
if (!host)
|
|
1865
|
+
throw Object.assign(new Error("Atomic session checkpoint capture is unavailable."), {
|
|
1866
|
+
code: "unavailable",
|
|
1867
|
+
});
|
|
1868
|
+
const entries =
|
|
1869
|
+
typeof (ctx as Partial<ExtensionContext>).getTranscript === "function" ? ctx.getTranscript() : undefined;
|
|
1870
|
+
if (!Array.isArray(entries))
|
|
1871
|
+
throw Object.assign(new Error("Atomic session checkpoint capture is unavailable."), {
|
|
1872
|
+
code: "unavailable",
|
|
1873
|
+
});
|
|
1874
|
+
return {
|
|
1875
|
+
entries,
|
|
1876
|
+
watermark: {
|
|
1877
|
+
revision: entries.length,
|
|
1878
|
+
generation: host.events.generation,
|
|
1879
|
+
seq: host.events.sequence,
|
|
1880
|
+
idle: ctx.isIdle(),
|
|
1881
|
+
},
|
|
1882
|
+
};
|
|
1883
|
+
},
|
|
1503
1884
|
getSteerStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
1504
1885
|
(options.steerStatusLookup ?? (value => reconciliation.lookup("steer", value)))(selector),
|
|
1505
1886
|
getModelProfiles: async () => {
|
|
@@ -1549,6 +1930,7 @@ export function createSdkSurfaceFactory(
|
|
|
1549
1930
|
turnResultLookup: options.turnResultLookup,
|
|
1550
1931
|
steerStatusLookup: options.steerStatusLookup,
|
|
1551
1932
|
hostTools: options.hostTools,
|
|
1933
|
+
getRuntimeHost: options.getRuntimeHost,
|
|
1552
1934
|
});
|
|
1553
1935
|
return {
|
|
1554
1936
|
policy,
|
|
@@ -1856,6 +2238,7 @@ function createControlSurface(
|
|
|
1856
2238
|
) => void,
|
|
1857
2239
|
canResolveGate: () => boolean = () => true,
|
|
1858
2240
|
trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
|
|
2241
|
+
onInvocationCompletionReconciledForTests?: (kind: InvocationKind, correlation: InvocationCorrelation) => void,
|
|
1859
2242
|
): ControlSurface {
|
|
1860
2243
|
const normalizePromptImages = (value: unknown): ImageContent[] => {
|
|
1861
2244
|
if (!Array.isArray(value)) return [];
|
|
@@ -2103,18 +2486,9 @@ function createControlSurface(
|
|
|
2103
2486
|
try {
|
|
2104
2487
|
await reconciliation.noteTransition(kind, correlation, {
|
|
2105
2488
|
type: "agent_end",
|
|
2106
|
-
...(typeof result === "string"
|
|
2107
|
-
? {
|
|
2108
|
-
content: {
|
|
2109
|
-
version: 1,
|
|
2110
|
-
type: "text",
|
|
2111
|
-
text: result,
|
|
2112
|
-
byteLength: 0,
|
|
2113
|
-
truncated: false,
|
|
2114
|
-
},
|
|
2115
|
-
}
|
|
2116
|
-
: {}),
|
|
2489
|
+
...(typeof result === "string" ? { content: sanitizeTurnResultContent(result) } : {}),
|
|
2117
2490
|
});
|
|
2491
|
+
onInvocationCompletionReconciledForTests?.(kind, correlation);
|
|
2118
2492
|
retirePendingOwner?.(kind, correlation, "always");
|
|
2119
2493
|
} catch (transitionError) {
|
|
2120
2494
|
if (kind === "prompt" || kind === "skill") {
|
|
@@ -2269,7 +2643,7 @@ function createControlSurface(
|
|
|
2269
2643
|
}
|
|
2270
2644
|
};
|
|
2271
2645
|
const terminalAbort = async (
|
|
2272
|
-
input: { mode: "terminal"; scope?: "turn" | "owned" },
|
|
2646
|
+
input: { mode: "terminal"; scope?: "turn" | "owned"; operator?: boolean },
|
|
2273
2647
|
idempotencyKey?: string,
|
|
2274
2648
|
): Promise<unknown> => {
|
|
2275
2649
|
const scope = input.scope === "owned" ? "owned" : "turn";
|
|
@@ -2300,10 +2674,12 @@ function createControlSurface(
|
|
|
2300
2674
|
typeof idempotencyKey === "string"
|
|
2301
2675
|
? crypto.createHash("sha256").update(idempotencyKey).digest("hex")
|
|
2302
2676
|
: undefined;
|
|
2303
|
-
const
|
|
2304
|
-
|
|
2305
|
-
.
|
|
2306
|
-
|
|
2677
|
+
const abortIdentity = terminalAbortIdentity(
|
|
2678
|
+
{ mode: "terminal", scope, ...(input.operator === true ? { operator: true } : {}) },
|
|
2679
|
+
input.operator === true,
|
|
2680
|
+
);
|
|
2681
|
+
if (!abortIdentity) throw new Error("Terminal abort identity is invalid after dispatch validation.");
|
|
2682
|
+
const inputHash = abortIdentity.inputHash;
|
|
2307
2683
|
const stored = (record: SdkOnlyTerminalScopeRecord | SdkOnlyEvictedTerminalKeyEntry) => ({
|
|
2308
2684
|
responseState: record.responseState ?? "pending",
|
|
2309
2685
|
responsePayloadHash: record.responsePayloadHash ?? inputHash,
|
|
@@ -2676,6 +3052,16 @@ function createControlSurface(
|
|
|
2676
3052
|
pendingPreflights.delete(requesterBucketKey);
|
|
2677
3053
|
}
|
|
2678
3054
|
};
|
|
3055
|
+
const returnNoActiveTurn = async () => {
|
|
3056
|
+
const result = {
|
|
3057
|
+
ok: true,
|
|
3058
|
+
selection: scope,
|
|
3059
|
+
turn: "no_active_turn",
|
|
3060
|
+
terminal: "terminal_no_effect",
|
|
3061
|
+
};
|
|
3062
|
+
await finalizeNoEffectReservation(result);
|
|
3063
|
+
return result;
|
|
3064
|
+
};
|
|
2679
3065
|
if (!handle || epoch === undefined) {
|
|
2680
3066
|
if ((await writeNoEffect()) === "conflict") {
|
|
2681
3067
|
throw Object.assign(new Error("Idempotency key was reused with different input."), {
|
|
@@ -2691,6 +3077,10 @@ function createControlSurface(
|
|
|
2691
3077
|
if (replayed !== undefined) return replayed;
|
|
2692
3078
|
}
|
|
2693
3079
|
cancelRequesterPreflights();
|
|
3080
|
+
// Operator authority overrides connection ownership only for a turn
|
|
3081
|
+
// observed at admission. It must never adopt a successor that starts
|
|
3082
|
+
// while the durable no-effect reservation is being written.
|
|
3083
|
+
if (input.operator === true) return await returnNoActiveTurn();
|
|
2694
3084
|
// A prompt for this requester may have become ACTIVE while the
|
|
2695
3085
|
// reservation awaited the filesystem transaction: its submit()
|
|
2696
3086
|
// cleanup already removed the preflight callback, so cancelling here
|
|
@@ -2706,14 +3096,7 @@ function createControlSurface(
|
|
|
2706
3096
|
// No prompt won the race: finalize the reserved row so a later
|
|
2707
3097
|
// same-key retry replays this deterministic no_active_turn result
|
|
2708
3098
|
// (review thread P2).
|
|
2709
|
-
|
|
2710
|
-
ok: true,
|
|
2711
|
-
selection: scope,
|
|
2712
|
-
turn: "no_active_turn",
|
|
2713
|
-
terminal: "terminal_no_effect",
|
|
2714
|
-
};
|
|
2715
|
-
await finalizeNoEffectReservation(noActiveTurnResult);
|
|
2716
|
-
return noActiveTurnResult;
|
|
3099
|
+
return await returnNoActiveTurn();
|
|
2717
3100
|
}
|
|
2718
3101
|
handle = recheckedHandle;
|
|
2719
3102
|
epoch = recheckedEpoch;
|
|
@@ -2743,7 +3126,11 @@ function createControlSurface(
|
|
|
2743
3126
|
// cleared after a terminal lifecycle boundary) authorizes no client, and
|
|
2744
3127
|
// a stale prior owner authorizes only that old client — never a later
|
|
2745
3128
|
// turn it did not submit (review thread P1).
|
|
2746
|
-
if (
|
|
3129
|
+
if (
|
|
3130
|
+
input.operator !== true &&
|
|
3131
|
+
handle &&
|
|
3132
|
+
(abortingConnectionId === undefined || !currentOwnerConnectionIds().has(abortingConnectionId))
|
|
3133
|
+
) {
|
|
2747
3134
|
if ((await writeNoEffect()) === "conflict") {
|
|
2748
3135
|
throw Object.assign(new Error("Idempotency key was reused with different input."), {
|
|
2749
3136
|
code: "idempotency_conflict",
|
|
@@ -3412,6 +3799,107 @@ export function registerSdkOnlyNotificationCommand(api: ExtensionAPI): void {
|
|
|
3412
3799
|
});
|
|
3413
3800
|
}
|
|
3414
3801
|
|
|
3802
|
+
export function masterAttestationForEffectiveHost(input: {
|
|
3803
|
+
masterCapability: string | undefined;
|
|
3804
|
+
attestationEpoch: string | undefined;
|
|
3805
|
+
ownerSessionId?: string;
|
|
3806
|
+
sessionId: string;
|
|
3807
|
+
pid: number;
|
|
3808
|
+
processIncarnation: string | undefined;
|
|
3809
|
+
direct: MasterRoleAttestationV2 | undefined;
|
|
3810
|
+
}): MasterRoleAttestationV2 | undefined {
|
|
3811
|
+
const direct = input.direct;
|
|
3812
|
+
if (
|
|
3813
|
+
input.masterCapability === undefined ||
|
|
3814
|
+
input.attestationEpoch === undefined ||
|
|
3815
|
+
direct === undefined ||
|
|
3816
|
+
direct.ownerSessionId !== (input.ownerSessionId ?? input.sessionId) ||
|
|
3817
|
+
direct.launchPid !== input.pid ||
|
|
3818
|
+
direct.launchProcessIncarnation !== input.processIncarnation ||
|
|
3819
|
+
direct.attestationEpoch !== input.attestationEpoch
|
|
3820
|
+
)
|
|
3821
|
+
return undefined;
|
|
3822
|
+
return direct;
|
|
3823
|
+
}
|
|
3824
|
+
|
|
3825
|
+
function masterDirectAttestation(input: {
|
|
3826
|
+
masterCapability: string | undefined;
|
|
3827
|
+
attestationEpoch: string | undefined;
|
|
3828
|
+
ownerSessionId?: string;
|
|
3829
|
+
sessionId: string;
|
|
3830
|
+
pid: number;
|
|
3831
|
+
processIncarnation: string | undefined;
|
|
3832
|
+
}): MasterRoleAttestationV2 | undefined {
|
|
3833
|
+
if (
|
|
3834
|
+
input.masterCapability === undefined ||
|
|
3835
|
+
input.attestationEpoch === undefined ||
|
|
3836
|
+
input.processIncarnation === undefined
|
|
3837
|
+
)
|
|
3838
|
+
return undefined;
|
|
3839
|
+
return {
|
|
3840
|
+
version: 2,
|
|
3841
|
+
ownerSessionId: input.ownerSessionId ?? input.sessionId,
|
|
3842
|
+
launchPid: input.pid,
|
|
3843
|
+
launchProcessIncarnation: input.processIncarnation,
|
|
3844
|
+
role: "master",
|
|
3845
|
+
attestationEpoch: input.attestationEpoch,
|
|
3846
|
+
};
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
function sameMasterAttestation(left: MasterRoleAttestationV2, right: MasterRoleAttestationV2): boolean {
|
|
3850
|
+
return (
|
|
3851
|
+
left.version === right.version &&
|
|
3852
|
+
left.ownerSessionId === right.ownerSessionId &&
|
|
3853
|
+
left.launchPid === right.launchPid &&
|
|
3854
|
+
left.launchProcessIncarnation === right.launchProcessIncarnation &&
|
|
3855
|
+
left.role === right.role &&
|
|
3856
|
+
left.attestationEpoch === right.attestationEpoch
|
|
3857
|
+
);
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3860
|
+
/**
|
|
3861
|
+
* Establish direct master authority for the session currently hosted by this
|
|
3862
|
+
* process. Session identity transitions retain the master capability and epoch
|
|
3863
|
+
* but require a new direct attestation before their effective endpoint can
|
|
3864
|
+
* adopt that authority.
|
|
3865
|
+
*/
|
|
3866
|
+
export async function reattestMasterSessionIdentity(input: {
|
|
3867
|
+
index: SessionIndex;
|
|
3868
|
+
locator: SessionLocatorV2;
|
|
3869
|
+
masterCapability: string | undefined;
|
|
3870
|
+
attestationEpoch: string | undefined;
|
|
3871
|
+
ownerSessionId?: string;
|
|
3872
|
+
sessionId: string;
|
|
3873
|
+
pid: number;
|
|
3874
|
+
processIncarnation: string | undefined;
|
|
3875
|
+
}): Promise<MasterRoleAttestationV2 | undefined> {
|
|
3876
|
+
const masterRole = masterDirectAttestation(input);
|
|
3877
|
+
if (!masterRole) return undefined;
|
|
3878
|
+
const directExists = input.index.listSessionIdentities().some(row => {
|
|
3879
|
+
const processIdentity = row.hostIncarnation ?? row.processIncarnation;
|
|
3880
|
+
return (
|
|
3881
|
+
row.sessionId === input.sessionId &&
|
|
3882
|
+
row.endpointGeneration === 0 &&
|
|
3883
|
+
row.pid === input.pid &&
|
|
3884
|
+
processIdentity === input.processIncarnation &&
|
|
3885
|
+
row.masterRole !== undefined &&
|
|
3886
|
+
sameMasterAttestation(row.masterRole, masterRole)
|
|
3887
|
+
);
|
|
3888
|
+
});
|
|
3889
|
+
if (!directExists) {
|
|
3890
|
+
await input.index.append({
|
|
3891
|
+
type: "host_registered",
|
|
3892
|
+
sessionId: input.sessionId,
|
|
3893
|
+
locator: input.locator,
|
|
3894
|
+
endpointGeneration: 0,
|
|
3895
|
+
pid: input.pid,
|
|
3896
|
+
processIncarnation: input.processIncarnation,
|
|
3897
|
+
masterRole,
|
|
3898
|
+
});
|
|
3899
|
+
}
|
|
3900
|
+
return masterRole;
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3415
3903
|
function quiescingFrame(frame: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
3416
3904
|
const error = { code: "session_quiescing", message: "The session endpoint is being replaced." };
|
|
3417
3905
|
const id = typeof frame.id === "string" ? frame.id : "";
|
|
@@ -3653,7 +4141,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3653
4141
|
lifecycleOwner?: LifecycleOwner,
|
|
3654
4142
|
terminalContent?: TurnResultContent,
|
|
3655
4143
|
terminalHasActivity = false,
|
|
3656
|
-
terminalOutcome?:
|
|
4144
|
+
terminalOutcome?: InvocationOutcome,
|
|
3657
4145
|
): Promise<void> => {
|
|
3658
4146
|
const current = lifecycleOwner?.state ?? active;
|
|
3659
4147
|
if (!current) return;
|
|
@@ -3918,7 +4406,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3918
4406
|
? ({
|
|
3919
4407
|
content: terminalContent,
|
|
3920
4408
|
hasActivity: terminalHasActivity,
|
|
3921
|
-
outcome: terminalOutcome,
|
|
4409
|
+
...(terminalOutcome?.kind === "stopped" ? { outcome: terminalOutcome } : {}),
|
|
3922
4410
|
} satisfies PromptTerminalTransitionEvidence)
|
|
3923
4411
|
: undefined,
|
|
3924
4412
|
);
|
|
@@ -4122,7 +4610,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4122
4610
|
const terminalOutcome =
|
|
4123
4611
|
event.stopReason === "cancelled"
|
|
4124
4612
|
? ({ kind: "stopped", reason: "cancelled", provenance: "client_cancel" } as const)
|
|
4125
|
-
: undefined
|
|
4613
|
+
: failure === undefined
|
|
4614
|
+
? undefined
|
|
4615
|
+
: canonicalFailedOutcome(failure);
|
|
4126
4616
|
return trackLifecycle(async () => {
|
|
4127
4617
|
if (failure && !failureAlreadyPublished) {
|
|
4128
4618
|
for (const key of genericFailureKeys) owner?.failureDiagnosticKeys.delete(key);
|
|
@@ -4172,6 +4662,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4172
4662
|
await current.registerBroker();
|
|
4173
4663
|
current.runtime.emitEvent({ type: "turn_start", sessionId: ctx.sessionManager.getSessionId() });
|
|
4174
4664
|
});
|
|
4665
|
+
const consumedMasterNonces = new Map<string, number>();
|
|
4175
4666
|
api.on("turn_end", (_event, ctx) =>
|
|
4176
4667
|
lifecycleStateForContext(ctx, "agent_end")?.runtime.emitEvent({
|
|
4177
4668
|
type: "turn_end",
|
|
@@ -4310,6 +4801,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4310
4801
|
});
|
|
4311
4802
|
await Promise.race([settled, timeout]);
|
|
4312
4803
|
};
|
|
4804
|
+
let runtime: SessionSdkSessionRuntime;
|
|
4313
4805
|
const surfaceFactory = createSdkSurfaceFactory({
|
|
4314
4806
|
ctx,
|
|
4315
4807
|
id: sessionId,
|
|
@@ -4319,6 +4811,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4319
4811
|
steerStatusLookup: selector => steerReconciliation.lookupSteer(selector),
|
|
4320
4812
|
configOverrides: options.configOverrides,
|
|
4321
4813
|
settings: options.settings,
|
|
4814
|
+
getRuntimeHost: () => runtime?.host,
|
|
4322
4815
|
});
|
|
4323
4816
|
const queryHandlers = new QueryHandlers(surfaceFactory.query, sessionId, revisions, cursors);
|
|
4324
4817
|
const inputGate = { quiescing: false };
|
|
@@ -4375,7 +4868,6 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4375
4868
|
skillRecoveryControllers.delete(key);
|
|
4376
4869
|
});
|
|
4377
4870
|
};
|
|
4378
|
-
let runtime: SessionSdkSessionRuntime;
|
|
4379
4871
|
// Durable-first bounded terminalization for accepted submissions that leave
|
|
4380
4872
|
// their queue or race a run WITHOUT consumption (exact-head review: clearing
|
|
4381
4873
|
// the lease before the durable writes strands the row accepted with no
|
|
@@ -4664,6 +5156,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4664
5156
|
},
|
|
4665
5157
|
() => acceptingGateResolutions,
|
|
4666
5158
|
trackGateResolution,
|
|
5159
|
+
options.onInvocationCompletionReconciledForTests,
|
|
4667
5160
|
);
|
|
4668
5161
|
const installProviderDefinitions = (capability: string, definitions: unknown): void => {
|
|
4669
5162
|
if (capability === "permission") {
|
|
@@ -4731,6 +5224,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4731
5224
|
};
|
|
4732
5225
|
runtime = new SessionSdkSessionRuntime({
|
|
4733
5226
|
transport,
|
|
5227
|
+
masterCapabilityVerify: frame =>
|
|
5228
|
+
verifyMasterCapabilityFrame({
|
|
5229
|
+
frame,
|
|
5230
|
+
expectedCapability: options.masterCapability,
|
|
5231
|
+
expectedEpoch: options.masterAttestationEpoch,
|
|
5232
|
+
replay: consumedMasterNonces,
|
|
5233
|
+
}),
|
|
4734
5234
|
onFrameAdmission: (_connectionId, frame) =>
|
|
4735
5235
|
inputGate.quiescing ? quiescingFrame(frame as Record<string, unknown>) : undefined,
|
|
4736
5236
|
control: async (connectionId, frame) => {
|
|
@@ -4776,21 +5276,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4776
5276
|
(request.input as { mode?: unknown }).mode !== "terminal"
|
|
4777
5277
|
)
|
|
4778
5278
|
return;
|
|
4779
|
-
const
|
|
4780
|
-
const
|
|
4781
|
-
const
|
|
4782
|
-
|
|
4783
|
-
if (
|
|
4784
|
-
// A malformed retry (e.g. {mode:"terminal", scope:"turn", extra:true})
|
|
4785
|
-
// rejected by dispatch must never hash as the valid turn input and
|
|
4786
|
-
// advance the legitimate stored row (review thread P2).
|
|
4787
|
-
for (const key of Object.keys(input)) if (key !== "mode" && key !== "scope") return;
|
|
4788
|
-
const scopeInput = rawScope === "owned" ? "owned" : "turn";
|
|
5279
|
+
const rawInput = request.input as Record<string, unknown>;
|
|
5280
|
+
const operatorAuthorized = hasBrokerRuntimeAbortCapability(rawInput);
|
|
5281
|
+
const { [BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: _capability, ...publicInput } = rawInput;
|
|
5282
|
+
const abortIdentity = terminalAbortIdentity(publicInput, operatorAuthorized);
|
|
5283
|
+
if (!abortIdentity) return;
|
|
4789
5284
|
const keyHash = crypto.createHash("sha256").update(String(request.idempotencyKey)).digest("hex");
|
|
4790
|
-
const inputHash =
|
|
4791
|
-
.createHash("sha256")
|
|
4792
|
-
.update(JSON.stringify({ mode: "terminal", scope: scopeInput }))
|
|
4793
|
-
.digest("hex");
|
|
5285
|
+
const inputHash = abortIdentity.inputHash;
|
|
4794
5286
|
// Hash the ACTUAL written response payload: the durable state may only
|
|
4795
5287
|
// advance when the written response corresponds to the row's payload.
|
|
4796
5288
|
// When more than 256 concurrent requests evict an in-flight abort from
|
|
@@ -4843,24 +5335,103 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4843
5335
|
},
|
|
4844
5336
|
});
|
|
4845
5337
|
if (isAutoroutingInactive(api)) markAutoroutingInactive(runtime.host);
|
|
4846
|
-
const
|
|
5338
|
+
const disposeWorkflowGate = ctx.workflowGate?.onGateEmitted?.(gate =>
|
|
4847
5339
|
runtime.emitEvent({ kind: "workflow_gate", payload: gate }),
|
|
4848
5340
|
);
|
|
5341
|
+
// Every fold (steer, chord, auto-background timer, `bash.background`)
|
|
5342
|
+
// publishes one replayable `bash_folded` frame; `runtime.jobs.list`
|
|
5343
|
+
// `foldReason` remains the source of truth clients reconcile against after
|
|
5344
|
+
// a replay, since a replayed frame may describe a job that has since ended.
|
|
5345
|
+
const disposeFold = ctx.onJobFold?.(event => {
|
|
5346
|
+
try {
|
|
5347
|
+
runtime.emitEvent({
|
|
5348
|
+
type: "bash_folded",
|
|
5349
|
+
sessionId,
|
|
5350
|
+
jobId: event.jobId,
|
|
5351
|
+
generation: event.generation,
|
|
5352
|
+
reason: event.reason,
|
|
5353
|
+
});
|
|
5354
|
+
} catch (error) {
|
|
5355
|
+
// The fold already happened; publication failure must not affect it,
|
|
5356
|
+
// but it must leave evidence because a client relying on the event
|
|
5357
|
+
// instead of runtime.jobs.list would otherwise never learn of the fold.
|
|
5358
|
+
logger.warn("sdk: bash_folded publication failed", {
|
|
5359
|
+
jobId: event.jobId,
|
|
5360
|
+
reason: event.reason,
|
|
5361
|
+
error: error instanceof Error ? error.message : String(error),
|
|
5362
|
+
});
|
|
5363
|
+
}
|
|
5364
|
+
});
|
|
5365
|
+
const disposeGate = (): void => {
|
|
5366
|
+
disposeWorkflowGate?.();
|
|
5367
|
+
disposeFold?.();
|
|
5368
|
+
};
|
|
5369
|
+
let publishedEndpointUrl: string | undefined;
|
|
4849
5370
|
let brokerRegistered = false;
|
|
4850
5371
|
const registerBroker = async (): Promise<void> => {
|
|
4851
5372
|
if (brokerRegistered) return;
|
|
4852
5373
|
try {
|
|
5374
|
+
if (options.brokerRegistrationRequired && !options.lifecycleRequestId)
|
|
5375
|
+
throw new Error("Lifecycle broker registration requires a request identity.");
|
|
4853
5376
|
await ensureBroker({ agentDir: options.agentDir });
|
|
4854
5377
|
const index = await new SessionIndex(options.agentDir).open();
|
|
4855
|
-
const locator =
|
|
5378
|
+
const locator = await resolveSessionLocator(ctx.cwd, stateRoot);
|
|
5379
|
+
const effectiveIncarnation = processIncarnation(process.pid);
|
|
5380
|
+
const direct = await reattestMasterSessionIdentity({
|
|
5381
|
+
index,
|
|
5382
|
+
locator,
|
|
5383
|
+
masterCapability: options.masterCapability,
|
|
5384
|
+
attestationEpoch: options.masterAttestationEpoch,
|
|
5385
|
+
ownerSessionId: options.masterOwnerSessionId,
|
|
5386
|
+
sessionId,
|
|
5387
|
+
pid: process.pid,
|
|
5388
|
+
processIncarnation: effectiveIncarnation,
|
|
5389
|
+
});
|
|
4856
5390
|
await runtime.registerWithBroker({
|
|
4857
5391
|
register: async input => {
|
|
4858
|
-
|
|
4859
|
-
.
|
|
4860
|
-
|
|
5392
|
+
if (publishedEndpointUrl === undefined)
|
|
5393
|
+
throw new Error("SDK transport endpoint was not published before broker registration.");
|
|
5394
|
+
const endpointPath = path.join(input.stateRoot, "sdk", `${input.sessionId}.json`);
|
|
5395
|
+
const file = await readEndpointFile(endpointPath);
|
|
5396
|
+
if (!file) throw new Error("SDK endpoint could not be read as a stable regular file.");
|
|
5397
|
+
const endpoint = JSON.parse(file.source) as Record<string, unknown>;
|
|
5398
|
+
if (
|
|
5399
|
+
endpoint.sessionId !== input.sessionId ||
|
|
5400
|
+
endpoint.pid !== process.pid ||
|
|
5401
|
+
endpoint.url !== publishedEndpointUrl ||
|
|
5402
|
+
endpoint.token !== transport.token
|
|
5403
|
+
)
|
|
5404
|
+
throw new Error("SDK endpoint did not match the published transport authority.");
|
|
5405
|
+
const endpointMtimeMs = file.mtimeMs;
|
|
5406
|
+
const endpointFileId = `${file.dev}:${file.ino}`;
|
|
5407
|
+
const masterRole = masterAttestationForEffectiveHost({
|
|
5408
|
+
masterCapability: options.masterCapability,
|
|
5409
|
+
attestationEpoch: options.masterAttestationEpoch,
|
|
5410
|
+
ownerSessionId: options.masterOwnerSessionId,
|
|
5411
|
+
sessionId: input.sessionId,
|
|
5412
|
+
pid: process.pid,
|
|
5413
|
+
processIncarnation: effectiveIncarnation,
|
|
5414
|
+
direct,
|
|
5415
|
+
});
|
|
5416
|
+
await index.append({
|
|
5417
|
+
type: "host_registered",
|
|
5418
|
+
...input,
|
|
5419
|
+
locator,
|
|
5420
|
+
pid: process.pid,
|
|
5421
|
+
endpointMtimeMs,
|
|
5422
|
+
endpointFileId,
|
|
5423
|
+
...(options.lifecycleRequestId ? { lifecycleRequestId: options.lifecycleRequestId } : {}),
|
|
5424
|
+
...(masterRole ? { masterRole } : {}),
|
|
5425
|
+
});
|
|
4861
5426
|
},
|
|
4862
5427
|
unregister: async input => {
|
|
4863
|
-
await index.append({
|
|
5428
|
+
await index.append({
|
|
5429
|
+
type: "host_unregistered",
|
|
5430
|
+
...input,
|
|
5431
|
+
locator,
|
|
5432
|
+
pid: process.pid,
|
|
5433
|
+
...(options.lifecycleRequestId ? { lifecycleRequestId: options.lifecycleRequestId } : {}),
|
|
5434
|
+
});
|
|
4864
5435
|
},
|
|
4865
5436
|
});
|
|
4866
5437
|
brokerRegistered = true;
|
|
@@ -4899,7 +5470,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4899
5470
|
lifecycleOwnerHolder.state = runtimeOwner;
|
|
4900
5471
|
active = runtimeOwner;
|
|
4901
5472
|
try {
|
|
4902
|
-
await runtime.start();
|
|
5473
|
+
publishedEndpointUrl = (await runtime.start()).url;
|
|
4903
5474
|
await registerBroker();
|
|
4904
5475
|
} catch (error) {
|
|
4905
5476
|
active = undefined;
|