@cmetech/otto 1.1.1 → 1.2.5

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 (433) hide show
  1. package/dist/coworker/persona-commands.d.ts +1 -0
  2. package/dist/coworker/persona-commands.js +5 -0
  3. package/dist/coworker/persona-commands.test.d.ts +1 -0
  4. package/dist/coworker/persona-commands.test.js +45 -0
  5. package/dist/resources/.managed-resources-content-hash +1 -1
  6. package/dist/resources/extensions/coworker-artifacts/artifacts-command.js +31 -0
  7. package/dist/resources/extensions/coworker-artifacts/artifacts-singleton.js +17 -0
  8. package/dist/resources/extensions/coworker-artifacts/extension-manifest.json +13 -0
  9. package/dist/resources/extensions/coworker-artifacts/index.js +125 -0
  10. package/dist/resources/extensions/coworker-artifacts/list-tool.js +27 -0
  11. package/dist/resources/extensions/coworker-artifacts/open-tool.js +25 -0
  12. package/dist/resources/extensions/coworker-memory/extension-manifest.json +13 -0
  13. package/dist/resources/extensions/coworker-memory/index.js +219 -0
  14. package/dist/resources/extensions/coworker-memory/memorize-tool.js +10 -0
  15. package/dist/resources/extensions/coworker-memory/memory-command.js +157 -0
  16. package/dist/resources/extensions/coworker-memory/memory-singleton.js +55 -0
  17. package/dist/resources/extensions/coworker-memory/recall-tool.js +18 -0
  18. package/dist/resources/extensions/coworker-memory/session-hooks.js +45 -0
  19. package/dist/resources/extensions/coworker-scratchpad/attach-banners.js +53 -0
  20. package/dist/resources/extensions/coworker-scratchpad/extension-manifest.json +13 -0
  21. package/dist/resources/extensions/coworker-scratchpad/format-age.js +9 -0
  22. package/dist/resources/extensions/coworker-scratchpad/helpers.js +38 -0
  23. package/dist/resources/extensions/coworker-scratchpad/index.js +199 -0
  24. package/dist/resources/extensions/coworker-scratchpad/mime-bundle.js +20 -0
  25. package/dist/resources/extensions/coworker-scratchpad/scratchpad-tool.js +118 -0
  26. package/dist/resources/extensions/coworker-scratchpad/session-sidecar.js +60 -0
  27. package/dist/resources/extensions/coworker-scratchpad/sp-command.js +597 -0
  28. package/dist/resources/extensions/coworker-scratchpad/workspace-pointer.js +41 -0
  29. package/dist/resources/extensions/coworker-scratchpad/workspace-root.js +17 -0
  30. package/dist/resources/extensions/coworker-vault/audit-command.js +35 -0
  31. package/dist/resources/extensions/coworker-vault/connect-command.js +42 -0
  32. package/dist/resources/extensions/coworker-vault/datasource-command.js +50 -0
  33. package/dist/resources/extensions/coworker-vault/extension-manifest.json +12 -0
  34. package/dist/resources/extensions/coworker-vault/index.js +171 -0
  35. package/dist/resources/extensions/coworker-vault/test-helpers.js +86 -0
  36. package/dist/resources/extensions/coworker-vault/vault-singleton.js +24 -0
  37. package/dist/resources/extensions/otto/commands/release-notes/_data.js +83 -0
  38. package/dist/resources/extensions/otto/commands/release-notes/command.js +15 -4
  39. package/dist/resources/extensions/otto/index.js +31 -6
  40. package/dist/resources/extensions/shared/coworker-paths.js +8 -0
  41. package/dist/resources/extensions/slash-commands/{audit.js → audit-codebase.js} +4 -4
  42. package/dist/resources/extensions/slash-commands/extension-manifest.json +1 -1
  43. package/dist/resources/extensions/slash-commands/index.js +2 -2
  44. package/dist/resources/extensions/subagent/index.js +8 -1
  45. package/dist/resources/extensions/subagent/launch.js +37 -5
  46. package/dist/resources/extensions/subagent/run-store.js +1 -0
  47. package/dist/resources/extensions/workflow/bootstrap/register-extension.js +2 -0
  48. package/dist/resources/extensions/workflow/bootstrap/register-hooks.js +10 -0
  49. package/dist/resources/extensions/workflow/health-widget-core.js +1 -1
  50. package/dist/resources/extensions/workflow/persona-status.js +87 -0
  51. package/package.json +26 -10
  52. package/packages/contracts/package.json +1 -1
  53. package/packages/coworker-artifacts/dist/artifact-store.d.ts +25 -0
  54. package/packages/coworker-artifacts/dist/artifact-store.js +187 -0
  55. package/packages/coworker-artifacts/dist/dir-snapshot.d.ts +7 -0
  56. package/packages/coworker-artifacts/dist/dir-snapshot.js +54 -0
  57. package/packages/coworker-artifacts/dist/errors.d.ts +18 -0
  58. package/packages/coworker-artifacts/dist/errors.js +37 -0
  59. package/packages/coworker-artifacts/dist/index.d.ts +7 -0
  60. package/packages/coworker-artifacts/dist/index.js +7 -0
  61. package/packages/coworker-artifacts/dist/readme-renderer.d.ts +5 -0
  62. package/packages/coworker-artifacts/dist/readme-renderer.js +47 -0
  63. package/packages/coworker-artifacts/dist/resolve-uri.d.ts +3 -0
  64. package/packages/coworker-artifacts/dist/resolve-uri.js +29 -0
  65. package/packages/coworker-artifacts/dist/slug.d.ts +4 -0
  66. package/packages/coworker-artifacts/dist/slug.js +32 -0
  67. package/packages/coworker-artifacts/dist/types.d.ts +52 -0
  68. package/packages/coworker-artifacts/dist/types.js +1 -0
  69. package/packages/coworker-artifacts/package.json +20 -0
  70. package/packages/coworker-artifacts/src/artifact-store.test.ts +188 -0
  71. package/packages/coworker-artifacts/src/artifact-store.ts +206 -0
  72. package/packages/coworker-artifacts/src/artifacts-integration.test.ts +109 -0
  73. package/packages/coworker-artifacts/src/dir-snapshot.test.ts +71 -0
  74. package/packages/coworker-artifacts/src/dir-snapshot.ts +52 -0
  75. package/packages/coworker-artifacts/src/errors.test.ts +37 -0
  76. package/packages/coworker-artifacts/src/errors.ts +28 -0
  77. package/packages/coworker-artifacts/src/index.test.ts +22 -0
  78. package/packages/coworker-artifacts/src/index.ts +7 -0
  79. package/packages/coworker-artifacts/src/readme-renderer.test.ts +72 -0
  80. package/packages/coworker-artifacts/src/readme-renderer.ts +56 -0
  81. package/packages/coworker-artifacts/src/resolve-uri.test.ts +46 -0
  82. package/packages/coworker-artifacts/src/resolve-uri.ts +29 -0
  83. package/packages/coworker-artifacts/src/slug.test.ts +47 -0
  84. package/packages/coworker-artifacts/src/slug.ts +31 -0
  85. package/packages/coworker-artifacts/src/types.ts +61 -0
  86. package/packages/coworker-artifacts/tsconfig.json +15 -0
  87. package/packages/coworker-artifacts/tsconfig.publish.json +4 -0
  88. package/packages/coworker-memory/dist/context-injection.d.ts +9 -0
  89. package/packages/coworker-memory/dist/context-injection.js +41 -0
  90. package/packages/coworker-memory/dist/errors.d.ts +25 -0
  91. package/packages/coworker-memory/dist/errors.js +51 -0
  92. package/packages/coworker-memory/dist/index.d.ts +12 -0
  93. package/packages/coworker-memory/dist/index.js +12 -0
  94. package/packages/coworker-memory/dist/layer-a-store.d.ts +16 -0
  95. package/packages/coworker-memory/dist/layer-a-store.js +78 -0
  96. package/packages/coworker-memory/dist/local-sqlite-backend.d.ts +28 -0
  97. package/packages/coworker-memory/dist/local-sqlite-backend.js +167 -0
  98. package/packages/coworker-memory/dist/memory-backend.d.ts +14 -0
  99. package/packages/coworker-memory/dist/memory-backend.js +1 -0
  100. package/packages/coworker-memory/dist/memory-recorder.d.ts +50 -0
  101. package/packages/coworker-memory/dist/memory-recorder.js +69 -0
  102. package/packages/coworker-memory/dist/migrations/001-init.sql +38 -0
  103. package/packages/coworker-memory/dist/migrations/002-artifact-kind.sql +50 -0
  104. package/packages/coworker-memory/dist/paste-detector.d.ts +5 -0
  105. package/packages/coworker-memory/dist/paste-detector.js +14 -0
  106. package/packages/coworker-memory/dist/persona-seed.d.ts +10 -0
  107. package/packages/coworker-memory/dist/persona-seed.js +38 -0
  108. package/packages/coworker-memory/dist/recall-formatter.d.ts +2 -0
  109. package/packages/coworker-memory/dist/recall-formatter.js +14 -0
  110. package/packages/coworker-memory/dist/scope-resolver.d.ts +9 -0
  111. package/packages/coworker-memory/dist/scope-resolver.js +10 -0
  112. package/packages/coworker-memory/dist/types.d.ts +51 -0
  113. package/packages/coworker-memory/dist/types.js +2 -0
  114. package/packages/coworker-memory/dist/workspace-id.d.ts +3 -0
  115. package/packages/coworker-memory/dist/workspace-id.js +54 -0
  116. package/packages/coworker-memory/package.json +35 -0
  117. package/packages/coworker-memory/src/activator-integration.test.ts +141 -0
  118. package/packages/coworker-memory/src/context-injection.test.ts +72 -0
  119. package/packages/coworker-memory/src/context-injection.ts +57 -0
  120. package/packages/coworker-memory/src/errors.test.ts +45 -0
  121. package/packages/coworker-memory/src/errors.ts +42 -0
  122. package/packages/coworker-memory/src/index.test.ts +21 -0
  123. package/packages/coworker-memory/src/index.ts +12 -0
  124. package/packages/coworker-memory/src/layer-a-store.test.ts +85 -0
  125. package/packages/coworker-memory/src/layer-a-store.ts +88 -0
  126. package/packages/coworker-memory/src/local-sqlite-backend.test.ts +110 -0
  127. package/packages/coworker-memory/src/local-sqlite-backend.ts +185 -0
  128. package/packages/coworker-memory/src/memory-backend.ts +10 -0
  129. package/packages/coworker-memory/src/memory-integration.test.ts +89 -0
  130. package/packages/coworker-memory/src/memory-recorder.test.ts +101 -0
  131. package/packages/coworker-memory/src/memory-recorder.ts +95 -0
  132. package/packages/coworker-memory/src/migrations/001-init.sql +38 -0
  133. package/packages/coworker-memory/src/migrations/002-artifact-kind.sql +50 -0
  134. package/packages/coworker-memory/src/paste-detector.test.ts +23 -0
  135. package/packages/coworker-memory/src/paste-detector.ts +18 -0
  136. package/packages/coworker-memory/src/persona-seed.test.ts +57 -0
  137. package/packages/coworker-memory/src/persona-seed.ts +46 -0
  138. package/packages/coworker-memory/src/recall-formatter.test.ts +34 -0
  139. package/packages/coworker-memory/src/recall-formatter.ts +15 -0
  140. package/packages/coworker-memory/src/scope-resolver.test.ts +23 -0
  141. package/packages/coworker-memory/src/scope-resolver.ts +18 -0
  142. package/packages/coworker-memory/src/types.ts +61 -0
  143. package/packages/coworker-memory/src/workspace-id.test.ts +48 -0
  144. package/packages/coworker-memory/src/workspace-id.ts +56 -0
  145. package/packages/coworker-memory/tsconfig.json +15 -0
  146. package/packages/coworker-memory/tsconfig.publish.json +4 -0
  147. package/packages/coworker-persona/dist/commands.d.ts +7 -0
  148. package/packages/coworker-persona/dist/commands.js +35 -0
  149. package/packages/coworker-persona/dist/defaults/manifest.yaml +12 -0
  150. package/packages/coworker-persona/dist/defaults/steering/identity.md +3 -0
  151. package/packages/coworker-persona/dist/index.d.ts +3 -0
  152. package/packages/coworker-persona/dist/index.js +3 -0
  153. package/packages/coworker-persona/dist/manifest.d.ts +24 -0
  154. package/packages/coworker-persona/dist/manifest.js +21 -0
  155. package/packages/coworker-persona/dist/registry.d.ts +22 -0
  156. package/packages/coworker-persona/dist/registry.js +142 -0
  157. package/packages/coworker-persona/package.json +28 -0
  158. package/packages/coworker-persona/scripts/copy-defaults.cjs +17 -0
  159. package/packages/coworker-persona/src/commands.ts +47 -0
  160. package/packages/coworker-persona/src/defaults/manifest.yaml +12 -0
  161. package/packages/coworker-persona/src/defaults/steering/identity.md +3 -0
  162. package/packages/coworker-persona/src/index.ts +3 -0
  163. package/packages/coworker-persona/src/manifest.test.ts +67 -0
  164. package/packages/coworker-persona/src/manifest.ts +49 -0
  165. package/packages/coworker-persona/src/registry.test.ts +89 -0
  166. package/packages/coworker-persona/src/registry.ts +147 -0
  167. package/packages/coworker-persona/tsconfig.json +15 -0
  168. package/packages/coworker-persona/tsconfig.publish.json +4 -0
  169. package/packages/coworker-scratchpad/dist/cell-archive.d.ts +39 -0
  170. package/packages/coworker-scratchpad/dist/cell-archive.js +77 -0
  171. package/packages/coworker-scratchpad/dist/cell-tree.d.ts +14 -0
  172. package/packages/coworker-scratchpad/dist/cell-tree.js +72 -0
  173. package/packages/coworker-scratchpad/dist/child-process-runtime.d.ts +129 -0
  174. package/packages/coworker-scratchpad/dist/child-process-runtime.js +427 -0
  175. package/packages/coworker-scratchpad/dist/collector-registry.d.ts +12 -0
  176. package/packages/coworker-scratchpad/dist/collector-registry.js +29 -0
  177. package/packages/coworker-scratchpad/dist/detect-kind.d.ts +3 -0
  178. package/packages/coworker-scratchpad/dist/detect-kind.js +19 -0
  179. package/packages/coworker-scratchpad/dist/file-collector.d.ts +15 -0
  180. package/packages/coworker-scratchpad/dist/file-collector.js +99 -0
  181. package/packages/coworker-scratchpad/dist/index.d.ts +13 -0
  182. package/packages/coworker-scratchpad/dist/index.js +13 -0
  183. package/packages/coworker-scratchpad/dist/kernel-bindings.d.ts +49 -0
  184. package/packages/coworker-scratchpad/dist/kernel-bindings.js +220 -0
  185. package/packages/coworker-scratchpad/dist/kernel-entry.d.ts +1 -0
  186. package/packages/coworker-scratchpad/dist/kernel-entry.js +355 -0
  187. package/packages/coworker-scratchpad/dist/kernel-protocol.d.ts +171 -0
  188. package/packages/coworker-scratchpad/dist/kernel-protocol.js +48 -0
  189. package/packages/coworker-scratchpad/dist/kernel-spawn.d.ts +3 -0
  190. package/packages/coworker-scratchpad/dist/kernel-spawn.js +54 -0
  191. package/packages/coworker-scratchpad/dist/namespace-codec.d.ts +22 -0
  192. package/packages/coworker-scratchpad/dist/namespace-codec.js +61 -0
  193. package/packages/coworker-scratchpad/dist/scratchpad-lock.d.ts +24 -0
  194. package/packages/coworker-scratchpad/dist/scratchpad-lock.js +86 -0
  195. package/packages/coworker-scratchpad/dist/scratchpad-manager.d.ts +193 -0
  196. package/packages/coworker-scratchpad/dist/scratchpad-manager.js +866 -0
  197. package/packages/coworker-scratchpad/dist/staleness-banner.d.ts +12 -0
  198. package/packages/coworker-scratchpad/dist/staleness-banner.js +27 -0
  199. package/packages/coworker-scratchpad/package.json +31 -0
  200. package/packages/coworker-scratchpad/src/cell-archive.test.ts +150 -0
  201. package/packages/coworker-scratchpad/src/cell-archive.ts +97 -0
  202. package/packages/coworker-scratchpad/src/cell-tree.test.ts +105 -0
  203. package/packages/coworker-scratchpad/src/cell-tree.ts +90 -0
  204. package/packages/coworker-scratchpad/src/child-process-runtime.test.ts +413 -0
  205. package/packages/coworker-scratchpad/src/child-process-runtime.ts +493 -0
  206. package/packages/coworker-scratchpad/src/collector-registry.test.ts +69 -0
  207. package/packages/coworker-scratchpad/src/collector-registry.ts +33 -0
  208. package/packages/coworker-scratchpad/src/detect-kind.test.ts +33 -0
  209. package/packages/coworker-scratchpad/src/detect-kind.ts +22 -0
  210. package/packages/coworker-scratchpad/src/file-collector.test.ts +109 -0
  211. package/packages/coworker-scratchpad/src/file-collector.ts +114 -0
  212. package/packages/coworker-scratchpad/src/index.ts +74 -0
  213. package/packages/coworker-scratchpad/src/kernel-bindings.test.ts +188 -0
  214. package/packages/coworker-scratchpad/src/kernel-bindings.ts +279 -0
  215. package/packages/coworker-scratchpad/src/kernel-entry.test.ts +123 -0
  216. package/packages/coworker-scratchpad/src/kernel-entry.ts +390 -0
  217. package/packages/coworker-scratchpad/src/kernel-protocol.test.ts +105 -0
  218. package/packages/coworker-scratchpad/src/kernel-protocol.ts +230 -0
  219. package/packages/coworker-scratchpad/src/kernel-spawn.test.ts +60 -0
  220. package/packages/coworker-scratchpad/src/kernel-spawn.ts +54 -0
  221. package/packages/coworker-scratchpad/src/namespace-codec.test.ts +102 -0
  222. package/packages/coworker-scratchpad/src/namespace-codec.ts +90 -0
  223. package/packages/coworker-scratchpad/src/scratchpad-lock.test.ts +98 -0
  224. package/packages/coworker-scratchpad/src/scratchpad-lock.ts +102 -0
  225. package/packages/coworker-scratchpad/src/scratchpad-manager.test.ts +1343 -0
  226. package/packages/coworker-scratchpad/src/scratchpad-manager.ts +891 -0
  227. package/packages/coworker-scratchpad/src/staleness-banner.test.ts +53 -0
  228. package/packages/coworker-scratchpad/src/staleness-banner.ts +33 -0
  229. package/packages/coworker-scratchpad/src/vault-integration.test.ts +221 -0
  230. package/packages/coworker-scratchpad/tsconfig.json +15 -0
  231. package/packages/coworker-scratchpad/tsconfig.publish.json +4 -0
  232. package/packages/coworker-types/dist/artifacts.d.ts +31 -0
  233. package/packages/coworker-types/dist/artifacts.js +2 -0
  234. package/packages/coworker-types/dist/contracts.d.ts +32 -0
  235. package/packages/coworker-types/dist/contracts.js +1 -0
  236. package/packages/coworker-types/dist/index.d.ts +5 -0
  237. package/packages/coworker-types/dist/index.js +5 -0
  238. package/packages/coworker-types/dist/memory.d.ts +61 -0
  239. package/packages/coworker-types/dist/memory.js +3 -0
  240. package/packages/coworker-types/dist/scratchpad.d.ts +43 -0
  241. package/packages/coworker-types/dist/scratchpad.js +2 -0
  242. package/packages/coworker-types/dist/vault.d.ts +34 -0
  243. package/packages/coworker-types/dist/vault.js +2 -0
  244. package/packages/coworker-types/package.json +24 -0
  245. package/packages/coworker-types/src/artifacts.test.ts +52 -0
  246. package/packages/coworker-types/src/artifacts.ts +35 -0
  247. package/packages/coworker-types/src/contracts.test.ts +43 -0
  248. package/packages/coworker-types/src/contracts.ts +36 -0
  249. package/packages/coworker-types/src/index.ts +5 -0
  250. package/packages/coworker-types/src/memory.test.ts +50 -0
  251. package/packages/coworker-types/src/memory.ts +79 -0
  252. package/packages/coworker-types/src/scratchpad.test.ts +46 -0
  253. package/packages/coworker-types/src/scratchpad.ts +51 -0
  254. package/packages/coworker-types/src/smoke.test.ts +34 -0
  255. package/packages/coworker-types/src/vault.test.ts +49 -0
  256. package/packages/coworker-types/src/vault.ts +40 -0
  257. package/packages/coworker-types/tsconfig.json +15 -0
  258. package/packages/coworker-types/tsconfig.publish.json +4 -0
  259. package/packages/coworker-utils/dist/audit-log.d.ts +34 -0
  260. package/packages/coworker-utils/dist/audit-log.js +88 -0
  261. package/packages/coworker-utils/dist/index.d.ts +6 -0
  262. package/packages/coworker-utils/dist/index.js +6 -0
  263. package/packages/coworker-utils/dist/lease.d.ts +7 -0
  264. package/packages/coworker-utils/dist/lease.js +67 -0
  265. package/packages/coworker-utils/dist/logger.d.ts +13 -0
  266. package/packages/coworker-utils/dist/logger.js +26 -0
  267. package/packages/coworker-utils/dist/migration-runner.d.ts +7 -0
  268. package/packages/coworker-utils/dist/migration-runner.js +36 -0
  269. package/packages/coworker-utils/dist/ndjson-channel.d.ts +3 -0
  270. package/packages/coworker-utils/dist/ndjson-channel.js +38 -0
  271. package/packages/coworker-utils/dist/secret-scanner.d.ts +10 -0
  272. package/packages/coworker-utils/dist/secret-scanner.js +42 -0
  273. package/packages/coworker-utils/package.json +24 -0
  274. package/packages/coworker-utils/src/audit-log.test.ts +140 -0
  275. package/packages/coworker-utils/src/audit-log.ts +107 -0
  276. package/packages/coworker-utils/src/index.ts +6 -0
  277. package/packages/coworker-utils/src/lease.test.ts +64 -0
  278. package/packages/coworker-utils/src/lease.ts +76 -0
  279. package/packages/coworker-utils/src/logger.test.ts +50 -0
  280. package/packages/coworker-utils/src/logger.ts +45 -0
  281. package/packages/coworker-utils/src/migration-runner.test.ts +65 -0
  282. package/packages/coworker-utils/src/migration-runner.ts +50 -0
  283. package/packages/coworker-utils/src/ndjson-channel.test.ts +76 -0
  284. package/packages/coworker-utils/src/ndjson-channel.ts +41 -0
  285. package/packages/coworker-utils/src/secret-scanner.test.ts +61 -0
  286. package/packages/coworker-utils/src/secret-scanner.ts +56 -0
  287. package/packages/coworker-utils/tsconfig.json +15 -0
  288. package/packages/coworker-utils/tsconfig.publish.json +4 -0
  289. package/packages/coworker-vault/dist/data-vault.d.ts +41 -0
  290. package/packages/coworker-vault/dist/data-vault.js +223 -0
  291. package/packages/coworker-vault/dist/engine-registry.d.ts +34 -0
  292. package/packages/coworker-vault/dist/engine-registry.js +90 -0
  293. package/packages/coworker-vault/dist/engines/jira.yaml +17 -0
  294. package/packages/coworker-vault/dist/errors.d.ts +28 -0
  295. package/packages/coworker-vault/dist/errors.js +57 -0
  296. package/packages/coworker-vault/dist/index.d.ts +6 -0
  297. package/packages/coworker-vault/dist/index.js +6 -0
  298. package/packages/coworker-vault/dist/injector.d.ts +19 -0
  299. package/packages/coworker-vault/dist/injector.js +77 -0
  300. package/packages/coworker-vault/dist/types.d.ts +28 -0
  301. package/packages/coworker-vault/dist/types.js +1 -0
  302. package/packages/coworker-vault/dist/vault-keep.d.ts +4 -0
  303. package/packages/coworker-vault/dist/vault-keep.js +21 -0
  304. package/packages/coworker-vault/package.json +29 -0
  305. package/packages/coworker-vault/src/data-vault.test.ts +199 -0
  306. package/packages/coworker-vault/src/data-vault.ts +257 -0
  307. package/packages/coworker-vault/src/engine-registry.test.ts +120 -0
  308. package/packages/coworker-vault/src/engine-registry.ts +107 -0
  309. package/packages/coworker-vault/src/engines/jira.yaml +17 -0
  310. package/packages/coworker-vault/src/errors.test.ts +58 -0
  311. package/packages/coworker-vault/src/errors.ts +50 -0
  312. package/packages/coworker-vault/src/index.test.ts +24 -0
  313. package/packages/coworker-vault/src/index.ts +6 -0
  314. package/packages/coworker-vault/src/injector.test.ts +109 -0
  315. package/packages/coworker-vault/src/injector.ts +98 -0
  316. package/packages/coworker-vault/src/types.ts +33 -0
  317. package/packages/coworker-vault/src/vault-keep.test.ts +49 -0
  318. package/packages/coworker-vault/src/vault-keep.ts +31 -0
  319. package/packages/coworker-vault/tsconfig.json +15 -0
  320. package/packages/coworker-vault/tsconfig.publish.json +4 -0
  321. package/packages/daemon/package.json +3 -3
  322. package/packages/mcp-server/package.json +3 -3
  323. package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
  324. package/packages/native/package.json +1 -1
  325. package/packages/native/tsconfig.tsbuildinfo +1 -1
  326. package/packages/pi-agent-core/package.json +1 -1
  327. package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
  328. package/packages/pi-ai/package.json +1 -1
  329. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  330. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +6 -1
  331. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  332. package/packages/pi-coding-agent/dist/core/extensions/runner.js +22 -3
  333. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  334. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js +11 -0
  335. package/packages/pi-coding-agent/dist/core/resolve-config-value.test.js.map +1 -1
  336. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.d.ts +47 -0
  337. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.d.ts.map +1 -0
  338. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.js +107 -0
  339. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.js.map +1 -0
  340. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.d.ts +19 -0
  341. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.d.ts.map +1 -0
  342. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.js +121 -0
  343. package/packages/pi-coding-agent/dist/modes/rpc/raw-stdout.regression.test.js.map +1 -0
  344. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  345. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +17 -1
  346. package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
  347. package/packages/pi-coding-agent/package.json +2 -2
  348. package/packages/pi-coding-agent/src/core/extensions/runner.ts +22 -3
  349. package/packages/pi-coding-agent/src/core/resolve-config-value.test.ts +11 -0
  350. package/packages/pi-coding-agent/src/modes/rpc/raw-stdout.regression.test.ts +129 -0
  351. package/packages/pi-coding-agent/src/modes/rpc/raw-stdout.ts +117 -0
  352. package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +18 -1
  353. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  354. package/packages/pi-tui/package.json +1 -1
  355. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  356. package/packages/rpc-client/package.json +2 -2
  357. package/packages/rpc-client/tsconfig.tsbuildinfo +1 -1
  358. package/pkg/package.json +1 -1
  359. package/scripts/install.js +6 -5
  360. package/src/resources/extensions/coworker-artifacts/artifacts-command.test.ts +54 -0
  361. package/src/resources/extensions/coworker-artifacts/artifacts-command.ts +43 -0
  362. package/src/resources/extensions/coworker-artifacts/artifacts-singleton.test.ts +25 -0
  363. package/src/resources/extensions/coworker-artifacts/artifacts-singleton.ts +29 -0
  364. package/src/resources/extensions/coworker-artifacts/extension-manifest.json +13 -0
  365. package/src/resources/extensions/coworker-artifacts/index.test.ts +46 -0
  366. package/src/resources/extensions/coworker-artifacts/index.ts +154 -0
  367. package/src/resources/extensions/coworker-artifacts/list-tool.test.ts +29 -0
  368. package/src/resources/extensions/coworker-artifacts/list-tool.ts +53 -0
  369. package/src/resources/extensions/coworker-artifacts/open-tool.test.ts +30 -0
  370. package/src/resources/extensions/coworker-artifacts/open-tool.ts +43 -0
  371. package/src/resources/extensions/coworker-memory/extension-manifest.json +13 -0
  372. package/src/resources/extensions/coworker-memory/index.test.ts +137 -0
  373. package/src/resources/extensions/coworker-memory/index.ts +257 -0
  374. package/src/resources/extensions/coworker-memory/memorize-tool.test.ts +41 -0
  375. package/src/resources/extensions/coworker-memory/memorize-tool.ts +20 -0
  376. package/src/resources/extensions/coworker-memory/memory-command.test.ts +134 -0
  377. package/src/resources/extensions/coworker-memory/memory-command.ts +131 -0
  378. package/src/resources/extensions/coworker-memory/memory-singleton.test.ts +41 -0
  379. package/src/resources/extensions/coworker-memory/memory-singleton.ts +89 -0
  380. package/src/resources/extensions/coworker-memory/recall-tool.test.ts +50 -0
  381. package/src/resources/extensions/coworker-memory/recall-tool.ts +35 -0
  382. package/src/resources/extensions/coworker-memory/session-hooks.test.ts +77 -0
  383. package/src/resources/extensions/coworker-memory/session-hooks.ts +61 -0
  384. package/src/resources/extensions/coworker-scratchpad/attach-banners.test.ts +124 -0
  385. package/src/resources/extensions/coworker-scratchpad/attach-banners.ts +67 -0
  386. package/src/resources/extensions/coworker-scratchpad/extension-manifest.json +13 -0
  387. package/src/resources/extensions/coworker-scratchpad/format-age.test.ts +30 -0
  388. package/src/resources/extensions/coworker-scratchpad/format-age.ts +6 -0
  389. package/src/resources/extensions/coworker-scratchpad/helpers.test.ts +93 -0
  390. package/src/resources/extensions/coworker-scratchpad/helpers.ts +42 -0
  391. package/src/resources/extensions/coworker-scratchpad/index.test.ts +514 -0
  392. package/src/resources/extensions/coworker-scratchpad/index.ts +207 -0
  393. package/src/resources/extensions/coworker-scratchpad/mime-bundle.test.ts +61 -0
  394. package/src/resources/extensions/coworker-scratchpad/mime-bundle.ts +23 -0
  395. package/src/resources/extensions/coworker-scratchpad/scratchpad-tool.test.ts +137 -0
  396. package/src/resources/extensions/coworker-scratchpad/scratchpad-tool.ts +165 -0
  397. package/src/resources/extensions/coworker-scratchpad/session-sidecar.test.ts +133 -0
  398. package/src/resources/extensions/coworker-scratchpad/session-sidecar.ts +68 -0
  399. package/src/resources/extensions/coworker-scratchpad/sp-command.test.ts +836 -0
  400. package/src/resources/extensions/coworker-scratchpad/sp-command.ts +602 -0
  401. package/src/resources/extensions/coworker-scratchpad/workspace-pointer.test.ts +74 -0
  402. package/src/resources/extensions/coworker-scratchpad/workspace-pointer.ts +55 -0
  403. package/src/resources/extensions/coworker-scratchpad/workspace-root.test.ts +51 -0
  404. package/src/resources/extensions/coworker-scratchpad/workspace-root.ts +16 -0
  405. package/src/resources/extensions/coworker-vault/audit-command.test.ts +109 -0
  406. package/src/resources/extensions/coworker-vault/audit-command.ts +56 -0
  407. package/src/resources/extensions/coworker-vault/connect-command.test.ts +103 -0
  408. package/src/resources/extensions/coworker-vault/connect-command.ts +69 -0
  409. package/src/resources/extensions/coworker-vault/datasource-command.test.ts +80 -0
  410. package/src/resources/extensions/coworker-vault/datasource-command.ts +81 -0
  411. package/src/resources/extensions/coworker-vault/extension-manifest.json +12 -0
  412. package/src/resources/extensions/coworker-vault/index.test.ts +82 -0
  413. package/src/resources/extensions/coworker-vault/index.ts +181 -0
  414. package/src/resources/extensions/coworker-vault/test-helpers.ts +120 -0
  415. package/src/resources/extensions/coworker-vault/vault-singleton.test.ts +27 -0
  416. package/src/resources/extensions/coworker-vault/vault-singleton.ts +40 -0
  417. package/src/resources/extensions/otto/commands/release-notes/_data.ts +97 -0
  418. package/src/resources/extensions/otto/commands/release-notes/command.ts +16 -3
  419. package/src/resources/extensions/otto/index.ts +29 -6
  420. package/src/resources/extensions/shared/coworker-paths.test.ts +40 -0
  421. package/src/resources/extensions/shared/coworker-paths.ts +10 -0
  422. package/src/resources/extensions/slash-commands/{audit.ts → audit-codebase.ts} +4 -4
  423. package/src/resources/extensions/slash-commands/extension-manifest.json +1 -1
  424. package/src/resources/extensions/slash-commands/index.ts +2 -2
  425. package/src/resources/extensions/subagent/index.ts +9 -0
  426. package/src/resources/extensions/subagent/launch.test.ts +97 -0
  427. package/src/resources/extensions/subagent/launch.ts +42 -5
  428. package/src/resources/extensions/subagent/run-store.ts +3 -1
  429. package/src/resources/extensions/workflow/bootstrap/register-extension.ts +2 -0
  430. package/src/resources/extensions/workflow/bootstrap/register-hooks.ts +10 -0
  431. package/src/resources/extensions/workflow/health-widget-core.ts +1 -1
  432. package/src/resources/extensions/workflow/persona-status.ts +109 -0
  433. package/src/resources/extensions/workflow/tests/auto-recovery.test.ts +34 -0
@@ -0,0 +1,866 @@
1
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { join } from 'node:path';
4
+ import { ChildProcessRuntime } from './child-process-runtime.js';
5
+ import { acquireLock, releaseLock } from './scratchpad-lock.js';
6
+ import { CellArchive } from './cell-archive.js';
7
+ import { projectTree, validateLeafId } from './cell-tree.js';
8
+ import { redactForJournal } from './kernel-bindings.js';
9
+ export class ForkKernelHangError extends Error {
10
+ srcName;
11
+ pid;
12
+ constructor(srcName, pid) {
13
+ super(`fork: source kernel for '${srcName}' (pid ${pid}) did not exit after SIGTERM + SIGKILL. Destination may be partially populated; clean up with /sp remove <dst>.`);
14
+ this.srcName = srcName;
15
+ this.pid = pid;
16
+ this.name = 'ForkKernelHangError';
17
+ }
18
+ }
19
+ const DEFAULT_MAX_LIVE = 8;
20
+ const DEFAULT_IDLE_MS = 600_000;
21
+ const DEFAULT_SWEEP_MS = 30_000;
22
+ const META_SCHEMA_VERSION = 4;
23
+ const MAX_RECOVERY_NOTES = 20;
24
+ const FORK_EXIT_TIMEOUT_MS = 5000;
25
+ function raceWithTimeout(p, ms, label) {
26
+ return new Promise((resolve, reject) => {
27
+ let settled = false;
28
+ const timer = setTimeout(() => {
29
+ if (settled)
30
+ return;
31
+ settled = true;
32
+ reject(new Error(`timeout: ${label}`));
33
+ }, ms);
34
+ timer.unref();
35
+ p.then((v) => { if (settled)
36
+ return; settled = true; clearTimeout(timer); resolve(v); }, (e) => { if (settled)
37
+ return; settled = true; clearTimeout(timer); reject(e); });
38
+ });
39
+ }
40
+ export class ScratchpadManager {
41
+ entries = new Map();
42
+ workspace;
43
+ root;
44
+ maxLive;
45
+ idleMs;
46
+ sessionId;
47
+ now;
48
+ runtimeOptions;
49
+ forkExitTimeoutMs;
50
+ injector;
51
+ audit;
52
+ onDataLoad;
53
+ onArtifactCreate;
54
+ getArtifactStore;
55
+ disposed = false;
56
+ sweepTimer = null;
57
+ constructor(options) {
58
+ this.workspace = options.workspace;
59
+ this.root = options.root ?? join(homedir(), '.otto', 'scratchpads');
60
+ this.maxLive = options.maxLiveKernels ?? DEFAULT_MAX_LIVE;
61
+ this.idleMs = options.idleMs ?? DEFAULT_IDLE_MS;
62
+ this.now = options.now ?? Date.now;
63
+ this.runtimeOptions = options.runtimeOptions ?? {};
64
+ this.sessionId = options.sessionId;
65
+ this.forkExitTimeoutMs = options.forkExitTimeoutMs ?? FORK_EXIT_TIMEOUT_MS;
66
+ this.injector = options.injector;
67
+ this.audit = options.audit;
68
+ this.onDataLoad = options.onDataLoad;
69
+ this.onArtifactCreate = options.onArtifactCreate;
70
+ this.getArtifactStore = options.getArtifactStore;
71
+ this.sweepTimer = setInterval(() => { void this.evictIdle(); }, options.sweepIntervalMs ?? DEFAULT_SWEEP_MS);
72
+ this.sweepTimer.unref();
73
+ }
74
+ dirFor(name) {
75
+ return join(this.root, name);
76
+ }
77
+ metaPath(name) {
78
+ return join(this.dirFor(name), 'meta.json');
79
+ }
80
+ existsOnDisk(name) {
81
+ return existsSync(this.metaPath(name));
82
+ }
83
+ payloadSize(dir) {
84
+ let total = 0;
85
+ for (const f of ['kernel.db', 'kernel.db.wal', 'namespace.json', 'cells.jsonl']) {
86
+ try {
87
+ total += statSync(join(dir, f)).size;
88
+ }
89
+ catch {
90
+ // not present -> skip (no-op contribution)
91
+ }
92
+ }
93
+ return total;
94
+ }
95
+ writeMetaAtomic(path, payload) {
96
+ const tmp = `${path}.tmp`;
97
+ writeFileSync(tmp, JSON.stringify(payload, null, 2));
98
+ renameSync(tmp, path);
99
+ }
100
+ writeMeta(name, initialBindings) {
101
+ const dir = this.dirFor(name);
102
+ const path = this.metaPath(name);
103
+ mkdirSync(dir, { recursive: true });
104
+ const nowIso = new Date(this.now()).toISOString();
105
+ let created_at = nowIso;
106
+ let attached_sessions = [];
107
+ // Phase 2 Task 12: bindings persistence + v3→v4 migration.
108
+ // - On first write (no prev), use the passed-in initialBindings (default []).
109
+ // - On subsequent writes, preserve whatever's on disk (migrating v3 → []).
110
+ let bindings = Array.isArray(initialBindings) ? [...initialBindings] : [];
111
+ const prevExtras = {};
112
+ if (existsSync(path)) {
113
+ try {
114
+ const prev = JSON.parse(readFileSync(path, 'utf8'));
115
+ if (typeof prev.created_at === 'string')
116
+ created_at = prev.created_at;
117
+ if (Array.isArray(prev.attached_sessions))
118
+ attached_sessions = prev.attached_sessions;
119
+ // v3 → v4 migration: bindings field is missing on v3; default to [].
120
+ bindings = Array.isArray(prev.bindings) ? prev.bindings : [];
121
+ for (const k of [
122
+ 'last_snapshot_cell_id', 'last_snapshot_at', 'namespace_skipped', 'recovery_notes',
123
+ 'cell_leaf_id', 'kernel_at_cell_id', 'recovery_notes_seen_at',
124
+ ]) {
125
+ if (k in prev)
126
+ prevExtras[k] = prev[k];
127
+ }
128
+ if (Array.isArray(prevExtras.recovery_notes)) {
129
+ const rn = prevExtras.recovery_notes;
130
+ prevExtras.recovery_notes = rn.slice(Math.max(0, rn.length - MAX_RECOVERY_NOTES));
131
+ }
132
+ }
133
+ catch {
134
+ // corrupt meta -> drop extras + rewrite fresh
135
+ }
136
+ }
137
+ if (this.sessionId && !attached_sessions.includes(this.sessionId)) {
138
+ attached_sessions.push(this.sessionId);
139
+ }
140
+ const archive = this.entries.get(name)?.archive;
141
+ if (archive && archive.leafId !== null) {
142
+ prevExtras.cell_leaf_id = archive.leafId;
143
+ }
144
+ const liveEntry = this.entries.get(name);
145
+ if (liveEntry && liveEntry.kernelAtCellId !== null) {
146
+ prevExtras.kernel_at_cell_id = liveEntry.kernelAtCellId;
147
+ }
148
+ const meta = {
149
+ name,
150
+ created_at,
151
+ last_used: nowIso,
152
+ attached_sessions,
153
+ bindings,
154
+ size_bytes: this.payloadSize(dir),
155
+ schema_version: META_SCHEMA_VERSION,
156
+ ...prevExtras,
157
+ kernel_db: { present: existsSync(join(dir, 'kernel.db')), path: 'kernel.db' },
158
+ namespace: { present: existsSync(join(dir, 'namespace.json')), schema_version: 1 },
159
+ };
160
+ this.writeMetaAtomic(path, meta);
161
+ }
162
+ appendRecoveryNotes(name, notes) {
163
+ if (notes.length === 0)
164
+ return;
165
+ const path = this.metaPath(name);
166
+ if (!existsSync(path))
167
+ return; // no meta yet; nothing to attach notes to
168
+ let cur = {};
169
+ try {
170
+ cur = JSON.parse(readFileSync(path, 'utf8'));
171
+ }
172
+ catch {
173
+ // corrupt meta -> do NOT rewrite as a fragment that destroys other fields.
174
+ // The next successful writeMeta call will re-establish a coherent shape.
175
+ return;
176
+ }
177
+ const prior = Array.isArray(cur.recovery_notes) ? cur.recovery_notes : [];
178
+ const stamped = notes.map((n) => ({ at: new Date(this.now()).toISOString(), ...n }));
179
+ const merged = [...prior, ...stamped];
180
+ cur.recovery_notes = merged.slice(Math.max(0, merged.length - MAX_RECOVERY_NOTES));
181
+ this.writeMetaAtomic(path, cur);
182
+ }
183
+ applySnapshotToMeta(name, entry, res) {
184
+ const path = this.metaPath(name);
185
+ if (!existsSync(path))
186
+ return;
187
+ let cur = {};
188
+ try {
189
+ cur = JSON.parse(readFileSync(path, 'utf8'));
190
+ }
191
+ catch {
192
+ return;
193
+ }
194
+ cur.last_snapshot_cell_id = entry.archive.lastId;
195
+ cur.last_snapshot_at = res.snapshotted_at;
196
+ cur.namespace_skipped = res.skipped;
197
+ cur.namespace = { present: true, schema_version: 1 };
198
+ cur.kernel_db = { present: existsSync(join(this.dirFor(name), 'kernel.db')), path: 'kernel.db' };
199
+ this.writeMetaAtomic(path, cur);
200
+ }
201
+ async snapshotThenDispose(name, entry) {
202
+ const rt = entry.runtime;
203
+ if (!rt)
204
+ return;
205
+ if (rt.hasActiveCell) {
206
+ // An active cell would block the snapshot indefinitely until cellTimeoutMs fires
207
+ // (kernel processes one NDJSON frame at a time). Skip the snapshot and dispose
208
+ // straight away; the next attach will see cells-since-snapshot divergence.
209
+ this.appendRecoveryNotes(name, [{ kind: 'snapshot-failed', message: 'skipped: active cell would block snapshot' }]);
210
+ await rt.dispose();
211
+ if (entry.runtime === rt)
212
+ entry.runtime = null;
213
+ return;
214
+ }
215
+ const res = await rt.snapshot();
216
+ if (res.ok) {
217
+ this.applySnapshotToMeta(name, entry, res);
218
+ }
219
+ else {
220
+ this.appendRecoveryNotes(name, [{ kind: 'snapshot-failed', message: res.error.message }]);
221
+ }
222
+ await rt.dispose();
223
+ // Only null the field if no concurrent caller has already replaced or cleared it.
224
+ if (entry.runtime === rt)
225
+ entry.runtime = null;
226
+ }
227
+ ingestRecoveryNotesOnAttach(name, entry) {
228
+ const notes = [...entry.runtime.recoveryNotes];
229
+ // Divergence: compare archive.lastId to last_snapshot_cell_id on disk.
230
+ const path = this.metaPath(name);
231
+ if (existsSync(path)) {
232
+ try {
233
+ const cur = JSON.parse(readFileSync(path, 'utf8'));
234
+ const last = cur.last_snapshot_cell_id;
235
+ const archiveId = entry.archive.lastId;
236
+ if (typeof last === 'number' && typeof archiveId === 'number' && archiveId > last) {
237
+ notes.push({ kind: 'cells-since-snapshot', n: archiveId - last });
238
+ }
239
+ }
240
+ catch {
241
+ // ignore; covered by the namespace-corrupt note path
242
+ }
243
+ }
244
+ this.appendRecoveryNotes(name, notes);
245
+ }
246
+ restoreLeafOnAttach(name, entry) {
247
+ const path = this.metaPath(name);
248
+ if (!existsSync(path))
249
+ return;
250
+ try {
251
+ const cur = JSON.parse(readFileSync(path, 'utf8'));
252
+ const persisted = cur.cell_leaf_id;
253
+ if (typeof persisted === 'number' && entry.archive.leafId !== persisted) {
254
+ entry.archive.setLeaf(persisted);
255
+ }
256
+ }
257
+ catch {
258
+ // ignore; leaf falls back to file-max (the constructor's default).
259
+ }
260
+ }
261
+ restoreKernelAtCellIdOnAttach(name, entry) {
262
+ // Cold restore: kernel was hydrated from namespace.json, which was written at
263
+ // last_snapshot_cell_id. That's where the in-VM state lives.
264
+ const path = this.metaPath(name);
265
+ if (!existsSync(path)) {
266
+ entry.kernelAtCellId = null;
267
+ return;
268
+ }
269
+ try {
270
+ const cur = JSON.parse(readFileSync(path, 'utf8'));
271
+ const last = cur.last_snapshot_cell_id;
272
+ entry.kernelAtCellId = typeof last === 'number' ? last : null;
273
+ }
274
+ catch {
275
+ entry.kernelAtCellId = null;
276
+ }
277
+ }
278
+ warmCount() {
279
+ let n = 0;
280
+ for (const e of this.entries.values())
281
+ if (e.runtime !== null)
282
+ n++;
283
+ return n;
284
+ }
285
+ /**
286
+ * Phase 2 Task 13: read meta.bindings from disk so each fresh spawn picks up
287
+ * the current binding set (bindings can change between attaches via /sp use).
288
+ * Returns [] if meta.json doesn't exist or doesn't have a v4 bindings array.
289
+ *
290
+ * Phase 2 Task 16: also surfaced as a public read for /sp list rendering and
291
+ * staleness-banner emission. Stays a thin read; callers that need to mutate
292
+ * use addBinding / removeBinding (which atomically RMW meta.json).
293
+ */
294
+ readBindings(name) {
295
+ const path = this.metaPath(name);
296
+ if (!existsSync(path))
297
+ return [];
298
+ try {
299
+ const cur = JSON.parse(readFileSync(path, 'utf8'));
300
+ return Array.isArray(cur.bindings) ? cur.bindings : [];
301
+ }
302
+ catch {
303
+ return [];
304
+ }
305
+ }
306
+ /**
307
+ * Phase 2 Task 16: append a binding ref (e.g. 'jira:prod') to meta.bindings.
308
+ * Idempotent — adding a ref already in the list is a no-op (the meta.json
309
+ * write still happens so callers can detect "added" vs "noop" only via the
310
+ * returned tuple). Atomically rewrites meta.json via writeMetaAtomic, so
311
+ * concurrent writers cannot interleave. Caller is responsible for validating
312
+ * `ref` (sp-command uses LocalDataVault.parseRef before invoking).
313
+ */
314
+ async addBinding(name, ref) {
315
+ this.assertNotDisposed();
316
+ if (!this.existsOnDisk(name))
317
+ throw new Error(`scratchpad not found: ${name}`);
318
+ const path = this.metaPath(name);
319
+ let cur = {};
320
+ try {
321
+ cur = JSON.parse(readFileSync(path, 'utf8'));
322
+ }
323
+ catch { /* corrupt -> overwrite */ }
324
+ const bindings = Array.isArray(cur.bindings) ? [...cur.bindings] : [];
325
+ if (bindings.includes(ref)) {
326
+ return { added: false };
327
+ }
328
+ bindings.push(ref);
329
+ cur.bindings = bindings;
330
+ if (typeof cur.schema_version !== 'number')
331
+ cur.schema_version = META_SCHEMA_VERSION;
332
+ this.writeMetaAtomic(path, cur);
333
+ return { added: true };
334
+ }
335
+ /**
336
+ * Phase 2 Task 16: remove a binding ref from meta.bindings. Returns whether
337
+ * a removal happened so callers can emit "no such binding" if needed.
338
+ */
339
+ async removeBinding(name, ref) {
340
+ this.assertNotDisposed();
341
+ if (!this.existsOnDisk(name))
342
+ throw new Error(`scratchpad not found: ${name}`);
343
+ const path = this.metaPath(name);
344
+ let cur = {};
345
+ try {
346
+ cur = JSON.parse(readFileSync(path, 'utf8'));
347
+ }
348
+ catch { /* corrupt -> overwrite */ }
349
+ const bindings = Array.isArray(cur.bindings) ? [...cur.bindings] : [];
350
+ const idx = bindings.indexOf(ref);
351
+ if (idx < 0) {
352
+ return { removed: false };
353
+ }
354
+ bindings.splice(idx, 1);
355
+ cur.bindings = bindings;
356
+ if (typeof cur.schema_version !== 'number')
357
+ cur.schema_version = META_SCHEMA_VERSION;
358
+ this.writeMetaAtomic(path, cur);
359
+ return { removed: true };
360
+ }
361
+ async spawnRuntime(name) {
362
+ // Phase 3 Task 19: bridge the manager-level onDataLoad (which receives
363
+ // the scratchpad name) to the runtime-level onDataLoad (which doesn't).
364
+ // Closure-bound to `name` here so each spawned runtime tags its drawers
365
+ // with the correct scratchpad even when the manager is shared. If both
366
+ // the manager and runtimeOptions supply an onDataLoad, the manager's
367
+ // wins — runtimeOptions.onDataLoad never had a way to know the name.
368
+ const fanout = this.onDataLoad;
369
+ const onDataLoad = fanout
370
+ ? (drawer) => fanout(drawer, name)
371
+ : this.runtimeOptions.onDataLoad;
372
+ // Phase 4 Task 10: mirror the onDataLoad fan-out pattern for artifact_create.
373
+ // Closure-binds the scratchpad name so multi-pad sessions tag drawers correctly.
374
+ const fanArtifactCreate = this.onArtifactCreate;
375
+ const onArtifactCreate = fanArtifactCreate
376
+ ? (drawer) => fanArtifactCreate(drawer, name)
377
+ : this.runtimeOptions.onArtifactCreate;
378
+ // Phase 4 Task 10: parent-side RPC handlers backed by the ArtifactStore.
379
+ // Both handlers look up the store lazily through getArtifactStore() so the
380
+ // host can toggle availability without re-spawning the kernel. When the
381
+ // store is unavailable we throw — the runtime converts the throw into an
382
+ // `ok:false` response frame so the awaiting cell rejects cleanly.
383
+ const getStore = this.getArtifactStore;
384
+ const handleArtifactCreate = getStore
385
+ ? async (req) => {
386
+ const store = getStore();
387
+ if (!store)
388
+ throw new Error('artifacts unavailable');
389
+ const handle = await store.create(req.kind, req.name);
390
+ return {
391
+ slug: handle.slug,
392
+ uri: handle.uri,
393
+ primary_path: handle.primaryPath,
394
+ };
395
+ }
396
+ : this.runtimeOptions.handleArtifactCreate;
397
+ const handleArtifactUpdate = getStore
398
+ ? async (req) => {
399
+ const store = getStore();
400
+ if (!store)
401
+ throw new Error('artifacts unavailable');
402
+ const handle = await store.get(req.slug);
403
+ if (!handle)
404
+ throw new Error(`unknown artifact: ${req.slug}`);
405
+ return await store.update(handle, req.files);
406
+ }
407
+ : this.runtimeOptions.handleArtifactUpdate;
408
+ const rt = new ChildProcessRuntime({
409
+ workspace: this.workspace,
410
+ scratchpadDir: this.dirFor(name),
411
+ ...this.runtimeOptions,
412
+ onDataLoad,
413
+ onArtifactCreate,
414
+ handleArtifactCreate,
415
+ handleArtifactUpdate,
416
+ // Phase 2 Task 13: env-injection wiring. Injector + bindings are read
417
+ // fresh on every spawn so cold-restarts and re-attaches see the latest
418
+ // bindings list. scratchpadName + sessionId stamp the injector's audit
419
+ // records so /audit can attribute each inject to a scratchpad+session.
420
+ injector: this.injector,
421
+ bindings: this.readBindings(name),
422
+ scratchpadName: name,
423
+ sessionId: this.sessionId ?? '',
424
+ });
425
+ await rt.start();
426
+ return rt;
427
+ }
428
+ async evictLruIfNeeded() {
429
+ while (this.warmCount() >= this.maxLive) {
430
+ let victim = null;
431
+ for (const e of this.entries.values()) {
432
+ if (e.runtime === null)
433
+ continue; // already cold
434
+ if (e.runtime.hasActiveCell)
435
+ continue; // never evict a busy kernel
436
+ if (victim === null || e.lastUsedAt < victim.lastUsedAt)
437
+ victim = e;
438
+ }
439
+ if (victim === null)
440
+ break; // every warm kernel is busy; pool may momentarily exceed (documented)
441
+ // The map key for the LRU victim is needed for snapshotThenDispose; find it now.
442
+ let victimName = null;
443
+ for (const [n, e] of this.entries) {
444
+ if (e === victim) {
445
+ victimName = n;
446
+ break;
447
+ }
448
+ }
449
+ if (victimName === null)
450
+ break; // defensive; should be impossible
451
+ await this.snapshotThenDispose(victimName, victim);
452
+ }
453
+ }
454
+ async create(name, opts = {}) {
455
+ this.assertNotDisposed();
456
+ if (this.entries.has(name) || this.existsOnDisk(name)) {
457
+ throw new Error(`scratchpad ${name} already exists`);
458
+ }
459
+ return this.attachUnmanaged(name, opts);
460
+ }
461
+ async getOrAttach(name, opts = {}) {
462
+ this.assertNotDisposed();
463
+ const existing = this.entries.get(name);
464
+ if (existing) {
465
+ existing.lastUsedAt = this.now();
466
+ if (existing.runtime)
467
+ return existing.runtime;
468
+ await this.evictLruIfNeeded();
469
+ existing.runtime = await this.spawnRuntime(name); // cold -> warm; namespace restored from disk (1d2)
470
+ this.ingestRecoveryNotesOnAttach(name, existing);
471
+ this.restoreLeafOnAttach(name, existing);
472
+ this.restoreKernelAtCellIdOnAttach(name, existing);
473
+ return existing.runtime;
474
+ }
475
+ return this.attachUnmanaged(name, opts);
476
+ }
477
+ async runCell(name, code, opts = {}) {
478
+ this.assertNotDisposed();
479
+ const runtime = await this.getOrAttach(name, opts);
480
+ const entry = this.entries.get(name);
481
+ entry.lastUsedAt = this.now();
482
+ // Phase 2 Task 14: forecast the id the archive will assign to this cell so
483
+ // any secret-scanner audit records carry the same cell_id the journal entry
484
+ // will get. archive.lastId may be null for an empty archive (id 1 is next).
485
+ const nextCellId = (entry.archive.lastId ?? 0) + 1;
486
+ try {
487
+ const result = await runtime.runCell(code);
488
+ // Phase 2 Task 14: live TUI output is UPSTREAM — `result` flows back to
489
+ // the tool unchanged for display. Only the journal copy of stdout is
490
+ // passed through redactForJournal. When no audit is plumbed, redaction
491
+ // is a pass-through (backward compat).
492
+ const journalStdout = this.redactStdout(result.stdout, name, nextCellId);
493
+ entry.archive.append({ code, ok: true, value: result.value, stdout: journalStdout });
494
+ entry.kernelAtCellId = entry.archive.lastId;
495
+ this.writeMeta(name);
496
+ return result;
497
+ }
498
+ catch (err) {
499
+ const e = err;
500
+ try {
501
+ // Phase 2 Task 14: redact the error message too — cell exceptions can
502
+ // embed user data in `e.message`. stdout is empty in this branch.
503
+ const journalErrMsg = this.redactStdout(e.message, name, nextCellId);
504
+ entry.archive.append({ code, ok: false, error: { name: e.name, message: journalErrMsg }, stdout: '' });
505
+ entry.kernelAtCellId = entry.archive.lastId;
506
+ this.writeMeta(name);
507
+ }
508
+ catch {
509
+ // recording the failure must never mask the original cell error
510
+ }
511
+ throw err;
512
+ }
513
+ }
514
+ /**
515
+ * Phase 2 Task 14: redact known-secret patterns from a cell-output string
516
+ * before journaling. No-op (pass-through) when no AuditLog is configured —
517
+ * the manager was constructed in test/legacy mode without vault wiring.
518
+ */
519
+ redactStdout(raw, scratchpadName, cellId) {
520
+ if (!this.audit)
521
+ return raw;
522
+ return redactForJournal(raw, {
523
+ audit: this.audit,
524
+ sessionId: this.sessionId ?? '',
525
+ scratchpadName,
526
+ pid: process.pid,
527
+ cellId: String(cellId),
528
+ });
529
+ }
530
+ /**
531
+ * Task D: Release a warm kernel's process+memory while preserving on-disk state
532
+ * (kernel.db, namespace.json, cells.jsonl, meta.json, lock.json). Cold-restart
533
+ * happens on the next attach.
534
+ *
535
+ * Without --force: refuses if a cell is mid-execution. With --force: cancels the
536
+ * active cell via runtime.cancel() (SIGINT → SIGTERM → SIGKILL escalation handled
537
+ * internally by ChildProcessRuntime). Post-cancel the kernel is dead, so we skip
538
+ * the snapshot — the next attach replays from cells.jsonl.
539
+ */
540
+ async evict(name, opts = {}) {
541
+ this.assertNotDisposed();
542
+ const entry = this.entries.get(name);
543
+ if (!entry || !entry.runtime) {
544
+ throw new Error(`scratchpad ${name} is not warm (already cold)`);
545
+ }
546
+ if (entry.runtime.hasActiveCell) {
547
+ if (!opts.force) {
548
+ throw new Error(`cannot evict ${name}: cell is running (use --force to interrupt)`);
549
+ }
550
+ // --force: cancel via existing SIGINT → SIGTERM → SIGKILL escalation in
551
+ // ChildProcessRuntime.cancel(). After cancel resolves the runtime is dead,
552
+ // so snapshotThenDispose would fail. Skip the snapshot and flip the entry
553
+ // to cold (runtime=null) like snapshotThenDispose would have. The session
554
+ // lock remains held so /sp list still shows the (cold) entry and the next
555
+ // attach cold-restarts from cells.jsonl.
556
+ const rt = entry.runtime;
557
+ await rt.cancel();
558
+ try {
559
+ await rt.dispose();
560
+ }
561
+ catch { /* already dead — best-effort cleanup */ }
562
+ if (entry.runtime === rt)
563
+ entry.runtime = null;
564
+ return { interrupted: true };
565
+ }
566
+ await this.snapshotThenDispose(name, entry);
567
+ return { interrupted: false };
568
+ }
569
+ async setLeaf(name, id) {
570
+ this.assertNotDisposed();
571
+ // Verify the scratchpad exists on disk (works for both warm and cold).
572
+ if (!this.existsOnDisk(name))
573
+ throw new Error(`scratchpad not found: ${name}`);
574
+ // Build a tree from the on-disk cells.jsonl so validation works even when cold.
575
+ const cells = [];
576
+ const cellsPath = join(this.dirFor(name), 'cells.jsonl');
577
+ if (existsSync(cellsPath)) {
578
+ for (const line of readFileSync(cellsPath, 'utf8').split('\n')) {
579
+ if (!line.trim())
580
+ continue;
581
+ try {
582
+ const obj = JSON.parse(line);
583
+ if (typeof obj.id === 'number')
584
+ cells.push(obj);
585
+ }
586
+ catch {
587
+ // header or trailing-corrupt line -> skip
588
+ }
589
+ }
590
+ }
591
+ const tree = projectTree(cells);
592
+ validateLeafId(tree, id);
593
+ // Warm path: update the live archive too.
594
+ const entry = this.entries.get(name);
595
+ if (entry)
596
+ entry.archive.setLeaf(id);
597
+ // Direct meta update so cold scratchpads persist the leaf.
598
+ const path = this.metaPath(name);
599
+ let cur = {};
600
+ try {
601
+ cur = JSON.parse(readFileSync(path, 'utf8'));
602
+ }
603
+ catch { /* fall through */ }
604
+ cur.cell_leaf_id = id;
605
+ // Phase 2 Task 12: bumping schema_version here doubles as a migration step
606
+ // for cold-only flows; ensure bindings exists so the v4 invariant holds.
607
+ if (!Array.isArray(cur.bindings))
608
+ cur.bindings = [];
609
+ cur.schema_version = META_SCHEMA_VERSION;
610
+ this.writeMetaAtomic(path, cur);
611
+ }
612
+ async fork(srcName, dstName) {
613
+ this.assertNotDisposed();
614
+ if (this.entries.has(dstName) || this.existsOnDisk(dstName)) {
615
+ throw new Error(`scratchpad ${dstName} already exists`);
616
+ }
617
+ if (!this.existsOnDisk(srcName)) {
618
+ throw new Error(`scratchpad not found: ${srcName}`);
619
+ }
620
+ // Auto-evict src to release the DuckDB kernel.db handle before we copy.
621
+ // Capture the raw child process reference before disposal so we can await its
622
+ // full exit — ensuring DuckDB flushes and closes kernel.db before copyFileSync.
623
+ const srcEntry = this.entries.get(srcName);
624
+ if (srcEntry && srcEntry.runtime) {
625
+ const rawChild = srcEntry.runtime.child;
626
+ await this.snapshotThenDispose(srcName, srcEntry);
627
+ if (rawChild && rawChild.exitCode === null) {
628
+ const exitPromise = new Promise((resolve) => rawChild.once('exit', () => resolve()));
629
+ try {
630
+ await raceWithTimeout(exitPromise, this.forkExitTimeoutMs, 'exit-after-SIGTERM');
631
+ }
632
+ catch {
633
+ rawChild.kill('SIGKILL');
634
+ const exitPromise2 = new Promise((resolve) => rawChild.once('exit', () => resolve()));
635
+ try {
636
+ await raceWithTimeout(exitPromise2, this.forkExitTimeoutMs, 'exit-after-SIGKILL');
637
+ }
638
+ catch {
639
+ throw new ForkKernelHangError(srcName, rawChild.pid ?? -1);
640
+ }
641
+ }
642
+ }
643
+ }
644
+ const srcDir = this.dirFor(srcName);
645
+ const dstDir = this.dirFor(dstName);
646
+ mkdirSync(dstDir, { recursive: true });
647
+ for (const file of ['kernel.db', 'kernel.db.wal', 'namespace.json', 'cells.jsonl']) {
648
+ if (existsSync(join(srcDir, file))) {
649
+ copyFileSync(join(srcDir, file), join(dstDir, file));
650
+ }
651
+ }
652
+ // Build dst meta inheriting selected fields from src.
653
+ let srcMeta = {};
654
+ try {
655
+ srcMeta = JSON.parse(readFileSync(join(srcDir, 'meta.json'), 'utf8'));
656
+ }
657
+ catch { /* leave empty */ }
658
+ const nowIso = new Date(this.now()).toISOString();
659
+ const dstMeta = {
660
+ name: dstName,
661
+ created_at: nowIso,
662
+ last_used: nowIso,
663
+ attached_sessions: this.sessionId ? [this.sessionId] : [],
664
+ // Phase 2 Task 16: fork inherits src's bindings so the forked scratchpad
665
+ // spawns its kernel with the same OTTO_DS_* env block. Users can
666
+ // /sp unuse on dst afterwards if they want a different binding shape.
667
+ bindings: Array.isArray(srcMeta.bindings) ? [...srcMeta.bindings] : [],
668
+ size_bytes: this.payloadSize(dstDir),
669
+ schema_version: META_SCHEMA_VERSION,
670
+ cell_leaf_id: typeof srcMeta.cell_leaf_id === 'number' ? srcMeta.cell_leaf_id : null,
671
+ last_snapshot_cell_id: typeof srcMeta.last_snapshot_cell_id === 'number' ? srcMeta.last_snapshot_cell_id : null,
672
+ last_snapshot_at: typeof srcMeta.last_snapshot_at === 'string' ? srcMeta.last_snapshot_at : null,
673
+ kernel_at_cell_id: typeof srcMeta.kernel_at_cell_id === 'number'
674
+ ? srcMeta.kernel_at_cell_id
675
+ : (typeof srcMeta.last_snapshot_cell_id === 'number' ? srcMeta.last_snapshot_cell_id : null),
676
+ namespace_skipped: [],
677
+ recovery_notes: [],
678
+ kernel_db: { present: existsSync(join(dstDir, 'kernel.db')), path: 'kernel.db' },
679
+ namespace: { present: existsSync(join(dstDir, 'namespace.json')), schema_version: 1 },
680
+ };
681
+ this.writeMetaAtomic(join(dstDir, 'meta.json'), dstMeta);
682
+ // Claim the new scratchpad for this session by acquiring its lock and registering
683
+ // a cold entry so getOrAttach can re-warm without re-acquiring the lock.
684
+ const dstLock = acquireLock(dstDir, { now: this.now });
685
+ const dstEntry = {
686
+ runtime: null,
687
+ lock: dstLock,
688
+ lastUsedAt: this.now(),
689
+ archive: new CellArchive(dstDir, this.now),
690
+ kernelAtCellId: dstMeta.kernel_at_cell_id,
691
+ };
692
+ this.entries.set(dstName, dstEntry);
693
+ }
694
+ async clearHistory(name) {
695
+ this.assertNotDisposed();
696
+ const entry = this.entries.get(name);
697
+ if (entry?.runtime?.hasActiveCell) {
698
+ throw new Error('cannot clear history while a cell is running');
699
+ }
700
+ if (entry?.archive) {
701
+ entry.archive.reset();
702
+ entry.kernelAtCellId = null;
703
+ }
704
+ else {
705
+ // Cold path: construct a temp archive solely to reuse its truncation logic.
706
+ const tmpArchive = new CellArchive(this.dirFor(name), this.now);
707
+ tmpArchive.reset();
708
+ }
709
+ // Direct meta read-modify-write; we explicitly do NOT route through writeMeta
710
+ // because writeMeta pulls cell_leaf_id from the live archive — which is exactly
711
+ // what we just nulled, but writeMeta would also re-add this.sessionId, which
712
+ // we want preserved untouched here. Safer to read+merge+write directly.
713
+ const path = this.metaPath(name);
714
+ if (existsSync(path)) {
715
+ let cur = {};
716
+ try {
717
+ cur = JSON.parse(readFileSync(path, 'utf8'));
718
+ }
719
+ catch { /* drop */ }
720
+ cur.cell_leaf_id = null;
721
+ cur.last_snapshot_cell_id = null;
722
+ cur.last_snapshot_at = null;
723
+ cur.kernel_at_cell_id = null;
724
+ this.writeMetaAtomic(path, cur);
725
+ }
726
+ }
727
+ async save(name) {
728
+ this.assertNotDisposed();
729
+ const entry = this.entries.get(name);
730
+ if (!entry || !entry.runtime) {
731
+ throw new Error(`scratchpad ${name} is not warm — nothing to save`);
732
+ }
733
+ if (entry.runtime.hasActiveCell) {
734
+ this.appendRecoveryNotes(name, [{ kind: 'snapshot-failed', message: 'active cell' }]);
735
+ throw new Error('cannot save while a cell is running');
736
+ }
737
+ const res = await entry.runtime.snapshot();
738
+ if (res.ok) {
739
+ this.applySnapshotToMeta(name, entry, res);
740
+ }
741
+ else {
742
+ this.appendRecoveryNotes(name, [{ kind: 'snapshot-failed', message: res.error.message }]);
743
+ throw new Error(`save failed: ${res.error.message}`);
744
+ }
745
+ }
746
+ async detach(name, sessionId) {
747
+ this.assertNotDisposed();
748
+ const path = this.metaPath(name);
749
+ if (!existsSync(path))
750
+ return;
751
+ let cur = {};
752
+ try {
753
+ cur = JSON.parse(readFileSync(path, 'utf8'));
754
+ }
755
+ catch {
756
+ return;
757
+ }
758
+ const arr = Array.isArray(cur.attached_sessions) ? cur.attached_sessions : [];
759
+ const idx = arr.indexOf(sessionId);
760
+ if (idx >= 0) {
761
+ cur.attached_sessions = [...arr.slice(0, idx), ...arr.slice(idx + 1)];
762
+ this.writeMetaAtomic(path, cur);
763
+ }
764
+ // Runtime explicitly NOT disposed. Pool LRU/idle eviction owns cleanup.
765
+ }
766
+ async markRecoveryNotesSeen(name) {
767
+ this.assertNotDisposed();
768
+ const path = this.metaPath(name);
769
+ if (!existsSync(path))
770
+ return;
771
+ let cur = {};
772
+ try {
773
+ cur = JSON.parse(readFileSync(path, 'utf8'));
774
+ }
775
+ catch {
776
+ return;
777
+ }
778
+ cur.recovery_notes_seen_at = new Date(this.now()).toISOString();
779
+ this.writeMetaAtomic(path, cur);
780
+ }
781
+ async attachUnmanaged(name, opts) {
782
+ const dir = this.dirFor(name);
783
+ const lock = acquireLock(dir, {
784
+ forceTakeover: opts.forceTakeover,
785
+ takeoverReason: opts.takeoverReason,
786
+ now: this.now,
787
+ });
788
+ // Phase 2 Task 12: opts.bindings is only honored on first create; on re-attach
789
+ // the on-disk bindings field wins via prevExtras in writeMeta.
790
+ this.writeMeta(name, opts.bindings);
791
+ await this.evictLruIfNeeded();
792
+ let runtime;
793
+ try {
794
+ runtime = await this.spawnRuntime(name);
795
+ }
796
+ catch (err) {
797
+ releaseLock(dir); // don't leak the lock if spawn fails
798
+ throw err;
799
+ }
800
+ this.writeMeta(name); // refresh: kernel.db is now on disk; payloadSize + kernel_db.present become accurate (Task E / Issue #2)
801
+ const entry = { runtime, lock, lastUsedAt: this.now(), archive: new CellArchive(dir, this.now), kernelAtCellId: null };
802
+ this.entries.set(name, entry);
803
+ this.ingestRecoveryNotesOnAttach(name, entry);
804
+ this.restoreLeafOnAttach(name, entry);
805
+ this.restoreKernelAtCellIdOnAttach(name, entry);
806
+ return runtime;
807
+ }
808
+ list() {
809
+ return [...this.entries].map(([name, e]) => ({
810
+ name,
811
+ live: e.runtime !== null,
812
+ lastUsedAt: e.lastUsedAt,
813
+ hasActiveCell: e.runtime?.hasActiveCell ?? false,
814
+ }));
815
+ }
816
+ async remove(name) {
817
+ const entry = this.entries.get(name);
818
+ if (entry) {
819
+ await entry.runtime?.dispose();
820
+ this.entries.delete(name);
821
+ }
822
+ rmSync(this.dirFor(name), { recursive: true, force: true }); // deletes lock.json + meta.json
823
+ }
824
+ async evictIdle() {
825
+ if (this.disposed)
826
+ return;
827
+ const cutoff = this.now() - this.idleMs;
828
+ for (const e of this.entries.values()) {
829
+ if (e.runtime === null)
830
+ continue;
831
+ if (e.runtime.hasActiveCell)
832
+ continue; // never evict a busy kernel
833
+ if (e.lastUsedAt <= cutoff) {
834
+ // Find the name for this entry to feed snapshotThenDispose.
835
+ let entryName = null;
836
+ for (const [n, ent] of this.entries) {
837
+ if (ent === e) {
838
+ entryName = n;
839
+ break;
840
+ }
841
+ }
842
+ if (entryName !== null)
843
+ await this.snapshotThenDispose(entryName, e);
844
+ }
845
+ }
846
+ }
847
+ async disposeAll() {
848
+ if (this.disposed)
849
+ return;
850
+ this.disposed = true;
851
+ if (this.sweepTimer) {
852
+ clearInterval(this.sweepTimer);
853
+ this.sweepTimer = null;
854
+ }
855
+ for (const [name, e] of this.entries) {
856
+ if (e.runtime)
857
+ await this.snapshotThenDispose(name, e);
858
+ releaseLock(this.dirFor(name)); // release lock; leave meta.json (durable)
859
+ }
860
+ this.entries.clear();
861
+ }
862
+ assertNotDisposed() {
863
+ if (this.disposed)
864
+ throw new Error('scratchpad manager disposed');
865
+ }
866
+ }