@gajae-code/coding-agent 0.11.1 → 0.11.2

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 (404) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/types/cli/config-cli.d.ts +11 -1
  3. package/dist/types/cli/notify-cli.d.ts +1 -0
  4. package/dist/types/cli/session-picker.d.ts +2 -1
  5. package/dist/types/cli/setup-cli.d.ts +1 -1
  6. package/dist/types/commands/mcp-serve.d.ts +33 -0
  7. package/dist/types/commands/mcp.d.ts +1 -1
  8. package/dist/types/commands/notify.d.ts +0 -2
  9. package/dist/types/commands/sdk.d.ts +1 -1
  10. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  11. package/dist/types/config/file-lock.d.ts +14 -4
  12. package/dist/types/config/keybindings.d.ts +42 -1
  13. package/dist/types/config/model-auth.d.ts +3 -0
  14. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  15. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  16. package/dist/types/config/model-equivalence.d.ts +20 -0
  17. package/dist/types/config/model-registry.d.ts +16 -15
  18. package/dist/types/config/model-resolver.d.ts +28 -18
  19. package/dist/types/config/models-config-schema.d.ts +11 -0
  20. package/dist/types/config/settings-schema.d.ts +56 -1
  21. package/dist/types/config/settings.d.ts +14 -11
  22. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  23. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  24. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  25. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  26. package/dist/types/discovery/helpers.d.ts +0 -6
  27. package/dist/types/edit/index.d.ts +3 -0
  28. package/dist/types/eval/py/env.d.ts +9 -0
  29. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  30. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  31. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  32. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  33. package/dist/types/extensibility/shared-events.d.ts +2 -0
  34. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  35. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  36. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  37. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  38. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  39. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  40. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  41. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  42. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  43. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  44. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  45. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  46. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  47. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  48. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  49. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  50. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  51. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  52. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  53. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  54. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  55. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  56. package/dist/types/goals/runtime.d.ts +1 -0
  57. package/dist/types/goals/state.d.ts +8 -0
  58. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  59. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  60. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  61. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  62. package/dist/types/hindsight/state.d.ts +11 -4
  63. package/dist/types/lsp/lspmux.d.ts +5 -3
  64. package/dist/types/lsp/path-trust.d.ts +1 -0
  65. package/dist/types/main.d.ts +3 -2
  66. package/dist/types/memories/storage.d.ts +1 -0
  67. package/dist/types/modes/action-registry.d.ts +29 -0
  68. package/dist/types/modes/components/chrome.d.ts +14 -0
  69. package/dist/types/modes/components/command-palette.d.ts +27 -0
  70. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  71. package/dist/types/modes/components/index.d.ts +2 -1
  72. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  73. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  74. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  75. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  76. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  77. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  78. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  79. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  80. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  81. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  82. package/dist/types/modes/components/status-line.d.ts +1 -94
  83. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  84. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  85. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  86. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  87. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  88. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  89. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  90. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  91. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  92. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  93. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  94. package/dist/types/modes/interactive-mode.d.ts +19 -17
  95. package/dist/types/modes/jobs-observer.d.ts +2 -0
  96. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  97. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
  98. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  99. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  100. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  101. package/dist/types/modes/types.d.ts +20 -13
  102. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  103. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  104. package/dist/types/sdk/broker/broker.d.ts +78 -12
  105. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  106. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  107. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  108. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  109. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  110. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  111. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  112. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  113. package/dist/types/sdk/bus/config.d.ts +11 -0
  114. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  115. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  116. package/dist/types/sdk/bus/index.d.ts +27 -0
  117. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  118. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  119. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  120. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  121. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  122. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  123. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  124. package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
  125. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  126. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  127. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  128. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  129. package/dist/types/sdk/host/host.d.ts +2 -0
  130. package/dist/types/sdk/index.d.ts +1 -0
  131. package/dist/types/sdk/session-directory.d.ts +56 -0
  132. package/dist/types/session/agent-session.d.ts +82 -47
  133. package/dist/types/session/agent-storage.d.ts +7 -0
  134. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  135. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  136. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  137. package/dist/types/session/messages.d.ts +16 -0
  138. package/dist/types/session/session-manager-internal.d.ts +14 -0
  139. package/dist/types/session/session-manager.d.ts +82 -12
  140. package/dist/types/session/session-storage.d.ts +43 -3
  141. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  142. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  143. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  144. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  145. package/dist/types/system-prompt.d.ts +2 -11
  146. package/dist/types/task/executor.d.ts +3 -1
  147. package/dist/types/thinking.d.ts +7 -0
  148. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  149. package/dist/types/tools/ask.d.ts +64 -2
  150. package/dist/types/tools/bash.d.ts +3 -0
  151. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  152. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  153. package/dist/types/tools/cron.d.ts +2 -0
  154. package/dist/types/tools/fetch.d.ts +1 -0
  155. package/dist/types/tools/index.d.ts +30 -15
  156. package/dist/types/tools/output-meta.d.ts +1 -0
  157. package/dist/types/tools/read.d.ts +3 -0
  158. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  159. package/dist/types/utils/event-bus.d.ts +1 -1
  160. package/dist/types/web/search/index.d.ts +3 -1
  161. package/package.json +21 -9
  162. package/scripts/build-sdk-package-smoke.ts +34 -1
  163. package/scripts/generate-hotkeys-docs.ts +74 -0
  164. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  165. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  166. package/src/async/job-manager.ts +49 -6
  167. package/src/cli/config-cli.ts +75 -2
  168. package/src/cli/notify-cli.ts +92 -26
  169. package/src/cli/session-picker.ts +20 -3
  170. package/src/cli/setup-cli.ts +1 -1
  171. package/src/cli.ts +1 -1
  172. package/src/commands/config.ts +1 -1
  173. package/src/commands/harness.ts +34 -106
  174. package/src/commands/launch.ts +6 -4
  175. package/src/commands/mcp-serve.ts +85 -8
  176. package/src/commands/mcp.ts +2 -2
  177. package/src/commands/notify.ts +8 -4
  178. package/src/commands/sdk.ts +61 -40
  179. package/src/commands/ultragoal.ts +4 -1
  180. package/src/config/atomic-yaml-patch.ts +80 -13
  181. package/src/config/file-lock.ts +105 -43
  182. package/src/config/keybindings.ts +64 -20
  183. package/src/config/model-auth.ts +9 -0
  184. package/src/config/model-bindings-applier.ts +139 -0
  185. package/src/config/model-discovery-manager.ts +224 -0
  186. package/src/config/model-equivalence.ts +66 -0
  187. package/src/config/model-registry.ts +169 -302
  188. package/src/config/model-resolver.ts +361 -369
  189. package/src/config/models-config-schema.ts +5 -4
  190. package/src/config/settings-schema.ts +159 -2
  191. package/src/config/settings.ts +458 -482
  192. package/src/coordinator-mcp/policy.ts +76 -11
  193. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  194. package/src/coordinator-mcp/question-state.ts +778 -0
  195. package/src/coordinator-mcp/server.ts +1702 -238
  196. package/src/debug/index.ts +1 -1
  197. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  198. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  199. package/src/discovery/helpers.ts +1 -50
  200. package/src/edit/index.ts +64 -0
  201. package/src/eval/py/env.ts +26 -0
  202. package/src/eval/py/kernel.ts +5 -3
  203. package/src/extensibility/extensions/loader.ts +9 -1
  204. package/src/extensibility/extensions/runner.ts +12 -3
  205. package/src/extensibility/extensions/types.ts +39 -1
  206. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  207. package/src/extensibility/hooks/runner.ts +2 -2
  208. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  209. package/src/extensibility/shared-events.ts +2 -0
  210. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  211. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  212. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  213. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  214. package/src/gjc-runtime/launch-tmux.ts +2 -5
  215. package/src/gjc-runtime/launch-worktree.ts +5 -5
  216. package/src/gjc-runtime/linux-proc.ts +71 -0
  217. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  218. package/src/gjc-runtime/session-resolution.ts +18 -3
  219. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  220. package/src/gjc-runtime/state-migrations.ts +1 -1
  221. package/src/gjc-runtime/state-runtime.ts +320 -249
  222. package/src/gjc-runtime/state-writer.ts +36 -40
  223. package/src/gjc-runtime/team-launch.ts +319 -0
  224. package/src/gjc-runtime/team-notify.ts +326 -0
  225. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  226. package/src/gjc-runtime/team-store.ts +828 -0
  227. package/src/gjc-runtime/team-workers.ts +962 -0
  228. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  229. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  230. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  231. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  232. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  233. package/src/gjc-runtime/ultragoal-guard.ts +29 -5
  234. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  235. package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
  236. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  237. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  238. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  239. package/src/goals/continuation-timeout-guard.ts +79 -0
  240. package/src/goals/runtime.ts +7 -5
  241. package/src/goals/state.ts +18 -0
  242. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  243. package/src/harness-control-plane/phase-rollup.ts +4 -7
  244. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  245. package/src/harness-control-plane/receipt-spool.ts +51 -7
  246. package/src/harness-control-plane/session-lease.ts +12 -130
  247. package/src/hindsight/backend.ts +26 -27
  248. package/src/hindsight/state.ts +101 -32
  249. package/src/hooks/skill-state.ts +17 -11
  250. package/src/internal-urls/docs-index.generated.ts +21 -18
  251. package/src/lsp/client.ts +1 -1
  252. package/src/lsp/config.ts +145 -26
  253. package/src/lsp/index.ts +1 -1
  254. package/src/lsp/lspmux.ts +39 -10
  255. package/src/lsp/path-trust.ts +81 -0
  256. package/src/main.ts +28 -8
  257. package/src/memories/index.ts +1 -0
  258. package/src/memories/storage.ts +6 -2
  259. package/src/modes/action-registry.ts +165 -0
  260. package/src/modes/components/chrome.ts +40 -0
  261. package/src/modes/components/command-palette.ts +168 -0
  262. package/src/modes/components/custom-editor.ts +1 -1
  263. package/src/modes/components/custom-provider-wizard.ts +51 -14
  264. package/src/modes/components/index.ts +2 -1
  265. package/src/modes/components/pet-selector.ts +11 -17
  266. package/src/modes/components/plan-preview-overlay.ts +295 -0
  267. package/src/modes/components/queue-mode-selector.ts +10 -27
  268. package/src/modes/components/queue-pane.ts +79 -0
  269. package/src/modes/components/read-tool-group.ts +13 -0
  270. package/src/modes/components/session-observer-overlay.ts +207 -772
  271. package/src/modes/components/sessions-dashboard.ts +181 -0
  272. package/src/modes/components/settings-selector.ts +1 -1
  273. package/src/modes/components/show-images-selector.ts +10 -24
  274. package/src/modes/components/status-line/types.ts +1 -1
  275. package/src/modes/components/status-line.ts +1 -969
  276. package/src/modes/components/tasks-pane.ts +73 -0
  277. package/src/modes/components/theme-selector.ts +11 -31
  278. package/src/modes/components/tool-execution.ts +22 -0
  279. package/src/modes/components/tool-status-header.ts +1056 -0
  280. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  281. package/src/modes/controllers/btw-controller.ts +1 -0
  282. package/src/modes/controllers/event-controller.ts +3 -2
  283. package/src/modes/controllers/extension-ui-controller.ts +24 -1
  284. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  285. package/src/modes/controllers/input-controller.ts +764 -229
  286. package/src/modes/controllers/mode-gate.ts +24 -0
  287. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  288. package/src/modes/controllers/selector-controller.ts +225 -10
  289. package/src/modes/controllers/stt-controller.ts +125 -0
  290. package/src/modes/interactive-mode.ts +289 -1150
  291. package/src/modes/jobs-observer.ts +3 -0
  292. package/src/modes/runtime-init.ts +8 -0
  293. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  294. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
  295. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  296. package/src/modes/tasks-aggregator.ts +199 -0
  297. package/src/modes/transcript-item-registry.ts +229 -0
  298. package/src/modes/types.ts +45 -18
  299. package/src/modes/utils/ui-helpers.ts +17 -9
  300. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  301. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  302. package/src/prompts/agents/architect.md +2 -8
  303. package/src/prompts/agents/critic.md +2 -8
  304. package/src/prompts/agents/executor.md +3 -0
  305. package/src/prompts/agents/planner.md +4 -12
  306. package/src/prompts/system/custom-system-prompt.md +1 -23
  307. package/src/prompts/system/plan-mode-approved.md +8 -0
  308. package/src/prompts/system/system-prompt.md +17 -96
  309. package/src/prompts/tools/bash.md +4 -4
  310. package/src/prompts/tools/browser.md +15 -52
  311. package/src/prompts/tools/find.md +2 -2
  312. package/src/prompts/tools/hashline.md +13 -55
  313. package/src/prompts/tools/read.md +3 -6
  314. package/src/prompts/tools/search-tool-bm25.md +0 -2
  315. package/src/prompts/tools/search.md +2 -4
  316. package/src/prompts/tools/skill-discovery.md +1 -2
  317. package/src/prompts/tools/task.md +1 -1
  318. package/src/runtime-mcp/manager.ts +1 -1
  319. package/src/sdk/broker/broker.ts +206 -78
  320. package/src/sdk/broker/discovery.ts +1 -1
  321. package/src/sdk/broker/ensure.ts +3 -1
  322. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  323. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  324. package/src/sdk/broker/lifecycle.ts +1399 -142
  325. package/src/sdk/broker/process-incarnation.ts +5 -13
  326. package/src/sdk/broker/session-index.ts +145 -56
  327. package/src/sdk/broker/state-version.ts +21 -0
  328. package/src/sdk/broker/transport.ts +1 -1
  329. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  330. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  331. package/src/sdk/bus/chat-effect-journal.ts +10 -2
  332. package/src/sdk/bus/config-commands.ts +5 -4
  333. package/src/sdk/bus/config.ts +98 -0
  334. package/src/sdk/bus/conversation-store.ts +5 -29
  335. package/src/sdk/bus/daemon-paths.ts +2 -0
  336. package/src/sdk/bus/html-format.ts +20 -3
  337. package/src/sdk/bus/index.ts +778 -93
  338. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  339. package/src/sdk/bus/natives-augment.d.ts +26 -1
  340. package/src/sdk/bus/notification-service.ts +168 -20
  341. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  342. package/src/sdk/bus/recent-activity.ts +188 -74
  343. package/src/sdk/bus/rich-document.ts +258 -0
  344. package/src/sdk/bus/rich-render.ts +62 -70
  345. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  346. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  347. package/src/sdk/bus/telegram-daemon.ts +1389 -237
  348. package/src/sdk/bus/threaded-inbound.ts +8 -3
  349. package/src/sdk/bus/threaded-render.ts +57 -1
  350. package/src/sdk/bus/topic-registry.ts +120 -13
  351. package/src/sdk/host/control/dispatch.ts +14 -2
  352. package/src/sdk/host/control/operations.ts +6 -1
  353. package/src/sdk/host/host.ts +31 -1
  354. package/src/sdk/host/query/handlers.ts +20 -22
  355. package/src/sdk/host/query/revision-store.ts +80 -57
  356. package/src/sdk/index.ts +1 -0
  357. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  358. package/src/sdk/session-directory.ts +141 -0
  359. package/src/sdk/session.ts +458 -479
  360. package/src/session/agent-session.ts +1700 -583
  361. package/src/session/agent-storage.ts +18 -0
  362. package/src/session/cache-economics.ts +25 -8
  363. package/src/session/fallback-chain-controller.ts +53 -3
  364. package/src/session/internal/managed-session-scope.ts +2226 -0
  365. package/src/session/internal/managed-session-storage.ts +453 -0
  366. package/src/session/messages.ts +91 -2
  367. package/src/session/session-manager-internal.ts +33 -0
  368. package/src/session/session-manager.ts +1112 -333
  369. package/src/session/session-storage.ts +444 -39
  370. package/src/session/volatile-context-pruning.ts +78 -0
  371. package/src/setup/credential-auto-import.ts +2 -1
  372. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  373. package/src/setup/provider-onboarding.ts +36 -61
  374. package/src/setup/provider-presets.json +5 -0
  375. package/src/skill-state/active-state.ts +3 -13
  376. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  377. package/src/skill-state/workflow-state-contract.ts +6 -41
  378. package/src/slash-commands/builtin-registry.ts +46 -11
  379. package/src/system-prompt.ts +37 -44
  380. package/src/task/agents.ts +19 -1
  381. package/src/task/executor.ts +90 -6
  382. package/src/task/index.ts +36 -21
  383. package/src/thinking.ts +21 -6
  384. package/src/tool-discovery/tool-index.ts +2 -134
  385. package/src/tools/ask.ts +157 -21
  386. package/src/tools/ast-edit.ts +1 -1
  387. package/src/tools/bash.ts +38 -0
  388. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  389. package/src/tools/browser/tab-supervisor.ts +282 -36
  390. package/src/tools/computer.ts +10 -3
  391. package/src/tools/cron.ts +30 -16
  392. package/src/tools/eval.ts +1 -2
  393. package/src/tools/fetch.ts +11 -4
  394. package/src/tools/index.ts +76 -24
  395. package/src/tools/output-meta.ts +13 -5
  396. package/src/tools/read.ts +37 -1
  397. package/src/tools/resource-gc.ts +2 -2
  398. package/src/tools/search-tool-bm25.ts +11 -74
  399. package/src/utils/event-bus.ts +9 -4
  400. package/src/web/search/index.ts +3 -3
  401. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  402. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  403. package/src/discovery/plugin-dir-roots.ts +0 -28
  404. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -1,7 +1,9 @@
1
1
  import { createHash } from "node:crypto";
2
+ import * as fsSync from "node:fs";
2
3
  import * as fs from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import type { SdkStartupFailure, SdkStartupRollbackResult } from "../startup-capability";
6
+ import { parseLifecycleJson } from "./lifecycle-codec";
5
7
  import { assertSupportedStateVersion, SDK_STATE_VERSION } from "./state-version";
6
8
 
7
9
  export type LifecycleState =
@@ -87,6 +89,55 @@ export type BeginResult =
87
89
  | { kind: "terminal_uncertain"; entry: LifecycleLedgerEntry }
88
90
  | { kind: "in_progress"; entry: LifecycleLedgerEntry };
89
91
  const terminal = (s: LifecycleState) => s === "terminal_ok" || s === "terminal_error";
92
+ const final = (s: LifecycleState) => terminal(s) || s === "terminal_uncertain";
93
+ export interface LifecycleLedgerLimits {
94
+ maxBytes?: number;
95
+ maxLineBytes?: number;
96
+ maxRows?: number;
97
+ }
98
+ const DEFAULT_LIFECYCLE_LEDGER_LIMITS: Required<LifecycleLedgerLimits> = {
99
+ maxBytes: 64 * 1024 * 1024,
100
+ maxLineBytes: 8 * 1024 * 1024,
101
+ maxRows: 10_000,
102
+ };
103
+ const MAX_LIFECYCLE_LEDGER_JSON_DEPTH = 64;
104
+ const MAX_LIFECYCLE_LEDGER_JSON_FIELDS = 1024;
105
+
106
+ function isBoundedLedgerJson(value: unknown, depth = 0, budget = { fields: 0 }): boolean {
107
+ if (depth > MAX_LIFECYCLE_LEDGER_JSON_DEPTH) return false;
108
+ if (value === null || typeof value !== "object") return true;
109
+ if (Array.isArray(value)) {
110
+ if (value.length > MAX_LIFECYCLE_LEDGER_JSON_FIELDS) return false;
111
+ return value.every(item => isBoundedLedgerJson(item, depth + 1, budget));
112
+ }
113
+ const record = value as Record<string, unknown>;
114
+ const keys = Object.keys(record);
115
+ budget.fields += keys.length;
116
+ return (
117
+ budget.fields <= MAX_LIFECYCLE_LEDGER_JSON_FIELDS &&
118
+ keys.every(key => isBoundedLedgerJson(record[key], depth + 1, budget))
119
+ );
120
+ }
121
+
122
+ function isLifecycleLedgerEntry(value: unknown): value is LifecycleLedgerEntry {
123
+ if (typeof value !== "object" || value === null || Array.isArray(value) || !isBoundedLedgerJson(value)) return false;
124
+ const entry = value as Partial<LifecycleLedgerEntry>;
125
+ return (
126
+ entry.version === SDK_STATE_VERSION &&
127
+ typeof entry.identity === "string" &&
128
+ entry.identity.length > 0 &&
129
+ typeof entry.requestHash === "string" &&
130
+ entry.requestHash.length > 0 &&
131
+ (entry.state === "accepted" ||
132
+ entry.state === "effect_started" ||
133
+ entry.state === "awaiting_ready" ||
134
+ entry.state === "terminal_ok" ||
135
+ entry.state === "terminal_error" ||
136
+ entry.state === "terminal_uncertain") &&
137
+ typeof entry.ts === "number" &&
138
+ Number.isSafeInteger(entry.ts)
139
+ );
140
+ }
90
141
  function canonicalJson(value: unknown): string {
91
142
  if (value === null || typeof value !== "object") return JSON.stringify(value);
92
143
  if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
@@ -115,111 +166,356 @@ export class LifecycleLedger {
115
166
  #corruptFile: string;
116
167
  #entries: LifecycleLedgerEntry[] = [];
117
168
  #byIdentity = new Map<string, LifecycleLedgerEntry>();
169
+ #limits: Required<LifecycleLedgerLimits>;
170
+ #rowCount = 0;
171
+ #byteCount = 0;
118
172
  #warnings: string[] = [];
119
- constructor(agentDir: string) {
173
+ #mutationTail: Promise<void> = Promise.resolve();
174
+
175
+ async #mutate<T>(operation: () => Promise<T>): Promise<T> {
176
+ const previous = this.#mutationTail;
177
+ const completion = Promise.withResolvers<void>();
178
+ this.#mutationTail = previous.then(() => completion.promise);
179
+ await previous;
180
+ try {
181
+ return await operation();
182
+ } finally {
183
+ completion.resolve();
184
+ }
185
+ }
186
+
187
+ constructor(agentDir: string, limits: LifecycleLedgerLimits = {}) {
120
188
  this.#file = path.join(agentDir, "sdk", "lifecycle-ledger.jsonl");
121
189
  this.#corruptFile = `${this.#file}.corrupt`;
190
+ this.#limits = {
191
+ maxBytes: limits.maxBytes ?? DEFAULT_LIFECYCLE_LEDGER_LIMITS.maxBytes,
192
+ maxLineBytes: limits.maxLineBytes ?? DEFAULT_LIFECYCLE_LEDGER_LIMITS.maxLineBytes,
193
+ maxRows: limits.maxRows ?? DEFAULT_LIFECYCLE_LEDGER_LIMITS.maxRows,
194
+ };
122
195
  }
123
196
  async open(): Promise<this> {
124
- await this.assertSupportedStateVersions();
197
+ return this.#mutate(async () => this.#open());
198
+ }
199
+
200
+ async #open(): Promise<this> {
125
201
  await fs.mkdir(path.dirname(this.#file), { recursive: true, mode: 0o700 });
126
202
  this.#entries = [];
127
- const quarantinedTerminal = new Set<string>();
128
203
  this.#byIdentity.clear();
129
204
  this.#warnings = [];
205
+ this.#rowCount = 0;
206
+ this.#byteCount = 0;
207
+ const invalidIdentities = new Set<string>();
208
+ const syntheticUncertain = new Map<string, LifecycleLedgerEntry>();
130
209
  const uncertainAfterCorruption = new Set<string>();
210
+ const source = await this.#readBoundedSource();
131
211
  let tornTail = false;
132
- try {
133
- const source = await fs.readFile(this.#file, "utf8");
134
- tornTail = source.length > 0 && !source.endsWith("\n");
135
- const lines = source.split("\n");
136
- for (const line of lines) {
137
- if (!line) continue;
212
+ if (source) {
213
+ this.#byteCount = source.length;
214
+ tornTail = source.length > 0 && source.at(-1) !== 0x0a;
215
+ let lineStart = 0;
216
+ for (let offset = 0; offset <= source.length; offset += 1) {
217
+ if (offset !== source.length && source[offset] !== 0x0a) continue;
218
+ const line = source.subarray(lineStart, offset);
219
+ lineStart = offset + 1;
220
+ if (line.length === 0) continue;
221
+ this.#rowCount += 1;
222
+ if (this.#rowCount > this.#limits.maxRows)
223
+ await this.#rejectOversizedSource("Lifecycle ledger exceeds the maximum row count.");
224
+ if (line.length > this.#limits.maxLineBytes)
225
+ await this.#rejectOversizedSource("Lifecycle ledger row exceeds the maximum byte length.");
138
226
  try {
139
- const e = JSON.parse(line) as LifecycleLedgerEntry;
140
- assertSupportedStateVersion(this.#file, e);
141
- if (!e.identity || !e.requestHash || !e.state) throw new Error("invalid ledger entry");
142
- if (!hasValidTerminalDigests(e)) {
227
+ const value = parseLifecycleJson(line);
228
+ assertSupportedStateVersion(this.#file, value);
229
+ if (!isLifecycleLedgerEntry(value)) throw new Error("invalid ledger entry");
230
+ const entry = value;
231
+ const prior = this.#byIdentity.get(entry.identity);
232
+ const invalidHistory =
233
+ invalidIdentities.has(entry.identity) ||
234
+ uncertainAfterCorruption.has(entry.identity) ||
235
+ !hasValidTerminalDigests(entry) ||
236
+ !this.#isValidHistoryContinuation(prior, entry);
237
+ if (invalidHistory) {
143
238
  await this.#quarantine(line);
144
- const {
145
- response: _response,
146
- responseDigest: _responseDigest,
147
- durableEffects: _durableEffects,
148
- ...uncertain
149
- } = e;
150
- const quarantined = { ...uncertain, state: "terminal_uncertain" as const, ts: Date.now() };
151
- this.#entries.push(quarantined);
152
- this.#byIdentity.set(quarantined.identity, quarantined);
153
- quarantinedTerminal.add(quarantined.identity);
239
+ invalidIdentities.add(entry.identity);
240
+ if (!syntheticUncertain.has(entry.identity))
241
+ syntheticUncertain.set(entry.identity, this.#uncertainFrom(prior ?? entry, prior !== undefined));
154
242
  continue;
155
243
  }
156
- this.#entries.push(e);
157
- this.#byIdentity.set(e.identity, e);
158
- uncertainAfterCorruption.delete(e.identity);
159
- } catch {
244
+ this.#entries.push(entry);
245
+ this.#byIdentity.set(entry.identity, entry);
246
+ } catch (error) {
247
+ if (error instanceof Error && "code" in error && error.code === "unsupported_state_version") throw error;
160
248
  for (const [identity, latest] of this.#byIdentity) {
161
- if (!terminal(latest.state) && latest.state !== "terminal_uncertain")
162
- uncertainAfterCorruption.add(identity);
249
+ if (!final(latest.state)) uncertainAfterCorruption.add(identity);
163
250
  }
164
251
  await this.#quarantine(line);
165
252
  }
166
253
  }
167
- } catch (e) {
168
- if ((e as NodeJS.ErrnoException).code !== "ENOENT") throw e;
169
254
  }
170
255
  if (tornTail) await this.#sealTornTail();
171
- for (const identity of quarantinedTerminal) {
172
- const entry = this.#byIdentity.get(identity);
173
- if (entry) await this.#append(entry);
256
+ for (const [identity, uncertain] of syntheticUncertain) {
257
+ this.#byIdentity.set(identity, uncertain);
258
+ if (this.#entries.some(entry => entry.identity === identity && entry.state === "accepted"))
259
+ await this.#append(uncertain);
174
260
  }
175
261
  for (const identity of uncertainAfterCorruption) {
176
262
  const entry = this.#byIdentity.get(identity);
177
- if (entry && !terminal(entry.state) && entry.state !== "terminal_uncertain") {
178
- await this.#append({ ...entry, state: "terminal_uncertain", ts: Date.now() });
179
- }
263
+ if (entry && !final(entry.state)) await this.#append(this.#uncertainFrom(entry));
180
264
  }
181
265
  // Effects may have completed after the last durable marker; do not retry them after a restart.
182
266
  for (const entry of [...this.#byIdentity.values()]) {
183
- if (entry.state === "effect_started" || entry.state === "awaiting_ready")
184
- await this.#append({ ...entry, state: "terminal_uncertain", ts: Date.now() });
267
+ if ((entry.state === "effect_started" && !this.#isCleanupPending(entry)) || entry.state === "awaiting_ready")
268
+ await this.#append(this.#uncertainFrom(entry));
185
269
  }
186
270
  return this;
187
271
  }
272
+ /**
273
+ * Reads terminal proof for one request without recovering or changing the ledger.
274
+ *
275
+ * This intentionally accepts an unrelated unterminated final write: concurrent
276
+ * appenders may have started a different row after this request's synced terminal
277
+ * row. Complete rows are still decoded and validated strictly, and any incomplete
278
+ * tail that identifies this request withholds proof.
279
+ */
280
+ async readTerminal(identity: string, requestHash: string): Promise<LifecycleLedgerEntry | undefined> {
281
+ const source = await this.#readBoundedSourceReadOnly();
282
+ if (!source) return undefined;
283
+ let prior: LifecycleLedgerEntry | undefined;
284
+ let latest: LifecycleLedgerEntry | undefined;
285
+ let rows = 0;
286
+ let lineStart = 0;
287
+ const finalNewline = source.length > 0 && source.at(-1) === 0x0a;
288
+ const completeLength = finalNewline ? source.length : source.lastIndexOf(0x0a) + 1;
289
+ for (let offset = 0; offset < completeLength; offset += 1) {
290
+ if (source[offset] !== 0x0a) continue;
291
+ const line = source.subarray(lineStart, offset);
292
+ lineStart = offset + 1;
293
+ if (line.length === 0) continue;
294
+ rows += 1;
295
+ if (rows > this.#limits.maxRows || line.length > this.#limits.maxLineBytes) return undefined;
296
+ let entry: LifecycleLedgerEntry;
297
+ try {
298
+ const value = parseLifecycleJson(line);
299
+ assertSupportedStateVersion(this.#file, value);
300
+ if (!isLifecycleLedgerEntry(value)) return undefined;
301
+ entry = value;
302
+ } catch {
303
+ return undefined;
304
+ }
305
+ if (entry.identity !== identity) continue;
306
+ if (
307
+ entry.requestHash !== requestHash ||
308
+ !hasValidTerminalDigests(entry) ||
309
+ !this.#isValidHistoryContinuation(prior, entry)
310
+ )
311
+ return undefined;
312
+ prior = entry;
313
+ latest = entry;
314
+ }
315
+ if (!finalNewline) {
316
+ const tail = source.subarray(completeLength);
317
+ // LifecycleLedger writes JSON.stringify entries, so this marker is exact for
318
+ // a partially persisted row from this identity without inspecting arbitrary
319
+ // malformed data as a valid record.
320
+ const identityMarker = Buffer.from(`"identity":${JSON.stringify(identity)}`);
321
+ if (tail.includes(identityMarker)) return undefined;
322
+ }
323
+ return latest && terminal(latest.state) ? latest : undefined;
324
+ }
325
+
326
+ #isValidHistoryContinuation(previous: LifecycleLedgerEntry | undefined, next: LifecycleLedgerEntry): boolean {
327
+ if (!previous) return next.state === "accepted";
328
+ if (previous.requestHash !== next.requestHash || final(previous.state)) return false;
329
+ if (previous.state === "accepted") return true;
330
+ return next.state === "effect_started" || next.state === "awaiting_ready" || final(next.state);
331
+ }
332
+ #isCleanupPending(entry: LifecycleLedgerEntry): boolean {
333
+ if (!entry.response || typeof entry.response !== "object") return false;
334
+ const response = entry.response as { ok?: unknown; error?: { code?: unknown; cleanup?: unknown } };
335
+ return (
336
+ response.ok === false && response.error?.code === "cleanup_pending" && response.error.cleanup !== undefined
337
+ );
338
+ }
339
+
340
+ #uncertainFrom(entry: LifecycleLedgerEntry, trusted = true): LifecycleLedgerEntry {
341
+ if (trusted) return { ...entry, state: "terminal_uncertain", ts: Date.now() };
342
+ return {
343
+ version: SDK_STATE_VERSION,
344
+ identity: entry.identity,
345
+ requestHash: entry.requestHash,
346
+ state: "terminal_uncertain",
347
+ ts: Date.now(),
348
+ };
349
+ }
350
+ async #readBoundedSource(): Promise<Buffer | undefined> {
351
+ let handle: fs.FileHandle | undefined;
352
+ try {
353
+ handle = await fs.open(this.#file, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
354
+ const stat = await handle.stat({ bigint: true });
355
+ if (!stat.isFile() || stat.size > BigInt(this.#limits.maxBytes))
356
+ await this.#rejectOversizedSource("Lifecycle ledger exceeds the maximum file byte length.");
357
+ const bytes = Buffer.alloc(Number(stat.size) + 1);
358
+ const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
359
+ if (bytesRead > this.#limits.maxBytes)
360
+ await this.#rejectOversizedSource("Lifecycle ledger exceeds the maximum file byte length.");
361
+ return bytes.subarray(0, bytesRead);
362
+ } catch (error) {
363
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
364
+ throw error;
365
+ } finally {
366
+ if (handle) await handle.close();
367
+ }
368
+ }
369
+ async #readBoundedSourceReadOnly(): Promise<Buffer | undefined> {
370
+ let handle: fs.FileHandle | undefined;
371
+ try {
372
+ handle = await fs.open(this.#file, fsSync.constants.O_RDONLY | fsSync.constants.O_NOFOLLOW);
373
+ const stat = await handle.stat({ bigint: true });
374
+ if (!stat.isFile() || stat.size > BigInt(this.#limits.maxBytes)) return undefined;
375
+ const bytes = Buffer.alloc(Number(stat.size) + 1);
376
+ const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
377
+ if (bytesRead > this.#limits.maxBytes) return undefined;
378
+ return bytes.subarray(0, bytesRead);
379
+ } catch (error) {
380
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
381
+ return undefined;
382
+ } finally {
383
+ if (handle) await handle.close();
384
+ }
385
+ }
386
+ async #rejectOversizedSource(reason: string): Promise<never> {
387
+ await this.#quarantine(reason);
388
+ throw new Error(reason);
389
+ }
390
+ async #openAppendRegular(file: string): Promise<fs.FileHandle> {
391
+ const handle = await fs.open(
392
+ file,
393
+ fsSync.constants.O_WRONLY | fsSync.constants.O_APPEND | fsSync.constants.O_CREAT | fsSync.constants.O_NOFOLLOW,
394
+ 0o600,
395
+ );
396
+ try {
397
+ if (!(await handle.stat()).isFile()) throw new Error("Lifecycle ledger write target is not a regular file.");
398
+ return handle;
399
+ } catch (error) {
400
+ await handle.close();
401
+ throw error;
402
+ }
403
+ }
188
404
  async #sealTornTail(): Promise<void> {
189
- const h = await fs.open(this.#file, "a", 0o600);
405
+ const h = await this.#openAppendRegular(this.#file);
190
406
  try {
191
407
  await h.writeFile("\n");
192
408
  await h.sync();
409
+ this.#byteCount += 1;
193
410
  } finally {
194
411
  await h.close();
195
412
  }
196
413
  }
197
- async #quarantine(line: string): Promise<void> {
198
- const h = await fs.open(this.#corruptFile, "a", 0o600);
414
+ async #quarantine(line: string | Uint8Array): Promise<void> {
415
+ const h = await this.#openAppendRegular(this.#corruptFile);
199
416
  try {
200
- await h.writeFile(`${line}\n`);
417
+ await h.writeFile(line);
418
+ await h.writeFile("\n");
201
419
  await h.sync();
202
420
  } finally {
203
421
  await h.close();
204
422
  }
205
423
  this.#warnings.push("Malformed lifecycle ledger entry quarantined");
206
424
  }
425
+ async #syncDirectory(): Promise<void> {
426
+ const directory = await fs.open(path.dirname(this.#file), fsSync.constants.O_RDONLY);
427
+ try {
428
+ await directory.sync();
429
+ } finally {
430
+ await directory.close();
431
+ }
432
+ }
433
+ async #compact(replacement?: LifecycleLedgerEntry): Promise<boolean> {
434
+ const anchors = new Map<string, LifecycleLedgerEntry>();
435
+ for (const entry of this.#entries) {
436
+ const latest = replacement?.identity === entry.identity ? replacement : this.#byIdentity.get(entry.identity);
437
+ if (entry.state === "accepted" && !anchors.has(entry.identity) && entry.requestHash === latest?.requestHash)
438
+ anchors.set(entry.identity, entry);
439
+ }
440
+ const snapshot: LifecycleLedgerEntry[] = [];
441
+ const compacted = new Map(this.#byIdentity);
442
+ if (replacement) compacted.set(replacement.identity, replacement);
443
+ for (const [identity, latest] of compacted) {
444
+ const anchor = anchors.get(identity);
445
+ if (!anchor) throw new Error("Lifecycle ledger compaction requires an accepted identity anchor.");
446
+ snapshot.push(anchor);
447
+ if (latest.state !== "accepted") snapshot.push(latest);
448
+ }
449
+ const contents = Buffer.from(snapshot.map(entry => `${JSON.stringify(entry)}\n`).join(""));
450
+ if (
451
+ snapshot.length > this.#limits.maxRows ||
452
+ contents.length > this.#limits.maxBytes ||
453
+ snapshot.some(entry => Buffer.byteLength(JSON.stringify(entry)) > this.#limits.maxLineBytes)
454
+ )
455
+ throw new Error("Lifecycle ledger compaction exceeds configured bounds.");
456
+ const temporary = path.join(
457
+ path.dirname(this.#file),
458
+ `.lifecycle-ledger.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`,
459
+ );
460
+ let renamed = false;
461
+ try {
462
+ const h = await fs.open(
463
+ temporary,
464
+ fsSync.constants.O_WRONLY |
465
+ fsSync.constants.O_CREAT |
466
+ fsSync.constants.O_EXCL |
467
+ fsSync.constants.O_NOFOLLOW,
468
+ 0o600,
469
+ );
470
+ try {
471
+ await h.writeFile(contents);
472
+ await h.sync();
473
+ } finally {
474
+ await h.close();
475
+ }
476
+ await fs.rename(temporary, this.#file);
477
+ renamed = true;
478
+ await this.#syncDirectory();
479
+ this.#entries = snapshot;
480
+ this.#byIdentity = compacted;
481
+ this.#rowCount = snapshot.length;
482
+ this.#byteCount = contents.length;
483
+ } finally {
484
+ if (!renamed) await fs.unlink(temporary).catch(() => {});
485
+ }
486
+ return replacement !== undefined;
487
+ }
207
488
  get warnings(): readonly string[] {
208
489
  return this.#warnings;
209
490
  }
210
491
  async #append(entry: LifecycleLedgerEntry): Promise<LifecycleLedgerEntry> {
211
- const h = await fs.open(this.#file, "a", 0o600);
492
+ const line = Buffer.from(`${JSON.stringify(entry)}\n`);
493
+ if (line.length - 1 > this.#limits.maxLineBytes)
494
+ throw new Error("Lifecycle ledger row exceeds the maximum byte length.");
495
+ let replacementCompacted = false;
496
+ if (this.#rowCount + 1 > this.#limits.maxRows || this.#byteCount + line.length > this.#limits.maxBytes)
497
+ replacementCompacted = await this.#compact(this.#byIdentity.has(entry.identity) ? entry : undefined);
498
+ if (replacementCompacted) return entry;
499
+ if (this.#rowCount + 1 > this.#limits.maxRows || this.#byteCount + line.length > this.#limits.maxBytes)
500
+ throw new Error("Lifecycle ledger append exceeds configured bounds.");
501
+ const h = await this.#openAppendRegular(this.#file);
212
502
  try {
213
- await h.writeFile(`${JSON.stringify(entry)}\n`);
503
+ await h.writeFile(line);
214
504
  await h.sync();
215
505
  } finally {
216
506
  await h.close();
217
507
  }
218
508
  this.#entries.push(entry);
219
509
  this.#byIdentity.set(entry.identity, entry);
510
+ this.#rowCount += 1;
511
+ this.#byteCount += line.length;
220
512
  return entry;
221
513
  }
222
514
  async begin(identity: string, requestHash: string): Promise<BeginResult> {
515
+ return this.#mutate(async () => this.#begin(identity, requestHash));
516
+ }
517
+
518
+ async #begin(identity: string, requestHash: string): Promise<BeginResult> {
223
519
  const prior = this.#byIdentity.get(identity);
224
520
  if (!prior)
225
521
  return {
@@ -233,7 +529,8 @@ export class LifecycleLedger {
233
529
  }),
234
530
  };
235
531
  if (prior.requestHash !== requestHash) return { kind: "idempotency_conflict" };
236
- if (terminal(prior.state)) return { kind: "replay", entry: prior };
532
+ if (terminal(prior.state) || (prior.state === "effect_started" && this.#isCleanupPending(prior)))
533
+ return { kind: "replay", entry: prior };
237
534
  if (prior.state === "terminal_uncertain") return { kind: "terminal_uncertain", entry: prior };
238
535
  // An accepted row has no durable side effect. Target serialization makes retrying it safe.
239
536
  if (prior.state === "accepted") return { kind: "new", entry: prior };
@@ -244,42 +541,42 @@ export class LifecycleLedger {
244
541
  state: LifecycleState,
245
542
  fields: Omit<Partial<LifecycleLedgerEntry>, "identity" | "requestHash" | "state" | "ts"> = {},
246
543
  ): Promise<LifecycleLedgerEntry> {
247
- const previous = this.#byIdentity.get(identity);
248
- if (!previous) throw new Error("Unknown lifecycle identity");
249
- const next = { ...previous, ...fields, state, ts: Date.now() };
250
- if (
251
- (terminal(state) || state === "terminal_uncertain") &&
252
- next.response !== undefined &&
253
- next.responseDigest === undefined
254
- )
255
- next.responseDigest = createHash("sha256").update(canonicalJson(next.response)).digest("hex");
256
- if (next.durableEffects && next.durableEffects.digest === undefined) {
257
- const { digest: _digest, ...body } = next.durableEffects;
258
- next.durableEffects = {
259
- ...body,
260
- digest: createHash("sha256").update(canonicalJson(body)).digest("hex"),
261
- };
262
- }
263
- return this.#append(next);
544
+ return this.#mutate(async () => {
545
+ const previous = this.#byIdentity.get(identity);
546
+ if (!previous) throw new Error("Unknown lifecycle identity");
547
+ const next = { ...previous, ...fields, state, ts: Date.now() };
548
+ if (
549
+ (terminal(state) || state === "terminal_uncertain") &&
550
+ next.response !== undefined &&
551
+ next.responseDigest === undefined
552
+ )
553
+ next.responseDigest = createHash("sha256").update(canonicalJson(next.response)).digest("hex");
554
+ if (next.durableEffects && next.durableEffects.digest === undefined) {
555
+ const { digest: _digest, ...body } = next.durableEffects;
556
+ next.durableEffects = {
557
+ ...body,
558
+ digest: createHash("sha256").update(canonicalJson(body)).digest("hex"),
559
+ };
560
+ }
561
+ return this.#append(next);
562
+ });
264
563
  }
265
564
  async assertSupportedStateVersions(): Promise<void> {
266
- let source: string;
267
- try {
268
- source = await fs.readFile(this.#file, "utf8");
269
- } catch (error) {
270
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return;
271
- throw error;
272
- }
273
- for (const line of source.split("\n")) {
274
- if (!line) continue;
565
+ const source = await this.#readBoundedSource();
566
+ if (!source) return;
567
+ let lineStart = 0;
568
+ for (let offset = 0; offset <= source.length; offset += 1) {
569
+ if (offset !== source.length && source[offset] !== 0x0a) continue;
570
+ const line = source.subarray(lineStart, offset);
571
+ lineStart = offset + 1;
572
+ if (line.length === 0 || line.length > this.#limits.maxLineBytes) continue;
275
573
  try {
276
- assertSupportedStateVersion(this.#file, JSON.parse(line));
574
+ assertSupportedStateVersion(this.#file, parseLifecycleJson(line));
277
575
  } catch (error) {
278
576
  if (error instanceof Error && "code" in error && error.code === "unsupported_state_version") throw error;
279
577
  }
280
578
  }
281
579
  }
282
-
283
580
  get(identity: string): LifecycleLedgerEntry | undefined {
284
581
  return this.#byIdentity.get(identity);
285
582
  }