create-walle 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/README.md +4 -0
  2. package/lib/owner-write-safety.js +135 -0
  3. package/lib/public-runtime.js +13 -5
  4. package/lib/release-schema.json +3 -2
  5. package/lib/runtime-closure-policy.json +6 -4
  6. package/package.json +1 -1
  7. package/template/bin/deps-doctor.js +1 -1
  8. package/template/bin/node-bin.sh +11 -0
  9. package/template/coding-task-manager/api-prompts.js +10 -22
  10. package/template/coding-task-manager/bin/claude-hook.js +7 -3
  11. package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
  12. package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
  13. package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
  14. package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
  15. package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
  16. package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
  17. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  18. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  19. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  20. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  21. package/template/coding-task-manager/db.js +705 -286
  22. package/template/coding-task-manager/git-utils.js +44 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
  24. package/template/coding-task-manager/lib/agent-presets.js +15 -0
  25. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  26. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  27. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  28. package/template/coding-task-manager/lib/api/ingress.js +35 -12
  29. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  30. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  31. package/template/coding-task-manager/lib/api/machines.js +461 -0
  32. package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
  33. package/template/coding-task-manager/lib/api/providers.js +53 -6
  34. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  35. package/template/coding-task-manager/lib/api/sessions.js +160 -8
  36. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  37. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  38. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  39. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  40. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  41. package/template/coding-task-manager/lib/auth-context.js +32 -2
  42. package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
  43. package/template/coding-task-manager/lib/auth-rules.js +186 -45
  44. package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
  45. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  46. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  47. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  48. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  49. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  50. package/template/coding-task-manager/lib/client-diag-handler.js +5 -0
  51. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  52. package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
  53. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  54. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  55. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  56. package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
  57. package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
  58. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  59. package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
  60. package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
  61. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  62. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  63. package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
  64. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  65. package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
  66. package/template/coding-task-manager/lib/db-domains.js +21 -2
  67. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  68. package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
  69. package/template/coding-task-manager/lib/feature-manifest.js +1 -1
  70. package/template/coding-task-manager/lib/fs-watcher.js +3 -2
  71. package/template/coding-task-manager/lib/harness-history-transition.js +3 -3
  72. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  73. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  74. package/template/coding-task-manager/lib/headless-term-service.js +55 -17
  75. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  76. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  77. package/template/coding-task-manager/lib/host-handoff.js +51 -13
  78. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  79. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  80. package/template/coding-task-manager/lib/host-stats.js +328 -0
  81. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  82. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  83. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  84. package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
  85. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
  86. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  87. package/template/coding-task-manager/lib/instance-manifest.js +13 -1
  88. package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
  89. package/template/coding-task-manager/lib/launch-model-override.js +160 -1
  90. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  91. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
  92. package/template/coding-task-manager/lib/lead-model-switch.js +3 -2
  93. package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
  94. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  95. package/template/coding-task-manager/lib/login-tripwire.js +1 -0
  96. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  97. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  98. package/template/coding-task-manager/lib/machine-registry.js +749 -0
  99. package/template/coding-task-manager/lib/machine-self.js +66 -0
  100. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +52 -4
  101. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  102. package/template/coding-task-manager/lib/media-journal.js +84 -27
  103. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  104. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  105. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  106. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  107. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  108. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  109. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  110. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  111. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  112. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  113. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  114. package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
  115. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  116. package/template/coding-task-manager/lib/native-history-control.js +2 -2
  117. package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
  118. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  119. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
  120. package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
  121. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  122. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  123. package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
  124. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  125. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  126. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  127. package/template/coding-task-manager/lib/read-pool-client.js +5 -2
  128. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  129. package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
  130. package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
  131. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  132. package/template/coding-task-manager/lib/remote-command-runtime.js +13 -1
  133. package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
  134. package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
  135. package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
  136. package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
  137. package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
  138. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  139. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  140. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  141. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  142. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  143. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  144. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  145. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  146. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  147. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  148. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  149. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  150. package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
  151. package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
  152. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  153. package/template/coding-task-manager/lib/root-prompt-admission.js +33 -2
  154. package/template/coding-task-manager/lib/root-prompt-flow.js +167 -33
  155. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  156. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  157. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  158. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  159. package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
  160. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  161. package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
  162. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  163. package/template/coding-task-manager/lib/session-create/provider-process.js +34 -37
  164. package/template/coding-task-manager/lib/session-create/response.js +30 -12
  165. package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
  166. package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
  167. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  168. package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
  169. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  170. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  171. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  172. package/template/coding-task-manager/lib/session-host-manager.js +152 -206
  173. package/template/coding-task-manager/lib/session-jobs.js +17 -9
  174. package/template/coding-task-manager/lib/session-lifecycle.js +18 -0
  175. package/template/coding-task-manager/lib/session-model-action-preconditions.js +9 -0
  176. package/template/coding-task-manager/lib/session-resource-sampler.js +19 -9
  177. package/template/coding-task-manager/lib/session-standup.js +67 -4
  178. package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
  179. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  180. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  181. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  182. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  183. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  184. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
  185. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  186. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  187. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  188. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  189. package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
  190. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  191. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  192. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  193. package/template/coding-task-manager/lib/transcript-source.js +8 -3
  194. package/template/coding-task-manager/lib/transcript-store.js +40 -0
  195. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  196. package/template/coding-task-manager/lib/typing-interaction-trace.js +92 -0
  197. package/template/coding-task-manager/lib/typing-worker-stages.js +60 -0
  198. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  199. package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
  200. package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
  201. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  202. package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
  203. package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
  204. package/template/coding-task-manager/lib/ws-router.js +68 -1
  205. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  206. package/template/coding-task-manager/oauth-proxy.js +99 -55
  207. package/template/coding-task-manager/package-lock.json +24 -0
  208. package/template/coding-task-manager/package.json +3 -0
  209. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  210. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  211. package/template/coding-task-manager/public/css/app.css +279 -1
  212. package/template/coding-task-manager/public/css/reviews.css +4 -0
  213. package/template/coding-task-manager/public/index.html +294 -223
  214. package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
  215. package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
  216. package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
  217. package/template/coding-task-manager/public/js/background-work.js +13 -7
  218. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
  219. package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
  220. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  221. package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
  222. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  223. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  224. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  225. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  226. package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
  227. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  228. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -8
  229. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  230. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  231. package/template/coding-task-manager/public/js/reviews.js +7 -3
  232. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  233. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  234. package/template/coding-task-manager/public/js/row-join.js +4 -2
  235. package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
  236. package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
  237. package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
  238. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  239. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  240. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  241. package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
  242. package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
  243. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  244. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  245. package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
  246. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  247. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  248. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -1
  249. package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
  250. package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
  251. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  252. package/template/coding-task-manager/public/m/app.css +226 -8
  253. package/template/coding-task-manager/public/m/app.js +502 -9
  254. package/template/coding-task-manager/public/m/index.html +38 -1
  255. package/template/coding-task-manager/public/m/machines.js +1313 -0
  256. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  257. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  258. package/template/coding-task-manager/public/m/sw.js +5 -0
  259. package/template/coding-task-manager/queue-engine.js +13 -0
  260. package/template/coding-task-manager/server/approvals.js +5 -6
  261. package/template/coding-task-manager/server/boot.js +136 -17
  262. package/template/coding-task-manager/server.js +902 -239
  263. package/template/coding-task-manager/session-integrity.js +7 -9
  264. package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
  265. package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
  266. package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
  267. package/template/coding-task-manager/workers/session-host-process.js +14 -3
  268. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  269. package/template/coding-task-manager/workers/worktree-worker.js +34 -0
  270. package/template/package-lock.json +2 -2
  271. package/template/package.json +1 -1
  272. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  273. package/template/shared/conversation-storage-v2-contract.js +42 -6
  274. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  275. package/template/shared/git-remote-key.js +83 -0
  276. package/template/shared/host-identity.js +98 -2
  277. package/template/shared/owner-write-safety.js +135 -0
  278. package/template/shared/public-runtime.js +13 -5
  279. package/template/shared/release-schema.json +3 -2
  280. package/template/shared/report-citations.js +14 -0
  281. package/template/shared/sqlite-write-lock.js +361 -223
  282. package/template/shared/storage/contract.js +35 -3
  283. package/template/wall-e/agent.js +21 -0
  284. package/template/wall-e/api-walle.js +5 -0
  285. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  286. package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
  287. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  288. package/template/wall-e/brain/migrations.js +51 -22
  289. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  290. package/template/wall-e/brain/owner-call-whitelist.js +16 -0
  291. package/template/wall-e/brain/schema-ensures.js +22 -0
  292. package/template/wall-e/brain.js +1 -1
  293. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  294. package/template/wall-e/coding/session-journal.js +28 -40
  295. package/template/wall-e/coding-orchestrator.js +23 -26
  296. package/template/wall-e/http/auth.js +18 -1
  297. package/template/wall-e/http/blog-views-api.js +318 -0
  298. package/template/wall-e/http/helper-rules-api.js +321 -0
  299. package/template/wall-e/http/model-proxy-providers.js +3 -1
  300. package/template/wall-e/http/telemetry-api.js +12 -0
  301. package/template/wall-e/lib/blog-view-policy.js +88 -0
  302. package/template/wall-e/lib/blog-view-store.js +169 -0
  303. package/template/wall-e/lib/client-config-lock.js +2 -1
  304. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  305. package/template/wall-e/lib/error-kinds.js +10 -1
  306. package/template/wall-e/lib/feature-manifest.js +1 -1
  307. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  308. package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
  309. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  310. package/template/wall-e/lib/helper-rules/grade.js +277 -0
  311. package/template/wall-e/lib/helper-rules/label.js +247 -0
  312. package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
  313. package/template/wall-e/lib/helper-rules/store.js +667 -0
  314. package/template/wall-e/lib/mcp-integration.js +5 -6
  315. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  316. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
  317. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  318. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  319. package/template/wall-e/llm/default-fallback.js +4 -0
  320. package/template/wall-e/llm/model-catalog.js +13 -4
  321. package/template/wall-e/llm/model-roles.js +6 -0
  322. package/template/wall-e/llm/portkey-http.js +150 -0
  323. package/template/wall-e/llm/portkey-sync.js +15 -145
  324. package/template/wall-e/llm/worker-mcp-routing.js +61 -0
  325. package/template/wall-e/llm/worker-route.js +43 -1
  326. package/template/wall-e/loops/brain-custom-writes.js +45 -0
  327. package/template/wall-e/mcp-protocol-contract.js +41 -1
  328. package/template/wall-e/mcp-server.js +13 -2
  329. package/template/wall-e/mcp-worker-schemas.js +20 -2
  330. package/template/wall-e/memory/ctm-context-client.js +25 -5
  331. package/template/wall-e/shared/release-schema.json +3 -2
  332. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  333. package/template/wall-e/shared/storage/contract.js +35 -3
  334. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  335. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
  336. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
  337. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
  338. package/template/wall-e/tools/local-tools.js +6 -4
  339. package/template/wall-e/workers/brain-owner-worker.js +9 -0
  340. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  341. package/template/wall-e/workers/telemetry-worker.js +8 -0
@@ -1,17 +1,21 @@
1
1
  'use strict';
2
2
 
3
- // Session transcript archiving (design: session-transcript-storage-resume-from-database). Idle transcripts
4
- // of every provider are discovered from disk at archive time, compressed and verified, their originals
5
- // moved to the transcript trash, and the trash purged after a grace period; a unit can be restored from
6
- // the trash or from the archive. Runs only inside workers/transcript-archive-worker.js (or a test): it
7
- // does filesystem work and hashing, and reaches the manifest table only through an injected async
8
- // `store` (readRows / listByState / rowsForUnit / applyArchive / ctmSessionIds / ...), which the worker
9
- // forwards to the db-owner op `transcriptManifestOp`. Nothing here opens a database or reads process.env.
3
+ // Session transcript archiving (design: transcript-never-removed-honest-resume-card-2026-10-24). Idle
4
+ // transcripts of every provider are discovered from disk at archive time, compressed, verified and kept as
5
+ // an ADDITIVE backup copy; a unit can be restored from the archive (or from a legacy trash entry). Runs only
6
+ // inside workers/transcript-archive-worker.js (or a test): it does filesystem work and hashing, and reaches
7
+ // the manifest table only through an injected async `store` (readRows / listByState / rowsForUnit /
8
+ // applyArchive / ctmSessionIds / ...), which the worker forwards to the db-owner op `transcriptManifestOp`.
9
+ // Nothing here opens a database or reads process.env.
10
10
  //
11
- // Rules this file keeps:
12
- // - a file is never modified, and never deleted unless a verified archive of exactly that version exists
13
- // (archiveUnit / purgeGraceExpired / scrub / recoverAtBoot, below); a failure at any step leaves the
14
- // original where it is;
11
+ // Rules this file keeps (user ruling 2026-10-24: "ctm should never ... remove transcript"):
12
+ // - an original under a provider transcript root is NEVER moved, renamed, truncated, modified or deleted
13
+ // by this file (D-1); the archiver only reads originals;
14
+ // - every byte CTM writes lands under <dataDir>/transcript-archive/<provider>/<sha[0:2]>/<sha256>.zst,
15
+ // with temp files under <dataDir>/transcript-archive/.incoming/ (D-3, D-4) -- never beside an original;
16
+ // - `archived` is terminal: the recorded version is in place, or the provider removed it and a verified
17
+ // copy of exactly that version exists (D-5). The states `original_trashed` and `cold` are read-only
18
+ // legacy values that no code path here writes any more;
15
19
  // - nothing is copied or linked before a unit is archived: a transcript deleted from disk before then
16
20
  // leaves no copy behind, and no manifest row either;
17
21
  // - sha256 is computed by streaming, once, in the compress pass.
@@ -22,7 +26,6 @@ const path = require('node:path');
22
26
  const crypto = require('node:crypto');
23
27
  const { getDataDir } = require('./data-dir');
24
28
  const { compressToFrames, verifyArchive, decodeFrame } = require('./transcript-archive-codec');
25
- const { trashSessionArtifacts, findTrashedTranscript } = require('./session-transcript-trash');
26
29
  const { RESTORED_GRACE_MS } = require('./transcript-source-identity');
27
30
  const { digestPassForMember } = require('./session-action-digest');
28
31
  const digestRunner = require('./transcript-digest-runner');
@@ -150,13 +153,13 @@ async function discoverClaudeUnits(opts = {}) {
150
153
  }
151
154
 
152
155
  // ---------------------------------------------------------------------------------------------------
153
- // WP-9: archive (D-16..D-19, D-22), grace purge, scrub (D-23) and boot recovery.
156
+ // WP-1: archive (D-16..D-19, D-22), scrub (D-23) and boot recovery. No grace purge any more.
154
157
  //
155
- // A unit is archived only when EVERY member is eligible; nothing is deleted unless a verified archive
156
- // exists; a failure at any step leaves the original where it is. The order per member is
158
+ // A unit is archived only when EVERY member is eligible; an original is never moved or deleted, at any
159
+ // step: `archived` is the terminal state. The order per member is
157
160
  // 1 planned row -> 2 compress (one pass, also yields sha256) -> 3 fsync tmp -> 4 full decode verify ->
158
- // 5 re-stat the source -> 6 link tmp to its final name, unlink tmp, fsync dir -> 7 row `archived`
159
- // and once every member reached `archived`: 8 move the originals to the transcript trash -> `original_trashed`.
161
+ // 5 re-stat the source -> 6 link tmp to its content-addressed name under the data dir, unlink tmp,
162
+ // fsync dir -> 7 row `archived`, and the original stays exactly where the provider put it.
160
163
  // Every manifest write goes through store.applyArchive.
161
164
  // ---------------------------------------------------------------------------------------------------
162
165
 
@@ -165,12 +168,15 @@ const IDLE_MS = 7 * DAY_MS; // D-18: every member's last write at least this lon
165
168
  const MIN_ARCHIVE_BYTES = 64 * 1024; // D-18: a unit whose members are all smaller is left alone
166
169
  const RESERVE_BYTES = 1 << 30; // D-18: free space >= 2 x expected archive + this
167
170
  const EXPECTED_RATIO = 8; // expected archive size = size / 8 (measured 88.0 GB -> 7.2 GB)
168
- const PURGE_GRACE_MS = 7 * DAY_MS;
169
171
  const SCRUB_MAX_BYTES = 2 * 1024 * 1024 * 1024;
170
- const ARCHIVE_SUBDIR = path.join('transcript-archive', 'claude');
172
+ const ARCHIVE_ROOT_SUBDIR = 'transcript-archive';
173
+ function archiveSubdirFor(provider) { return path.join(ARCHIVE_ROOT_SUBDIR, provider); }
174
+ // Backward-compatible alias for readers that only ever knew the Claude layout (D-4/S-6).
175
+ const ARCHIVE_SUBDIR = archiveSubdirFor('claude');
171
176
  const INCOMING_DIR = '.incoming';
172
177
  const TMP_RE = /\.zst\.tmp-\d+-\d+-\d+$/;
173
- const RESTORABLE_DONE = new Set(['original_trashed', 'cold']);
178
+ // D-5: read-only legacy states. Nothing in this file writes them any more; recovery (WP-2) reads them.
179
+ const LEGACY_REMOVED_STATES = new Set(['original_trashed', 'cold']);
174
180
  const WORK_STATES = new Set(['archived']);
175
181
  // Rows no code writes any more (dev databases only): deleted at the start of a run, no file is touched.
176
182
  const LEGACY_STATES = ['pinned', 'survivor', 'lost'];
@@ -190,7 +196,8 @@ function _archiveContext(opts) {
190
196
  ctx.minBytes = Number.isFinite(Number(opts.minBytes)) && opts.minBytes !== undefined ? Number(opts.minBytes) : MIN_ARCHIVE_BYTES;
191
197
  ctx.reserveBytes = Number.isFinite(Number(opts.reserveBytes)) && opts.reserveBytes !== undefined ? Number(opts.reserveBytes) : RESERVE_BYTES;
192
198
  ctx.failpoint = typeof opts.failpoint === 'function' ? opts.failpoint : null;
193
- ctx.archiveRoot = path.join(ctx.dataDir, ARCHIVE_SUBDIR);
199
+ ctx.archiveRoot = path.join(ctx.dataDir, ARCHIVE_ROOT_SUBDIR);
200
+ // D-5: the archiver never moves an original, so nothing here can trash or purge one.
194
201
  ctx.counters = { archived: 0, bytesIn: 0, bytesOut: 0, trashed: 0, purged: 0, verified: 0, corrupt: 0, skipped: {} };
195
202
  return ctx;
196
203
  }
@@ -212,11 +219,6 @@ async function _step(ctx, n, info) {
212
219
  }
213
220
  }
214
221
 
215
- function _rootsForTrash(ctx) {
216
- const unset = (name) => path.join(ctx.dataDir, `.unset-root-${name}`);
217
- return [ctx.projectsRoot, ctx.walleSessionsDir || unset('walle'), ctx.codexSessionsDir || unset('codex')];
218
- }
219
-
220
222
  function _under(root, p) {
221
223
  const rel = path.relative(root, p);
222
224
  return Boolean(rel) && !rel.startsWith('..') && !path.isAbsolute(rel);
@@ -330,18 +332,15 @@ async function _publish(ctx, tmp, finalWanted, res, contentAddressed) {
330
332
  throw Object.assign(new Error('archive path is occupied by different bytes'), { code: 'archive_path_occupied' });
331
333
  }
332
334
 
335
+ // D-4: every provider's archive object is content-addressed under CTM's data dir, never beside the source.
333
336
  function _finalPathFor(ctx, unit, member, res) {
334
- if (unit.provider === 'claude') {
335
- return path.join(ctx.archiveRoot, res.sha256.slice(0, 2), `${res.sha256}.zst`);
336
- }
337
- return `${member.sourcePath}.zst`;
337
+ return path.join(ctx.dataDir, archiveSubdirFor(unit.provider), res.sha256.slice(0, 2), `${res.sha256}.zst`);
338
338
  }
339
339
 
340
340
  function _tmpPathFor(ctx, unit, member) {
341
341
  tmpSeq += 1;
342
342
  const tag = `.zst.tmp-${process.pid}-${ctx.now()}-${tmpSeq}`;
343
- if (unit.provider === 'claude') return path.join(ctx.archiveRoot, INCOMING_DIR, `${path.basename(member.sourcePath)}${tag}`);
344
- return `${member.sourcePath}${tag}`;
343
+ return path.join(ctx.dataDir, ARCHIVE_ROOT_SUBDIR, INCOMING_DIR, `${path.basename(member.sourcePath)}${tag}`);
345
344
  }
346
345
 
347
346
  // Steps 1..7 for one member. Returns { row } (state archived) or { aborted: reason }.
@@ -355,7 +354,7 @@ async function _archiveMember(ctx, unit, member, track) {
355
354
  await _step(ctx, 1, { member });
356
355
 
357
356
  const tmp = _tmpPathFor(ctx, unit, member);
358
- if (unit.provider === 'claude') await ctx.fsp.mkdir(path.dirname(tmp), { recursive: true });
357
+ await ctx.fsp.mkdir(path.dirname(tmp), { recursive: true });
359
358
  track.tmp = tmp;
360
359
  const digest = digestPassForMember(unit, member, size); // D-24: the compress pass is also the digest pass
361
360
  let res;
@@ -368,7 +367,7 @@ async function _archiveMember(ctx, unit, member, track) {
368
367
  }
369
368
  await _step(ctx, 2, { member, tmp });
370
369
 
371
- if (unit.provider !== 'claude') await ctx.fsp.utimes(tmp, st0.atime, st0.mtime); // Codex orders rollouts by mtime
370
+ if (unit.provider !== 'claude') await ctx.fsp.utimes(tmp, st0.atime, st0.mtime); // Codex orders rollouts by mtime (the archive copy only)
372
371
  await _fsyncFile(ctx, tmp);
373
372
  await _step(ctx, 3, { member, tmp });
374
373
 
@@ -382,13 +381,10 @@ async function _archiveMember(ctx, unit, member, track) {
382
381
  return { aborted: 'source_changed' };
383
382
  }
384
383
  await _step(ctx, 5, { member, tmp });
385
- if (digest) await ctx.store.replaceDigest(member.sourcePath, digest.finish()); // fail closed: no digest, no trash
384
+ if (digest) await ctx.store.replaceDigest(member.sourcePath, digest.finish()); // fail closed: no digest, no archive
386
385
 
387
386
  const wanted = _finalPathFor(ctx, unit, member, res);
388
- if (unit.provider !== 'claude' && member.row && member.row.archive_path === wanted && member.row.state === 'archived') {
389
- await _unlinkIfPresent(ctx, wanted); // our own stale archive of an older version of this source
390
- }
391
- const final = await _publish(ctx, tmp, wanted, res, unit.provider === 'claude');
387
+ const final = await _publish(ctx, tmp, wanted, res, true);
392
388
  await _unlinkIfPresent(ctx, tmp);
393
389
  track.tmp = '';
394
390
  await _fsyncDir(ctx, path.dirname(final));
@@ -429,23 +425,6 @@ function _claudeProjectDir(ctx, members) {
429
425
  return path.join(ctx.projectsRoot, rel.split(path.sep)[0]);
430
426
  }
431
427
 
432
- // An `archived` row whose original is gone: the crash window between the trash move and step 8's row
433
- // write. Looks the original up in the trash manifests; '' when it cannot be proven to be there.
434
- async function _findInTrash(ctx, member, unitId) {
435
- if (!ctx.trashRoot) return '';
436
- const names = new Set([path.basename(member.sourcePath)]);
437
- if (unitId) names.add(unitId);
438
- for (const fileName of names) {
439
- const hit = await findTrashedTranscript({ trashRoot: ctx.trashRoot, fileName, fs: ctx.fsp });
440
- if (!hit) continue;
441
- const src = member.sourcePath;
442
- if (hit.originalPath !== src && !src.startsWith(hit.originalPath + path.sep)) continue;
443
- const trashPath = hit.trashPath + src.slice(hit.originalPath.length);
444
- if (await _statOrNull(ctx, trashPath)) return trashPath;
445
- }
446
- return '';
447
- }
448
-
449
428
  async function _loadMembers(ctx, unit) {
450
429
  if (unit.provider === 'claude') {
451
430
  // The members found on disk, joined with their manifest rows, plus the rows of the unit whose file is not
@@ -462,7 +441,7 @@ async function _loadMembers(ctx, unit) {
462
441
  return unit.members.map((m) => ({ ...m, row: rows.get(m.sourcePath) || null }));
463
442
  }
464
443
 
465
- // D-18 for one unit. Returns { skip } or { work: members still to be archived / trashed, total }.
444
+ // D-18 for one unit. Returns { skip } or { work: members still to be archived, total }.
466
445
  async function _eligibility(ctx, unit, members) {
467
446
  if (!ctx.isPrimary) return { skip: 'not_primary' };
468
447
  if (ctx.live.has(String(unit.ctmSessionId)) || ctx.live.has(String(unit.unitId))) return { skip: 'live_session' };
@@ -471,8 +450,16 @@ async function _eligibility(ctx, unit, members) {
471
450
  const work = [];
472
451
  for (const member of members) {
473
452
  const state = member.row ? member.row.state : '';
474
- if (RESTORABLE_DONE.has(state)) continue;
453
+ // D-5: a unit holding a legacy removed row is left entirely alone -- WP-2 restores it, never this file.
454
+ if (LEGACY_REMOVED_STATES.has(state)) return { skip: 'awaiting_recovery' };
475
455
  if (state && !WORK_STATES.has(state) && state !== 'planned') return { skip: 'unit_not_archivable' };
456
+ if (state === 'archived') {
457
+ const st = await _statOrNull(ctx, member.sourcePath);
458
+ if (!st) continue; // D-6: the provider removed it; the archive is the only copy and rung `restore` serves it
459
+ member.stat = st;
460
+ if (_sameSource(st, member.row)) continue; // D-5: the recorded version is in place -- done
461
+ // D-6: the original changed (resumed/appended) -- re-archive it and replace the row below.
462
+ }
476
463
  work.push(member);
477
464
  }
478
465
  if (!work.length) return { skip: 'already_archived' };
@@ -480,17 +467,13 @@ async function _eligibility(ctx, unit, members) {
480
467
  let large = false;
481
468
  const now = ctx.now();
482
469
  for (const member of work) {
483
- const st = await _statOrNull(ctx, member.sourcePath);
484
- if (!st) {
485
- if (member.row && member.row.state === 'archived') { member.missing = true; continue; }
486
- return { skip: 'member_missing' };
487
- }
470
+ const st = member.stat || await _statOrNull(ctx, member.sourcePath);
471
+ if (!st) return { skip: 'member_missing' };
488
472
  if (now - _mtimeOf(st) < ctx.idleMs) return { skip: 'not_idle' };
489
473
  member.stat = st;
490
474
  total += Number(st.size);
491
475
  if (Number(st.size) >= ctx.minBytes) large = true;
492
476
  }
493
- if (work.every((m) => m.missing)) return { work, total };
494
477
  if (!large) return { skip: 'too_small' };
495
478
  if (unit.provider === 'claude') {
496
479
  const projectDir = _claudeProjectDir(ctx, members);
@@ -499,7 +482,7 @@ async function _eligibility(ctx, unit, members) {
499
482
  const onDisk = await _enumerateClaudeUnit(ctx, projectDir, unit.unitId);
500
483
  if (onDisk.some((p) => !known.has(p))) return { skip: 'unit_changed' };
501
484
  }
502
- const jsonl = work.filter((m) => !m.missing && /\.jsonl$/.test(m.sourcePath) && (m.memberKind === 'main' || m.memberKind === 'subagent'));
485
+ const jsonl = work.filter((m) => /\.jsonl$/.test(m.sourcePath) && (m.memberKind === 'main' || m.memberKind === 'subagent'));
503
486
  if (jsonl.length) {
504
487
  const cursors = await ctx.store.importCursors(jsonl.map((m) => m.sourcePath));
505
488
  for (const member of jsonl) {
@@ -508,93 +491,14 @@ async function _eligibility(ctx, unit, members) {
508
491
  }
509
492
  }
510
493
  if (typeof ctx.fsp.statfs === 'function') {
511
- const probe = unit.provider === 'claude' ? ctx.dataDir : path.dirname(work[0].sourcePath);
512
- const fsStat = await ctx.fsp.statfs(probe);
494
+ // D-4: the archive (and its temp) always lands on the data dir's filesystem, whatever the provider.
495
+ const fsStat = await ctx.fsp.statfs(ctx.dataDir);
513
496
  const free = Number(fsStat.bavail) * Number(fsStat.bsize);
514
497
  if (free < 2 * Math.ceil(total / EXPECTED_RATIO) + ctx.reserveBytes) return { skip: 'low_disk' };
515
498
  }
516
499
  return { work, total };
517
500
  }
518
501
 
519
- function _trashPathFor(entries, sourcePath) {
520
- for (const entry of entries) {
521
- if (entry.originalPath === sourcePath) return entry.trashPath;
522
- if (sourcePath.startsWith(entry.originalPath + path.sep)) return entry.trashPath + sourcePath.slice(entry.originalPath.length);
523
- }
524
- return '';
525
- }
526
-
527
- // Step 8 for every member whose row is `archived` with the original still in place.
528
- async function _trashUnit(ctx, unit, members) {
529
- const archived = [];
530
- for (const member of members) {
531
- if (member.row && member.row.state === 'archived' && await _statOrNull(ctx, member.sourcePath)) archived.push(member);
532
- }
533
- if (!archived.length) return { trashed: 0 };
534
- // Nothing moves unless every original is exactly the version that was archived.
535
- const changed = [];
536
- for (const member of archived) {
537
- if (!_sameSource(await _statOrNull(ctx, member.sourcePath), member.row)) changed.push(member);
538
- }
539
- if (changed.length) {
540
- for (const member of changed) {
541
- const change = _revertChange(ctx, unit, member);
542
- await ctx.store.applyArchive(change);
543
- }
544
- _skip(ctx, 'source_changed');
545
- return { trashed: 0, skipped: 'source_changed' };
546
- }
547
- const paths = [];
548
- let unitDir = '';
549
- if (unit.provider === 'claude') {
550
- const projectDir = _claudeProjectDir(ctx, members);
551
- const known = new Set(members.map((m) => m.sourcePath));
552
- const onDisk = await _enumerateClaudeUnit(ctx, projectDir, unit.unitId);
553
- if (onDisk.some((p) => !known.has(p))) {
554
- _skip(ctx, 'unit_changed');
555
- return { trashed: 0, skipped: 'unit_changed' };
556
- }
557
- const dir = path.join(projectDir, unit.unitId);
558
- if (await _statOrNull(ctx, dir)) unitDir = dir;
559
- }
560
- const archivedPaths = new Set(archived.map((m) => m.sourcePath));
561
- for (const member of archived) {
562
- if (unitDir && member.sourcePath.startsWith(unitDir + path.sep)) continue; // moves with its directory
563
- paths.push(member.sourcePath);
564
- }
565
- if (unitDir) {
566
- const hasAnchor = [`${unit.unitId}.jsonl`, `${unit.unitId}.jsonl.bak`].some((n) => archivedPaths.has(path.join(path.dirname(unitDir), n)));
567
- if (hasAnchor) paths.push(unitDir);
568
- else for (const member of archived) if (member.sourcePath.startsWith(unitDir + path.sep)) paths.push(member.sourcePath);
569
- }
570
- const result = await trashSessionArtifacts({
571
- paths,
572
- roots: _rootsForTrash(ctx),
573
- reason: 'archived',
574
- ctmSessionId: unit.ctmSessionId || null,
575
- isPrimary: true,
576
- trashRoot: ctx.trashRoot,
577
- fs: ctx.fsp,
578
- now: () => new Date(ctx.now()),
579
- warn: () => {},
580
- error: (message) => ctx.stats.errors.push({ path: unit.unitId, code: String(message).slice(0, 200) }),
581
- });
582
- await _step(ctx, 8, { unit });
583
- const now = ctx.now();
584
- const upserts = [];
585
- for (const member of archived) {
586
- const trashPath = _trashPathFor(result.entries, member.sourcePath);
587
- if (!trashPath) continue;
588
- upserts.push({ ...member.row, state: 'original_trashed', trash_path: trashPath, state_changed_at_ms: now, last_error: '' });
589
- }
590
- if (upserts.length) await ctx.store.applyArchive({ upserts, deletes: [] });
591
- if (upserts.length < archived.length) {
592
- ctx.stats.errors.push({ path: unit.unitId, code: 'trash_incomplete' });
593
- }
594
- ctx.counters.trashed += upserts.length;
595
- return { trashed: upserts.length };
596
- }
597
-
598
502
  async function _failMember(ctx, unit, track, err) {
599
503
  if (track.tmp) await _unlinkIfPresent(ctx, track.tmp).catch((cleanup) => ctx.stats.errors.push({ path: track.tmp, code: cleanup && cleanup.code }));
600
504
  if (track.member) {
@@ -604,30 +508,18 @@ async function _failMember(ctx, unit, track, err) {
604
508
  }
605
509
 
606
510
  /**
607
- * D-19. Archive one unit and move its originals to the trash. unit = { unitId, provider, ctmSessionId,
511
+ * D-19. Archive one unit as an additive backup. unit = { unitId, provider, ctmSessionId,
608
512
  * members: [{ sourcePath, memberKind }] } (Claude members come from the manifest rows of the unit).
609
- * @returns {{ archived: number, trashed: number, bytesIn: number, bytesOut: number, skipped: string, errors: object[] }}
513
+ * Originals are only ever read: no member is moved, modified or deleted.
514
+ * @returns {{ archived: number, bytesIn: number, bytesOut: number, skipped: string, errors: object[] }}
610
515
  */
611
516
  async function archiveUnit(unit, opts = {}) {
612
517
  const ctx = _archiveContext(opts);
613
- const out = { archived: 0, trashed: 0, bytesIn: 0, bytesOut: 0, skipped: '', errors: [] };
518
+ const out = { archived: 0, bytesIn: 0, bytesOut: 0, skipped: '', errors: [] };
614
519
  const finish = () => { out.errors = ctx.stats.errors.splice(0); return out; };
615
520
  if (!ctx.isPrimary) { out.skipped = _skip(ctx, 'not_primary').skipped; return finish(); }
616
- if (!ctx.trashRoot) throw new TypeError('transcript archiver: opts.trashRoot is required');
617
521
  const members = await _loadMembers(ctx, unit);
618
522
 
619
- // A crash between the trash move and its row write: find the original in the trash and finish the row.
620
- for (const member of members) {
621
- if (!member.row || member.row.state !== 'archived') continue;
622
- if (await _statOrNull(ctx, member.sourcePath)) continue;
623
- const trashPath = await _findInTrash(ctx, member, unit.provider === 'claude' ? unit.unitId : '');
624
- if (trashPath) {
625
- member.row = { ...member.row, state: 'original_trashed', trash_path: trashPath, state_changed_at_ms: ctx.now(), last_error: '' };
626
- await ctx.store.applyArchive({ upserts: [member.row], deletes: [] });
627
- ctx.counters.trashed += 1;
628
- }
629
- }
630
-
631
523
  const eligible = await _eligibility(ctx, unit, members);
632
524
  if (eligible.skip) { out.skipped = _skip(ctx, eligible.skip).skipped; return finish(); }
633
525
 
@@ -635,7 +527,6 @@ async function archiveUnit(unit, opts = {}) {
635
527
  try {
636
528
  for (const member of eligible.work) {
637
529
  track.member = null;
638
- if (member.missing) continue; // archived earlier, original already gone (trash lookup failed): nothing to do
639
530
  const row = member.row;
640
531
  if (row && row.state === 'archived' && _sameSource(member.stat, row) && await _statOrNull(ctx, row.archive_path)) continue; // finished earlier
641
532
  const result = await _archiveMember(ctx, unit, member, track);
@@ -650,9 +541,6 @@ async function archiveUnit(unit, opts = {}) {
650
541
  out.bytesIn += result.bytesIn;
651
542
  out.bytesOut += result.bytesOut;
652
543
  }
653
- const trashed = await _trashUnit(ctx, unit, members);
654
- out.trashed = trashed.trashed;
655
- if (trashed.skipped) out.skipped = trashed.skipped;
656
544
  } catch (err) {
657
545
  if (err && err.failpoint) throw err;
658
546
  await _failMember(ctx, unit, track, err);
@@ -664,7 +552,7 @@ async function archiveUnit(unit, opts = {}) {
664
552
  return finish();
665
553
  }
666
554
 
667
- // ---- purge, scrub ------------------------------------------------------------------------------
555
+ // ---- scrub --------------------------------------------------------------------------------------
668
556
 
669
557
  function _framesOf(row) {
670
558
  try {
@@ -711,44 +599,6 @@ async function _removeEmptyParents(ctx, start, stopAt) {
711
599
  }
712
600
  }
713
601
 
714
- // 7 days after the move: decode + hash the archive again; only then unlink the trash entry (-> `cold`). A failed verify sets `corrupt` and deletes nothing.
715
- async function purgeGraceExpired(opts = {}) {
716
- const ctx = _archiveContext(opts);
717
- const graceMs = Number.isFinite(Number(opts.graceMs)) && opts.graceMs !== undefined ? Number(opts.graceMs) : PURGE_GRACE_MS;
718
- if (!ctx.isPrimary) { _skip(ctx, 'not_primary'); return { purged: 0, corrupt: 0, verified: 0, skipped: { not_primary: 1 }, errors: [] }; }
719
- const out = { purged: 0, corrupt: 0, verified: 0, skipped: {}, errors: [] };
720
- const rows = await ctx.store.listByState({
721
- states: ['original_trashed'], olderThanMs: ctx.now() - graceMs, orderBy: 'changed', limit: Number(opts.limit) || 200,
722
- });
723
- const verify = _verifier(ctx);
724
- for (const row of rows) {
725
- try {
726
- const verdict = await verify(row);
727
- if (!verdict.ok) {
728
- await ctx.store.applyArchive({ upserts: [{ ...row, state: 'corrupt', state_changed_at_ms: ctx.now(), last_error: `purge_${verdict.code}` }], deletes: [] });
729
- out.corrupt += 1;
730
- continue;
731
- }
732
- out.verified += 1;
733
- if (row.trash_path && ctx.trashRoot && _under(ctx.trashRoot, row.trash_path)) {
734
- await _unlinkIfPresent(ctx, row.trash_path);
735
- await _removeEmptyParents(ctx, row.trash_path, ctx.trashRoot);
736
- }
737
- await ctx.store.applyArchive({
738
- upserts: [{ ...row, state: 'cold', trash_path: '', pin_path: '', last_verified_at_ms: ctx.now(), state_changed_at_ms: ctx.now(), last_error: '' }],
739
- deletes: [],
740
- });
741
- out.purged += 1;
742
- } catch (err) {
743
- out.errors.push({ path: row.source_path, code: (err && err.code) || String((err && err.message) || err) });
744
- }
745
- }
746
- ctx.counters.purged += out.purged;
747
- ctx.counters.verified += out.verified;
748
- ctx.counters.corrupt += out.corrupt;
749
- return out;
750
- }
751
-
752
602
  // D-23. Re-verify the least recently verified archives, up to `maxBytes` of archive bytes per run.
753
603
  async function scrub(opts = {}) {
754
604
  const ctx = _archiveContext(opts);
@@ -794,12 +644,13 @@ async function _removeTmpFiles(ctx, dir, out) {
794
644
  }
795
645
 
796
646
  /**
797
- * Removes `*.zst.tmp-*` leftovers in the archive dirs, deletes rows stuck in `planned`, and repairs
798
- * `archived` rows whose archive or original went missing. Never touches an original.
647
+ * Removes `*.zst.tmp-*` leftovers in the archive dirs, deletes rows stuck in `planned`, and repairs rows
648
+ * whose archive went missing (a row is only ever dropped or marked `corrupt`). Never touches an original:
649
+ * D-5, an `archived` row whose original is gone is done, not repaired.
799
650
  */
800
651
  async function recoverAtBoot(opts = {}) {
801
652
  const ctx = _archiveContext(opts);
802
- const out = { tmpRemoved: 0, deleted: 0, archiveMissing: 0, trashRecovered: 0, errors: [] };
653
+ const out = { tmpRemoved: 0, deleted: 0, archiveMissing: 0, errors: [] };
803
654
  if (!ctx.isPrimary) return out;
804
655
  await _removeTmpFiles(ctx, ctx.archiveRoot, out);
805
656
  if (ctx.codexSessionsDir) await _removeTmpFiles(ctx, ctx.codexSessionsDir, out);
@@ -825,12 +676,6 @@ async function recoverAtBoot(opts = {}) {
825
676
  } else {
826
677
  await ctx.store.applyArchive({ upserts: [{ ...row, state: 'corrupt', state_changed_at_ms: now, last_error: 'archive_missing' }], deletes: [] });
827
678
  }
828
- } else if (row.state === 'archived' && !source) {
829
- const trashPath = await _findInTrash(ctx, { sourcePath: row.source_path }, row.provider === 'claude' ? row.unit_id : '');
830
- if (trashPath) {
831
- await ctx.store.applyArchive({ upserts: [{ ...row, state: 'original_trashed', trash_path: trashPath, state_changed_at_ms: now, last_error: '' }], deletes: [] });
832
- out.trashRecovered += 1;
833
- }
834
679
  }
835
680
  } catch (err) {
836
681
  out.errors.push({ path: row.source_path, code: (err && err.code) || String((err && err.message) || err) });
@@ -906,11 +751,11 @@ async function _retireRows(ctx) {
906
751
  }
907
752
 
908
753
  /**
909
- * One archive run (scheduler job transcript-archive): boot recovery (first run only), up to `maxUnits`
910
- * units archived oldest first, the grace purge, and a scrub when `scrub` is set. Every provider's units are
754
+ * One archive run (scheduler job transcript-archive): boot recovery (first run only), the D-7 recovery pass,
755
+ * up to `maxUnits` units archived oldest first, and a scrub when `scrub` is set. Every provider's units are
911
756
  * discovered from disk (Claude by project directory, Codex and Wall-E by path), resumable by the cursors
912
757
  * `claudeAfter` / `codexAfter` / `walleAfter`; a unit not idle for the threshold is skipped. Nothing runs
913
- * while the host is shedding load. Non-primary instances do nothing.
758
+ * while the host is shedding load. Non-primary instances do nothing. Originals are never moved or deleted.
914
759
  */
915
760
  async function runArchiveCycle(opts = {}) {
916
761
  const ctx = _archiveContext(opts);
@@ -919,16 +764,15 @@ async function runArchiveCycle(opts = {}) {
919
764
  if (opts.recover) result.recovered = await recoverAtBoot({ ...opts, _ctx: ctx });
920
765
  if (ctx.hostShedding) { _skip(ctx, 'host_shedding'); return result; }
921
766
  await _retireRows(ctx);
767
+ // D-7: put every original the pre-WP-1 archiver moved away back BEFORE discovery, so a unit restored here is
768
+ // never re-archived in the same run. Its unit budget is its own constant: the archive pass's `maxUnits`
769
+ // (20 per hour, lib/transcript-guard-job.js) must not throttle a recovery that has thousands of files to
770
+ // put back.
771
+ result.restoredOriginals = await restoreRemovedOriginals({ ...opts, _ctx: ctx, maxUnits: RESTORE_MAX_UNITS });
922
772
 
923
773
  const now = ctx.now();
924
774
  const maxUnits = Math.max(1, Number(opts.maxUnits) || 20);
925
775
  const byUnit = new Map();
926
- // An `archived` row whose original is already gone (the crash window before the trash move) is finished from the row.
927
- for (const row of await ctx.store.listByState({ states: ['archived'], mtimeBeforeMs: now - ctx.idleMs, orderBy: 'mtime', limit: 2000 })) {
928
- if (row.provider !== 'claude') continue;
929
- if (!byUnit.has(row.unit_id)) byUnit.set(row.unit_id, { unitId: row.unit_id, provider: 'claude', ctmSessionId: row.ctm_session_id, members: [], mtimeMs: row.source_mtime_ms });
930
- byUnit.get(row.unit_id).members.push({ sourcePath: row.source_path, memberKind: row.member_kind });
931
- }
932
776
  const found = await discoverClaudeUnits({ _ctx: ctx, afterPath: opts.claudeAfter || '', limit: opts.scanLimit });
933
777
  result.nextClaudeAfter = found.nextAfter;
934
778
  for (const unit of found.units) {
@@ -964,13 +808,11 @@ async function runArchiveCycle(opts = {}) {
964
808
  try {
965
809
  const r = await archiveUnit(unit, { ...opts, _ctx: ctx });
966
810
  result.errors.push(...r.errors);
967
- if (r.archived > 0 || r.trashed > 0) archivedUnits += 1;
811
+ if (r.archived > 0) archivedUnits += 1;
968
812
  } catch (err) {
969
813
  result.errors.push({ path: unit.unitId, code: (err && err.code) || String((err && err.message) || err) });
970
814
  }
971
815
  }
972
- const purge = await purgeGraceExpired({ ...opts, _ctx: ctx });
973
- result.errors.push(...purge.errors);
974
816
  if (opts.scrub) {
975
817
  const scrubbed = await scrub({ ...opts, _ctx: ctx, maxBytes: opts.scrubMaxBytes });
976
818
  result.errors.push(...scrubbed.errors);
@@ -1106,7 +948,10 @@ async function _restoreMember(ctx, row) {
1106
948
  if (!await _matchesRow(ctx, target, row)) throw _restoreError('target_exists');
1107
949
  } else {
1108
950
  ({ how, trashPath = '' } = await _placeMember(ctx, row));
1109
- await ctx.fsp.utimes(target, new Date(ctx.now()), new Date(ctx.now()));
951
+ // `now` (default) is today's behaviour; `recorded` puts the manifest's own source_mtime_ms back so a
952
+ // recovery pass can restore a file exactly as the provider last saw it.
953
+ const when = ctx.restoreMtime === 'recorded' ? new Date(Number(row.source_mtime_ms)) : new Date(ctx.now());
954
+ await ctx.fsp.utimes(target, when, when);
1110
955
  await _fsyncDir(ctx, path.dirname(target));
1111
956
  }
1112
957
  const st = await ctx.fsp.stat(target); // number-valued, as the importer's own stat reports
@@ -1136,9 +981,12 @@ async function _restoreMember(ctx, row) {
1136
981
  }
1137
982
 
1138
983
  /** D-20. Restore a unit (id, { unitId } or { sourcePath }); the store needs restampIdentity. Returns
1139
- * { restored: [{path, how}], plain: [path], failed: [{path, code}], skipped } (`plain` = already in place). */
984
+ * { restored: [{path, how}], plain: [path], failed: [{path, code}], skipped } (`plain` = already in place).
985
+ * `opts.mtime` is `'now'` (default; the restored file is stamped with today's time) or `'recorded'` (the
986
+ * row's own source_mtime_ms is put back for both atime and mtime). */
1140
987
  async function restoreUnit(unit, opts = {}) {
1141
988
  const ctx = _archiveContext(opts);
989
+ ctx.restoreMtime = opts.mtime === 'recorded' ? 'recorded' : 'now';
1142
990
  const out = { restored: [], plain: [], failed: [], skipped: '' };
1143
991
  if (!ctx.isPrimary) { out.skipped = 'not_primary'; return out; }
1144
992
  if (typeof ctx.store.restampIdentity !== 'function') throw new TypeError('transcript archiver: store.restampIdentity is required to restore');
@@ -1165,17 +1013,169 @@ async function restoreUnit(unit, opts = {}) {
1165
1013
  return out;
1166
1014
  }
1167
1015
 
1016
+ // ---------------------------------------------------------------------------------------------------
1017
+ // WP-2 / D-7 recovery: put every original the pre-WP-1 archiver moved away back to its `source_path`.
1018
+ // ---------------------------------------------------------------------------------------------------
1019
+
1020
+ const RESTORE_FLOOR_BYTES = 10 * 1024 * 1024 * 1024; // D-7: never decode into the last bytes of a disk
1021
+ const RESTORE_MAX_DECODE_BYTES = 4 * 1024 * 1024 * 1024; // D-7: decoded bytes per call
1022
+ const RESTORE_MAX_UNITS = 200; // D-7: units per call (a trash link-back is cheap)
1023
+
1024
+ // Free bytes on the filesystem that will hold the restored file. An absent directory is resolved to its
1025
+ // nearest existing ancestor: restoreUnit creates the directory (same filesystem, barring a mount point inside
1026
+ // it), so this is the space the decode actually has. null = nothing on the chain could be read.
1027
+ async function _freeBytesAt(ctx, dir) {
1028
+ let probe = dir;
1029
+ for (;;) {
1030
+ try {
1031
+ const st = await ctx.fsp.statfs(probe);
1032
+ return Number(st.bavail) * Number(st.bsize);
1033
+ } catch (err) {
1034
+ if (!err || (err.code !== 'ENOENT' && err.code !== 'ENOTDIR')) throw err;
1035
+ }
1036
+ const up = path.dirname(probe);
1037
+ if (up === probe) return null;
1038
+ probe = up;
1039
+ }
1040
+ }
1041
+
1042
+ // D-9: `row.archive_path` is provably CTM's own legacy sibling copy of this original -- a `.jsonl.zst` beside
1043
+ // the source, outside the data dir, named exactly `<source_path>.zst`, holding the bytes the row recorded.
1044
+ async function _isOwnSiblingArchive(ctx, row) {
1045
+ const p = String(row.archive_path || '');
1046
+ if (!p.endsWith('.jsonl.zst') || p !== `${row.source_path}.zst`) return false;
1047
+ if (_under(ctx.dataDir, p)) return false;
1048
+ if (!row.archive_sha256 || !await _statOrNull(ctx, p)) return false;
1049
+ try {
1050
+ return (await _archiveFileSha(p)) === row.archive_sha256;
1051
+ } catch (err) {
1052
+ if (err && err.code === 'ENOENT') return false; // expected: gone between the stat and the read
1053
+ throw err;
1054
+ }
1055
+ }
1056
+
1057
+ // One unit through restoreUnit, with the counters D-7 reports and the D-9 sibling cleanup after a verified
1058
+ // restore. A failed member never changes another member's outcome.
1059
+ async function _recoverUnit(ctx, unitId, opts, out) {
1060
+ let result;
1061
+ try {
1062
+ result = await restoreUnit({ unitId }, opts);
1063
+ } catch (err) {
1064
+ out.failed.push({ path: String(unitId), code: (err && err.code) || String((err && err.message) || err) });
1065
+ return;
1066
+ }
1067
+ for (const failure of result.failed) {
1068
+ if (failure.code === 'target_exists') out.targetExists.push(failure.path);
1069
+ else out.failed.push(failure);
1070
+ }
1071
+ if (!result.restored.length) return;
1072
+ out.restoredUnits += 1;
1073
+ out.restoredFiles += result.restored.length;
1074
+ const rows = new Map((await ctx.store.rowsForUnit(unitId)).map((r) => [r.source_path, r]));
1075
+ for (const entry of result.restored) {
1076
+ const row = rows.get(entry.path) || {};
1077
+ if (entry.how === 'trash') out.linkedFromTrash += 1;
1078
+ else if (entry.how === 'archive') out.decodedBytes += Number(row.size_bytes) || 0;
1079
+ }
1080
+ for (const entry of result.restored) {
1081
+ const row = rows.get(entry.path);
1082
+ if (!row || row.state !== 'hot') continue;
1083
+ if (!await _isOwnSiblingArchive(ctx, row)) continue;
1084
+ // D-9: the original is verified back in place, so CTM's own sibling copy has no job left. The row keeps
1085
+ // running as a plain `hot` row; the next idle cycle re-archives the unit under D-4.
1086
+ await _unlinkIfPresent(ctx, row.archive_path);
1087
+ await ctx.store.applyArchive({
1088
+ upserts: [{ ...row, archive_path: '', archive_bytes: 0, archive_sha256: '', frames_json: '[]' }],
1089
+ deletes: [],
1090
+ });
1091
+ }
1092
+ }
1093
+
1094
+ /**
1095
+ * D-7. Put every original a pre-WP-1 archiver moved away back to its `source_path`, cheapest source first:
1096
+ * trash entries are hard-linked back (no extra bytes at all), then `cold` units are decoded newest source
1097
+ * first, while the free space left after the decode stays at or above `floorBytes` and the bytes decoded this
1098
+ * call stay under `maxDecodeBytes`. An occupied `source_path` is reported in `targetExists` and left alone,
1099
+ * with its row, its trash copy and its archive untouched. Primary only: a non-primary instance returns
1100
+ * `skipped: 'not_primary'` without making a single filesystem call.
1101
+ * @param {object} opts same context as runArchiveCycle (store, dataDir, trashRoot, isPrimary, fs, now, ...)
1102
+ * plus floorBytes / maxDecodeBytes / maxUnits.
1103
+ * @returns {{ restoredUnits: number, restoredFiles: number, linkedFromTrash: number, decodedBytes: number,
1104
+ * deferredLowDisk: number, targetExists: string[], failed: {path: string, code: string}[], skipped: string }}
1105
+ */
1106
+ async function restoreRemovedOriginals(opts = {}) {
1107
+ const ctx = _archiveContext(opts);
1108
+ const floorBytes = Number.isFinite(Number(opts.floorBytes)) && opts.floorBytes !== undefined ? Number(opts.floorBytes) : RESTORE_FLOOR_BYTES;
1109
+ const maxDecodeBytes = Number.isFinite(Number(opts.maxDecodeBytes)) && opts.maxDecodeBytes !== undefined ? Number(opts.maxDecodeBytes) : RESTORE_MAX_DECODE_BYTES;
1110
+ const maxUnits = Math.max(1, Number(opts.maxUnits) || RESTORE_MAX_UNITS);
1111
+ const out = {
1112
+ restoredUnits: 0, restoredFiles: 0, linkedFromTrash: 0, decodedBytes: 0, deferredLowDisk: 0,
1113
+ targetExists: [], failed: [], skipped: '',
1114
+ };
1115
+ if (!ctx.isPrimary) { out.skipped = 'not_primary'; return out; }
1116
+ const perUnit = { ...opts, _ctx: ctx, mtime: 'recorded' };
1117
+ // Phase A (D-7): a unit with a trashed original, or a corrupt row that still names a trash copy. Cheapest
1118
+ // first -- a link-back costs no bytes -- and oldest state change first.
1119
+ const placed = new Set();
1120
+ const phaseA = [];
1121
+ for (const row of await ctx.store.listByState({ states: ['original_trashed', 'corrupt'], orderBy: 'changed', limit: 2000 })) {
1122
+ if (row.state !== 'original_trashed' && !(row.state === 'corrupt' && row.trash_path)) continue;
1123
+ if (placed.has(row.unit_id)) continue;
1124
+ placed.add(row.unit_id);
1125
+ phaseA.push(row.unit_id);
1126
+ }
1127
+ let done = 0;
1128
+ for (const unitId of phaseA) {
1129
+ if (done >= maxUnits) break;
1130
+ done += 1;
1131
+ await _recoverUnit(ctx, unitId, perUnit, out);
1132
+ }
1133
+ if (done >= maxUnits) return out;
1134
+ // Phase B (D-7): units whose removed rows are all `cold`, newest source first, and only while the disk
1135
+ // keeps `floorBytes` free and this call stays under `maxDecodeBytes`. The byte budget stops only a call
1136
+ // that has already decoded something, so a single unit larger than the whole budget still runs -- alone --
1137
+ // instead of breaking on every call and stalling every unit behind it (the phase B order is deterministic).
1138
+ const phaseB = [];
1139
+ for (const row of await ctx.store.listByState({ states: ['cold'], orderBy: 'mtime_desc', limit: 2000 })) {
1140
+ if (placed.has(row.unit_id)) continue;
1141
+ placed.add(row.unit_id);
1142
+ phaseB.push(row.unit_id);
1143
+ }
1144
+ for (const unitId of phaseB) {
1145
+ if (done >= maxUnits) break;
1146
+ const rows = await ctx.store.rowsForUnit(unitId);
1147
+ if (!rows.length) continue;
1148
+ if (rows.some((r) => r.state === 'original_trashed' || (r.state === 'corrupt' && r.trash_path))) continue; // phase A's unit
1149
+ let need = 0;
1150
+ for (const row of rows) if (!await _statOrNull(ctx, row.source_path)) need += Number(row.size_bytes) || 0;
1151
+ if (typeof ctx.fsp.statfs === 'function') {
1152
+ const free = await _freeBytesAt(ctx, path.dirname(String(rows[0].source_path)));
1153
+ if (free !== null && free - need < floorBytes) { out.deferredLowDisk += 1; break; }
1154
+ }
1155
+ if (out.decodedBytes > 0 && out.decodedBytes + need > maxDecodeBytes) break;
1156
+ done += 1;
1157
+ await _recoverUnit(ctx, unitId, perUnit, out);
1158
+ }
1159
+ return out;
1160
+ }
1161
+
1168
1162
  module.exports = {
1169
1163
  ARCHIVE_SUBDIR,
1164
+ ARCHIVE_ROOT_SUBDIR,
1165
+ archiveSubdirFor,
1166
+ LEGACY_REMOVED_STATES,
1170
1167
  IDLE_MS,
1171
1168
  MIN_ARCHIVE_BYTES,
1169
+ RESTORE_FLOOR_BYTES,
1170
+ RESTORE_MAX_DECODE_BYTES,
1171
+ RESTORE_MAX_UNITS,
1172
1172
  discoverClaudeUnits,
1173
1173
  discoverFlatUnits,
1174
1174
  archiveUnit,
1175
- purgeGraceExpired,
1176
1175
  scrub,
1177
1176
  recoverAtBoot,
1178
1177
  runArchiveCycle, runDigestCycle,
1179
1178
  restoreUnit,
1179
+ restoreRemovedOriginals,
1180
1180
  RESTORED_GRACE_MS,
1181
1181
  };