@gajae-code/coding-agent 0.15.5 → 0.16.0
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 +85 -1
- package/dist/types/async/job-manager.d.ts +83 -0
- package/dist/types/cli/args.d.ts +2 -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/root-flags.d.ts +14 -0
- package/dist/types/cli/update-cli.d.ts +15 -0
- package/dist/types/cli.d.ts +7 -0
- package/dist/types/commands/auth-gateway.d.ts +3 -0
- package/dist/types/commands/gc.d.ts +13 -0
- package/dist/types/commands/launch.d.ts +10 -0
- package/dist/types/commands/model-presets.d.ts +21 -0
- package/dist/types/config/autorouting-tier-map.d.ts +120 -0
- package/dist/types/config/file-lock.d.ts +54 -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 +17 -5
- package/dist/types/config/settings-schema.d.ts +19 -1
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
- package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
- package/dist/types/crash/record-loader.d.ts +15 -0
- package/dist/types/extensibility/extensions/types.d.ts +2 -5
- 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/launch-worktree-reservation.d.ts +21 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.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 +72 -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/index.d.ts +1 -0
- package/dist/types/main.d.ts +3 -1
- 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/components/frictionless-onboarding-selector.d.ts +10 -0
- package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
- package/dist/types/modes/components/oauth-selector.d.ts +1 -0
- 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/jobs-observer.d.ts +36 -0
- package/dist/types/modes/print-mode.d.ts +17 -0
- package/dist/types/modes/ui-language.d.ts +7 -1
- package/dist/types/sdk/broker/broker.d.ts +80 -1
- package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
- package/dist/types/sdk/broker/index.d.ts +3 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +26 -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/state-version.d.ts +4 -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/index.d.ts +6 -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 +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 +4 -3
- package/dist/types/sdk/cli/session-cli.d.ts +18 -2
- package/dist/types/sdk/client/client.d.ts +3 -2
- package/dist/types/sdk/client/discovery.d.ts +1 -0
- 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/host.d.ts +10 -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 +67 -1
- 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 +2 -0
- 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 +11 -0
- package/dist/types/sdk/session.d.ts +16 -0
- package/dist/types/sdk/startup-capability.d.ts +2 -1
- package/dist/types/session/account-inventory.d.ts +1 -0
- package/dist/types/session/agent-session.d.ts +24 -7
- package/dist/types/session/credential-store-errors.d.ts +1 -0
- package/dist/types/session/fold-coordinator.d.ts +201 -0
- 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/session/session-manager.d.ts +65 -0
- package/dist/types/session/yield-queue.d.ts +5 -4
- package/dist/types/setup/frictionless-onboarding.d.ts +1 -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/bash.d.ts +4 -0
- package/dist/types/tools/gh.d.ts +8 -0
- package/dist/types/tools/index.d.ts +10 -5
- 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 +9 -3
- package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
- package/src/async/job-manager.ts +671 -39
- package/src/cli/accounts-cli.ts +9 -5
- package/src/cli/args.ts +17 -0
- 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 +4 -0
- package/src/cli/model-presets-cli.ts +100 -0
- package/src/cli/root-flags.ts +5 -0
- package/src/cli/update-cli.ts +117 -16
- package/src/cli.ts +6 -2
- package/src/commands/auth-gateway.ts +12 -8
- package/src/commands/gc.ts +19 -0
- package/src/commands/launch.ts +86 -18
- package/src/commands/model-presets.ts +24 -0
- package/src/commands/sdk.ts +113 -21
- package/src/config/autorouting-tier-map.ts +44 -0
- package/src/config/file-lock-gc.ts +9 -4
- package/src/config/file-lock.ts +890 -99
- 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 +2795 -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 +1044 -122
- package/src/config/model-resolver.ts +5 -0
- package/src/config/settings-schema.ts +19 -1
- package/src/config/settings.ts +11 -1
- package/src/coordinator-mcp/model-preset.ts +32 -17
- package/src/coordinator-mcp/projection-scan.ts +592 -0
- package/src/coordinator-mcp/question-state.ts +9 -11
- package/src/coordinator-mcp/server.ts +111 -42
- package/src/coordinator-mcp/session-reaper.ts +9 -3
- package/src/crash/index-store.ts +2 -0
- package/src/crash/record-loader.ts +10 -0
- package/src/extensibility/extensions/loader.ts +0 -1
- package/src/extensibility/extensions/types.ts +2 -3
- package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -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-reservation.ts +242 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
- package/src/gjc-runtime/session-state-lock.ts +1321 -147
- package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
- package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
- package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
- package/src/gjc-runtime/tmux-sessions.ts +252 -13
- package/src/hooks/skill-state.ts +6 -0
- package/src/index.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +14 -13
- package/src/internal-urls/issue-pr-protocol.ts +4 -2
- package/src/lsp/config.ts +4 -3
- package/src/lsp/path-trust.ts +12 -6
- package/src/main.ts +66 -4
- 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 +616 -161
- package/src/modes/components/footer.ts +4 -2
- package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
- package/src/modes/components/jobs-overlay-model.ts +99 -16
- package/src/modes/components/model-selector.ts +158 -18
- package/src/modes/components/oauth-selector.ts +6 -1
- 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 +29 -10
- package/src/modes/components/tool-status-header.ts +151 -15
- package/src/modes/controllers/extension-ui-controller.ts +1 -1
- package/src/modes/controllers/input-controller.ts +51 -6
- package/src/modes/controllers/selector-controller.ts +191 -96
- package/src/modes/interactive-mode.ts +14 -1
- package/src/modes/jobs-observer.ts +149 -16
- package/src/modes/print-mode.ts +64 -0
- package/src/modes/runtime-init.ts +1 -1
- package/src/modes/ui-language.ts +49 -2
- package/src/modes/utils/injected-user-submission.ts +15 -1
- package/src/prompts/system/master-mode.md +28 -0
- 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 +1671 -34
- package/src/sdk/broker/endpoint-authority.ts +86 -0
- package/src/sdk/broker/ensure.ts +5 -0
- package/src/sdk/broker/index.ts +3 -0
- package/src/sdk/broker/lifecycle.ts +217 -46
- 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 +548 -0
- package/src/sdk/broker/state-version.ts +26 -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 +46 -5
- package/src/sdk/bus/index.ts +51 -4
- package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
- package/src/sdk/bus/prompt-reconciliation.ts +28 -4
- package/src/sdk/bus/slack-conversation.ts +5 -1
- 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 +11 -8
- package/src/sdk/cli/session-cli.ts +276 -6
- package/src/sdk/client/client.ts +14 -8
- package/src/sdk/client/discovery.ts +3 -1
- package/src/sdk/host/control/dispatch.ts +101 -37
- 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/host.ts +27 -2
- 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 +581 -43
- package/src/sdk/host/session-runtime.ts +655 -101
- package/src/sdk/lifecycle/broker-client.ts +28 -0
- package/src/sdk/lifecycle/service.ts +279 -20
- package/src/sdk/lifecycle-session.ts +10 -2
- 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 +53 -2
- package/src/sdk/protocol/operation-registry.ts +17 -4
- package/src/sdk/router/session-router.ts +294 -154
- package/src/sdk/session.ts +440 -167
- package/src/sdk/startup-capability.ts +1 -0
- package/src/session/account-inventory.ts +27 -11
- package/src/session/agent-session.ts +1292 -180
- package/src/session/blob-store.ts +15 -5
- package/src/session/credential-store-errors.ts +2 -0
- package/src/session/fold-coordinator.ts +433 -0
- package/src/session/internal/managed-session-storage.ts +153 -78
- 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 +342 -28
- package/src/session/yield-queue.ts +24 -10
- package/src/setup/frictionless-onboarding.ts +2 -0
- 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 +1268 -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 +1028 -109
- package/src/tools/gh.ts +104 -48
- package/src/tools/github-cache.ts +2 -2
- package/src/tools/index.ts +11 -5
- 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,
|
|
@@ -23,10 +37,16 @@ import {
|
|
|
23
37
|
settleOwnedWork,
|
|
24
38
|
} from "../../session/terminal-abort";
|
|
25
39
|
import { parseThinkingLevel } from "../../thinking";
|
|
40
|
+
import { readEndpointFile } from "../broker/endpoint-authority";
|
|
26
41
|
import { ensureBroker } from "../broker/ensure";
|
|
27
|
-
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";
|
|
28
49
|
import {
|
|
29
|
-
collectAuthenticatedProfileProviders,
|
|
30
50
|
parseSyntheticModelId,
|
|
31
51
|
resolveSyntheticModelSelection,
|
|
32
52
|
SYNTHETIC_PROVIDER_ID,
|
|
@@ -34,8 +54,9 @@ import {
|
|
|
34
54
|
syntheticNamespaceCollision,
|
|
35
55
|
} from "../model-profile-model";
|
|
36
56
|
import { projectQ10Models } from "../models.js";
|
|
37
|
-
import { PromptDeadlineManager } from "../prompt-deadline-manager";
|
|
57
|
+
import { PromptDeadlineManager, type PromptTerminalTransitionEvidence } from "../prompt-deadline-manager";
|
|
38
58
|
import { formatPromptFailureForLocalLog, sanitizePromptFailure } from "../prompt-failure";
|
|
59
|
+
import { validateRequiredPromptText } from "../protocol/adapter-validation";
|
|
39
60
|
import { OPERATIONS } from "../protocol/operation-registry";
|
|
40
61
|
import {
|
|
41
62
|
createKindAwareReconciliation,
|
|
@@ -43,11 +64,17 @@ import {
|
|
|
43
64
|
type KindAwareReconciliation,
|
|
44
65
|
resolveReconciliationSessionFile,
|
|
45
66
|
} from "../reconciliation-extensions";
|
|
46
|
-
import {
|
|
47
|
-
import {
|
|
67
|
+
import { sanitizeTurnResultContent, type TurnResultContent } from "../turn-result";
|
|
68
|
+
import { type ControlSurface, controlRequestFromFrame, dispatchControl, terminalAbortIdentity } from "./control";
|
|
69
|
+
import {
|
|
70
|
+
BROKER_RUNTIME_ABORT_CAPABILITY_FIELD,
|
|
71
|
+
BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD,
|
|
72
|
+
hasBrokerRuntimeAbortCapability,
|
|
73
|
+
} from "./control/runtime-gate";
|
|
48
74
|
import { SessionSdkHost, type SessionSdkHostOptions } from "./host";
|
|
49
75
|
import { clearAutoroutingInactive, isAutoroutingInactive, markAutoroutingInactive } from "./internal-autorouting-state";
|
|
50
76
|
import { CursorRegistry, QueryHandlers, RevisionStore, type SessionSurface } from "./query";
|
|
77
|
+
import { createSdkRunCapability } from "./sdk-run-capability";
|
|
51
78
|
import {
|
|
52
79
|
createSdkCapabilities,
|
|
53
80
|
createSdkSurfacePolicyForContext,
|
|
@@ -87,6 +114,65 @@ const SDK_ONLY_TERMINAL_PUBLICATION_WAIT_MS = 1_000;
|
|
|
87
114
|
* after this bound are abandoned — their durable broker state is the recovery
|
|
88
115
|
* authority, and outcomes are inherently uncertain. */
|
|
89
116
|
const GATE_RESOLUTION_QUIESCENCE_MS = 5_000;
|
|
117
|
+
/** Master verification fields are ephemeral protocol values, not unbounded input. */
|
|
118
|
+
const MASTER_AUTH_VALUE_MAX_LENGTH = 512;
|
|
119
|
+
const MASTER_AUTH_VALUE_PATTERN = /^[A-Za-z0-9_-]+$/u;
|
|
120
|
+
/** Keep replay protection finite even when an authenticated client floods nonces. */
|
|
121
|
+
const MASTER_NONCE_REPLAY_MAX_ENTRIES = 1_024;
|
|
122
|
+
/** Verification exchanges complete in seconds; retain replay evidence for a bounded window. */
|
|
123
|
+
const MASTER_NONCE_REPLAY_TTL_MS = 5 * 60_000;
|
|
124
|
+
|
|
125
|
+
export type MasterCapabilityReplayState = Map<string, number>;
|
|
126
|
+
|
|
127
|
+
export function verifyMasterCapabilityFrame(input: {
|
|
128
|
+
frame: { nonce: unknown; attestationEpoch: unknown; capability: unknown };
|
|
129
|
+
expectedCapability: string | undefined;
|
|
130
|
+
expectedEpoch: string | undefined;
|
|
131
|
+
replay: MasterCapabilityReplayState;
|
|
132
|
+
now?: number;
|
|
133
|
+
}): { ok: boolean; nonce: string; attestationEpoch: string } {
|
|
134
|
+
const now = input.now ?? Date.now();
|
|
135
|
+
for (const [consumedNonce, expiresAt] of input.replay) {
|
|
136
|
+
if (expiresAt <= now) input.replay.delete(consumedNonce);
|
|
137
|
+
}
|
|
138
|
+
const bounded = (value: unknown): value is string =>
|
|
139
|
+
typeof value === "string" &&
|
|
140
|
+
value.length > 0 &&
|
|
141
|
+
value.length <= MASTER_AUTH_VALUE_MAX_LENGTH &&
|
|
142
|
+
MASTER_AUTH_VALUE_PATTERN.test(value);
|
|
143
|
+
const nonce = bounded(input.frame.nonce) ? input.frame.nonce : "";
|
|
144
|
+
const epoch = bounded(input.frame.attestationEpoch) ? input.frame.attestationEpoch : "";
|
|
145
|
+
const capability = bounded(input.frame.capability) ? input.frame.capability : "";
|
|
146
|
+
const expectedCapability = bounded(input.expectedCapability) ? input.expectedCapability : "";
|
|
147
|
+
const expectedEpoch = bounded(input.expectedEpoch) ? input.expectedEpoch : "";
|
|
148
|
+
const authenticatedFields =
|
|
149
|
+
bounded(input.frame.nonce) &&
|
|
150
|
+
bounded(input.frame.attestationEpoch) &&
|
|
151
|
+
bounded(input.frame.capability) &&
|
|
152
|
+
bounded(input.expectedCapability) &&
|
|
153
|
+
bounded(input.expectedEpoch) &&
|
|
154
|
+
epoch === expectedEpoch;
|
|
155
|
+
const expectedBytes = Buffer.from(expectedCapability);
|
|
156
|
+
const capabilityBytes = Buffer.from(capability);
|
|
157
|
+
const capabilityMatches =
|
|
158
|
+
authenticatedFields &&
|
|
159
|
+
expectedBytes.length === capabilityBytes.length &&
|
|
160
|
+
crypto.timingSafeEqual(expectedBytes, capabilityBytes);
|
|
161
|
+
const reusable = capabilityMatches && !input.replay.has(nonce);
|
|
162
|
+
if (reusable) {
|
|
163
|
+
if (input.replay.size >= MASTER_NONCE_REPLAY_MAX_ENTRIES) {
|
|
164
|
+
const oldest = input.replay.keys().next().value;
|
|
165
|
+
if (typeof oldest === "string") input.replay.delete(oldest);
|
|
166
|
+
}
|
|
167
|
+
input.replay.set(nonce, now + MASTER_NONCE_REPLAY_TTL_MS);
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
ok: reusable,
|
|
171
|
+
nonce,
|
|
172
|
+
attestationEpoch: authenticatedFields ? epoch : "",
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
90
176
|
/** Maximum time a replaced runtime may retain a lifecycle persistence task. */
|
|
91
177
|
const LIFECYCLE_QUIESCENCE_MS = 1_000;
|
|
92
178
|
|
|
@@ -126,6 +212,8 @@ export interface SessionSdkRuntimeOptions
|
|
|
126
212
|
settings?: Settings;
|
|
127
213
|
/** Mutable shadow of patched config values merged into query readback. */
|
|
128
214
|
configOverrides?: Map<string, unknown>;
|
|
215
|
+
/** Stable master lineage identity retained across session switches/branches. */
|
|
216
|
+
masterOwnerSessionId?: string;
|
|
129
217
|
}
|
|
130
218
|
|
|
131
219
|
export interface SdkOnlyInvocationRecord extends InvocationCorrelation {
|
|
@@ -384,6 +472,8 @@ export interface CreateSdkSessionRuntimeOptions {
|
|
|
384
472
|
agentDir: string;
|
|
385
473
|
/** Lifecycle-owned sessions require broker publication before they become usable. */
|
|
386
474
|
brokerRegistrationRequired?: boolean;
|
|
475
|
+
/** Trusted broker-issued lifecycle marker bound to lifecycle host index events. */
|
|
476
|
+
lifecycleRequestId?: string;
|
|
387
477
|
createTransport(input: {
|
|
388
478
|
sessionId: string;
|
|
389
479
|
stateRoot: string;
|
|
@@ -395,6 +485,12 @@ export interface CreateSdkSessionRuntimeOptions {
|
|
|
395
485
|
onSdkRequest?: SessionSdkHostOptions["onRequest"];
|
|
396
486
|
/** Mutable shadow of patched config values merged into query readback. */
|
|
397
487
|
configOverrides?: Map<string, unknown>;
|
|
488
|
+
/** In-memory master capability for private broker verification only. */
|
|
489
|
+
masterCapability?: string;
|
|
490
|
+
/** Opaque direct-role epoch this effective host may adopt. */
|
|
491
|
+
masterAttestationEpoch?: string;
|
|
492
|
+
/** Stable master lineage identity retained across session switches/branches. */
|
|
493
|
+
masterOwnerSessionId?: string;
|
|
398
494
|
/** Private session-owned terminal-abort capabilities; never exposed on ExtensionContext. */
|
|
399
495
|
terminalAbortSeams?: SdkOnlyTerminalAbortSeams;
|
|
400
496
|
/** Callback when a frame is admitted to the runtime (test harness). */
|
|
@@ -420,6 +516,7 @@ export interface InvocationCorrelation {
|
|
|
420
516
|
|
|
421
517
|
export type InvocationKind = "prompt" | "skill" | "steer";
|
|
422
518
|
type InvocationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed" | "uncertain";
|
|
519
|
+
const EMPTY_PROMPT_FAILURE = { code: "prompt_failed", message: "Prompt submission failed." } as const;
|
|
423
520
|
interface InvocationRecord extends InvocationCorrelation {
|
|
424
521
|
kind: InvocationKind;
|
|
425
522
|
revision: number;
|
|
@@ -440,7 +537,14 @@ export interface InvocationReconciliation {
|
|
|
440
537
|
noteTransition(
|
|
441
538
|
kind: InvocationKind,
|
|
442
539
|
correlation: InvocationCorrelation | undefined,
|
|
443
|
-
frame:
|
|
540
|
+
frame:
|
|
541
|
+
| {
|
|
542
|
+
type: "agent_start" | "agent_end";
|
|
543
|
+
content?: TurnResultContent;
|
|
544
|
+
hasActivity?: boolean;
|
|
545
|
+
outcome?: { kind: "stopped"; reason: "cancelled"; provenance: "client_cancel" };
|
|
546
|
+
}
|
|
547
|
+
| { type: "agent_failed"; error: unknown; content?: TurnResultContent; hasActivity?: boolean },
|
|
444
548
|
): Promise<void>;
|
|
445
549
|
lookup(kind: InvocationKind, selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown;
|
|
446
550
|
lookupResult(kind: InvocationKind, selector: { commandId?: string; turnId?: string; clientRef?: string }): unknown;
|
|
@@ -464,7 +568,13 @@ export interface InvocationReconciliation {
|
|
|
464
568
|
correlation: InvocationCorrelation,
|
|
465
569
|
outcome?: { kind: string; code: string; message: string; provenance?: string },
|
|
466
570
|
arg4?: (() => boolean) | { code: string; message: string },
|
|
571
|
+
// Terminal evidence accepted by finalizeOutcome (exact-head review P1/P2):
|
|
572
|
+
// when an explicit outcome is absent, a content-bearing, activity-bearing,
|
|
573
|
+
// or explicitly stopped finalization is still a successful terminal; only a
|
|
574
|
+
// genuinely empty, no-activity completion fails closed with the sanitized
|
|
575
|
+
// empty-prompt failure. Mirrors the noteTransition evidence predicate.
|
|
467
576
|
arg5?: unknown,
|
|
577
|
+
arg6?: { content?: TurnResultContent; hasActivity?: boolean; outcomeKind?: string },
|
|
468
578
|
): Promise<void>;
|
|
469
579
|
markUncertain(
|
|
470
580
|
kind: InvocationKind,
|
|
@@ -699,7 +809,14 @@ export function createInvocationReconciliation(
|
|
|
699
809
|
// so a real lifecycle event is never swallowed.
|
|
700
810
|
}
|
|
701
811
|
const current = records.get(recordKey);
|
|
702
|
-
if (
|
|
812
|
+
if (
|
|
813
|
+
frame.type === "agent_end" &&
|
|
814
|
+
current === pending.finalizedRecord &&
|
|
815
|
+
(kind !== "prompt" ||
|
|
816
|
+
frame.outcome?.kind === "stopped" ||
|
|
817
|
+
frame.hasActivity === true ||
|
|
818
|
+
frame.content?.text.trim())
|
|
819
|
+
) {
|
|
703
820
|
// A real lifecycle end that arrived during deadline finalization is
|
|
704
821
|
// stronger evidence than the synthetic deadline outcome. Upgrade the
|
|
705
822
|
// durable terminal instead of treating the event as a duplicate.
|
|
@@ -734,7 +851,14 @@ export function createInvocationReconciliation(
|
|
|
734
851
|
}
|
|
735
852
|
}
|
|
736
853
|
if (record.terminalAt !== undefined) {
|
|
737
|
-
if (
|
|
854
|
+
if (
|
|
855
|
+
frame.type === "agent_end" &&
|
|
856
|
+
record.error?.code === "prompt_deadline_exceeded" &&
|
|
857
|
+
(kind !== "prompt" ||
|
|
858
|
+
frame.outcome?.kind === "stopped" ||
|
|
859
|
+
frame.hasActivity === true ||
|
|
860
|
+
frame.content?.text.trim())
|
|
861
|
+
) {
|
|
738
862
|
const upgraded = { ...record, revision: ++mutationRevision, status: "terminal_ok" as const };
|
|
739
863
|
// The synthetic deadline error is superseded evidence; terminal_ok must not
|
|
740
864
|
// surface a deadline failure for a prompt that actually completed.
|
|
@@ -795,7 +919,17 @@ export function createInvocationReconciliation(
|
|
|
795
919
|
if (next.error === undefined || (next.error.code === "agent_failed" && failure.code !== "agent_failed"))
|
|
796
920
|
next.error = failure;
|
|
797
921
|
} else {
|
|
798
|
-
|
|
922
|
+
if (
|
|
923
|
+
kind === "prompt" &&
|
|
924
|
+
next.error === undefined &&
|
|
925
|
+
frame.type === "agent_end" &&
|
|
926
|
+
frame.outcome?.kind !== "stopped" &&
|
|
927
|
+
!frame.content?.text.trim() &&
|
|
928
|
+
!frame.hasActivity
|
|
929
|
+
) {
|
|
930
|
+
next.status = "failed";
|
|
931
|
+
next.error = EMPTY_PROMPT_FAILURE;
|
|
932
|
+
} else next.status = next.error === undefined ? "terminal_ok" : "failed";
|
|
799
933
|
next.terminalAt = Date.now();
|
|
800
934
|
}
|
|
801
935
|
records.set(recordKey, next);
|
|
@@ -868,6 +1002,7 @@ export function createInvocationReconciliation(
|
|
|
868
1002
|
outcome,
|
|
869
1003
|
arg4?: (() => boolean) | { code: string; message: string },
|
|
870
1004
|
arg5?: unknown,
|
|
1005
|
+
arg6?: { content?: TurnResultContent; hasActivity?: boolean; outcomeKind?: string },
|
|
871
1006
|
) {
|
|
872
1007
|
// Normalize every supported positional form (exact-head review P1):
|
|
873
1008
|
// (…, isCurrent) | (…, recordError) | (…, undefined, recordError) |
|
|
@@ -880,6 +1015,14 @@ export function createInvocationReconciliation(
|
|
|
880
1015
|
? (arg5 as { code: string; message: string })
|
|
881
1016
|
: undefined;
|
|
882
1017
|
const recordError = recordErrorCandidate;
|
|
1018
|
+
// Terminal evidence may also arrive positionally through arg5 (legacy
|
|
1019
|
+
// callers) when it does not carry a recordError shape; normalize it so
|
|
1020
|
+
// the empty predicate below sees every evidence channel consistently.
|
|
1021
|
+
const legacyEvidenceCandidate =
|
|
1022
|
+
typeof arg5 === "object" && arg5 !== null && !("code" in arg5)
|
|
1023
|
+
? (arg5 as { content?: TurnResultContent; hasActivity?: boolean; outcomeKind?: string })
|
|
1024
|
+
: undefined;
|
|
1025
|
+
const evidence = arg6 ?? legacyEvidenceCandidate;
|
|
883
1026
|
const recordKey = key(kind, correlation);
|
|
884
1027
|
const record = records.get(recordKey);
|
|
885
1028
|
if (!record || record.terminalAt !== undefined || record.kind !== kind) return;
|
|
@@ -896,6 +1039,19 @@ export function createInvocationReconciliation(
|
|
|
896
1039
|
recordError !== undefined
|
|
897
1040
|
? { code: recordError.code, message: recordError.message }
|
|
898
1041
|
: { code: finalOutcome.code, message: finalOutcome.message };
|
|
1042
|
+
} else if (kind === "prompt" && finalOutcome === undefined) {
|
|
1043
|
+
// Evidence-based empty predicate (exact-head review P1/P2): the same
|
|
1044
|
+
// semantics as the noteTransition agent_end classifier. A
|
|
1045
|
+
// content-bearing, activity-bearing, or explicitly stopped
|
|
1046
|
+
// finalization without an explicit outcome is a successful terminal;
|
|
1047
|
+
// only a genuinely empty, no-activity completion fails closed.
|
|
1048
|
+
const terminalText = evidence?.content?.text.trim() ?? "";
|
|
1049
|
+
if (terminalText === "" && !evidence?.hasActivity && evidence?.outcomeKind !== "stopped") {
|
|
1050
|
+
finalizedRecord.status = "failed";
|
|
1051
|
+
finalizedRecord.error = EMPTY_PROMPT_FAILURE;
|
|
1052
|
+
} else {
|
|
1053
|
+
finalizedRecord.status = "terminal_ok";
|
|
1054
|
+
}
|
|
899
1055
|
} else {
|
|
900
1056
|
finalizedRecord.status = "terminal_ok";
|
|
901
1057
|
}
|
|
@@ -1041,37 +1197,63 @@ function createQuerySurface(
|
|
|
1041
1197
|
kind: ctx.sessionMetadata?.kind ?? "main",
|
|
1042
1198
|
});
|
|
1043
1199
|
const lastAssistant = () => {
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
if (
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
.
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
)
|
|
1054
|
-
.map(block => block.text)
|
|
1055
|
-
.join("");
|
|
1200
|
+
const transcript =
|
|
1201
|
+
typeof (ctx as Partial<ExtensionContext>).getTranscript === "function" ? ctx.getTranscript() : [];
|
|
1202
|
+
for (const entry of transcript.toReversed()) {
|
|
1203
|
+
if (entry.role !== "assistant") continue;
|
|
1204
|
+
const text =
|
|
1205
|
+
typeof entry.body === "string"
|
|
1206
|
+
? entry.body
|
|
1207
|
+
: entry.content?.flatMap(block => (block.type === "text" ? [block.text] : [])).join("\n");
|
|
1208
|
+
if (text !== undefined && text.trim().length > 0) return text;
|
|
1056
1209
|
}
|
|
1057
1210
|
return undefined;
|
|
1058
1211
|
};
|
|
1059
1212
|
const getProfileCredentialSessionId = () => ctx.credentialSessionId ?? id;
|
|
1213
|
+
const profileSettings = (options.settings ?? ctx.settings) as Pick<Settings, "get"> | undefined;
|
|
1214
|
+
const getProfileAvailableModels = (): Model<Api>[] => {
|
|
1215
|
+
const getAvailableForProfileActivation = ctx.modelRegistry.getAvailableForProfileActivation;
|
|
1216
|
+
return typeof getAvailableForProfileActivation === "function"
|
|
1217
|
+
? getAvailableForProfileActivation.call(ctx.modelRegistry)
|
|
1218
|
+
: ctx.modelRegistry.getAvailable();
|
|
1219
|
+
};
|
|
1060
1220
|
const resolveProfileAvailability = async (
|
|
1061
1221
|
profile: ModelProfileDefinition,
|
|
1062
1222
|
authenticatedProviders: ReadonlySet<string>,
|
|
1063
1223
|
): Promise<{ available: boolean; defaultModel?: Model<Api> }> => {
|
|
1224
|
+
if (profile.source !== "user" && inspectProxyProviderId(profileSettings).status === "invalid")
|
|
1225
|
+
return { available: false };
|
|
1226
|
+
const proxyProvider = profile.source === "user" ? undefined : tryResolveProxyProviderId(profileSettings);
|
|
1227
|
+
const proxyAuthenticated = proxyProvider !== undefined && authenticatedProviders.has(proxyProvider);
|
|
1228
|
+
const profileAuthenticated = new Set(authenticatedProviders);
|
|
1229
|
+
if (proxyAuthenticated) {
|
|
1230
|
+
for (const provider of getProxyRoutableProviders(profile)) profileAuthenticated.add(provider);
|
|
1231
|
+
}
|
|
1064
1232
|
const rewriteSelectorProvider = (selector: string): string => {
|
|
1065
1233
|
const slash = selector.indexOf("/");
|
|
1066
1234
|
if (slash < 0) return selector;
|
|
1067
1235
|
const provider = selector.slice(0, slash);
|
|
1068
|
-
if (
|
|
1236
|
+
if (profileAuthenticated.has(provider)) return selector;
|
|
1069
1237
|
const group = (profile.alternativeProviderGroups ?? []).find(candidates => candidates.includes(provider));
|
|
1070
1238
|
if (!group) return selector;
|
|
1071
|
-
const replacement = group.find(candidate =>
|
|
1239
|
+
const replacement = group.find(candidate => profileAuthenticated.has(candidate));
|
|
1072
1240
|
return replacement ? replacement + selector.slice(slash) : selector;
|
|
1073
1241
|
};
|
|
1074
1242
|
try {
|
|
1243
|
+
const proxyMode = profile.source === "user" ? "fallback" : resolveProxyMode(profileSettings);
|
|
1244
|
+
if (profile.source !== "user") {
|
|
1245
|
+
const configuredProviders = ctx.modelRegistry.getConfiguredProviderIds?.() ?? [];
|
|
1246
|
+
if (proxyProvider !== undefined && !configuredProviders.includes(proxyProvider))
|
|
1247
|
+
return { available: false };
|
|
1248
|
+
}
|
|
1249
|
+
if (profile.source !== "user" && proxyMode === "always") {
|
|
1250
|
+
if (
|
|
1251
|
+
proxyProvider === undefined ||
|
|
1252
|
+
!proxyAuthenticated ||
|
|
1253
|
+
!(ctx.modelRegistry.getConfiguredProviderIds?.() ?? []).includes(proxyProvider)
|
|
1254
|
+
)
|
|
1255
|
+
return { available: false };
|
|
1256
|
+
}
|
|
1075
1257
|
const bindings = resolveProfileBindings(profile);
|
|
1076
1258
|
const assignments: Array<{ value: ModelSelectorValue; isDefault: boolean }> = [];
|
|
1077
1259
|
if (bindings.defaultSelector !== undefined) {
|
|
@@ -1079,23 +1261,44 @@ function createQuerySurface(
|
|
|
1079
1261
|
}
|
|
1080
1262
|
for (const value of Object.values(bindings.modelRoles)) assignments.push({ value, isDefault: false });
|
|
1081
1263
|
for (const value of Object.values(bindings.agentModelOverrides)) assignments.push({ value, isDefault: false });
|
|
1264
|
+
const availableModels = getProfileAvailableModels();
|
|
1265
|
+
const resolutionRegistry = {
|
|
1266
|
+
...ctx.modelRegistry,
|
|
1267
|
+
getAvailable: () => availableModels,
|
|
1268
|
+
getApiKey: (model: Model<Api>, sessionId?: string) =>
|
|
1269
|
+
ctx.modelRegistry.getApiKeyForProvider(model.provider, sessionId, model.baseUrl),
|
|
1270
|
+
resolveCanonicalModel: ctx.modelRegistry.resolveCanonicalModel?.bind(ctx.modelRegistry),
|
|
1271
|
+
getCanonicalVariants: ctx.modelRegistry.getCanonicalVariants?.bind(ctx.modelRegistry),
|
|
1272
|
+
getCanonicalId: ctx.modelRegistry.getCanonicalId?.bind(ctx.modelRegistry),
|
|
1273
|
+
resolveModelByLookupAlias: ctx.modelRegistry.resolveModelByLookupAlias?.bind(ctx.modelRegistry),
|
|
1274
|
+
lookupAliasExists: ctx.modelRegistry.lookupAliasExists?.bind(ctx.modelRegistry),
|
|
1275
|
+
clearCanonicalVariant: ctx.modelRegistry.clearCanonicalVariant?.bind(ctx.modelRegistry),
|
|
1276
|
+
};
|
|
1082
1277
|
let defaultModel: Model<Api> | undefined;
|
|
1083
1278
|
for (const assignment of assignments) {
|
|
1084
|
-
|
|
1279
|
+
let selectors = normalizeModelSelectorValue(assignment.value).map(rewriteSelectorProvider);
|
|
1280
|
+
if (proxyProvider !== undefined && proxyAuthenticated && profile.source !== "user") {
|
|
1281
|
+
selectors = selectors.map(selector =>
|
|
1282
|
+
rewriteSelectorForProxy(
|
|
1283
|
+
selector,
|
|
1284
|
+
proxyProvider,
|
|
1285
|
+
proxyMode,
|
|
1286
|
+
availableModels,
|
|
1287
|
+
new Set(authenticatedProviders),
|
|
1288
|
+
getProxyRoutableProviders(profile),
|
|
1289
|
+
),
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1085
1292
|
const hasBareSelector = selectors.some(selector => {
|
|
1086
1293
|
const suffix = splitSelectorThinkingSuffix(selector);
|
|
1087
1294
|
const identity = suffix.thinkingLevel ? suffix.selector : selector;
|
|
1088
1295
|
return !identity.includes("/");
|
|
1089
1296
|
});
|
|
1090
|
-
if (!assignment.isDefault && !hasBareSelector)
|
|
1297
|
+
if (!assignment.isDefault && !requiresQualifiedModelProfileRoleResolution(profile) && !hasBareSelector)
|
|
1298
|
+
continue;
|
|
1091
1299
|
const resolution = await resolveModelChainWithAuth(
|
|
1092
1300
|
selectors,
|
|
1093
|
-
|
|
1094
|
-
...ctx.modelRegistry,
|
|
1095
|
-
getAvailable: () => ctx.modelRegistry.getAvailable(),
|
|
1096
|
-
getApiKey: (model: Model<Api>, sessionId?: string) =>
|
|
1097
|
-
ctx.modelRegistry.getApiKeyForProvider(model.provider, sessionId, model.baseUrl),
|
|
1098
|
-
},
|
|
1301
|
+
resolutionRegistry,
|
|
1099
1302
|
options.settings,
|
|
1100
1303
|
getProfileCredentialSessionId(),
|
|
1101
1304
|
{
|
|
@@ -1113,6 +1316,45 @@ function createQuerySurface(
|
|
|
1113
1316
|
return { available: false };
|
|
1114
1317
|
}
|
|
1115
1318
|
};
|
|
1319
|
+
const collectProfileAuthentication = async (
|
|
1320
|
+
profiles: ReadonlyMap<string, ModelProfileDefinition>,
|
|
1321
|
+
): Promise<Set<string>> => {
|
|
1322
|
+
const providers = new Set<string>();
|
|
1323
|
+
for (const profile of profiles.values()) {
|
|
1324
|
+
for (const provider of profile.requiredProviders) providers.add(provider);
|
|
1325
|
+
for (const group of profile.alternativeProviderGroups ?? []) {
|
|
1326
|
+
for (const provider of group) providers.add(provider);
|
|
1327
|
+
}
|
|
1328
|
+
for (const provider of deriveModelProfileMappedProviders(profile)) providers.add(provider);
|
|
1329
|
+
}
|
|
1330
|
+
const authenticated = new Set<string>();
|
|
1331
|
+
await Promise.all(
|
|
1332
|
+
[...providers].map(async provider => {
|
|
1333
|
+
try {
|
|
1334
|
+
const apiKey = await ctx.modelRegistry.getApiKeyForProvider(provider, getProfileCredentialSessionId());
|
|
1335
|
+
if (apiKey === kNoAuth || isAuthenticated(apiKey)) authenticated.add(provider);
|
|
1336
|
+
} catch {
|
|
1337
|
+
// A provider whose credential state cannot be read is not currently configurable.
|
|
1338
|
+
}
|
|
1339
|
+
}),
|
|
1340
|
+
);
|
|
1341
|
+
const proxyProviders = new Set<string>();
|
|
1342
|
+
for (const profile of profiles.values()) {
|
|
1343
|
+
if (profile.source === "user") continue;
|
|
1344
|
+
const proxyProvider = tryResolveProxyProviderId(profileSettings);
|
|
1345
|
+
if (proxyProvider !== undefined) proxyProviders.add(proxyProvider);
|
|
1346
|
+
}
|
|
1347
|
+
for (const proxyProvider of proxyProviders) {
|
|
1348
|
+
try {
|
|
1349
|
+
const apiKey = await ctx.modelRegistry.getApiKeyForProvider(proxyProvider, getProfileCredentialSessionId());
|
|
1350
|
+
if (apiKey === kNoAuth || isAuthenticated(apiKey)) authenticated.add(proxyProvider);
|
|
1351
|
+
} catch {
|
|
1352
|
+
// Passive availability must degrade to unavailable when proxy credential
|
|
1353
|
+
// refresh/storage fails; explicit activation retains its diagnostics.
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
return authenticated;
|
|
1357
|
+
};
|
|
1116
1358
|
const getDiff = async () => {
|
|
1117
1359
|
try {
|
|
1118
1360
|
const { stdout } = await execFileAsync("git", ["diff", "--no-ext-diff"], {
|
|
@@ -1245,9 +1487,7 @@ function createQuerySurface(
|
|
|
1245
1487
|
if (collision) return degraded();
|
|
1246
1488
|
let authenticatedProviders: ReadonlySet<string>;
|
|
1247
1489
|
try {
|
|
1248
|
-
authenticatedProviders = await
|
|
1249
|
-
ctx.modelRegistry.getApiKeyForProvider(provider, getProfileCredentialSessionId()),
|
|
1250
|
-
);
|
|
1490
|
+
authenticatedProviders = await collectProfileAuthentication(profiles);
|
|
1251
1491
|
} catch {
|
|
1252
1492
|
// Availability join failed: degrade only the synthetic facade,
|
|
1253
1493
|
// retain concrete rows and the active marker readback.
|
|
@@ -1265,7 +1505,12 @@ function createQuerySurface(
|
|
|
1265
1505
|
);
|
|
1266
1506
|
const availableProfileIds = new Set<string>();
|
|
1267
1507
|
for (const [name, profile] of profiles) {
|
|
1268
|
-
if (
|
|
1508
|
+
if (profile.source !== "user" && inspectProxyProviderId(profileSettings).status === "invalid") continue;
|
|
1509
|
+
const profileAuthenticated = new Set(authenticatedProviders);
|
|
1510
|
+
const proxyProvider = profile.source === "user" ? undefined : tryResolveProxyProviderId(profileSettings);
|
|
1511
|
+
if (proxyProvider !== undefined && profileAuthenticated.has(proxyProvider))
|
|
1512
|
+
for (const provider of getProxyRoutableProviders(profile)) profileAuthenticated.add(provider);
|
|
1513
|
+
if (!isModelProfileProviderAvailable(profile, profileAuthenticated)) continue;
|
|
1269
1514
|
if (!fullyResolvedProfiles.has(name)) continue;
|
|
1270
1515
|
// A profile with a default mapping is selectable only when its
|
|
1271
1516
|
// default chain actually resolves to an authenticated model:
|
|
@@ -1336,18 +1581,19 @@ function createQuerySurface(
|
|
|
1336
1581
|
(options.steerStatusLookup ?? (value => reconciliation.lookup("steer", value)))(selector),
|
|
1337
1582
|
getModelProfiles: async () => {
|
|
1338
1583
|
const profiles = ctx.modelRegistry.getModelProfiles();
|
|
1339
|
-
const authenticatedProviders = await
|
|
1340
|
-
ctx.modelRegistry.getApiKeyForProvider(provider, getProfileCredentialSessionId()),
|
|
1341
|
-
);
|
|
1584
|
+
const authenticatedProviders = await collectProfileAuthentication(profiles);
|
|
1342
1585
|
return (await Promise.all(
|
|
1343
|
-
projectModelProfileCatalog(profiles, ctx.modelRegistry.getError()).map(async item =>
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1586
|
+
projectModelProfileCatalog(profiles, ctx.modelRegistry.getError()).map(async item => {
|
|
1587
|
+
const profile = profiles.get(item.id)!;
|
|
1588
|
+
const profileAuthenticated = new Set(authenticatedProviders);
|
|
1589
|
+
const proxyProvider = profile.source === "user" ? undefined : tryResolveProxyProviderId(profileSettings);
|
|
1590
|
+
if (proxyProvider !== undefined && profileAuthenticated.has(proxyProvider))
|
|
1591
|
+
for (const provider of getProxyRoutableProviders(profile)) profileAuthenticated.add(provider);
|
|
1592
|
+
const available =
|
|
1593
|
+
isModelProfileProviderAvailable(profile, profileAuthenticated) &&
|
|
1594
|
+
(await resolveProfileAvailability(profile, authenticatedProviders)).available;
|
|
1595
|
+
return { ...item, available };
|
|
1596
|
+
}),
|
|
1351
1597
|
)) as unknown[];
|
|
1352
1598
|
},
|
|
1353
1599
|
installedQueries: policy.installedQueries,
|
|
@@ -1600,6 +1846,55 @@ function providerFailureFromAgentEnd(event: unknown): { code: string; message: s
|
|
|
1600
1846
|
}
|
|
1601
1847
|
}
|
|
1602
1848
|
|
|
1849
|
+
interface PromptTerminalEvidence {
|
|
1850
|
+
content?: TurnResultContent;
|
|
1851
|
+
hasActivity: boolean;
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
function promptTerminalEvidenceFromAgentEnd(event: unknown): PromptTerminalEvidence {
|
|
1855
|
+
try {
|
|
1856
|
+
if (!event || typeof event !== "object") return { hasActivity: false };
|
|
1857
|
+
const messages = (event as { messages?: unknown }).messages;
|
|
1858
|
+
if (!Array.isArray(messages)) return { hasActivity: false };
|
|
1859
|
+
const assistant = [...messages]
|
|
1860
|
+
.reverse()
|
|
1861
|
+
.find(
|
|
1862
|
+
message => message && typeof message === "object" && (message as { role?: unknown }).role === "assistant",
|
|
1863
|
+
);
|
|
1864
|
+
if (!assistant || typeof assistant !== "object") return { hasActivity: false };
|
|
1865
|
+
const content = (assistant as { content?: unknown }).content;
|
|
1866
|
+
if (typeof content === "string") {
|
|
1867
|
+
const bounded = sanitizeTurnResultContent(content);
|
|
1868
|
+
return { content: bounded, hasActivity: content.trim().length > 0 };
|
|
1869
|
+
}
|
|
1870
|
+
if (Array.isArray(content)) {
|
|
1871
|
+
const hasActivity = content.some(block => {
|
|
1872
|
+
if (block === null || typeof block !== "object") return false;
|
|
1873
|
+
const type = (block as { type?: unknown }).type;
|
|
1874
|
+
if (type === "text") {
|
|
1875
|
+
const text = (block as { text?: unknown }).text;
|
|
1876
|
+
return typeof text === "string" && text.trim().length > 0;
|
|
1877
|
+
}
|
|
1878
|
+
return typeof type === "string" && type.length > 0;
|
|
1879
|
+
});
|
|
1880
|
+
const text = content
|
|
1881
|
+
.filter(
|
|
1882
|
+
(block): block is { type: "text"; text: string } =>
|
|
1883
|
+
block !== null &&
|
|
1884
|
+
typeof block === "object" &&
|
|
1885
|
+
(block as { type?: unknown }).type === "text" &&
|
|
1886
|
+
typeof (block as { text?: unknown }).text === "string",
|
|
1887
|
+
)
|
|
1888
|
+
.map(block => block.text)
|
|
1889
|
+
.join("");
|
|
1890
|
+
return { content: sanitizeTurnResultContent(text), hasActivity };
|
|
1891
|
+
}
|
|
1892
|
+
return { content: sanitizeTurnResultContent(""), hasActivity: false };
|
|
1893
|
+
} catch {
|
|
1894
|
+
return { hasActivity: false };
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1603
1898
|
function createControlSurface(
|
|
1604
1899
|
ctx: ExtensionContext,
|
|
1605
1900
|
api: ExtensionAPI,
|
|
@@ -1611,6 +1906,7 @@ function createControlSurface(
|
|
|
1611
1906
|
startsOwnTurn: boolean,
|
|
1612
1907
|
sdkRunToken: string,
|
|
1613
1908
|
) => void,
|
|
1909
|
+
armPromptDeadline: (correlation: InvocationCorrelation) => void,
|
|
1614
1910
|
steerReconciliation: KindAwareReconciliation,
|
|
1615
1911
|
onPromotedTurn?: (
|
|
1616
1912
|
kind: InvocationKind,
|
|
@@ -1638,6 +1934,36 @@ function createControlSurface(
|
|
|
1638
1934
|
canResolveGate: () => boolean = () => true,
|
|
1639
1935
|
trackGateResolution: <T>(resolution: Promise<T>) => Promise<T> = async resolution => await resolution,
|
|
1640
1936
|
): ControlSurface {
|
|
1937
|
+
const normalizePromptImages = (value: unknown): ImageContent[] => {
|
|
1938
|
+
if (!Array.isArray(value)) return [];
|
|
1939
|
+
const normalized: ImageContent[] = [];
|
|
1940
|
+
for (const candidate of value) {
|
|
1941
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
|
|
1942
|
+
const image = candidate as Record<string, unknown>;
|
|
1943
|
+
if (typeof image.data !== "string" || image.data.trim().length === 0) continue;
|
|
1944
|
+
if (image.mimeType !== undefined && typeof image.mimeType !== "string") continue;
|
|
1945
|
+
normalized.push({
|
|
1946
|
+
type: "image",
|
|
1947
|
+
data: image.data,
|
|
1948
|
+
mimeType: typeof image.mimeType === "string" && image.mimeType.length > 0 ? image.mimeType : "image/png",
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
return normalized;
|
|
1952
|
+
};
|
|
1953
|
+
type InternalSendOptions = NonNullable<Parameters<ExtensionAPI["sendUserMessage"]>[1]> & {
|
|
1954
|
+
sdkRunCapability?: SdkRunCapability;
|
|
1955
|
+
};
|
|
1956
|
+
type InternalSdkApi = Omit<ExtensionAPI, "sendUserMessage"> & {
|
|
1957
|
+
sendUserMessage: (
|
|
1958
|
+
content: Parameters<ExtensionAPI["sendUserMessage"]>[0],
|
|
1959
|
+
options?: InternalSendOptions,
|
|
1960
|
+
) => Promise<void>;
|
|
1961
|
+
};
|
|
1962
|
+
const internalApi = api as InternalSdkApi;
|
|
1963
|
+
const sendSdkUserMessage = (
|
|
1964
|
+
content: Parameters<ExtensionAPI["sendUserMessage"]>[0],
|
|
1965
|
+
options?: Record<string, unknown>,
|
|
1966
|
+
): Promise<void> => internalApi.sendUserMessage(content, options);
|
|
1641
1967
|
const surfacePolicy =
|
|
1642
1968
|
policy ?? createSdkSurfacePolicyForContext(ctx, hasSdkWorkflowGateCapability(ctx.workflowGate));
|
|
1643
1969
|
const typed = (operation: string, input: Record<string, unknown> = {}) =>
|
|
@@ -1722,7 +2048,7 @@ function createControlSurface(
|
|
|
1722
2048
|
kind: InvocationKind,
|
|
1723
2049
|
clientRef: string | undefined,
|
|
1724
2050
|
run: (options: {
|
|
1725
|
-
|
|
2051
|
+
sdkRunCapability: SdkRunCapability;
|
|
1726
2052
|
onPreflightAccepted: () => void;
|
|
1727
2053
|
onPreflightAcceptCommit: () => Promise<void>;
|
|
1728
2054
|
/** Internal disposition before a queued submission is actually consumed. */
|
|
@@ -1743,6 +2069,7 @@ function createControlSurface(
|
|
|
1743
2069
|
reconciliation.admit(kind, retainedClientRef);
|
|
1744
2070
|
const correlation = newCorrelation();
|
|
1745
2071
|
const sdkRunToken = `${correlation.commandId}:${correlation.turnId}`;
|
|
2072
|
+
const sdkRunCapability = createSdkRunCapability(sdkRunToken);
|
|
1746
2073
|
const preflight = Promise.withResolvers<void>();
|
|
1747
2074
|
let accepted = false;
|
|
1748
2075
|
let settled = false;
|
|
@@ -1766,6 +2093,7 @@ function createControlSurface(
|
|
|
1766
2093
|
await reconciliation.noteAccepted(kind, correlation, retainedClientRef);
|
|
1767
2094
|
accepted = true;
|
|
1768
2095
|
settled = true;
|
|
2096
|
+
if (kind === "prompt" && startsOwnTurn) armPromptDeadline(correlation);
|
|
1769
2097
|
// The accepted submission does NOT own the active turn until its run
|
|
1770
2098
|
// actually STARTS: the connection is carried on the pending entry and
|
|
1771
2099
|
// associated at agent_start instead (review thread P1).
|
|
@@ -1801,7 +2129,7 @@ function createControlSurface(
|
|
|
1801
2129
|
const submission = Promise.resolve(
|
|
1802
2130
|
run({
|
|
1803
2131
|
onPreflightAccepted: () => void accept().catch(() => undefined),
|
|
1804
|
-
|
|
2132
|
+
sdkRunCapability,
|
|
1805
2133
|
onPreflightAcceptCommit: accept,
|
|
1806
2134
|
onDispatchDisposition: promotion => {
|
|
1807
2135
|
promotionStartsOwnRun = promotion.startsOwnRun;
|
|
@@ -2018,7 +2346,7 @@ function createControlSurface(
|
|
|
2018
2346
|
}
|
|
2019
2347
|
};
|
|
2020
2348
|
const terminalAbort = async (
|
|
2021
|
-
input: { mode: "terminal"; scope?: "turn" | "owned" },
|
|
2349
|
+
input: { mode: "terminal"; scope?: "turn" | "owned"; operator?: boolean },
|
|
2022
2350
|
idempotencyKey?: string,
|
|
2023
2351
|
): Promise<unknown> => {
|
|
2024
2352
|
const scope = input.scope === "owned" ? "owned" : "turn";
|
|
@@ -2049,10 +2377,12 @@ function createControlSurface(
|
|
|
2049
2377
|
typeof idempotencyKey === "string"
|
|
2050
2378
|
? crypto.createHash("sha256").update(idempotencyKey).digest("hex")
|
|
2051
2379
|
: undefined;
|
|
2052
|
-
const
|
|
2053
|
-
|
|
2054
|
-
.
|
|
2055
|
-
|
|
2380
|
+
const abortIdentity = terminalAbortIdentity(
|
|
2381
|
+
{ mode: "terminal", scope, ...(input.operator === true ? { operator: true } : {}) },
|
|
2382
|
+
input.operator === true,
|
|
2383
|
+
);
|
|
2384
|
+
if (!abortIdentity) throw new Error("Terminal abort identity is invalid after dispatch validation.");
|
|
2385
|
+
const inputHash = abortIdentity.inputHash;
|
|
2056
2386
|
const stored = (record: SdkOnlyTerminalScopeRecord | SdkOnlyEvictedTerminalKeyEntry) => ({
|
|
2057
2387
|
responseState: record.responseState ?? "pending",
|
|
2058
2388
|
responsePayloadHash: record.responsePayloadHash ?? inputHash,
|
|
@@ -2425,6 +2755,16 @@ function createControlSurface(
|
|
|
2425
2755
|
pendingPreflights.delete(requesterBucketKey);
|
|
2426
2756
|
}
|
|
2427
2757
|
};
|
|
2758
|
+
const returnNoActiveTurn = async () => {
|
|
2759
|
+
const result = {
|
|
2760
|
+
ok: true,
|
|
2761
|
+
selection: scope,
|
|
2762
|
+
turn: "no_active_turn",
|
|
2763
|
+
terminal: "terminal_no_effect",
|
|
2764
|
+
};
|
|
2765
|
+
await finalizeNoEffectReservation(result);
|
|
2766
|
+
return result;
|
|
2767
|
+
};
|
|
2428
2768
|
if (!handle || epoch === undefined) {
|
|
2429
2769
|
if ((await writeNoEffect()) === "conflict") {
|
|
2430
2770
|
throw Object.assign(new Error("Idempotency key was reused with different input."), {
|
|
@@ -2440,6 +2780,10 @@ function createControlSurface(
|
|
|
2440
2780
|
if (replayed !== undefined) return replayed;
|
|
2441
2781
|
}
|
|
2442
2782
|
cancelRequesterPreflights();
|
|
2783
|
+
// Operator authority overrides connection ownership only for a turn
|
|
2784
|
+
// observed at admission. It must never adopt a successor that starts
|
|
2785
|
+
// while the durable no-effect reservation is being written.
|
|
2786
|
+
if (input.operator === true) return await returnNoActiveTurn();
|
|
2443
2787
|
// A prompt for this requester may have become ACTIVE while the
|
|
2444
2788
|
// reservation awaited the filesystem transaction: its submit()
|
|
2445
2789
|
// cleanup already removed the preflight callback, so cancelling here
|
|
@@ -2455,14 +2799,7 @@ function createControlSurface(
|
|
|
2455
2799
|
// No prompt won the race: finalize the reserved row so a later
|
|
2456
2800
|
// same-key retry replays this deterministic no_active_turn result
|
|
2457
2801
|
// (review thread P2).
|
|
2458
|
-
|
|
2459
|
-
ok: true,
|
|
2460
|
-
selection: scope,
|
|
2461
|
-
turn: "no_active_turn",
|
|
2462
|
-
terminal: "terminal_no_effect",
|
|
2463
|
-
};
|
|
2464
|
-
await finalizeNoEffectReservation(noActiveTurnResult);
|
|
2465
|
-
return noActiveTurnResult;
|
|
2802
|
+
return await returnNoActiveTurn();
|
|
2466
2803
|
}
|
|
2467
2804
|
handle = recheckedHandle;
|
|
2468
2805
|
epoch = recheckedEpoch;
|
|
@@ -2492,7 +2829,11 @@ function createControlSurface(
|
|
|
2492
2829
|
// cleared after a terminal lifecycle boundary) authorizes no client, and
|
|
2493
2830
|
// a stale prior owner authorizes only that old client — never a later
|
|
2494
2831
|
// turn it did not submit (review thread P1).
|
|
2495
|
-
if (
|
|
2832
|
+
if (
|
|
2833
|
+
input.operator !== true &&
|
|
2834
|
+
handle &&
|
|
2835
|
+
(abortingConnectionId === undefined || !currentOwnerConnectionIds().has(abortingConnectionId))
|
|
2836
|
+
) {
|
|
2496
2837
|
if ((await writeNoEffect()) === "conflict") {
|
|
2497
2838
|
throw Object.assign(new Error("Idempotency key was reused with different input."), {
|
|
2498
2839
|
code: "idempotency_conflict",
|
|
@@ -2881,52 +3222,77 @@ function createControlSurface(
|
|
|
2881
3222
|
}
|
|
2882
3223
|
};
|
|
2883
3224
|
return {
|
|
2884
|
-
prompt: async (text, images, clientRef) =>
|
|
2885
|
-
|
|
2886
|
-
|
|
2887
|
-
|
|
3225
|
+
prompt: async (text, images, clientRef) => {
|
|
3226
|
+
const invalid = validateRequiredPromptText("turn.prompt", { text, images });
|
|
3227
|
+
if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
|
|
3228
|
+
return await submit("prompt", clientRef, ({ queuedAtDispatch, sdkRunCapability, ...options }) =>
|
|
3229
|
+
sendSdkUserMessage(
|
|
3230
|
+
typeof images === "undefined"
|
|
3231
|
+
? text
|
|
3232
|
+
: ([{ type: "text", text }, ...normalizePromptImages(images)] as [
|
|
3233
|
+
{ type: "text"; text: string },
|
|
3234
|
+
...ImageContent[],
|
|
3235
|
+
]),
|
|
2888
3236
|
{
|
|
2889
3237
|
...options,
|
|
2890
|
-
|
|
3238
|
+
sdkRunCapability,
|
|
3239
|
+
// ACP terminal settlement is owned by the correlated agent_end
|
|
3240
|
+
// publication. Post-prompt recovery may include independent
|
|
3241
|
+
// subagent work and must not hold that client-facing boundary.
|
|
2891
3242
|
...(queuedAtDispatch ? { queuedAtDispatch: true } : {}),
|
|
2892
3243
|
},
|
|
2893
3244
|
),
|
|
2894
|
-
)
|
|
3245
|
+
);
|
|
3246
|
+
},
|
|
2895
3247
|
steer: async (text, clientRef) => {
|
|
3248
|
+
const invalid = validateRequiredPromptText("turn.steer", { text });
|
|
3249
|
+
if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
|
|
2896
3250
|
const retainedClientRef = normalizeClientRef(clientRef);
|
|
2897
3251
|
if (retainedClientRef === undefined) {
|
|
2898
3252
|
const correlation = newCorrelation();
|
|
2899
|
-
await
|
|
3253
|
+
await sendSdkUserMessage(text, { deliverAs: "steer" });
|
|
2900
3254
|
return { accepted: true, ...correlation };
|
|
2901
3255
|
}
|
|
2902
3256
|
const durable = steerReconciliation;
|
|
2903
3257
|
const reservation = await durable.reserveSteer(retainedClientRef, text);
|
|
2904
3258
|
if (reservation.replay) return { accepted: reservation.result.status === "accepted", ...reservation.result };
|
|
2905
3259
|
try {
|
|
2906
|
-
await
|
|
3260
|
+
await sendSdkUserMessage(text, { deliverAs: "steer" });
|
|
2907
3261
|
return { accepted: true, ...(await durable.settleSteer(retainedClientRef, "accepted")) };
|
|
2908
3262
|
} catch (error) {
|
|
2909
3263
|
return { accepted: false, ...(await durable.settleSteer(retainedClientRef, "rejected", error)) };
|
|
2910
3264
|
}
|
|
2911
3265
|
},
|
|
2912
|
-
followUp: async text =>
|
|
2913
|
-
|
|
3266
|
+
followUp: async text => {
|
|
3267
|
+
const invalid = validateRequiredPromptText("turn.follow_up", { text });
|
|
3268
|
+
if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
|
|
3269
|
+
return await submit(
|
|
2914
3270
|
"prompt",
|
|
2915
3271
|
undefined,
|
|
2916
|
-
|
|
3272
|
+
({ sdkRunCapability, ...options }) =>
|
|
3273
|
+
sendSdkUserMessage(text, {
|
|
3274
|
+
...options,
|
|
3275
|
+
sdkRunCapability,
|
|
3276
|
+
deliverAs: "followUp",
|
|
3277
|
+
}),
|
|
2917
3278
|
undefined,
|
|
2918
3279
|
false,
|
|
2919
3280
|
// Follow-ups never start inline; ownership correlates at promotion.
|
|
2920
3281
|
true,
|
|
2921
|
-
)
|
|
3282
|
+
);
|
|
3283
|
+
},
|
|
2922
3284
|
abort: async () => {
|
|
2923
3285
|
await Promise.resolve(ctx.abort()).catch(() => undefined);
|
|
2924
3286
|
return { aborted: true };
|
|
2925
3287
|
},
|
|
2926
3288
|
abortTerminal: terminalAbort,
|
|
2927
3289
|
abortAndPrompt: async text => {
|
|
3290
|
+
const invalid = validateRequiredPromptText("turn.abort_and_prompt", { text });
|
|
3291
|
+
if (invalid) throw Object.assign(new Error(invalid.message), { code: invalid.code });
|
|
2928
3292
|
await ctx.abort();
|
|
2929
|
-
return await submit("prompt", undefined,
|
|
3293
|
+
return await submit("prompt", undefined, ({ sdkRunCapability, ...options }) =>
|
|
3294
|
+
sendSdkUserMessage(text, { ...options, sdkRunCapability }),
|
|
3295
|
+
);
|
|
2930
3296
|
},
|
|
2931
3297
|
answerAsk: unavailable("ask.answer"),
|
|
2932
3298
|
answerGate: async (id, response, expectedSessionId, idempotencyKey) =>
|
|
@@ -2953,9 +3319,10 @@ function createControlSurface(
|
|
|
2953
3319
|
return await submit(
|
|
2954
3320
|
"skill",
|
|
2955
3321
|
clientRef,
|
|
2956
|
-
options =>
|
|
3322
|
+
({ sdkRunCapability, ...options }) =>
|
|
2957
3323
|
ctx.invokeSkill!(name, args, {
|
|
2958
3324
|
...options,
|
|
3325
|
+
sdkRunCapability,
|
|
2959
3326
|
onSkillPrepared: meta => {
|
|
2960
3327
|
prepared = meta;
|
|
2961
3328
|
},
|
|
@@ -3135,6 +3502,107 @@ export function registerSdkOnlyNotificationCommand(api: ExtensionAPI): void {
|
|
|
3135
3502
|
});
|
|
3136
3503
|
}
|
|
3137
3504
|
|
|
3505
|
+
export function masterAttestationForEffectiveHost(input: {
|
|
3506
|
+
masterCapability: string | undefined;
|
|
3507
|
+
attestationEpoch: string | undefined;
|
|
3508
|
+
ownerSessionId?: string;
|
|
3509
|
+
sessionId: string;
|
|
3510
|
+
pid: number;
|
|
3511
|
+
processIncarnation: string | undefined;
|
|
3512
|
+
direct: MasterRoleAttestationV2 | undefined;
|
|
3513
|
+
}): MasterRoleAttestationV2 | undefined {
|
|
3514
|
+
const direct = input.direct;
|
|
3515
|
+
if (
|
|
3516
|
+
input.masterCapability === undefined ||
|
|
3517
|
+
input.attestationEpoch === undefined ||
|
|
3518
|
+
direct === undefined ||
|
|
3519
|
+
direct.ownerSessionId !== (input.ownerSessionId ?? input.sessionId) ||
|
|
3520
|
+
direct.launchPid !== input.pid ||
|
|
3521
|
+
direct.launchProcessIncarnation !== input.processIncarnation ||
|
|
3522
|
+
direct.attestationEpoch !== input.attestationEpoch
|
|
3523
|
+
)
|
|
3524
|
+
return undefined;
|
|
3525
|
+
return direct;
|
|
3526
|
+
}
|
|
3527
|
+
|
|
3528
|
+
function masterDirectAttestation(input: {
|
|
3529
|
+
masterCapability: string | undefined;
|
|
3530
|
+
attestationEpoch: string | undefined;
|
|
3531
|
+
ownerSessionId?: string;
|
|
3532
|
+
sessionId: string;
|
|
3533
|
+
pid: number;
|
|
3534
|
+
processIncarnation: string | undefined;
|
|
3535
|
+
}): MasterRoleAttestationV2 | undefined {
|
|
3536
|
+
if (
|
|
3537
|
+
input.masterCapability === undefined ||
|
|
3538
|
+
input.attestationEpoch === undefined ||
|
|
3539
|
+
input.processIncarnation === undefined
|
|
3540
|
+
)
|
|
3541
|
+
return undefined;
|
|
3542
|
+
return {
|
|
3543
|
+
version: 2,
|
|
3544
|
+
ownerSessionId: input.ownerSessionId ?? input.sessionId,
|
|
3545
|
+
launchPid: input.pid,
|
|
3546
|
+
launchProcessIncarnation: input.processIncarnation,
|
|
3547
|
+
role: "master",
|
|
3548
|
+
attestationEpoch: input.attestationEpoch,
|
|
3549
|
+
};
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
function sameMasterAttestation(left: MasterRoleAttestationV2, right: MasterRoleAttestationV2): boolean {
|
|
3553
|
+
return (
|
|
3554
|
+
left.version === right.version &&
|
|
3555
|
+
left.ownerSessionId === right.ownerSessionId &&
|
|
3556
|
+
left.launchPid === right.launchPid &&
|
|
3557
|
+
left.launchProcessIncarnation === right.launchProcessIncarnation &&
|
|
3558
|
+
left.role === right.role &&
|
|
3559
|
+
left.attestationEpoch === right.attestationEpoch
|
|
3560
|
+
);
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
/**
|
|
3564
|
+
* Establish direct master authority for the session currently hosted by this
|
|
3565
|
+
* process. Session identity transitions retain the master capability and epoch
|
|
3566
|
+
* but require a new direct attestation before their effective endpoint can
|
|
3567
|
+
* adopt that authority.
|
|
3568
|
+
*/
|
|
3569
|
+
export async function reattestMasterSessionIdentity(input: {
|
|
3570
|
+
index: SessionIndex;
|
|
3571
|
+
locator: SessionLocatorV2;
|
|
3572
|
+
masterCapability: string | undefined;
|
|
3573
|
+
attestationEpoch: string | undefined;
|
|
3574
|
+
ownerSessionId?: string;
|
|
3575
|
+
sessionId: string;
|
|
3576
|
+
pid: number;
|
|
3577
|
+
processIncarnation: string | undefined;
|
|
3578
|
+
}): Promise<MasterRoleAttestationV2 | undefined> {
|
|
3579
|
+
const masterRole = masterDirectAttestation(input);
|
|
3580
|
+
if (!masterRole) return undefined;
|
|
3581
|
+
const directExists = input.index.listSessionIdentities().some(row => {
|
|
3582
|
+
const processIdentity = row.hostIncarnation ?? row.processIncarnation;
|
|
3583
|
+
return (
|
|
3584
|
+
row.sessionId === input.sessionId &&
|
|
3585
|
+
row.endpointGeneration === 0 &&
|
|
3586
|
+
row.pid === input.pid &&
|
|
3587
|
+
processIdentity === input.processIncarnation &&
|
|
3588
|
+
row.masterRole !== undefined &&
|
|
3589
|
+
sameMasterAttestation(row.masterRole, masterRole)
|
|
3590
|
+
);
|
|
3591
|
+
});
|
|
3592
|
+
if (!directExists) {
|
|
3593
|
+
await input.index.append({
|
|
3594
|
+
type: "host_registered",
|
|
3595
|
+
sessionId: input.sessionId,
|
|
3596
|
+
locator: input.locator,
|
|
3597
|
+
endpointGeneration: 0,
|
|
3598
|
+
pid: input.pid,
|
|
3599
|
+
processIncarnation: input.processIncarnation,
|
|
3600
|
+
masterRole,
|
|
3601
|
+
});
|
|
3602
|
+
}
|
|
3603
|
+
return masterRole;
|
|
3604
|
+
}
|
|
3605
|
+
|
|
3138
3606
|
function quiescingFrame(frame: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
3139
3607
|
const error = { code: "session_quiescing", message: "The session endpoint is being replaced." };
|
|
3140
3608
|
const id = typeof frame.id === "string" ? frame.id : "";
|
|
@@ -3374,6 +3842,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3374
3842
|
failureCause?: unknown,
|
|
3375
3843
|
maintenanceOutcome?: string,
|
|
3376
3844
|
lifecycleOwner?: LifecycleOwner,
|
|
3845
|
+
terminalContent?: TurnResultContent,
|
|
3846
|
+
terminalHasActivity = false,
|
|
3847
|
+
terminalOutcome?: { kind: "stopped"; reason: "cancelled"; provenance: "client_cancel" },
|
|
3377
3848
|
): Promise<void> => {
|
|
3378
3849
|
const current = lifecycleOwner?.state ?? active;
|
|
3379
3850
|
if (!current) return;
|
|
@@ -3461,6 +3932,11 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3461
3932
|
if (current.activeInvocation?.kind === "prompt") {
|
|
3462
3933
|
current.deadlineManager.onAccepted(current.activeInvocation.correlation);
|
|
3463
3934
|
}
|
|
3935
|
+
} else {
|
|
3936
|
+
// An empty drain is an agent-initiated successor run. Clear the
|
|
3937
|
+
// predecessor's SDK owner and active invocation so abort ownership and
|
|
3938
|
+
// tool-progress renewal cannot leak into the successor.
|
|
3939
|
+
adoptLifecycleBatch(undefined);
|
|
3464
3940
|
}
|
|
3465
3941
|
transitions = drained.map(({ kind, correlation }) => ({ kind, correlation }));
|
|
3466
3942
|
} else {
|
|
@@ -3626,7 +4102,17 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3626
4102
|
const reasonKey = `${invocation.correlation.commandId}:${invocation.correlation.turnId}`;
|
|
3627
4103
|
const unrecorded = type === "agent_end" ? current.unrecordedFailureReasons?.get(reasonKey) : undefined;
|
|
3628
4104
|
if (type === "agent_end" && invocation.kind === "prompt")
|
|
3629
|
-
current.deadlineManager.noteTerminalTransition(
|
|
4105
|
+
current.deadlineManager.noteTerminalTransition(
|
|
4106
|
+
invocation.correlation,
|
|
4107
|
+
unrecorded,
|
|
4108
|
+
terminalContent !== undefined || terminalHasActivity || terminalOutcome !== undefined
|
|
4109
|
+
? ({
|
|
4110
|
+
content: terminalContent,
|
|
4111
|
+
hasActivity: terminalHasActivity,
|
|
4112
|
+
outcome: terminalOutcome,
|
|
4113
|
+
} satisfies PromptTerminalTransitionEvidence)
|
|
4114
|
+
: undefined,
|
|
4115
|
+
);
|
|
3630
4116
|
if (type === "agent_end") {
|
|
3631
4117
|
// Compound recovery (exact-head review P1): if this run's
|
|
3632
4118
|
// agent_failed write failed, re-record the sanitized reason
|
|
@@ -3650,7 +4136,16 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3650
4136
|
error:
|
|
3651
4137
|
failureCause ?? Object.assign(new Error("agent run failed"), { code: "agent_failed" }),
|
|
3652
4138
|
}
|
|
3653
|
-
: {
|
|
4139
|
+
: {
|
|
4140
|
+
type,
|
|
4141
|
+
...(type === "agent_end" && terminalContent !== undefined
|
|
4142
|
+
? { content: terminalContent }
|
|
4143
|
+
: {}),
|
|
4144
|
+
...(type === "agent_end" && terminalHasActivity ? { hasActivity: true } : {}),
|
|
4145
|
+
...(type === "agent_end" && terminalOutcome !== undefined
|
|
4146
|
+
? { outcome: terminalOutcome }
|
|
4147
|
+
: {}),
|
|
4148
|
+
};
|
|
3654
4149
|
await current.reconciliation.noteTransition(invocation.kind, invocation.correlation, frame as never);
|
|
3655
4150
|
if ((type as string) === "agent_end") {
|
|
3656
4151
|
if (invocation.kind === "prompt") current.deadlineManager.clear(invocation.correlation);
|
|
@@ -3814,6 +4309,11 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3814
4309
|
owner?.failureDiagnosticKeys.has(lifecycleCorrelationKey(correlation)),
|
|
3815
4310
|
);
|
|
3816
4311
|
const failureAlreadyPublished = failure === undefined || (hasExistingFailure && genericFailureKeys.length === 0);
|
|
4312
|
+
const terminalEvidence = promptTerminalEvidenceFromAgentEnd(event);
|
|
4313
|
+
const terminalOutcome =
|
|
4314
|
+
event.stopReason === "cancelled"
|
|
4315
|
+
? ({ kind: "stopped", reason: "cancelled", provenance: "client_cancel" } as const)
|
|
4316
|
+
: undefined;
|
|
3817
4317
|
return trackLifecycle(async () => {
|
|
3818
4318
|
if (failure && !failureAlreadyPublished) {
|
|
3819
4319
|
for (const key of genericFailureKeys) owner?.failureDiagnosticKeys.delete(key);
|
|
@@ -3825,6 +4325,9 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3825
4325
|
undefined,
|
|
3826
4326
|
event.stopReason === "maintenance" ? event.maintenanceOutcome : undefined,
|
|
3827
4327
|
lifecycleOwner,
|
|
4328
|
+
terminalEvidence.content,
|
|
4329
|
+
terminalEvidence.hasActivity,
|
|
4330
|
+
terminalOutcome,
|
|
3828
4331
|
);
|
|
3829
4332
|
}, owner).finally(() => {
|
|
3830
4333
|
if (typeof event.sdkRunToken === "string" && lifecycleRunOwners.get(event.sdkRunToken)?.state === owner)
|
|
@@ -3860,6 +4363,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
3860
4363
|
await current.registerBroker();
|
|
3861
4364
|
current.runtime.emitEvent({ type: "turn_start", sessionId: ctx.sessionManager.getSessionId() });
|
|
3862
4365
|
});
|
|
4366
|
+
const consumedMasterNonces = new Map<string, number>();
|
|
3863
4367
|
api.on("turn_end", (_event, ctx) =>
|
|
3864
4368
|
lifecycleStateForContext(ctx, "agent_end")?.runtime.emitEvent({
|
|
3865
4369
|
type: "turn_end",
|
|
@@ -4159,8 +4663,8 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4159
4663
|
// promotion/agent_start instead, so a prompt waiting behind a
|
|
4160
4664
|
// legitimately long turn never false-fires. The agent_start
|
|
4161
4665
|
// re-entry in emitLifecycle is a no-op for an existing lease.
|
|
4162
|
-
if (startsOwnTurn && kind === "prompt") deadlineManager.onAccepted(correlation);
|
|
4163
4666
|
},
|
|
4667
|
+
correlation => deadlineManager.onAccepted(correlation),
|
|
4164
4668
|
steerReconciliation,
|
|
4165
4669
|
(kind, correlation, connectionId, sdkRunToken, promotion) => {
|
|
4166
4670
|
const bindPromotedToken = (batch?: LifecycleBatch): void => {
|
|
@@ -4419,6 +4923,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4419
4923
|
};
|
|
4420
4924
|
runtime = new SessionSdkSessionRuntime({
|
|
4421
4925
|
transport,
|
|
4926
|
+
masterCapabilityVerify: frame =>
|
|
4927
|
+
verifyMasterCapabilityFrame({
|
|
4928
|
+
frame,
|
|
4929
|
+
expectedCapability: options.masterCapability,
|
|
4930
|
+
expectedEpoch: options.masterAttestationEpoch,
|
|
4931
|
+
replay: consumedMasterNonces,
|
|
4932
|
+
}),
|
|
4422
4933
|
onFrameAdmission: (_connectionId, frame) =>
|
|
4423
4934
|
inputGate.quiescing ? quiescingFrame(frame as Record<string, unknown>) : undefined,
|
|
4424
4935
|
control: async (connectionId, frame) => {
|
|
@@ -4464,21 +4975,13 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4464
4975
|
(request.input as { mode?: unknown }).mode !== "terminal"
|
|
4465
4976
|
)
|
|
4466
4977
|
return;
|
|
4467
|
-
const
|
|
4468
|
-
const
|
|
4469
|
-
const
|
|
4470
|
-
|
|
4471
|
-
if (
|
|
4472
|
-
// A malformed retry (e.g. {mode:"terminal", scope:"turn", extra:true})
|
|
4473
|
-
// rejected by dispatch must never hash as the valid turn input and
|
|
4474
|
-
// advance the legitimate stored row (review thread P2).
|
|
4475
|
-
for (const key of Object.keys(input)) if (key !== "mode" && key !== "scope") return;
|
|
4476
|
-
const scopeInput = rawScope === "owned" ? "owned" : "turn";
|
|
4978
|
+
const rawInput = request.input as Record<string, unknown>;
|
|
4979
|
+
const operatorAuthorized = hasBrokerRuntimeAbortCapability(rawInput);
|
|
4980
|
+
const { [BROKER_RUNTIME_ABORT_CAPABILITY_FIELD]: _capability, ...publicInput } = rawInput;
|
|
4981
|
+
const abortIdentity = terminalAbortIdentity(publicInput, operatorAuthorized);
|
|
4982
|
+
if (!abortIdentity) return;
|
|
4477
4983
|
const keyHash = crypto.createHash("sha256").update(String(request.idempotencyKey)).digest("hex");
|
|
4478
|
-
const inputHash =
|
|
4479
|
-
.createHash("sha256")
|
|
4480
|
-
.update(JSON.stringify({ mode: "terminal", scope: scopeInput }))
|
|
4481
|
-
.digest("hex");
|
|
4984
|
+
const inputHash = abortIdentity.inputHash;
|
|
4482
4985
|
// Hash the ACTUAL written response payload: the durable state may only
|
|
4483
4986
|
// advance when the written response corresponds to the row's payload.
|
|
4484
4987
|
// When more than 256 concurrent requests evict an in-flight abort from
|
|
@@ -4534,21 +5037,72 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4534
5037
|
const disposeGate = ctx.workflowGate?.onGateEmitted?.(gate =>
|
|
4535
5038
|
runtime.emitEvent({ kind: "workflow_gate", payload: gate }),
|
|
4536
5039
|
);
|
|
5040
|
+
let publishedEndpointUrl: string | undefined;
|
|
4537
5041
|
let brokerRegistered = false;
|
|
4538
5042
|
const registerBroker = async (): Promise<void> => {
|
|
4539
5043
|
if (brokerRegistered) return;
|
|
4540
5044
|
try {
|
|
5045
|
+
if (options.brokerRegistrationRequired && !options.lifecycleRequestId)
|
|
5046
|
+
throw new Error("Lifecycle broker registration requires a request identity.");
|
|
4541
5047
|
await ensureBroker({ agentDir: options.agentDir });
|
|
4542
5048
|
const index = await new SessionIndex(options.agentDir).open();
|
|
4543
|
-
const locator =
|
|
5049
|
+
const locator = await resolveSessionLocator(ctx.cwd, stateRoot);
|
|
5050
|
+
const effectiveIncarnation = processIncarnation(process.pid);
|
|
5051
|
+
const direct = await reattestMasterSessionIdentity({
|
|
5052
|
+
index,
|
|
5053
|
+
locator,
|
|
5054
|
+
masterCapability: options.masterCapability,
|
|
5055
|
+
attestationEpoch: options.masterAttestationEpoch,
|
|
5056
|
+
ownerSessionId: options.masterOwnerSessionId,
|
|
5057
|
+
sessionId,
|
|
5058
|
+
pid: process.pid,
|
|
5059
|
+
processIncarnation: effectiveIncarnation,
|
|
5060
|
+
});
|
|
4544
5061
|
await runtime.registerWithBroker({
|
|
4545
5062
|
register: async input => {
|
|
4546
|
-
|
|
4547
|
-
.
|
|
4548
|
-
|
|
5063
|
+
if (publishedEndpointUrl === undefined)
|
|
5064
|
+
throw new Error("SDK transport endpoint was not published before broker registration.");
|
|
5065
|
+
const endpointPath = path.join(input.stateRoot, "sdk", `${input.sessionId}.json`);
|
|
5066
|
+
const file = await readEndpointFile(endpointPath);
|
|
5067
|
+
if (!file) throw new Error("SDK endpoint could not be read as a stable regular file.");
|
|
5068
|
+
const endpoint = JSON.parse(file.source) as Record<string, unknown>;
|
|
5069
|
+
if (
|
|
5070
|
+
endpoint.sessionId !== input.sessionId ||
|
|
5071
|
+
endpoint.pid !== process.pid ||
|
|
5072
|
+
endpoint.url !== publishedEndpointUrl ||
|
|
5073
|
+
endpoint.token !== transport.token
|
|
5074
|
+
)
|
|
5075
|
+
throw new Error("SDK endpoint did not match the published transport authority.");
|
|
5076
|
+
const endpointMtimeMs = file.mtimeMs;
|
|
5077
|
+
const endpointFileId = `${file.dev}:${file.ino}`;
|
|
5078
|
+
const masterRole = masterAttestationForEffectiveHost({
|
|
5079
|
+
masterCapability: options.masterCapability,
|
|
5080
|
+
attestationEpoch: options.masterAttestationEpoch,
|
|
5081
|
+
ownerSessionId: options.masterOwnerSessionId,
|
|
5082
|
+
sessionId: input.sessionId,
|
|
5083
|
+
pid: process.pid,
|
|
5084
|
+
processIncarnation: effectiveIncarnation,
|
|
5085
|
+
direct,
|
|
5086
|
+
});
|
|
5087
|
+
await index.append({
|
|
5088
|
+
type: "host_registered",
|
|
5089
|
+
...input,
|
|
5090
|
+
locator,
|
|
5091
|
+
pid: process.pid,
|
|
5092
|
+
endpointMtimeMs,
|
|
5093
|
+
endpointFileId,
|
|
5094
|
+
...(options.lifecycleRequestId ? { lifecycleRequestId: options.lifecycleRequestId } : {}),
|
|
5095
|
+
...(masterRole ? { masterRole } : {}),
|
|
5096
|
+
});
|
|
4549
5097
|
},
|
|
4550
5098
|
unregister: async input => {
|
|
4551
|
-
await index.append({
|
|
5099
|
+
await index.append({
|
|
5100
|
+
type: "host_unregistered",
|
|
5101
|
+
...input,
|
|
5102
|
+
locator,
|
|
5103
|
+
pid: process.pid,
|
|
5104
|
+
...(options.lifecycleRequestId ? { lifecycleRequestId: options.lifecycleRequestId } : {}),
|
|
5105
|
+
});
|
|
4552
5106
|
},
|
|
4553
5107
|
});
|
|
4554
5108
|
brokerRegistered = true;
|
|
@@ -4587,7 +5141,7 @@ export function createSdkSessionRuntimeExtension(api: ExtensionAPI, options: Cre
|
|
|
4587
5141
|
lifecycleOwnerHolder.state = runtimeOwner;
|
|
4588
5142
|
active = runtimeOwner;
|
|
4589
5143
|
try {
|
|
4590
|
-
await runtime.start();
|
|
5144
|
+
publishedEndpointUrl = (await runtime.start()).url;
|
|
4591
5145
|
await registerBroker();
|
|
4592
5146
|
} catch (error) {
|
|
4593
5147
|
active = undefined;
|