@gajae-code/coding-agent 0.11.1 → 0.11.3

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 (413) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/types/cli/config-cli.d.ts +11 -1
  3. package/dist/types/cli/launch-disposition.d.ts +23 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -0
  5. package/dist/types/cli/session-picker.d.ts +2 -1
  6. package/dist/types/cli/setup-cli.d.ts +1 -1
  7. package/dist/types/commands/mcp-serve.d.ts +33 -0
  8. package/dist/types/commands/mcp.d.ts +1 -1
  9. package/dist/types/commands/notify.d.ts +0 -2
  10. package/dist/types/commands/sdk.d.ts +1 -1
  11. package/dist/types/config/atomic-yaml-patch.d.ts +18 -0
  12. package/dist/types/config/file-lock.d.ts +14 -4
  13. package/dist/types/config/keybindings.d.ts +42 -1
  14. package/dist/types/config/model-auth.d.ts +3 -0
  15. package/dist/types/config/model-bindings-applier.d.ts +13 -0
  16. package/dist/types/config/model-discovery-manager.d.ts +49 -0
  17. package/dist/types/config/model-equivalence.d.ts +20 -0
  18. package/dist/types/config/model-registry.d.ts +16 -15
  19. package/dist/types/config/model-resolver.d.ts +28 -18
  20. package/dist/types/config/models-config-schema.d.ts +11 -0
  21. package/dist/types/config/settings-schema.d.ts +141 -11
  22. package/dist/types/config/settings.d.ts +14 -11
  23. package/dist/types/coordinator-mcp/policy.d.ts +5 -0
  24. package/dist/types/coordinator-mcp/question-gate-codec.d.ts +129 -0
  25. package/dist/types/coordinator-mcp/question-state.d.ts +356 -0
  26. package/dist/types/coordinator-mcp/server.d.ts +4 -0
  27. package/dist/types/discovery/helpers.d.ts +0 -6
  28. package/dist/types/edit/index.d.ts +3 -0
  29. package/dist/types/eval/py/env.d.ts +9 -0
  30. package/dist/types/extensibility/extensions/loader.d.ts +2 -1
  31. package/dist/types/extensibility/extensions/runner.d.ts +2 -2
  32. package/dist/types/extensibility/extensions/types.d.ts +34 -3
  33. package/dist/types/extensibility/hooks/runner.d.ts +1 -1
  34. package/dist/types/extensibility/shared-events.d.ts +2 -0
  35. package/dist/types/gjc-runtime/deep-interview-recorder.d.ts +10 -1
  36. package/dist/types/gjc-runtime/deep-interview-state.d.ts +44 -0
  37. package/dist/types/gjc-runtime/goal-mode-request.d.ts +11 -1
  38. package/dist/types/gjc-runtime/launch-worktree.d.ts +4 -2
  39. package/dist/types/gjc-runtime/linux-proc.d.ts +29 -0
  40. package/dist/types/gjc-runtime/session-resolution.d.ts +2 -2
  41. package/dist/types/gjc-runtime/state-migrations.d.ts +1 -1
  42. package/dist/types/gjc-runtime/state-writer.d.ts +2 -1
  43. package/dist/types/gjc-runtime/team-launch.d.ts +59 -0
  44. package/dist/types/gjc-runtime/team-notify.d.ts +45 -0
  45. package/dist/types/gjc-runtime/team-runtime.d.ts +29 -121
  46. package/dist/types/gjc-runtime/team-store.d.ts +160 -0
  47. package/dist/types/gjc-runtime/team-workers.d.ts +78 -0
  48. package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +8 -0
  49. package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +13 -0
  50. package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +38 -0
  51. package/dist/types/gjc-runtime/ultragoal-guard.d.ts +7 -0
  52. package/dist/types/gjc-runtime/ultragoal-pipeline.d.ts +23 -0
  53. package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +55 -33
  54. package/dist/types/gjc-runtime/workflow-cli-common.d.ts +8 -0
  55. package/dist/types/gjc-runtime/workflow-manifest.d.ts +3 -0
  56. package/dist/types/goals/continuation-timeout-guard.d.ts +22 -0
  57. package/dist/types/goals/runtime.d.ts +1 -0
  58. package/dist/types/goals/state.d.ts +8 -0
  59. package/dist/types/harness-control-plane/lifecycle-reconciliation.d.ts +17 -0
  60. package/dist/types/harness-control-plane/phase-rollup.d.ts +4 -7
  61. package/dist/types/harness-control-plane/receipt-ingest.d.ts +5 -0
  62. package/dist/types/harness-control-plane/receipt-spool.d.ts +4 -0
  63. package/dist/types/hindsight/state.d.ts +11 -4
  64. package/dist/types/lsp/lspmux.d.ts +5 -3
  65. package/dist/types/lsp/path-trust.d.ts +1 -0
  66. package/dist/types/main.d.ts +4 -2
  67. package/dist/types/memories/storage.d.ts +1 -0
  68. package/dist/types/modes/action-registry.d.ts +29 -0
  69. package/dist/types/modes/components/chrome.d.ts +14 -0
  70. package/dist/types/modes/components/command-palette.d.ts +27 -0
  71. package/dist/types/modes/components/custom-provider-wizard.d.ts +1 -0
  72. package/dist/types/modes/components/index.d.ts +2 -1
  73. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  74. package/dist/types/modes/components/plan-preview-overlay.d.ts +43 -0
  75. package/dist/types/modes/components/queue-mode-selector.d.ts +1 -1
  76. package/dist/types/modes/components/queue-pane.d.ts +12 -0
  77. package/dist/types/modes/components/read-tool-group.d.ts +6 -0
  78. package/dist/types/modes/components/session-observer-overlay.d.ts +3 -4
  79. package/dist/types/modes/components/sessions-dashboard.d.ts +32 -0
  80. package/dist/types/modes/components/settings-selector.d.ts +1 -1
  81. package/dist/types/modes/components/show-images-selector.d.ts +1 -1
  82. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  83. package/dist/types/modes/components/status-line.d.ts +1 -94
  84. package/dist/types/modes/components/tasks-pane.d.ts +15 -0
  85. package/dist/types/modes/components/theme-selector.d.ts +1 -1
  86. package/dist/types/modes/components/tool-execution.d.ts +15 -0
  87. package/dist/types/modes/components/tool-status-header.d.ts +111 -0
  88. package/dist/types/modes/components/transcript-viewer-overlay.d.ts +42 -0
  89. package/dist/types/modes/controllers/goal-mode-controller.d.ts +62 -0
  90. package/dist/types/modes/controllers/input-controller.d.ts +8 -1
  91. package/dist/types/modes/controllers/mode-gate.d.ts +8 -0
  92. package/dist/types/modes/controllers/plan-mode-controller.d.ts +65 -0
  93. package/dist/types/modes/controllers/selector-controller.d.ts +15 -3
  94. package/dist/types/modes/controllers/stt-controller.d.ts +26 -0
  95. package/dist/types/modes/interactive-mode.d.ts +19 -17
  96. package/dist/types/modes/jobs-observer.d.ts +2 -0
  97. package/dist/types/modes/prompt-action-autocomplete.d.ts +4 -0
  98. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +4 -11
  99. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +20 -2
  100. package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +31 -0
  101. package/dist/types/modes/tasks-aggregator.d.ts +39 -0
  102. package/dist/types/modes/transcript-item-registry.d.ts +60 -0
  103. package/dist/types/modes/types.d.ts +20 -13
  104. package/dist/types/modes/utils/ui-helpers.d.ts +2 -2
  105. package/dist/types/runtime-mcp/manager.d.ts +1 -1
  106. package/dist/types/sdk/broker/broker.d.ts +78 -12
  107. package/dist/types/sdk/broker/lifecycle-codec.d.ts +4 -0
  108. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +15 -1
  109. package/dist/types/sdk/broker/lifecycle.d.ts +6 -2
  110. package/dist/types/sdk/broker/process-incarnation.d.ts +2 -0
  111. package/dist/types/sdk/broker/session-index.d.ts +0 -1
  112. package/dist/types/sdk/broker/state-version.d.ts +2 -0
  113. package/dist/types/sdk/bus/chat-effect-journal.d.ts +1 -0
  114. package/dist/types/sdk/bus/config-commands.d.ts +1 -1
  115. package/dist/types/sdk/bus/config.d.ts +11 -0
  116. package/dist/types/sdk/bus/daemon-paths.d.ts +1 -0
  117. package/dist/types/sdk/bus/html-format.d.ts +3 -2
  118. package/dist/types/sdk/bus/index.d.ts +29 -0
  119. package/dist/types/sdk/bus/lifecycle-control-runtime.d.ts +4 -0
  120. package/dist/types/sdk/bus/notification-service.d.ts +20 -0
  121. package/dist/types/sdk/bus/rate-limit-pool.d.ts +13 -2
  122. package/dist/types/sdk/bus/recent-activity.d.ts +21 -12
  123. package/dist/types/sdk/bus/rich-document.d.ts +7 -0
  124. package/dist/types/sdk/bus/rich-render.d.ts +18 -41
  125. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +9 -4
  126. package/dist/types/sdk/bus/telegram-daemon.d.ts +46 -3
  127. package/dist/types/sdk/bus/threaded-inbound.d.ts +1 -1
  128. package/dist/types/sdk/bus/threaded-render.d.ts +13 -0
  129. package/dist/types/sdk/bus/topic-registry.d.ts +24 -2
  130. package/dist/types/sdk/host/control/operations.d.ts +1 -1
  131. package/dist/types/sdk/host/host.d.ts +2 -0
  132. package/dist/types/sdk/index.d.ts +1 -0
  133. package/dist/types/sdk/session-directory.d.ts +56 -0
  134. package/dist/types/sdk/session.d.ts +2 -0
  135. package/dist/types/session/agent-session.d.ts +82 -47
  136. package/dist/types/session/agent-storage.d.ts +7 -0
  137. package/dist/types/session/fallback-chain-controller.d.ts +10 -2
  138. package/dist/types/session/internal/managed-session-scope.d.ts +102 -0
  139. package/dist/types/session/internal/managed-session-storage.d.ts +38 -0
  140. package/dist/types/session/messages.d.ts +16 -0
  141. package/dist/types/session/session-manager-internal.d.ts +14 -0
  142. package/dist/types/session/session-manager.d.ts +82 -12
  143. package/dist/types/session/session-storage.d.ts +43 -3
  144. package/dist/types/session/volatile-context-pruning.d.ts +15 -0
  145. package/dist/types/skill-state/{deep-interview-mutation-guard.d.ts → workflow-mutation-guard.d.ts} +24 -8
  146. package/dist/types/skill-state/workflow-state-contract.d.ts +1 -3
  147. package/dist/types/slash-commands/builtin-registry.d.ts +4 -1
  148. package/dist/types/system-prompt.d.ts +2 -11
  149. package/dist/types/task/executor.d.ts +3 -1
  150. package/dist/types/thinking.d.ts +7 -0
  151. package/dist/types/tool-discovery/tool-index.d.ts +1 -50
  152. package/dist/types/tools/ask.d.ts +64 -2
  153. package/dist/types/tools/bash.d.ts +3 -0
  154. package/dist/types/tools/browser/dead-tab-recovery.d.ts +24 -0
  155. package/dist/types/tools/browser/tab-supervisor.d.ts +21 -0
  156. package/dist/types/tools/cron.d.ts +2 -0
  157. package/dist/types/tools/fetch.d.ts +1 -0
  158. package/dist/types/tools/index.d.ts +30 -15
  159. package/dist/types/tools/output-meta.d.ts +1 -0
  160. package/dist/types/tools/read.d.ts +5 -0
  161. package/dist/types/tools/search-tool-bm25.d.ts +3 -4
  162. package/dist/types/tools/subagent.d.ts +1 -0
  163. package/dist/types/utils/event-bus.d.ts +1 -1
  164. package/dist/types/utils/file-mentions.d.ts +3 -3
  165. package/dist/types/web/search/index.d.ts +3 -1
  166. package/package.json +21 -9
  167. package/scripts/build-sdk-package-smoke.ts +34 -1
  168. package/scripts/generate-hotkeys-docs.ts +74 -0
  169. package/scripts/generate-sdk-operation-inventory.ts +64 -20
  170. package/scripts/verify-gjc-sdk-canonicalization.ts +182 -2
  171. package/src/async/job-manager.ts +49 -6
  172. package/src/cli/config-cli.ts +75 -2
  173. package/src/cli/launch-disposition.ts +42 -0
  174. package/src/cli/notify-cli.ts +92 -26
  175. package/src/cli/session-picker.ts +20 -3
  176. package/src/cli/setup-cli.ts +1 -1
  177. package/src/cli.ts +1 -1
  178. package/src/commands/config.ts +1 -1
  179. package/src/commands/harness.ts +34 -106
  180. package/src/commands/launch.ts +6 -4
  181. package/src/commands/mcp-serve.ts +85 -8
  182. package/src/commands/mcp.ts +2 -2
  183. package/src/commands/notify.ts +8 -4
  184. package/src/commands/sdk.ts +61 -40
  185. package/src/commands/ultragoal.ts +4 -1
  186. package/src/config/atomic-yaml-patch.ts +80 -13
  187. package/src/config/file-lock.ts +105 -43
  188. package/src/config/keybindings.ts +64 -20
  189. package/src/config/model-auth.ts +9 -0
  190. package/src/config/model-bindings-applier.ts +139 -0
  191. package/src/config/model-discovery-manager.ts +224 -0
  192. package/src/config/model-equivalence.ts +66 -0
  193. package/src/config/model-registry.ts +169 -302
  194. package/src/config/model-resolver.ts +361 -369
  195. package/src/config/models-config-schema.ts +5 -4
  196. package/src/config/settings-schema.ts +214 -12
  197. package/src/config/settings.ts +458 -482
  198. package/src/coordinator-mcp/policy.ts +76 -11
  199. package/src/coordinator-mcp/question-gate-codec.ts +274 -0
  200. package/src/coordinator-mcp/question-state.ts +778 -0
  201. package/src/coordinator-mcp/server.ts +1702 -238
  202. package/src/debug/index.ts +1 -1
  203. package/src/defaults/gjc/skills/deep-interview/SKILL.md +13 -1
  204. package/src/defaults/gjc/skills/team/SKILL.md +16 -0
  205. package/src/discovery/helpers.ts +1 -50
  206. package/src/edit/index.ts +64 -0
  207. package/src/eval/py/env.ts +26 -0
  208. package/src/eval/py/kernel.ts +5 -3
  209. package/src/extensibility/extensions/loader.ts +9 -1
  210. package/src/extensibility/extensions/runner.ts +12 -3
  211. package/src/extensibility/extensions/types.ts +39 -1
  212. package/src/extensibility/gjc-plugins/installer.ts +16 -16
  213. package/src/extensibility/hooks/runner.ts +2 -2
  214. package/src/extensibility/runtime-skill-discovery.ts +33 -13
  215. package/src/extensibility/shared-events.ts +2 -0
  216. package/src/gjc-runtime/deep-interview-recorder.ts +63 -3
  217. package/src/gjc-runtime/deep-interview-runtime.ts +76 -27
  218. package/src/gjc-runtime/deep-interview-state.ts +317 -5
  219. package/src/gjc-runtime/goal-mode-request.ts +55 -8
  220. package/src/gjc-runtime/launch-tmux.ts +2 -5
  221. package/src/gjc-runtime/launch-worktree.ts +5 -5
  222. package/src/gjc-runtime/linux-proc.ts +71 -0
  223. package/src/gjc-runtime/ralplan-runtime.ts +74 -90
  224. package/src/gjc-runtime/session-resolution.ts +18 -3
  225. package/src/gjc-runtime/session-state-sidecar.ts +6 -107
  226. package/src/gjc-runtime/state-migrations.ts +1 -1
  227. package/src/gjc-runtime/state-runtime.ts +327 -249
  228. package/src/gjc-runtime/state-writer.ts +36 -40
  229. package/src/gjc-runtime/team-launch.ts +319 -0
  230. package/src/gjc-runtime/team-notify.ts +326 -0
  231. package/src/gjc-runtime/team-runtime.ts +1099 -1482
  232. package/src/gjc-runtime/team-store.ts +828 -0
  233. package/src/gjc-runtime/team-workers.ts +962 -0
  234. package/src/gjc-runtime/tmux-owner-isolation-cli.ts +2 -11
  235. package/src/gjc-runtime/tmux-owner-isolation.ts +14 -9
  236. package/src/gjc-runtime/tmux-sessions.ts +4 -21
  237. package/src/gjc-runtime/ultragoal-change-set.ts +172 -0
  238. package/src/gjc-runtime/ultragoal-evidence.ts +874 -0
  239. package/src/gjc-runtime/ultragoal-guard.ts +29 -5
  240. package/src/gjc-runtime/ultragoal-pipeline.ts +306 -0
  241. package/src/gjc-runtime/ultragoal-runtime.ts +84 -1311
  242. package/src/gjc-runtime/workflow-cli-common.ts +30 -0
  243. package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
  244. package/src/gjc-runtime/workflow-manifest.ts +18 -0
  245. package/src/goals/continuation-timeout-guard.ts +79 -0
  246. package/src/goals/runtime.ts +7 -5
  247. package/src/goals/state.ts +18 -0
  248. package/src/harness-control-plane/lifecycle-reconciliation.ts +64 -0
  249. package/src/harness-control-plane/phase-rollup.ts +4 -7
  250. package/src/harness-control-plane/receipt-ingest.ts +5 -0
  251. package/src/harness-control-plane/receipt-spool.ts +51 -7
  252. package/src/harness-control-plane/session-lease.ts +12 -130
  253. package/src/hindsight/backend.ts +26 -27
  254. package/src/hindsight/state.ts +101 -32
  255. package/src/hooks/skill-state.ts +17 -11
  256. package/src/internal-urls/docs-index.generated.ts +21 -18
  257. package/src/lsp/client.ts +1 -1
  258. package/src/lsp/config.ts +145 -26
  259. package/src/lsp/index.ts +1 -1
  260. package/src/lsp/lspmux.ts +39 -10
  261. package/src/lsp/path-trust.ts +81 -0
  262. package/src/main.ts +127 -30
  263. package/src/memories/index.ts +1 -0
  264. package/src/memories/storage.ts +6 -2
  265. package/src/modes/action-registry.ts +165 -0
  266. package/src/modes/components/chrome.ts +40 -0
  267. package/src/modes/components/command-palette.ts +168 -0
  268. package/src/modes/components/custom-editor.ts +1 -1
  269. package/src/modes/components/custom-provider-wizard.ts +51 -14
  270. package/src/modes/components/index.ts +2 -1
  271. package/src/modes/components/pet-selector.ts +11 -17
  272. package/src/modes/components/plan-preview-overlay.ts +295 -0
  273. package/src/modes/components/queue-mode-selector.ts +10 -27
  274. package/src/modes/components/queue-pane.ts +79 -0
  275. package/src/modes/components/read-tool-group.ts +13 -0
  276. package/src/modes/components/session-observer-overlay.ts +207 -772
  277. package/src/modes/components/sessions-dashboard.ts +181 -0
  278. package/src/modes/components/settings-selector.ts +1 -1
  279. package/src/modes/components/show-images-selector.ts +10 -24
  280. package/src/modes/components/status-line/types.ts +1 -1
  281. package/src/modes/components/status-line.ts +1 -969
  282. package/src/modes/components/tasks-pane.ts +73 -0
  283. package/src/modes/components/theme-selector.ts +11 -31
  284. package/src/modes/components/tool-execution.ts +22 -0
  285. package/src/modes/components/tool-status-header.ts +1056 -0
  286. package/src/modes/components/transcript-viewer-overlay.ts +384 -0
  287. package/src/modes/controllers/btw-controller.ts +1 -0
  288. package/src/modes/controllers/event-controller.ts +3 -2
  289. package/src/modes/controllers/extension-ui-controller.ts +24 -1
  290. package/src/modes/controllers/goal-mode-controller.ts +474 -0
  291. package/src/modes/controllers/input-controller.ts +764 -229
  292. package/src/modes/controllers/mode-gate.ts +24 -0
  293. package/src/modes/controllers/plan-mode-controller.ts +517 -0
  294. package/src/modes/controllers/selector-controller.ts +225 -10
  295. package/src/modes/controllers/stt-controller.ts +125 -0
  296. package/src/modes/interactive-mode.ts +289 -1150
  297. package/src/modes/jobs-observer.ts +3 -0
  298. package/src/modes/prompt-action-autocomplete.ts +7 -0
  299. package/src/modes/runtime-init.ts +8 -0
  300. package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -106
  301. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +71 -16
  302. package/src/modes/shared/agent-wire/workflow-gate-types.ts +177 -0
  303. package/src/modes/tasks-aggregator.ts +199 -0
  304. package/src/modes/transcript-item-registry.ts +229 -0
  305. package/src/modes/types.ts +45 -18
  306. package/src/modes/utils/ui-helpers.ts +17 -9
  307. package/src/prompts/agent-fragments/ralplan-persistence.md +6 -0
  308. package/src/prompts/agent-fragments/restricted-bash.md +1 -0
  309. package/src/prompts/agents/architect.md +2 -8
  310. package/src/prompts/agents/critic.md +2 -8
  311. package/src/prompts/agents/executor.md +3 -0
  312. package/src/prompts/agents/planner.md +4 -12
  313. package/src/prompts/system/custom-system-prompt.md +1 -23
  314. package/src/prompts/system/plan-mode-approved.md +8 -0
  315. package/src/prompts/system/system-prompt.md +17 -96
  316. package/src/prompts/tools/bash.md +4 -4
  317. package/src/prompts/tools/browser.md +15 -52
  318. package/src/prompts/tools/find.md +2 -2
  319. package/src/prompts/tools/hashline.md +13 -55
  320. package/src/prompts/tools/read.md +3 -6
  321. package/src/prompts/tools/search-tool-bm25.md +0 -2
  322. package/src/prompts/tools/search.md +2 -4
  323. package/src/prompts/tools/skill-discovery.md +1 -2
  324. package/src/prompts/tools/task.md +1 -1
  325. package/src/runtime-mcp/manager.ts +1 -1
  326. package/src/sdk/broker/broker.ts +206 -78
  327. package/src/sdk/broker/discovery.ts +1 -1
  328. package/src/sdk/broker/ensure.ts +3 -1
  329. package/src/sdk/broker/lifecycle-codec.ts +11 -0
  330. package/src/sdk/broker/lifecycle-ledger.ts +371 -74
  331. package/src/sdk/broker/lifecycle.ts +1399 -142
  332. package/src/sdk/broker/process-incarnation.ts +5 -13
  333. package/src/sdk/broker/session-index.ts +145 -56
  334. package/src/sdk/broker/state-version.ts +21 -0
  335. package/src/sdk/broker/transport.ts +1 -1
  336. package/src/sdk/bus/chat-daemon-cli.ts +2 -0
  337. package/src/sdk/bus/chat-daemon-control.ts +8 -28
  338. package/src/sdk/bus/chat-effect-journal.ts +10 -2
  339. package/src/sdk/bus/config-commands.ts +5 -4
  340. package/src/sdk/bus/config.ts +98 -0
  341. package/src/sdk/bus/conversation-store.ts +5 -29
  342. package/src/sdk/bus/daemon-paths.ts +2 -0
  343. package/src/sdk/bus/html-format.ts +20 -3
  344. package/src/sdk/bus/index.ts +782 -94
  345. package/src/sdk/bus/lifecycle-control-runtime.ts +91 -33
  346. package/src/sdk/bus/natives-augment.d.ts +26 -1
  347. package/src/sdk/bus/notification-service.ts +168 -20
  348. package/src/sdk/bus/rate-limit-pool.ts +58 -14
  349. package/src/sdk/bus/recent-activity.ts +188 -74
  350. package/src/sdk/bus/rich-document.ts +258 -0
  351. package/src/sdk/bus/rich-render.ts +62 -70
  352. package/src/sdk/bus/telegram-daemon-cli.ts +15 -59
  353. package/src/sdk/bus/telegram-daemon-contract.ts +9 -4
  354. package/src/sdk/bus/telegram-daemon.ts +1389 -237
  355. package/src/sdk/bus/threaded-inbound.ts +8 -3
  356. package/src/sdk/bus/threaded-render.ts +57 -1
  357. package/src/sdk/bus/topic-registry.ts +120 -13
  358. package/src/sdk/host/control/dispatch.ts +14 -2
  359. package/src/sdk/host/control/operations.ts +6 -1
  360. package/src/sdk/host/host.ts +44 -2
  361. package/src/sdk/host/query/handlers.ts +20 -22
  362. package/src/sdk/host/query/revision-store.ts +80 -57
  363. package/src/sdk/index.ts +1 -0
  364. package/src/sdk/protocol/operation-inventory.generated.json +943 -1907
  365. package/src/sdk/session-directory.ts +141 -0
  366. package/src/sdk/session.ts +462 -480
  367. package/src/session/agent-session.ts +1700 -583
  368. package/src/session/agent-storage.ts +18 -0
  369. package/src/session/cache-economics.ts +25 -8
  370. package/src/session/fallback-chain-controller.ts +53 -3
  371. package/src/session/internal/managed-session-scope.ts +2226 -0
  372. package/src/session/internal/managed-session-storage.ts +453 -0
  373. package/src/session/messages.ts +91 -2
  374. package/src/session/session-manager-internal.ts +33 -0
  375. package/src/session/session-manager.ts +1112 -333
  376. package/src/session/session-storage.ts +444 -39
  377. package/src/session/volatile-context-pruning.ts +78 -0
  378. package/src/setup/credential-auto-import.ts +2 -1
  379. package/src/setup/hermes/templates/operator-instructions.v1.md +1 -1
  380. package/src/setup/provider-onboarding.ts +36 -61
  381. package/src/setup/provider-presets.json +5 -0
  382. package/src/skill-state/active-state.ts +3 -13
  383. package/src/skill-state/{deep-interview-mutation-guard.ts → workflow-mutation-guard.ts} +47 -29
  384. package/src/skill-state/workflow-state-contract.ts +6 -41
  385. package/src/slash-commands/builtin-registry.ts +46 -11
  386. package/src/system-prompt.ts +37 -44
  387. package/src/task/agents.ts +19 -1
  388. package/src/task/executor.ts +90 -6
  389. package/src/task/index.ts +36 -21
  390. package/src/thinking.ts +21 -6
  391. package/src/tool-discovery/tool-index.ts +2 -134
  392. package/src/tools/ask.ts +157 -21
  393. package/src/tools/ast-edit.ts +1 -1
  394. package/src/tools/bash.ts +38 -0
  395. package/src/tools/browser/dead-tab-recovery.ts +78 -0
  396. package/src/tools/browser/tab-supervisor.ts +282 -36
  397. package/src/tools/computer.ts +10 -3
  398. package/src/tools/cron.ts +30 -16
  399. package/src/tools/eval.ts +1 -2
  400. package/src/tools/fetch.ts +11 -4
  401. package/src/tools/index.ts +76 -24
  402. package/src/tools/output-meta.ts +19 -7
  403. package/src/tools/read.ts +219 -64
  404. package/src/tools/resource-gc.ts +2 -2
  405. package/src/tools/search-tool-bm25.ts +11 -74
  406. package/src/tools/subagent.ts +43 -2
  407. package/src/utils/event-bus.ts +9 -4
  408. package/src/utils/file-mentions.ts +4 -4
  409. package/src/web/search/index.ts +3 -3
  410. package/dist/types/discovery/plugin-dir-roots.d.ts +0 -15
  411. package/dist/types/runtime-mcp/discoverable-tool-metadata.d.ts +0 -7
  412. package/src/discovery/plugin-dir-roots.ts +0 -28
  413. package/src/runtime-mcp/discoverable-tool-metadata.ts +0 -25
@@ -1,6 +1,9 @@
1
+ import { createHash } from "node:crypto";
2
+
1
3
  import * as fs from "node:fs";
2
- import * as fsp from "node:fs/promises";
3
4
  import * as path from "node:path";
5
+ import * as native from "@gajae-code/natives";
6
+
4
7
  import { isEnoent, pathIsWithin, peekFile, toError } from "@gajae-code/utils";
5
8
 
6
9
  const utf8Decoder = new TextDecoder("utf-8");
@@ -155,10 +158,13 @@ export interface SessionStorage {
155
158
  // Verified hard-delete identity + typed partial-cleanup evidence
156
159
  // =============================================================================
157
160
 
158
- /** Exact (dev, ino) identity fields used for ACP authorization binding. */
161
+ /** Exact authorization evidence for a transcript or artifact path. */
159
162
  export interface SessionStorageFileIdentity {
160
163
  dev: bigint;
161
164
  ino: bigint;
165
+ size: number;
166
+ mtimeNs: bigint;
167
+ sha256: string;
162
168
  }
163
169
 
164
170
  /** Kind of verification failure surfaced by {@link deleteSessionVerified}. */
@@ -209,6 +215,18 @@ export interface VerifiedSessionDeleteTarget {
209
215
  * fails closed. Omit on first attempt or to accept recorded absence.
210
216
  */
211
217
  expectedArtifactsIdentity?: SessionStorageFileIdentity;
218
+ /** Stable native recursive-tree evidence captured before artifact detachment. */
219
+ expectedArtifactsTree?: NativeDirectoryTreeSnapshot;
220
+ /** Identity-bound quarantine path retained when recursive artifact cleanup failed. */
221
+ detachedArtifactsPath?: string;
222
+ /** Identity-bound quarantine path retained when transcript unlink deferred cleanup. */
223
+ detachedTranscriptPath?: string;
224
+ /** Caller-published, no-replace quarantine pathname for the next artifact detach. */
225
+ plannedArtifactsPath?: string;
226
+ /** Caller-published, no-replace quarantine pathname for the next transcript detach. */
227
+ plannedTranscriptPath?: string;
228
+ /** Set only after a durable caller receipt records successful artifact removal. */
229
+ artifactsRemoved?: true;
212
230
  }
213
231
 
214
232
  /**
@@ -218,6 +236,7 @@ export interface VerifiedSessionDeleteTarget {
218
236
  * `deleted` and never `{}`.
219
237
  */
220
238
  export type VerifiedSessionDeleteResult =
239
+ | { kind: "artifacts_removed"; phase: "artifacts"; transcriptIdentity: SessionStorageFileIdentity }
221
240
  | { kind: "deleted" }
222
241
  | {
223
242
  kind: "cleanup_pending";
@@ -225,6 +244,10 @@ export type VerifiedSessionDeleteResult =
225
244
  error: Error;
226
245
  /** Artifact directory identity at failure time; undefined when absent. */
227
246
  artifactsIdentity: SessionStorageFileIdentity | undefined;
247
+ /** Identity-bound quarantine path retained when recursive cleanup failed. */
248
+ detachedArtifactsPath: string;
249
+ /** Native snapshot required for an identity-bound recursive retry. */
250
+ artifactsTree: NativeDirectoryTreeSnapshot;
228
251
  /** Transcript identity (unchanged) for retry binding. */
229
252
  transcriptIdentity: SessionStorageFileIdentity;
230
253
  }
@@ -234,6 +257,8 @@ export type VerifiedSessionDeleteResult =
234
257
  error: Error;
235
258
  /** Transcript identity at failure time for retry binding. */
236
259
  transcriptIdentity: SessionStorageFileIdentity;
260
+ /** Optional identity-bound transcript quarantine path for restart cleanup. */
261
+ detachedTranscriptPath?: string;
237
262
  };
238
263
 
239
264
  /** Default OS-close dispatcher: a direct `fs.closeSync`. */
@@ -243,6 +268,110 @@ const defaultCloseAdapter: SessionStorageWriterCloseAdapter = {
243
268
  },
244
269
  };
245
270
 
271
+ type NativeSecurity = { ok: true } | { ok: false; code: string };
272
+
273
+ type NativeExactUnlinkResult = { ok: true; detachedPath?: string } | { ok: false; code: string; detachedPath?: string };
274
+ type NativeExactUnlink = (
275
+ path: string,
276
+ identity: {
277
+ dev: bigint;
278
+ ino: bigint;
279
+ size: bigint;
280
+ mtimeNs: bigint;
281
+ /** Required for regular-file deletion; directories are identity-bound only. */
282
+ sha256?: string;
283
+ directory?: boolean;
284
+ /** Optional caller-planned no-replace quarantine destination component. */
285
+ quarantineName?: string;
286
+ },
287
+ ) => NativeExactUnlinkResult;
288
+
289
+ function nativeExactUnlink(
290
+ pathname: string,
291
+ identity: {
292
+ dev: bigint;
293
+ ino: bigint;
294
+ size: bigint;
295
+ mtimeNs: bigint;
296
+ /** Required for regular-file deletion; directories are identity-bound only. */
297
+ sha256?: string;
298
+ directory?: boolean;
299
+ quarantineName?: string;
300
+ },
301
+ ): NativeExactUnlinkResult {
302
+ return (native.exactUnlink as unknown as NativeExactUnlink)(pathname, identity);
303
+ }
304
+
305
+ type NativeDirectoryTreeEntry = {
306
+ relativePath: string;
307
+ kind: string;
308
+ dev: string;
309
+ ino: string;
310
+ size: string;
311
+ mtimeNs: string;
312
+ sha256?: string;
313
+ };
314
+ export type NativeDirectoryTreeSnapshot = {
315
+ rootDev: string;
316
+ rootIno: string;
317
+ entries: NativeDirectoryTreeEntry[];
318
+ };
319
+ type NativeDirectoryTreeResult =
320
+ | { ok: true; snapshot: NativeDirectoryTreeSnapshot }
321
+ | { ok: false; code: string; snapshot?: undefined };
322
+ type NativeDirectoryTreeApi = {
323
+ snapshotDirectoryTree(pathname: string): NativeDirectoryTreeResult;
324
+ exactRemoveDirectoryTree(pathname: string, snapshot: NativeDirectoryTreeSnapshot): NativeExactUnlinkResult;
325
+ };
326
+ function nativeDirectoryTreeApi(): NativeDirectoryTreeApi {
327
+ return native as unknown as NativeDirectoryTreeApi;
328
+ }
329
+ function snapshotDirectoryTree(pathname: string): NativeDirectoryTreeSnapshot {
330
+ const result = nativeDirectoryTreeApi().snapshotDirectoryTree(pathname);
331
+ if (!result.ok || !result.snapshot)
332
+ throw new SessionDeleteVerificationError(
333
+ "artifacts",
334
+ `Native artifact snapshot rejected: ${result.ok ? "missing_snapshot" : result.code}`,
335
+ );
336
+ return result.snapshot;
337
+ }
338
+ function removeDirectoryTreeExact(pathname: string, snapshot: NativeDirectoryTreeSnapshot): NativeExactUnlinkResult {
339
+ return nativeDirectoryTreeApi().exactRemoveDirectoryTree(pathname, snapshot);
340
+ }
341
+
342
+ function exactUnlinkFailure(result: NativeExactUnlinkResult): SessionDeleteVerificationError {
343
+ if (result.ok) throw new Error("Expected exact unlink failure");
344
+ const kind: VerifiedDeleteFailureKind =
345
+ result.code === "reparse_point" || result.code === "not_regular_file"
346
+ ? "symlink"
347
+ : result.code === "identity_mismatch"
348
+ ? "identity"
349
+ : "stat";
350
+ return new SessionDeleteVerificationError(kind, `Exact transcript deletion rejected: ${result.code}`);
351
+ }
352
+ function secureOwnerOnlyFile(pathname: string): void {
353
+ const applied = native.applyOwnerOnlyPathSecurity(pathname, "file") as NativeSecurity;
354
+ if (!applied.ok) throw new Error(`Owner-only security rejected ${pathname}: ${applied.code}`);
355
+ const verified = native.verifyOwnerOnlyPathSecurity(pathname, "file") as NativeSecurity;
356
+ if (!verified.ok) throw new Error(`Owner-only security rejected ${pathname}: ${verified.code}`);
357
+ }
358
+
359
+ /** Reject a symlink/junction/reparse component before a storage path is created or opened. */
360
+ function assertNoReparsePath(pathname: string): void {
361
+ const resolved = path.resolve(pathname);
362
+ const parsed = path.parse(resolved);
363
+ let current = parsed.root;
364
+ for (const part of resolved.slice(parsed.root.length).split(path.sep)) {
365
+ if (!part) continue;
366
+ current = path.join(current, part);
367
+ try {
368
+ if (fs.lstatSync(current).isSymbolicLink()) throw new Error(`Unsafe reparse storage path: ${current}`);
369
+ } catch (error) {
370
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
371
+ }
372
+ }
373
+ }
374
+
246
375
  // FinalizationRegistry to clean up leaked file descriptors
247
376
  const writerRegistry = new FinalizationRegistry<number>(fd => {
248
377
  try {
@@ -264,14 +393,25 @@ class FileSessionStorageWriter implements SessionStorageWriter {
264
393
  this.#onError = options?.onError;
265
394
  this.#closeAdapter = options?.closeAdapter ?? defaultCloseAdapter;
266
395
  const flags = options?.flags ?? "a";
267
- // Ensure parent directory exists
268
396
  const dir = path.dirname(fpath);
269
- if (!fs.existsSync(dir)) {
270
- fs.mkdirSync(dir, { recursive: true });
397
+ assertNoReparsePath(dir);
398
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
399
+ assertNoReparsePath(dir);
400
+ assertNoReparsePath(fpath);
401
+ // The creation mode prevents a POSIX exposure before the native verifier runs.
402
+ const openFlags =
403
+ (flags === "w"
404
+ ? fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC
405
+ : fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND) | (fs.constants.O_NOFOLLOW ?? 0);
406
+ const fd = fs.openSync(fpath, openFlags, 0o600);
407
+ try {
408
+ // Do not rely on directory inheritance: secure and verify every file independently.
409
+ secureOwnerOnlyFile(fpath);
410
+ } catch (error) {
411
+ fs.closeSync(fd);
412
+ throw error;
271
413
  }
272
- // Open file once, keep fd for lifetime
273
- this.#fd = fs.openSync(fpath, flags === "w" ? "w" : "a");
274
- // Register for cleanup if abandoned without close()
414
+ this.#fd = fd;
275
415
  writerRegistry.register(this, this.#fd, this);
276
416
  }
277
417
 
@@ -491,20 +631,18 @@ export class FileSessionStorage implements SessionStorage {
491
631
  }
492
632
 
493
633
  /**
494
- * Delete a session file and its artifacts directory.
495
- * Artifacts are stored in a sibling directory with the same name minus .jsonl extension.
634
+ * Delete a session and sibling artifacts in an operator-selected explicit directory.
635
+ * Default managed roots use deleteSessionVerified and never call this path.
496
636
  */
497
637
  async deleteSessionWithArtifacts(sessionPath: string): Promise<void> {
498
- // Delete the session file itself
499
- await this.unlink(sessionPath);
500
-
501
- // Compute artifacts directory: /path/to/session.jsonl -> /path/to/session
638
+ try {
639
+ await this.unlink(sessionPath);
640
+ } catch (error) {
641
+ if (!isEnoent(error)) throw error;
642
+ }
502
643
  const artifactsDir = sessionPath.slice(0, -6);
503
-
504
- // Delete artifacts directory if it exists. Missing directories are fine, but
505
- // surface real cleanup failures because the session file is already gone.
506
644
  try {
507
- await fsp.rm(artifactsDir, { recursive: true, force: true });
645
+ await fs.promises.rm(artifactsDir, { recursive: true, force: true });
508
646
  } catch (err) {
509
647
  const error = toError(err);
510
648
  throw new Error(
@@ -521,68 +659,314 @@ export class FileSessionStorage implements SessionStorage {
521
659
  * evidence; identity/symlink/containment/header/cwd mismatch throws.
522
660
  */
523
661
  async deleteSessionVerified(target: VerifiedSessionDeleteTarget): Promise<VerifiedSessionDeleteResult> {
524
- const { sessionsRoot, transcriptPath, sessionId, cwd, transcriptIdentity, expectedArtifactsIdentity } = target;
662
+ const {
663
+ sessionsRoot,
664
+ transcriptPath,
665
+ sessionId,
666
+ cwd,
667
+ transcriptIdentity,
668
+ expectedArtifactsIdentity,
669
+ expectedArtifactsTree,
670
+ detachedArtifactsPath,
671
+ detachedTranscriptPath,
672
+ plannedArtifactsPath,
673
+ plannedTranscriptPath,
674
+ artifactsRemoved,
675
+ } = target;
676
+ try {
677
+ assertNoReparsePath(sessionsRoot);
678
+ assertNoReparsePath(transcriptPath);
679
+ } catch (err) {
680
+ throw new SessionDeleteVerificationError("symlink", "Sessions root or transcript path is a symlink", {
681
+ cause: toError(err),
682
+ });
683
+ }
525
684
  if (!transcriptPath.endsWith(".jsonl")) {
526
685
  throw new SessionDeleteVerificationError("containment", "Transcript path is not a .jsonl file");
527
686
  }
528
687
  if (!pathIsWithin(sessionsRoot, transcriptPath)) {
529
688
  throw new SessionDeleteVerificationError("containment", "Transcript is outside the sessions root");
530
689
  }
531
- const initial = this.#verifiedReadAndHeader(transcriptPath, sessionId, cwd);
532
- const initialStat = initial.snapshot.stat;
533
- if (initialStat.dev !== transcriptIdentity.dev || initialStat.ino !== transcriptIdentity.ino) {
690
+ if (
691
+ !plannedArtifactsPath ||
692
+ !plannedTranscriptPath ||
693
+ path.dirname(plannedArtifactsPath) !== path.dirname(transcriptPath) ||
694
+ path.dirname(plannedTranscriptPath) !== path.dirname(transcriptPath) ||
695
+ !path.basename(plannedArtifactsPath).startsWith(".gjc-delete-") ||
696
+ !path.basename(plannedTranscriptPath).startsWith(".gjc-delete-") ||
697
+ plannedArtifactsPath === plannedTranscriptPath
698
+ ) {
699
+ throw new SessionDeleteVerificationError(
700
+ "artifacts",
701
+ "Verified deletion requires caller-persisted quarantine paths",
702
+ );
703
+ }
704
+ const cleanupTranscriptPath = detachedTranscriptPath ?? transcriptPath;
705
+ const hasDetachedTranscript = detachedTranscriptPath !== undefined;
706
+ if (
707
+ detachedTranscriptPath &&
708
+ (path.dirname(detachedTranscriptPath) !== path.dirname(transcriptPath) ||
709
+ !path.basename(detachedTranscriptPath).startsWith(".gjc-delete-") ||
710
+ detachedTranscriptPath === plannedTranscriptPath)
711
+ ) {
712
+ throw new SessionDeleteVerificationError(
713
+ "identity",
714
+ "Detached transcript retry requires a fresh quarantine destination",
715
+ );
716
+ }
717
+ if (hasDetachedTranscript && fs.existsSync(transcriptPath)) {
718
+ throw new SessionDeleteVerificationError(
719
+ "identity",
720
+ "Original transcript pathname became occupied during detached cleanup replay",
721
+ );
722
+ }
723
+ const artifactRemovalRoot = `${plannedArtifactsPath}.removing`;
724
+ if (detachedArtifactsPath === plannedArtifactsPath) {
725
+ throw new SessionDeleteVerificationError(
726
+ "artifacts",
727
+ "Detached artifact retry requires a fresh quarantine destination",
728
+ );
729
+ }
730
+ const retainedArtifactRoot = (input: string): string =>
731
+ input.endsWith(".removing") ? input : `${input}.removing`;
732
+
733
+ const initial = hasDetachedTranscript ? undefined : this.#verifiedReadAndHeader(transcriptPath, sessionId, cwd);
734
+ const initialStat = initial?.snapshot.stat;
735
+ const initialDigest = initial ? createHash("sha256").update(initial.snapshot.bytes).digest("hex") : undefined;
736
+ if (
737
+ initialStat &&
738
+ (initialStat.dev !== transcriptIdentity.dev ||
739
+ initialStat.ino !== transcriptIdentity.ino ||
740
+ initialStat.size !== transcriptIdentity.size ||
741
+ initialStat.mtimeNs !== transcriptIdentity.mtimeNs ||
742
+ initialDigest !== transcriptIdentity.sha256)
743
+ ) {
534
744
  throw new SessionDeleteVerificationError("identity", "Transcript identity does not match authorization");
535
745
  }
746
+
536
747
  const parentIdentity = this.#directoryIdentity(path.dirname(transcriptPath));
748
+ if (detachedArtifactsPath) {
749
+ if (
750
+ !expectedArtifactsIdentity ||
751
+ path.dirname(detachedArtifactsPath) !== path.dirname(transcriptPath) ||
752
+ !path.basename(detachedArtifactsPath).startsWith(".gjc-delete-")
753
+ ) {
754
+ throw new SessionDeleteVerificationError("artifacts", "Detached artifact cleanup evidence is invalid");
755
+ }
756
+ const detachedIdentity = this.#optionalDirectoryIdentity(detachedArtifactsPath);
757
+ if (
758
+ !detachedIdentity ||
759
+ detachedIdentity.dev !== expectedArtifactsIdentity.dev ||
760
+ detachedIdentity.ino !== expectedArtifactsIdentity.ino
761
+ ) {
762
+ throw new SessionDeleteVerificationError("artifacts", "Detached artifact identity changed before retry");
763
+ }
764
+ if (!expectedArtifactsTree)
765
+ throw new SessionDeleteVerificationError(
766
+ "artifacts",
767
+ "Detached artifact cleanup requires a persisted tree snapshot",
768
+ );
769
+ const removal = removeDirectoryTreeExact(detachedArtifactsPath, expectedArtifactsTree);
770
+ if (!removal.ok) {
771
+ const retainedRoot = removal.detachedPath ?? detachedArtifactsPath;
772
+ if (retainedRoot !== detachedArtifactsPath && retainedRoot !== retainedArtifactRoot(detachedArtifactsPath))
773
+ throw new SessionDeleteVerificationError(
774
+ "artifacts",
775
+ "Native artifact removal returned an unauthorized root",
776
+ );
777
+ return {
778
+ kind: "cleanup_pending",
779
+ phase: "artifacts",
780
+ error: new SessionDeleteVerificationError(
781
+ "artifacts",
782
+ `Exact detached artifact removal rejected: ${removal.code}`,
783
+ ),
784
+ artifactsIdentity: expectedArtifactsIdentity,
785
+ detachedArtifactsPath: retainedRoot,
786
+ artifactsTree: expectedArtifactsTree,
787
+ transcriptIdentity,
788
+ };
789
+ }
790
+ }
537
791
 
792
+ if (artifactsRemoved && (detachedArtifactsPath || expectedArtifactsIdentity)) {
793
+ throw new SessionDeleteVerificationError(
794
+ "artifacts",
795
+ "Artifact phase receipt conflicts with pending artifact cleanup",
796
+ );
797
+ }
538
798
  const artifactsDir = transcriptPath.slice(0, -6);
539
799
  const artifactsIdentity = this.#optionalDirectoryIdentity(artifactsDir);
540
- if (artifactsIdentity) {
800
+ if (artifactsRemoved && artifactsIdentity) {
801
+ throw new SessionDeleteVerificationError(
802
+ "artifacts",
803
+ "Artifact path reappeared after durable artifact-phase completion",
804
+ );
805
+ }
806
+ if (!artifactsIdentity && expectedArtifactsIdentity && !detachedArtifactsPath && !artifactsRemoved) {
807
+ // Absence at the original path alone is not completion: native recursive removal
808
+ // may retain the planned root or its deterministic `.removing` final-stage root.
809
+ if (fs.existsSync(plannedArtifactsPath) || fs.existsSync(artifactRemovalRoot))
810
+ throw new SessionDeleteVerificationError(
811
+ "artifacts",
812
+ "Authorized artifact removal root remains after restart",
813
+ );
814
+ return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
815
+ }
816
+
817
+ if (artifactsIdentity && !artifactsRemoved) {
541
818
  if (
542
819
  expectedArtifactsIdentity &&
543
820
  (artifactsIdentity.dev !== expectedArtifactsIdentity.dev ||
544
- artifactsIdentity.ino !== expectedArtifactsIdentity.ino)
821
+ artifactsIdentity.ino !== expectedArtifactsIdentity.ino ||
822
+ artifactsIdentity.size !== expectedArtifactsIdentity.size ||
823
+ artifactsIdentity.mtimeNs !== expectedArtifactsIdentity.mtimeNs ||
824
+ artifactsIdentity.sha256 !== expectedArtifactsIdentity.sha256)
545
825
  ) {
546
826
  throw new SessionDeleteVerificationError(
547
827
  "artifacts",
548
828
  "Artifact directory identity does not match recorded cleanup evidence",
549
829
  );
550
830
  }
551
- try {
552
- await fsp.rm(artifactsDir, { recursive: true, force: true });
553
- } catch (err) {
831
+ const artifactStat = fs.lstatSync(artifactsDir, { bigint: true });
832
+ if (
833
+ artifactStat.isSymbolicLink() ||
834
+ !artifactStat.isDirectory() ||
835
+ artifactStat.dev !== artifactsIdentity.dev ||
836
+ artifactStat.ino !== artifactsIdentity.ino
837
+ ) {
838
+ throw new SessionDeleteVerificationError("artifacts", "Artifact directory changed before deletion");
839
+ }
840
+ const observedArtifactsTree = snapshotDirectoryTree(artifactsDir);
841
+ if (expectedArtifactsTree && JSON.stringify(observedArtifactsTree) !== JSON.stringify(expectedArtifactsTree))
842
+ throw new SessionDeleteVerificationError("artifacts", "Artifact tree changed before root detach");
843
+ const artifactsTree = expectedArtifactsTree ?? observedArtifactsTree;
844
+ const detach = nativeExactUnlink(artifactsDir, {
845
+ dev: artifactStat.dev,
846
+ ino: artifactStat.ino,
847
+ size: artifactStat.size,
848
+ mtimeNs: artifactStat.mtimeNs,
849
+ directory: true,
850
+ quarantineName: path.basename(plannedArtifactsPath),
851
+ });
852
+ if (!detach.ok || !detach.detachedPath) {
853
+ throw new SessionDeleteVerificationError(
854
+ "artifacts",
855
+ `Exact artifact detach rejected: ${detach.ok ? "missing_path" : detach.code}`,
856
+ );
857
+ }
858
+ const removal = removeDirectoryTreeExact(detach.detachedPath, artifactsTree);
859
+ if (!removal.ok) {
860
+ const retainedRoot = removal.detachedPath ?? detach.detachedPath;
861
+ if (retainedRoot !== detach.detachedPath && retainedRoot !== retainedArtifactRoot(detach.detachedPath))
862
+ throw new SessionDeleteVerificationError(
863
+ "artifacts",
864
+ "Native artifact removal returned an unauthorized root",
865
+ );
554
866
  return {
555
867
  kind: "cleanup_pending",
556
868
  phase: "artifacts",
557
- error: toError(err),
869
+ error: new SessionDeleteVerificationError(
870
+ "artifacts",
871
+ `Exact detached artifact removal rejected: ${removal.code}`,
872
+ ),
558
873
  artifactsIdentity,
559
- transcriptIdentity: { dev: initialStat.dev, ino: initialStat.ino },
874
+ detachedArtifactsPath: retainedRoot,
875
+ artifactsTree,
876
+ transcriptIdentity,
560
877
  };
561
878
  }
562
879
  }
563
-
880
+ if (!artifactsRemoved) {
881
+ if (process.platform !== "win32") {
882
+ let descriptor: number | undefined;
883
+ try {
884
+ descriptor = fs.openSync(
885
+ path.dirname(transcriptPath),
886
+ fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
887
+ );
888
+ const durableParent = fs.fstatSync(descriptor, { bigint: true });
889
+ if (
890
+ !durableParent.isDirectory() ||
891
+ durableParent.dev !== parentIdentity.dev ||
892
+ durableParent.ino !== parentIdentity.ino
893
+ )
894
+ throw new Error("parent_changed");
895
+ fs.fsyncSync(descriptor);
896
+ } catch (error) {
897
+ throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
898
+ } finally {
899
+ if (descriptor !== undefined) fs.closeSync(descriptor);
900
+ }
901
+ }
902
+ return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
903
+ }
904
+ if (hasDetachedTranscript) {
905
+ const deletion = nativeExactUnlink(cleanupTranscriptPath, {
906
+ dev: transcriptIdentity.dev,
907
+ ino: transcriptIdentity.ino,
908
+ size: BigInt(transcriptIdentity.size),
909
+ mtimeNs: transcriptIdentity.mtimeNs,
910
+ sha256: transcriptIdentity.sha256,
911
+ quarantineName: path.basename(plannedTranscriptPath),
912
+ });
913
+ if (!deletion.ok) {
914
+ const error = exactUnlinkFailure(deletion);
915
+ if (error.kind === "identity" || error.kind === "symlink") throw error;
916
+ return {
917
+ kind: "cleanup_pending",
918
+ phase: "transcript",
919
+ error,
920
+ transcriptIdentity,
921
+ detachedTranscriptPath: deletion.detachedPath ?? detachedTranscriptPath,
922
+ };
923
+ }
924
+ return { kind: "deleted" };
925
+ }
926
+ if (!initialStat || !initialDigest)
927
+ throw new SessionDeleteVerificationError("stat", "Transcript cleanup state is invalid");
564
928
  const revalidate = this.#verifiedReadAndHeader(transcriptPath, sessionId, cwd);
565
929
  const revalidateStat = revalidate.snapshot.stat;
566
- if (revalidateStat.dev !== initialStat.dev || revalidateStat.ino !== initialStat.ino) {
930
+ const revalidateDigest = createHash("sha256").update(revalidate.snapshot.bytes).digest("hex");
931
+ if (
932
+ revalidateStat.dev !== initialStat.dev ||
933
+ revalidateStat.ino !== initialStat.ino ||
934
+ revalidateStat.size !== initialStat.size ||
935
+ revalidateStat.mtimeNs !== initialStat.mtimeNs ||
936
+ revalidateDigest !== initialDigest
937
+ ) {
567
938
  throw new SessionDeleteVerificationError(
568
939
  "identity",
569
940
  "Transcript identity changed after artifact removal (replacement detected)",
570
941
  );
571
942
  }
943
+
572
944
  const parentIdentityNow = this.#directoryIdentity(path.dirname(transcriptPath));
573
945
  if (parentIdentityNow.dev !== parentIdentity.dev || parentIdentityNow.ino !== parentIdentity.ino) {
574
946
  throw new SessionDeleteVerificationError("identity", "Parent directory identity changed during deletion");
575
947
  }
576
948
 
577
- try {
578
- await this.unlink(transcriptPath);
579
- } catch (err) {
580
- if (isEnoent(err)) return { kind: "deleted" };
949
+ this.#assertPathMatchesSnapshot(transcriptPath, revalidate.snapshot);
950
+ if (!Number.isSafeInteger(revalidateStat.size) || revalidateStat.size < 0) {
951
+ throw new SessionDeleteVerificationError("identity", "Transcript size cannot be bound exactly for deletion");
952
+ }
953
+ const deletion = nativeExactUnlink(transcriptPath, {
954
+ dev: initialStat.dev,
955
+ ino: initialStat.ino,
956
+ size: BigInt(initialStat.size),
957
+ mtimeNs: initialStat.mtimeNs,
958
+ sha256: initialDigest,
959
+ quarantineName: path.basename(plannedTranscriptPath),
960
+ });
961
+ if (!deletion.ok) {
962
+ const error = exactUnlinkFailure(deletion);
963
+ if (error.kind === "identity" || error.kind === "symlink") throw error;
581
964
  return {
582
965
  kind: "cleanup_pending",
583
966
  phase: "transcript",
584
- error: toError(err),
585
- transcriptIdentity: { dev: revalidateStat.dev, ino: revalidateStat.ino },
967
+ error,
968
+ transcriptIdentity,
969
+ detachedTranscriptPath: deletion.detachedPath,
586
970
  };
587
971
  }
588
972
  return { kind: "deleted" };
@@ -627,6 +1011,27 @@ export class FileSessionStorage implements SessionStorage {
627
1011
  return { snapshot };
628
1012
  }
629
1013
 
1014
+ #assertPathMatchesSnapshot(transcriptPath: string, snapshot: SessionStorageSnapshot): void {
1015
+ let named: fs.BigIntStats;
1016
+ try {
1017
+ named = fs.lstatSync(transcriptPath, { bigint: true });
1018
+ } catch (err) {
1019
+ throw new SessionDeleteVerificationError("stat", "Transcript path could not be revalidated", {
1020
+ cause: toError(err),
1021
+ });
1022
+ }
1023
+ if (
1024
+ named.isSymbolicLink() ||
1025
+ !named.isFile() ||
1026
+ named.dev !== snapshot.stat.dev ||
1027
+ named.ino !== snapshot.stat.ino ||
1028
+ Number(named.size) !== snapshot.stat.size ||
1029
+ named.mtimeNs !== snapshot.stat.mtimeNs
1030
+ ) {
1031
+ throw new SessionDeleteVerificationError("identity", "Transcript path changed before deletion");
1032
+ }
1033
+ }
1034
+
630
1035
  #directoryIdentity(dirPath: string): SessionStorageFileIdentity {
631
1036
  let stat: fs.BigIntStats;
632
1037
  try {
@@ -637,7 +1042,7 @@ export class FileSessionStorage implements SessionStorage {
637
1042
  if (stat.isSymbolicLink() || !stat.isDirectory()) {
638
1043
  throw new SessionDeleteVerificationError("symlink", "Directory is a symlink or not a directory");
639
1044
  }
640
- return { dev: stat.dev, ino: stat.ino };
1045
+ return { dev: stat.dev, ino: stat.ino, size: Number(stat.size), mtimeNs: stat.mtimeNs, sha256: "" };
641
1046
  }
642
1047
 
643
1048
  #optionalDirectoryIdentity(dirPath: string): SessionStorageFileIdentity | undefined {
@@ -660,7 +1065,7 @@ export class FileSessionStorage implements SessionStorage {
660
1065
  // Fail closed before any mutation.
661
1066
  throw new SessionDeleteVerificationError("artifacts", "Artifact path exists but is not a directory");
662
1067
  }
663
- return { dev: stat.dev, ino: stat.ino };
1068
+ return { dev: stat.dev, ino: stat.ino, size: Number(stat.size), mtimeNs: stat.mtimeNs, sha256: "" };
664
1069
  }
665
1070
  }
666
1071