@gajae-code/coding-agent 0.11.11 → 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 (182) hide show
  1. package/CHANGELOG.md +49 -3
  2. package/dist/types/cli/read-cli.d.ts +1 -0
  3. package/dist/types/commands/deep-interview.d.ts +9 -0
  4. package/dist/types/commands/harness.d.ts +22 -0
  5. package/dist/types/commands/read.d.ts +6 -0
  6. package/dist/types/config/model-registry.d.ts +4 -0
  7. package/dist/types/config/provider-auth-health.d.ts +14 -0
  8. package/dist/types/config/provider-ranking.d.ts +59 -0
  9. package/dist/types/config/settings-schema.d.ts +29 -1
  10. package/dist/types/config/settings.d.ts +2 -0
  11. package/dist/types/extensibility/extensions/types.d.ts +22 -2
  12. package/dist/types/extensibility/shared-events.d.ts +6 -0
  13. package/dist/types/gjc-runtime/deep-interview-ambiguity.d.ts +5 -12
  14. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +17 -32
  15. package/dist/types/gjc-runtime/deep-interview-stage.d.ts +37 -0
  16. package/dist/types/gjc-runtime/deep-interview-state.d.ts +4 -135
  17. package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
  18. package/dist/types/gjc-runtime/memory-guard-owner-claims.d.ts +47 -0
  19. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +52 -1
  20. package/dist/types/gjc-runtime/state-writer.d.ts +5 -46
  21. package/dist/types/gjc-runtime/team-launch.d.ts +1 -1
  22. package/dist/types/gjc-runtime/team-runtime.d.ts +2 -1
  23. package/dist/types/gjc-runtime/team-store.d.ts +12 -0
  24. package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +92 -0
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +7 -0
  26. package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +8 -0
  27. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -5
  28. package/dist/types/internal-urls/local-root-gc.d.ts +2 -0
  29. package/dist/types/main.d.ts +1 -1
  30. package/dist/types/memories/index.d.ts +2 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +8 -0
  32. package/dist/types/modes/components/assistant-message.d.ts +3 -1
  33. package/dist/types/modes/components/irc-sidebar.d.ts +41 -0
  34. package/dist/types/modes/components/read-tool-group.d.ts +4 -4
  35. package/dist/types/modes/components/session-observer-overlay.d.ts +4 -0
  36. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  37. package/dist/types/modes/components/tool-execution.d.ts +8 -0
  38. package/dist/types/modes/controllers/event-controller.d.ts +3 -0
  39. package/dist/types/modes/interactive-mode.d.ts +2 -0
  40. package/dist/types/modes/irc-observation-ledger.d.ts +17 -1
  41. package/dist/types/modes/types.d.ts +2 -0
  42. package/dist/types/modes/utils/ui-helpers.d.ts +16 -0
  43. package/dist/types/runtime-mcp/smithery-auth.d.ts +5 -0
  44. package/dist/types/sdk/broker/lifecycle.d.ts +5 -0
  45. package/dist/types/sdk/bus/control-drain-lease.d.ts +52 -0
  46. package/dist/types/sdk/bus/index.d.ts +15 -1
  47. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +37 -0
  48. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +2 -1
  49. package/dist/types/sdk/bus/reconciliation-store.d.ts +65 -0
  50. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  51. package/dist/types/sdk/host/host.d.ts +2 -0
  52. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  53. package/dist/types/session/agent-session.d.ts +44 -10
  54. package/dist/types/session/memory-guard-checkpoint-participant.d.ts +69 -0
  55. package/dist/types/session/session-manager.d.ts +25 -13
  56. package/dist/types/session/streaming-output.d.ts +60 -5
  57. package/dist/types/skill-state/active-state.d.ts +0 -4
  58. package/dist/types/skill-state/workflow-hud.d.ts +3 -0
  59. package/dist/types/tools/browser/launch.d.ts +7 -0
  60. package/dist/types/tools/fetch.d.ts +17 -1
  61. package/dist/types/tools/output-meta.d.ts +21 -1
  62. package/dist/types/tools/read-internals.d.ts +12 -0
  63. package/dist/types/tools/read.d.ts +45 -1
  64. package/dist/types/tools/resource-gc.d.ts +4 -0
  65. package/dist/types/tools/tool-result.d.ts +7 -1
  66. package/package.json +9 -13
  67. package/scripts/benchmark-sticky-viewport-pr1.ts +239 -0
  68. package/scripts/capture-sticky-viewport-showcase.ts +292 -0
  69. package/scripts/generate-sdk-operation-inventory.ts +2 -0
  70. package/scripts/verify-gjc-sdk-canonicalization.ts +122 -18
  71. package/scripts/verify-sticky-viewport-showcase.ts +611 -0
  72. package/src/capability/index.ts +8 -0
  73. package/src/cli/read-cli.ts +2 -1
  74. package/src/cli.ts +1 -0
  75. package/src/commands/deep-interview.ts +29 -9
  76. package/src/commands/harness.ts +35 -12
  77. package/src/commands/read.ts +11 -2
  78. package/src/commands/team.ts +1 -1
  79. package/src/config/model-registry.ts +21 -2
  80. package/src/config/provider-auth-health.ts +42 -0
  81. package/src/config/provider-ranking.ts +119 -0
  82. package/src/config/settings-schema.ts +23 -1
  83. package/src/config/settings.ts +246 -108
  84. package/src/defaults/gjc/skills/deep-interview/SKILL.md +229 -82
  85. package/src/defaults/gjc/skills/ralplan/SKILL.md +58 -25
  86. package/src/defaults/gjc/skills/team/SKILL.md +1 -1
  87. package/src/defaults/gjc/skills/ultragoal/SKILL.md +25 -15
  88. package/src/eval/py/executor.ts +7 -2
  89. package/src/extensibility/extensions/types.ts +28 -4
  90. package/src/extensibility/shared-events.ts +5 -0
  91. package/src/gjc-runtime/deep-interview-ambiguity.ts +47 -82
  92. package/src/gjc-runtime/deep-interview-recorder.ts +227 -310
  93. package/src/gjc-runtime/deep-interview-runtime.ts +109 -270
  94. package/src/gjc-runtime/deep-interview-stage.ts +1016 -0
  95. package/src/gjc-runtime/deep-interview-state.ts +12 -943
  96. package/src/gjc-runtime/gc-render.ts +1 -0
  97. package/src/gjc-runtime/gc-runtime.ts +11 -1
  98. package/src/gjc-runtime/memory-guard-owner-claims.ts +389 -0
  99. package/src/gjc-runtime/ralplan-runtime.ts +742 -85
  100. package/src/gjc-runtime/state-runtime.ts +2 -1
  101. package/src/gjc-runtime/state-writer.ts +70 -222
  102. package/src/gjc-runtime/team-launch.ts +34 -5
  103. package/src/gjc-runtime/team-runtime.ts +970 -33
  104. package/src/gjc-runtime/team-store.ts +32 -0
  105. package/src/gjc-runtime/team-worker-memory-guard.ts +363 -0
  106. package/src/gjc-runtime/team-workers.ts +1 -0
  107. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -0
  108. package/src/gjc-runtime/ultragoal-receipt-freshness.ts +20 -0
  109. package/src/gjc-runtime/ultragoal-runtime.ts +13 -7
  110. package/src/gjc-runtime/workflow-manifest.generated.json +97 -350
  111. package/src/gjc-runtime/workflow-manifest.ts +34 -179
  112. package/src/hooks/native-skill-hook.ts +17 -4
  113. package/src/hooks/skill-state.ts +1 -23
  114. package/src/internal-urls/docs-index.generated.ts +8 -9
  115. package/src/internal-urls/local-root-gc.ts +127 -0
  116. package/src/main.ts +9 -5
  117. package/src/memories/index.ts +10 -0
  118. package/src/modes/DESIGN.md +47 -0
  119. package/src/modes/acp/acp-agent.ts +43 -7
  120. package/src/modes/acp/acp-event-mapper.ts +14 -7
  121. package/src/modes/acp/acp-mode.ts +39 -3
  122. package/src/modes/components/assistant-message.ts +123 -22
  123. package/src/modes/components/irc-sidebar.ts +176 -62
  124. package/src/modes/components/model-selector.ts +61 -4
  125. package/src/modes/components/oauth-selector.ts +42 -12
  126. package/src/modes/components/read-tool-group.ts +82 -14
  127. package/src/modes/components/session-observer-overlay.ts +187 -32
  128. package/src/modes/components/settings-selector.ts +60 -18
  129. package/src/modes/components/tool-execution.ts +138 -27
  130. package/src/modes/controllers/event-controller.ts +109 -27
  131. package/src/modes/controllers/extension-ui-controller.ts +19 -2
  132. package/src/modes/controllers/input-controller.ts +18 -3
  133. package/src/modes/controllers/selector-controller.ts +83 -37
  134. package/src/modes/interactive-mode.ts +62 -3
  135. package/src/modes/irc-observation-ledger.ts +71 -15
  136. package/src/modes/runtime-init.ts +19 -2
  137. package/src/modes/types.ts +2 -0
  138. package/src/modes/utils/ui-helpers.ts +94 -1
  139. package/src/prompts/agents/architect.md +9 -1
  140. package/src/prompts/agents/critic.md +9 -0
  141. package/src/prompts/system/system-prompt.md +0 -1
  142. package/src/prompts/tools/read.md +4 -2
  143. package/src/runtime-mcp/smithery-auth.ts +17 -2
  144. package/src/runtime-mcp/smithery-connect.ts +11 -1
  145. package/src/sdk/broker/lifecycle.ts +25 -6
  146. package/src/sdk/bus/control-drain-lease.ts +104 -0
  147. package/src/sdk/bus/index.ts +341 -38
  148. package/src/sdk/bus/kind-aware-reconciliation.ts +242 -0
  149. package/src/sdk/bus/prompt-reconciliation.ts +2 -1
  150. package/src/sdk/bus/reconciliation-store.ts +224 -0
  151. package/src/sdk/host/control/dispatch.ts +5 -1
  152. package/src/sdk/host/control/operations.ts +1 -1
  153. package/src/sdk/host/host.ts +23 -1
  154. package/src/sdk/host/query/handlers.ts +55 -0
  155. package/src/sdk/mcp/server.ts +46 -18
  156. package/src/sdk/protocol/operation-inventory.generated.json +29 -0
  157. package/src/sdk/protocol/operation-registry.ts +7 -2
  158. package/src/sdk/session.ts +13 -1
  159. package/src/session/agent-session.ts +466 -50
  160. package/src/session/memory-guard-checkpoint-participant.ts +131 -0
  161. package/src/session/session-manager.ts +764 -17
  162. package/src/session/streaming-output.ts +262 -43
  163. package/src/setup/provider-onboarding.ts +19 -4
  164. package/src/skill-state/active-state.ts +0 -10
  165. package/src/skill-state/workflow-hud.ts +25 -2
  166. package/src/skill-state/workflow-mutation-guard.ts +65 -7
  167. package/src/tools/ask.ts +89 -69
  168. package/src/tools/browser/launch.ts +41 -6
  169. package/src/tools/fetch.ts +114 -23
  170. package/src/tools/output-meta.ts +176 -9
  171. package/src/tools/path-utils.ts +5 -1
  172. package/src/tools/read-internals.ts +44 -0
  173. package/src/tools/read.ts +1542 -310
  174. package/src/tools/resource-gc.ts +182 -18
  175. package/src/tools/tool-result.ts +14 -1
  176. package/src/web/search/providers/codex.ts +1 -1
  177. package/dist/types/gjc-runtime/deep-interview-draft.d.ts +0 -8
  178. package/dist/types/gjc-runtime/deep-interview-payload.d.ts +0 -41
  179. package/dist/types/gjc-runtime/deep-interview-repair.d.ts +0 -17
  180. package/src/gjc-runtime/deep-interview-draft.ts +0 -843
  181. package/src/gjc-runtime/deep-interview-payload.ts +0 -262
  182. package/src/gjc-runtime/deep-interview-repair.ts +0 -1674
@@ -35,6 +35,7 @@ import { isModelProfileProviderAvailable, projectModelProfileCatalog } from "../
35
35
  import { isAuthenticated, kNoAuth } from "../../config/model-registry";
36
36
  import { Settings } from "../../config/settings";
37
37
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "../../extensibility/extensions";
38
+ import { INTERACTIVE_SELECTOR_RESUME_ORIGIN } from "../../extensibility/shared-events";
38
39
  import { toAgentWireEventPayload } from "../../modes/shared/agent-wire/event-envelope";
39
40
  import {
40
41
  NotificationGatePolicyChangedError,
@@ -82,9 +83,16 @@ import {
82
83
  sessionTag,
83
84
  } from "./config";
84
85
  import { telegramControlCommandUsage } from "./config-commands";
86
+ import {
87
+ isNativeControlDrainAvailable,
88
+ runIdentityControlSuccessPath,
89
+ type TerminalSendOutcome,
90
+ } from "./control-drain-lease";
85
91
  import { imageAttachmentsFromMessage, notificationActionPayload, summaryFromMessage, truncate } from "./helpers";
92
+ import { createKindAwareReconciliation } from "./kind-aware-reconciliation";
86
93
  import { assertNativeRuntimeCompatibility } from "./native-runtime-compatibility";
87
94
  import { createPromptReconciliation, sanitizePromptFailure } from "./prompt-reconciliation";
95
+ import { createReconciliationStore } from "./reconciliation-store";
88
96
  import { NotificationSessionController, type NotificationSessionRuntime } from "./session-control";
89
97
  import {
90
98
  ASK_SELECTED_ACK_CAPABILITY,
@@ -95,6 +103,17 @@ import {
95
103
  unregisterNotificationRoot,
96
104
  } from "./telegram-daemon";
97
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
+
98
117
  // ===========================================================================
99
118
  // Session lifecycle control protocol (TypeScript mirror of the Rust wire
100
119
  // contract in `crates/gjc-sdk/src/lifecycle.rs`).
@@ -886,6 +905,8 @@ interface SessionRuntime {
886
905
  disposeGateListener: () => void;
887
906
  /** Whether notification-only delivery and answer resources are active. */
888
907
  notificationsActive: boolean;
908
+ /** Rejects new SDK frames while a leased terminal response drains. */
909
+ inboundFenced: boolean;
889
910
  /** Set as soon as terminal teardown is requested, before startup settles. */
890
911
  stopping: boolean;
891
912
  /** Recreates notification-only resources after `/notify on`. */
@@ -1807,6 +1828,9 @@ function sdkQuerySurface(
1807
1828
  }),
1808
1829
  configOverrides: ReadonlyMap<string, unknown> = new Map(),
1809
1830
  promptStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown,
1831
+ skillStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown = () => ({
1832
+ status: "unknown",
1833
+ }),
1810
1834
  ): SessionSurface {
1811
1835
  const metadata = () => ({
1812
1836
  sessionId: id,
@@ -1935,6 +1959,8 @@ function sdkQuerySurface(
1935
1959
  getJobs: () => ctx.getJobs(),
1936
1960
  getPromptStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
1937
1961
  promptStatusLookup(selector),
1962
+ getSkillInvokeStatus: (selector: { commandId?: string; turnId?: string; clientRef?: string }) =>
1963
+ skillStatusLookup(selector),
1938
1964
  installedQueries: installedOperations(ctx, "query"),
1939
1965
  };
1940
1966
  }
@@ -1962,7 +1988,7 @@ function sdkControlSurface(
1962
1988
  requesterConnectionId?: string,
1963
1989
  clientRef?: string,
1964
1990
  trackReconciliation?: boolean,
1965
- ) => void = () => {},
1991
+ ) => void | Promise<void> = () => {},
1966
1992
  onPromptFailed: (correlation: { commandId: string; turnId: string }, error: unknown) => void = () => {},
1967
1993
  acceptGateResolution: () => boolean,
1968
1994
  trackGateResolution: <T>(resolution: Promise<T>) => Promise<T>,
@@ -1971,6 +1997,20 @@ function sdkControlSurface(
1971
1997
  settings?: Settings,
1972
1998
  configOverrides: Map<string, unknown> = new Map(),
1973
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
+ },
1974
2014
  ): ControlSurface {
1975
2015
  const unavailable = (operation: string, reason: string) => () => {
1976
2016
  throw Object.assign(new Error(`${operation} is unavailable: ${reason}`), { code: "unavailable" });
@@ -2101,12 +2141,17 @@ function sdkControlSurface(
2101
2141
  error: Object.assign(new Error("Prompt preflight was cancelled before execution."), { code: "busy" }),
2102
2142
  });
2103
2143
  pendingPreflightCancellations.add(cancelPreflight);
2104
- const onPreflightAccepted = () => {
2144
+ const settleAccepted = async () => {
2105
2145
  if (preflightSettled) return;
2106
2146
  accepted = true;
2107
- onPromptAccepted(correlation, requesterConnectionId, trimmedClientRef, trackReconciliation);
2147
+ await onPromptAccepted(correlation, requesterConnectionId, trimmedClientRef, trackReconciliation);
2108
2148
  settlePreflight({ status: "accepted" });
2109
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
+ };
2110
2155
  // Do not acknowledge the prompt until AgentSession's async preflight
2111
2156
  // succeeds. The terminal result records correlation before agent_start can fire.
2112
2157
  let submission: Promise<void> | undefined;
@@ -2114,6 +2159,7 @@ function sdkControlSurface(
2114
2159
  submission = Promise.resolve(
2115
2160
  api.sendUserMessage(content, {
2116
2161
  ...(deliverAs ? { deliverAs } : !forceFresh && isBusy() ? { deliverAs: "steer" as const } : {}),
2162
+ onPreflightAcceptCommit,
2117
2163
  onPreflightAccepted,
2118
2164
  }),
2119
2165
  );
@@ -2324,13 +2370,84 @@ function sdkControlSurface(
2324
2370
  code: "terminal_uncertain",
2325
2371
  });
2326
2372
  },
2327
- invokeSkill: (name, args) => {
2373
+ invokeSkill: async (name, args, clientRef) => {
2328
2374
  if (!bindings.has("invokeSkill") || !ctx.invokeSkill)
2329
2375
  return unavailable("skill.invoke", "no skill invocation seam is installed")();
2330
2376
 
2331
2377
  if (typeof args !== "undefined" && typeof args !== "string")
2332
2378
  throw Object.assign(new Error("skill.invoke args must be a string."), { code: "invalid_input" });
2333
- 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;
2334
2451
  },
2335
2452
  setPlanMode: async on => {
2336
2453
  if (!bindings.has("setPlanMode") || !ctx.setPlanMode)
@@ -2866,6 +2983,18 @@ export async function ensureConfiguredProviderDaemons(
2866
2983
  if (isSlackConfigured(cfg)) await ensureProviderDaemon("slack", settings);
2867
2984
  }
2868
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
+
2869
2998
  export function createNotificationsExtension(
2870
2999
  api: ExtensionAPI,
2871
3000
  options: {
@@ -2905,10 +3034,15 @@ export function createNotificationsExtension(
2905
3034
  const cleanupRetries = new Map<string, SessionRuntime>();
2906
3035
  const sessionStartPromises = new Map<string, Promise<SessionStartResult>>();
2907
3036
  const branchStartupTasks = new Set<Promise<void>>();
3037
+ const sessionLifecycleTasks = new Set<Promise<void>>();
2908
3038
  let activeRuntimeId: string | undefined;
2909
3039
  let identityControlInFlight = false;
2910
3040
  let deferredIdentityRotation:
2911
- | { event: { previousSessionFile?: string }; ctx: ExtensionContext; awaitStartup: boolean }
3041
+ | {
3042
+ event: { previousSessionFile?: string; transition?: { origin: string } };
3043
+ ctx: ExtensionContext;
3044
+ awaitStartup: boolean;
3045
+ }
2912
3046
  | undefined;
2913
3047
  let extensionShuttingDown = false;
2914
3048
 
@@ -2969,6 +3103,7 @@ export function createNotificationsExtension(
2969
3103
  const requestedRuntime = retryRuntime ?? activeRuntime;
2970
3104
  if (expectedRuntime && requestedRuntime !== expectedRuntime) return false;
2971
3105
  if (reason === "session" && requestedRuntime) {
3106
+ requestedRuntime.inboundFenced = true;
2972
3107
  requestedRuntime.stopping = true;
2973
3108
  requestedRuntime.abortEphemeralTurns();
2974
3109
  }
@@ -3288,10 +3423,29 @@ export function createNotificationsExtension(
3288
3423
  // (contract documented in ./prompt-reconciliation and ../prompt-status).
3289
3424
  // Active records never age into terminal; documented TTL/capacity
3290
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.
3291
3438
  const reconciliation = createPromptReconciliation();
3292
- const admitPromptSubmission = reconciliation.admit;
3293
- const notePromptReconciliationAccepted = reconciliation.noteAccepted;
3294
- 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);
3295
3449
  const removePendingPromptCorrelation = (correlation: { commandId: string; turnId: string }) => {
3296
3450
  const pendingIndex = pendingPromptCorrelations.findIndex(
3297
3451
  candidate => candidate.commandId === correlation.commandId && candidate.turnId === correlation.turnId,
@@ -3318,6 +3472,7 @@ export function createNotificationsExtension(
3318
3472
  addTerminalTombstone(key);
3319
3473
  };
3320
3474
  const cleanupPromptRecords = (now = Date.now()) => {
3475
+ kindReconciliation.cleanup();
3321
3476
  reconciliation.cleanup();
3322
3477
  for (const [key, expiresAt] of promptTerminalTombstones)
3323
3478
  if (expiresAt <= now) promptTerminalTombstones.delete(key);
@@ -3396,7 +3551,7 @@ export function createNotificationsExtension(
3396
3551
  if (commandId && turnId) finalizePrompt(key, { commandId, turnId });
3397
3552
  }
3398
3553
  };
3399
- const recordPromptAccepted = (
3554
+ const recordPromptAccepted = async (
3400
3555
  correlation: { commandId: string; turnId: string },
3401
3556
  requesterConnectionId?: string,
3402
3557
  clientRef?: string,
@@ -3405,10 +3560,11 @@ export function createNotificationsExtension(
3405
3560
  if (!requesterConnectionId) {
3406
3561
  // No delivery owner: tracked prompts cannot be reconciled. Release
3407
3562
  // their admission reservation instead of leaking the active slot.
3408
- if (trackReconciliation) reconciliation.releaseAdmission(clientRef);
3563
+ if (trackReconciliation) releasePromptAdmission(clientRef);
3409
3564
  return;
3410
3565
  }
3411
- 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.
3412
3568
  cleanupPromptRecords();
3413
3569
  while (promptSubmissions.size >= PROMPT_SUBMISSION_CAPACITY) {
3414
3570
  const oldest = promptSubmissions.entries().next().value as [string, PromptSubmission] | undefined;
@@ -3427,6 +3583,7 @@ export function createNotificationsExtension(
3427
3583
  createdAt: Date.now(),
3428
3584
  bufferedFrames: [],
3429
3585
  });
3586
+ if (trackReconciliation) await notePromptReconciliationAccepted(correlation, clientRef);
3430
3587
  };
3431
3588
  const recordPromptTerminal = (correlation: { commandId: string; turnId: string } | undefined) => {
3432
3589
  if (!correlation) return false;
@@ -3440,7 +3597,7 @@ export function createNotificationsExtension(
3440
3597
  };
3441
3598
  const emitPromptFailure = (correlation: { commandId: string; turnId: string }, error: unknown) => {
3442
3599
  const sanitized = sanitizePromptFailure(error);
3443
- reconciliation.noteTransition(correlation, { type: "agent_failed", error: sanitized });
3600
+ void kindReconciliation.noteTransition("prompt", correlation, { type: "agent_failed", error: sanitized });
3444
3601
  const submission = promptSubmissions.get(promptSubmissionKey(correlation));
3445
3602
  if (!submission || !runtime || !recordPromptTerminal(correlation)) return;
3446
3603
  emitPromptLifecycle(correlation, {
@@ -3491,6 +3648,7 @@ export function createNotificationsExtension(
3491
3648
  },
3492
3649
  configOverrides,
3493
3650
  lookupPromptStatus,
3651
+ selector => kindReconciliation.lookup("skill", selector),
3494
3652
  ),
3495
3653
  id,
3496
3654
  revisions,
@@ -3507,10 +3665,18 @@ export function createNotificationsExtension(
3507
3665
  () => runtime?.stopping !== true,
3508
3666
  trackGateResolution,
3509
3667
  admitPromptSubmission,
3510
- reconciliation.releaseAdmission,
3668
+ releasePromptAdmission,
3511
3669
  settings,
3512
3670
  configOverrides,
3513
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
+ },
3514
3680
  );
3515
3681
  const abandonPromptResponse = (connectionId: string, frame: Record<string, unknown>) => {
3516
3682
  if (
@@ -3576,6 +3742,75 @@ export function createNotificationsExtension(
3576
3742
  };
3577
3743
  },
3578
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
+ },
3579
3814
  afterControlResponse: async (connectionId, request, response) => {
3580
3815
  if (
3581
3816
  (request.operation === "turn.prompt" ||
@@ -3596,13 +3831,6 @@ export function createNotificationsExtension(
3596
3831
  }
3597
3832
 
3598
3833
  if (request.operation === "session.close" && response.ok === true) ctx.shutdown();
3599
- if (typeof request.operation === "string" && identityControlOperations.has(request.operation)) {
3600
- const pending = deferredIdentityRotation;
3601
- deferredIdentityRotation = undefined;
3602
- identityControlInFlight = false;
3603
- if (response.ok === true && pending)
3604
- await rotateSessionAuthority(pending.event, pending.ctx, pending.awaitStartup);
3605
- }
3606
3834
  },
3607
3835
  control: async (connectionId, frame) => {
3608
3836
  const request = frame as {
@@ -3622,6 +3850,21 @@ export function createNotificationsExtension(
3622
3850
  ok: false,
3623
3851
  error: { code: "conflict", message: "session identity mutation is already active" },
3624
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
+ };
3625
3868
  if (rotatesIdentity) identityControlInFlight = true;
3626
3869
  const response = await controlRequesterContext.run(connectionId, () =>
3627
3870
  dispatchControl(
@@ -3693,6 +3936,7 @@ export function createNotificationsExtension(
3693
3936
  workflowGate: undefined,
3694
3937
  gatePresentations,
3695
3938
  stopping: false,
3939
+ inboundFenced: false,
3696
3940
  abortEphemeralTurns: () => {},
3697
3941
  disableEphemeralTurns: () => {},
3698
3942
  cancelPostmortemCleanup: () => {},
@@ -3708,7 +3952,9 @@ export function createNotificationsExtension(
3708
3952
  pendingPromptCorrelations,
3709
3953
  activePromptCorrelation: undefined,
3710
3954
  recordPromptTerminal,
3711
- notePromptReconciliation: reconciliation.noteTransition,
3955
+ notePromptReconciliation: (correlation, frame) => {
3956
+ void kindReconciliation.noteTransition("prompt", correlation, frame);
3957
+ },
3712
3958
  emitPromptFailure,
3713
3959
  emitPromptLifecycle,
3714
3960
  emitPromptEvent,
@@ -3774,6 +4020,22 @@ export function createNotificationsExtension(
3774
4020
  const frame = JSON.parse(inbound.json) as unknown;
3775
4021
  if (!frame || typeof frame !== "object") return;
3776
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
+ }
3777
4039
  if (typedFrame.type === "ephemeral_turn" || typedFrame.type === "ephemeral_turn_cancel") return;
3778
4040
  if (typedFrame.type === "event_replay") {
3779
4041
  const capabilities = Array.isArray(typedFrame.capabilities) ? typedFrame.capabilities : [];
@@ -3808,7 +4070,12 @@ export function createNotificationsExtension(
3808
4070
 
3809
4071
  server.onReply((err, reply) => {
3810
4072
  if (err || !reply) return;
3811
- 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
+ ) {
3812
4079
  try {
3813
4080
  server.closeClaimInvalid(reply.replyReceiptId, "session_stopping");
3814
4081
  } catch {}
@@ -4050,6 +4317,7 @@ export function createNotificationsExtension(
4050
4317
  // thread (forwarded by the daemon over the WS, fail-closed at the daemon).
4051
4318
  server.onInbound((err, inbound) => {
4052
4319
  if (err || !inbound) return;
4320
+ if (initializedRuntime.inboundFenced) return;
4053
4321
  const authenticatedInbound = inbound as typeof inbound & {
4054
4322
  connectionId: string;
4055
4323
  messageId?: number;
@@ -4642,7 +4910,16 @@ export function createNotificationsExtension(
4642
4910
  };
4643
4911
 
4644
4912
  api.on("session_start", async (_event, ctx) => {
4645
- 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
+ }
4646
4923
  });
4647
4924
 
4648
4925
  // A session endpoint's token and generation are authority for exactly one
@@ -4688,10 +4965,27 @@ export function createNotificationsExtension(
4688
4965
  });
4689
4966
  };
4690
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
+
4691
4984
  const rotateSessionAuthority = async (
4692
4985
  event: { previousSessionFile?: string },
4693
4986
  ctx: ExtensionContext,
4694
4987
  awaitStartup: boolean,
4988
+ options: { deferPredecessorStop?: boolean } = {},
4695
4989
  ): Promise<void> => {
4696
4990
  if (extensionShuttingDown) return;
4697
4991
  const newId = sessionId(ctx);
@@ -4700,7 +4994,11 @@ export function createNotificationsExtension(
4700
4994
  const pendingStartup = sessionStartPromises.get(newId);
4701
4995
  if (pendingStartup) {
4702
4996
  if (awaitStartup) {
4703
- 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
+ );
4704
5002
  if (!extensionShuttingDown && runtimes.has(newId) && activeRuntimeId === newId)
4705
5003
  await controller.reconcileCurrentSession(ctx);
4706
5004
  } else {
@@ -4715,14 +5013,14 @@ export function createNotificationsExtension(
4715
5013
  }
4716
5014
  if (prevId && prevId !== newId) {
4717
5015
  controller.rekeySession(prevId, newId);
4718
- try {
4719
- await stopSession(prevId);
4720
- } catch (error) {
4721
- // A retained owner-release failure keeps the exact runtime in
4722
- // cleanupRetries for an explicit later retry; log it rather than
4723
- // surfacing a red extension error
4724
- // while rotating session authority (/new, fork, resume, branch).
4725
- 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.`);
4726
5024
  }
4727
5025
  }
4728
5026
  if (extensionShuttingDown) return;
@@ -4743,18 +5041,19 @@ export function createNotificationsExtension(
4743
5041
  trackBranchStartup(newId, ctx, startup);
4744
5042
  };
4745
5043
  api.on("session_switch", async (event, ctx) => {
5044
+ const awaitStartup = shouldAwaitNotificationStartup(event);
4746
5045
  if (identityControlInFlight) {
4747
- deferredIdentityRotation = { event, ctx, awaitStartup: true };
5046
+ deferredIdentityRotation = { event, ctx, awaitStartup };
4748
5047
  return;
4749
5048
  }
4750
- await rotateSessionAuthority(event, ctx, true);
5049
+ await rotateSessionAuthority(event, ctx, awaitStartup);
4751
5050
  });
4752
5051
  api.on("session_branch", async (event, ctx) => {
4753
5052
  if (identityControlInFlight) {
4754
- deferredIdentityRotation = { event, ctx, awaitStartup: false };
5053
+ deferredIdentityRotation = { event, ctx, awaitStartup: true };
4755
5054
  return;
4756
5055
  }
4757
- await rotateSessionAuthority(event, ctx, false);
5056
+ await rotateSessionAuthority(event, ctx, true);
4758
5057
  });
4759
5058
 
4760
5059
  const terminalizeInFlightTools = (
@@ -5292,8 +5591,12 @@ export function createNotificationsExtension(
5292
5591
  extensionShuttingDown = true;
5293
5592
  identityControlInFlight = false;
5294
5593
  deferredIdentityRotation = undefined;
5295
- await Promise.allSettled([...branchStartupTasks]);
5296
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]);
5297
5600
  const rt = runtimes.get(id);
5298
5601
  if (rt) terminalizeInFlightTools(rt, id, "cancelled");
5299
5602
  // Startup is only genuinely in flight when a `sessionStartPromises` entry