@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.
Files changed (249) hide show
  1. package/CHANGELOG.md +86 -2
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/cli.d.ts +8 -1
  4. package/dist/types/commands/deep-interview.d.ts +9 -0
  5. package/dist/types/commands/harness.d.ts +22 -0
  6. package/dist/types/commands/read.d.ts +6 -0
  7. package/dist/types/config/model-profile-activation.d.ts +1 -0
  8. package/dist/types/config/model-profile-contract.d.ts +2 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/provider-auth-health.d.ts +14 -0
  11. package/dist/types/config/provider-ranking.d.ts +59 -0
  12. package/dist/types/config/settings-schema.d.ts +86 -3
  13. package/dist/types/config/settings.d.ts +3 -0
  14. package/dist/types/extensibility/extensions/types.d.ts +43 -2
  15. package/dist/types/extensibility/shared-events.d.ts +6 -0
  16. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  17. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  18. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  19. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  20. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  21. package/dist/types/gjc-runtime/linux-proc.d.ts +22 -14
  22. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  23. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +112 -1
  24. package/dist/types/gjc-runtime/state-runtime.d.ts +3 -1
  25. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  26. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  27. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  28. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  29. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  30. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  31. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  32. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  33. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  34. package/dist/types/main.d.ts +1 -1
  35. package/dist/types/memories/index.d.ts +2 -0
  36. package/dist/types/modes/acp/acp-agent.d.ts +22 -9
  37. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  38. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  39. package/dist/types/modes/components/queue-pane.d.ts +8 -0
  40. package/dist/types/modes/components/queued-message-selector.d.ts +6 -0
  41. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  42. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  43. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  44. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  45. package/dist/types/modes/components/welcome.d.ts +1 -0
  46. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  47. package/dist/types/modes/interactive-mode.d.ts +7 -1
  48. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  49. package/dist/types/modes/types.d.ts +2 -0
  50. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  51. package/dist/types/runtime/memory-domain.d.ts +9 -0
  52. package/dist/types/runtime/memory-guard-contract.d.ts +71 -0
  53. package/dist/types/runtime/memory-guard.d.ts +39 -0
  54. package/dist/types/runtime/memory-limit.d.ts +11 -0
  55. package/dist/types/runtime-mcp/manager.d.ts +8 -0
  56. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  57. package/dist/types/sdk/acp/final-text.d.ts +19 -0
  58. package/dist/types/sdk/acp/mcp.d.ts +23 -0
  59. package/dist/types/sdk/broker/lifecycle.d.ts +7 -0
  60. package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
  61. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  62. package/dist/types/sdk/bus/index.d.ts +18 -2
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  65. package/dist/types/sdk/bus/rate-limit-pool.d.ts +2 -0
  66. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  67. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -4
  68. package/dist/types/sdk/bus/telegram-daemon.d.ts +23 -3
  69. package/dist/types/sdk/host/control/operations.d.ts +2 -1
  70. package/dist/types/sdk/host/host.d.ts +2 -0
  71. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  72. package/dist/types/sdk/lifecycle-session.d.ts +10 -1
  73. package/dist/types/sdk/startup-capability.d.ts +6 -0
  74. package/dist/types/session/agent-session.d.ts +51 -19
  75. package/dist/types/session/fallback-chain-controller.d.ts +18 -2
  76. package/dist/types/session/internal/managed-session-scope.d.ts +27 -0
  77. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  78. package/dist/types/session/session-manager.d.ts +59 -13
  79. package/dist/types/session/streaming-output.d.ts +60 -5
  80. package/dist/types/skill-state/active-state.d.ts +0 -4
  81. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  82. package/dist/types/task/executor.d.ts +6 -0
  83. package/dist/types/task/types.d.ts +36 -0
  84. package/dist/types/task/ultragoal-redteam-activation.d.ts +33 -0
  85. package/dist/types/tools/browser/launch.d.ts +7 -0
  86. package/dist/types/tools/fetch.d.ts +17 -1
  87. package/dist/types/tools/output-meta.d.ts +21 -1
  88. package/dist/types/tools/read-internals.d.ts +12 -0
  89. package/dist/types/tools/read.d.ts +45 -1
  90. package/dist/types/tools/resource-gc.d.ts +23 -0
  91. package/dist/types/tools/tool-result.d.ts +7 -1
  92. package/package.json +9 -13
  93. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  94. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  95. package/scripts/generate-sdk-operation-inventory.ts +4 -0
  96. package/scripts/run-test-manifest.ts +6 -4
  97. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  98. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  99. package/src/capability/index.ts +8 -0
  100. package/src/cli/args.ts +14 -8
  101. package/src/cli/fast-help.ts +1 -1
  102. package/src/cli/read-cli.ts +2 -1
  103. package/src/cli.ts +49 -2
  104. package/src/commands/deep-interview.ts +29 -9
  105. package/src/commands/harness.ts +35 -12
  106. package/src/commands/read.ts +11 -2
  107. package/src/commands/sdk.ts +91 -7
  108. package/src/commands/team.ts +1 -1
  109. package/src/config/model-profile-activation.ts +1 -0
  110. package/src/config/model-profile-contract.ts +13 -0
  111. package/src/config/model-profiles.ts +24 -0
  112. package/src/config/model-registry.ts +21 -2
  113. package/src/config/provider-auth-health.ts +42 -0
  114. package/src/config/provider-ranking.ts +119 -0
  115. package/src/config/settings-schema.ts +84 -3
  116. package/src/config/settings.ts +254 -108
  117. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  118. package/src/defaults/gjc/skills/ralplan/SKILL.md +78 -26
  119. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  120. package/src/defaults/gjc/skills/ultragoal/SKILL.md +26 -16
  121. package/src/eval/py/executor.ts +7 -2
  122. package/src/extensibility/extensions/runner.ts +4 -0
  123. package/src/extensibility/extensions/types.ts +42 -4
  124. package/src/extensibility/shared-events.ts +5 -0
  125. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -68
  126. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  127. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  128. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  129. package/src/gjc-runtime/deep-interview-state.ts +12 -921
  130. package/src/gjc-runtime/gc-render.ts +1 -0
  131. package/src/gjc-runtime/gc-runtime.ts +11 -1
  132. package/src/gjc-runtime/linux-proc.ts +70 -36
  133. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  134. package/src/gjc-runtime/ralplan-runtime.ts +979 -71
  135. package/src/gjc-runtime/state-runtime.ts +50 -12
  136. package/src/gjc-runtime/state-writer.ts +70 -222
  137. package/src/gjc-runtime/team-launch.ts +34 -5
  138. package/src/gjc-runtime/team-runtime.ts +970 -33
  139. package/src/gjc-runtime/team-store.ts +32 -0
  140. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  141. package/src/gjc-runtime/team-workers.ts +1 -0
  142. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  143. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  144. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  145. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  146. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  147. package/src/hooks/native-skill-hook.ts +17 -4
  148. package/src/hooks/skill-state.ts +1 -23
  149. package/src/internal-urls/docs-index.generated.ts +10 -11
  150. package/src/internal-urls/local-protocol.ts +23 -4
  151. package/src/internal-urls/local-root-gc.ts +127 -0
  152. package/src/main.ts +9 -5
  153. package/src/memories/index.ts +10 -0
  154. package/src/modes/DESIGN.md +47 -0
  155. package/src/modes/acp/acp-agent.ts +597 -67
  156. package/src/modes/acp/acp-event-mapper.ts +106 -12
  157. package/src/modes/acp/acp-mode.ts +39 -3
  158. package/src/modes/components/assistant-message.ts +123 -22
  159. package/src/modes/components/irc-sidebar.ts +176 -62
  160. package/src/modes/components/model-selector.ts +76 -5
  161. package/src/modes/components/notifications-settings-editor.ts +1 -1
  162. package/src/modes/components/oauth-selector.ts +42 -12
  163. package/src/modes/components/queue-pane.ts +55 -9
  164. package/src/modes/components/queued-message-selector.ts +53 -10
  165. package/src/modes/components/read-tool-group.ts +82 -14
  166. package/src/modes/components/session-observer-overlay.ts +187 -32
  167. package/src/modes/components/settings-selector.ts +60 -18
  168. package/src/modes/components/tool-execution.ts +138 -27
  169. package/src/modes/components/tool-status-header.ts +1 -4
  170. package/src/modes/components/welcome.ts +18 -3
  171. package/src/modes/controllers/command-controller.ts +4 -0
  172. package/src/modes/controllers/event-controller.ts +109 -27
  173. package/src/modes/controllers/extension-ui-controller.ts +23 -12
  174. package/src/modes/controllers/input-controller.ts +62 -6
  175. package/src/modes/controllers/selector-controller.ts +83 -37
  176. package/src/modes/interactive-mode.ts +127 -29
  177. package/src/modes/irc-observation-ledger.ts +71 -15
  178. package/src/modes/runtime-init.ts +20 -2
  179. package/src/modes/types.ts +2 -0
  180. package/src/modes/utils/ui-helpers.ts +94 -1
  181. package/src/prompts/agents/architect.md +9 -1
  182. package/src/prompts/agents/critic.md +9 -0
  183. package/src/prompts/agents/executor.md +1 -1
  184. package/src/prompts/system/system-prompt.md +0 -1
  185. package/src/prompts/tools/read.md +4 -2
  186. package/src/runtime/memory-domain.ts +66 -0
  187. package/src/runtime/memory-guard-contract.ts +73 -0
  188. package/src/runtime/memory-guard.ts +292 -0
  189. package/src/runtime/memory-limit.ts +50 -0
  190. package/src/runtime-mcp/manager.ts +26 -6
  191. package/src/runtime-mcp/smithery-auth.ts +17 -2
  192. package/src/runtime-mcp/smithery-connect.ts +11 -1
  193. package/src/sdk/acp/adapter.ts +8 -1
  194. package/src/sdk/acp/final-text.ts +40 -0
  195. package/src/sdk/acp/mcp.ts +26 -0
  196. package/src/sdk/broker/lifecycle.ts +107 -7
  197. package/src/sdk/bus/chat-daemon-cli.ts +2 -2
  198. package/src/sdk/bus/chat-daemon-control.ts +4 -3
  199. package/src/sdk/bus/config.ts +1 -1
  200. package/src/sdk/bus/control-drain-lease.ts +104 -0
  201. package/src/sdk/bus/index.ts +503 -60
  202. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  203. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  204. package/src/sdk/bus/rate-limit-pool.ts +8 -0
  205. package/src/sdk/bus/reconciliation-store.ts +224 -0
  206. package/src/sdk/bus/telegram-daemon-contract.ts +6 -4
  207. package/src/sdk/bus/telegram-daemon.ts +531 -53
  208. package/src/sdk/host/control/dispatch.ts +7 -1
  209. package/src/sdk/host/control/operations.ts +2 -1
  210. package/src/sdk/host/host.ts +25 -3
  211. package/src/sdk/host/query/handlers.ts +55 -0
  212. package/src/sdk/lifecycle-session.ts +19 -3
  213. package/src/sdk/mcp/server.ts +46 -18
  214. package/src/sdk/protocol/operation-inventory.generated.json +76 -0
  215. package/src/sdk/protocol/operation-registry.ts +8 -2
  216. package/src/sdk/session.ts +25 -2
  217. package/src/sdk/startup-capability.ts +6 -0
  218. package/src/session/agent-session.ts +720 -190
  219. package/src/session/fallback-chain-controller.ts +47 -8
  220. package/src/session/internal/managed-session-scope.ts +40 -10
  221. package/src/session/internal/native-publish-outcome.ts +8 -3
  222. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  223. package/src/session/session-manager.ts +1372 -42
  224. package/src/session/streaming-output.ts +262 -43
  225. package/src/setup/provider-onboarding.ts +19 -4
  226. package/src/skill-state/active-state.ts +0 -10
  227. package/src/skill-state/workflow-hud.ts +25 -2
  228. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  229. package/src/task/executor.ts +11 -3
  230. package/src/task/index.ts +2 -0
  231. package/src/task/types.ts +22 -11
  232. package/src/task/ultragoal-redteam-activation.ts +75 -0
  233. package/src/tools/ask.ts +89 -69
  234. package/src/tools/browser/launch.ts +41 -6
  235. package/src/tools/fetch.ts +114 -23
  236. package/src/tools/output-meta.ts +176 -9
  237. package/src/tools/path-utils.ts +5 -1
  238. package/src/tools/read-internals.ts +44 -0
  239. package/src/tools/read.ts +1542 -310
  240. package/src/tools/resource-gc.ts +665 -125
  241. package/src/tools/tool-result.ts +14 -1
  242. package/src/web/search/providers/codex.ts +1 -1
  243. package/src/workflow/workflow-intent-diff.ts +4 -1
  244. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  245. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  246. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  247. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  248. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  249. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -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
- | { type: "agent_start" | "agent_end"; sessionId: string; commandId?: string; turnId?: string }
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<string, { toolName: string; args: unknown }>;
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: true },
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
- projectModelProfileCatalog(ctx.modelRegistry.getModelProfiles(), ctx.modelRegistry.getError()),
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 onPreflightAccepted = () => {
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
- return ctx.invokeSkill(name, args);
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
- | { event: { previousSessionFile?: string }; ctx: ExtensionContext; awaitStartup: boolean }
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
- | { type: "agent_start" | "agent_end"; sessionId: string; commandId?: string; turnId?: string }
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 = reconciliation.admit;
3249
- const notePromptReconciliationAccepted = reconciliation.noteAccepted;
3250
- const lookupPromptStatus = reconciliation.lookup;
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) reconciliation.releaseAdmission(clientRef);
3563
+ if (trackReconciliation) releasePromptAdmission(clientRef);
3365
3564
  return;
3366
3565
  }
3367
- if (trackReconciliation) notePromptReconciliationAccepted(correlation, clientRef);
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
- reconciliation.noteTransition(correlation, { type: "agent_failed", error: sanitized });
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
- reconciliation.releaseAdmission,
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: reconciliation.noteTransition,
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<string, { toolName: string; args: unknown }>(),
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 (runtime?.stopping || runtime?.policySuspended || runtimes.get(id) !== runtime) {
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, "unknown");
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, "unknown");
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
- await startAndReconcileSession(ctx);
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
- try {
4664
- await stopSession(prevId);
4665
- } catch (error) {
4666
- // A retained owner-release failure keeps the exact runtime in
4667
- // cleanupRetries for an explicit later retry; log it rather than
4668
- // surfacing a red extension error
4669
- // while rotating session authority (/new, fork, resume, branch).
4670
- logger.error(`notifications: SDK notification runtime cleanup failed: ${String(error)}`);
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: true };
5046
+ deferredIdentityRotation = { event, ctx, awaitStartup };
4693
5047
  return;
4694
5048
  }
4695
- await rotateSessionAuthority(event, ctx, true);
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: false };
5053
+ deferredIdentityRotation = { event, ctx, awaitStartup: true };
4700
5054
  return;
4701
5055
  }
4702
- await rotateSessionAuthority(event, ctx, false);
5056
+ await rotateSessionAuthority(event, ctx, true);
4703
5057
  });
4704
5058
 
4705
- const terminalizeInFlightTools = (rt: SessionRuntime, id: string, phase: "cancelled" | "unknown"): void => {
4706
- if (rt.notificationsActive && !rt.redact) {
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
- rt.emitPromptLifecycle(correlation, { type: "agent_end", sessionId: id, ...correlation });
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" : "unknown");
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 (!rt.notificationsActive || rt.redact) {
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, "unknown");
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
- void controllerStop.catch(error => logger.warn(`notifications: controller shutdown failed: ${String(error)}`));
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
  }