@gajae-code/coding-agent 0.15.5 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/dist/types/async/job-manager.d.ts +83 -0
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/cli/auth-gateway-cli.d.ts +17 -0
  5. package/dist/types/cli/model-presets-cli.d.ts +9 -0
  6. package/dist/types/cli/root-flags.d.ts +14 -0
  7. package/dist/types/cli/update-cli.d.ts +15 -0
  8. package/dist/types/cli.d.ts +7 -0
  9. package/dist/types/commands/auth-gateway.d.ts +3 -0
  10. package/dist/types/commands/gc.d.ts +13 -0
  11. package/dist/types/commands/launch.d.ts +10 -0
  12. package/dist/types/commands/model-presets.d.ts +21 -0
  13. package/dist/types/config/autorouting-tier-map.d.ts +120 -0
  14. package/dist/types/config/file-lock.d.ts +54 -2
  15. package/dist/types/config/internal/model-preset-registry-test-state.d.ts +4 -0
  16. package/dist/types/config/internal/model-preset-registry-test-support.d.ts +3 -0
  17. package/dist/types/config/keybindings.d.ts +3 -2
  18. package/dist/types/config/model-preset-registry.d.ts +368 -0
  19. package/dist/types/config/model-profile-activation.d.ts +39 -0
  20. package/dist/types/config/model-profiles.d.ts +5 -3
  21. package/dist/types/config/model-registry.d.ts +17 -5
  22. package/dist/types/config/settings-schema.d.ts +19 -1
  23. package/dist/types/config/settings.d.ts +2 -0
  24. package/dist/types/coordinator-mcp/model-preset.d.ts +7 -5
  25. package/dist/types/coordinator-mcp/projection-scan.d.ts +54 -0
  26. package/dist/types/crash/record-loader.d.ts +15 -0
  27. package/dist/types/extensibility/extensions/types.d.ts +2 -5
  28. package/dist/types/gjc-runtime/empty-delete-gc.d.ts +58 -0
  29. package/dist/types/gjc-runtime/gc-runtime.d.ts +2 -0
  30. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  31. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  32. package/dist/types/gjc-runtime/session-state-lock.d.ts +35 -6
  33. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +72 -1
  34. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  35. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  36. package/dist/types/index.d.ts +1 -0
  37. package/dist/types/main.d.ts +3 -1
  38. package/dist/types/master-mode/context.d.ts +21 -0
  39. package/dist/types/master-mode/first-request.d.ts +34 -0
  40. package/dist/types/master-mode/snapshot.d.ts +32 -0
  41. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  42. package/dist/types/modes/components/jobs-overlay-model.d.ts +4 -2
  43. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  44. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  45. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  46. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  47. package/dist/types/modes/jobs-observer.d.ts +36 -0
  48. package/dist/types/modes/print-mode.d.ts +17 -0
  49. package/dist/types/modes/ui-language.d.ts +7 -1
  50. package/dist/types/sdk/broker/broker.d.ts +80 -1
  51. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  52. package/dist/types/sdk/broker/index.d.ts +3 -0
  53. package/dist/types/sdk/broker/lifecycle.d.ts +26 -0
  54. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  56. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  57. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  58. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  59. package/dist/types/sdk/broker/state-version.d.ts +4 -1
  60. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  61. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  62. package/dist/types/sdk/bus/index.d.ts +6 -0
  63. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +3 -0
  64. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +4 -0
  65. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  66. package/dist/types/sdk/cli/index.d.ts +1 -0
  67. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  68. package/dist/types/sdk/cli/rows.d.ts +4 -3
  69. package/dist/types/sdk/cli/session-cli.d.ts +18 -2
  70. package/dist/types/sdk/client/client.d.ts +3 -2
  71. package/dist/types/sdk/client/discovery.d.ts +1 -0
  72. package/dist/types/sdk/host/control/dispatch.d.ts +10 -0
  73. package/dist/types/sdk/host/control/index.d.ts +1 -1
  74. package/dist/types/sdk/host/control/operations.d.ts +2 -0
  75. package/dist/types/sdk/host/control/runtime-gate.d.ts +21 -7
  76. package/dist/types/sdk/host/host.d.ts +10 -0
  77. package/dist/types/sdk/host/model-pin.d.ts +7 -2
  78. package/dist/types/sdk/host/sdk-run-capability.d.ts +1 -0
  79. package/dist/types/sdk/host/session-runtime.d.ts +67 -1
  80. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  81. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  82. package/dist/types/sdk/lifecycle-session.d.ts +2 -0
  83. package/dist/types/sdk/prompt-deadline-manager.d.ts +11 -1
  84. package/dist/types/sdk/protocol/adapter-validation.d.ts +2 -0
  85. package/dist/types/sdk/router/session-router.d.ts +11 -0
  86. package/dist/types/sdk/session.d.ts +16 -0
  87. package/dist/types/sdk/startup-capability.d.ts +2 -1
  88. package/dist/types/session/account-inventory.d.ts +1 -0
  89. package/dist/types/session/agent-session.d.ts +24 -7
  90. package/dist/types/session/credential-store-errors.d.ts +1 -0
  91. package/dist/types/session/fold-coordinator.d.ts +201 -0
  92. package/dist/types/session/sdk-run-capability-internal.d.ts +7 -0
  93. package/dist/types/session/sdk-run-capability.d.ts +2 -0
  94. package/dist/types/session/session-manager.d.ts +65 -0
  95. package/dist/types/session/yield-queue.d.ts +5 -4
  96. package/dist/types/setup/frictionless-onboarding.d.ts +1 -0
  97. package/dist/types/telemetry/control.d.ts +11 -0
  98. package/dist/types/telemetry/events.d.ts +20 -0
  99. package/dist/types/telemetry/index.d.ts +3 -0
  100. package/dist/types/telemetry/transport.d.ts +22 -0
  101. package/dist/types/tools/bash-interactive.d.ts +23 -0
  102. package/dist/types/tools/bash.d.ts +4 -0
  103. package/dist/types/tools/gh.d.ts +8 -0
  104. package/dist/types/tools/index.d.ts +10 -5
  105. package/dist/types/web/search/providers/base.d.ts +9 -4
  106. package/dist/types/web/search/providers/xai.d.ts +2 -1
  107. package/dist/types/web/search/types.d.ts +14 -0
  108. package/package.json +10 -7
  109. package/scripts/generate-hotkeys-docs.ts +4 -0
  110. package/scripts/generate-sdk-operation-inventory.ts +9 -3
  111. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  112. package/src/async/job-manager.ts +671 -39
  113. package/src/cli/accounts-cli.ts +9 -5
  114. package/src/cli/args.ts +17 -0
  115. package/src/cli/auth-broker-cli.ts +40 -12
  116. package/src/cli/auth-gateway-cli.ts +273 -97
  117. package/src/cli/config-cli.ts +2 -2
  118. package/src/cli/fast-help.ts +4 -0
  119. package/src/cli/model-presets-cli.ts +100 -0
  120. package/src/cli/root-flags.ts +5 -0
  121. package/src/cli/update-cli.ts +117 -16
  122. package/src/cli.ts +6 -2
  123. package/src/commands/auth-gateway.ts +12 -8
  124. package/src/commands/gc.ts +19 -0
  125. package/src/commands/launch.ts +86 -18
  126. package/src/commands/model-presets.ts +24 -0
  127. package/src/commands/sdk.ts +113 -21
  128. package/src/config/autorouting-tier-map.ts +44 -0
  129. package/src/config/file-lock-gc.ts +9 -4
  130. package/src/config/file-lock.ts +890 -99
  131. package/src/config/internal/model-preset-registry-test-state.ts +28 -0
  132. package/src/config/internal/model-preset-registry-test-support.ts +11 -0
  133. package/src/config/keybindings.ts +6 -2
  134. package/src/config/model-preset-registry.ts +2795 -0
  135. package/src/config/model-profile-activation.ts +115 -28
  136. package/src/config/model-profile-contract.ts +4 -1
  137. package/src/config/model-profiles.ts +56 -14
  138. package/src/config/model-registry.ts +1044 -122
  139. package/src/config/model-resolver.ts +5 -0
  140. package/src/config/settings-schema.ts +19 -1
  141. package/src/config/settings.ts +11 -1
  142. package/src/coordinator-mcp/model-preset.ts +32 -17
  143. package/src/coordinator-mcp/projection-scan.ts +592 -0
  144. package/src/coordinator-mcp/question-state.ts +9 -11
  145. package/src/coordinator-mcp/server.ts +111 -42
  146. package/src/coordinator-mcp/session-reaper.ts +9 -3
  147. package/src/crash/index-store.ts +2 -0
  148. package/src/crash/record-loader.ts +10 -0
  149. package/src/extensibility/extensions/loader.ts +0 -1
  150. package/src/extensibility/extensions/types.ts +2 -3
  151. package/src/extensibility/gjc-plugins/runtime-adapters.ts +1 -0
  152. package/src/gjc-runtime/empty-delete-gc.ts +301 -0
  153. package/src/gjc-runtime/gc-render.ts +29 -0
  154. package/src/gjc-runtime/gc-runtime.ts +174 -11
  155. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  156. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  157. package/src/gjc-runtime/session-state-lock.ts +1321 -147
  158. package/src/gjc-runtime/session-state-sidecar.ts +1004 -25
  159. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  160. package/src/gjc-runtime/tmux-owner-isolation.ts +2 -2
  161. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  162. package/src/hooks/skill-state.ts +6 -0
  163. package/src/index.ts +1 -0
  164. package/src/internal-urls/docs-index.generated.ts +14 -13
  165. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  166. package/src/lsp/config.ts +4 -3
  167. package/src/lsp/path-trust.ts +12 -6
  168. package/src/main.ts +66 -4
  169. package/src/master-mode/context.ts +50 -0
  170. package/src/master-mode/first-request.ts +65 -0
  171. package/src/master-mode/snapshot.ts +205 -0
  172. package/src/modes/acp/acp-agent.ts +616 -161
  173. package/src/modes/components/footer.ts +4 -2
  174. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  175. package/src/modes/components/jobs-overlay-model.ts +99 -16
  176. package/src/modes/components/model-selector.ts +158 -18
  177. package/src/modes/components/oauth-selector.ts +6 -1
  178. package/src/modes/components/settings-selector.ts +1 -0
  179. package/src/modes/components/status-line/model-name.ts +53 -0
  180. package/src/modes/components/status-line/priority-row.ts +107 -0
  181. package/src/modes/components/status-line/segments.ts +29 -10
  182. package/src/modes/components/tool-status-header.ts +151 -15
  183. package/src/modes/controllers/extension-ui-controller.ts +1 -1
  184. package/src/modes/controllers/input-controller.ts +51 -6
  185. package/src/modes/controllers/selector-controller.ts +191 -96
  186. package/src/modes/interactive-mode.ts +14 -1
  187. package/src/modes/jobs-observer.ts +149 -16
  188. package/src/modes/print-mode.ts +64 -0
  189. package/src/modes/runtime-init.ts +1 -1
  190. package/src/modes/ui-language.ts +49 -2
  191. package/src/modes/utils/injected-user-submission.ts +15 -1
  192. package/src/prompts/system/master-mode.md +28 -0
  193. package/src/prompts/tools/bash.md +2 -2
  194. package/src/prompts/tools/fold-receipt.md +3 -0
  195. package/src/prompts/tools/job.md +1 -1
  196. package/src/sdk/acp/adapter.ts +17 -2
  197. package/src/sdk/broker/broker.ts +1671 -34
  198. package/src/sdk/broker/endpoint-authority.ts +86 -0
  199. package/src/sdk/broker/ensure.ts +5 -0
  200. package/src/sdk/broker/index.ts +3 -0
  201. package/src/sdk/broker/lifecycle.ts +217 -46
  202. package/src/sdk/broker/master-capability.ts +232 -0
  203. package/src/sdk/broker/session-index.ts +121 -17
  204. package/src/sdk/broker/session-scope.ts +272 -0
  205. package/src/sdk/broker/spawn-authority.ts +1031 -0
  206. package/src/sdk/broker/spawn-substrate.ts +548 -0
  207. package/src/sdk/broker/state-version.ts +26 -14
  208. package/src/sdk/broker/transport.ts +1 -0
  209. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  210. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  211. package/src/sdk/bus/chat-daemon-runtime.ts +46 -5
  212. package/src/sdk/bus/index.ts +51 -4
  213. package/src/sdk/bus/kind-aware-reconciliation.ts +36 -6
  214. package/src/sdk/bus/prompt-reconciliation.ts +28 -4
  215. package/src/sdk/bus/slack-conversation.ts +5 -1
  216. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  217. package/src/sdk/cli/index.ts +1 -0
  218. package/src/sdk/cli/master-cli.ts +259 -0
  219. package/src/sdk/cli/rows.ts +11 -8
  220. package/src/sdk/cli/session-cli.ts +276 -6
  221. package/src/sdk/client/client.ts +14 -8
  222. package/src/sdk/client/discovery.ts +3 -1
  223. package/src/sdk/host/control/dispatch.ts +101 -37
  224. package/src/sdk/host/control/index.ts +2 -0
  225. package/src/sdk/host/control/operations.ts +2 -0
  226. package/src/sdk/host/control/runtime-gate.ts +203 -11
  227. package/src/sdk/host/host.ts +27 -2
  228. package/src/sdk/host/model-pin.ts +185 -25
  229. package/src/sdk/host/sdk-run-capability.ts +1 -0
  230. package/src/sdk/host/session-runtime.test.ts +581 -43
  231. package/src/sdk/host/session-runtime.ts +655 -101
  232. package/src/sdk/lifecycle/broker-client.ts +28 -0
  233. package/src/sdk/lifecycle/service.ts +279 -20
  234. package/src/sdk/lifecycle-session.ts +10 -2
  235. package/src/sdk/prompt-deadline-manager.ts +16 -1
  236. package/src/sdk/protocol/adapter-validation.ts +27 -0
  237. package/src/sdk/protocol/operation-inventory.generated.json +53 -2
  238. package/src/sdk/protocol/operation-registry.ts +17 -4
  239. package/src/sdk/router/session-router.ts +294 -154
  240. package/src/sdk/session.ts +440 -167
  241. package/src/sdk/startup-capability.ts +1 -0
  242. package/src/session/account-inventory.ts +27 -11
  243. package/src/session/agent-session.ts +1292 -180
  244. package/src/session/blob-store.ts +15 -5
  245. package/src/session/credential-store-errors.ts +2 -0
  246. package/src/session/fold-coordinator.ts +433 -0
  247. package/src/session/internal/managed-session-storage.ts +153 -78
  248. package/src/session/sdk-run-capability-internal.ts +15 -0
  249. package/src/session/sdk-run-capability.ts +2 -0
  250. package/src/session/session-manager.ts +342 -28
  251. package/src/session/yield-queue.ts +24 -10
  252. package/src/setup/frictionless-onboarding.ts +2 -0
  253. package/src/setup/provider-presets.json +1 -1
  254. package/src/slash-commands/helpers/credential-switch.ts +6 -1
  255. package/src/task/executor.ts +19 -3
  256. package/src/telemetry/control.ts +26 -0
  257. package/src/telemetry/events.ts +1268 -0
  258. package/src/telemetry/index.ts +3 -0
  259. package/src/telemetry/transport.ts +87 -0
  260. package/src/tools/bash-interactive.ts +182 -85
  261. package/src/tools/bash.ts +1028 -109
  262. package/src/tools/gh.ts +104 -48
  263. package/src/tools/github-cache.ts +2 -2
  264. package/src/tools/index.ts +11 -5
  265. package/src/tools/tool-catalog.generated.ts +1 -1
  266. package/src/web/search/provider.ts +120 -11
  267. package/src/web/search/providers/anthropic.ts +89 -17
  268. package/src/web/search/providers/base.ts +9 -4
  269. package/src/web/search/providers/gemini.ts +87 -15
  270. package/src/web/search/providers/openai-compatible.ts +23 -8
  271. package/src/web/search/providers/xai.ts +98 -12
  272. package/src/web/search/types.ts +16 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Which session currently holds a GJC-managed worktree.
3
+ *
4
+ * Both launch paths need this answer and must not disagree about it: the broker
5
+ * refuses a lifecycle launch into an occupied worktree, and the interactive
6
+ * `gjc --worktree` launch refuses for the same reason. Keeping one predicate —
7
+ * and one definition of "still running" — here prevents the two from drifting
8
+ * into different notions of liveness.
9
+ */
10
+
11
+ import type * as NativeBindings from "@gajae-code/natives";
12
+ import { resolveEquivalentPath } from "@gajae-code/utils";
13
+ import { probeLinuxProcPidSync } from "../../gjc-runtime/linux-proc";
14
+ import { processIncarnation } from "./process-incarnation";
15
+ import type { IndexedSession } from "./session-index";
16
+
17
+ function nativeLifecycle(): typeof NativeBindings {
18
+ return require("@gajae-code/natives") as typeof NativeBindings;
19
+ }
20
+
21
+ export type ProcessObservation = "alive" | "exited" | "uncertain";
22
+
23
+ /** Only ESRCH or a changed, readable incarnation proves the owned process exited. */
24
+ export function observeProcess(
25
+ pid: number,
26
+ expectedIncarnation: string | undefined,
27
+ readIncarnation: (pid: number) => string | undefined = processIncarnation,
28
+ ): ProcessObservation {
29
+ if (process.platform === "win32") {
30
+ try {
31
+ const reference = nativeLifecycle().Process.fromPid(pid);
32
+ if (reference?.status() !== "running") return "exited";
33
+ } catch {
34
+ return "uncertain";
35
+ }
36
+ if (!expectedIncarnation) return "uncertain";
37
+ const actualIncarnation = readIncarnation(pid);
38
+ if (!actualIncarnation) return "uncertain";
39
+ return actualIncarnation === expectedIncarnation ? "alive" : "exited";
40
+ }
41
+ try {
42
+ process.kill(pid, 0);
43
+ } catch (error) {
44
+ return (error as NodeJS.ErrnoException).code === "ESRCH" ? "exited" : "uncertain";
45
+ }
46
+ if (process.platform === "linux") {
47
+ const probe = probeLinuxProcPidSync(pid);
48
+ if (probe.kind === "live" && probe.state === "Z" && expectedIncarnation === `linux:${probe.startTime}`)
49
+ return "exited";
50
+ }
51
+ if (!expectedIncarnation) return "uncertain";
52
+ const actualIncarnation = readIncarnation(pid);
53
+ if (!actualIncarnation) return "uncertain";
54
+ return actualIncarnation === expectedIncarnation ? "alive" : "exited";
55
+ }
56
+
57
+ /**
58
+ * The id of a session still occupying `worktreePath`, or null when it is free.
59
+ *
60
+ * `ensureLaunchWorktree` reuses an existing worktree without asking whether
61
+ * anyone is in it, and an unnamed launch derives its directory deterministically
62
+ * from the repository's current branch. Two concurrent sessions in one
63
+ * repository therefore land in the same checkout and overwrite each other's
64
+ * files with no error.
65
+ *
66
+ * Only a process observed as definitively exited releases the worktree.
67
+ * `uncertain` counts as occupied: refusing a launch is recoverable by picking
68
+ * another worktree name, whereas two live sessions sharing a checkout corrupts
69
+ * work already done.
70
+ */
71
+ export function worktreeOccupant(
72
+ sessions: readonly IndexedSession[],
73
+ worktreePath: string,
74
+ observe: (pid: number, expectedIncarnation: string | undefined) => ProcessObservation = observeProcess,
75
+ ): string | null {
76
+ // Session locators retain the canonical Git worktree root separately from the
77
+ // session cwd. A host may start below that root, and a launch plan may arrive
78
+ // through a symlink, so compare the root identity rather than the cwd spelling.
79
+ const target = resolveEquivalentPath(worktreePath);
80
+ for (const session of sessions) {
81
+ const sessionWorktreeRoot = session.locator.worktreeRoot;
82
+ if (session.terminal || typeof sessionWorktreeRoot !== "string" || sessionWorktreeRoot.length === 0) continue;
83
+ if (resolveEquivalentPath(sessionWorktreeRoot) !== target) continue;
84
+ // `live` is heartbeat-derived and can be stale. Only positive process-exit
85
+ // evidence releases a matching retained session's worktree.
86
+ if (observe(session.pid, session.hostIncarnation ?? session.processIncarnation) === "exited") continue;
87
+ return session.sessionId;
88
+ }
89
+ return null;
90
+ }
@@ -130,10 +130,13 @@ export type ChatDaemonAction = "stop" | "reload";
130
130
  * session-index lock every 2s; staleness retirement moved to a 30s sweep and
131
131
  * lease heartbeats no longer force an authority reconcile. A pre-upgrade owner
132
132
  * would retain the old hot polling loop, so replacement is required.
133
+ * Discord generation 72 / Slack generation 75 serialize same-generation
134
+ * successor attachment admission behind predecessor provider retirement, so a
135
+ * pre-upgrade owner cannot retain the queue race fixed by #5120.
133
136
  */
134
137
  export const CHAT_DAEMON_GENERATIONS: Readonly<Record<ChatDaemonKind, number>> = {
135
- discord: 70,
136
- slack: 73,
138
+ discord: 72,
139
+ slack: 75,
137
140
  };
138
141
 
139
142
  export function chatDaemonGeneration(kind: ChatDaemonKind): number {
@@ -114,6 +114,10 @@ function readGeneration(value: unknown): number | undefined {
114
114
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
115
115
  }
116
116
 
117
+ function readSequence(value: unknown): number | undefined {
118
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : undefined;
119
+ }
120
+
117
121
  /**
118
122
  * What one representation of a frame states about an identity it owns.
119
123
  *
@@ -138,7 +142,7 @@ const ABSENT_IDENTITY: IdentityClaim<never> = { state: "absent" };
138
142
  */
139
143
  function identityClaim<T>(
140
144
  frame: Record<string, unknown> | undefined,
141
- key: "sessionId" | "generation" | "name" | "kind",
145
+ key: "sessionId" | "generation" | "name" | "kind" | "seq",
142
146
  read: (value: unknown) => T | undefined,
143
147
  ): IdentityClaim<T> {
144
148
  if (!frame || !Object.hasOwn(frame, key)) return ABSENT_IDENTITY;
@@ -232,11 +236,17 @@ function correlateFrame(frame: Record<string, unknown>): CorrelatedFrame | undef
232
236
  identityClaim(payload, "generation", readGeneration),
233
237
  );
234
238
  if (!generation.ok) return undefined;
239
+ const seq = reconcileIdentity(
240
+ identityClaim(frame, "seq", readSequence),
241
+ identityClaim(payload, "seq", readSequence),
242
+ );
243
+ if (!seq.ok) return undefined;
235
244
  return {
236
245
  body,
237
246
  name: envelopeName.value ?? bodyName,
238
247
  sessionId: sessionId.value,
239
248
  generation: generation.value,
249
+ seq: seq.value,
240
250
  };
241
251
  }
242
252
 
@@ -248,6 +258,7 @@ export class ChatDaemonRuntime {
248
258
  readonly #router: SessionRouter;
249
259
  readonly #attachments = new Map<string, SessionAttachment>();
250
260
  readonly #cleanupWork = new Map<string, Promise<void>>();
261
+ readonly #retirementWork = new Map<string, Promise<void>>();
251
262
  readonly #attachmentBarriers = new Map<string, Promise<void>>();
252
263
  #discord: DiscordNotificationDaemon | undefined;
253
264
  #slack: SlackNotificationDaemon | undefined;
@@ -271,12 +282,18 @@ export class ChatDaemonRuntime {
271
282
  }
272
283
 
273
284
  async start(): Promise<void> {
274
- if (this.#cleanupWork.size > 0) {
285
+ if (this.#cleanupWork.size > 0 || this.#retirementWork.size > 0) {
286
+ const retirements = [...this.#retirementWork.entries()];
275
287
  const settled = await Promise.race([
276
- Promise.allSettled([...this.#cleanupWork.values()]).then(() => true),
288
+ Promise.all([
289
+ Promise.allSettled([...this.#cleanupWork.values()]),
290
+ Promise.all(retirements.map(([, retirement]) => retirement)),
291
+ ]).then(() => true),
277
292
  Bun.sleep(5_000).then(() => false),
278
293
  ]);
279
294
  if (!settled) throw new Error("Prior provider cleanup did not settle before chat daemon restart.");
295
+ for (const [sessionId, retirement] of retirements)
296
+ if (this.#retirementWork.get(sessionId) === retirement) this.#retirementWork.delete(sessionId);
280
297
  }
281
298
  this.#attachments.clear();
282
299
  const retainedProvider =
@@ -418,6 +435,12 @@ export class ChatDaemonRuntime {
418
435
  const discord = this.#discord;
419
436
  const slack = this.#slack;
420
437
  const barrier = (async () => {
438
+ const retirement = this.#retirementWork.get(attachment.sessionId);
439
+ if (retirement) {
440
+ await retirement;
441
+ if (this.#retirementWork.get(attachment.sessionId) === retirement)
442
+ this.#retirementWork.delete(attachment.sessionId);
443
+ }
421
444
  await this.#cleanupWork.get(attachment.sessionId)?.catch(() => undefined);
422
445
  await discord?.recoverCleanup(attachment.sessionId, attachment.generation);
423
446
  await slack?.recoverCleanup(attachment.sessionId, attachment.generation);
@@ -441,13 +464,31 @@ export class ChatDaemonRuntime {
441
464
  ): Promise<void> {
442
465
  if (this.#attachments.get(attachment.sessionId) !== attachment) return;
443
466
  if (reason === "replaced_same_generation") {
444
- await this.#discord?.retireAttachment(attachment.sessionId, attachment.generation);
445
- await this.#slack?.retireAttachment(attachment.sessionId, attachment.generation);
467
+ const retirement = this.#trackRetirement(attachment);
468
+ this.#attachments.delete(attachment.sessionId);
469
+ await retirement;
470
+ return;
446
471
  }
447
472
  this.#attachments.delete(attachment.sessionId);
448
473
  if (reason === "removed") await this.#trackCleanup(attachment);
449
474
  }
450
475
 
476
+ #trackRetirement(attachment: SessionAttachment): Promise<void> {
477
+ const sessionId = attachment.sessionId;
478
+ const discord = this.#discord;
479
+ const slack = this.#slack;
480
+ const previous = this.#retirementWork.get(sessionId);
481
+ const work = (previous ?? Promise.resolve()).then(async () => {
482
+ // Capture the predecessor providers before stop() can clear the runtime
483
+ // fields. The successor barrier owns this promise until every retirement
484
+ // waiter settles, even when takeover overlaps worker shutdown.
485
+ await discord?.retireAttachment(sessionId, attachment.generation);
486
+ await slack?.retireAttachment(sessionId, attachment.generation);
487
+ });
488
+ this.#retirementWork.set(sessionId, work);
489
+ return work;
490
+ }
491
+
451
492
  async #handleFrame(attachment: SessionAttachment, correlated: CorrelatedFrame): Promise<void> {
452
493
  if (this.#attachments.get(attachment.sessionId) !== attachment) return;
453
494
  await this.#attachmentBarriers.get(attachment.sessionId);
@@ -90,14 +90,17 @@ import { acpFinalTextFromMessage } from "../acp/final-text";
90
90
  import { ensureBroker } from "../broker/ensure";
91
91
  import { publishSessionHostRuntimeEvidence, type SessionHostRuntimePublication } from "../broker/lifecycle";
92
92
  import { processIncarnation } from "../broker/process-incarnation";
93
- import { SessionIndex } from "../broker/session-index";
93
+ import { resolveSessionLocator, SessionIndex } from "../broker/session-index";
94
94
  import {
95
95
  CAP_GATED_FRAME_KINDS,
96
96
  createSdkSurfaceFactory,
97
+ masterAttestationForEffectiveHost,
98
+ reattestMasterSessionIdentity,
97
99
  type SessionSdkHost,
98
100
  SessionSdkSessionRuntime,
99
101
  shouldHostSdk,
100
102
  TOOL_ACTIVITY_CAPABILITY,
103
+ verifyMasterCapabilityFrame,
101
104
  } from "../host";
102
105
  import { type AbortScope, type ControlSurface, dispatchControl, TypedControlError } from "../host/control";
103
106
  import { BROKER_RUNTIME_CLOSE_CAPABILITY_FIELD } from "../host/control/runtime-gate";
@@ -4043,6 +4046,12 @@ export function createNotificationsExtension(
4043
4046
  controller?: NotificationSessionController;
4044
4047
  /** Whether this host mode can own the root SDK endpoint. Default: true. */
4045
4048
  sdkHostModeSupported?: boolean;
4049
+ /** In-memory master capability for private broker verification only. */
4050
+ masterCapability?: string;
4051
+ /** Opaque direct-role epoch this effective host may adopt. */
4052
+ masterAttestationEpoch?: string;
4053
+ /** Stable master lineage identity retained across session switches/branches. */
4054
+ masterOwnerSessionId?: string;
4046
4055
  onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
4047
4056
  runBtwTurn?: (question: string, signal: AbortSignal) => Promise<{ replyText: string }>;
4048
4057
  /** Observes settlement of optional session-branch startup after reconciliation completes. */
@@ -4103,6 +4112,7 @@ export function createNotificationsExtension(
4103
4112
  }
4104
4113
  | undefined;
4105
4114
  let extensionShuttingDown = false;
4115
+ const consumedMasterNonces = new Map<string, number>();
4106
4116
 
4107
4117
  async function ensureTelegramOwner(settings: Settings): Promise<"ready" | "blocked_identity"> {
4108
4118
  if (!telegramTopicsEnabled()) return "blocked_identity";
@@ -4489,12 +4499,14 @@ export function createNotificationsExtension(
4489
4499
 
4490
4500
  async function startSession(ctx: ExtensionContext): Promise<SessionStartResult> {
4491
4501
  const id = sessionId(ctx);
4492
- const lifecycleRequestId = safeLifecycleRequestId(process.env.GJC_LIFECYCLE_REQUEST_ID);
4493
4502
  const { settings, cfg, settingsAvailable } = resolveSettings(options.settings);
4494
4503
  const notificationsEnabledForSession = controller.query(ctx).genericSessionEnabled;
4495
4504
  const sdkEnabledForSession =
4496
4505
  (options.sdkHostModeSupported ?? true) && shouldHostSdk(settings, isNotificationEligibleContext(ctx));
4497
4506
  const lifecycleRequired = lifecycleStartupCapability !== undefined;
4507
+ const lifecycleRequestId = lifecycleRequired
4508
+ ? safeLifecycleRequestId(lifecycleStartupCapability.lifecycleRequestId)
4509
+ : safeLifecycleRequestId(process.env.GJC_LIFECYCLE_REQUEST_ID);
4498
4510
  /** The broker-issued readiness intent for this exact lifecycle-managed session. */
4499
4511
  const lifecycleReadiness = lifecycleStartupCapability?.readiness ?? "immediate";
4500
4512
  const failLifecycleStartup = (
@@ -6588,6 +6600,7 @@ export function createNotificationsExtension(
6588
6600
  }
6589
6601
 
6590
6602
  sdkRuntime = new SessionSdkSessionRuntime({
6603
+ masterOwnerSessionId: options.masterOwnerSessionId,
6591
6604
  transport: {
6592
6605
  sessionId: id,
6593
6606
  stateRoot,
@@ -6607,6 +6620,13 @@ export function createNotificationsExtension(
6607
6620
  ...(activationGate ? { activationGate } : {}),
6608
6621
  ...(settings ? { settings } : {}),
6609
6622
  ...(configOverrides ? { configOverrides } : {}),
6623
+ masterCapabilityVerify: frame =>
6624
+ verifyMasterCapabilityFrame({
6625
+ frame,
6626
+ expectedCapability: options.masterCapability,
6627
+ expectedEpoch: options.masterAttestationEpoch,
6628
+ replay: consumedMasterNonces,
6629
+ }),
6610
6630
  connectionCapabilities: connectionId => hostCapCache.get(connectionId),
6611
6631
  installProviderDefinitions,
6612
6632
  onProviderDefinitionsRemoved: removeProviderDefinitions,
@@ -7751,6 +7771,8 @@ export function createNotificationsExtension(
7751
7771
  }
7752
7772
  const agentDir = lifecycleAgentDir ?? settings?.getAgentDir?.();
7753
7773
  if (lifecycleRequired && !agentDir) throw new Error("Lifecycle SDK host requires an agent directory.");
7774
+ if (lifecycleRequired && !lifecycleRequestId)
7775
+ throw new Error("Lifecycle SDK host requires a capability-bound request identity.");
7754
7776
 
7755
7777
  if (agentDir) {
7756
7778
  try {
@@ -7758,13 +7780,36 @@ export function createNotificationsExtension(
7758
7780
  throwIfLifecycleStopped();
7759
7781
  const index = await new SessionIndex(agentDir).open();
7760
7782
  throwIfLifecycleStopped();
7761
- const locator = { repo: path.resolve(ctx.cwd), stateRoot: endpointStateRoot };
7762
- const endpointMtimeMs = fs.statSync(path.join(endpointStateRoot, "sdk", `${id}.json`)).mtimeMs;
7783
+ const locator = await resolveSessionLocator(ctx.cwd, endpointStateRoot);
7784
+ const endpointPath = path.join(endpointStateRoot, "sdk", `${id}.json`);
7785
+ const endpointMtimeMs = fs.statSync(endpointPath).mtimeMs;
7786
+ const endpointIdentity = fs.statSync(endpointPath, { bigint: true });
7787
+ const endpointFileId = `${endpointIdentity.dev}:${endpointIdentity.ino}`;
7763
7788
  const hostProcessIncarnation = processIncarnation(process.pid);
7789
+ const direct = await reattestMasterSessionIdentity({
7790
+ index,
7791
+ locator,
7792
+ masterCapability: options.masterCapability,
7793
+ attestationEpoch: options.masterAttestationEpoch,
7794
+ ownerSessionId: options.masterOwnerSessionId,
7795
+ sessionId: id,
7796
+ pid: process.pid,
7797
+ processIncarnation: hostProcessIncarnation,
7798
+ });
7799
+ throwIfLifecycleStopped();
7764
7800
  await host.registerWithBroker({
7765
7801
  // The endpoint is written before registration. Its exact mtime
7766
7802
  // binds this index generation to that discovery record.
7767
7803
  register: async input => {
7804
+ const masterRole = masterAttestationForEffectiveHost({
7805
+ masterCapability: options.masterCapability,
7806
+ attestationEpoch: options.masterAttestationEpoch,
7807
+ ownerSessionId: options.masterOwnerSessionId,
7808
+ sessionId: input.sessionId,
7809
+ pid: process.pid,
7810
+ processIncarnation: hostProcessIncarnation,
7811
+ direct,
7812
+ });
7768
7813
  await index.append({
7769
7814
  type: "host_registered",
7770
7815
  ...input,
@@ -7776,6 +7821,8 @@ export function createNotificationsExtension(
7776
7821
  // teardown identity provable after that workspace is gone.
7777
7822
  ...(hostProcessIncarnation ? { processIncarnation: hostProcessIncarnation } : {}),
7778
7823
  endpointMtimeMs,
7824
+ endpointFileId,
7825
+ ...(masterRole ? { masterRole } : {}),
7779
7826
  ...(lifecycleRequestId ? { lifecycleRequestId } : {}),
7780
7827
  });
7781
7828
  },
@@ -21,6 +21,8 @@ import type {
21
21
  ReconciliationStore,
22
22
  } from "./reconciliation-store";
23
23
 
24
+ const EMPTY_PROMPT_FAILURE = { code: "prompt_failed", message: "Prompt submission failed." } as const;
25
+
24
26
  export type { ReconciliationKind };
25
27
 
26
28
  export interface KindCorrelation extends PromptCorrelation {
@@ -51,8 +53,13 @@ export interface KindAwareReconciliation {
51
53
  kind: ReconciliationKind,
52
54
  correlation: PromptCorrelation | undefined,
53
55
  frame:
54
- | { type: "agent_start" | "agent_end"; content?: TurnResultContent }
55
- | { type: "agent_failed"; error: unknown; content?: TurnResultContent },
56
+ | {
57
+ type: "agent_start" | "agent_end";
58
+ content?: TurnResultContent;
59
+ hasActivity?: boolean;
60
+ outcome?: SdkPromptTerminalOutcome;
61
+ }
62
+ | { type: "agent_failed"; error: unknown; content?: TurnResultContent; hasActivity?: boolean },
56
63
  ): Promise<void>;
57
64
  claimPendingOutcome(
58
65
  kind: ReconciliationKind,
@@ -340,8 +347,13 @@ export function createKindAwareReconciliation(
340
347
  kind: ReconciliationKind,
341
348
  correlation: PromptCorrelation | undefined,
342
349
  frame:
343
- | { type: "agent_start" | "agent_end"; content?: TurnResultContent }
344
- | { type: "agent_failed"; error: unknown; content?: TurnResultContent },
350
+ | {
351
+ type: "agent_start" | "agent_end";
352
+ content?: TurnResultContent;
353
+ hasActivity?: boolean;
354
+ outcome?: SdkPromptTerminalOutcome;
355
+ }
356
+ | { type: "agent_failed"; error: unknown; content?: TurnResultContent; hasActivity?: boolean },
345
357
  ) => {
346
358
  if (!correlation) return;
347
359
  await queueMutation(candidate => {
@@ -412,7 +424,17 @@ export function createKindAwareReconciliation(
412
424
  record.receiptState = record.pendingReceiptState ?? "missing";
413
425
  record.pendingReceiptState = undefined;
414
426
  } else {
415
- record.status = record.error === undefined ? "terminal_ok" : "failed";
427
+ if (
428
+ kind === "prompt" &&
429
+ record.error === undefined &&
430
+ frame.type === "agent_end" &&
431
+ frame.outcome?.kind !== "stopped" &&
432
+ !frame.content?.text.trim() &&
433
+ !frame.hasActivity
434
+ ) {
435
+ record.status = "failed";
436
+ record.error = EMPTY_PROMPT_FAILURE;
437
+ } else record.status = record.error === undefined ? "terminal_ok" : "failed";
416
438
  record.receiptState = frame.content?.text?.trim() ? "present" : "missing";
417
439
  }
418
440
  cleanupRecords(candidate);
@@ -453,6 +475,7 @@ export function createKindAwareReconciliation(
453
475
  : undefined;
454
476
  const content =
455
477
  typeof arg4 === "function" ? arg6 : typeof arg5 === "object" && arg5 !== null && "code" in arg5 ? arg6 : arg5;
478
+ const sanitizedContent = sanitizeTurnResultContent(content);
456
479
  await queueMutation(candidate => {
457
480
  if (isCurrent !== undefined && !isCurrent()) return { value: undefined, changed: false };
458
481
  const record = candidate.get(keyOf(kind, correlation));
@@ -463,8 +486,15 @@ export function createKindAwareReconciliation(
463
486
  if (finalOutcome?.kind === "failed") {
464
487
  record.status = "failed";
465
488
  record.error = recordError ?? { code: finalOutcome.code, message: finalOutcome.message };
489
+ } else if (
490
+ kind === "prompt" &&
491
+ ((finalOutcome === undefined && !sanitizedContent?.text.trim()) ||
492
+ (finalOutcome?.kind !== "stopped" && content !== undefined && !sanitizedContent?.text.trim()))
493
+ ) {
494
+ record.status = "failed";
495
+ record.error = EMPTY_PROMPT_FAILURE;
466
496
  } else record.status = "terminal_ok";
467
- record.content = sanitizeTurnResultContent(content);
497
+ record.content = sanitizedContent;
468
498
  record.outcome = finalOutcome;
469
499
  record.receiptState = record.pendingReceiptState ?? "unknown";
470
500
  record.pendingOutcome = undefined;
@@ -23,11 +23,13 @@
23
23
  */
24
24
 
25
25
  import { PROMPT_FAILURE_CODE_MAX, sanitizePromptFailure } from "../prompt-failure";
26
+ import type { SdkPromptTerminalOutcome } from "../prompt-status";
26
27
  import type { ReceiptState } from "../receipt-state";
27
28
 
28
29
  export { PROMPT_FAILURE_CODE_MAX, sanitizePromptFailure };
29
30
  export const PROMPT_RECONCILIATION_ACTIVE_CAPACITY = 128;
30
31
  export const PROMPT_RECONCILIATION_TERMINAL_CAPACITY = 256;
32
+ const EMPTY_PROMPT_FAILURE = { code: "prompt_failed", message: "Prompt submission failed." } as const;
31
33
 
32
34
  export type PromptReconciliationStatus = "accepted" | "in_flight" | "terminal_ok" | "failed";
33
35
 
@@ -101,8 +103,13 @@ export interface PromptReconciliation {
101
103
  correlation: PromptCorrelation | undefined,
102
104
  frame:
103
105
  | { type: "agent_start" }
104
- | { type: "agent_end"; finalText?: string }
105
- | { type: "agent_failed"; error: unknown; finalText?: string },
106
+ | {
107
+ type: "agent_end";
108
+ finalText?: string;
109
+ hasActivity?: boolean;
110
+ outcome?: SdkPromptTerminalOutcome;
111
+ }
112
+ | { type: "agent_failed"; error: unknown; finalText?: string; hasActivity?: boolean },
106
113
  ): void;
107
114
  lookup(selector: { commandId?: string; turnId?: string; clientRef?: string }): TurnPromptReconciliation;
108
115
  cleanup(): void;
@@ -190,8 +197,13 @@ export function createPromptReconciliation(options: { now?: () => number } = {})
190
197
  correlation: PromptCorrelation | undefined,
191
198
  frame:
192
199
  | { type: "agent_start" }
193
- | { type: "agent_end"; finalText?: string }
194
- | { type: "agent_failed"; error: unknown; finalText?: string },
200
+ | {
201
+ type: "agent_end";
202
+ finalText?: string;
203
+ hasActivity?: boolean;
204
+ outcome?: SdkPromptTerminalOutcome;
205
+ }
206
+ | { type: "agent_failed"; error: unknown; finalText?: string; hasActivity?: boolean },
195
207
  ) => {
196
208
  if (!correlation) return;
197
209
  const record = records.get(keyOf(correlation));
@@ -221,6 +233,18 @@ export function createPromptReconciliation(options: { now?: () => number } = {})
221
233
  if (frame.type === "agent_failed") {
222
234
  record.status = "failed";
223
235
  record.error = sanitizePromptFailure(frame.error);
236
+ } else if (record.error !== undefined) {
237
+ // Preserve an actionable provider failure already recorded before a late
238
+ // empty agent_end; the empty frame cannot downgrade or replace its cause.
239
+ record.status = "failed";
240
+ } else if (frame.type === "agent_end" && frame.outcome?.kind === "stopped") {
241
+ record.status = "terminal_ok";
242
+ } else if (!frame.finalText?.trim() && !frame.hasActivity) {
243
+ // A prompt cannot claim successful completion without assistant content.
244
+ // Keep the terminal transition first-wins, but fail closed instead of
245
+ // exposing an empty zero-token turn as terminal_ok.
246
+ record.status = "failed";
247
+ record.error = EMPTY_PROMPT_FAILURE;
224
248
  } else {
225
249
  record.status = "terminal_ok";
226
250
  }
@@ -74,5 +74,9 @@ export function normalizeSlackConversation(record: SlackConversation): SlackConv
74
74
  }
75
75
 
76
76
  export function acceptsSlackInbound(record: SlackConversation, rootTs: string, endpointGeneration: number): boolean {
77
- return record.state === "active" && record.rootTs === rootTs && record.endpointGeneration === endpointGeneration;
77
+ return (
78
+ (record.state === "active" || record.state === "resumed_root") &&
79
+ record.rootTs === rootTs &&
80
+ record.endpointGeneration === endpointGeneration
81
+ );
78
82
  }
@@ -291,7 +291,7 @@ export const SDK_LIFECYCLE_ROUTER_PROTOCOL_VERSION = 1;
291
291
  * killing a session's mirroring after one transient rejection and must be
292
292
  * replaced across this upgrade.
293
293
  */
294
- export const DAEMON_GENERATION = 179;
294
+ export const DAEMON_GENERATION = 180;
295
295
 
296
296
  /**
297
297
  * Serving-compatibility boundary for daemon lifecycle requests. Epoch 7
@@ -1 +1,2 @@
1
+ export * from "./master-cli";
1
2
  export * from "./session-cli";