@gajae-code/coding-agent 0.15.6 → 0.16.1

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 (270) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/dist/types/async/job-manager.d.ts +32 -1
  3. package/dist/types/browser-backend.d.ts +12 -0
  4. package/dist/types/cli/args.d.ts +2 -0
  5. package/dist/types/cli/root-flags.d.ts +14 -0
  6. package/dist/types/cli/update-cli.d.ts +14 -0
  7. package/dist/types/cli.d.ts +7 -0
  8. package/dist/types/commands/launch.d.ts +10 -0
  9. package/dist/types/commands/sdk.d.ts +2 -0
  10. package/dist/types/config/autorouting-tier-map.d.ts +24 -0
  11. package/dist/types/config/billing-path.d.ts +25 -0
  12. package/dist/types/config/config-schema-version.d.ts +13 -1
  13. package/dist/types/config/file-lock.d.ts +5 -0
  14. package/dist/types/config/model-registry.d.ts +22 -0
  15. package/dist/types/config/settings-schema.d.ts +27 -8
  16. package/dist/types/config/settings.d.ts +2 -0
  17. package/dist/types/coordinator-mcp/projection-scan.d.ts +8 -0
  18. package/dist/types/crash/record-loader.d.ts +15 -0
  19. package/dist/types/extensibility/extensions/types.d.ts +8 -3
  20. package/dist/types/gjc-runtime/launch-worktree-reservation.d.ts +21 -0
  21. package/dist/types/gjc-runtime/launch-worktree.d.ts +25 -1
  22. package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +2 -0
  23. package/dist/types/gjc-runtime/session-state-lock.d.ts +22 -0
  24. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +66 -1
  25. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +1 -0
  26. package/dist/types/gjc-runtime/tmux-sessions.d.ts +27 -0
  27. package/dist/types/main.d.ts +1 -0
  28. package/dist/types/master-mode/context.d.ts +21 -0
  29. package/dist/types/master-mode/first-request.d.ts +34 -0
  30. package/dist/types/master-mode/snapshot.d.ts +32 -0
  31. package/dist/types/modes/acp/acp-agent.d.ts +21 -21
  32. package/dist/types/modes/components/frictionless-onboarding-selector.d.ts +10 -0
  33. package/dist/types/modes/components/model-selector.d.ts +4 -0
  34. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  35. package/dist/types/modes/components/session-selector.d.ts +1 -1
  36. package/dist/types/modes/components/sessions-dashboard.d.ts +2 -1
  37. package/dist/types/modes/components/status-line/model-name.d.ts +18 -0
  38. package/dist/types/modes/components/status-line/priority-row.d.ts +34 -0
  39. package/dist/types/modes/components/status-line/segments.d.ts +10 -0
  40. package/dist/types/modes/interactive-mode.d.ts +16 -2
  41. package/dist/types/modes/print-mode.d.ts +17 -0
  42. package/dist/types/modes/theme/theme.d.ts +2 -1
  43. package/dist/types/modes/types.d.ts +2 -0
  44. package/dist/types/modes/ui-language.d.ts +7 -1
  45. package/dist/types/sdk/broker/broker.d.ts +79 -2
  46. package/dist/types/sdk/broker/endpoint-authority.d.ts +24 -0
  47. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  48. package/dist/types/sdk/broker/index.d.ts +3 -0
  49. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +6 -0
  50. package/dist/types/sdk/broker/lifecycle.d.ts +40 -0
  51. package/dist/types/sdk/broker/master-capability.d.ts +10 -0
  52. package/dist/types/sdk/broker/session-index.d.ts +40 -11
  53. package/dist/types/sdk/broker/session-scope.d.ts +72 -0
  54. package/dist/types/sdk/broker/spawn-authority.d.ts +201 -0
  55. package/dist/types/sdk/broker/spawn-substrate.d.ts +30 -0
  56. package/dist/types/sdk/broker/startup-budget.d.ts +52 -0
  57. package/dist/types/sdk/broker/state-version.d.ts +21 -1
  58. package/dist/types/sdk/broker/worktree-occupancy.d.ts +28 -0
  59. package/dist/types/sdk/bus/chat-daemon-control.d.ts +3 -0
  60. package/dist/types/sdk/bus/discord-daemon.d.ts +1 -1
  61. package/dist/types/sdk/bus/index.d.ts +9 -1
  62. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +1 -1
  63. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +1 -1
  64. package/dist/types/sdk/bus/slack-conversation.d.ts +1 -1
  65. package/dist/types/sdk/bus/slack-daemon.d.ts +1 -1
  66. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
  67. package/dist/types/sdk/cli/index.d.ts +1 -0
  68. package/dist/types/sdk/cli/master-cli.d.ts +63 -0
  69. package/dist/types/sdk/cli/rows.d.ts +12 -5
  70. package/dist/types/sdk/cli/session-cli.d.ts +20 -3
  71. package/dist/types/sdk/client/client.d.ts +3 -2
  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/events.d.ts +1 -0
  77. package/dist/types/sdk/host/host.d.ts +10 -0
  78. package/dist/types/sdk/host/query/cursor.d.ts +4 -2
  79. package/dist/types/sdk/host/query/handlers.d.ts +1 -0
  80. package/dist/types/sdk/host/query/revision-store.d.ts +1 -0
  81. package/dist/types/sdk/host/session-runtime.d.ts +70 -17
  82. package/dist/types/sdk/lifecycle/broker-client.d.ts +6 -0
  83. package/dist/types/sdk/lifecycle/service.d.ts +15 -3
  84. package/dist/types/sdk/lifecycle-session.d.ts +3 -1
  85. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -5
  86. package/dist/types/sdk/receipt-state.d.ts +2 -0
  87. package/dist/types/sdk/router/session-router.d.ts +6 -0
  88. package/dist/types/sdk/session.d.ts +16 -0
  89. package/dist/types/sdk/startup-capability.d.ts +2 -1
  90. package/dist/types/sdk/turn-result.d.ts +6 -0
  91. package/dist/types/session/agent-session.d.ts +52 -7
  92. package/dist/types/session/credential-store-errors.d.ts +1 -0
  93. package/dist/types/session/fold-coordinator.d.ts +29 -2
  94. package/dist/types/session/session-manager.d.ts +79 -2
  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/tools/ask.d.ts +1 -0
  98. package/dist/types/tools/bash.d.ts +16 -0
  99. package/dist/types/tools/browser/launch.d.ts +10 -0
  100. package/dist/types/tools/browser/registry.d.ts +8 -0
  101. package/dist/types/tools/gh.d.ts +8 -0
  102. package/dist/types/tools/index.d.ts +10 -10
  103. package/dist/types/tools/job.d.ts +3 -1
  104. package/dist/types/utils/external-editor.d.ts +7 -0
  105. package/examples/extensions/README.md +1 -1
  106. package/examples/extensions/pirate.ts +7 -4
  107. package/package.json +7 -7
  108. package/scripts/generate-sdk-operation-inventory.ts +13 -1
  109. package/scripts/verify-gjc-sdk-canonicalization.ts +35 -1
  110. package/src/async/job-manager.ts +107 -9
  111. package/src/browser-backend.ts +29 -0
  112. package/src/cli/accounts-cli.ts +9 -5
  113. package/src/cli/args.ts +17 -0
  114. package/src/cli/fast-help.ts +1 -0
  115. package/src/cli/plugin-cli.ts +29 -0
  116. package/src/cli/root-flags.ts +5 -0
  117. package/src/cli/update-cli.ts +140 -23
  118. package/src/commands/launch.ts +86 -18
  119. package/src/commands/sdk.ts +135 -28
  120. package/src/config/autorouting-tier-map.ts +12 -0
  121. package/src/config/billing-path.ts +42 -0
  122. package/src/config/config-schema-version.ts +18 -1
  123. package/src/config/file-lock.ts +158 -83
  124. package/src/config/model-preset-registry.ts +178 -23
  125. package/src/config/model-profiles.ts +5 -5
  126. package/src/config/model-registry.ts +291 -42
  127. package/src/config/model-resolver.ts +5 -0
  128. package/src/config/settings-schema.ts +27 -8
  129. package/src/config/settings.ts +51 -15
  130. package/src/coordinator-mcp/projection-scan.ts +27 -6
  131. package/src/coordinator-mcp/question-state.ts +9 -11
  132. package/src/coordinator-mcp/server.ts +115 -21
  133. package/src/crash/index-store.ts +2 -0
  134. package/src/crash/record-loader.ts +10 -0
  135. package/src/defaults/gjc/skills/deep-interview/SKILL.md +1 -1
  136. package/src/extensibility/extensions/runner.ts +27 -0
  137. package/src/extensibility/extensions/types.ts +8 -3
  138. package/src/extensibility/gjc-plugins/installer.ts +51 -11
  139. package/src/extensibility/runtime-skill-discovery.ts +110 -3
  140. package/src/extensibility/skills.ts +22 -12
  141. package/src/gjc-runtime/launch-worktree-reservation.ts +242 -0
  142. package/src/gjc-runtime/launch-worktree.ts +285 -4
  143. package/src/gjc-runtime/managed-owner-supervisor.ts +63 -51
  144. package/src/gjc-runtime/session-state-lock.ts +213 -14
  145. package/src/gjc-runtime/session-state-sidecar.ts +938 -39
  146. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +4 -7
  147. package/src/gjc-runtime/tmux-owner-isolation.ts +18 -3
  148. package/src/gjc-runtime/tmux-sessions.ts +252 -13
  149. package/src/hooks/skill-state.ts +6 -0
  150. package/src/internal-urls/docs-index.generated.ts +15 -15
  151. package/src/internal-urls/issue-pr-protocol.ts +4 -2
  152. package/src/main.ts +61 -3
  153. package/src/master-mode/context.ts +50 -0
  154. package/src/master-mode/first-request.ts +65 -0
  155. package/src/master-mode/snapshot.ts +205 -0
  156. package/src/modes/acp/acp-agent.ts +610 -161
  157. package/src/modes/components/footer.ts +4 -2
  158. package/src/modes/components/frictionless-onboarding-selector.ts +48 -2
  159. package/src/modes/components/model-selector.ts +76 -8
  160. package/src/modes/components/oauth-selector.ts +6 -1
  161. package/src/modes/components/session-selector.ts +23 -15
  162. package/src/modes/components/sessions-dashboard.ts +8 -5
  163. package/src/modes/components/settings-selector.ts +1 -0
  164. package/src/modes/components/status-line/model-name.ts +53 -0
  165. package/src/modes/components/status-line/priority-row.ts +107 -0
  166. package/src/modes/components/status-line/segments.ts +23 -9
  167. package/src/modes/components/tool-status-header.ts +151 -15
  168. package/src/modes/controllers/command-controller.ts +1 -1
  169. package/src/modes/controllers/event-controller.ts +30 -4
  170. package/src/modes/controllers/extension-ui-controller.ts +13 -13
  171. package/src/modes/controllers/input-controller.ts +30 -2
  172. package/src/modes/controllers/selector-controller.ts +200 -98
  173. package/src/modes/interactive-mode.ts +36 -12
  174. package/src/modes/print-mode.ts +64 -0
  175. package/src/modes/prompt-action-autocomplete.ts +30 -4
  176. package/src/modes/runtime-init.ts +12 -9
  177. package/src/modes/theme/theme.ts +5 -0
  178. package/src/modes/types.ts +2 -0
  179. package/src/modes/ui-language.ts +49 -2
  180. package/src/modes/utils/ui-helpers.ts +22 -3
  181. package/src/prompts/agent-fragments/browser-aside.md +22 -0
  182. package/src/prompts/system/master-mode.md +28 -0
  183. package/src/prompts/tools/ask.md +1 -1
  184. package/src/prompts/tools/bash.md +1 -0
  185. package/src/prompts/tools/job.md +1 -1
  186. package/src/sdk/acp/adapter.ts +5 -0
  187. package/src/sdk/broker/broker.ts +1646 -27
  188. package/src/sdk/broker/endpoint-authority.ts +86 -0
  189. package/src/sdk/broker/ensure.ts +81 -3
  190. package/src/sdk/broker/index.ts +3 -0
  191. package/src/sdk/broker/lifecycle-ledger.ts +6 -0
  192. package/src/sdk/broker/lifecycle.ts +440 -75
  193. package/src/sdk/broker/master-capability.ts +232 -0
  194. package/src/sdk/broker/session-index.ts +121 -17
  195. package/src/sdk/broker/session-scope.ts +272 -0
  196. package/src/sdk/broker/spawn-authority.ts +1031 -0
  197. package/src/sdk/broker/spawn-substrate.ts +591 -0
  198. package/src/sdk/broker/startup-budget.ts +144 -2
  199. package/src/sdk/broker/state-version.ts +43 -14
  200. package/src/sdk/broker/transport.ts +1 -0
  201. package/src/sdk/broker/worktree-occupancy.ts +90 -0
  202. package/src/sdk/bus/chat-daemon-control.ts +5 -2
  203. package/src/sdk/bus/chat-daemon-runtime.ts +136 -18
  204. package/src/sdk/bus/discord-daemon.ts +50 -10
  205. package/src/sdk/bus/index.ts +84 -7
  206. package/src/sdk/bus/kind-aware-reconciliation.ts +296 -48
  207. package/src/sdk/bus/lifecycle-commands.ts +4 -0
  208. package/src/sdk/bus/notification-service.ts +22 -5
  209. package/src/sdk/bus/prompt-reconciliation.ts +7 -2
  210. package/src/sdk/bus/reconciliation-store.ts +55 -31
  211. package/src/sdk/bus/slack-conversation.ts +1 -1
  212. package/src/sdk/bus/slack-daemon.ts +72 -38
  213. package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
  214. package/src/sdk/cli/index.ts +1 -0
  215. package/src/sdk/cli/master-cli.ts +259 -0
  216. package/src/sdk/cli/rows.ts +41 -15
  217. package/src/sdk/cli/session-cli.ts +297 -26
  218. package/src/sdk/client/client.ts +14 -8
  219. package/src/sdk/host/control/dispatch.ts +101 -40
  220. package/src/sdk/host/control/index.ts +2 -0
  221. package/src/sdk/host/control/operations.ts +2 -0
  222. package/src/sdk/host/control/runtime-gate.ts +203 -11
  223. package/src/sdk/host/events.ts +1 -0
  224. package/src/sdk/host/host.ts +29 -2
  225. package/src/sdk/host/query/cursor.ts +52 -7
  226. package/src/sdk/host/query/handlers.ts +33 -12
  227. package/src/sdk/host/query/revision-store.ts +12 -0
  228. package/src/sdk/host/session-runtime.test.ts +673 -14
  229. package/src/sdk/host/session-runtime.ts +674 -103
  230. package/src/sdk/host/surface-policy.ts +3 -3
  231. package/src/sdk/lifecycle/broker-client.ts +28 -0
  232. package/src/sdk/lifecycle/service.ts +279 -20
  233. package/src/sdk/lifecycle-session.ts +17 -4
  234. package/src/sdk/prompt-deadline-manager.ts +2 -1
  235. package/src/sdk/protocol/operation-inventory.generated.json +105 -3
  236. package/src/sdk/protocol/operation-registry.ts +14 -4
  237. package/src/sdk/receipt-state.ts +10 -0
  238. package/src/sdk/router/session-router.ts +219 -118
  239. package/src/sdk/session.ts +416 -195
  240. package/src/sdk/startup-capability.ts +1 -0
  241. package/src/sdk/turn-result.ts +22 -0
  242. package/src/session/agent-session.ts +1628 -318
  243. package/src/session/blob-store.ts +15 -5
  244. package/src/session/credential-store-errors.ts +2 -0
  245. package/src/session/fold-coordinator.ts +40 -3
  246. package/src/session/internal/managed-session-storage.ts +153 -78
  247. package/src/session/session-manager.ts +850 -97
  248. package/src/session/yield-queue.ts +24 -10
  249. package/src/setup/frictionless-onboarding.ts +2 -0
  250. package/src/slash-commands/builtin-registry.ts +41 -0
  251. package/src/task/executor.ts +9 -4
  252. package/src/telemetry/events.ts +1138 -12
  253. package/src/tools/ask-contract.test.ts +64 -0
  254. package/src/tools/ask-contract.ts +36 -1
  255. package/src/tools/ask.ts +10 -8
  256. package/src/tools/bash-interactive.ts +11 -2
  257. package/src/tools/bash.ts +412 -75
  258. package/src/tools/browser/launch.ts +37 -0
  259. package/src/tools/browser/registry.ts +99 -9
  260. package/src/tools/cron.ts +6 -4
  261. package/src/tools/descriptors.test.ts +15 -0
  262. package/src/tools/descriptors.ts +5 -1
  263. package/src/tools/gh.ts +104 -48
  264. package/src/tools/github-cache.ts +2 -2
  265. package/src/tools/index.ts +10 -6
  266. package/src/tools/job.ts +4 -1
  267. package/src/tools/monitor.ts +12 -9
  268. package/src/tools/resolve.ts +30 -10
  269. package/src/tools/tool-catalog.generated.ts +2 -2
  270. package/src/utils/external-editor.ts +72 -1
@@ -166,8 +166,9 @@ export class DiscordNotificationDaemon {
166
166
  expectedGeneration?: number,
167
167
  ) => SessionAttachment | null | Promise<SessionAttachment | null>;
168
168
  readonly #effects: ChatEffectJournal;
169
- readonly #activeWork = new Set<Promise<unknown>>();
169
+ readonly #activeWork = new Map<Promise<unknown>, string | undefined>();
170
170
  readonly #workInvalidators = new Set<() => Promise<void>>();
171
+ readonly #sessionInvalidators = new Map<string, Set<() => Promise<void>>>();
171
172
  readonly #inflightInbound = new Set<string>();
172
173
  #started = false;
173
174
  #lifecycleGeneration = 0;
@@ -264,7 +265,8 @@ export class DiscordNotificationDaemon {
264
265
  await this.options.provider.start(
265
266
  async event => {
266
267
  if (lifecycleGeneration !== this.#lifecycleGeneration || !this.#started) return;
267
- await this.#track(this.handleInbound(event));
268
+ const record = await this.#byThread(event.guildId, event.parentId, event.threadId);
269
+ await this.#track(this.handleInbound(event), record?.sessionId);
268
270
  },
269
271
  () => {},
270
272
  );
@@ -375,6 +377,9 @@ export class DiscordNotificationDaemon {
375
377
  this.#workGeneration += 1;
376
378
  await Promise.allSettled([...this.#workInvalidators].map(invalidate => invalidate()));
377
379
  }
380
+ async #invalidateSessionWork(sessionId: string): Promise<void> {
381
+ await Promise.allSettled([...(this.#sessionInvalidators.get(sessionId) ?? [])].map(invalidate => invalidate()));
382
+ }
378
383
 
379
384
  async #drainActiveWork(deadline: number): Promise<void> {
380
385
  while (this.#activeWork.size > 0) {
@@ -385,7 +390,7 @@ export class DiscordNotificationDaemon {
385
390
  return;
386
391
  }
387
392
  const settled = await Promise.race([
388
- Promise.allSettled([...this.#activeWork]).then(() => true),
393
+ Promise.allSettled([...this.#activeWork.keys()]).then(() => true),
389
394
  Bun.sleep(remaining).then(() => false),
390
395
  ]);
391
396
  if (!settled) {
@@ -395,6 +400,17 @@ export class DiscordNotificationDaemon {
395
400
  }
396
401
  }
397
402
  }
403
+ async #drainSessionWork(sessionId: string, deadline: number): Promise<void> {
404
+ while (true) {
405
+ const work = [...this.#activeWork.entries()]
406
+ .filter(([, activeSessionId]) => activeSessionId === sessionId)
407
+ .map(([promise]) => promise);
408
+ if (work.length === 0) return;
409
+ const remaining = deadline - this.#now();
410
+ if (remaining <= 0) return;
411
+ await Promise.race([Promise.allSettled(work), Bun.sleep(remaining)]);
412
+ }
413
+ }
398
414
 
399
415
  #recordProviderLifecycleError(error: unknown): void {
400
416
  this.#providerLifecycleError = error;
@@ -422,7 +438,7 @@ export class DiscordNotificationDaemon {
422
438
  * legitimate create into a spurious teardown rejection.
423
439
  */
424
440
  async notify(input: DiscordNotificationInput): Promise<DiscordConversation> {
425
- return await this.#track(this.#notify(input));
441
+ return await this.#track(this.#notify(input), input.sessionId);
426
442
  }
427
443
  async #notify(input: DiscordNotificationInput): Promise<DiscordConversation> {
428
444
  await this.#requireLiveBinding(input.sessionId, input.endpointGeneration, input.attachmentAuthorityId);
@@ -457,7 +473,7 @@ export class DiscordNotificationDaemon {
457
473
 
458
474
  /** Posts a safe command outcome to the active mapped conversation. */
459
475
  async postCommandResult(sessionId: string, content: string): Promise<boolean> {
460
- return await this.#track(this.#postCommandResult(sessionId, content));
476
+ return await this.#track(this.#postCommandResult(sessionId, content), sessionId);
461
477
  }
462
478
  async #postCommandResult(sessionId: string, content: string): Promise<boolean> {
463
479
  const record = await this.#bySession(sessionId);
@@ -471,13 +487,20 @@ export class DiscordNotificationDaemon {
471
487
  if (closing) await this.#driveClose(closing);
472
488
  }
473
489
 
474
- async recoverCleanup(sessionId: string, endpointGeneration: number): Promise<void> {
490
+ async recoverCleanup(sessionId: string, endpointGeneration: number, authorityId?: string): Promise<boolean> {
475
491
  const record = await this.#bySession(sessionId);
476
- if (record?.endpointGeneration !== endpointGeneration || !closingIntent(record)) return;
492
+ if (record?.endpointGeneration !== endpointGeneration) return true;
493
+ if (record?.state === "closed" || record?.state === "archived" || record?.state === "resuming") return true;
494
+ if (!closingIntent(record) && record?.state === "active" && record.attachmentAuthorityId === authorityId)
495
+ return true;
496
+ if (!closingIntent(record)) return false;
477
497
  await this.#driveClose(record);
498
+ return true;
478
499
  }
479
500
 
480
501
  async retireAttachment(sessionId: string, endpointGeneration: number): Promise<void> {
502
+ await this.#invalidateSessionWork(sessionId);
503
+ await this.#drainSessionWork(sessionId, this.#now() + 5_000);
481
504
  const intentKey = this.#intentKey(sessionId);
482
505
  const now = this.#now();
483
506
  await this.#store.transact(intentKey, current => {
@@ -548,7 +571,7 @@ export class DiscordNotificationDaemon {
548
571
  endpointGeneration: number,
549
572
  attachmentAuthorityId?: string,
550
573
  ): Promise<DiscordConversation | undefined> {
551
- return await this.#track(this.#resumeAdmission(sessionId, endpointGeneration, attachmentAuthorityId));
574
+ return await this.#track(this.#resumeAdmission(sessionId, endpointGeneration, attachmentAuthorityId), sessionId);
552
575
  }
553
576
  async #resumeAdmission(
554
577
  sessionId: string,
@@ -665,8 +688,8 @@ export class DiscordNotificationDaemon {
665
688
  }
666
689
  }
667
690
 
668
- #track<T>(work: Promise<T>): Promise<T> {
669
- this.#activeWork.add(work);
691
+ #track<T>(work: Promise<T>, sessionId?: string): Promise<T> {
692
+ this.#activeWork.set(work, sessionId);
670
693
  return work.finally(() => this.#activeWork.delete(work));
671
694
  }
672
695
 
@@ -1871,6 +1894,11 @@ export class DiscordNotificationDaemon {
1871
1894
  #intentKey(sessionId: string): string {
1872
1895
  return `${this.options.provider.applicationId}:${this.options.guildId}:${this.options.parentChannelId}:creating:${sessionId}`;
1873
1896
  }
1897
+ #removeSessionInvalidator(sessionId: string, set: Set<() => Promise<void>>, invalidate: () => Promise<void>): void {
1898
+ set.delete(invalidate);
1899
+ if (set.size === 0 && this.#sessionInvalidators.get(sessionId) === set)
1900
+ this.#sessionInvalidators.delete(sessionId);
1901
+ }
1874
1902
  async #withCreateIntentLease<T>(intent: DiscordConversation, work: () => Promise<T>): Promise<T> {
1875
1903
  let lost = false;
1876
1904
  let renewal: Promise<boolean> | undefined;
@@ -1886,6 +1914,9 @@ export class DiscordNotificationDaemon {
1886
1914
  await abandonCurrent();
1887
1915
  };
1888
1916
  this.#workInvalidators.add(invalidate);
1917
+ const sessionInvalidators = this.#sessionInvalidators.get(intent.sessionId!) ?? new Set<() => Promise<void>>();
1918
+ sessionInvalidators.add(invalidate);
1919
+ this.#sessionInvalidators.set(intent.sessionId!, sessionInvalidators);
1889
1920
  const renew = async (): Promise<boolean> => {
1890
1921
  if (lost || workGeneration !== this.#workGeneration) {
1891
1922
  await invalidate();
@@ -1953,6 +1984,7 @@ export class DiscordNotificationDaemon {
1953
1984
  } finally {
1954
1985
  clearInterval(timer);
1955
1986
  this.#workInvalidators.delete(invalidate);
1987
+ this.#removeSessionInvalidator(intent.sessionId!, sessionInvalidators, invalidate);
1956
1988
  }
1957
1989
  }
1958
1990
  async #clearPending(record: DiscordConversation, actionId: string, actionNonce: string): Promise<void> {
@@ -2076,6 +2108,12 @@ export class DiscordNotificationDaemon {
2076
2108
  );
2077
2109
  };
2078
2110
  this.#workInvalidators.add(invalidate);
2111
+ let sessionInvalidators: Set<() => Promise<void>> | undefined;
2112
+ if (sessionId !== undefined) {
2113
+ sessionInvalidators = this.#sessionInvalidators.get(sessionId) ?? new Set<() => Promise<void>>();
2114
+ sessionInvalidators.add(invalidate);
2115
+ this.#sessionInvalidators.set(sessionId, sessionInvalidators);
2116
+ }
2079
2117
  const renewLease = async (): Promise<boolean> => {
2080
2118
  if (renewalLost || workGeneration !== this.#workGeneration) {
2081
2119
  renewalLost = true;
@@ -2136,6 +2174,8 @@ export class DiscordNotificationDaemon {
2136
2174
  } finally {
2137
2175
  clearInterval(timer);
2138
2176
  this.#workInvalidators.delete(invalidate);
2177
+ if (sessionId !== undefined && sessionInvalidators !== undefined)
2178
+ this.#removeSessionInvalidator(sessionId, sessionInvalidators, invalidate);
2139
2179
  }
2140
2180
  }
2141
2181
 
@@ -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";
@@ -371,6 +374,8 @@ export type LifecycleErrorReason =
371
374
  | "spawn_failed"
372
375
  | "discovery_timeout"
373
376
  | "readiness_timeout"
377
+ | "worktree_preparation_timeout"
378
+ | "dependency_preparation_timeout"
374
379
  | "close_refused"
375
380
  | "not_found"
376
381
  | "terminal_uncertain"
@@ -1220,6 +1225,8 @@ interface SessionRuntime {
1220
1225
  disposeGateTerminalController: () => void;
1221
1226
  disposeAckRecoveryParticipant: () => void;
1222
1227
  disposeGateEmitterListener: () => void;
1228
+ /** Deregisters this session's job-fold listener (`bash_folded`). */
1229
+ disposeFoldListener: () => void;
1223
1230
  /** Aborts and fences side turns while notification delivery is disabled. */
1224
1231
  disableEphemeralTurns: () => void;
1225
1232
  waitForGateResolutionQuiescence: () => Promise<void>;
@@ -2381,6 +2388,7 @@ function sdkQuerySurface(
2381
2388
  steerStatusLookup: (selector: { commandId?: string; turnId?: string; clientRef?: string }) => unknown = () => ({
2382
2389
  status: "unknown",
2383
2390
  }),
2391
+ getRuntimeHost: () => SessionSdkHost | undefined = () => undefined,
2384
2392
  ): SessionSurface {
2385
2393
  return createSdkSurfaceFactory({
2386
2394
  ctx,
@@ -2393,6 +2401,7 @@ function sdkQuerySurface(
2393
2401
  turnResultLookup,
2394
2402
  steerStatusLookup,
2395
2403
  hostTools: () => getInstalledDefinitions("host_tools") !== undefined,
2404
+ getRuntimeHost,
2396
2405
  }).query;
2397
2406
  }
2398
2407
 
@@ -3519,7 +3528,7 @@ function sdkControlSurface(
3519
3528
  setQueueMode: (kind, mode) => {
3520
3529
  if (!bindings.has("setQueueMode"))
3521
3530
  return unavailable(`queue.${kind}_mode.set`, "no session queue-mode seam is installed")();
3522
- if (!ctx.setQueueMode(kind as "steering" | "follow_up" | "interrupt", mode))
3531
+ if (!ctx.setQueueMode(kind as "steering" | "follow_up" | "tool_interrupt", mode))
3523
3532
  throw Object.assign(new Error("Invalid queue mode."), { code: "invalid_input" });
3524
3533
  return { changed: true };
3525
3534
  },
@@ -4043,6 +4052,12 @@ export function createNotificationsExtension(
4043
4052
  controller?: NotificationSessionController;
4044
4053
  /** Whether this host mode can own the root SDK endpoint. Default: true. */
4045
4054
  sdkHostModeSupported?: boolean;
4055
+ /** In-memory master capability for private broker verification only. */
4056
+ masterCapability?: string;
4057
+ /** Opaque direct-role epoch this effective host may adopt. */
4058
+ masterAttestationEpoch?: string;
4059
+ /** Stable master lineage identity retained across session switches/branches. */
4060
+ masterOwnerSessionId?: string;
4046
4061
  onSdkRequest?: (kind: "control" | "query", connectionId: string, frame: Record<string, unknown>) => void;
4047
4062
  runBtwTurn?: (question: string, signal: AbortSignal) => Promise<{ replyText: string }>;
4048
4063
  /** Observes settlement of optional session-branch startup after reconciliation completes. */
@@ -4103,6 +4118,7 @@ export function createNotificationsExtension(
4103
4118
  }
4104
4119
  | undefined;
4105
4120
  let extensionShuttingDown = false;
4121
+ const consumedMasterNonces = new Map<string, number>();
4106
4122
 
4107
4123
  async function ensureTelegramOwner(settings: Settings): Promise<"ready" | "blocked_identity"> {
4108
4124
  if (!telegramTopicsEnabled()) return "blocked_identity";
@@ -4368,8 +4384,14 @@ export function createNotificationsExtension(
4368
4384
  rt.workflowGate?.setRuntimeTurnProvider?.(null);
4369
4385
  } catch {}
4370
4386
  try {
4387
+ const attachedClients = rt.server.clientCount();
4371
4388
  const delivered = await pushTerminalSessionFrame(rt, { type: "session_closed", sessionId: id });
4372
- if (!delivered) logger.warn("notifications: session_closed socket delivery was not acknowledged");
4389
+ if (!delivered) {
4390
+ const fields = { sessionId: id, endpointScope: rt.endpointScope, attachedClients };
4391
+ if (attachedClients > 0)
4392
+ logger.warn("notifications: session_closed socket delivery was not acknowledged", fields);
4393
+ else logger.debug("notifications: session_closed had no attached client to acknowledge", fields);
4394
+ }
4373
4395
  } catch (e) {
4374
4396
  logger.warn(`notifications: session_closed failed: ${String(e)}`);
4375
4397
  }
@@ -4380,6 +4402,9 @@ export function createNotificationsExtension(
4380
4402
  try {
4381
4403
  rt.disposeGateEmitterListener();
4382
4404
  } catch {}
4405
+ try {
4406
+ rt.disposeFoldListener();
4407
+ } catch {}
4383
4408
  rt.gatePresentations?.dispose();
4384
4409
  try {
4385
4410
  rt.disposeGateTerminalController();
@@ -4489,12 +4514,14 @@ export function createNotificationsExtension(
4489
4514
 
4490
4515
  async function startSession(ctx: ExtensionContext): Promise<SessionStartResult> {
4491
4516
  const id = sessionId(ctx);
4492
- const lifecycleRequestId = safeLifecycleRequestId(process.env.GJC_LIFECYCLE_REQUEST_ID);
4493
4517
  const { settings, cfg, settingsAvailable } = resolveSettings(options.settings);
4494
4518
  const notificationsEnabledForSession = controller.query(ctx).genericSessionEnabled;
4495
4519
  const sdkEnabledForSession =
4496
4520
  (options.sdkHostModeSupported ?? true) && shouldHostSdk(settings, isNotificationEligibleContext(ctx));
4497
4521
  const lifecycleRequired = lifecycleStartupCapability !== undefined;
4522
+ const lifecycleRequestId = lifecycleRequired
4523
+ ? safeLifecycleRequestId(lifecycleStartupCapability.lifecycleRequestId)
4524
+ : safeLifecycleRequestId(process.env.GJC_LIFECYCLE_REQUEST_ID);
4498
4525
  /** The broker-issued readiness intent for this exact lifecycle-managed session. */
4499
4526
  const lifecycleReadiness = lifecycleStartupCapability?.readiness ?? "immediate";
4500
4527
  const failLifecycleStartup = (
@@ -5270,7 +5297,6 @@ export function createNotificationsExtension(
5270
5297
  submission.reconciliationKind,
5271
5298
  correlation,
5272
5299
  winner,
5273
- undefined,
5274
5300
  extra?.error,
5275
5301
  extra?.finalText,
5276
5302
  );
@@ -5442,6 +5468,7 @@ export function createNotificationsExtension(
5442
5468
  settings,
5443
5469
  selector => kindReconciliation.lookupResult(selector.kind, selector),
5444
5470
  selector => kindReconciliation.lookupSteer(selector),
5471
+ () => host,
5445
5472
  ),
5446
5473
  id,
5447
5474
  revisions,
@@ -6588,6 +6615,7 @@ export function createNotificationsExtension(
6588
6615
  }
6589
6616
 
6590
6617
  sdkRuntime = new SessionSdkSessionRuntime({
6618
+ masterOwnerSessionId: options.masterOwnerSessionId,
6591
6619
  transport: {
6592
6620
  sessionId: id,
6593
6621
  stateRoot,
@@ -6607,6 +6635,13 @@ export function createNotificationsExtension(
6607
6635
  ...(activationGate ? { activationGate } : {}),
6608
6636
  ...(settings ? { settings } : {}),
6609
6637
  ...(configOverrides ? { configOverrides } : {}),
6638
+ masterCapabilityVerify: frame =>
6639
+ verifyMasterCapabilityFrame({
6640
+ frame,
6641
+ expectedCapability: options.masterCapability,
6642
+ expectedEpoch: options.masterAttestationEpoch,
6643
+ replay: consumedMasterNonces,
6644
+ }),
6610
6645
  connectionCapabilities: connectionId => hostCapCache.get(connectionId),
6611
6646
  installProviderDefinitions,
6612
6647
  onProviderDefinitionsRemoved: removeProviderDefinitions,
@@ -6965,6 +7000,7 @@ export function createNotificationsExtension(
6965
7000
  ]);
6966
7001
  },
6967
7002
  disposeGateEmitterListener: () => {},
7003
+ disposeFoldListener: () => {},
6968
7004
  trackGateResolution,
6969
7005
  waitForGateResolutionQuiescence: async () => {
6970
7006
  await Promise.allSettled(inFlightGateResolutions);
@@ -7751,6 +7787,8 @@ export function createNotificationsExtension(
7751
7787
  }
7752
7788
  const agentDir = lifecycleAgentDir ?? settings?.getAgentDir?.();
7753
7789
  if (lifecycleRequired && !agentDir) throw new Error("Lifecycle SDK host requires an agent directory.");
7790
+ if (lifecycleRequired && !lifecycleRequestId)
7791
+ throw new Error("Lifecycle SDK host requires a capability-bound request identity.");
7754
7792
 
7755
7793
  if (agentDir) {
7756
7794
  try {
@@ -7758,13 +7796,36 @@ export function createNotificationsExtension(
7758
7796
  throwIfLifecycleStopped();
7759
7797
  const index = await new SessionIndex(agentDir).open();
7760
7798
  throwIfLifecycleStopped();
7761
- const locator = { repo: path.resolve(ctx.cwd), stateRoot: endpointStateRoot };
7762
- const endpointMtimeMs = fs.statSync(path.join(endpointStateRoot, "sdk", `${id}.json`)).mtimeMs;
7799
+ const locator = await resolveSessionLocator(ctx.cwd, endpointStateRoot);
7800
+ const endpointPath = path.join(endpointStateRoot, "sdk", `${id}.json`);
7801
+ const endpointMtimeMs = fs.statSync(endpointPath).mtimeMs;
7802
+ const endpointIdentity = fs.statSync(endpointPath, { bigint: true });
7803
+ const endpointFileId = `${endpointIdentity.dev}:${endpointIdentity.ino}`;
7763
7804
  const hostProcessIncarnation = processIncarnation(process.pid);
7805
+ const direct = await reattestMasterSessionIdentity({
7806
+ index,
7807
+ locator,
7808
+ masterCapability: options.masterCapability,
7809
+ attestationEpoch: options.masterAttestationEpoch,
7810
+ ownerSessionId: options.masterOwnerSessionId,
7811
+ sessionId: id,
7812
+ pid: process.pid,
7813
+ processIncarnation: hostProcessIncarnation,
7814
+ });
7815
+ throwIfLifecycleStopped();
7764
7816
  await host.registerWithBroker({
7765
7817
  // The endpoint is written before registration. Its exact mtime
7766
7818
  // binds this index generation to that discovery record.
7767
7819
  register: async input => {
7820
+ const masterRole = masterAttestationForEffectiveHost({
7821
+ masterCapability: options.masterCapability,
7822
+ attestationEpoch: options.masterAttestationEpoch,
7823
+ ownerSessionId: options.masterOwnerSessionId,
7824
+ sessionId: input.sessionId,
7825
+ pid: process.pid,
7826
+ processIncarnation: hostProcessIncarnation,
7827
+ direct,
7828
+ });
7768
7829
  await index.append({
7769
7830
  type: "host_registered",
7770
7831
  ...input,
@@ -7776,6 +7837,8 @@ export function createNotificationsExtension(
7776
7837
  // teardown identity provable after that workspace is gone.
7777
7838
  ...(hostProcessIncarnation ? { processIncarnation: hostProcessIncarnation } : {}),
7778
7839
  endpointMtimeMs,
7840
+ endpointFileId,
7841
+ ...(masterRole ? { masterRole } : {}),
7779
7842
  ...(lifecycleRequestId ? { lifecycleRequestId } : {}),
7780
7843
  });
7781
7844
  },
@@ -7962,6 +8025,20 @@ export function createNotificationsExtension(
7962
8025
  };
7963
8026
  activeRuntime.disposeGateEmitterListener = registerWorkflowGateEmitterListener(id, attachWorkflowGate);
7964
8027
  if (ctx.workflowGate) attachWorkflowGate(ctx.workflowGate);
8028
+ // Every fold (steer, chord, auto-background timer, `bash.background`)
8029
+ // publishes one replayable `bash_folded` frame. `runtime.jobs.list`
8030
+ // `foldReason` remains the source of truth clients reconcile against after
8031
+ // a replay, since a replayed frame may describe a job that has since ended.
8032
+ activeRuntime.disposeFoldListener =
8033
+ ctx.onJobFold?.(event => {
8034
+ emitSessionEvent(activeRuntime, {
8035
+ type: "bash_folded",
8036
+ sessionId: id,
8037
+ jobId: event.jobId,
8038
+ generation: event.generation,
8039
+ reason: event.reason,
8040
+ });
8041
+ }) ?? (() => {});
7965
8042
  finishStartup({ status: "started", runtime: initializedRuntime });
7966
8043
  return { status: "started", runtime: initializedRuntime };
7967
8044
  } catch (e) {