@gajae-code/coding-agent 0.13.2 → 0.14.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 (610) hide show
  1. package/CHANGELOG.md +215 -8
  2. package/README.md +2 -0
  3. package/bin/gajaessi.js +4 -0
  4. package/dist/types/async/job-manager.d.ts +55 -2
  5. package/dist/types/async/support.d.ts +14 -0
  6. package/dist/types/capability/mcp.d.ts +6 -0
  7. package/dist/types/capability/types.d.ts +6 -0
  8. package/dist/types/cli/accounts-cli.d.ts +19 -0
  9. package/dist/types/cli/args.d.ts +3 -0
  10. package/dist/types/cli/config-cli.d.ts +6 -0
  11. package/dist/types/cli/crash-cli.d.ts +10 -0
  12. package/dist/types/cli/customize-doctor.d.ts +145 -0
  13. package/dist/types/cli/daemon-cli.d.ts +3 -1
  14. package/dist/types/cli/mcp-cli.d.ts +11 -0
  15. package/dist/types/cli/notify-cli.d.ts +1 -1
  16. package/dist/types/cli/root-flags.d.ts +12 -0
  17. package/dist/types/cli/setup-cli.d.ts +3 -1
  18. package/dist/types/cli/skills-cli.d.ts +2 -1
  19. package/dist/types/cli/update-cli.d.ts +19 -1
  20. package/dist/types/cli.d.ts +6 -0
  21. package/dist/types/commands/accounts.d.ts +40 -0
  22. package/dist/types/commands/crash.d.ts +24 -0
  23. package/dist/types/commands/customize.d.ts +22 -0
  24. package/dist/types/commands/daemon.d.ts +1 -1
  25. package/dist/types/commands/launch.d.ts +6 -0
  26. package/dist/types/commands/setup.d.ts +6 -0
  27. package/dist/types/commands/skills.d.ts +6 -1
  28. package/dist/types/commands/update.d.ts +7 -0
  29. package/dist/types/config/atomic-yaml-patch-worker.d.ts +25 -0
  30. package/dist/types/config/atomic-yaml-patch.d.ts +62 -2
  31. package/dist/types/config/config-schema-version.d.ts +10 -0
  32. package/dist/types/config/file-lock.d.ts +18 -0
  33. package/dist/types/config/model-profile-activation.d.ts +3 -1
  34. package/dist/types/config/model-registry.d.ts +10 -1
  35. package/dist/types/config/models-config-schema.d.ts +2 -0
  36. package/dist/types/config/settings-schema.d.ts +73 -9
  37. package/dist/types/config/settings.d.ts +63 -0
  38. package/dist/types/config/skill-settings-defaults.d.ts +26 -0
  39. package/dist/types/config/task-delegation.d.ts +39 -0
  40. package/dist/types/config/update-channel.d.ts +10 -0
  41. package/dist/types/coordinator-mcp/server.d.ts +5 -1
  42. package/dist/types/crash/index-store.d.ts +87 -0
  43. package/dist/types/crash/nudge.d.ts +50 -0
  44. package/dist/types/crash/record-loader.d.ts +17 -0
  45. package/dist/types/crash/report.d.ts +134 -0
  46. package/dist/types/crash/sanitize.d.ts +25 -0
  47. package/dist/types/cursor.d.ts +13 -2
  48. package/dist/types/customization/import.d.ts +37 -0
  49. package/dist/types/customization/index.d.ts +4 -0
  50. package/dist/types/customization/inventory.d.ts +25 -0
  51. package/dist/types/customization/mutations.d.ts +32 -0
  52. package/dist/types/customization/types.d.ts +115 -0
  53. package/dist/types/daemon/control-types.d.ts +8 -1
  54. package/dist/types/daemon/operator-contract.d.ts +4 -4
  55. package/dist/types/discovery/claude.d.ts +39 -1
  56. package/dist/types/discovery/codex.d.ts +39 -1
  57. package/dist/types/discovery/index.d.ts +2 -0
  58. package/dist/types/discovery/mcp-compat.d.ts +30 -0
  59. package/dist/types/edit/renderer.d.ts +23 -0
  60. package/dist/types/extensibility/extensions/runner.d.ts +3 -0
  61. package/dist/types/extensibility/extensions/types.d.ts +21 -4
  62. package/dist/types/extensibility/gjc-plugins/observability.d.ts +6 -1
  63. package/dist/types/extensibility/gjc-plugins/registry.d.ts +8 -1
  64. package/dist/types/extensibility/hooks/loader.d.ts +19 -5
  65. package/dist/types/extensibility/runtime-skill-discovery.d.ts +22 -1
  66. package/dist/types/extensibility/skill-management.d.ts +107 -0
  67. package/dist/types/gjc-runtime/deep-interview-runtime.d.ts +3 -1
  68. package/dist/types/gjc-runtime/launch-worktree.d.ts +8 -0
  69. package/dist/types/gjc-runtime/linux-proc.d.ts +3 -0
  70. package/dist/types/gjc-runtime/ralplan-runtime.d.ts +10 -6
  71. package/dist/types/gjc-runtime/session-state-lock.d.ts +104 -0
  72. package/dist/types/gjc-runtime/session-state-sidecar.d.ts +120 -2
  73. package/dist/types/gjc-runtime/state-writer.d.ts +10 -1
  74. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +4 -2
  75. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -6
  76. package/dist/types/gjc-runtime/workflow-settings.d.ts +109 -0
  77. package/dist/types/hooks/events.d.ts +60 -0
  78. package/dist/types/hooks/normalize.d.ts +83 -0
  79. package/dist/types/hooks/skill-state.d.ts +9 -2
  80. package/dist/types/main.d.ts +2 -1
  81. package/dist/types/modes/acp/abort-scope.d.ts +13 -0
  82. package/dist/types/modes/acp/acp-agent.d.ts +7 -4
  83. package/dist/types/modes/components/customization/customization-dashboard.d.ts +44 -0
  84. package/dist/types/modes/components/customization/import-wizard.d.ts +21 -0
  85. package/dist/types/modes/components/customization/index.d.ts +1 -0
  86. package/dist/types/modes/components/dynamic-theme-text.d.ts +9 -0
  87. package/dist/types/modes/components/gajae-pet-widget.d.ts +6 -28
  88. package/dist/types/modes/components/irc-sidebar.d.ts +1 -0
  89. package/dist/types/modes/components/iterm-pet-transport.d.ts +122 -0
  90. package/dist/types/modes/components/oauth-selector.d.ts +10 -4
  91. package/dist/types/modes/components/pet-capability.d.ts +7 -21
  92. package/dist/types/modes/components/settings-selector.d.ts +5 -0
  93. package/dist/types/modes/components/status-line/gh.d.ts +6 -2
  94. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  95. package/dist/types/modes/controllers/selector-controller.d.ts +7 -8
  96. package/dist/types/modes/controllers/stt-controller.d.ts +1 -1
  97. package/dist/types/modes/interactive-mode.d.ts +1 -0
  98. package/dist/types/modes/theme/defaults/index.d.ts +118 -0
  99. package/dist/types/modes/types.d.ts +1 -0
  100. package/dist/types/modes/utils/injected-user-submission.d.ts +10 -0
  101. package/dist/types/runtime-mcp/config.d.ts +9 -0
  102. package/dist/types/runtime-mcp/manager.d.ts +26 -1
  103. package/dist/types/runtime-mcp/oauth-discovery.d.ts +4 -0
  104. package/dist/types/runtime-mcp/oauth-flow.d.ts +24 -1
  105. package/dist/types/runtime-mcp/pool-key.d.ts +2 -0
  106. package/dist/types/runtime-mcp/protocol.d.ts +278 -0
  107. package/dist/types/runtime-mcp/scope-config.d.ts +29 -0
  108. package/dist/types/runtime-mcp/tool-bridge.d.ts +15 -13
  109. package/dist/types/runtime-mcp/transports/http.d.ts +9 -0
  110. package/dist/types/runtime-mcp/types.d.ts +102 -10
  111. package/dist/types/sdk/acp/adapter.d.ts +26 -5
  112. package/dist/types/sdk/broker/broker.d.ts +3 -55
  113. package/dist/types/sdk/broker/lifecycle.d.ts +18 -12
  114. package/dist/types/sdk/broker/session-index.d.ts +14 -5
  115. package/dist/types/sdk/broker/startup-failure.d.ts +16 -1
  116. package/dist/types/sdk/bus/chat-daemon-control.d.ts +51 -1
  117. package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -41
  118. package/dist/types/sdk/bus/config.d.ts +11 -0
  119. package/dist/types/sdk/bus/conversation-store.d.ts +0 -1
  120. package/dist/types/sdk/bus/daemon-paths.d.ts +6 -0
  121. package/dist/types/sdk/bus/discord-conversation.d.ts +2 -0
  122. package/dist/types/sdk/bus/discord-daemon.d.ts +17 -12
  123. package/dist/types/sdk/bus/existing-thread-readiness.d.ts +4 -34
  124. package/dist/types/sdk/bus/inbound-reaction-ordering.d.ts +61 -0
  125. package/dist/types/sdk/bus/index.d.ts +85 -99
  126. package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +29 -7
  127. package/dist/types/sdk/bus/managed-daemon.d.ts +2 -32
  128. package/dist/types/sdk/bus/notification-service.d.ts +52 -0
  129. package/dist/types/sdk/bus/operator-runtime.d.ts +1 -1
  130. package/dist/types/sdk/bus/prompt-reconciliation.d.ts +16 -5
  131. package/dist/types/sdk/bus/reconciliation-store.d.ts +113 -5
  132. package/dist/types/sdk/bus/session-control.d.ts +25 -1
  133. package/dist/types/sdk/bus/slack-conversation.d.ts +4 -0
  134. package/dist/types/sdk/bus/slack-daemon.d.ts +9 -17
  135. package/dist/types/sdk/bus/slack-live-provider.d.ts +3 -1
  136. package/dist/types/sdk/bus/slack-provider.d.ts +4 -0
  137. package/dist/types/sdk/bus/slack-thread-binding.d.ts +6 -40
  138. package/dist/types/sdk/bus/telegram-adoption-intent.d.ts +30 -104
  139. package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +10 -0
  140. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +262 -19
  141. package/dist/types/sdk/bus/telegram-daemon-orphan-reap.d.ts +125 -0
  142. package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +22 -0
  143. package/dist/types/sdk/bus/telegram-daemon.d.ts +119 -310
  144. package/dist/types/sdk/bus/telegram-reference.d.ts +1 -47
  145. package/dist/types/sdk/bus/threaded-render.d.ts +1 -0
  146. package/dist/types/sdk/bus/topic-registry.d.ts +33 -17
  147. package/dist/types/sdk/cli/rows.d.ts +2 -11
  148. package/dist/types/sdk/cli/session-cli.d.ts +15 -25
  149. package/dist/types/sdk/client/client.d.ts +97 -1
  150. package/dist/types/sdk/host/control/dispatch.d.ts +0 -7
  151. package/dist/types/sdk/host/control/index.d.ts +1 -1
  152. package/dist/types/sdk/host/control/operations.d.ts +18 -5
  153. package/dist/types/sdk/host/control/runtime-gate.d.ts +19 -0
  154. package/dist/types/sdk/host/host.d.ts +15 -1
  155. package/dist/types/sdk/host/query/handlers.d.ts +5 -0
  156. package/dist/types/sdk/host/reverse-leases.d.ts +1 -1
  157. package/dist/types/sdk/host/session-runtime.d.ts +127 -7
  158. package/dist/types/sdk/host/types.d.ts +1 -1
  159. package/dist/types/sdk/host/websocket-transport.d.ts +1 -1
  160. package/dist/types/sdk/index.d.ts +2 -1
  161. package/dist/types/sdk/lifecycle/broker-client.d.ts +8 -0
  162. package/dist/types/sdk/lifecycle/client.d.ts +59 -0
  163. package/dist/types/sdk/lifecycle/index.d.ts +3 -0
  164. package/dist/types/sdk/lifecycle/service.d.ts +232 -0
  165. package/dist/types/sdk/mcp/server.d.ts +9 -6
  166. package/dist/types/sdk/prompt-deadline-lease.d.ts +43 -0
  167. package/dist/types/sdk/prompt-deadline-manager.d.ts +28 -0
  168. package/dist/types/sdk/prompt-status.d.ts +10 -3
  169. package/dist/types/sdk/protocol/operation-registry.d.ts +5 -0
  170. package/dist/types/sdk/receipt-state.d.ts +17 -0
  171. package/dist/types/sdk/reconciliation-extensions.d.ts +2 -0
  172. package/dist/types/sdk/router/index.d.ts +1 -0
  173. package/dist/types/sdk/router/session-router.d.ts +152 -0
  174. package/dist/types/sdk/session-list.d.ts +22 -0
  175. package/dist/types/sdk/session-reconnect.d.ts +43 -1
  176. package/dist/types/sdk/session.d.ts +34 -1
  177. package/dist/types/sdk/transport/serve-cli.d.ts +1 -4
  178. package/dist/types/session/account-inventory.d.ts +86 -0
  179. package/dist/types/session/agent-session.d.ts +145 -6
  180. package/dist/types/session/agent-storage.d.ts +10 -0
  181. package/dist/types/session/coordinator-tool-label.d.ts +28 -0
  182. package/dist/types/session/internal/managed-session-storage.d.ts +23 -4
  183. package/dist/types/session/messages.d.ts +1 -1
  184. package/dist/types/session/secure-token-file.d.ts +3 -0
  185. package/dist/types/session/session-manager.d.ts +47 -1
  186. package/dist/types/session/startup-auth-config.d.ts +44 -0
  187. package/dist/types/session/terminal-abort.d.ts +378 -0
  188. package/dist/types/session/yield-queue.d.ts +7 -0
  189. package/dist/types/session-import/claude.d.ts +26 -0
  190. package/dist/types/session-import/codex.d.ts +88 -0
  191. package/dist/types/session-import/command.d.ts +35 -0
  192. package/dist/types/session-import/detect.d.ts +22 -0
  193. package/dist/types/session-import/index.d.ts +7 -0
  194. package/dist/types/session-import/provider-codex.d.ts +22 -0
  195. package/dist/types/session-import/provider-service.d.ts +38 -0
  196. package/dist/types/session-import/redact.d.ts +23 -0
  197. package/dist/types/session-import/service.d.ts +47 -0
  198. package/dist/types/session-import/types.d.ts +120 -0
  199. package/dist/types/setup/paseo/check.d.ts +16 -0
  200. package/dist/types/setup/paseo/install-saga.d.ts +115 -0
  201. package/dist/types/setup/paseo/json-publisher.d.ts +76 -0
  202. package/dist/types/setup/paseo/orchestration-preferences.d.ts +24 -0
  203. package/dist/types/setup/paseo/paseo-ownership.d.ts +87 -0
  204. package/dist/types/setup/paseo/paseo-setup.d.ts +29 -0
  205. package/dist/types/setup/paseo/provider-config.d.ts +35 -0
  206. package/dist/types/setup/paseo/remove.d.ts +14 -0
  207. package/dist/types/setup/paseo/result-types.d.ts +59 -0
  208. package/dist/types/setup/paseo/setup-deps.d.ts +73 -0
  209. package/dist/types/setup/paseo/skills-bridge.d.ts +44 -0
  210. package/dist/types/skill-state/active-state.d.ts +5 -1
  211. package/dist/types/slash-commands/acp-builtins.d.ts +5 -1
  212. package/dist/types/slash-commands/builtin-registry.d.ts +2 -0
  213. package/dist/types/slash-commands/helpers/credential-switch.d.ts +12 -0
  214. package/dist/types/slash-commands/helpers/usage-report.d.ts +5 -6
  215. package/dist/types/slash-commands/types.d.ts +7 -1
  216. package/dist/types/task/executor.d.ts +10 -0
  217. package/dist/types/task/index.d.ts +3 -1
  218. package/dist/types/tools/ask-answer-registry.d.ts +6 -3
  219. package/dist/types/tools/bash-sleep-advisory.d.ts +14 -0
  220. package/dist/types/tools/bash.d.ts +2 -6
  221. package/dist/types/tools/browser/attach.d.ts +11 -0
  222. package/dist/types/tools/browser/launch.d.ts +20 -0
  223. package/dist/types/tools/browser/profile-discovery.d.ts +6 -0
  224. package/dist/types/tools/browser/render.d.ts +1 -0
  225. package/dist/types/tools/browser.d.ts +3 -1
  226. package/dist/types/tools/image-gen.d.ts +10 -2
  227. package/dist/types/tools/index.d.ts +8 -0
  228. package/dist/types/tools/monitor.d.ts +1 -1
  229. package/dist/types/tools/skill-discovery.d.ts +8 -1
  230. package/dist/types/tools/subagent.d.ts +2 -0
  231. package/dist/types/tools/ultragoal-ask-guard.d.ts +2 -2
  232. package/dist/types/utils/herdr-pane.d.ts +76 -0
  233. package/package.json +19 -9
  234. package/scripts/build-binary.ts +2 -1
  235. package/scripts/build-sdk-package-smoke.ts +2 -13
  236. package/scripts/capture-sticky-viewport-showcase.ts +0 -1
  237. package/scripts/compile-args.ts +2 -0
  238. package/scripts/generate-sdk-adapter-parity-manifest.ts +31 -16
  239. package/scripts/generate-sdk-operation-inventory.ts +28 -0
  240. package/scripts/generate-telegram-baseline-manifest.ts +1 -0
  241. package/scripts/generate-tool-catalog.ts +6 -4
  242. package/scripts/verify-gjc-sdk-canonicalization.ts +5 -39
  243. package/src/async/job-manager.ts +352 -17
  244. package/src/async/support.ts +25 -0
  245. package/src/autoresearch/dashboard.ts +28 -13
  246. package/src/capability/index.ts +3 -1
  247. package/src/capability/mcp.ts +6 -0
  248. package/src/capability/types.ts +6 -0
  249. package/src/cli/accounts-cli.ts +395 -0
  250. package/src/cli/args.ts +23 -0
  251. package/src/cli/auth-broker-cli.ts +136 -66
  252. package/src/cli/auth-gateway-cli.ts +94 -70
  253. package/src/cli/config-cli.ts +27 -2
  254. package/src/cli/crash-cli.ts +78 -0
  255. package/src/cli/customize-doctor.ts +1605 -0
  256. package/src/cli/daemon-cli.ts +12 -3
  257. package/src/cli/fast-help.ts +1 -0
  258. package/src/cli/mcp-cli.ts +128 -60
  259. package/src/cli/notify-cli.ts +8 -14
  260. package/src/cli/root-flags.ts +8 -0
  261. package/src/cli/setup-cli.ts +145 -0
  262. package/src/cli/skills-cli.ts +38 -2
  263. package/src/cli/update-cli.ts +167 -12
  264. package/src/cli.ts +4 -0
  265. package/src/commands/accounts.ts +71 -0
  266. package/src/commands/crash.ts +35 -0
  267. package/src/commands/customize.ts +84 -0
  268. package/src/commands/daemon.ts +2 -2
  269. package/src/commands/deep-interview.ts +2 -0
  270. package/src/commands/launch.ts +1 -0
  271. package/src/commands/mcp.ts +12 -7
  272. package/src/commands/ralplan.ts +2 -0
  273. package/src/commands/sdk.ts +65 -32
  274. package/src/commands/setup.ts +5 -0
  275. package/src/commands/skills.ts +14 -4
  276. package/src/commands/ultragoal.ts +10 -1
  277. package/src/commands/update.ts +26 -15
  278. package/src/config/atomic-yaml-patch-worker.ts +57 -0
  279. package/src/config/atomic-yaml-patch.ts +769 -29
  280. package/src/config/config-schema-version.ts +10 -0
  281. package/src/config/file-lock.ts +70 -1
  282. package/src/config/mcp-schema.json +52 -14
  283. package/src/config/model-profile-activation.ts +23 -2
  284. package/src/config/model-profiles.ts +88 -21
  285. package/src/config/model-registry.ts +147 -21
  286. package/src/config/models-config-schema.ts +1 -1
  287. package/src/config/settings-schema.ts +77 -7
  288. package/src/config/settings.ts +4038 -199
  289. package/src/config/skill-settings-defaults.ts +41 -3
  290. package/src/config/task-delegation.ts +87 -0
  291. package/src/config/update-channel.ts +23 -0
  292. package/src/coordinator-mcp/server.ts +377 -308
  293. package/src/crash/index-store.ts +723 -0
  294. package/src/crash/nudge.ts +89 -0
  295. package/src/crash/record-loader.ts +149 -0
  296. package/src/crash/report.ts +551 -0
  297. package/src/crash/sanitize.ts +129 -0
  298. package/src/cursor.ts +36 -10
  299. package/src/customization/import.ts +946 -0
  300. package/src/customization/index.ts +4 -0
  301. package/src/customization/inventory.ts +335 -0
  302. package/src/customization/mutations.ts +141 -0
  303. package/src/customization/types.ts +223 -0
  304. package/src/daemon/control-types.ts +8 -1
  305. package/src/daemon/operator-contract.ts +6 -6
  306. package/src/defaults/gjc/extensions/grok-cli-vendor/src/models/catalog.ts +31 -6
  307. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -22
  308. package/src/defaults/gjc/skills/ralplan/SKILL.md +36 -23
  309. package/src/defaults/gjc/skills/ultragoal/SKILL.md +16 -0
  310. package/src/discovery/claude-hooks.ts +38 -0
  311. package/src/discovery/claude.ts +142 -209
  312. package/src/discovery/codex-hooks.ts +38 -0
  313. package/src/discovery/codex.ts +132 -198
  314. package/src/discovery/helpers.ts +8 -1
  315. package/src/discovery/index.ts +2 -0
  316. package/src/discovery/mcp-compat.ts +234 -0
  317. package/src/discovery/mcp-json.ts +6 -0
  318. package/src/edit/renderer.ts +40 -0
  319. package/src/edit/streaming.ts +4 -2
  320. package/src/exec/bash-executor.ts +24 -20
  321. package/src/export/html/index.ts +51 -2
  322. package/src/extensibility/extensions/loader.ts +9 -1
  323. package/src/extensibility/extensions/runner.ts +17 -4
  324. package/src/extensibility/extensions/types.ts +31 -6
  325. package/src/extensibility/gjc-plugins/constrained-hooks.ts +33 -0
  326. package/src/extensibility/gjc-plugins/installer.ts +9 -5
  327. package/src/extensibility/gjc-plugins/observability.ts +8 -2
  328. package/src/extensibility/gjc-plugins/registry.ts +13 -2
  329. package/src/extensibility/gjc-plugins/runtime-adapters.ts +3 -1
  330. package/src/extensibility/gjc-plugins/schema.ts +222 -39
  331. package/src/extensibility/hooks/loader.ts +154 -10
  332. package/src/extensibility/runtime-skill-discovery.ts +257 -60
  333. package/src/extensibility/skill-management.ts +368 -0
  334. package/src/extensibility/skills.ts +52 -16
  335. package/src/gjc-runtime/deep-interview-runtime.ts +45 -61
  336. package/src/gjc-runtime/launch-worktree.ts +56 -2
  337. package/src/gjc-runtime/linux-proc.ts +7 -1
  338. package/src/gjc-runtime/ralplan-runtime.ts +100 -164
  339. package/src/gjc-runtime/session-state-lock.ts +992 -0
  340. package/src/gjc-runtime/session-state-sidecar.ts +644 -142
  341. package/src/gjc-runtime/state-writer.ts +39 -14
  342. package/src/gjc-runtime/tmux-owner-isolation.ts +5 -4
  343. package/src/gjc-runtime/ultragoal-guard.ts +11 -4
  344. package/src/gjc-runtime/ultragoal-runtime.ts +44 -41
  345. package/src/gjc-runtime/workflow-settings.ts +596 -0
  346. package/src/goals/tools/goal-tool.ts +6 -1
  347. package/src/hooks/events.ts +291 -0
  348. package/src/hooks/native-skill-hook.ts +4 -0
  349. package/src/hooks/normalize.ts +369 -0
  350. package/src/hooks/skill-state.ts +165 -46
  351. package/src/internal-urls/docs-index.generated.ts +41 -32
  352. package/src/main.ts +36 -5
  353. package/src/migrate/mcp-mapper.ts +25 -2
  354. package/src/modes/acp/abort-scope.ts +29 -0
  355. package/src/modes/acp/acp-agent.ts +423 -98
  356. package/src/modes/components/assistant-message.ts +6 -1
  357. package/src/modes/components/chrome.ts +4 -3
  358. package/src/modes/components/customization/customization-dashboard.ts +443 -0
  359. package/src/modes/components/customization/import-wizard.ts +372 -0
  360. package/src/modes/components/customization/index.ts +1 -0
  361. package/src/modes/components/dynamic-theme-text.ts +22 -0
  362. package/src/modes/components/gajae-pet-widget.ts +607 -193
  363. package/src/modes/components/irc-sidebar.ts +11 -0
  364. package/src/modes/components/iterm-pet-transport.ts +585 -0
  365. package/src/modes/components/model-selector.ts +1 -6
  366. package/src/modes/components/oauth-selector.ts +277 -86
  367. package/src/modes/components/pet-capability.ts +35 -31
  368. package/src/modes/components/settings-selector.ts +29 -5
  369. package/src/modes/components/status-line/gh.ts +45 -1
  370. package/src/modes/components/status-line/segments.ts +19 -4
  371. package/src/modes/components/tool-status-header.ts +13 -3
  372. package/src/modes/controllers/command-controller.ts +20 -8
  373. package/src/modes/controllers/extension-ui-controller.ts +56 -257
  374. package/src/modes/controllers/input-controller.ts +10 -0
  375. package/src/modes/controllers/runtime-mcp-command-controller.ts +16 -3
  376. package/src/modes/controllers/selector-controller.ts +238 -77
  377. package/src/modes/controllers/stt-controller.ts +3 -3
  378. package/src/modes/interactive-mode.ts +88 -16
  379. package/src/modes/print-mode.ts +35 -6
  380. package/src/modes/runtime-init.ts +21 -68
  381. package/src/modes/theme/defaults/index.ts +2 -0
  382. package/src/modes/theme/defaults/ouroboros.json +118 -0
  383. package/src/modes/types.ts +1 -0
  384. package/src/modes/utils/injected-user-submission.ts +25 -0
  385. package/src/modes/utils/terminal-bell.ts +15 -1
  386. package/src/prompts/agent-fragments/ralplan-persistence.md +1 -1
  387. package/src/prompts/agent-fragments/restricted-bash.md +1 -1
  388. package/src/prompts/agents/critic.md +1 -1
  389. package/src/prompts/system/rlm-research.md +1 -1
  390. package/src/prompts/system/system-prompt.md +0 -2
  391. package/src/prompts/tools/bash.md +7 -1
  392. package/src/prompts/tools/browser.md +1 -1
  393. package/src/prompts/tools/cron.md +6 -6
  394. package/src/prompts/tools/read.md +0 -2
  395. package/src/prompts/tools/skill-discovery.md +4 -2
  396. package/src/runtime-mcp/client.ts +427 -23
  397. package/src/runtime-mcp/config.ts +37 -5
  398. package/src/runtime-mcp/manager-pool.test.ts +56 -0
  399. package/src/runtime-mcp/manager.ts +52 -2
  400. package/src/runtime-mcp/oauth-discovery.ts +13 -0
  401. package/src/runtime-mcp/oauth-flow.ts +46 -1
  402. package/src/runtime-mcp/pool-key.ts +5 -1
  403. package/src/runtime-mcp/pool.test.ts +9 -0
  404. package/src/runtime-mcp/protocol.test.ts +353 -0
  405. package/src/runtime-mcp/protocol.ts +624 -0
  406. package/src/runtime-mcp/scope-config.ts +78 -0
  407. package/src/runtime-mcp/tool-bridge.ts +44 -10
  408. package/src/runtime-mcp/transports/http.ts +84 -13
  409. package/src/runtime-mcp/types.ts +123 -12
  410. package/src/sdk/acp/adapter.ts +328 -97
  411. package/src/sdk/broker/broker.ts +160 -741
  412. package/src/sdk/broker/ensure.ts +9 -1
  413. package/src/sdk/broker/lifecycle.ts +510 -116
  414. package/src/sdk/broker/operation-receipt-ledger.ts +11 -2
  415. package/src/sdk/broker/process-incarnation.ts +19 -0
  416. package/src/sdk/broker/session-index.ts +516 -117
  417. package/src/sdk/broker/startup-failure.ts +28 -5
  418. package/src/sdk/broker/transport.ts +1 -13
  419. package/src/sdk/bus/chat-daemon-cli.ts +4 -2
  420. package/src/sdk/bus/chat-daemon-control.ts +67 -6
  421. package/src/sdk/bus/chat-daemon-runtime.ts +183 -859
  422. package/src/sdk/bus/config.ts +13 -0
  423. package/src/sdk/bus/conversation-store.ts +59 -171
  424. package/src/sdk/bus/daemon-paths.ts +20 -0
  425. package/src/sdk/bus/discord-conversation.ts +2 -0
  426. package/src/sdk/bus/discord-daemon.ts +524 -126
  427. package/src/sdk/bus/discord-live-provider.ts +206 -79
  428. package/src/sdk/bus/existing-thread-readiness.ts +4 -97
  429. package/src/sdk/bus/inbound-reaction-ordering.ts +122 -0
  430. package/src/sdk/bus/index.ts +2480 -456
  431. package/src/sdk/bus/kind-aware-reconciliation.ts +204 -34
  432. package/src/sdk/bus/lifecycle-commands.ts +2 -2
  433. package/src/sdk/bus/managed-daemon.ts +2 -145
  434. package/src/sdk/bus/notification-service.ts +196 -0
  435. package/src/sdk/bus/operator-runtime.ts +1 -1
  436. package/src/sdk/bus/prompt-reconciliation.ts +24 -15
  437. package/src/sdk/bus/reconciliation-store.ts +553 -24
  438. package/src/sdk/bus/session-control.ts +49 -3
  439. package/src/sdk/bus/slack-conversation.ts +4 -0
  440. package/src/sdk/bus/slack-daemon.ts +733 -134
  441. package/src/sdk/bus/slack-live-provider.ts +65 -15
  442. package/src/sdk/bus/slack-provider.ts +4 -0
  443. package/src/sdk/bus/slack-thread-binding.ts +9 -104
  444. package/src/sdk/bus/telegram-adoption-intent.ts +299 -209
  445. package/src/sdk/bus/telegram-daemon-cli.ts +194 -7
  446. package/src/sdk/bus/telegram-daemon-contract.ts +265 -19
  447. package/src/sdk/bus/telegram-daemon-control.ts +3 -13
  448. package/src/sdk/bus/telegram-daemon-orphan-reap.ts +498 -0
  449. package/src/sdk/bus/telegram-daemon-owner-registry.ts +176 -0
  450. package/src/sdk/bus/telegram-daemon.ts +2552 -2280
  451. package/src/sdk/bus/telegram-reference.ts +1 -226
  452. package/src/sdk/bus/telegram-setup.ts +2 -2
  453. package/src/sdk/bus/threaded-render.ts +1 -0
  454. package/src/sdk/bus/topic-registry.ts +279 -146
  455. package/src/sdk/cli/rows.ts +2 -35
  456. package/src/sdk/cli/session-cli.ts +776 -700
  457. package/src/sdk/client/client.ts +892 -1
  458. package/src/sdk/host/control/dispatch.ts +93 -58
  459. package/src/sdk/host/control/index.ts +1 -1
  460. package/src/sdk/host/control/operations.ts +18 -10
  461. package/src/sdk/host/control/runtime-gate.ts +40 -0
  462. package/src/sdk/host/host.ts +63 -12
  463. package/src/sdk/host/query/handlers.ts +66 -7
  464. package/src/sdk/host/reverse-leases.ts +2 -2
  465. package/src/sdk/host/sdk-surface-parity.test.ts +1 -1
  466. package/src/sdk/host/session-runtime.test.ts +2787 -34
  467. package/src/sdk/host/session-runtime.ts +1489 -101
  468. package/src/sdk/host/surface-policy.ts +12 -8
  469. package/src/sdk/host/types.ts +1 -1
  470. package/src/sdk/host/websocket-transport.lifecycle.test.ts +31 -59
  471. package/src/sdk/host/websocket-transport.ts +85 -50
  472. package/src/sdk/index.ts +2 -1
  473. package/src/sdk/lifecycle/broker-client.ts +53 -0
  474. package/src/sdk/lifecycle/client.ts +234 -0
  475. package/src/sdk/lifecycle/index.ts +3 -0
  476. package/src/sdk/{bus/recent-activity.ts → lifecycle/recent-sessions.ts} +5 -2
  477. package/src/sdk/lifecycle/service.ts +720 -0
  478. package/src/sdk/mcp/server.ts +226 -188
  479. package/src/sdk/prompt-deadline-lease.ts +55 -0
  480. package/src/sdk/prompt-deadline-manager.ts +148 -0
  481. package/src/sdk/prompt-status.ts +11 -3
  482. package/src/sdk/protocol/operation-inventory.generated.json +272 -129
  483. package/src/sdk/protocol/operation-registry.ts +71 -8
  484. package/src/sdk/receipt-state.ts +43 -0
  485. package/src/sdk/reconciliation-extensions.ts +14 -0
  486. package/src/sdk/router/index.ts +1 -0
  487. package/src/sdk/router/session-router.ts +1658 -0
  488. package/src/sdk/session-list.ts +93 -0
  489. package/src/sdk/session-reconnect.ts +45 -1
  490. package/src/sdk/session.ts +716 -120
  491. package/src/sdk/transport/serve-cli.ts +18 -34
  492. package/src/session/account-inventory.ts +603 -0
  493. package/src/session/agent-session.ts +2873 -559
  494. package/src/session/agent-storage.ts +35 -0
  495. package/src/session/blob-store.ts +32 -1
  496. package/src/session/coordinator-tool-label.ts +40 -0
  497. package/src/session/internal/managed-session-scope.ts +4 -0
  498. package/src/session/internal/managed-session-storage.ts +199 -31
  499. package/src/session/messages.ts +1 -1
  500. package/src/session/secure-token-file.ts +80 -0
  501. package/src/session/session-manager.ts +757 -57
  502. package/src/session/session-storage.ts +25 -16
  503. package/src/session/startup-auth-config.ts +297 -0
  504. package/src/session/terminal-abort.ts +1253 -0
  505. package/src/session/yield-queue.ts +57 -15
  506. package/src/session-import/claude.ts +334 -0
  507. package/src/session-import/codex.ts +1033 -0
  508. package/src/session-import/command.ts +77 -0
  509. package/src/session-import/detect.ts +133 -0
  510. package/src/session-import/index.ts +7 -0
  511. package/src/session-import/provider-codex.ts +327 -0
  512. package/src/session-import/provider-service.ts +457 -0
  513. package/src/session-import/redact.ts +128 -0
  514. package/src/session-import/service.ts +1264 -0
  515. package/src/session-import/types.ts +148 -0
  516. package/src/setup/paseo/check.ts +213 -0
  517. package/src/setup/paseo/install-saga.ts +293 -0
  518. package/src/setup/paseo/json-publisher.ts +263 -0
  519. package/src/setup/paseo/orchestration-preferences.ts +86 -0
  520. package/src/setup/paseo/paseo-ownership.ts +224 -0
  521. package/src/setup/paseo/paseo-setup.ts +253 -0
  522. package/src/setup/paseo/provider-config.ts +139 -0
  523. package/src/setup/paseo/remove.ts +183 -0
  524. package/src/setup/paseo/result-types.ts +79 -0
  525. package/src/setup/paseo/setup-deps.ts +175 -0
  526. package/src/setup/paseo/skills-bridge.ts +260 -0
  527. package/src/skill-state/active-state.ts +21 -11
  528. package/src/slash-commands/acp-builtins.ts +9 -1
  529. package/src/slash-commands/builtin-registry.ts +117 -19
  530. package/src/slash-commands/helpers/credential-switch.ts +143 -0
  531. package/src/slash-commands/helpers/mcp.ts +9 -3
  532. package/src/slash-commands/helpers/usage-report.ts +61 -56
  533. package/src/slash-commands/types.ts +7 -2
  534. package/src/task/executor.ts +30 -11
  535. package/src/task/index.ts +66 -7
  536. package/src/task/render.ts +23 -9
  537. package/src/tools/ask-answer-registry.ts +23 -7
  538. package/src/tools/ask.ts +8 -4
  539. package/src/tools/bash-sleep-advisory.ts +115 -0
  540. package/src/tools/bash.ts +98 -18
  541. package/src/tools/browser/attach.ts +71 -3
  542. package/src/tools/browser/launch.ts +86 -13
  543. package/src/tools/browser/profile-discovery.ts +57 -7
  544. package/src/tools/browser/render.ts +8 -1
  545. package/src/tools/browser.ts +130 -21
  546. package/src/tools/computer.enforcement.test.ts +1 -1
  547. package/src/tools/computer.ts +3 -2
  548. package/src/tools/cron.ts +12 -1
  549. package/src/tools/image-gen.ts +349 -142
  550. package/src/tools/index.ts +8 -0
  551. package/src/tools/job.ts +33 -6
  552. package/src/tools/monitor.ts +41 -2
  553. package/src/tools/skill-discovery.ts +21 -29
  554. package/src/tools/subagent.ts +142 -24
  555. package/src/tools/todo-contract.ts +55 -3
  556. package/src/tools/todo-write.ts +4 -3
  557. package/src/tools/tool-catalog.generated.ts +6 -6
  558. package/src/tools/ultragoal-ask-guard.ts +10 -3
  559. package/src/utils/herdr-pane.ts +345 -0
  560. package/src/utils/title-generator.ts +2 -0
  561. package/src/web/scrapers/index.ts +6 -4
  562. package/dist/types/cli/malloc-env-guard.test.d.ts +0 -1
  563. package/dist/types/defaults/gjc-defaults.test.d.ts +0 -1
  564. package/dist/types/memory-backend/lazy-loading.test.d.ts +0 -1
  565. package/dist/types/runtime/boot-ordering.test.d.ts +0 -1
  566. package/dist/types/runtime/lazy-service.test.d.ts +0 -1
  567. package/dist/types/runtime/network-prewarm-service.test.d.ts +0 -1
  568. package/dist/types/runtime/workspace-tree-service.test.d.ts +0 -1
  569. package/dist/types/runtime-mcp/config.test.d.ts +0 -1
  570. package/dist/types/runtime-mcp/http.test.d.ts +0 -1
  571. package/dist/types/runtime-mcp/manager-pool.test.d.ts +0 -1
  572. package/dist/types/runtime-mcp/pool-key.test.d.ts +0 -1
  573. package/dist/types/runtime-mcp/pool.test.d.ts +0 -1
  574. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +0 -117
  575. package/dist/types/sdk/bus/lifecycle-orchestrator.d.ts +0 -141
  576. package/dist/types/sdk/bus/telegram-cli.d.ts +0 -31
  577. package/dist/types/sdk/elevation/allowlist.d.ts +0 -20
  578. package/dist/types/sdk/elevation/audit.d.ts +0 -93
  579. package/dist/types/sdk/elevation/capability.d.ts +0 -3
  580. package/dist/types/sdk/elevation/digest.d.ts +0 -72
  581. package/dist/types/sdk/elevation/dispatch-receipt.d.ts +0 -85
  582. package/dist/types/sdk/elevation/grant-ledger.d.ts +0 -163
  583. package/dist/types/sdk/elevation/owner.d.ts +0 -48
  584. package/dist/types/sdk/host/import-graph.test.d.ts +0 -1
  585. package/dist/types/sdk/host/sdk-surface-parity.test.d.ts +0 -1
  586. package/dist/types/sdk/host/session-runtime.test.d.ts +0 -1
  587. package/dist/types/sdk/host/websocket-transport.lifecycle.test.d.ts +0 -1
  588. package/dist/types/session/auth-broker-config.d.ts +0 -13
  589. package/dist/types/session/heap-eviction-artifacts.test.d.ts +0 -1
  590. package/dist/types/skills/skills.test.d.ts +0 -1
  591. package/dist/types/slash-commands/notify-command.test.d.ts +0 -1
  592. package/dist/types/task/token-log.test.d.ts +0 -1
  593. package/dist/types/tools/computer.enforcement.test.d.ts +0 -1
  594. package/dist/types/tools/descriptors.test.d.ts +0 -1
  595. package/scripts/g005-daemon-smoke.ts +0 -180
  596. package/scripts/g011-daemon-path-smoke.ts +0 -153
  597. package/src/sdk/bus/lifecycle-control-runtime.ts +0 -1733
  598. package/src/sdk/bus/lifecycle-orchestrator.ts +0 -489
  599. package/src/sdk/bus/telegram-cli.ts +0 -212
  600. package/src/sdk/elevation/allowlist.ts +0 -54
  601. package/src/sdk/elevation/audit.ts +0 -208
  602. package/src/sdk/elevation/capability.ts +0 -41
  603. package/src/sdk/elevation/digest.ts +0 -129
  604. package/src/sdk/elevation/dispatch-receipt.ts +0 -184
  605. package/src/sdk/elevation/grant-ledger.ts +0 -1142
  606. package/src/sdk/elevation/owner.ts +0 -109
  607. package/src/session/auth-broker-config.ts +0 -109
  608. /package/dist/types/{ai-core-import-gate.test.d.ts → discovery/claude-hooks.d.ts} +0 -0
  609. /package/dist/types/{cli/fixture-report.test.d.ts → discovery/codex-hooks.d.ts} +0 -0
  610. /package/dist/types/sdk/{bus/recent-activity.d.ts → lifecycle/recent-sessions.d.ts} +0 -0
@@ -0,0 +1,1253 @@
1
+ /**
2
+ * Private terminal-abort machinery for C04 `turn.abort` `mode:"terminal"`.
3
+ *
4
+ * Corrected semantics (approved plan, user-directed; see the plan's prominent
5
+ * design note): `scope:"turn"` stops the ROOT WORKER's current turn and blocks
6
+ * ONLY its own continuation routes (same-turn retry, TTSR/`agent.continue`,
7
+ * steering continuation, hidden-next-turn, maintenance/worker successor,
8
+ * accepted-pre-close same-attempt continuation). Left-running owned work
9
+ * (background Bash/task jobs, detached subagents) keeps running and its
10
+ * completions are DELIVERED NORMALLY through the existing
11
+ * YieldQueue -> `agent.followUp`/`agent.prompt` path so the root worker can
12
+ * resume with a fresh attempt. Owned delivery is intentionally NOT suppressed.
13
+ *
14
+ * The earlier stage-04 no-successor delivery fence was a misunderstanding and
15
+ * must not be reinstated under any name.
16
+ */
17
+ import { createHash, randomUUID } from "node:crypto";
18
+
19
+ /** Origin class assigned to every causal callback/queue entry before escape. */
20
+ export type DeliveryOrigin =
21
+ | Readonly<{
22
+ kind: "turn-continuation";
23
+ lineageIdHash: string;
24
+ attemptEpoch: number;
25
+ continuationId: string;
26
+ }>
27
+ | Readonly<{
28
+ kind: "owned-completion";
29
+ lineageIdHash: string;
30
+ attemptEpoch: number;
31
+ registration: TurnRegistrationKey;
32
+ }>
33
+ | Readonly<{ kind: "ordinary"; source: string }>;
34
+
35
+ /** Exact causal registration key bound before a job/subagent handle escapes. */
36
+ export interface TurnRegistrationKey {
37
+ /** Endpoint (top-level session) identity that owns this job: the registry is
38
+ * process-global and job ids/generations restart per AsyncJobManager, so
39
+ * two concurrent sessions both mint bg_1/job:1 — without the endpoint in
40
+ * the key the second registration would overwrite the first and an owned
41
+ * abort of the first turn could find an empty causal set (review thread
42
+ * P1). Absent (legacy/test fixtures) = a default scope. */
43
+ endpointId?: string;
44
+ endpointGeneration: number;
45
+ lineageIdHash: string;
46
+ promptAttemptEpoch: number;
47
+ jobId: string;
48
+ jobGeneration: string;
49
+ }
50
+
51
+ /** Per-completion delivery key: registration tuple plus entry identity. */
52
+ export type TurnDeliveryKey = TurnRegistrationKey & {
53
+ entryId: string;
54
+ progressSeq?: number;
55
+ };
56
+ /** Private origin envelope carried through the plain AgentMessage boundary. */
57
+ export interface OwnedCompletionEnvelope {
58
+ lineageIdHash: string;
59
+ promptAttemptEpoch: number;
60
+ /** Exact registered five-tuple so the final gate can validate source authority. */
61
+ registration: TurnRegistrationKey;
62
+ }
63
+
64
+ export type TurnContinuationFenceState = "open" | "closing" | "closed" | "retained" | "released";
65
+
66
+ export type OwnedCompletionPolicy = "enabled" | "disabled";
67
+
68
+ /**
69
+ * Continuation fence lifecycle: `open -> closing -> closed` happens
70
+ * synchronously before the first await that interrupts the root turn. Closing
71
+ * records exact continuation tombstones and invalidates ONLY continuation
72
+ * tokens; it never invalidates an owned-completion token, cancels a manager
73
+ * job, or creates a turn delivery receipt. `retained` keeps tombstones for
74
+ * restart/later-owned binding; `released` requires exact tokens gone, teardown
75
+ * with no live continuation, or bounded durable retention. Host response
76
+ * success/replay/retry never releases it.
77
+ */
78
+ export interface TurnContinuationFence {
79
+ state: TurnContinuationFenceState;
80
+ lineageIdHash: string;
81
+ abortedAttemptEpoch: number;
82
+ terminalScopeId: string;
83
+ blockedContinuationIds: ReadonlySet<string>;
84
+ predecessorTombstones: ReadonlySet<string>;
85
+ ownedCompletionPolicy: OwnedCompletionPolicy;
86
+ }
87
+
88
+ /**
89
+ * The one gate consulted immediately before turn-origin continuation calls and
90
+ * owned-completion admission.
91
+ *
92
+ * `authorizeContinuation` denies any post-close same-turn continuation and
93
+ * allows only a call already linearized as a predecessor before close.
94
+ * `authorizeOwnedCompletion` does NOT consult the closed continuation state as
95
+ * a suppression flag; it validates exact source metadata and, when allowed,
96
+ * AgentSession allocates a FRESH attempt/lineage for the new turn.
97
+ */
98
+ export interface TurnContinuationGate {
99
+ close(reason: "terminal-turn"): void;
100
+ authorizeContinuation(origin: DeliveryOrigin): "deny" | "allow-predecessor";
101
+ authorizeOwnedCompletion(origin: DeliveryOrigin): "allow-new-turn" | "deny";
102
+ }
103
+
104
+ export type OwnedDeliverySettlementPath =
105
+ | "enqueue-acknowledged-return"
106
+ | "acknowledgeDeliveries-queue-purge"
107
+ | "delivery-loop-acknowledged-skip"
108
+ | "deliverDelivery-acknowledged-return"
109
+ | "terminal-wait-acknowledge-suppression-purge"
110
+ | "filtered-drain-post-selection-suppression";
111
+
112
+ /** Owned-scope-only settlement observer (never installed for turn scope). */
113
+ export type OwnedDeliverySettlementObserver = (event: {
114
+ key: TurnDeliveryKey;
115
+ path: OwnedDeliverySettlementPath;
116
+ action: "owned_settled" | "owned_absent";
117
+ }) => void;
118
+
119
+ /** Safe, bounded reasons surfaced on `terminal_uncertain` responses. */
120
+ export const TERMINAL_UNCERTAIN_REASONS = [
121
+ "persistence_unavailable",
122
+ "publication_failed",
123
+ "delivery_failed",
124
+ "owned_unsettled",
125
+ "worker_unsettled",
126
+ "unknown_origin",
127
+ "registration_authority_unavailable",
128
+ ] as const;
129
+ export type TerminalUncertainReason = (typeof TERMINAL_UNCERTAIN_REASONS)[number];
130
+
131
+ export interface TerminalScopeDispositions {
132
+ selection: "turn" | "owned";
133
+ turnDisposition: "pending" | "stopped" | "uncertain";
134
+ ownedWorkDisposition: "not_requested" | "left_running" | "stopped" | "uncertain";
135
+ automaticDeliveryDisposition: "enabled" | "none";
136
+ resumeOnOwnedCompletion: boolean;
137
+ }
138
+ export interface ActiveTerminalScope {
139
+ scopeId: string;
140
+ lineageIdHash: string;
141
+ abortedAttemptEpoch: number;
142
+ gate: TurnContinuationGate;
143
+ fence: TurnContinuationFence;
144
+ }
145
+
146
+ const MAX_ACTIVE_TERMINAL_SCOPES = 1024;
147
+ const MAX_OWNED_REGISTRATIONS = 8192;
148
+ const MAX_RETAINED_OWNERSHIP_TUPLES = 2048;
149
+ const MAX_RETAINED_ATTEMPT_POLICIES = 2048;
150
+ const activeScopes = new Map<string, ActiveTerminalScope>();
151
+ /**
152
+ * Compact authorization evidence for registrations evicted by the 8,192 cap:
153
+ * a TERMINAL job may still have an owned-completion envelope queued but not
154
+ * yet consumed/acknowledged, and evicting its registration would remove the
155
+ * authorizeOwnedCompletion authority (the queued result would be denied after
156
+ * a scope:"turn" abort, losing the promised fresh completion). Evicted tuples
157
+ * are retained here (bounded, FIFO) and removed once the delivery settles
158
+ * (unregisterOwnedRegistration) — review thread P2.
159
+ */
160
+ const retainedOwnershipTuples = new Map<string, TurnRegistrationKey>();
161
+ const activeScopeByAttempt = new Map<string, string>();
162
+ const ownedRegistrations = new Map<string, TurnRegistrationKey>();
163
+ /**
164
+ * Compact attempt-policy tombstones for scopes evicted by the cap: when a
165
+ * scope is evicted, its ownedCompletionPolicy is retained so a still-running
166
+ * owned completion from that attempt keeps classifying correctly (fresh
167
+ * resume for scope:"turn", drop for scope:"owned") instead of degrading to
168
+ * ordinary (review thread P2). Bounded; oldest tombstone evicted first.
169
+ */
170
+ const retainedAttemptPolicies = new Map<string, { ownedCompletionPolicy: "enabled" | "disabled" }>();
171
+ /** Attempts whose owned-registration set is KNOWN incomplete (registry
172
+ * saturation skipped a registration under the 8192 cap). Bounded by the
173
+ * number of saturation events — fail-closed authority for those attempts.
174
+ */
175
+ const incompleteOwnedAttempts = new Set<string>();
176
+ /** Evicted call key → its attempt marker + retained lineage until that tool call settles. */
177
+ const incompleteToolCallWindows = new Map<string, { incompleteKey: string; binding: LineageBinding }>();
178
+ /** Number of evicted tool-call windows still capable of registering per attempt. */
179
+ const incompleteAttemptWindowCounts = new Map<string, number>();
180
+ /**
181
+ * Saturation authority owner map (attempt key -> endpoint id): when the
182
+ * process-global registry is full with no evictable tuple, the skipped
183
+ * registration's attempt can never prove an exact causal set. The authority
184
+ * is retained PER AFFECTED ATTEMPT (lineage+epoch, unique per session) and
185
+ * retired when its endpoint's registrations are torn down — a transient
186
+ * backlog must not permanently disable owned aborts for unrelated sessions
187
+ * or endpoints (review thread P2).
188
+ */
189
+ const saturatedAttemptEndpoints = new Map<string, string>();
190
+ /**
191
+ * EvictKeys whose TERMINAL tuple could not move into the retained evidence
192
+ * (its cap is full): the tuple stays in the LIVE map so authorizeOwnedCompletion
193
+ * still finds it, and the eviction scan skips these keys so a later
194
+ * registration does not re-evict the same protected tuple (review thread P2).
195
+ */
196
+ const retentionBacklog = new Set<string>();
197
+
198
+ /** Register one active terminal scope (scopeId -> seam). Bounded; evicts oldest. */
199
+ export function registerTerminalScope(scope: ActiveTerminalScope): boolean {
200
+ if (activeScopes.size >= MAX_ACTIVE_TERMINAL_SCOPES) {
201
+ const oldest = activeScopes.keys().next().value;
202
+ if (oldest !== undefined) {
203
+ const evicted = activeScopes.get(oldest);
204
+ if (evicted) {
205
+ if (retainedAttemptPolicies.size >= MAX_RETAINED_ATTEMPT_POLICIES) {
206
+ // Evict only a tombstone whose attempt has NO remaining
207
+ // registrations (its owned completions have settled) — never
208
+ // by age alone while a registration could still classify and
209
+ // require the policy (review thread P2).
210
+ let evictPolicyKey: string | undefined;
211
+ for (const [candidateKey] of retainedAttemptPolicies) {
212
+ const separator = candidateKey.lastIndexOf("\u0000");
213
+ const lineage = candidateKey.slice(0, separator);
214
+ const epoch = Number(candidateKey.slice(separator + 1));
215
+ const hasRegistrations =
216
+ [...ownedRegistrations.values()].some(
217
+ reg => reg.lineageIdHash === lineage && reg.promptAttemptEpoch === epoch,
218
+ ) ||
219
+ // A TERMINAL job awaiting delivery may have been
220
+ // evicted into the retained evidence; its attempt is
221
+ // NOT settled and its policy must survive (review P2).
222
+ [...retainedOwnershipTuples.values()].some(
223
+ reg => reg.lineageIdHash === lineage && reg.promptAttemptEpoch === epoch,
224
+ );
225
+ if (!hasRegistrations) {
226
+ evictPolicyKey = candidateKey;
227
+ break;
228
+ }
229
+ }
230
+ if (evictPolicyKey === undefined) {
231
+ // Every retained tombstone still has LIVE registrations:
232
+ // evicting the OLDEST ACTIVE scope would drop ITS policy
233
+ // while its work remains registered/classifiable, changing
234
+ // its later completion from fresh/drop to ordinary. FAIL
235
+ // the NEW scope admission instead — the oldest scope keeps
236
+ // its policy (review thread P2).
237
+ return false;
238
+ }
239
+ retainedAttemptPolicies.delete(evictPolicyKey);
240
+ }
241
+ retainedAttemptPolicies.set(`${evicted.lineageIdHash}\u0000${evicted.abortedAttemptEpoch}`, {
242
+ ownedCompletionPolicy: evicted.fence.ownedCompletionPolicy,
243
+ });
244
+ }
245
+ unregisterTerminalScope(oldest);
246
+ }
247
+ }
248
+ activeScopes.set(scope.scopeId, scope);
249
+ activeScopeByAttempt.set(`${scope.lineageIdHash}\u0000${scope.abortedAttemptEpoch}`, scope.scopeId);
250
+ return true;
251
+ }
252
+
253
+ /** Look up the active terminal scope for an aborted attempt (exact lineage+epoch). */
254
+ export function lookupTerminalScope(lineageIdHash: string, attemptEpoch: number): ActiveTerminalScope | undefined {
255
+ const scopeId = activeScopeByAttempt.get(`${lineageIdHash}\u0000${attemptEpoch}`);
256
+ return scopeId === undefined ? undefined : activeScopes.get(scopeId);
257
+ }
258
+
259
+ export function unregisterTerminalScope(scopeId: string): void {
260
+ const scope = activeScopes.get(scopeId);
261
+ if (!scope) return;
262
+ activeScopes.delete(scopeId);
263
+ activeScopeByAttempt.delete(`${scope.lineageIdHash}\u0000${scope.abortedAttemptEpoch}`);
264
+ }
265
+
266
+ /** Record an exact owned registration before its handle escapes (bounded). */
267
+ export function registerOwnedRegistration(
268
+ key: TurnRegistrationKey,
269
+ options?: { isJobTerminal?: (candidate: TurnRegistrationKey) => boolean | undefined },
270
+ ): void {
271
+ // Key by (endpoint, jobId, jobGeneration): job ids and generations restart
272
+ // per AsyncJobManager, so CONCURRENT sessions' bg_1/job:1 must coexist —
273
+ // the endpoint identity prevents the second registration from overwriting
274
+ // the first (review thread P1). A same-ENDPOINT (jobId, jobGeneration)
275
+ // rebind (replaced manager within one session) still overwrites the stale
276
+ // entry; a same-turn re-registration (exact tuple) is a no-op.
277
+ const mapKey = `${key.endpointId ?? ""}\u0000${key.jobId}\u0000${key.jobGeneration}`;
278
+ const existing = ownedRegistrations.get(mapKey);
279
+ if (
280
+ existing &&
281
+ existing.lineageIdHash === key.lineageIdHash &&
282
+ existing.promptAttemptEpoch === key.promptAttemptEpoch &&
283
+ existing.endpointId === key.endpointId
284
+ ) {
285
+ return;
286
+ }
287
+ if (ownedRegistrations.size >= MAX_OWNED_REGISTRATIONS) {
288
+ // Prefer evicting a TERMINAL registration: its job already finished, so
289
+ // a long-lived LIVE job must never be dropped by shorter finished jobs —
290
+ // otherwise scope:"owned" would omit it from the exact causal set and
291
+ // could report stopped_owned while it keeps running (review thread P2).
292
+ let evictKey: string | undefined;
293
+ if (options?.isJobTerminal) {
294
+ for (const [candidateKey, candidate] of ownedRegistrations) {
295
+ // Skip tuples already protected in the retention backlog (their
296
+ // pending authorization must not be re-evicted, review thread P2).
297
+ if (retentionBacklog.has(candidateKey)) continue;
298
+ if (options.isJobTerminal(candidate) === true) {
299
+ evictKey = candidateKey;
300
+ break;
301
+ }
302
+ }
303
+ }
304
+ if (evictKey === undefined) {
305
+ // ALL retained registrations are live: evicting any of them would lose
306
+ // a still-running job's ownership authority. FAIL CLOSED instead —
307
+ // do not evict, skip the new registration. Mark the SKIPPED
308
+ // registration's attempt as having an incomplete causal set: the
309
+ // unregistered job may still launch, so its exact set is unknowable.
310
+ // The marker is scoped to this attempt — never a process-lifetime
311
+ // daemon flag — and is retired when the attempt's endpoint is torn
312
+ // down (review thread P2).
313
+ markAttemptSaturated(key);
314
+ return;
315
+ }
316
+ const evictedTuple = ownedRegistrations.get(evictKey);
317
+ ownedRegistrations.delete(evictKey);
318
+ if (evictedTuple) {
319
+ if (retainedOwnershipTuples.size >= MAX_RETAINED_OWNERSHIP_TUPLES) {
320
+ // Retained evidence at cap: NEVER drop a pending authorization by
321
+ // age — keep the terminal tuple in the LIVE map (backlogged) so
322
+ // authorizeOwnedCompletion still finds it while its completion
323
+ // envelope is queued (review thread P2). The live map only
324
+ // transiently exceeds its cap under pathological concurrent
325
+ // pending deliveries; the backlog is cleared as deliveries settle.
326
+ retentionBacklog.add(evictKey);
327
+ ownedRegistrations.set(evictKey, evictedTuple);
328
+ // The pending tuple cannot move to retained evidence, so the new
329
+ // registration has NO eviction headroom: FAIL CLOSED instead of
330
+ // inserting it and letting both the live map and the backlog grow
331
+ // without bound while deliveries stay stalled. The skipped
332
+ // registration's attempt is marked incomplete (scoped to the
333
+ // attempt, retired at endpoint teardown) (review thread P2).
334
+ markAttemptSaturated(key);
335
+ return;
336
+ }
337
+ retainedOwnershipTuples.set(evictKey, evictedTuple);
338
+ }
339
+ }
340
+ ownedRegistrations.set(mapKey, key);
341
+ }
342
+
343
+ /** Record an attempt whose owned-registration set is KNOWN incomplete — the
344
+ * fail-closed authority for an EVICTED in-flight lineage binding whose tool
345
+ * may still launch unregistered work. Every marker is paired with a
346
+ * registration window that settles it at afterToolCall, so the set is bounded
347
+ * by the open-window map. Registry-saturation skips record the affected
348
+ * attempt through markAttemptSaturated instead (review thread P2). */
349
+ function markAttemptRegistrationIncomplete(lineageIdHash: string, attemptEpoch: number): void {
350
+ const incompleteKey = `${lineageIdHash}\u0000${attemptEpoch}`;
351
+ // A marker can be cleared only once its affected call is known unable to
352
+ // register. FIFO aging loses that authority while a delayed in-flight tool
353
+ // may still launch, allowing scope:"owned" to claim stopped_owned over a
354
+ // live unregistered job (review thread P2).
355
+ incompleteOwnedAttempts.add(incompleteKey);
356
+ }
357
+
358
+ /** Record a REGISTRY-SATURATION skip for an attempt: the new registration was
359
+ * refused while the process-global registry had no evictable tuple, so the
360
+ * skipped job may still launch unregistered and the attempt's exact causal
361
+ * set can never be proven. Unlike window-backed markers there is no
362
+ * registration window to settle these; they are scoped to the affected
363
+ * attempt and retired when the owning endpoint's registrations are torn down
364
+ * (retireOwnedRegistrationsForEndpoint), so a transient backlog never
365
+ * permanently disables owned aborts daemon-wide (review thread P2). */
366
+ function markAttemptSaturated(key: TurnRegistrationKey): void {
367
+ const incompleteKey = `${key.lineageIdHash}\u0000${key.promptAttemptEpoch}`;
368
+ incompleteOwnedAttempts.add(incompleteKey);
369
+ saturatedAttemptEndpoints.set(incompleteKey, key.endpointId ?? "");
370
+ }
371
+
372
+ /** Whether an attempt's owned registration set is KNOWN incomplete (an
373
+ * evicted in-flight lineage binding or a registry-saturation skip) — a
374
+ * scope:"owned" abort of that exact attempt must fail closed to uncertainty
375
+ * (review thread P2). */
376
+ export function isOwnedAttemptRegistrationIncomplete(lineageIdHash: string, attemptEpoch: number): boolean {
377
+ // Per-attempt incomplete authority: window-backed markers from evicted
378
+ // in-flight lineage bindings AND registry-saturation skips both land in
379
+ // incompleteOwnedAttempts, so the check is scoped to the attempt — never a
380
+ // process-lifetime daemon flag — and saturation markers retire at endpoint
381
+ // teardown (review thread P2).
382
+ return incompleteOwnedAttempts.has(`${lineageIdHash}\u0000${attemptEpoch}`);
383
+ }
384
+
385
+ /** Exact (jobId, jobGeneration) lookup for completion-origin classification. */
386
+ export function lookupOwnedRegistration(
387
+ jobId: string,
388
+ jobGeneration: string,
389
+ endpointId?: string,
390
+ ): TurnRegistrationKey | undefined {
391
+ const prefix = `${endpointId ?? ""}\u0000${jobId}\u0000${jobGeneration}`;
392
+ if (endpointId !== undefined) {
393
+ const exact = ownedRegistrations.get(prefix);
394
+ if (exact) return exact;
395
+ // The live entry may have been evicted by the cap with its compact
396
+ // authorization evidence retained (pending delivery — review P2).
397
+ const retained = retainedOwnershipTuples.get(prefix);
398
+ if (retained) return retained;
399
+ }
400
+ // An ENDPOINT-qualified lookup NEVER falls back to the cross-endpoint scan:
401
+ // with concurrent sessions reusing bg_1/job:1, the scan could return
402
+ // another session's registration — B's monitor would attach A's ownership
403
+ // envelope, or B's Job/Bash settlement would unregister A's tuple while A's
404
+ // job keeps running (review thread P1). The scan is reserved for callers
405
+ // that OMIT an endpoint (legacy callers/tests).
406
+ if (endpointId === undefined) {
407
+ for (const reg of ownedRegistrations.values()) {
408
+ if (reg.jobId === jobId && reg.jobGeneration === jobGeneration) return reg;
409
+ }
410
+ for (const reg of retainedOwnershipTuples.values()) {
411
+ if (reg.jobId === jobId && reg.jobGeneration === jobGeneration) return reg;
412
+ }
413
+ }
414
+ return undefined;
415
+ }
416
+
417
+ export function unregisterOwnedRegistration(key: TurnRegistrationKey): void {
418
+ const mapKey = `${key.endpointId ?? ""}\u0000${key.jobId}\u0000${key.jobGeneration}`;
419
+ // Only remove the EXACT five-tuple: the (jobId, jobGeneration) pair may
420
+ // have been REBOUND to another lineage/epoch/endpoint (which
421
+ // registerOwnedRegistration explicitly allows), and deleting by the pair
422
+ // alone would drop the replacement job's authoritative registration — a
423
+ // later scope:"owned" abort could then omit it and report stopped_owned
424
+ // while it stays active (review thread P1).
425
+ const matches = (r: TurnRegistrationKey) =>
426
+ r.lineageIdHash === key.lineageIdHash &&
427
+ r.promptAttemptEpoch === key.promptAttemptEpoch &&
428
+ r.endpointGeneration === key.endpointGeneration &&
429
+ r.jobId === key.jobId &&
430
+ r.jobGeneration === key.jobGeneration;
431
+ const existing = ownedRegistrations.get(mapKey);
432
+ if (existing && matches(existing)) {
433
+ ownedRegistrations.delete(mapKey);
434
+ }
435
+ // The registration may already have been EVICTED into the retained
436
+ // evidence (a TERMINAL job whose envelope is still queued) while its
437
+ // delivery now settles; settle the retained tuple too. Without this the
438
+ // stale evidence would fill the FIFO and evict authorization for
439
+ // genuinely queued completions (review thread P2).
440
+ const retained = retainedOwnershipTuples.get(mapKey);
441
+ if (retained && matches(retained)) {
442
+ retainedOwnershipTuples.delete(mapKey);
443
+ }
444
+ retentionBacklog.delete(mapKey);
445
+ }
446
+ /**
447
+ * Enumerate every exact owned registration belonging to one aborted turn
448
+ * (matching lineage + attempt epoch). Used by `scope:"owned"` cleanup to
449
+ * capture the exact causal job set; foreign/unclassified work is never
450
+ * returned and is never swept.
451
+ */
452
+ /** Retire EVERY owned registration owned by a disposing endpoint (live,
453
+ * retained-evidence, and backlogged tuples): after the endpoint's manager is
454
+ * unregistered the tuples can no longer reach a delivery settlement boundary,
455
+ * and future managers deliberately cannot classify foreign-endpoint tuples as
456
+ * terminal for eviction — repeatedly disposing distinct sessions with pending
457
+ * jobs would accumulate tuples until the 8192-entry registry saturates and
458
+ * all later owned aborts fail closed (review thread P2). */
459
+ export function retireOwnedRegistrationsForEndpoint(endpointId: string): void {
460
+ for (const reg of [...ownedRegistrations.values(), ...retainedOwnershipTuples.values()]) {
461
+ if (reg.endpointId === endpointId) unregisterOwnedRegistration(reg);
462
+ }
463
+ // Retire the endpoint's saturation authority too: the skipped
464
+ // (unregistered) job's work is torn down with the endpoint, so the affected
465
+ // attempts no longer need to fail closed (review thread P2).
466
+ for (const [attemptKey, owner] of [...saturatedAttemptEndpoints]) {
467
+ if (owner === endpointId) {
468
+ saturatedAttemptEndpoints.delete(attemptKey);
469
+ incompleteOwnedAttempts.delete(attemptKey);
470
+ }
471
+ }
472
+ }
473
+
474
+ export function findOwnedRegistrationsForTurn(lineageIdHash: string, attemptEpoch: number): TurnRegistrationKey[] {
475
+ const matches: TurnRegistrationKey[] = [];
476
+ for (const key of ownedRegistrations.values()) {
477
+ if (key.lineageIdHash === lineageIdHash && key.promptAttemptEpoch === attemptEpoch) {
478
+ matches.push(key);
479
+ }
480
+ }
481
+ // A live registration evicted into the retained evidence (a terminal job
482
+ // whose execution promise is still unwinding) is still exact causal work of
483
+ // this attempt: include it so a scope:"owned" abort generation-verifies,
484
+ // cancels, and waits for its quiescence before claiming stopped_owned
485
+ // (review thread P2). unregisterOwnedRegistration settles retained tuples
486
+ // too, so the settlement path retires them.
487
+ for (const key of retainedOwnershipTuples.values()) {
488
+ if (key.lineageIdHash === lineageIdHash && key.promptAttemptEpoch === attemptEpoch) {
489
+ matches.push(key);
490
+ }
491
+ }
492
+ return matches;
493
+ }
494
+ export interface OwnedCompletionClassification {
495
+ lineageIdHash: string;
496
+ promptAttemptEpoch: number;
497
+ registration: TurnRegistrationKey;
498
+ terminalScopeId: string;
499
+ }
500
+
501
+ /**
502
+ * Classify a manager completion/progress delivery against the terminal-abort
503
+ * registries. Returns an exact owned-completion classification ONLY when the
504
+ * job carries an exact registered five-tuple AND a terminal scope exists for
505
+ * that turn. Missing or mismatched metadata fails closed (undefined) and the
506
+ * delivery is then ordinary. Classification is source/lineage-based, never
507
+ * timing-based; a closed terminal record does NOT suppress an exact
508
+ * left-running owned completion (corrected turn semantics).
509
+ */
510
+ export function classifyOwnedCompletion(
511
+ jobId: string,
512
+ jobGeneration: string | undefined,
513
+ ): OwnedCompletionClassification | undefined {
514
+ if (!jobGeneration) return undefined;
515
+ const registration = lookupOwnedRegistration(jobId, jobGeneration);
516
+ if (!registration) return undefined;
517
+ const scope = lookupTerminalScope(registration.lineageIdHash, registration.promptAttemptEpoch);
518
+ if (!scope) return undefined;
519
+ return {
520
+ lineageIdHash: registration.lineageIdHash,
521
+ promptAttemptEpoch: registration.promptAttemptEpoch,
522
+ registration,
523
+ terminalScopeId: scope.scopeId,
524
+ };
525
+ }
526
+ /**
527
+ * Whether an owned-completion envelope is authorized by its owning terminal
528
+ * scope as a fresh-turn resume. Used at batch build (sdk/session.ts) and the
529
+ * final injection boundary (agent-session.ts): a denied envelope — owned scope
530
+ * (policy disabled), forged/unregistered tuple, or vanished scope — must be
531
+ * dropped/partitioned out so stopped work can never call followUp/prompt.
532
+ */
533
+ /**
534
+ * Classify an owned-completion envelope into three states. A registration
535
+ * without a terminal scope (no abort yet) is ORDINARY — normal delivery;
536
+ * only a scope with the owned policy disabled DROPS, and a turn-scope
537
+ * enabled policy is FRESH (new-turn resume). This lets the batch keep
538
+ * ownership on the entry and reclassify at flush/abort time (review
539
+ * thread P1: a completion finished before the abort must not become an
540
+ * unpurgeable ordinary entry that can still resume the agent).
541
+ */
542
+ /**
543
+ * Structural validation for envelopes carried through the public AgentMessage
544
+ * `details` boundary: `ExtensionAPI.sendMessage` allows arbitrary details, so
545
+ * a malformed `ownedCompletions` entry (object, null item, missing tuple
546
+ * fields) must never crash the delivery path — invalid entries are skipped,
547
+ * never classified (review thread P2).
548
+ */
549
+ export function isOwnedCompletionEnvelope(value: unknown): value is OwnedCompletionEnvelope {
550
+ if (typeof value !== "object" || value === null) return false;
551
+ const envelope = value as Record<string, unknown>;
552
+ if (typeof envelope.lineageIdHash !== "string") return false;
553
+ if (typeof envelope.promptAttemptEpoch !== "number") return false;
554
+ const registration = envelope.registration;
555
+ if (typeof registration !== "object" || registration === null) return false;
556
+ const key = registration as Record<string, unknown>;
557
+ return (
558
+ typeof key.jobId === "string" &&
559
+ typeof key.jobGeneration === "string" &&
560
+ typeof key.endpointGeneration === "number" &&
561
+ typeof key.lineageIdHash === "string" &&
562
+ typeof key.promptAttemptEpoch === "number" &&
563
+ (key.endpointId === undefined || typeof key.endpointId === "string")
564
+ );
565
+ }
566
+
567
+ export function classifyOwnedEnvelope(envelope: OwnedCompletionEnvelope): "ordinary" | "fresh" | "drop" {
568
+ const scope = lookupTerminalScope(envelope.lineageIdHash, envelope.promptAttemptEpoch);
569
+ if (scope) {
570
+ return scope.gate.authorizeOwnedCompletion({
571
+ kind: "owned-completion",
572
+ lineageIdHash: envelope.lineageIdHash,
573
+ attemptEpoch: envelope.promptAttemptEpoch,
574
+ registration: envelope.registration,
575
+ }) === "allow-new-turn"
576
+ ? "fresh"
577
+ : "drop";
578
+ }
579
+ // The scope was evicted by the cap but its attempt-policy tombstone
580
+ // survives: classify by the retained policy while the registration is
581
+ // still exact (review thread P2).
582
+ const retained = retainedAttemptPolicies.get(`${envelope.lineageIdHash}\u0000${envelope.promptAttemptEpoch}`);
583
+ if (!retained) return "ordinary";
584
+ if (retained.ownedCompletionPolicy === "disabled") return "drop";
585
+ const registered = lookupOwnedRegistration(
586
+ envelope.registration.jobId,
587
+ envelope.registration.jobGeneration,
588
+ envelope.registration.endpointId,
589
+ );
590
+ // Compare EVERY tuple component against the authoritative registration: a
591
+ // (jobId, jobGeneration) may have been REBOUND to another lineage/epoch
592
+ // since the envelope was built, and classifying the stale envelope as fresh
593
+ // would resume work under the wrong ownership (review thread P2).
594
+ if (!registered) return "ordinary";
595
+ if (registered.endpointGeneration !== envelope.registration.endpointGeneration) return "ordinary";
596
+ if (registered.lineageIdHash !== envelope.lineageIdHash) return "ordinary";
597
+ if (registered.promptAttemptEpoch !== envelope.promptAttemptEpoch) return "ordinary";
598
+ if (registered.jobId !== envelope.registration.jobId) return "ordinary";
599
+ if (registered.jobGeneration !== envelope.registration.jobGeneration) return "ordinary";
600
+ return "fresh";
601
+ }
602
+
603
+ /** Whether an envelope must be kept in the batch (not an owned-scope drop). */
604
+ export function isOwnedCompletionEnvelopeAllowed(envelope: OwnedCompletionEnvelope): boolean {
605
+ return classifyOwnedEnvelope(envelope) !== "drop";
606
+ }
607
+ /** Structural subset of AsyncJobManager used by owned-stop settlement (avoids an import cycle). */
608
+ export interface OwnedStopManager {
609
+ cancel(jobId: string): boolean;
610
+ getJob(jobId: string): { generation?: string; status?: string } | undefined;
611
+ getJobPromise?(jobId: string, generation: string): Promise<void> | undefined;
612
+ acknowledgeDeliveries(jobIds: string[]): number;
613
+ }
614
+
615
+ /**
616
+ * Settle exact owned work for `scope:"owned"`: generation-verified cancel, a
617
+ * fixed grace, a second quiescence proof, then a delivery purge. Returns
618
+ * "stopped" only when every captured job is terminal after the grace; a reused
619
+ * job id with a new generation, a missing/evicted record, or still-running/
620
+ * paused work fails closed to "unsettled" (AC 16/36 — foreign work is never
621
+ * swept and unprovable quiescence never claims stopped).
622
+ */
623
+ export async function settleOwnedWork(
624
+ manager: OwnedStopManager,
625
+ exactJobs: TurnRegistrationKey[],
626
+ graceMs: number,
627
+ ): Promise<"stopped" | "unsettled"> {
628
+ let generationExact = true;
629
+ for (const reg of exactJobs) {
630
+ const live = manager.getJob(reg.jobId);
631
+ if (live !== undefined && live.generation !== reg.jobGeneration) {
632
+ generationExact = false;
633
+ continue;
634
+ }
635
+ manager.cancel(reg.jobId);
636
+ }
637
+ if (!generationExact) return "unsettled";
638
+ const deadline = Date.now() + graceMs;
639
+ await Bun.sleep(graceMs);
640
+ // Second proof: every captured job must still be the EXACT captured
641
+ // generation and terminal (cancelled/completed/failed). A reused job id
642
+ // with a NEW generation during the grace, a missing/evicted record, or a
643
+ // still-running/paused job fails closed — foreign work is never swept and
644
+ // unprovable quiescence never claims stopped. Additionally, the ACTUAL job
645
+ // promise must settle within the grace: AsyncJobManager.cancel() updates
646
+ // status to "cancelled" eagerly while the job function may still be
647
+ // unwinding, so the status alone is not proof of quiescence (review P1).
648
+ const quiescent = await Promise.all(
649
+ exactJobs.map(async reg => {
650
+ const job = manager.getJob(reg.jobId);
651
+ if (!job || job.generation !== reg.jobGeneration) return false;
652
+ if (job.status === "running" || job.status === "paused") return false;
653
+ const promise = manager.getJobPromise?.(reg.jobId, reg.jobGeneration);
654
+ if (!promise) return true; // no promise exposed — fall back to status
655
+ let settled = false;
656
+ try {
657
+ await Promise.race([
658
+ promise.then(
659
+ () => {
660
+ settled = true;
661
+ },
662
+ () => {
663
+ settled = true;
664
+ },
665
+ ),
666
+ Bun.sleep(Math.max(0, deadline - Date.now())),
667
+ ]);
668
+ } catch {
669
+ // ignore: a rejected promise still counts as unwound
670
+ }
671
+ return settled;
672
+ }),
673
+ );
674
+ if (!quiescent.every(Boolean)) return "unsettled";
675
+ manager.acknowledgeDeliveries(exactJobs.map(reg => reg.jobId));
676
+ // The jobs are terminal and their deliveries are suppressed: the owned
677
+ // registrations are settled, so remove them — otherwise they occupy the
678
+ // attempt indefinitely and the retained-policy tombstone eviction cannot
679
+ // distinguish live work from finished work (review thread P2).
680
+ for (const reg of exactJobs) unregisterOwnedRegistration(reg);
681
+ return "stopped";
682
+ }
683
+ export interface LineageBinding {
684
+ lineageIdHash: string;
685
+ promptAttemptEpoch: number;
686
+ endpointGeneration: number;
687
+ /** Endpoint (top-level session) identity that minted this binding. */
688
+ endpointId?: string;
689
+ }
690
+
691
+ const MAX_LINEAGE_BINDINGS = 8192;
692
+ // Keyed by (endpointId, toolCallId): concurrent sessions receive the SAME
693
+ // provider-local toolCallId, and a toolCallId-only key lets the later
694
+ // session's beforeToolCall overwrite the first session's binding — the first
695
+ // session's background Bash/task job would then be attributed to the second
696
+ // session's endpoint and lineage (review thread P1).
697
+ const lineageByToolCall = new Map<string, LineageBinding>();
698
+ // Tool calls whose afterToolCall settlement already ran. A later eviction of
699
+ // such a binding must NOT open a registration window: the call and its
700
+ // background registrations settled long ago, and the only removal path
701
+ // (settleToolLineageRegistrationWindow) will never run again, so the window
702
+ // and its incomplete-attempt marker would leak forever (review thread P2).
703
+ // Bounded by lineageByToolCall: entries are cleared on rebind and eviction.
704
+ const settledToolCallLineages = new Set<string>();
705
+ const toolCallLineageKey = (endpointId: string | undefined, toolCallId: string) =>
706
+ `${endpointId ?? ""}\u0000${toolCallId}`;
707
+
708
+ /**
709
+ * Bind immutable lineage/attempt metadata to an attempt-scoped tool call
710
+ * identity (toolCallId). The binding is set once at prompt admission and must
711
+ * never be mutated from a session-current fallback; missing/mismatched
712
+ * context fails closed (resolve returns undefined).
713
+ */
714
+ export function bindToolLineage(toolCallId: string, binding: LineageBinding): void {
715
+ if (lineageByToolCall.size >= MAX_LINEAGE_BINDINGS) {
716
+ const oldest = lineageByToolCall.keys().next().value;
717
+ if (oldest !== undefined) {
718
+ const evicted = lineageByToolCall.get(oldest);
719
+ lineageByToolCall.delete(oldest);
720
+ if (evicted) {
721
+ if (settledToolCallLineages.has(oldest)) {
722
+ // The evicted call already settled (afterToolCall ran): its
723
+ // owned registrations were captured during execution, so there
724
+ // is no in-flight window to protect. Retire the binding without
725
+ // creating a window or an incomplete marker — both would leak
726
+ // forever because the settlement callback never runs again
727
+ // (review thread P2).
728
+ settledToolCallLineages.delete(oldest);
729
+ } else {
730
+ // An evicted binding may belong to an IN-FLIGHT tool call whose
731
+ // background tool has not yet reached registerOwnedIfLineaged:
732
+ // its job would then launch with no owned registration and no
733
+ // evidence. Mark the evicted attempt's registration authority
734
+ // incomplete so a later scope:"owned" abort of THAT attempt fails
735
+ // closed to uncertainty instead of reporting stopped_owned
736
+ // over an incomplete causal set.
737
+ const incompleteKey = `${evicted.lineageIdHash}\u0000${evicted.promptAttemptEpoch}`;
738
+ markAttemptRegistrationIncomplete(evicted.lineageIdHash, evicted.promptAttemptEpoch);
739
+ // Retain the full lineage binding in the window: if this in-flight tool
740
+ // call still launches a background job, registerOwnedIfLineaged can
741
+ // register it under the retained lineage instead of leaving the
742
+ // attempt's causal set empty (review thread P2).
743
+ incompleteToolCallWindows.set(oldest, { incompleteKey, binding: evicted });
744
+ incompleteAttemptWindowCounts.set(
745
+ incompleteKey,
746
+ (incompleteAttemptWindowCounts.get(incompleteKey) ?? 0) + 1,
747
+ );
748
+ // The per-attempt marker above is deliberately scoped to the
749
+ // evicted attempt (never process-lifetime): every tool call binds
750
+ // a lineage entry and production never unbinds it (bindings
751
+ // intentionally survive the tool call so resumed registrations
752
+ // re-use the original id and task batches register one job per
753
+ // item), so a healthy long-running daemon inevitably reaches the
754
+ // 8,192 cap and the first eviction is ordinary historical churn —
755
+ // the evicted call and its job have settled long ago. A
756
+ // process-lifetime flag on ANY eviction would make every
757
+ // subsequent scope:"owned" abort fail with uncertainty forever,
758
+ // even when all evicted calls settled (review thread P2).
759
+ }
760
+ }
761
+ }
762
+ }
763
+ // A rebind supersedes any prior execution of the same id: the new execution
764
+ // is in flight until its own afterToolCall settles.
765
+ settledToolCallLineages.delete(toolCallLineageKey(binding.endpointId, toolCallId));
766
+ lineageByToolCall.set(toolCallLineageKey(binding.endpointId, toolCallId), binding);
767
+ }
768
+
769
+ export function resolveToolLineage(toolCallId: string | undefined, endpointId?: string): LineageBinding | undefined {
770
+ if (toolCallId === undefined) return undefined;
771
+ if (endpointId !== undefined) return lineageByToolCall.get(toolCallLineageKey(endpointId, toolCallId));
772
+ // Legacy endpoint-less callers: match by toolCallId. Prefer a binding whose
773
+ // endpoint equals the caller's, else the first match (review thread P1 —
774
+ // endpoint-aware callers must pass their endpoint to disambiguate
775
+ // concurrent sessions).
776
+ for (const [key, binding] of lineageByToolCall) {
777
+ if (key.endsWith(`\u0000${toolCallId}`)) return binding;
778
+ }
779
+ return undefined;
780
+ }
781
+
782
+ /** Close an evicted tool's registration window after its execution settles. */
783
+ export function settleToolLineageRegistrationWindow(toolCallId: string, endpointId?: string): void {
784
+ const key = toolCallLineageKey(endpointId, toolCallId);
785
+ // Mark the execution settled ONLY while its binding remains in the lineage
786
+ // map: a later FIFO eviction of that binding consults this set and removes
787
+ // the key. An already-evicted binding lives only in the window closed
788
+ // below — retaining the marker there would leak the key forever because
789
+ // no eviction can ever execute the delete (review thread P2).
790
+ if (lineageByToolCall.has(key)) settledToolCallLineages.add(key);
791
+ const window = incompleteToolCallWindows.get(key);
792
+ if (window !== undefined) {
793
+ incompleteToolCallWindows.delete(key);
794
+ const remaining = (incompleteAttemptWindowCounts.get(window.incompleteKey) ?? 1) - 1;
795
+ if (remaining <= 0) {
796
+ incompleteAttemptWindowCounts.delete(window.incompleteKey);
797
+ // The same attempt may ALSO carry saturation evidence (an owned job
798
+ // whose registration was rejected because the owned registry was
799
+ // full): keep the incomplete marker while either source remains, or
800
+ // a later scope:"owned" abort could omit that still-live job and
801
+ // claim stopped_owned (review thread P2).
802
+ if (!saturatedAttemptEndpoints.has(window.incompleteKey)) {
803
+ incompleteOwnedAttempts.delete(window.incompleteKey);
804
+ }
805
+ } else {
806
+ incompleteAttemptWindowCounts.set(window.incompleteKey, remaining);
807
+ }
808
+ }
809
+ }
810
+
811
+ export function unbindToolLineage(toolCallId: string, endpointId?: string): void {
812
+ if (endpointId !== undefined) {
813
+ lineageByToolCall.delete(toolCallLineageKey(endpointId, toolCallId));
814
+ return;
815
+ }
816
+ for (const key of lineageByToolCall.keys()) {
817
+ if (key.endsWith(`\u0000${toolCallId}`)) lineageByToolCall.delete(key);
818
+ }
819
+ }
820
+
821
+ /**
822
+ * Mint an unforgeable opaque lineage id for one prompt turn. The hash binds
823
+ * session id, attempt epoch, and a per-session secret; it never contains
824
+ * prompt body and cannot be re-derived from public session data. It is
825
+ * created before model/tool execution and must never be mutated from a
826
+ * session-current fallback.
827
+ */
828
+ export function mintTurnLineageIdHash(sessionId: string, promptAttemptEpoch: number, sessionSecret: string): string {
829
+ return createHash("sha256")
830
+ .update(`turn-lineage-v1:${sessionId}\u0000${promptAttemptEpoch}\u0000${sessionSecret}`)
831
+ .digest("hex");
832
+ }
833
+ /**
834
+ * Register an exact owned registration when the tool call carries immutable
835
+ * lineage metadata. The generation is read synchronously from the manager's
836
+ * job record; a missing generation fails closed (no ownership claim). A
837
+ * registry failure never breaks ordinary registration.
838
+ */
839
+ export function registerOwnedIfLineaged(
840
+ manager: { getJob?(id: string): { generation?: string; status?: string } | undefined },
841
+ toolCallId: string | undefined,
842
+ jobId: string,
843
+ endpointId?: string,
844
+ ): void {
845
+ try {
846
+ let lineage = resolveToolLineage(toolCallId, endpointId);
847
+ if (!lineage && toolCallId !== undefined) {
848
+ // The lineage binding may have been EVICTED by the cap while this tool
849
+ // call was still in flight. The eviction retained the binding's lineage
850
+ // in the registration window: register the launched job under that
851
+ // retained lineage so the attempt's causal set is complete instead of
852
+ // empty. Settling the window here would remove the attempt's only
853
+ // fail-closed marker while the unregistered job keeps running and a
854
+ // later scope:"owned" abort could claim stopped_owned over nothing
855
+ // (review thread P2). The window (and attempt marker) settles at
856
+ // afterToolCall once the tool call's execution ends.
857
+ lineage = incompleteToolCallWindows.get(toolCallLineageKey(endpointId, toolCallId))?.binding;
858
+ }
859
+ if (!lineage) return;
860
+ const job = manager.getJob?.(jobId);
861
+ const jobGeneration = job?.generation;
862
+ if (!jobGeneration) return;
863
+ registerOwnedRegistration(
864
+ {
865
+ ...(lineage.endpointId ? { endpointId: lineage.endpointId } : {}),
866
+ endpointGeneration: lineage.endpointGeneration,
867
+ lineageIdHash: lineage.lineageIdHash,
868
+ promptAttemptEpoch: lineage.promptAttemptEpoch,
869
+ jobId,
870
+ jobGeneration,
871
+ },
872
+ {
873
+ // Only FINISHED jobs are evictable under the cap, so a long-lived
874
+ // live job's ownership tuple is never dropped by shorter completed
875
+ // jobs (review thread P2). The check is scoped to the CANDIDATE's
876
+ // OWN endpoint: a candidate from another session (same reusable
877
+ // job id bg_1) must not be judged terminal using THIS manager's
878
+ // unrelated bg_1 record (review thread P2).
879
+ isJobTerminal: candidate => {
880
+ if ((candidate.endpointId ?? "") !== (lineage.endpointId ?? "")) return undefined;
881
+ const status = manager.getJob?.(candidate.jobId)?.status;
882
+ return status === "completed" || status === "cancelled" || status === "failed" || status === "evicted";
883
+ },
884
+ },
885
+ );
886
+ } catch {
887
+ // ignore: never break ordinary registration
888
+ }
889
+ }
890
+
891
+ /** Retire the exact (endpoint, jobId, jobGeneration) owned registration when
892
+ * its completion delivery is dead-lettered by the manager (delivery-queue
893
+ * overflow or retry exhaustion): no message is injected and no later
894
+ * consumption boundary will settle it, so the terminal tuple would otherwise
895
+ * occupy the global registries until saturation (review thread P2). The
896
+ * delivery is enqueued only when the job is terminal, so no live job's
897
+ * authority is dropped. */
898
+ export function retireOwnedRegistrationForDeadLetter(
899
+ endpointId: string | undefined,
900
+ jobId: string,
901
+ jobGeneration: string,
902
+ ): void {
903
+ const tuple = lookupOwnedRegistration(jobId, jobGeneration, endpointId);
904
+ if (tuple) unregisterOwnedRegistration(tuple);
905
+ }
906
+
907
+ let attemptEpochCounter = 0;
908
+
909
+ /** Monotonic fresh-attempt epoch for `resumeFromOwnedCompletion` allocation. */
910
+ export function nextPromptAttemptEpoch(): number {
911
+ return ++attemptEpochCounter;
912
+ }
913
+
914
+ /** Mint a fresh terminal scope id (opaque, never persisted raw). */
915
+ export function newTerminalScopeId(): string {
916
+ return randomUUID();
917
+ }
918
+
919
+ export interface TurnContinuationSeam {
920
+ fence: TurnContinuationFence;
921
+ gate: TurnContinuationGate;
922
+ }
923
+
924
+ /**
925
+ * Create a continuation fence + gate for one terminal scope. The fence starts
926
+ * `open` and is closed synchronously via `gate.close()` before the root turn is
927
+ * interrupted. Continuation authorization is source-based (lineageIdHash +
928
+ * attemptEpoch + continuationId); timing alone never authorizes.
929
+ */
930
+ export function createTurnContinuationSeam(options: {
931
+ lineageIdHash: string;
932
+ abortedAttemptEpoch: number;
933
+ terminalScopeId: string;
934
+ ownedCompletionPolicy?: OwnedCompletionPolicy;
935
+ blockedContinuationIds?: readonly string[];
936
+ }): TurnContinuationSeam {
937
+ const blocked = new Set<string>(options.blockedContinuationIds ?? []);
938
+ const predecessors = new Set<string>();
939
+ let state: TurnContinuationFenceState = "open";
940
+
941
+ const fence: TurnContinuationFence = {
942
+ state: "open",
943
+ lineageIdHash: options.lineageIdHash,
944
+ abortedAttemptEpoch: options.abortedAttemptEpoch,
945
+ terminalScopeId: options.terminalScopeId,
946
+ blockedContinuationIds: blocked,
947
+ predecessorTombstones: predecessors,
948
+ ownedCompletionPolicy: options.ownedCompletionPolicy ?? "enabled",
949
+ };
950
+
951
+ const gate: TurnContinuationGate = {
952
+ close(_reason: "terminal-turn") {
953
+ if (state === "closing" || state === "closed") return;
954
+ state = "closing";
955
+ state = "closed";
956
+ fence.state = state;
957
+ },
958
+ authorizeContinuation(origin) {
959
+ if (origin.kind !== "turn-continuation") return "deny";
960
+ if (origin.lineageIdHash !== fence.lineageIdHash || origin.attemptEpoch !== fence.abortedAttemptEpoch)
961
+ return "deny";
962
+ // A call linearized BEFORE close is a predecessor: record it once and
963
+ // allow it to finish its already-started work; it must never start a
964
+ // successor. After close, only recorded predecessors pass; every other
965
+ // same-turn continuation (retry/TTSR/steering/hidden/maintenance) is
966
+ // denied.
967
+ if (state === "open" || state === "closing") {
968
+ predecessors.add(origin.continuationId);
969
+ return "allow-predecessor";
970
+ }
971
+ return predecessors.has(origin.continuationId) ? "allow-predecessor" : "deny";
972
+ },
973
+ authorizeOwnedCompletion(origin) {
974
+ // Owned completion is intentionally NOT suppressed by a closed turn
975
+ // record. Validate exact source metadata and fail closed otherwise.
976
+ if (origin.kind !== "owned-completion") return "deny";
977
+ if (!origin.registration || typeof origin.registration !== "object") return "deny";
978
+ if (origin.lineageIdHash !== fence.lineageIdHash) return "deny";
979
+ if (origin.attemptEpoch !== fence.abortedAttemptEpoch) return "deny";
980
+ const { endpointGeneration, promptAttemptEpoch, jobId, jobGeneration } = origin.registration;
981
+ if (promptAttemptEpoch !== fence.abortedAttemptEpoch) return "deny";
982
+ if (fence.ownedCompletionPolicy === "disabled") return "deny";
983
+ if (
984
+ !Number.isFinite(endpointGeneration) ||
985
+ typeof jobId !== "string" ||
986
+ !jobId ||
987
+ typeof jobGeneration !== "string" ||
988
+ !jobGeneration
989
+ )
990
+ return "deny";
991
+ // The tuple must be an EXACT registered five-tuple: an unregistered,
992
+ // forged, or mutated registration fails closed even when the outer
993
+ // lineage/epoch match the aborted turn (AC 25 — missing/copied/
994
+ // mismatched origin never authorizes an automatic call). The lookup
995
+ // is ENDPOINT-scoped so concurrent sessions' same reusable job id
996
+ // never resolves to a foreign session's registration, which would
997
+ // deny a valid completion (review thread P1).
998
+ const registered = lookupOwnedRegistration(jobId, jobGeneration, origin.registration.endpointId);
999
+ if (!registered) return "deny";
1000
+ if (
1001
+ registered.lineageIdHash !== origin.lineageIdHash ||
1002
+ registered.promptAttemptEpoch !== promptAttemptEpoch ||
1003
+ registered.endpointGeneration !== endpointGeneration ||
1004
+ (registered.endpointId ?? "") !== (origin.registration.endpointId ?? "")
1005
+ )
1006
+ return "deny";
1007
+ return "allow-new-turn";
1008
+ },
1009
+ };
1010
+
1011
+ return { fence, gate };
1012
+ }
1013
+ export interface RegisteredTerminalScope {
1014
+ scopeId: string;
1015
+ lineageIdHash: string;
1016
+ promptAttemptEpoch: number;
1017
+ seam: TurnContinuationSeam;
1018
+ }
1019
+
1020
+ /**
1021
+ * Create, register, and synchronously close a terminal scope for one aborted
1022
+ * turn. The fence closes before the first await that interrupts the root turn;
1023
+ * owned-completion policy is enabled for `scope:"turn"` (left-running owned
1024
+ * delivery intentionally resumes the agent as a fresh turn) and disabled for
1025
+ * `scope:"owned"`. Registered scopes are process-local and bounded; the exact
1026
+ * (lineageIdHash, attemptEpoch) key makes later owned-completion classification
1027
+ * source-exact and fail-closed.
1028
+ */
1029
+ export function registerTerminalTurnScope(options: {
1030
+ lineageIdHash: string;
1031
+ promptAttemptEpoch: number;
1032
+ terminalScopeId?: string;
1033
+ ownedCompletionPolicy?: OwnedCompletionPolicy;
1034
+ blockedContinuationIds?: readonly string[];
1035
+ }): RegisteredTerminalScope | undefined {
1036
+ // Concurrent aborts of the SAME turn (dispatch-cache eviction can admit two
1037
+ // same-turn requests before either settles) must not overwrite the attempt
1038
+ // index with a second scope of a different owned-completion policy — a
1039
+ // losing scope:"owned" request would otherwise make a successful
1040
+ // scope:"turn" request's completions drop instead of resuming the fresh
1041
+ // turn (review thread P2). Reuse the first scope's authority.
1042
+ const existing = lookupTerminalScope(options.lineageIdHash, options.promptAttemptEpoch);
1043
+ if (existing) {
1044
+ // A concurrent abort with a DIFFERENT owned-completion policy must not
1045
+ // reuse this scope's gate: the losing request would report semantics
1046
+ // (automaticDelivery/ownedWork) that belong to the winner's policy — an
1047
+ // owned request winning would let the concurrent turn request claim
1048
+ // left_running/enabled, and vice versa. Fail the mismatched admission
1049
+ // closed so nothing is attributed to the losing request (review thread
1050
+ // P1); the winner's scope and policy stay untouched.
1051
+ if (existing.fence.ownedCompletionPolicy !== options.ownedCompletionPolicy) return undefined;
1052
+ return {
1053
+ scopeId: existing.scopeId,
1054
+ lineageIdHash: existing.lineageIdHash,
1055
+ promptAttemptEpoch: existing.abortedAttemptEpoch,
1056
+ seam: { fence: existing.fence, gate: existing.gate },
1057
+ };
1058
+ }
1059
+ const terminalScopeId = options.terminalScopeId ?? newTerminalScopeId();
1060
+ const seam = createTurnContinuationSeam({
1061
+ lineageIdHash: options.lineageIdHash,
1062
+ abortedAttemptEpoch: options.promptAttemptEpoch,
1063
+ terminalScopeId,
1064
+ ownedCompletionPolicy: options.ownedCompletionPolicy,
1065
+ blockedContinuationIds: options.blockedContinuationIds,
1066
+ });
1067
+ seam.gate.close("terminal-turn");
1068
+ const admitted = registerTerminalScope({
1069
+ scopeId: terminalScopeId,
1070
+ lineageIdHash: options.lineageIdHash,
1071
+ abortedAttemptEpoch: options.promptAttemptEpoch,
1072
+ gate: seam.gate,
1073
+ fence: seam.fence,
1074
+ });
1075
+ if (!admitted) return undefined;
1076
+ return {
1077
+ scopeId: terminalScopeId,
1078
+ lineageIdHash: options.lineageIdHash,
1079
+ promptAttemptEpoch: options.promptAttemptEpoch,
1080
+ seam,
1081
+ };
1082
+ }
1083
+
1084
+ /**
1085
+ * TEST-ONLY: clear the module-global terminal-abort registries so tests get
1086
+ * isolated lineage/binding/scope state. Never call from production code —
1087
+ * the registries are intentionally process-lifetime in the runtime.
1088
+ */
1089
+ export function resetTerminalAbortRegistriesForTests(): void {
1090
+ activeScopes.clear();
1091
+ activeScopeByAttempt.clear();
1092
+ ownedRegistrations.clear();
1093
+ lineageByToolCall.clear();
1094
+ settledToolCallLineages.clear();
1095
+ retainedOwnershipTuples.clear();
1096
+ retainedAttemptPolicies.clear();
1097
+ incompleteOwnedAttempts.clear();
1098
+ incompleteToolCallWindows.clear();
1099
+ incompleteAttemptWindowCounts.clear();
1100
+ retentionBacklog.clear();
1101
+ saturatedAttemptEndpoints.clear();
1102
+ }
1103
+
1104
+ /**
1105
+ * Structural subset of a durable terminal-scope row needed for bounding
1106
+ * retention (review thread P2). Only rows with a COMPLETED disposition are
1107
+ * evictable; pending markers are never touched.
1108
+ */
1109
+ export interface DurableScopeRetentionRow {
1110
+ idempotencyKeyHash?: string;
1111
+ idempotencyInputHash?: string;
1112
+ turnDisposition: "pending" | "no_effect" | (string & {});
1113
+ acceptedAt?: number;
1114
+ ownedWorkDisposition?: "not_requested" | "left_running" | "stopped" | "uncertain";
1115
+ responseState?: "pending" | "sent" | "delivered" | "failed";
1116
+ responsePayloadHash?: string;
1117
+ replayPayloadHash?: string;
1118
+ terminalPublished?: boolean;
1119
+ }
1120
+
1121
+ /**
1122
+ * Evict the OLDEST COMPLETED terminal-scope rows beyond `cap`, mirroring the
1123
+ * bounded in-memory idempotency cache so a long-lived session cannot grow
1124
+ * the durable reconciliation document indefinitely. Completed dispositions
1125
+ * (stopped/uncertain/no_effect) are evicted oldest-first; pending markers and
1126
+ * TRANSITIONAL no_effect_reserved reservations (an in-flight abort that may
1127
+ * still transition to active) are never evicted — evicting a reserved row
1128
+ * would leave a tombstone that replays uncertainty over an unfinalized
1129
+ * reservation (review thread P2). Returns a new array.
1130
+ */
1131
+ export function boundCompletedTerminalScopeRows<T extends DurableScopeRetentionRow>(rows: T[], cap: number): T[] {
1132
+ const isCompleted = (row: T): boolean =>
1133
+ row.turnDisposition !== "pending" && row.turnDisposition !== "no_effect_reserved";
1134
+ const retentionKey = (row: T): string => `${row.idempotencyKeyHash ?? ""}\u0000${row.idempotencyInputHash ?? ""}`;
1135
+ const completed = rows.filter(isCompleted);
1136
+ if (completed.length <= cap) return rows;
1137
+ const overflow = completed.length - cap;
1138
+ const evict = new Set(
1139
+ [...completed]
1140
+ .sort((a, b) => (a.acceptedAt ?? 0) - (b.acceptedAt ?? 0))
1141
+ .slice(0, overflow)
1142
+ .map(retentionKey),
1143
+ );
1144
+ // The evict set is keyed by key+input hash, so it must only be applied to
1145
+ // COMPLETED rows: a transitional no_effect_reserved reservation that happens
1146
+ // to share an evicted row's key pair must survive, exactly like a pending
1147
+ // marker (review thread P2).
1148
+ return rows.filter(row => !(isCompleted(row) && evict.has(retentionKey(row))));
1149
+ }
1150
+
1151
+ /**
1152
+ * Compact key tombstones for completed rows evicted by the retention cap: the
1153
+ * key+input hashes are retained durably so a same-key retry after dispatch
1154
+ * cache expiry/restart still replays instead of aborting an unrelated later
1155
+ * prompt (review thread P2).
1156
+ */
1157
+ /** Compact key tombstone with enough disposition metadata to reconstruct the
1158
+ * original replay result (the retention cap can evict stopped/uncertain rows,
1159
+ * not only no-effect reservations — review thread P2). */
1160
+ export interface EvictedTerminalKey {
1161
+ keyHash: string;
1162
+ inputHash: string;
1163
+ turnDisposition: "stopped" | "uncertain" | "no_effect" | "no_effect_marker_failure";
1164
+ ownedWorkDisposition: "not_requested" | "left_running" | "stopped" | "uncertain";
1165
+ responseState?: "pending" | "sent" | "delivered" | "failed";
1166
+ responsePayloadHash?: string;
1167
+ replayPayloadHash?: string;
1168
+ terminalPublished?: boolean;
1169
+ }
1170
+
1171
+ /** Bound the retained evicted-key tombstone collection FIFO: when a
1172
+ * long-lived session's unique terminal-abort keys evict completed rows until
1173
+ * the tombstone cap is reached, the OLDEST tombstones expire instead of the
1174
+ * next finalization throwing after the destructive stop already happened —
1175
+ * the client would otherwise receive an error while its durable row stays
1176
+ * pending, and subsequent aborts repeat the failure and accumulate
1177
+ * non-evictable pending rows (review thread P2). A dropped tombstone only
1178
+ * loses replay authority for keys older than the bound; the idempotency
1179
+ * guarantee degrades to the in-memory cache horizon instead of disabling
1180
+ * future aborts. */
1181
+ export function boundEvictedTerminalKeys<T extends { keyHash: string }>(keys: T[], cap: number): T[] {
1182
+ if (keys.length <= cap) return keys;
1183
+ return keys.slice(keys.length - cap);
1184
+ }
1185
+
1186
+ export function collectEvictedTerminalKeys<T extends DurableScopeRetentionRow>(
1187
+ before: T[],
1188
+ after: T[],
1189
+ ): EvictedTerminalKey[] {
1190
+ const afterKeys = new Set(after.map(s => `${s.idempotencyKeyHash ?? ""}\u0000${s.idempotencyInputHash ?? ""}`));
1191
+ const evicted: EvictedTerminalKey[] = [];
1192
+ for (const row of before) {
1193
+ // A TRANSITIONAL reservation is never evictable, so it must never mint a
1194
+ // tombstone either: the same completed-row definition as
1195
+ // `boundCompletedTerminalScopeRows` (review thread P2).
1196
+ if (row.turnDisposition === "pending" || row.turnDisposition === "no_effect_reserved") continue;
1197
+ const key = `${row.idempotencyKeyHash ?? ""}\u0000${row.idempotencyInputHash ?? ""}`;
1198
+ if (!afterKeys.has(key) && row.idempotencyKeyHash && row.idempotencyInputHash) {
1199
+ evicted.push({
1200
+ keyHash: row.idempotencyKeyHash,
1201
+ inputHash: row.idempotencyInputHash,
1202
+ turnDisposition: row.turnDisposition as "stopped" | "uncertain" | "no_effect" | "no_effect_marker_failure",
1203
+ ownedWorkDisposition: row.ownedWorkDisposition ?? "not_requested",
1204
+ responseState: row.responseState,
1205
+ responsePayloadHash: row.responsePayloadHash,
1206
+ // A pending finalized row's written replay must still advance the
1207
+ // evicted tombstone; preserve its replay-shaped hash (review
1208
+ // thread P2).
1209
+ replayPayloadHash: row.replayPayloadHash,
1210
+ terminalPublished: row.terminalPublished,
1211
+ });
1212
+ }
1213
+ }
1214
+ return evicted;
1215
+ }
1216
+
1217
+ /** Durable terminal-scope reservation cap. Idle/already-terminal aborts write
1218
+ * durable no-effect reservations, so a client sending idle aborts with unique
1219
+ * keys must not grow the reconciliation document indefinitely: only the OLDEST
1220
+ * COMPLETED rows beyond this cap are evicted (review thread P2). */
1221
+ export const MAX_DURABLE_TERMINAL_RESERVATIONS = 256;
1222
+ /** Retained evicted-key tombstone cap; see {@link boundEvictedTerminalKeys}. */
1223
+ export const MAX_RETAINED_TERMINAL_KEY_TOMBSTONES = 4096;
1224
+
1225
+ /**
1226
+ * Apply the durable terminal-scope retention bound to a pending
1227
+ * `transactTerminalState` mutation: evict the oldest COMPLETED scope rows past
1228
+ * {@link MAX_DURABLE_TERMINAL_RESERVATIONS}, retain a compact key tombstone for
1229
+ * every evicted row ATOMICALLY with the scope write, and FIFO-expire tombstones
1230
+ * past {@link MAX_RETAINED_TERMINAL_KEY_TOMBSTONES} instead of throwing after a
1231
+ * destructive stop already happened (review thread P2).
1232
+ *
1233
+ * Every durable terminal-state write — admission markers, no-effect
1234
+ * reservations, reservation finalization, and pending-marker transitions — must
1235
+ * go through this single bound in BOTH session runtimes (the notifications-hosted
1236
+ * bus runtime and the SDK-only host runtime); a hand-rolled copy is how the two
1237
+ * paths drift apart.
1238
+ */
1239
+ export function boundTerminalRetentionState<Row extends DurableScopeRetentionRow, Key extends { keyHash: string }>(
1240
+ priorKeys: readonly Key[],
1241
+ nextScopes: Row[],
1242
+ maxScopes: number = MAX_DURABLE_TERMINAL_RESERVATIONS,
1243
+ ): { scopes: Row[]; keys: Array<Key | EvictedTerminalKey> } {
1244
+ const scopes = boundCompletedTerminalScopeRows(nextScopes, maxScopes);
1245
+ const evicted = collectEvictedTerminalKeys(nextScopes, scopes);
1246
+ return {
1247
+ scopes,
1248
+ keys: boundEvictedTerminalKeys<Key | EvictedTerminalKey>(
1249
+ [...priorKeys, ...evicted],
1250
+ MAX_RETAINED_TERMINAL_KEY_TOMBSTONES,
1251
+ ),
1252
+ };
1253
+ }