@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
@@ -5,7 +5,10 @@ import * as path from "node:path";
5
5
  import type { AssistantMessage } from "@gajae-code/ai/core";
6
6
  import { normalizePathForComparison, postmortem } from "@gajae-code/utils";
7
7
  import { withFileLock } from "../config/file-lock";
8
+ import { reduceTerminalReceiptState } from "../sdk/receipt-state";
9
+ import { TOOL_CATALOG } from "../tools/tool-catalog.generated";
8
10
  import { sessionRoot, sessionRuntimeDir } from "./session-layout";
11
+ import { SessionStateLockUnavailableError, withSessionStateFileLock } from "./session-state-lock";
9
12
  import {
10
13
  isValidOwnerIntent,
11
14
  lifecyclePaths,
@@ -45,11 +48,31 @@ export type RuntimeState = "ready_for_input" | "running" | "needs_user_input" |
45
48
  type FinalResponseSource = "agent_end" | "launch_error";
46
49
  const MAX_PUBLIC_ERROR_MESSAGE_LENGTH = 2000;
47
50
  const HEARTBEAT_MS = 1000;
48
- const MAX_PUBLIC_ACTIVE_TOOLS = 8;
49
- const SAFE_TOOL_NAME = /^[A-Za-z][A-Za-z0-9_-]{0,63}$/;
50
51
 
51
- type LastPayloadCacheEntry = { mtimeMs: number; size: number; payload: Record<string, unknown> };
52
- const lastPayloadByStateFile = new Map<string, LastPayloadCacheEntry>();
52
+ /**
53
+ * Bound on the PUBLIC tool-activity list persisted in the coordinator-shared state file.
54
+ * The list is a "what is running right now" snapshot, not a log: an unbounded public list
55
+ * would let a fan-out turn grow the payload every coordinator reader must parse.
56
+ *
57
+ * Only the public projection is capped. The private `in_flight` table is exact current
58
+ * state — capping it would silently drop a live correlation and make `active_tool_count`
59
+ * a lie — so it holds every currently in-flight call and is emptied by their own ends.
60
+ */
61
+ const MAX_ACTIVE_TOOL_ENTRIES = 8;
62
+ /** Recorded whenever the caller could not prove a canonical public label for a tool. */
63
+ export const UNPROVEN_TOOL_LABEL = "custom";
64
+ /**
65
+ * The closed vocabulary a persisted or published tool label may draw from: `custom`, or
66
+ * the canonical name of a built-in tool descriptor.
67
+ *
68
+ * A pattern test is not a safety property — `cat`, `curl`, or any other token-shaped
69
+ * string passes one — so an arbitrary label is never accepted merely because it looks
70
+ * safe. Platform-excluded descriptors are included so a snapshot written on one host
71
+ * stays readable on another; the writer still only ever produces labels for the tools it
72
+ * actually resolved.
73
+ */
74
+ const RUNTIME_TOOL_LABELS: ReadonlySet<string> = new Set([UNPROVEN_TOOL_LABEL, ...Object.keys(TOOL_CATALOG)]);
75
+
53
76
  const stateFileWriteChains = new Map<string, Promise<void>>();
54
77
 
55
78
  /** Test-only counters for runtime sidecar hot-path assertions. */
@@ -64,10 +87,452 @@ interface RuntimeStateEvent {
64
87
  type: string;
65
88
  messages?: unknown[];
66
89
  toolCallId?: unknown;
67
- toolName?: unknown;
68
90
  isError?: unknown;
69
91
  }
70
92
 
93
+ export type RuntimeToolActivityPhase = "started" | "finished";
94
+ /**
95
+ * A finished call's outcome. `cancelled` is the only honest answer for a call that was
96
+ * still in flight when the session settled: it neither succeeded nor failed, and nothing
97
+ * observed its end.
98
+ */
99
+ export type RuntimeToolActivityOutcome = "success" | "failure" | "cancelled";
100
+ const RUNTIME_TOOL_ACTIVITY_OUTCOMES: readonly RuntimeToolActivityOutcome[] = ["success", "failure", "cancelled"];
101
+ /** Full SHA-256 hex: a prefix would let distinct calls collide into one correlation slot. */
102
+ const TOOL_CALL_DIGEST_PATTERN = /^[0-9a-f]{64}$/;
103
+
104
+ /**
105
+ * What a session is doing right now, split into a bounded public projection and the
106
+ * minimum private state needed to keep that projection exact.
107
+ *
108
+ * Deliberately carries no tool arguments, results, command text, paths, output, prompt or
109
+ * model text, environment values, or credentials — only a proven public tool label, a
110
+ * phase, and timing.
111
+ */
112
+ export interface RuntimeToolActivity {
113
+ seq: number;
114
+ last_activity_at: string;
115
+ tool: string;
116
+ phase: RuntimeToolActivityPhase;
117
+ outcome: RuntimeToolActivityOutcome | null;
118
+ elapsed_ms: number | null;
119
+ /** Exact number of in-flight calls; derived with `active_tools`, so it can never be smaller. */
120
+ active_tool_count: number;
121
+ /** Public projection of the newest in-flight calls, capped at `MAX_ACTIVE_TOOL_ENTRIES`. */
122
+ active_tools: Array<{ tool: string; started_at: string }>;
123
+ /**
124
+ * Private exact correlation state. Each digest is one-way over the session id and the
125
+ * tool call id, so no raw call id is persisted, and the whole field is stripped by
126
+ * `publicRuntimeToolActivity` before any coordinator reader sees the snapshot.
127
+ */
128
+ in_flight: Array<{ digest: string; tool: string; started_at: string }>;
129
+ }
130
+
131
+ /**
132
+ * Collapses anything the caller could not prove into `custom`.
133
+ *
134
+ * Public safety comes from the caller's proof plus this closed vocabulary, never from a
135
+ * syntax check: a label that merely looks like a token is still model-influenced text.
136
+ */
137
+ export function safeRuntimeToolLabel(value: unknown): string {
138
+ return typeof value === "string" && RUNTIME_TOOL_LABELS.has(value) ? value : UNPROVEN_TOOL_LABEL;
139
+ }
140
+
141
+ /**
142
+ * Whether a label read back from disk is already one this writer could have produced.
143
+ * Persisted text is never normalized into `custom`: silently rewriting arbitrary disk
144
+ * content into a valid-looking label would publish a value nothing ever proved.
145
+ */
146
+ function isSafeRuntimeToolLabel(value: unknown): value is string {
147
+ return typeof value === "string" && RUNTIME_TOOL_LABELS.has(value);
148
+ }
149
+
150
+ /** Exact own-key sets. An unknown key means the row was written by something else. */
151
+ const RUNTIME_TOOL_ACTIVITY_KEYS: readonly string[] = [
152
+ "seq",
153
+ "last_activity_at",
154
+ "tool",
155
+ "phase",
156
+ "outcome",
157
+ "elapsed_ms",
158
+ "active_tool_count",
159
+ "active_tools",
160
+ "in_flight",
161
+ ];
162
+ const IN_FLIGHT_TOOL_CALL_KEYS: readonly string[] = ["digest", "tool", "started_at"];
163
+ const ACTIVE_TOOL_KEYS: readonly string[] = ["tool", "started_at"];
164
+
165
+ /**
166
+ * An object read back from disk must carry EXACTLY the keys this writer emits.
167
+ *
168
+ * Tolerating extra keys would let a hand-edited or foreign writer smuggle `args`,
169
+ * `result`, `raw_id`, or a credential through every projection: the validator would
170
+ * ignore them and the reader would still see them in the file. Unknown keys make the
171
+ * whole activity malformed instead.
172
+ */
173
+ function hasExactOwnKeys(value: object, keys: readonly string[]): boolean {
174
+ const own = Object.keys(value);
175
+ return own.length === keys.length && keys.every(key => Object.hasOwn(value, key));
176
+ }
177
+
178
+ /** A timestamp is accepted only in the exact canonical form this writer emits. */
179
+ function isCanonicalIsoTimestamp(value: unknown): value is string {
180
+ if (typeof value !== "string" || value.length === 0) return false;
181
+ const parsed = new Date(value);
182
+ return Number.isFinite(parsed.getTime()) && parsed.toISOString() === value;
183
+ }
184
+
185
+ export function toolActivityPhaseForEvent(event: RuntimeStateEvent): RuntimeToolActivityPhase | null {
186
+ if (event.type === "tool_execution_start") return "started";
187
+ if (event.type === "tool_execution_end") return "finished";
188
+ return null;
189
+ }
190
+
191
+ /**
192
+ * Length-delimited UTF-16 code units of one identity field.
193
+ *
194
+ * JavaScript strings are UTF-16 code-unit sequences, and a call id may legitimately be
195
+ * any of them. Hashing the default UTF-8 encoding would map every distinct LONE
196
+ * SURROGATE to the same replacement character, so two different call ids would produce
197
+ * one digest and collide into a single in-flight correlation slot. Every code unit is
198
+ * hashed exactly, big-endian, behind its own uint32 length so no concatenation of two
199
+ * fields can be reinterpreted as a different pair.
200
+ */
201
+ function utf16IdentityField(value: string): Uint8Array {
202
+ const bytes = new Uint8Array(4 + value.length * 2);
203
+ const view = new DataView(bytes.buffer);
204
+ view.setUint32(0, value.length, false);
205
+ for (let index = 0; index < value.length; index++) view.setUint16(4 + index * 2, value.charCodeAt(index), false);
206
+ return bytes;
207
+ }
208
+
209
+ function toolCallDigest(sessionId: string, toolCallId: unknown): string | null {
210
+ // An all-whitespace id carries no correlation, but a non-empty one is hashed over its
211
+ // exact code units: trimming first would collapse ` a` and `a ` into one in-flight slot.
212
+ if (typeof toolCallId !== "string" || toolCallId.trim().length === 0) return null;
213
+ return createHash("sha256")
214
+ .update(utf16IdentityField(sessionId))
215
+ .update(utf16IdentityField(toolCallId))
216
+ .digest("hex");
217
+ }
218
+
219
+ function validInFlightToolCall(value: unknown): value is RuntimeToolActivity["in_flight"][number] {
220
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
221
+ if (!hasExactOwnKeys(value, IN_FLIGHT_TOOL_CALL_KEYS)) return false;
222
+ const entry = value as Record<string, unknown>;
223
+ return (
224
+ typeof entry.digest === "string" &&
225
+ TOOL_CALL_DIGEST_PATTERN.test(entry.digest) &&
226
+ isSafeRuntimeToolLabel(entry.tool) &&
227
+ isCanonicalIsoTimestamp(entry.started_at)
228
+ );
229
+ }
230
+
231
+ function validPublicActiveTool(value: unknown): boolean {
232
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
233
+ if (!hasExactOwnKeys(value, ACTIVE_TOOL_KEYS)) return false;
234
+ const entry = value as Record<string, unknown>;
235
+ return isSafeRuntimeToolLabel(entry.tool) && isCanonicalIsoTimestamp(entry.started_at);
236
+ }
237
+
238
+ /** The count and the capped list are always derived together, so they cannot contradict. */
239
+ function projectedActiveTools(
240
+ inFlight: RuntimeToolActivity["in_flight"],
241
+ ): Pick<RuntimeToolActivity, "active_tool_count" | "active_tools"> {
242
+ return {
243
+ active_tool_count: inFlight.length,
244
+ // Newest wins on overflow: the snapshot answers "what is running now".
245
+ active_tools: inFlight
246
+ .slice(-MAX_ACTIVE_TOOL_ENTRIES)
247
+ .map(entry => ({ tool: entry.tool, started_at: entry.started_at })),
248
+ };
249
+ }
250
+
251
+ export type RuntimeToolActivityReadout =
252
+ | { kind: "absent" }
253
+ | { kind: "malformed" }
254
+ | { kind: "valid"; activity: RuntimeToolActivity };
255
+
256
+ /**
257
+ * Re-validates an activity snapshot read back from disk.
258
+ *
259
+ * `absent` and `malformed` are deliberately distinct outcomes: an absent snapshot may be
260
+ * seeded from sequence 1, while a malformed one must never be replaced by a lower
261
+ * sequence. The public counters are re-derived from the private in-flight table rather
262
+ * than trusted from disk, so a hand-edited file cannot publish a contradiction.
263
+ */
264
+ export function classifyRuntimeToolActivity(value: unknown): RuntimeToolActivityReadout {
265
+ if (value === undefined) return { kind: "absent" };
266
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { kind: "malformed" };
267
+ if (!hasExactOwnKeys(value, RUNTIME_TOOL_ACTIVITY_KEYS)) return { kind: "malformed" };
268
+ const activity = value as Record<string, unknown>;
269
+ if (typeof activity.seq !== "number" || !Number.isSafeInteger(activity.seq) || activity.seq < 1)
270
+ return { kind: "malformed" };
271
+ if (!isCanonicalIsoTimestamp(activity.last_activity_at)) return { kind: "malformed" };
272
+ if (!isSafeRuntimeToolLabel(activity.tool)) return { kind: "malformed" };
273
+ if (activity.phase !== "started" && activity.phase !== "finished") return { kind: "malformed" };
274
+ // A started call has not finished, so it can carry neither an outcome nor a duration.
275
+ // A finished one must name an allowlisted outcome, and only a matched success/failure
276
+ // may carry a duration: an unmatched or cancelled call has no measured interval.
277
+ if (activity.phase === "started") {
278
+ if (activity.outcome !== null || activity.elapsed_ms !== null) return { kind: "malformed" };
279
+ } else {
280
+ if (!RUNTIME_TOOL_ACTIVITY_OUTCOMES.includes(activity.outcome as RuntimeToolActivityOutcome))
281
+ return { kind: "malformed" };
282
+ if (activity.elapsed_ms !== null) {
283
+ if (activity.outcome === "cancelled") return { kind: "malformed" };
284
+ // A duration is milliseconds of wall clock. Beyond the safe-integer range it is
285
+ // no longer an exact value, so it can never be one this writer measured.
286
+ if (
287
+ typeof activity.elapsed_ms !== "number" ||
288
+ !Number.isSafeInteger(activity.elapsed_ms) ||
289
+ activity.elapsed_ms < 0
290
+ )
291
+ return { kind: "malformed" };
292
+ }
293
+ }
294
+ // One invalid private row makes the whole snapshot malformed. Filtering rows and
295
+ // continuing would publish an exact-looking count derived from a file that was
296
+ // already corrupt.
297
+ if (!Array.isArray(activity.in_flight) || !activity.in_flight.every(validInFlightToolCall))
298
+ return { kind: "malformed" };
299
+ // The public rows are validated in their own right, not merely re-derived: an extra
300
+ // key on a public row is exactly what a coordinator reader would receive.
301
+ if (!Array.isArray(activity.active_tools) || !activity.active_tools.every(validPublicActiveTool))
302
+ return { kind: "malformed" };
303
+ const inFlight = (activity.in_flight as RuntimeToolActivity["in_flight"]).map(entry => ({
304
+ digest: entry.digest,
305
+ tool: entry.tool,
306
+ started_at: entry.started_at,
307
+ }));
308
+ if (new Set(inFlight.map(entry => entry.digest)).size !== inFlight.length) return { kind: "malformed" };
309
+ // A cancelled outcome is terminal settlement: it is only ever written together with an
310
+ // emptied in-flight set, so a cancelled snapshot that still claims live calls was not
311
+ // produced here.
312
+ if (activity.outcome === "cancelled" && inFlight.length > 0) return { kind: "malformed" };
313
+ // The persisted public counters must already equal the projection of the private set;
314
+ // repairing a contradiction here would republish a hand-edited file as authoritative.
315
+ const projected = projectedActiveTools(inFlight);
316
+ if (
317
+ activity.active_tool_count !== projected.active_tool_count ||
318
+ JSON.stringify(activity.active_tools) !== JSON.stringify(projected.active_tools)
319
+ )
320
+ return { kind: "malformed" };
321
+ return {
322
+ kind: "valid",
323
+ activity: {
324
+ seq: activity.seq,
325
+ last_activity_at: activity.last_activity_at,
326
+ tool: activity.tool,
327
+ phase: activity.phase,
328
+ outcome: (activity.outcome as RuntimeToolActivityOutcome | null) ?? null,
329
+ elapsed_ms: activity.elapsed_ms as number | null,
330
+ ...projected,
331
+ in_flight: inFlight,
332
+ },
333
+ };
334
+ }
335
+
336
+ export function normalizedRuntimeToolActivity(value: unknown): RuntimeToolActivity | null {
337
+ const readout = classifyRuntimeToolActivity(value);
338
+ return readout.kind === "valid" ? readout.activity : null;
339
+ }
340
+
341
+ /** The lifecycle states that are authority that nothing can still be running. */
342
+ const TERMINAL_LIFECYCLE_STATES: ReadonlySet<string> = new Set(["completed", "errored"]);
343
+
344
+ /**
345
+ * Whether an individually valid snapshot is consistent with the lifecycle state it would
346
+ * be published NEXT TO.
347
+ *
348
+ * The two fields are written by different authorities — the runtime sidecar annotates
349
+ * activity, the Coordinator writes lifecycle — so each can be valid while the pair is a
350
+ * contradiction: a settled session that still claims a tool is starting, or a live one
351
+ * whose calls were cancelled by a terminal it never reached. Publishing either would tell
352
+ * a coordinator reader something no writer ever observed.
353
+ *
354
+ * A terminal snapshot may still describe the call that just ended, and the terminal
355
+ * `cancelled` shape is exactly how settlement reports orphaned calls, so both stay
356
+ * publishable.
357
+ */
358
+ function activityMatchesLifecycle(activity: RuntimeToolActivity, lifecycleState: unknown): boolean {
359
+ if (typeof lifecycleState === "string" && TERMINAL_LIFECYCLE_STATES.has(lifecycleState)) {
360
+ if (activity.phase === "started") return false;
361
+ return activity.in_flight.length === 0 && activity.active_tool_count === 0;
362
+ }
363
+ return activity.outcome !== "cancelled";
364
+ }
365
+
366
+ /**
367
+ * The only activity shape a coordinator reader may see: the private correlation digests
368
+ * are dropped here, so no projection can leak them, and a snapshot that contradicts the
369
+ * lifecycle state it accompanies is withheld entirely.
370
+ *
371
+ * Withheld, never repaired: the bytes on disk are preserved exactly as written — a
372
+ * malformed or contradictory snapshot is evidence — while every public projection shows
373
+ * nothing rather than a reconciled guess.
374
+ */
375
+ export function publicRuntimeToolActivity(value: unknown, lifecycleState: unknown): Record<string, unknown> | null {
376
+ const activity = normalizedRuntimeToolActivity(value);
377
+ if (!activity || !activityMatchesLifecycle(activity, lifecycleState)) return null;
378
+ return {
379
+ seq: activity.seq,
380
+ last_activity_at: activity.last_activity_at,
381
+ tool: activity.tool,
382
+ phase: activity.phase,
383
+ outcome: activity.outcome,
384
+ elapsed_ms: activity.elapsed_ms,
385
+ active_tool_count: activity.active_tool_count,
386
+ active_tools: activity.active_tools,
387
+ };
388
+ }
389
+
390
+ /**
391
+ * What the caller observed at the exact moment the agent event was dispatched.
392
+ *
393
+ * Both the timestamp and the label are captured at that synchronous boundary, never
394
+ * re-derived here: a later lookup would see a tool registry that may already have been
395
+ * replaced, and a later clock reading would fold subscriber and lock latency into the
396
+ * measured interval.
397
+ */
398
+ export interface CoordinatorToolObservation {
399
+ /** Canonical public label proven at the observation boundary; anything else is `custom`. */
400
+ label: string;
401
+ /** Wall clock captured when the agent event was observed, before any queueing or lock wait. */
402
+ observedAt: string;
403
+ }
404
+
405
+ interface RuntimeToolObservation {
406
+ phase: RuntimeToolActivityPhase;
407
+ /** Canonical public label proven by the caller; anything else records `custom`. */
408
+ label: string | undefined;
409
+ /** One-way correlation digest, or null when the event carried no usable call id. */
410
+ digest: string | null;
411
+ isError: boolean;
412
+ /** Wall clock captured when the event was observed, before any queueing or lock wait. */
413
+ observedAt: string;
414
+ }
415
+
416
+ function nextInFlightToolCalls(
417
+ previous: RuntimeToolActivity["in_flight"],
418
+ observation: RuntimeToolObservation,
419
+ tool: string,
420
+ ): RuntimeToolActivity["in_flight"] {
421
+ // An event without a usable call id cannot be correlated, so it must neither add nor
422
+ // remove exact accounting state.
423
+ if (observation.digest === null) return previous;
424
+ if (observation.phase === "started") {
425
+ // A valid start is idempotent by digest, and a repeat keeps the first observation.
426
+ if (previous.some(entry => entry.digest === observation.digest)) return previous;
427
+ // Never capped: every live correlation is kept so the count stays exact.
428
+ return [...previous, { digest: observation.digest, tool, started_at: observation.observedAt }];
429
+ }
430
+ // Only a confirmed active call is closed; a duplicate or unmatched end changes nothing.
431
+ const index = previous.findIndex(entry => entry.digest === observation.digest);
432
+ if (index < 0) return previous;
433
+ return previous.filter((_entry, position) => position !== index);
434
+ }
435
+
436
+ /** Sequence is monotonic; at the safe-integer ceiling it stops rather than wrapping. */
437
+ function nextRuntimeToolActivitySeq(previous: number): number {
438
+ return Number.isSafeInteger(previous + 1) ? previous + 1 : previous;
439
+ }
440
+
441
+ /**
442
+ * Milliseconds between two observed events, or `null` when there is no exact answer.
443
+ *
444
+ * The reader accepts only a non-negative safe integer, because past that a millisecond
445
+ * count is no longer an exact value and so cannot be one anything measured. The writer
446
+ * therefore has to reach the same verdict on the same rule, or it can persist a number its
447
+ * own validator calls malformed — and a malformed snapshot fences out every later activity
448
+ * write on that session.
449
+ *
450
+ * Two canonical timestamps really can be that far apart: the representable `Date` range
451
+ * spans 1.728e16 ms, nearly twice `MAX_SAFE_INTEGER`. Saturating at the ceiling would
452
+ * publish a duration nothing observed, so an unmeasurable interval is reported as exactly
453
+ * that — the outcome still stands, only the interval is absent.
454
+ */
455
+ function measuredElapsedMs(startedAtMs: number, observedAtMs: number): number | null {
456
+ if (!Number.isFinite(startedAtMs) || !Number.isFinite(observedAtMs)) return null;
457
+ const elapsed = Math.round(observedAtMs - startedAtMs);
458
+ return Number.isSafeInteger(elapsed) && elapsed >= 0 ? elapsed : null;
459
+ }
460
+
461
+ function nextRuntimeToolActivity(
462
+ previous: RuntimeToolActivity | null,
463
+ observation: RuntimeToolObservation,
464
+ ): RuntimeToolActivity {
465
+ const priorInFlight = previous?.in_flight ?? [];
466
+ const match =
467
+ observation.phase === "finished" && observation.digest !== null
468
+ ? priorInFlight.find(entry => entry.digest === observation.digest)
469
+ : undefined;
470
+ // A matched end reports the label its own START proved. Re-labelling from the end
471
+ // event would read a registry that may have been replaced mid-call, so one call could
472
+ // open as `bash` and close as something else. An unmatched end proved nothing.
473
+ const tool = match ? match.tool : safeRuntimeToolLabel(observation.label);
474
+ const startedAtMs = match ? Date.parse(match.started_at) : Number.NaN;
475
+ const inFlight = nextInFlightToolCalls(priorInFlight, observation, tool);
476
+ return {
477
+ seq: nextRuntimeToolActivitySeq(previous?.seq ?? 0),
478
+ last_activity_at: observation.observedAt,
479
+ tool,
480
+ phase: observation.phase,
481
+ outcome: observation.phase === "started" ? null : observation.isError ? "failure" : "success",
482
+ // Both ends are event observation times, so lock contention cannot compress a real
483
+ // interval into near zero.
484
+ elapsed_ms: measuredElapsedMs(startedAtMs, Date.parse(observation.observedAt)),
485
+ ...projectedActiveTools(inFlight),
486
+ in_flight: inFlight,
487
+ };
488
+ }
489
+
490
+ /**
491
+ * Settle the activity snapshot a terminal lifecycle transition inherits.
492
+ *
493
+ * A settled session cannot still be running a tool, and later tool events are fenced out,
494
+ * so an unmatched start would otherwise leave a nonzero `active_tool_count` forever. The
495
+ * orphans are represented honestly — finished, `cancelled`, no elapsed interval — rather
496
+ * than claimed as a success or a failure nothing observed.
497
+ *
498
+ * The same helper serves the runtime `agent_end`/postmortem path and the Coordinator's
499
+ * canonical terminal repair so the two writers cannot settle differently.
500
+ *
501
+ * Two shapes need settling, not one. An orphaned in-flight call is the obvious case. The
502
+ * other is a `phase: started` snapshot with NOTHING correlated — a start whose call id was
503
+ * missing or blank never entered the in-flight table, so no end can ever close it — and
504
+ * leaving it would write the prohibited terminal+started pair, a settled session that
505
+ * still claims a tool is starting. An already-finished snapshot with nothing in flight is
506
+ * returned unchanged: a normal terminal transition is not an activity event.
507
+ */
508
+ export function terminallySettledRuntimeToolActivity(value: unknown, observedAt: string): RuntimeToolActivityReadout {
509
+ const readout = classifyRuntimeToolActivity(value);
510
+ if (readout.kind !== "valid") return readout;
511
+ const previous = readout.activity;
512
+ if (previous.in_flight.length === 0 && previous.phase !== "started") return readout;
513
+ const previousMs = Date.parse(previous.last_activity_at);
514
+ const observedMs = Date.parse(observedAt);
515
+ // Terminal authority wins even at the sequence ceiling, where seq stays put; the
516
+ // last-observation timestamp never moves backwards.
517
+ const lastActivityAt =
518
+ isCanonicalIsoTimestamp(observedAt) && (!Number.isFinite(previousMs) || observedMs >= previousMs)
519
+ ? observedAt
520
+ : previous.last_activity_at;
521
+ return {
522
+ kind: "valid",
523
+ activity: {
524
+ seq: nextRuntimeToolActivitySeq(previous.seq),
525
+ last_activity_at: lastActivityAt,
526
+ tool: previous.tool,
527
+ phase: "finished",
528
+ outcome: "cancelled",
529
+ elapsed_ms: null,
530
+ ...projectedActiveTools([]),
531
+ in_flight: [],
532
+ },
533
+ };
534
+ }
535
+
71
536
  export interface OwnerTerminalContext {
72
537
  generation: string;
73
538
  stateDir: string;
@@ -373,96 +838,9 @@ export function stateForEvent(event: RuntimeStateEvent): RuntimeState | null {
373
838
  return null;
374
839
  }
375
840
 
841
+ /** True for every event the coordinator-shared file records: lifecycle state or tool activity. */
376
842
  export function eventAffectsCoordinatorRuntimeState(event: RuntimeStateEvent): boolean {
377
- return stateForEvent(event) !== null || toolActivityForEvent(event) !== null;
378
- }
379
-
380
- function safeToolName(value: unknown): string {
381
- return typeof value === "string" && SAFE_TOOL_NAME.test(value) ? value : "custom";
382
- }
383
-
384
- function toolActivityForEvent(
385
- event: RuntimeStateEvent,
386
- ):
387
- | { phase: "started"; toolCallKey: string; toolName: string }
388
- | { phase: "finished"; toolCallKey: string; toolName: string; outcome: "succeeded" | "failed" }
389
- | null {
390
- if (event.type !== "tool_execution_start" && event.type !== "tool_execution_end") return null;
391
- if (typeof event.toolCallId !== "string") return null;
392
- const toolCallKey = createHash("sha256").update(event.toolCallId).digest("hex");
393
- if (event.type === "tool_execution_start")
394
- return { phase: "started", toolCallKey, toolName: safeToolName(event.toolName) };
395
- return {
396
- phase: "finished",
397
- toolCallKey,
398
- toolName: safeToolName(event.toolName),
399
- outcome: event.isError === true ? "failed" : "succeeded",
400
- };
401
- }
402
-
403
- function runtimeStateFromPrevious(value: unknown): RuntimeState {
404
- return value === "ready_for_input" ||
405
- value === "running" ||
406
- value === "needs_user_input" ||
407
- value === "completed" ||
408
- value === "errored"
409
- ? value
410
- : "running";
411
- }
412
-
413
- function activityFieldsForEvent(
414
- previous: Record<string, unknown>,
415
- activityEvent: NonNullable<ReturnType<typeof toolActivityForEvent>>,
416
- now: string,
417
- nowMs: number,
418
- ): Record<string, unknown> {
419
- const activeToolCalls: Record<string, { tool_name: string; started_at: string }> = {};
420
- const existing = previous.active_tool_calls;
421
- if (existing && typeof existing === "object" && !Array.isArray(existing)) {
422
- for (const [key, value] of Object.entries(existing)) {
423
- if (!value || typeof value !== "object" || Array.isArray(value)) continue;
424
- const entry = value as Record<string, unknown>;
425
- if (typeof entry.tool_name !== "string" || typeof entry.started_at !== "string") continue;
426
- if (!Number.isFinite(Date.parse(entry.started_at))) continue;
427
- activeToolCalls[key] = { tool_name: safeToolName(entry.tool_name), started_at: entry.started_at };
428
- }
429
- }
430
- const prior = activeToolCalls[activityEvent.toolCallKey];
431
- if (activityEvent.phase === "started")
432
- activeToolCalls[activityEvent.toolCallKey] = { tool_name: activityEvent.toolName, started_at: now };
433
- else delete activeToolCalls[activityEvent.toolCallKey];
434
- const previousActivity =
435
- previous.activity && typeof previous.activity === "object" && !Array.isArray(previous.activity)
436
- ? (previous.activity as Record<string, unknown>)
437
- : null;
438
- const sequence =
439
- previousActivity &&
440
- typeof previousActivity.sequence === "number" &&
441
- Number.isSafeInteger(previousActivity.sequence) &&
442
- previousActivity.sequence >= 0
443
- ? Math.min(Number.MAX_SAFE_INTEGER, previousActivity.sequence + 1)
444
- : 1;
445
- const activeTools = Object.values(activeToolCalls)
446
- .sort((left, right) => left.started_at.localeCompare(right.started_at))
447
- .slice(0, MAX_PUBLIC_ACTIVE_TOOLS);
448
- const startedAt = activityEvent.phase === "started" ? now : prior?.started_at;
449
- const elapsedMs = startedAt ? Math.max(0, nowMs - Date.parse(startedAt)) : undefined;
450
- return {
451
- last_activity_at: now,
452
- activity: {
453
- sequence,
454
- kind: "tool_execution",
455
- phase: activityEvent.phase,
456
- tool_name: activityEvent.toolName,
457
- observed_at: now,
458
- ...(startedAt ? { started_at: startedAt } : {}),
459
- ...(elapsedMs === undefined ? {} : { elapsed_ms: elapsedMs }),
460
- ...(activityEvent.phase === "finished" ? { outcome: activityEvent.outcome } : {}),
461
- active_tool_count: Object.keys(activeToolCalls).length,
462
- },
463
- active_tools: activeTools,
464
- active_tool_calls: activeToolCalls,
465
- };
843
+ return stateForEvent(event) !== null || toolActivityPhaseForEvent(event) !== null;
466
844
  }
467
845
 
468
846
  class PreviousRuntimeStateReadError extends Error {
@@ -472,6 +850,13 @@ class PreviousRuntimeStateReadError extends Error {
472
850
  }
473
851
  }
474
852
 
853
+ class RuntimeToolActivityRefusedError extends Error {
854
+ constructor(reason: string) {
855
+ super(`Refusing to overwrite the coordinator tool-activity snapshot: ${reason}.`);
856
+ this.name = "RuntimeToolActivityRefusedError";
857
+ }
858
+ }
859
+
475
860
  function isAbsentStateFileError(error: unknown): boolean {
476
861
  return (error as { code?: unknown }).code === "ENOENT";
477
862
  }
@@ -520,7 +905,7 @@ function validPreviousRuntimeStatePayload(value: unknown): value is Record<strin
520
905
  )
521
906
  return false;
522
907
  if (payload.ready_for_input !== undefined) {
523
- const expectedReady = payload.state === "ready_for_input" || payload.state === "completed";
908
+ const expectedReady = payload.state === "ready_for_input";
524
909
  if (payload.ready_for_input !== expectedReady) return false;
525
910
  }
526
911
  if (payload.live !== undefined && payload.live !== null && payload.live !== (payload.state === "running"))
@@ -533,40 +918,38 @@ function readPreviousPayload(stateFile: string): Record<string, unknown> {
533
918
  try {
534
919
  raw = fsSync.readFileSync(stateFile, "utf8");
535
920
  } catch (error) {
536
- lastPayloadByStateFile.delete(stateFile);
537
921
  if (isAbsentStateFileError(error)) return {};
538
922
  throw new PreviousRuntimeStateReadError();
539
923
  }
540
924
  try {
541
925
  return parsePreviousPayload(raw);
542
926
  } catch (error) {
543
- lastPayloadByStateFile.delete(stateFile);
544
927
  if (error instanceof PreviousRuntimeStateReadError) throw error;
545
928
  throw new PreviousRuntimeStateReadError();
546
929
  }
547
930
  }
548
931
 
932
+ /**
933
+ * Reads the authoritative bytes on disk, every time, inside the state-file lock.
934
+ *
935
+ * There is deliberately no metadata (mtime+size) cache here: another process holding the
936
+ * same lock can rewrite this file to a same-size terminal payload within one filesystem
937
+ * timestamp tick, and a cache hit would then let a late tool event overwrite a settled
938
+ * session back into a running-looking one. The file is small and the read happens once
939
+ * per lock acquisition.
940
+ */
549
941
  async function readPreviousPayloadForEvent(stateFile: string): Promise<Record<string, unknown>> {
550
942
  let stat: fsSync.Stats;
551
943
  try {
552
944
  stat = await fs.stat(stateFile);
553
945
  } catch (error) {
554
- lastPayloadByStateFile.delete(stateFile);
555
946
  if (isAbsentStateFileError(error)) return {};
556
947
  throw new PreviousRuntimeStateReadError();
557
948
  }
558
- if (!stat.isFile()) {
559
- lastPayloadByStateFile.delete(stateFile);
560
- throw new PreviousRuntimeStateReadError();
561
- }
562
- const cached = lastPayloadByStateFile.get(stateFile);
563
- if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) return cached.payload;
949
+ if (!stat.isFile()) throw new PreviousRuntimeStateReadError();
564
950
  try {
565
- const payload = parsePreviousPayload(await Bun.file(stateFile).text());
566
- lastPayloadByStateFile.set(stateFile, { mtimeMs: stat.mtimeMs, size: stat.size, payload });
567
- return payload;
951
+ return parsePreviousPayload(await fs.readFile(stateFile, "utf8"));
568
952
  } catch (error) {
569
- lastPayloadByStateFile.delete(stateFile);
570
953
  if (error instanceof PreviousRuntimeStateReadError) throw error;
571
954
  throw new PreviousRuntimeStateReadError();
572
955
  }
@@ -591,14 +974,6 @@ function shouldSkipRuntimeStateWrite(
591
974
  return nowMs - previousUpdatedAt < HEARTBEAT_MS;
592
975
  }
593
976
 
594
- function rememberWrittenPayload(stateFile: string, payload: Record<string, unknown>): void {
595
- try {
596
- const stat = fsSync.statSync(stateFile);
597
- lastPayloadByStateFile.set(stateFile, { mtimeMs: stat.mtimeMs, size: stat.size, payload });
598
- } catch {
599
- lastPayloadByStateFile.delete(stateFile);
600
- }
601
- }
602
977
  function shouldPreserveTerminalPayload(previous: RuntimeStateSidecarPayload, input: RuntimeStateIdentity): boolean {
603
978
  if (!validRuntimeStateMarker(previous)) return false;
604
979
  if (previous.state !== "completed" && previous.state !== "errored") return false;
@@ -661,11 +1036,22 @@ function basePayload(input: {
661
1036
  }): Record<string, unknown> {
662
1037
  const identity = normalizedIdentity(input.context);
663
1038
  if (identity.sessionId !== input.sessionId) throw new PreviousRuntimeStateReadError();
1039
+ // A lifecycle transition says nothing about tool activity, so a valid snapshot is
1040
+ // carried forward and a malformed one verbatim: dropping it would silently reset the
1041
+ // per-session sequence, and the public projection refuses to publish it either way.
1042
+ // A TERMINAL transition is the one exception — it is authority that nothing can still
1043
+ // be running, so any orphaned in-flight call is settled here.
1044
+ const terminal = input.state === "completed" || input.state === "errored";
1045
+ const readout = terminal
1046
+ ? terminallySettledRuntimeToolActivity(input.previous.activity, input.now)
1047
+ : classifyRuntimeToolActivity(input.previous.activity);
1048
+ const activity =
1049
+ readout.kind === "absent" ? undefined : readout.kind === "valid" ? readout.activity : input.previous.activity;
664
1050
  return {
665
1051
  schema_version: 1,
666
1052
  session_id: identity.sessionId,
667
1053
  state: input.state,
668
- ready_for_input: input.state === "completed" || input.state === "ready_for_input",
1054
+ ready_for_input: input.state === "ready_for_input",
669
1055
  updated_at: input.now,
670
1056
  current_turn_id: typeof input.previous.current_turn_id === "string" ? input.previous.current_turn_id : null,
671
1057
  last_turn_id: typeof input.previous.last_turn_id === "string" ? input.previous.last_turn_id : null,
@@ -677,16 +1063,7 @@ function basePayload(input: {
677
1063
  workdir: identity.workdir,
678
1064
  branch: branchForContext(input.context),
679
1065
  session_file: identity.sessionFile,
680
- ...(typeof input.previous.last_activity_at === "string"
681
- ? { last_activity_at: input.previous.last_activity_at }
682
- : {}),
683
- ...(input.previous.activity && typeof input.previous.activity === "object"
684
- ? { activity: input.previous.activity }
685
- : {}),
686
- ...(Array.isArray(input.previous.active_tools) ? { active_tools: input.previous.active_tools } : {}),
687
- ...(input.previous.active_tool_calls && typeof input.previous.active_tool_calls === "object"
688
- ? { active_tool_calls: input.previous.active_tool_calls }
689
- : {}),
1066
+ ...(activity === undefined ? {} : { activity }),
690
1067
  ...(input.context.ownerTerminal ? { owner_generation: input.context.ownerTerminal.generation } : {}),
691
1068
  };
692
1069
  }
@@ -849,13 +1226,20 @@ async function writeStateFileSync(stateFile: string, payload: Record<string, unk
849
1226
  await writeStateFile(stateFile, payload);
850
1227
  }
851
1228
 
1229
+ /**
1230
+ * The state-file critical section, shared byte-for-byte with the Coordinator MCP writer.
1231
+ *
1232
+ * The shared implementation writes the base Coordinator's regular-file `<file>.lock`
1233
+ * owner JSON, so both writers recognize each other's owners. The base RUNTIME guarded
1234
+ * this same path with the generic directory-style lock instead, so a leftover
1235
+ * `<file>.lock/` directory is also recognized and reclaimed under its own protocol rather
1236
+ * than faulting forever.
1237
+ */
852
1238
  async function withStateFileLock<T>(stateFile: string, operation: () => Promise<T>): Promise<T> {
853
1239
  try {
854
- return await withFileLock(stateFile, operation, { staleMs: 30_000, retries: 12_000, retryDelayMs: 5 });
1240
+ return await withSessionStateFileLock(stateFile, operation);
855
1241
  } catch (error) {
856
- if (error instanceof Error && error.message.startsWith("Failed to acquire lock")) {
857
- throw new PreviousRuntimeStateReadError();
858
- }
1242
+ if (error instanceof SessionStateLockUnavailableError) throw new PreviousRuntimeStateReadError();
859
1243
  throw error;
860
1244
  }
861
1245
  }
@@ -864,14 +1248,28 @@ function coordinatorTransactionLockFile(stateFile: string): string {
864
1248
  return path.resolve(path.dirname(stateFile), "..", "locks", "mutation.lock");
865
1249
  }
866
1250
 
1251
+ /**
1252
+ * The outer namespace-wide transaction lock. It guards no single JSON document of its
1253
+ * own, so it stays on the generic directory-style lock rather than the state-file owner
1254
+ * protocol.
1255
+ */
867
1256
  async function withCoordinatorTransactionLock<T>(stateFile: string, operation: () => Promise<T>): Promise<T> {
868
- return await withStateFileLock(coordinatorTransactionLockFile(stateFile), operation);
1257
+ try {
1258
+ return await withFileLock(coordinatorTransactionLockFile(stateFile), operation, {
1259
+ staleMs: 30_000,
1260
+ retries: 12_000,
1261
+ retryDelayMs: 5,
1262
+ });
1263
+ } catch (error) {
1264
+ if (error instanceof Error && error.message.startsWith("Failed to acquire lock"))
1265
+ throw new PreviousRuntimeStateReadError();
1266
+ throw error;
1267
+ }
869
1268
  }
870
1269
 
871
1270
  async function writeStateFile(stateFile: string, payload: Record<string, unknown>): Promise<void> {
872
1271
  await fs.mkdir(path.dirname(stateFile), { recursive: true });
873
1272
  await Bun.write(stateFile, `${JSON.stringify(payload)}\n`);
874
- rememberWrittenPayload(stateFile, payload);
875
1273
  }
876
1274
 
877
1275
  function contextWithManagedOwnerGeneration(context: RuntimeStateContext): RuntimeStateContext {
@@ -881,15 +1279,93 @@ function contextWithManagedOwnerGeneration(context: RuntimeStateContext): Runtim
881
1279
  return ownerTerminal ? { ...context, ownerTerminal } : context;
882
1280
  }
883
1281
 
1282
+ /**
1283
+ * Annotates the existing coordinator-shared runtime state with a tool-activity snapshot.
1284
+ *
1285
+ * Activity is an annotation on a lifecycle state, never a lifecycle state of its own: it
1286
+ * refuses to seed a state file, refuses to touch a session that already settled, and leaves
1287
+ * `updated_at` alone so the running-heartbeat cadence stays measured from lifecycle writes.
1288
+ */
1289
+ async function persistCoordinatorRuntimeToolActivity(
1290
+ event: RuntimeStateEvent,
1291
+ context: RuntimeStateContext,
1292
+ stateFile: string,
1293
+ phase: RuntimeToolActivityPhase,
1294
+ label: string | undefined,
1295
+ observedAt: string,
1296
+ ): Promise<void> {
1297
+ const identity = normalizedIdentity(context);
1298
+ await serializeStateFileWrite(
1299
+ stateFile,
1300
+ async () =>
1301
+ await withCoordinatorTransactionLock(
1302
+ stateFile,
1303
+ async () =>
1304
+ await withStateFileLock(stateFile, async () => {
1305
+ const previous = await readPreviousPayloadForEvent(stateFile);
1306
+ if (Object.keys(previous).length === 0) return;
1307
+ assertPreviousRuntimeStateIdentity(previous, identity);
1308
+ // A tool event that lands after the session settled must never
1309
+ // resurrect it into a live-looking state.
1310
+ if (previous.state === "completed" || previous.state === "errored") return;
1311
+ const readout = classifyRuntimeToolActivity(previous.activity);
1312
+ // Fail closed rather than replace an unreadable snapshot, or a sequence
1313
+ // that can no longer advance, with a lower one.
1314
+ if (readout.kind === "malformed")
1315
+ throw new RuntimeToolActivityRefusedError("the persisted snapshot is malformed");
1316
+ const priorActivity = readout.kind === "valid" ? readout.activity : null;
1317
+ if (!Number.isSafeInteger((priorActivity?.seq ?? 0) + 1))
1318
+ throw new RuntimeToolActivityRefusedError("its sequence cannot advance safely");
1319
+ await writeStateFile(stateFile, {
1320
+ ...previous,
1321
+ activity: nextRuntimeToolActivity(priorActivity, {
1322
+ phase,
1323
+ label,
1324
+ digest: toolCallDigest(identity.sessionId, event.toolCallId),
1325
+ isError: event.isError === true,
1326
+ observedAt,
1327
+ }),
1328
+ });
1329
+ }),
1330
+ ),
1331
+ );
1332
+ }
1333
+
884
1334
  export async function persistCoordinatorRuntimeStateFromEvent(
885
1335
  event: RuntimeStateEvent,
886
1336
  context: RuntimeStateContext,
1337
+ /**
1338
+ * What the caller observed at the synchronous agent-event boundary: the canonical
1339
+ * public label it proved against the ACTIVE tool object, and the wall clock it read
1340
+ * there. Without one, a tool event records `custom` at this writer's own clock — the
1341
+ * sidecar never trusts a model-supplied name.
1342
+ */
1343
+ observation?: CoordinatorToolObservation,
887
1344
  ): Promise<void> {
1345
+ // The caller's observation time wins so that queueing, subscriber latency, and lock
1346
+ // contention cannot compress a real elapsed interval into near zero. A value this
1347
+ // writer could not have produced is not trusted enough to persist.
1348
+ const observedAt =
1349
+ observation && isCanonicalIsoTimestamp(observation.observedAt)
1350
+ ? observation.observedAt
1351
+ : new Date().toISOString();
888
1352
  __sessionStateSidecarPerfCounters.persistFromEventCalls += 1;
889
1353
  const stateFile = runtimeStateFileForContext(context);
890
- const eventState = stateForEvent(event);
891
- const activityEvent = toolActivityForEvent(event);
892
- if (!stateFile || (!eventState && !activityEvent)) return;
1354
+ const state = stateForEvent(event);
1355
+ if (!stateFile) return;
1356
+ if (!state) {
1357
+ const activityPhase = toolActivityPhaseForEvent(event);
1358
+ if (!activityPhase) return;
1359
+ await persistCoordinatorRuntimeToolActivity(
1360
+ event,
1361
+ context,
1362
+ stateFile,
1363
+ activityPhase,
1364
+ observation?.label,
1365
+ observedAt,
1366
+ );
1367
+ return;
1368
+ }
893
1369
  context = contextWithManagedOwnerGeneration(context);
894
1370
  const identity = normalizedIdentity(context);
895
1371
  await serializeStateFileWrite(
@@ -903,7 +1379,14 @@ export async function persistCoordinatorRuntimeStateFromEvent(
903
1379
  const now = new Date(nowMs).toISOString();
904
1380
  const previous = await readPreviousPayloadForEvent(stateFile);
905
1381
  assertPreviousRuntimeStateIdentity(previous, identity);
906
- const state = eventState ?? runtimeStateFromPrevious(previous.state);
1382
+ const finalResponse = finalResponseForEvent(event);
1383
+ const terminalReceipt =
1384
+ state === "completed" || state === "errored"
1385
+ ? reduceTerminalReceiptState({
1386
+ execution: state === "errored" ? "failed" : "completed",
1387
+ reportable: Boolean(finalResponse?.text?.trim()),
1388
+ })
1389
+ : null;
907
1390
  const payload = {
908
1391
  ...basePayload({
909
1392
  context,
@@ -915,12 +1398,31 @@ export async function persistCoordinatorRuntimeStateFromEvent(
915
1398
  reason: null,
916
1399
  sessionId: identity.sessionId,
917
1400
  }),
918
- ...(state === "completed" || state === "errored" ? { ended_at: now } : {}),
919
- ...(finalResponseForEvent(event) ? { final_response: finalResponseForEvent(event) } : {}),
920
- ...(state === "errored"
921
- ? { error: { code: "agent_error", message: "GJC agent reported an error", recoverable: true } }
1401
+ ...(terminalReceipt
1402
+ ? {
1403
+ execution_state: terminalReceipt.execution,
1404
+ receipt_state: terminalReceipt.receipt,
1405
+ ended_at: now,
1406
+ }
922
1407
  : {}),
923
- ...(activityEvent ? activityFieldsForEvent(previous, activityEvent, now, nowMs) : {}),
1408
+ ...(finalResponse ? { final_response: finalResponse } : {}),
1409
+ ...(terminalReceipt?.receipt === "missing"
1410
+ ? {
1411
+ error: {
1412
+ code: "receipt_missing",
1413
+ message: "Agent completed without reportable final response text or artifact path.",
1414
+ recoverable: true,
1415
+ },
1416
+ }
1417
+ : state === "errored"
1418
+ ? {
1419
+ error: {
1420
+ code: "agent_error",
1421
+ message: "GJC agent reported an error",
1422
+ recoverable: true,
1423
+ },
1424
+ }
1425
+ : {}),
924
1426
  };
925
1427
  if (shouldSkipRuntimeStateWrite(previous, payload, nowMs)) return;
926
1428
  await writeStateFile(stateFile, payload);