@gajae-code/coding-agent 0.11.10 → 0.12.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 +86 -2
- package/dist/types/cli/read-cli.d.ts +1 -0
- package/dist/types/cli.d.ts +8 -1
- package/dist/types/commands/deep-interview.d.ts +9 -0
- package/dist/types/commands/harness.d.ts +22 -0
- package/dist/types/commands/read.d.ts +6 -0
- package/dist/types/config/model-profile-activation.d.ts +1 -0
- package/dist/types/config/model-profile-contract.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/provider-auth-health.d.ts +14 -0
- package/dist/types/config/provider-ranking.d.ts +59 -0
- package/dist/types/config/settings-schema.d.ts +86 -3
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +43 -2
- package/dist/types/extensibility/shared-events.d.ts +6 -0
- package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
- package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
- package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
- package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
- package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
- package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
- package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
- package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
- package/dist/types/gjc-runtime/team-store.d.ts +12 -0
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
- package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
- package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/memories/index.d.ts +2 -0
- package/dist/types/modes/acp/acp-agent.d.ts +22 -9
- package/dist/types/modes/components/assistant-message.d.ts +3 -1
- package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
- package/dist/types/modes/components/queue-pane.d.ts +8 -0
- package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
- package/dist/types/modes/components/read-tool-group.d.ts +4 -4
- package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +8 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/event-controller.d.ts +3 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -1
- package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
- package/dist/types/modes/types.d.ts +2 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
- package/dist/types/runtime/memory-domain.d.ts +9 -0
- package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
- package/dist/types/runtime/memory-guard.d.ts +39 -0
- package/dist/types/runtime/memory-limit.d.ts +11 -0
- package/dist/types/runtime-mcp/manager.d.ts +8 -0
- package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
- package/dist/types/sdk/acp/final-text.d.ts +19 -0
- package/dist/types/sdk/acp/mcp.d.ts +23 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
- package/dist/types/sdk/bus/index.d.ts +18 -2
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
- package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
- package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
- package/dist/types/sdk/host/control/operations.d.ts +2 -1
- package/dist/types/sdk/host/host.d.ts +2 -0
- package/dist/types/sdk/host/query/handlers.d.ts +5 -0
- package/dist/types/sdk/lifecycle-session.d.ts +10 -1
- package/dist/types/sdk/startup-capability.d.ts +6 -0
- package/dist/types/session/agent-session.d.ts +51 -19
- package/dist/types/session/fallback-chain-controller.d.ts +18 -2
- package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
- package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
- package/dist/types/session/session-manager.d.ts +59 -13
- package/dist/types/session/streaming-output.d.ts +60 -5
- package/dist/types/skill-state/active-state.d.ts +0 -4
- package/dist/types/skill-state/workflow-hud.d.ts +3 -0
- package/dist/types/task/executor.d.ts +6 -0
- package/dist/types/task/types.d.ts +36 -0
- package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
- package/dist/types/tools/browser/launch.d.ts +7 -0
- package/dist/types/tools/fetch.d.ts +17 -1
- package/dist/types/tools/output-meta.d.ts +21 -1
- package/dist/types/tools/read-internals.d.ts +12 -0
- package/dist/types/tools/read.d.ts +45 -1
- package/dist/types/tools/resource-gc.d.ts +23 -0
- package/dist/types/tools/tool-result.d.ts +7 -1
- package/package.json +9 -13
- package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
- package/scripts/capture-sticky-viewport-showcase.ts +292 -0
- package/scripts/generate-sdk-operation-inventory.ts +4 -0
- package/scripts/run-test-manifest.ts +6 -4
- package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
- package/scripts/verify-sticky-viewport-showcase.ts +611 -0
- package/src/capability/index.ts +8 -0
- package/src/cli/args.ts +14 -8
- package/src/cli/fast-help.ts +1 -1
- package/src/cli/read-cli.ts +2 -1
- package/src/cli.ts +49 -2
- package/src/commands/deep-interview.ts +29 -9
- package/src/commands/harness.ts +35 -12
- package/src/commands/read.ts +11 -2
- package/src/commands/sdk.ts +91 -7
- package/src/commands/team.ts +1 -1
- package/src/config/model-profile-activation.ts +1 -0
- package/src/config/model-profile-contract.ts +13 -0
- package/src/config/model-profiles.ts +24 -0
- package/src/config/model-registry.ts +21 -2
- package/src/config/provider-auth-health.ts +42 -0
- package/src/config/provider-ranking.ts +119 -0
- package/src/config/settings-schema.ts +84 -3
- package/src/config/settings.ts +254 -108
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
- package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
- package/src/defaults/gjc/skills/team/SKILL.md +1 -1
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
- package/src/eval/py/executor.ts +7 -2
- package/src/extensibility/extensions/runner.ts +4 -0
- package/src/extensibility/extensions/types.ts +42 -4
- package/src/extensibility/shared-events.ts +5 -0
- package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
- package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
- package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
- package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
- package/src/gjc-runtime/deep-interview-state.ts +12 -921
- package/src/gjc-runtime/gc-render.ts +1 -0
- package/src/gjc-runtime/gc-runtime.ts +11 -1
- package/src/gjc-runtime/linux-proc.ts +70 -36
- package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
- package/src/gjc-runtime/ralplan-runtime.ts +979 -71
- package/src/gjc-runtime/state-runtime.ts +50 -12
- package/src/gjc-runtime/state-writer.ts +70 -222
- package/src/gjc-runtime/team-launch.ts +34 -5
- package/src/gjc-runtime/team-runtime.ts +970 -33
- package/src/gjc-runtime/team-store.ts +32 -0
- package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
- package/src/gjc-runtime/team-workers.ts +1 -0
- package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
- package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
- package/src/gjc-runtime/workflow-manifest.ts +34 -179
- package/src/hooks/native-skill-hook.ts +17 -4
- package/src/hooks/skill-state.ts +1 -23
- package/src/internal-urls/docs-index.generated.ts +10 -11
- package/src/internal-urls/local-protocol.ts +23 -4
- package/src/internal-urls/local-root-gc.ts +127 -0
- package/src/main.ts +9 -5
- package/src/memories/index.ts +10 -0
- package/src/modes/DESIGN.md +47 -0
- package/src/modes/acp/acp-agent.ts +597 -67
- package/src/modes/acp/acp-event-mapper.ts +106 -12
- package/src/modes/acp/acp-mode.ts +39 -3
- package/src/modes/components/assistant-message.ts +123 -22
- package/src/modes/components/irc-sidebar.ts +176 -62
- package/src/modes/components/model-selector.ts +76 -5
- package/src/modes/components/notifications-settings-editor.ts +1 -1
- package/src/modes/components/oauth-selector.ts +42 -12
- package/src/modes/components/queue-pane.ts +55 -9
- package/src/modes/components/queued-message-selector.ts +53 -10
- package/src/modes/components/read-tool-group.ts +82 -14
- package/src/modes/components/session-observer-overlay.ts +187 -32
- package/src/modes/components/settings-selector.ts +60 -18
- package/src/modes/components/tool-execution.ts +138 -27
- package/src/modes/components/tool-status-header.ts +1 -4
- package/src/modes/components/welcome.ts +18 -3
- package/src/modes/controllers/command-controller.ts +4 -0
- package/src/modes/controllers/event-controller.ts +109 -27
- package/src/modes/controllers/extension-ui-controller.ts +23 -12
- package/src/modes/controllers/input-controller.ts +62 -6
- package/src/modes/controllers/selector-controller.ts +83 -37
- package/src/modes/interactive-mode.ts +127 -29
- package/src/modes/irc-observation-ledger.ts +71 -15
- package/src/modes/runtime-init.ts +20 -2
- package/src/modes/types.ts +2 -0
- package/src/modes/utils/ui-helpers.ts +94 -1
- package/src/prompts/agents/architect.md +9 -1
- package/src/prompts/agents/critic.md +9 -0
- package/src/prompts/agents/executor.md +1 -1
- package/src/prompts/system/system-prompt.md +0 -1
- package/src/prompts/tools/read.md +4 -2
- package/src/runtime/memory-domain.ts +66 -0
- package/src/runtime/memory-guard-contract.ts +73 -0
- package/src/runtime/memory-guard.ts +292 -0
- package/src/runtime/memory-limit.ts +50 -0
- package/src/runtime-mcp/manager.ts +26 -6
- package/src/runtime-mcp/smithery-auth.ts +17 -2
- package/src/runtime-mcp/smithery-connect.ts +11 -1
- package/src/sdk/acp/adapter.ts +8 -1
- package/src/sdk/acp/final-text.ts +40 -0
- package/src/sdk/acp/mcp.ts +26 -0
- package/src/sdk/broker/lifecycle.ts +107 -7
- package/src/sdk/bus/chat-daemon-cli.ts +2 -2
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/config.ts +1 -1
- package/src/sdk/bus/control-drain-lease.ts +104 -0
- package/src/sdk/bus/index.ts +503 -60
- package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
- package/src/sdk/bus/prompt-reconciliation.ts +2 -1
- package/src/sdk/bus/rate-limit-pool.ts +8 -0
- package/src/sdk/bus/reconciliation-store.ts +224 -0
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
- package/src/sdk/bus/telegram-daemon.ts +531 -53
- package/src/sdk/host/control/dispatch.ts +7 -1
- package/src/sdk/host/control/operations.ts +2 -1
- package/src/sdk/host/host.ts +25 -3
- package/src/sdk/host/query/handlers.ts +55 -0
- package/src/sdk/lifecycle-session.ts +19 -3
- package/src/sdk/mcp/server.ts +46 -18
- package/src/sdk/protocol/operation-inventory.generated.json +76 -0
- package/src/sdk/protocol/operation-registry.ts +8 -2
- package/src/sdk/session.ts +25 -2
- package/src/sdk/startup-capability.ts +6 -0
- package/src/session/agent-session.ts +720 -190
- package/src/session/fallback-chain-controller.ts +47 -8
- package/src/session/internal/managed-session-scope.ts +40 -10
- package/src/session/internal/native-publish-outcome.ts +8 -3
- package/src/session/memory-guard-checkpoint-participant.ts +131 -0
- package/src/session/session-manager.ts +1372 -42
- package/src/session/streaming-output.ts +262 -43
- package/src/setup/provider-onboarding.ts +19 -4
- package/src/skill-state/active-state.ts +0 -10
- package/src/skill-state/workflow-hud.ts +25 -2
- package/src/skill-state/workflow-mutation-guard.ts +65 -7
- package/src/task/executor.ts +11 -3
- package/src/task/index.ts +2 -0
- package/src/task/types.ts +22 -11
- package/src/task/ultragoal-redteam-activation.ts +75 -0
- package/src/tools/ask.ts +89 -69
- package/src/tools/browser/launch.ts +41 -6
- package/src/tools/fetch.ts +114 -23
- package/src/tools/output-meta.ts +176 -9
- package/src/tools/path-utils.ts +5 -1
- package/src/tools/read-internals.ts +44 -0
- package/src/tools/read.ts +1542 -310
- package/src/tools/resource-gc.ts +665 -125
- package/src/tools/tool-result.ts +14 -1
- package/src/web/search/providers/codex.ts +1 -1
- package/src/workflow/workflow-intent-diff.ts +4 -1
- package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
- package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
- package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
- package/src/gjc-runtime/deep-interview-draft.ts +0 -843
- package/src/gjc-runtime/deep-interview-payload.ts +0 -262
- package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
package/src/sdk/bus/index.ts
CHANGED
|
@@ -31,9 +31,11 @@ import { ThinkingLevel } from "@gajae-code/agent-core";
|
|
|
31
31
|
import type { ImageContent, TextContent, Tool } from "@gajae-code/ai";
|
|
32
32
|
import { NotificationServer, nativeBuildInfo } from "@gajae-code/natives";
|
|
33
33
|
import { logger, postmortem, VERSION } from "@gajae-code/utils";
|
|
34
|
-
import { projectModelProfileCatalog } from "../../config/model-profile-contract";
|
|
34
|
+
import { isModelProfileProviderAvailable, projectModelProfileCatalog } from "../../config/model-profile-contract";
|
|
35
|
+
import { isAuthenticated, kNoAuth } from "../../config/model-registry";
|
|
35
36
|
import { Settings } from "../../config/settings";
|
|
36
37
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
|
|
38
|
+
import { INTERACTIVE_SELECTOR_RESUME_ORIGIN } from "../../extensibility/shared-events";
|
|
37
39
|
import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
|
|
38
40
|
import {
|
|
39
41
|
NotificationGatePolicyChangedError,
|
|
@@ -54,6 +56,7 @@ import type {
|
|
|
54
56
|
AskSettlementResult,
|
|
55
57
|
} from "../../tools";
|
|
56
58
|
import { registerAskAnswerSource, registerWorkflowGateEmitterListener } from "../../tools/ask-answer-registry";
|
|
59
|
+
import { acpFinalTextFromMessage } from "../acp/final-text";
|
|
57
60
|
import { ensureBroker } from "../broker/ensure";
|
|
58
61
|
import { SessionIndex } from "../broker/session-index";
|
|
59
62
|
import { SessionSdkHost, shouldHostSdk } from "../host";
|
|
@@ -80,9 +83,16 @@ import {
|
|
|
80
83
|
sessionTag,
|
|
81
84
|
} from "./config";
|
|
82
85
|
import { telegramControlCommandUsage } from "./config-commands";
|
|
86
|
+
import {
|
|
87
|
+
isNativeControlDrainAvailable,
|
|
88
|
+
runIdentityControlSuccessPath,
|
|
89
|
+
type TerminalSendOutcome,
|
|
90
|
+
} from "./control-drain-lease";
|
|
83
91
|
import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage, truncate } from "./helpers";
|
|
92
|
+
import { createKindAwareReconciliation } from "./kind-aware-reconciliation";
|
|
84
93
|
import { assertNativeRuntimeCompatibility } from "./native-runtime-compatibility";
|
|
85
94
|
import { createPromptReconciliation, sanitizePromptFailure } from "./prompt-reconciliation";
|
|
95
|
+
import { createReconciliationStore } from "./reconciliation-store";
|
|
86
96
|
import { NotificationSessionController, type NotificationSessionRuntime } from "./session-control";
|
|
87
97
|
import {
|
|
88
98
|
ASK_SELECTED_ACK_CAPABILITY,
|
|
@@ -93,6 +103,17 @@ import {
|
|
|
93
103
|
unregisterNotificationRoot,
|
|
94
104
|
} from "./telegram-daemon";
|
|
95
105
|
|
|
106
|
+
export type {
|
|
107
|
+
IdentityControlSuccessPathInput,
|
|
108
|
+
IdentityControlTerminalPathInput,
|
|
109
|
+
TerminalSendOutcome,
|
|
110
|
+
} from "./control-drain-lease";
|
|
111
|
+
export {
|
|
112
|
+
isNativeControlDrainAvailable,
|
|
113
|
+
runIdentityControlSuccessPath,
|
|
114
|
+
runIdentityControlTerminalPath,
|
|
115
|
+
} from "./control-drain-lease";
|
|
116
|
+
|
|
96
117
|
// ===========================================================================
|
|
97
118
|
// Session lifecycle control protocol (TypeScript mirror of the Rust wire
|
|
98
119
|
// contract in `crates/gjc-sdk/src/lifecycle.rs`).
|
|
@@ -884,6 +905,8 @@ interface SessionRuntime {
|
|
|
884
905
|
disposeGateListener: () => void;
|
|
885
906
|
/** Whether notification-only delivery and answer resources are active. */
|
|
886
907
|
notificationsActive: boolean;
|
|
908
|
+
/** Rejects new SDK frames while a leased terminal response drains. */
|
|
909
|
+
inboundFenced: boolean;
|
|
887
910
|
/** Set as soon as terminal teardown is requested, before startup settles. */
|
|
888
911
|
stopping: boolean;
|
|
889
912
|
/** Recreates notification-only resources after `/notify on`. */
|
|
@@ -935,7 +958,13 @@ interface SessionRuntime {
|
|
|
935
958
|
emitPromptLifecycle: (
|
|
936
959
|
correlation: { commandId: string; turnId: string } | undefined,
|
|
937
960
|
frame:
|
|
938
|
-
| {
|
|
961
|
+
| {
|
|
962
|
+
type: "agent_start" | "agent_end";
|
|
963
|
+
sessionId: string;
|
|
964
|
+
commandId?: string;
|
|
965
|
+
turnId?: string;
|
|
966
|
+
finalText?: string;
|
|
967
|
+
}
|
|
939
968
|
| {
|
|
940
969
|
type: "agent_failed";
|
|
941
970
|
sessionId: string;
|
|
@@ -975,7 +1004,10 @@ interface SessionRuntime {
|
|
|
975
1004
|
/** SDK control frames received during provisional ownership; replayed only after stable activation. */
|
|
976
1005
|
deferredInboundControls: Array<() => void>;
|
|
977
1006
|
/** Started tool calls awaiting a terminal activity frame, keyed by tool call id. */
|
|
978
|
-
inFlightTools: Map<
|
|
1007
|
+
inFlightTools: Map<
|
|
1008
|
+
string,
|
|
1009
|
+
{ toolName: string; args?: unknown; pendingPhase?: "completed" | "failed" | "cancelled" }
|
|
1010
|
+
>;
|
|
979
1011
|
/** Cancels the postmortem cleanup that emits `session_closed` on process teardown. */
|
|
980
1012
|
cancelPostmortemCleanup: () => void;
|
|
981
1013
|
/** Disposes side-turn resources when their owning logical session becomes unavailable. */
|
|
@@ -1105,7 +1137,7 @@ const defaultConfig: NotificationConfig = {
|
|
|
1105
1137
|
idleTimeoutMs: 60_000,
|
|
1106
1138
|
rich: { enabled: true },
|
|
1107
1139
|
richDraft: { enabled: false },
|
|
1108
|
-
toolActivity: { enabled:
|
|
1140
|
+
toolActivity: { enabled: false },
|
|
1109
1141
|
streaming: { enabled: true },
|
|
1110
1142
|
topics: {},
|
|
1111
1143
|
btw: { enabled: true },
|
|
@@ -1698,6 +1730,7 @@ const UNINSTALLED_CONTROL_OPERATIONS = new Set(["auth.login", "host_tools.regist
|
|
|
1698
1730
|
|
|
1699
1731
|
const CONTROL_BINDINGS: Readonly<Record<string, string | undefined>> = {
|
|
1700
1732
|
"model.cycle": "cycleModel",
|
|
1733
|
+
"model.profile.set": "setModelProfile",
|
|
1701
1734
|
"thinking.cycle": "cycleThinkingLevel",
|
|
1702
1735
|
"queue.steering_mode.set": "setQueueMode",
|
|
1703
1736
|
"queue.follow_up_mode.set": "setQueueMode",
|
|
@@ -1795,6 +1828,9 @@ function sdkQuerySurface(
|
|
|
1795
1828
|
}),
|
|
1796
1829
|
configOverrides: ReadonlyMap<string, unknown> = new Map(),
|
|
1797
1830
|
promptStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown,
|
|
1831
|
+
skillStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown = () => ({
|
|
1832
|
+
status: "unknown",
|
|
1833
|
+
}),
|
|
1798
1834
|
): SessionSurface {
|
|
1799
1835
|
const metadata = () => ({
|
|
1800
1836
|
sessionId: id,
|
|
@@ -1862,8 +1898,26 @@ function sdkQuerySurface(
|
|
|
1862
1898
|
const currentThinkingLevel = api.getThinkingLevel();
|
|
1863
1899
|
return projectQ10Models({ models, currentModel, currentThinkingLevel });
|
|
1864
1900
|
},
|
|
1865
|
-
getModelProfiles: () =>
|
|
1866
|
-
|
|
1901
|
+
getModelProfiles: async () => {
|
|
1902
|
+
const profiles = ctx.modelRegistry.getModelProfiles();
|
|
1903
|
+
const catalog = projectModelProfileCatalog(profiles, ctx.modelRegistry.getError());
|
|
1904
|
+
const providers = new Set([...profiles.values()].flatMap(profile => profile.requiredProviders));
|
|
1905
|
+
const authenticatedProviders = new Set<string>();
|
|
1906
|
+
await Promise.all(
|
|
1907
|
+
[...providers].map(async provider => {
|
|
1908
|
+
try {
|
|
1909
|
+
const credential = await ctx.modelRegistry.getApiKeyForProvider(provider, id);
|
|
1910
|
+
if (credential === kNoAuth || isAuthenticated(credential)) authenticatedProviders.add(provider);
|
|
1911
|
+
} catch {
|
|
1912
|
+
// A provider whose credential state cannot be read is not currently configurable.
|
|
1913
|
+
}
|
|
1914
|
+
}),
|
|
1915
|
+
);
|
|
1916
|
+
return catalog.map(item => ({
|
|
1917
|
+
...item,
|
|
1918
|
+
available: isModelProfileProviderAvailable(profiles.get(item.id)!, authenticatedProviders),
|
|
1919
|
+
}));
|
|
1920
|
+
},
|
|
1867
1921
|
getSkillState: () => ctx.getSkillState(),
|
|
1868
1922
|
getGates: () => {
|
|
1869
1923
|
const workflowGate = ctx.workflowGate;
|
|
@@ -1905,6 +1959,8 @@ function sdkQuerySurface(
|
|
|
1905
1959
|
getJobs: () => ctx.getJobs(),
|
|
1906
1960
|
getPromptStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
1907
1961
|
promptStatusLookup(selector),
|
|
1962
|
+
getSkillInvokeStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
1963
|
+
skillStatusLookup(selector),
|
|
1908
1964
|
installedQueries: installedOperations(ctx, "query"),
|
|
1909
1965
|
};
|
|
1910
1966
|
}
|
|
@@ -1932,7 +1988,7 @@ function sdkControlSurface(
|
|
|
1932
1988
|
requesterConnectionId?: string,
|
|
1933
1989
|
clientRef?: string,
|
|
1934
1990
|
trackReconciliation?: boolean,
|
|
1935
|
-
) => void = () => {},
|
|
1991
|
+
) => void | Promise<void> = () => {},
|
|
1936
1992
|
onPromptFailed: (correlation: { commandId: string; turnId: string }, error: unknown) => void = () => {},
|
|
1937
1993
|
acceptGateResolution: () => boolean,
|
|
1938
1994
|
trackGateResolution: <T>(resolution: Promise<T>) => Promise<T>,
|
|
@@ -1941,6 +1997,20 @@ function sdkControlSurface(
|
|
|
1941
1997
|
settings?: Settings,
|
|
1942
1998
|
configOverrides: Map<string, unknown> = new Map(),
|
|
1943
1999
|
configRevision: { current: number } = { current: 0 },
|
|
2000
|
+
skillRecon?: {
|
|
2001
|
+
admit: (clientRef?: string) => void;
|
|
2002
|
+
release: (clientRef?: string) => void;
|
|
2003
|
+
noteAccepted: (
|
|
2004
|
+
correlation: { commandId: string; turnId: string },
|
|
2005
|
+
clientRef?: string,
|
|
2006
|
+
extra?: { skillName?: string },
|
|
2007
|
+
) => Promise<void>;
|
|
2008
|
+
noteTransition: (
|
|
2009
|
+
correlation: { commandId: string; turnId: string } | undefined,
|
|
2010
|
+
frame: { type: "agent_start" | "agent_end" } | { type: "agent_failed"; error: unknown },
|
|
2011
|
+
) => Promise<void>;
|
|
2012
|
+
lookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown;
|
|
2013
|
+
},
|
|
1944
2014
|
): ControlSurface {
|
|
1945
2015
|
const unavailable = (operation: string, reason: string) => () => {
|
|
1946
2016
|
throw Object.assign(new Error(`${operation} is unavailable: ${reason}`), { code: "unavailable" });
|
|
@@ -2071,12 +2141,17 @@ function sdkControlSurface(
|
|
|
2071
2141
|
error: Object.assign(new Error("Prompt preflight was cancelled before execution."), { code: "busy" }),
|
|
2072
2142
|
});
|
|
2073
2143
|
pendingPreflightCancellations.add(cancelPreflight);
|
|
2074
|
-
const
|
|
2144
|
+
const settleAccepted = async () => {
|
|
2075
2145
|
if (preflightSettled) return;
|
|
2076
2146
|
accepted = true;
|
|
2077
|
-
onPromptAccepted(correlation, requesterConnectionId, trimmedClientRef, trackReconciliation);
|
|
2147
|
+
await onPromptAccepted(correlation, requesterConnectionId, trimmedClientRef, trackReconciliation);
|
|
2078
2148
|
settlePreflight({ status: "accepted" });
|
|
2079
2149
|
};
|
|
2150
|
+
// Durable fence preferred; keep legacy onPreflightAccepted for hosts/tests that only fire the sync hook.
|
|
2151
|
+
const onPreflightAcceptCommit = settleAccepted;
|
|
2152
|
+
const onPreflightAccepted = () => {
|
|
2153
|
+
void settleAccepted();
|
|
2154
|
+
};
|
|
2080
2155
|
// Do not acknowledge the prompt until AgentSession's async preflight
|
|
2081
2156
|
// succeeds. The terminal result records correlation before agent_start can fire.
|
|
2082
2157
|
let submission: Promise<void> | undefined;
|
|
@@ -2084,6 +2159,7 @@ function sdkControlSurface(
|
|
|
2084
2159
|
submission = Promise.resolve(
|
|
2085
2160
|
api.sendUserMessage(content, {
|
|
2086
2161
|
...(deliverAs ? { deliverAs } : !forceFresh && isBusy() ? { deliverAs: "steer" as const } : {}),
|
|
2162
|
+
onPreflightAcceptCommit,
|
|
2087
2163
|
onPreflightAccepted,
|
|
2088
2164
|
}),
|
|
2089
2165
|
);
|
|
@@ -2294,13 +2370,84 @@ function sdkControlSurface(
|
|
|
2294
2370
|
code: "terminal_uncertain",
|
|
2295
2371
|
});
|
|
2296
2372
|
},
|
|
2297
|
-
invokeSkill: (name, args) => {
|
|
2373
|
+
invokeSkill: async (name, args, clientRef) => {
|
|
2298
2374
|
if (!bindings.has("invokeSkill") || !ctx.invokeSkill)
|
|
2299
2375
|
return unavailable("skill.invoke", "no skill invocation seam is installed")();
|
|
2300
2376
|
|
|
2301
2377
|
if (typeof args !== "undefined" && typeof args !== "string")
|
|
2302
2378
|
throw Object.assign(new Error("skill.invoke args must be a string."), { code: "invalid_input" });
|
|
2303
|
-
|
|
2379
|
+
const trimmedClientRef =
|
|
2380
|
+
typeof clientRef === "string" ? clientRef.trim() : clientRef === undefined ? undefined : "";
|
|
2381
|
+
if (clientRef !== undefined && (!trimmedClientRef || trimmedClientRef.length > 128))
|
|
2382
|
+
throw Object.assign(new Error("clientRef must be a non-empty string of at most 128 characters."), {
|
|
2383
|
+
code: "invalid_input",
|
|
2384
|
+
});
|
|
2385
|
+
const commandId = crypto.randomUUID();
|
|
2386
|
+
const turnId = crypto.randomUUID();
|
|
2387
|
+
const correlation = { commandId, turnId };
|
|
2388
|
+
if (skillRecon) skillRecon.admit(trimmedClientRef);
|
|
2389
|
+
const { promise: acceptedP, resolve, reject } = Promise.withResolvers<Record<string, unknown>>();
|
|
2390
|
+
let phase: "pending" | "accepted" | "rejected" = "pending";
|
|
2391
|
+
const settleAccept = (value: Record<string, unknown>) => {
|
|
2392
|
+
if (phase !== "pending") return;
|
|
2393
|
+
phase = "accepted";
|
|
2394
|
+
resolve(value);
|
|
2395
|
+
};
|
|
2396
|
+
const settleReject = (error: unknown) => {
|
|
2397
|
+
if (phase !== "pending") return;
|
|
2398
|
+
phase = "rejected";
|
|
2399
|
+
reject(error);
|
|
2400
|
+
};
|
|
2401
|
+
let prepared: { name: string; path: string; lineCount?: number; cleanedArgs?: string } | undefined;
|
|
2402
|
+
const run = Promise.resolve(
|
|
2403
|
+
ctx.invokeSkill(name, args as string | undefined, {
|
|
2404
|
+
onSkillPrepared: meta => {
|
|
2405
|
+
prepared = meta;
|
|
2406
|
+
},
|
|
2407
|
+
onPreflightAcceptCommit: async () => {
|
|
2408
|
+
const meta = prepared ?? { name: String(name), path: "" };
|
|
2409
|
+
if (skillRecon)
|
|
2410
|
+
await skillRecon.noteAccepted(correlation, trimmedClientRef, { skillName: meta.name });
|
|
2411
|
+
settleAccept({
|
|
2412
|
+
accepted: true,
|
|
2413
|
+
commandId,
|
|
2414
|
+
turnId,
|
|
2415
|
+
name: meta.name,
|
|
2416
|
+
path: meta.path,
|
|
2417
|
+
...(meta.lineCount !== undefined ? { lineCount: meta.lineCount } : {}),
|
|
2418
|
+
...(meta.cleanedArgs !== undefined ? { args: meta.cleanedArgs } : {}),
|
|
2419
|
+
...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}),
|
|
2420
|
+
});
|
|
2421
|
+
},
|
|
2422
|
+
}),
|
|
2423
|
+
).then(
|
|
2424
|
+
result => {
|
|
2425
|
+
if (phase === "pending") {
|
|
2426
|
+
// Completed without fence (e.g. queue path) — still surface result.
|
|
2427
|
+
settleAccept({
|
|
2428
|
+
accepted: true,
|
|
2429
|
+
commandId,
|
|
2430
|
+
turnId,
|
|
2431
|
+
...(typeof result === "object" && result ? (result as object) : {}),
|
|
2432
|
+
...(trimmedClientRef ? { clientRef: trimmedClientRef } : {}),
|
|
2433
|
+
});
|
|
2434
|
+
} else if (skillRecon) {
|
|
2435
|
+
void skillRecon.noteTransition(correlation, { type: "agent_end" });
|
|
2436
|
+
}
|
|
2437
|
+
return result;
|
|
2438
|
+
},
|
|
2439
|
+
error => {
|
|
2440
|
+
if (phase === "pending") {
|
|
2441
|
+
if (skillRecon) skillRecon.release(trimmedClientRef);
|
|
2442
|
+
settleReject(error);
|
|
2443
|
+
} else if (skillRecon) {
|
|
2444
|
+
void skillRecon.noteTransition(correlation, { type: "agent_failed", error });
|
|
2445
|
+
}
|
|
2446
|
+
throw error;
|
|
2447
|
+
},
|
|
2448
|
+
);
|
|
2449
|
+
void run.catch(() => {});
|
|
2450
|
+
return await acceptedP;
|
|
2304
2451
|
},
|
|
2305
2452
|
setPlanMode: async on => {
|
|
2306
2453
|
if (!bindings.has("setPlanMode") || !ctx.setPlanMode)
|
|
@@ -2333,6 +2480,14 @@ function sdkControlSurface(
|
|
|
2333
2480
|
);
|
|
2334
2481
|
return typed("model.set", { id: `${model.provider}/${model.id}`, thinkingLevel });
|
|
2335
2482
|
},
|
|
2483
|
+
setModelProfile: async id => {
|
|
2484
|
+
if (!bindings.has("setModelProfile") || !ctx.setModelProfile)
|
|
2485
|
+
return unavailable("model.profile.set", "no model-profile activation seam is installed")();
|
|
2486
|
+
if (!id) throw Object.assign(new Error("model.profile.set requires a profile id."), { code: "invalid_input" });
|
|
2487
|
+
if (!ctx.modelRegistry.getModelProfile(id))
|
|
2488
|
+
throw Object.assign(new Error(`Model profile ${id} was not found.`), { code: "invalid_input" });
|
|
2489
|
+
return { changed: await ctx.setModelProfile(id), id };
|
|
2490
|
+
},
|
|
2336
2491
|
cycleModel: async () => {
|
|
2337
2492
|
if (!bindings.has("cycleModel"))
|
|
2338
2493
|
return unavailable("model.cycle", "no session model-cycle seam is installed")();
|
|
@@ -2828,6 +2983,18 @@ export async function ensureConfiguredProviderDaemons(
|
|
|
2828
2983
|
if (isSlackConfigured(cfg)) await ensureProviderDaemon("slack", settings);
|
|
2829
2984
|
}
|
|
2830
2985
|
|
|
2986
|
+
/**
|
|
2987
|
+
* Classify whether a session identity event must await notification endpoint startup.
|
|
2988
|
+
* Only an interactive selector resume may defer this ancillary startup; all other
|
|
2989
|
+
* events, including branches and unknown origins, fail closed to awaiting it.
|
|
2990
|
+
*/
|
|
2991
|
+
export function shouldAwaitNotificationStartup(event: {
|
|
2992
|
+
type: "session_switch" | "session_branch";
|
|
2993
|
+
transition?: { origin: string };
|
|
2994
|
+
}): boolean {
|
|
2995
|
+
return event.type !== "session_switch" || event.transition?.origin !== INTERACTIVE_SELECTOR_RESUME_ORIGIN;
|
|
2996
|
+
}
|
|
2997
|
+
|
|
2831
2998
|
export function createNotificationsExtension(
|
|
2832
2999
|
api: ExtensionAPI,
|
|
2833
3000
|
options: {
|
|
@@ -2867,10 +3034,15 @@ export function createNotificationsExtension(
|
|
|
2867
3034
|
const cleanupRetries = new Map<string, SessionRuntime>();
|
|
2868
3035
|
const sessionStartPromises = new Map<string, Promise<SessionStartResult>>();
|
|
2869
3036
|
const branchStartupTasks = new Set<Promise<void>>();
|
|
3037
|
+
const sessionLifecycleTasks = new Set<Promise<void>>();
|
|
2870
3038
|
let activeRuntimeId: string | undefined;
|
|
2871
3039
|
let identityControlInFlight = false;
|
|
2872
3040
|
let deferredIdentityRotation:
|
|
2873
|
-
| {
|
|
3041
|
+
| {
|
|
3042
|
+
event: { previousSessionFile?: string; transition?: { origin: string } };
|
|
3043
|
+
ctx: ExtensionContext;
|
|
3044
|
+
awaitStartup: boolean;
|
|
3045
|
+
}
|
|
2874
3046
|
| undefined;
|
|
2875
3047
|
let extensionShuttingDown = false;
|
|
2876
3048
|
|
|
@@ -2931,6 +3103,7 @@ export function createNotificationsExtension(
|
|
|
2931
3103
|
const requestedRuntime = retryRuntime ?? activeRuntime;
|
|
2932
3104
|
if (expectedRuntime && requestedRuntime !== expectedRuntime) return false;
|
|
2933
3105
|
if (reason === "session" && requestedRuntime) {
|
|
3106
|
+
requestedRuntime.inboundFenced = true;
|
|
2934
3107
|
requestedRuntime.stopping = true;
|
|
2935
3108
|
requestedRuntime.abortEphemeralTurns();
|
|
2936
3109
|
}
|
|
@@ -3219,7 +3392,13 @@ export function createNotificationsExtension(
|
|
|
3219
3392
|
const promptSubmissionKey = (correlation: { commandId: string; turnId: string }) =>
|
|
3220
3393
|
`${correlation.commandId}:${correlation.turnId}`;
|
|
3221
3394
|
type PromptLifecycleFrame =
|
|
3222
|
-
| {
|
|
3395
|
+
| {
|
|
3396
|
+
type: "agent_start" | "agent_end";
|
|
3397
|
+
sessionId: string;
|
|
3398
|
+
commandId?: string;
|
|
3399
|
+
turnId?: string;
|
|
3400
|
+
finalText?: string;
|
|
3401
|
+
}
|
|
3223
3402
|
| {
|
|
3224
3403
|
type: "agent_failed";
|
|
3225
3404
|
sessionId: string;
|
|
@@ -3244,10 +3423,29 @@ export function createNotificationsExtension(
|
|
|
3244
3423
|
// (contract documented in ./prompt-reconciliation and ../prompt-status).
|
|
3245
3424
|
// Active records never age into terminal; documented TTL/capacity
|
|
3246
3425
|
// eviction is the only removal, after which lookups report `unknown`.
|
|
3426
|
+
const sessionFile =
|
|
3427
|
+
typeof ctx.sessionManager?.getSessionFile === "function" ? ctx.sessionManager.getSessionFile() : null;
|
|
3428
|
+
const reconciliationSessionId =
|
|
3429
|
+
typeof ctx.sessionManager?.getSessionId === "function" ? ctx.sessionManager.getSessionId() : "";
|
|
3430
|
+
const durableStore =
|
|
3431
|
+
sessionFile && reconciliationSessionId
|
|
3432
|
+
? createReconciliationStore({ sessionFile, sessionId: String(reconciliationSessionId) })
|
|
3433
|
+
: null;
|
|
3434
|
+
const kindReconciliation = createKindAwareReconciliation({ store: durableStore });
|
|
3435
|
+
if (durableStore) void kindReconciliation.hydrateFromStore();
|
|
3436
|
+
// Backward-compatible process-local prompt reconciler kept for unit-test isolation;
|
|
3437
|
+
// production path uses kindReconciliation for prompt+skill.
|
|
3247
3438
|
const reconciliation = createPromptReconciliation();
|
|
3248
|
-
const admitPromptSubmission =
|
|
3249
|
-
const notePromptReconciliationAccepted =
|
|
3250
|
-
|
|
3439
|
+
const admitPromptSubmission = (clientRef?: string) => kindReconciliation.admit("prompt", clientRef);
|
|
3440
|
+
const notePromptReconciliationAccepted = async (
|
|
3441
|
+
correlation: { commandId: string; turnId: string },
|
|
3442
|
+
clientRef?: string,
|
|
3443
|
+
) => {
|
|
3444
|
+
await kindReconciliation.noteAccepted("prompt", correlation, clientRef);
|
|
3445
|
+
};
|
|
3446
|
+
const lookupPromptStatus = (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
|
|
3447
|
+
kindReconciliation.lookup("prompt", selector);
|
|
3448
|
+
const releasePromptAdmission = (clientRef?: string) => kindReconciliation.releaseAdmission("prompt", clientRef);
|
|
3251
3449
|
const removePendingPromptCorrelation = (correlation: { commandId: string; turnId: string }) => {
|
|
3252
3450
|
const pendingIndex = pendingPromptCorrelations.findIndex(
|
|
3253
3451
|
candidate => candidate.commandId === correlation.commandId && candidate.turnId === correlation.turnId,
|
|
@@ -3274,6 +3472,7 @@ export function createNotificationsExtension(
|
|
|
3274
3472
|
addTerminalTombstone(key);
|
|
3275
3473
|
};
|
|
3276
3474
|
const cleanupPromptRecords = (now = Date.now()) => {
|
|
3475
|
+
kindReconciliation.cleanup();
|
|
3277
3476
|
reconciliation.cleanup();
|
|
3278
3477
|
for (const [key, expiresAt] of promptTerminalTombstones)
|
|
3279
3478
|
if (expiresAt <= now) promptTerminalTombstones.delete(key);
|
|
@@ -3352,7 +3551,7 @@ export function createNotificationsExtension(
|
|
|
3352
3551
|
if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
|
|
3353
3552
|
}
|
|
3354
3553
|
};
|
|
3355
|
-
const recordPromptAccepted = (
|
|
3554
|
+
const recordPromptAccepted = async (
|
|
3356
3555
|
correlation: { commandId: string; turnId: string },
|
|
3357
3556
|
requesterConnectionId?: string,
|
|
3358
3557
|
clientRef?: string,
|
|
@@ -3361,10 +3560,11 @@ export function createNotificationsExtension(
|
|
|
3361
3560
|
if (!requesterConnectionId) {
|
|
3362
3561
|
// No delivery owner: tracked prompts cannot be reconciled. Release
|
|
3363
3562
|
// their admission reservation instead of leaking the active slot.
|
|
3364
|
-
if (trackReconciliation)
|
|
3563
|
+
if (trackReconciliation) releasePromptAdmission(clientRef);
|
|
3365
3564
|
return;
|
|
3366
3565
|
}
|
|
3367
|
-
|
|
3566
|
+
// Register delivery ownership synchronously before any await so post-accept
|
|
3567
|
+
// failures that race the durable write still emit correlated terminals.
|
|
3368
3568
|
cleanupPromptRecords();
|
|
3369
3569
|
while (promptSubmissions.size >= PROMPT_SUBMISSION_CAPACITY) {
|
|
3370
3570
|
const oldest = promptSubmissions.entries().next().value as [string, PromptSubmission] | undefined;
|
|
@@ -3383,6 +3583,7 @@ export function createNotificationsExtension(
|
|
|
3383
3583
|
createdAt: Date.now(),
|
|
3384
3584
|
bufferedFrames: [],
|
|
3385
3585
|
});
|
|
3586
|
+
if (trackReconciliation) await notePromptReconciliationAccepted(correlation, clientRef);
|
|
3386
3587
|
};
|
|
3387
3588
|
const recordPromptTerminal = (correlation: { commandId: string; turnId: string } | undefined) => {
|
|
3388
3589
|
if (!correlation) return false;
|
|
@@ -3396,7 +3597,7 @@ export function createNotificationsExtension(
|
|
|
3396
3597
|
};
|
|
3397
3598
|
const emitPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
|
|
3398
3599
|
const sanitized = sanitizePromptFailure(error);
|
|
3399
|
-
|
|
3600
|
+
void kindReconciliation.noteTransition("prompt", correlation, { type: "agent_failed", error: sanitized });
|
|
3400
3601
|
const submission = promptSubmissions.get(promptSubmissionKey(correlation));
|
|
3401
3602
|
if (!submission || !runtime || !recordPromptTerminal(correlation)) return;
|
|
3402
3603
|
emitPromptLifecycle(correlation, {
|
|
@@ -3447,6 +3648,7 @@ export function createNotificationsExtension(
|
|
|
3447
3648
|
},
|
|
3448
3649
|
configOverrides,
|
|
3449
3650
|
lookupPromptStatus,
|
|
3651
|
+
selector => kindReconciliation.lookup("skill", selector),
|
|
3450
3652
|
),
|
|
3451
3653
|
id,
|
|
3452
3654
|
revisions,
|
|
@@ -3463,10 +3665,18 @@ export function createNotificationsExtension(
|
|
|
3463
3665
|
() => runtime?.stopping !== true,
|
|
3464
3666
|
trackGateResolution,
|
|
3465
3667
|
admitPromptSubmission,
|
|
3466
|
-
|
|
3668
|
+
releasePromptAdmission,
|
|
3467
3669
|
settings,
|
|
3468
3670
|
configOverrides,
|
|
3469
3671
|
configRevision,
|
|
3672
|
+
{
|
|
3673
|
+
admit: (clientRef?: string) => kindReconciliation.admit("skill", clientRef),
|
|
3674
|
+
release: (clientRef?: string) => kindReconciliation.releaseAdmission("skill", clientRef),
|
|
3675
|
+
noteAccepted: (correlation, clientRef, extra) =>
|
|
3676
|
+
kindReconciliation.noteAccepted("skill", correlation, clientRef, extra),
|
|
3677
|
+
noteTransition: (correlation, frame) => kindReconciliation.noteTransition("skill", correlation, frame),
|
|
3678
|
+
lookup: selector => kindReconciliation.lookup("skill", selector),
|
|
3679
|
+
},
|
|
3470
3680
|
);
|
|
3471
3681
|
const abandonPromptResponse = (connectionId: string, frame: Record<string, unknown>) => {
|
|
3472
3682
|
if (
|
|
@@ -3532,6 +3742,75 @@ export function createNotificationsExtension(
|
|
|
3532
3742
|
};
|
|
3533
3743
|
},
|
|
3534
3744
|
onRequest: options.onSdkRequest,
|
|
3745
|
+
beforeControlResponse: async (_connectionId, request, response, sendTerminal) => {
|
|
3746
|
+
if (typeof request.operation !== "string" || !identityControlOperations.has(request.operation)) return;
|
|
3747
|
+
const pending = deferredIdentityRotation;
|
|
3748
|
+
deferredIdentityRotation = undefined;
|
|
3749
|
+
identityControlInFlight = false;
|
|
3750
|
+
if (response.ok !== true || !pending) return;
|
|
3751
|
+
|
|
3752
|
+
const predecessorId = activeRuntimeId ?? sessionIdFromFile(pending.event.previousSessionFile);
|
|
3753
|
+
if (!predecessorId) throw new Error("notifications: identity control predecessor is unknown.");
|
|
3754
|
+
let terminalAttempted = false;
|
|
3755
|
+
let terminalOutcome: TerminalSendOutcome | undefined;
|
|
3756
|
+
try {
|
|
3757
|
+
terminalOutcome = await runIdentityControlSuccessPath({
|
|
3758
|
+
fence: () => {
|
|
3759
|
+
const predecessor = runtimes.get(predecessorId);
|
|
3760
|
+
if (!predecessor || predecessor.stopping || predecessor.serverStopped)
|
|
3761
|
+
throw new Error(`notifications: predecessor runtime ${predecessorId} cannot be fenced.`);
|
|
3762
|
+
predecessor.inboundFenced = true;
|
|
3763
|
+
},
|
|
3764
|
+
ensurePredecessorSendCapable: () => {
|
|
3765
|
+
const predecessor = runtimes.get(predecessorId);
|
|
3766
|
+
if (!predecessor || predecessor.stopping || predecessor.serverStopped)
|
|
3767
|
+
throw new Error(`notifications: predecessor runtime ${predecessorId} is not send-capable.`);
|
|
3768
|
+
},
|
|
3769
|
+
startSuccessor: async () => {
|
|
3770
|
+
const successorId = sessionId(pending.ctx);
|
|
3771
|
+
try {
|
|
3772
|
+
await rotateSessionAuthority(pending.event, pending.ctx, true, {
|
|
3773
|
+
deferPredecessorStop: true,
|
|
3774
|
+
});
|
|
3775
|
+
const successor = runtimes.get(successorId);
|
|
3776
|
+
if (!successor?.host.started || activeRuntimeId !== successorId)
|
|
3777
|
+
throw new Error(`notifications: successor runtime ${successorId} was not ready.`);
|
|
3778
|
+
} catch (error) {
|
|
3779
|
+
(response as Record<string, unknown>).ok = false;
|
|
3780
|
+
delete (response as Record<string, unknown>).result;
|
|
3781
|
+
(response as Record<string, unknown>).error = {
|
|
3782
|
+
code: "unavailable",
|
|
3783
|
+
message: error instanceof Error ? error.message : "Successor session was not ready.",
|
|
3784
|
+
};
|
|
3785
|
+
}
|
|
3786
|
+
},
|
|
3787
|
+
sendTerminal: async () => {
|
|
3788
|
+
terminalAttempted = true;
|
|
3789
|
+
try {
|
|
3790
|
+
await sendTerminal();
|
|
3791
|
+
return "written";
|
|
3792
|
+
} catch {
|
|
3793
|
+
return "write_failed";
|
|
3794
|
+
}
|
|
3795
|
+
},
|
|
3796
|
+
stopPredecessor: async () => {
|
|
3797
|
+
try {
|
|
3798
|
+
await stopSession(predecessorId);
|
|
3799
|
+
} catch (error) {
|
|
3800
|
+
if (!terminalAttempted) throw error;
|
|
3801
|
+
logger.error(`notifications: deferred predecessor cleanup failed: ${String(error)}`);
|
|
3802
|
+
}
|
|
3803
|
+
},
|
|
3804
|
+
requireNativeControlDrain:
|
|
3805
|
+
(request as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true,
|
|
3806
|
+
});
|
|
3807
|
+
} catch (error) {
|
|
3808
|
+
if (!terminalAttempted) throw error;
|
|
3809
|
+
logger.error(
|
|
3810
|
+
`notifications: identity terminal delivery failed (${terminalOutcome ?? "unknown"}): ${String(error)}`,
|
|
3811
|
+
);
|
|
3812
|
+
}
|
|
3813
|
+
},
|
|
3535
3814
|
afterControlResponse: async (connectionId, request, response) => {
|
|
3536
3815
|
if (
|
|
3537
3816
|
(request.operation === "turn.prompt" ||
|
|
@@ -3552,13 +3831,6 @@ export function createNotificationsExtension(
|
|
|
3552
3831
|
}
|
|
3553
3832
|
|
|
3554
3833
|
if (request.operation === "session.close" && response.ok === true) ctx.shutdown();
|
|
3555
|
-
if (typeof request.operation === "string" && identityControlOperations.has(request.operation)) {
|
|
3556
|
-
const pending = deferredIdentityRotation;
|
|
3557
|
-
deferredIdentityRotation = undefined;
|
|
3558
|
-
identityControlInFlight = false;
|
|
3559
|
-
if (response.ok === true && pending)
|
|
3560
|
-
await rotateSessionAuthority(pending.event, pending.ctx, pending.awaitStartup);
|
|
3561
|
-
}
|
|
3562
3834
|
},
|
|
3563
3835
|
control: async (connectionId, frame) => {
|
|
3564
3836
|
const request = frame as {
|
|
@@ -3578,6 +3850,21 @@ export function createNotificationsExtension(
|
|
|
3578
3850
|
ok: false,
|
|
3579
3851
|
error: { code: "conflict", message: "session identity mutation is already active" },
|
|
3580
3852
|
};
|
|
3853
|
+
const requireNativeControlDrain =
|
|
3854
|
+
(request as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true ||
|
|
3855
|
+
(!!request.input &&
|
|
3856
|
+
typeof request.input === "object" &&
|
|
3857
|
+
!Array.isArray(request.input) &&
|
|
3858
|
+
(request.input as { requireNativeControlDrain?: unknown }).requireNativeControlDrain === true);
|
|
3859
|
+
if (requireNativeControlDrain && !isNativeControlDrainAvailable())
|
|
3860
|
+
return {
|
|
3861
|
+
id: requestId,
|
|
3862
|
+
ok: false,
|
|
3863
|
+
error: {
|
|
3864
|
+
code: "unavailable",
|
|
3865
|
+
message: "SDK identity control requires the native control-drain lease.",
|
|
3866
|
+
},
|
|
3867
|
+
};
|
|
3581
3868
|
if (rotatesIdentity) identityControlInFlight = true;
|
|
3582
3869
|
const response = await controlRequesterContext.run(connectionId, () =>
|
|
3583
3870
|
dispatchControl(
|
|
@@ -3649,6 +3936,7 @@ export function createNotificationsExtension(
|
|
|
3649
3936
|
workflowGate: undefined,
|
|
3650
3937
|
gatePresentations,
|
|
3651
3938
|
stopping: false,
|
|
3939
|
+
inboundFenced: false,
|
|
3652
3940
|
abortEphemeralTurns: () => {},
|
|
3653
3941
|
disableEphemeralTurns: () => {},
|
|
3654
3942
|
cancelPostmortemCleanup: () => {},
|
|
@@ -3664,12 +3952,17 @@ export function createNotificationsExtension(
|
|
|
3664
3952
|
pendingPromptCorrelations,
|
|
3665
3953
|
activePromptCorrelation: undefined,
|
|
3666
3954
|
recordPromptTerminal,
|
|
3667
|
-
notePromptReconciliation:
|
|
3955
|
+
notePromptReconciliation: (correlation, frame) => {
|
|
3956
|
+
void kindReconciliation.noteTransition("prompt", correlation, frame);
|
|
3957
|
+
},
|
|
3668
3958
|
emitPromptFailure,
|
|
3669
3959
|
emitPromptLifecycle,
|
|
3670
3960
|
emitPromptEvent,
|
|
3671
3961
|
pendingInbound: new Set<number>(),
|
|
3672
|
-
inFlightTools: new Map<
|
|
3962
|
+
inFlightTools: new Map<
|
|
3963
|
+
string,
|
|
3964
|
+
{ toolName: string; args?: unknown; pendingPhase?: "completed" | "failed" | "cancelled" }
|
|
3965
|
+
>(),
|
|
3673
3966
|
deferredGatePresentations: [],
|
|
3674
3967
|
deferredInboundControls: [],
|
|
3675
3968
|
notificationRootRegistration: undefined,
|
|
@@ -3727,6 +4020,22 @@ export function createNotificationsExtension(
|
|
|
3727
4020
|
const frame = JSON.parse(inbound.json) as unknown;
|
|
3728
4021
|
if (!frame || typeof frame !== "object") return;
|
|
3729
4022
|
const typedFrame = frame as Record<string, unknown>;
|
|
4023
|
+
if (initializedRuntime.inboundFenced) {
|
|
4024
|
+
if (typedFrame.type === "control_request" && typeof typedFrame.id === "string") {
|
|
4025
|
+
try {
|
|
4026
|
+
server.sendTo(
|
|
4027
|
+
inbound.connectionId,
|
|
4028
|
+
JSON.stringify({
|
|
4029
|
+
type: "control_response",
|
|
4030
|
+
id: typedFrame.id,
|
|
4031
|
+
ok: false,
|
|
4032
|
+
error: { code: "endpoint_stale", message: "session endpoint is draining." },
|
|
4033
|
+
}),
|
|
4034
|
+
);
|
|
4035
|
+
} catch {}
|
|
4036
|
+
}
|
|
4037
|
+
return;
|
|
4038
|
+
}
|
|
3730
4039
|
if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
|
|
3731
4040
|
if (typedFrame.type === "event_replay") {
|
|
3732
4041
|
const capabilities = Array.isArray(typedFrame.capabilities) ? typedFrame.capabilities : [];
|
|
@@ -3761,7 +4070,12 @@ export function createNotificationsExtension(
|
|
|
3761
4070
|
|
|
3762
4071
|
server.onReply((err, reply) => {
|
|
3763
4072
|
if (err || !reply) return;
|
|
3764
|
-
if (
|
|
4073
|
+
if (
|
|
4074
|
+
runtime?.inboundFenced ||
|
|
4075
|
+
runtime?.stopping ||
|
|
4076
|
+
runtime?.policySuspended ||
|
|
4077
|
+
runtimes.get(id) !== runtime
|
|
4078
|
+
) {
|
|
3765
4079
|
try {
|
|
3766
4080
|
server.closeClaimInvalid(reply.replyReceiptId, "session_stopping");
|
|
3767
4081
|
} catch {}
|
|
@@ -4003,6 +4317,7 @@ export function createNotificationsExtension(
|
|
|
4003
4317
|
// thread (forwarded by the daemon over the WS, fail-closed at the daemon).
|
|
4004
4318
|
server.onInbound((err, inbound) => {
|
|
4005
4319
|
if (err || !inbound) return;
|
|
4320
|
+
if (initializedRuntime.inboundFenced) return;
|
|
4006
4321
|
const authenticatedInbound = inbound as typeof inbound & {
|
|
4007
4322
|
connectionId: string;
|
|
4008
4323
|
messageId?: number;
|
|
@@ -4103,7 +4418,7 @@ export function createNotificationsExtension(
|
|
|
4103
4418
|
}
|
|
4104
4419
|
if (typeof inbound.redact === "boolean") {
|
|
4105
4420
|
if (inbound.redact && !runtime.committedRedact) {
|
|
4106
|
-
terminalizeInFlightTools(runtime, runtime.id, "
|
|
4421
|
+
terminalizeInFlightTools(runtime, runtime.id, "cancelled");
|
|
4107
4422
|
}
|
|
4108
4423
|
runtime.committedRedact = inbound.redact;
|
|
4109
4424
|
runtime.redact = inbound.redact;
|
|
@@ -4450,8 +4765,15 @@ export function createNotificationsExtension(
|
|
|
4450
4765
|
runtime.redact = true;
|
|
4451
4766
|
runtime.verbosity = "lean";
|
|
4452
4767
|
runtime.stream = false;
|
|
4768
|
+
for (const [toolCallId, tool] of runtime.inFlightTools) {
|
|
4769
|
+
runtime.inFlightTools.set(toolCallId, {
|
|
4770
|
+
toolName: tool.toolName,
|
|
4771
|
+
...(tool.pendingPhase ? { pendingPhase: tool.pendingPhase } : {}),
|
|
4772
|
+
});
|
|
4773
|
+
}
|
|
4453
4774
|
return;
|
|
4454
4775
|
}
|
|
4776
|
+
const wasPolicySuspended = runtime.policySuspended;
|
|
4455
4777
|
const redactionEnabled = policy.redact && !runtime.committedRedact;
|
|
4456
4778
|
runtime.policyGeneration++;
|
|
4457
4779
|
runtime.committedRedact = policy.redact;
|
|
@@ -4459,7 +4781,8 @@ export function createNotificationsExtension(
|
|
|
4459
4781
|
runtime.redact = policy.redact;
|
|
4460
4782
|
runtime.verbosity = policy.verbosity;
|
|
4461
4783
|
runtime.stream = policy.stream;
|
|
4462
|
-
if (redactionEnabled) terminalizeInFlightTools(runtime, runtime.id, "
|
|
4784
|
+
if (redactionEnabled) terminalizeInFlightTools(runtime, runtime.id, "cancelled", true);
|
|
4785
|
+
else if (wasPolicySuspended && !policy.redact) settleProvisionalToolTerminals(runtime, runtime.id);
|
|
4463
4786
|
},
|
|
4464
4787
|
activate: binding => {
|
|
4465
4788
|
const runtime = runtimes.get(binding.sessionId);
|
|
@@ -4587,7 +4910,16 @@ export function createNotificationsExtension(
|
|
|
4587
4910
|
};
|
|
4588
4911
|
|
|
4589
4912
|
api.on("session_start", async (_event, ctx) => {
|
|
4590
|
-
|
|
4913
|
+
const task = startAndReconcileSession(ctx);
|
|
4914
|
+
// Track full start+reconcile so settled startups join replacement-token
|
|
4915
|
+
// reconcile before owner release. Pending native startup (/notify on) stays
|
|
4916
|
+
// nonblocking: shutdown only awaits these tasks when sessionStartPromises is clear.
|
|
4917
|
+
sessionLifecycleTasks.add(task);
|
|
4918
|
+
try {
|
|
4919
|
+
await task;
|
|
4920
|
+
} finally {
|
|
4921
|
+
sessionLifecycleTasks.delete(task);
|
|
4922
|
+
}
|
|
4591
4923
|
});
|
|
4592
4924
|
|
|
4593
4925
|
// A session endpoint's token and generation are authority for exactly one
|
|
@@ -4633,10 +4965,27 @@ export function createNotificationsExtension(
|
|
|
4633
4965
|
});
|
|
4634
4966
|
};
|
|
4635
4967
|
|
|
4968
|
+
const preparePredecessorForTerminal = async (id: string): Promise<void> => {
|
|
4969
|
+
const predecessor = runtimes.get(id);
|
|
4970
|
+
if (!predecessor || cleanupRetries.has(id))
|
|
4971
|
+
throw new Error(`notifications: predecessor runtime ${id} is not safely send-capable.`);
|
|
4972
|
+
predecessor.inboundFenced = true;
|
|
4973
|
+
// Release broker/host authority while leaving the native server alive for
|
|
4974
|
+
// the one accepted terminal response. stopAndWait is deferred to stopSession.
|
|
4975
|
+
const stopped = await predecessor.host.stop();
|
|
4976
|
+
if (stopped !== "stopped" && predecessor.host.started)
|
|
4977
|
+
throw new Error(`notifications: predecessor runtime ${id} host release was not proven.`);
|
|
4978
|
+
predecessor.hostStopped = true;
|
|
4979
|
+
predecessor.brokerRegistrationReleased = !predecessor.brokerRegistrationActive || predecessor.hostStopped;
|
|
4980
|
+
if (predecessor.brokerRegistrationActive && predecessor.hostStopped) predecessor.brokerRegistrationActive = false;
|
|
4981
|
+
predecessor.host.reverse.dispose();
|
|
4982
|
+
};
|
|
4983
|
+
|
|
4636
4984
|
const rotateSessionAuthority = async (
|
|
4637
4985
|
event: { previousSessionFile?: string },
|
|
4638
4986
|
ctx: ExtensionContext,
|
|
4639
4987
|
awaitStartup: boolean,
|
|
4988
|
+
options: { deferPredecessorStop?: boolean } = {},
|
|
4640
4989
|
): Promise<void> => {
|
|
4641
4990
|
if (extensionShuttingDown) return;
|
|
4642
4991
|
const newId = sessionId(ctx);
|
|
@@ -4645,7 +4994,11 @@ export function createNotificationsExtension(
|
|
|
4645
4994
|
const pendingStartup = sessionStartPromises.get(newId);
|
|
4646
4995
|
if (pendingStartup) {
|
|
4647
4996
|
if (awaitStartup) {
|
|
4648
|
-
await pendingStartup;
|
|
4997
|
+
const result = await pendingStartup;
|
|
4998
|
+
if (!lifecycleStartupCapability && result.status === "failed")
|
|
4999
|
+
throw new Error(
|
|
5000
|
+
`notifications: SDK startup failed: ${result.failure?.message ?? "Unknown startup failure."}`,
|
|
5001
|
+
);
|
|
4649
5002
|
if (!extensionShuttingDown && runtimes.has(newId) && activeRuntimeId === newId)
|
|
4650
5003
|
await controller.reconcileCurrentSession(ctx);
|
|
4651
5004
|
} else {
|
|
@@ -4660,14 +5013,14 @@ export function createNotificationsExtension(
|
|
|
4660
5013
|
}
|
|
4661
5014
|
if (prevId && prevId !== newId) {
|
|
4662
5015
|
controller.rekeySession(prevId, newId);
|
|
4663
|
-
|
|
4664
|
-
await
|
|
4665
|
-
}
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
5016
|
+
if (options.deferPredecessorStop) {
|
|
5017
|
+
await preparePredecessorForTerminal(prevId);
|
|
5018
|
+
} else {
|
|
5019
|
+
const stopped = await stopSession(prevId);
|
|
5020
|
+
if (cleanupRetries.has(prevId) || runtimes.has(prevId) || sessionStartPromises.has(prevId))
|
|
5021
|
+
throw new Error(`notifications: predecessor runtime ${prevId} release is uncertain.`);
|
|
5022
|
+
if (!stopped && activeRuntimeId === prevId)
|
|
5023
|
+
throw new Error(`notifications: predecessor runtime ${prevId} release was not proven.`);
|
|
4671
5024
|
}
|
|
4672
5025
|
}
|
|
4673
5026
|
if (extensionShuttingDown) return;
|
|
@@ -4688,22 +5041,37 @@ export function createNotificationsExtension(
|
|
|
4688
5041
|
trackBranchStartup(newId, ctx, startup);
|
|
4689
5042
|
};
|
|
4690
5043
|
api.on("session_switch", async (event, ctx) => {
|
|
5044
|
+
const awaitStartup = shouldAwaitNotificationStartup(event);
|
|
4691
5045
|
if (identityControlInFlight) {
|
|
4692
|
-
deferredIdentityRotation = { event, ctx, awaitStartup
|
|
5046
|
+
deferredIdentityRotation = { event, ctx, awaitStartup };
|
|
4693
5047
|
return;
|
|
4694
5048
|
}
|
|
4695
|
-
await rotateSessionAuthority(event, ctx,
|
|
5049
|
+
await rotateSessionAuthority(event, ctx, awaitStartup);
|
|
4696
5050
|
});
|
|
4697
5051
|
api.on("session_branch", async (event, ctx) => {
|
|
4698
5052
|
if (identityControlInFlight) {
|
|
4699
|
-
deferredIdentityRotation = { event, ctx, awaitStartup:
|
|
5053
|
+
deferredIdentityRotation = { event, ctx, awaitStartup: true };
|
|
4700
5054
|
return;
|
|
4701
5055
|
}
|
|
4702
|
-
await rotateSessionAuthority(event, ctx,
|
|
5056
|
+
await rotateSessionAuthority(event, ctx, true);
|
|
4703
5057
|
});
|
|
4704
5058
|
|
|
4705
|
-
const terminalizeInFlightTools = (
|
|
4706
|
-
|
|
5059
|
+
const terminalizeInFlightTools = (
|
|
5060
|
+
rt: SessionRuntime,
|
|
5061
|
+
id: string,
|
|
5062
|
+
phase: "cancelled" | "failed",
|
|
5063
|
+
allowSafeRedactedFrame = false,
|
|
5064
|
+
): void => {
|
|
5065
|
+
if (rt.policySuspended && !allowSafeRedactedFrame) {
|
|
5066
|
+
for (const [toolCallId, tool] of rt.inFlightTools) {
|
|
5067
|
+
rt.inFlightTools.set(toolCallId, {
|
|
5068
|
+
toolName: tool.toolName,
|
|
5069
|
+
pendingPhase: tool.pendingPhase ?? phase,
|
|
5070
|
+
});
|
|
5071
|
+
}
|
|
5072
|
+
return;
|
|
5073
|
+
}
|
|
5074
|
+
if (rt.notificationsActive && (!rt.redact || allowSafeRedactedFrame)) {
|
|
4707
5075
|
for (const [toolCallId, { toolName }] of rt.inFlightTools) {
|
|
4708
5076
|
try {
|
|
4709
5077
|
pushSessionFrame(rt, { type: "tool_activity", sessionId: id, toolCallId, toolName, phase });
|
|
@@ -4715,6 +5083,27 @@ export function createNotificationsExtension(
|
|
|
4715
5083
|
rt.inFlightTools.clear();
|
|
4716
5084
|
};
|
|
4717
5085
|
|
|
5086
|
+
const settleProvisionalToolTerminals = (rt: SessionRuntime, id: string): void => {
|
|
5087
|
+
for (const [toolCallId, tool] of rt.inFlightTools) {
|
|
5088
|
+
if (!tool.pendingPhase) continue;
|
|
5089
|
+
try {
|
|
5090
|
+
if (rt.notificationsActive && !rt.redact) {
|
|
5091
|
+
pushSessionFrame(rt, {
|
|
5092
|
+
type: "tool_activity",
|
|
5093
|
+
sessionId: id,
|
|
5094
|
+
toolCallId,
|
|
5095
|
+
toolName: tool.toolName,
|
|
5096
|
+
phase: tool.pendingPhase,
|
|
5097
|
+
});
|
|
5098
|
+
}
|
|
5099
|
+
} catch (e) {
|
|
5100
|
+
logger.warn(`notifications: provisional tool_activity settlement failed: ${String(e)}`);
|
|
5101
|
+
} finally {
|
|
5102
|
+
rt.inFlightTools.delete(toolCallId);
|
|
5103
|
+
}
|
|
5104
|
+
}
|
|
5105
|
+
};
|
|
5106
|
+
|
|
4718
5107
|
const resetTurnStreamState = (rt: SessionRuntime): void => {
|
|
4719
5108
|
rt.currentTurnText = undefined;
|
|
4720
5109
|
rt.preAskFlushedText = undefined;
|
|
@@ -4851,14 +5240,28 @@ export function createNotificationsExtension(
|
|
|
4851
5240
|
});
|
|
4852
5241
|
else {
|
|
4853
5242
|
rt.notePromptReconciliation(correlation, { type: "agent_end" });
|
|
4854
|
-
if (rt.recordPromptTerminal(correlation))
|
|
4855
|
-
|
|
5243
|
+
if (rt.recordPromptTerminal(correlation)) {
|
|
5244
|
+
const finalAssistant = (Array.isArray(event.messages) ? [...event.messages].reverse() : []).find(
|
|
5245
|
+
message =>
|
|
5246
|
+
message &&
|
|
5247
|
+
typeof message === "object" &&
|
|
5248
|
+
(message as { role?: unknown }).role === "assistant" &&
|
|
5249
|
+
(message as { stopReason?: unknown }).stopReason === "stop",
|
|
5250
|
+
);
|
|
5251
|
+
const finalText = finalAssistant ? acpFinalTextFromMessage(finalAssistant).text : "";
|
|
5252
|
+
rt.emitPromptLifecycle(correlation, {
|
|
5253
|
+
type: "agent_end",
|
|
5254
|
+
sessionId: id,
|
|
5255
|
+
...correlation,
|
|
5256
|
+
...(finalText ? { finalText } : {}),
|
|
5257
|
+
});
|
|
5258
|
+
}
|
|
4856
5259
|
}
|
|
4857
5260
|
} else {
|
|
4858
5261
|
rt.emitPromptLifecycle(undefined, { type: "agent_end", sessionId: id });
|
|
4859
5262
|
}
|
|
4860
5263
|
rt.activePromptCorrelation = undefined;
|
|
4861
|
-
terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "
|
|
5264
|
+
terminalizeInFlightTools(rt, id, event.stopReason === "cancelled" ? "cancelled" : "failed");
|
|
4862
5265
|
try {
|
|
4863
5266
|
pushSessionFrame(rt, { type: "activity", sessionId: id, state: "idle" });
|
|
4864
5267
|
} catch (e) {
|
|
@@ -4976,14 +5379,13 @@ export function createNotificationsExtension(
|
|
|
4976
5379
|
// path and ordered before it — unlike message_update, which is queued async),
|
|
4977
5380
|
// then flushed here before the ask tool's execute calls registerAsk.
|
|
4978
5381
|
api.on("tool_execution_start", (event, ctx) => {
|
|
5382
|
+
const id = sessionId(ctx);
|
|
5383
|
+
const rt = runtimes.get(id);
|
|
5384
|
+
rt?.emitPromptEvent(event);
|
|
4979
5385
|
if (event.toolName === "ask") {
|
|
4980
|
-
const id = sessionId(ctx);
|
|
4981
|
-
const rt = runtimes.get(id);
|
|
4982
5386
|
if (!rt?.notificationsActive || rt.redact) return;
|
|
4983
5387
|
flushTurnText(rt, id, rt.currentTurnText, false);
|
|
4984
5388
|
}
|
|
4985
|
-
const id = sessionId(ctx);
|
|
4986
|
-
const rt = runtimes.get(id);
|
|
4987
5389
|
if (!rt?.notificationsActive || rt.redact) return;
|
|
4988
5390
|
rt.inFlightTools.set(event.toolCallId, { toolName: event.toolName, args: event.args });
|
|
4989
5391
|
try {
|
|
@@ -4999,12 +5401,32 @@ export function createNotificationsExtension(
|
|
|
4999
5401
|
}
|
|
5000
5402
|
});
|
|
5001
5403
|
|
|
5404
|
+
api.on("tool_execution_update", (event, ctx) => {
|
|
5405
|
+
const rt = runtimes.get(sessionId(ctx));
|
|
5406
|
+
rt?.emitPromptEvent(event);
|
|
5407
|
+
});
|
|
5408
|
+
|
|
5002
5409
|
api.on("tool_execution_end", (event, ctx) => {
|
|
5003
5410
|
const id = sessionId(ctx);
|
|
5004
5411
|
const rt = runtimes.get(id);
|
|
5005
5412
|
if (!rt) return;
|
|
5413
|
+
rt.emitPromptEvent(event);
|
|
5006
5414
|
const inFlight = rt.inFlightTools.get(event.toolCallId);
|
|
5007
|
-
if (!
|
|
5415
|
+
if (!inFlight) return;
|
|
5416
|
+
if (!rt.notificationsActive) {
|
|
5417
|
+
rt.inFlightTools.delete(event.toolCallId);
|
|
5418
|
+
return;
|
|
5419
|
+
}
|
|
5420
|
+
if (rt.policySuspended) {
|
|
5421
|
+
if (!inFlight.pendingPhase) {
|
|
5422
|
+
rt.inFlightTools.set(event.toolCallId, {
|
|
5423
|
+
toolName: inFlight.toolName,
|
|
5424
|
+
pendingPhase: event.isError ? "failed" : "completed",
|
|
5425
|
+
});
|
|
5426
|
+
}
|
|
5427
|
+
return;
|
|
5428
|
+
}
|
|
5429
|
+
if (rt.redact) {
|
|
5008
5430
|
rt.inFlightTools.delete(event.toolCallId);
|
|
5009
5431
|
return;
|
|
5010
5432
|
}
|
|
@@ -5169,13 +5591,29 @@ export function createNotificationsExtension(
|
|
|
5169
5591
|
extensionShuttingDown = true;
|
|
5170
5592
|
identityControlInFlight = false;
|
|
5171
5593
|
deferredIdentityRotation = undefined;
|
|
5172
|
-
await Promise.allSettled([...branchStartupTasks]);
|
|
5173
5594
|
const id = sessionId(ctx);
|
|
5595
|
+
// Join settled start+reconcile before owner release. Keep pending native
|
|
5596
|
+
// startup (/notify on) nonblocking — do not await sessionLifecycleTasks while
|
|
5597
|
+
// sessionStartPromises still has this id.
|
|
5598
|
+
if (!sessionStartPromises.has(id)) await Promise.allSettled([...sessionLifecycleTasks]);
|
|
5599
|
+
await Promise.allSettled([...branchStartupTasks]);
|
|
5174
5600
|
const rt = runtimes.get(id);
|
|
5175
|
-
if (rt) terminalizeInFlightTools(rt, id, "
|
|
5601
|
+
if (rt) terminalizeInFlightTools(rt, id, "cancelled");
|
|
5602
|
+
// Startup is only genuinely in flight when a `sessionStartPromises` entry
|
|
5603
|
+
// exists. Once startup has settled, the host is broker-visible and its
|
|
5604
|
+
// post-start `reconcileCurrentSession` may already have minted a
|
|
5605
|
+
// replacement notification-root token whose unregister is still awaiting
|
|
5606
|
+
// its file lock and atomic registry write. Returning before that settles
|
|
5607
|
+
// leaves a stale `sessions[id]` row that the retained older token is
|
|
5608
|
+
// correctly fenced from removing, so shutdown must join it.
|
|
5609
|
+
const startupWasPending = sessionStartPromises.has(id);
|
|
5176
5610
|
const controllerStop =
|
|
5177
5611
|
typeof ctx.sessionManager.getCwd === "function" ? controller.stopCurrentSession(ctx) : Promise.resolve(false);
|
|
5178
|
-
|
|
5612
|
+
const settledControllerStop = controllerStop.catch(error => {
|
|
5613
|
+
logger.warn(`notifications: controller shutdown failed: ${String(error)}`);
|
|
5614
|
+
return false;
|
|
5615
|
+
});
|
|
5616
|
+
if (startupWasPending) void settledControllerStop;
|
|
5179
5617
|
try {
|
|
5180
5618
|
await stopSession(id);
|
|
5181
5619
|
} catch (error) {
|
|
@@ -5186,5 +5624,10 @@ export function createNotificationsExtension(
|
|
|
5186
5624
|
// error severity (matching the postmortem cleanup precedent).
|
|
5187
5625
|
logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
|
|
5188
5626
|
}
|
|
5627
|
+
// Keep shutdown nonblocking only while native startup is genuinely
|
|
5628
|
+
// pending (the `/notify on` path); otherwise await the controller queue so
|
|
5629
|
+
// completed-start reconciliation and its replacement-token cleanup are
|
|
5630
|
+
// joined before lifecycle shutdown returns.
|
|
5631
|
+
if (!startupWasPending) await settledControllerStop;
|
|
5189
5632
|
});
|
|
5190
5633
|
}
|