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
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // WP-2 (docs/reader-claude-table-layout-parity-2026-10-25.html §1.2 the census, §6 the CLI sketch,
5
+ // §8 WP-2, D-7): the census that produced the design's parity floor, kept as a repo tool so a
6
+ // Claude Code upgrade that changes the Markdown-table layout is MEASURED, not guessed (R-8: the
7
+ // port pins one version and there is no runtime switch -- drift is caught here and by the golden
8
+ // fixtures).
9
+ //
10
+ // READ-ONLY. It opens the painted-row stores (`~/.walle/data/painted-rows`) and the provider
11
+ // transcripts (`~/.claude/projects`) read-only: never a database, never a file write. It prints
12
+ // numbers and 8-character session-id prefixes only -- never a painted row, never a cell.
13
+ //
14
+ // node bin/claude-table-parity.js [--painted DIR] [--projects DIR] [--json]
15
+ //
16
+ // For every session directory that has a painted-row store and a transcript: lex every assistant
17
+ // text block with the repo's own `marked`; every GFM `table` token is one table. Locate the drawing
18
+ // (the first body cell of at least 8 characters, its first 24 characters, searched in the painted
19
+ // rows -- one record per stretch file that holds a hit) and re-draw the table with the SHIPPED
20
+ // renderer (`lib/claude-markdown-ansi.emitMarkdownRows` -> `lib/claude-table-layout`) at every
21
+ // `cols` the session's stretch headers recorded. `predicted` is the form that re-draw took (a row
22
+ // carrying `│` means the box), `identical` the most rows that matched the painted window.
23
+ //
24
+ // Exits 0 always -- a missing store, a torn line or an unknown flag is not a census failure.
25
+
26
+ const fs = require('fs');
27
+ const path = require('path');
28
+ const os = require('os');
29
+ const marked = require('marked');
30
+ const { emitMarkdownRows } = require('../lib/claude-markdown-ansi');
31
+
32
+ // The renderer's own style object (lib/claude-transcript-terminal-renderer.js MARKDOWN_STYLE, as
33
+ // its test copies it). It is not exported; the comparison strips SGR anyway, but `cardLines`
34
+ // styles the `Header:` key through it, so the re-draw must carry it.
35
+ const MD_STYLE = Object.freeze({
36
+ bold: [1], boldOff: [22],
37
+ italic: [3], italicOff: [23],
38
+ del: [9], delOff: [29],
39
+ code: [38, 5, 180], codeOff: [39],
40
+ dim: [2],
41
+ });
42
+
43
+ const SGR_RE = /\x1b\[[0-9;?]*[ -\/]*[@-~]/g;
44
+ const CARD_RULE = '─'.repeat(40); // the vertical form's card separator (RULE_MAX)
45
+ const PROBE_MIN = 8; // a probe cell must be at least this long
46
+ const PROBE_LEN = 24; // ... and only its first 24 characters are searched for
47
+ const WINDOW_BEFORE = 40; // painted rows kept before the hit
48
+ const WINDOW_AFTER = 400; // painted rows kept after the hit
49
+
50
+ const stripSgr = (text) => String(text == null ? '' : text).replace(SGR_RE, '');
51
+ const trimEnd = (text) => String(text == null ? '' : text).replace(/\s+$/, '');
52
+
53
+ function parseArgs(argv) {
54
+ const opts = {
55
+ painted: path.join(os.homedir(), '.walle', 'data', 'painted-rows'),
56
+ projects: path.join(os.homedir(), '.claude', 'projects'),
57
+ json: false,
58
+ };
59
+ for (let i = 0; i < argv.length; i += 1) {
60
+ const arg = String(argv[i]);
61
+ const eq = arg.indexOf('=');
62
+ const name = eq < 0 ? arg : arg.slice(0, eq);
63
+ const inline = eq < 0 ? null : arg.slice(eq + 1);
64
+ if (name === '--painted') opts.painted = (inline != null ? inline : argv[i + 1]) || opts.painted;
65
+ else if (name === '--projects') opts.projects = (inline != null ? inline : argv[i + 1]) || opts.projects;
66
+ else if (name === '--json') opts.json = true;
67
+ }
68
+ return opts;
69
+ }
70
+
71
+ // Immediate sub-directory names of `dir`, sorted (deterministic output). A missing root is a
72
+ // legitimate empty census on a machine that never ran the reader.
73
+ function listDirs(dir) {
74
+ let entries = [];
75
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return []; } /* expected: the store/projects root may not exist here */
76
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
77
+ }
78
+
79
+ function readText(file) {
80
+ try { return fs.readFileSync(file, 'utf8'); } catch { return ''; } /* expected: a store file may vanish under a live writer */
81
+ }
82
+
83
+ // One `JSON<TAB>crc` line; the CRC is not verified (the stores are append-only and the census
84
+ // reads numbers, not truth).
85
+ function parseLine(prefix) {
86
+ try { return JSON.parse(prefix); } catch { return null; } // a torn / non-JSON line is not a record
87
+ }
88
+
89
+ function findTranscript(projectsRoot, projectDirs, sessionId) {
90
+ for (const project of projectDirs) {
91
+ const file = path.join(projectsRoot, project, `${sessionId}.jsonl`);
92
+ if (fs.existsSync(file)) return file;
93
+ }
94
+ return null;
95
+ }
96
+
97
+ // Every painted row of one session's store: `{ r, f, cols, t }` with `t` = SGR-stripped,
98
+ // right-trimmed text. `cols` is the width the surrounding stretch header recorded (a row written
99
+ // before the first header of the file keeps `null`).
100
+ function readPainted(dir) {
101
+ const rows = [];
102
+ for (const file of listFiles(dir).filter((f) => /^stretch-\d+\.jsonl$/.test(f)).sort()) {
103
+ let cols = null;
104
+ const text = readText(path.join(dir, file));
105
+ if (!text) continue;
106
+ for (const line of text.split('\n')) {
107
+ const tab = line.lastIndexOf('\t');
108
+ if (tab < 0) continue;
109
+ const record = parseLine(line.slice(0, tab));
110
+ if (!record) continue;
111
+ if (record.stretch != null && record.cols) cols = record.cols;
112
+ if (record.r != null) rows.push({ r: record.r, f: file, cols, t: trimEnd(stripSgr(record.a)) });
113
+ }
114
+ }
115
+ return rows;
116
+ }
117
+
118
+ function listFiles(dir) {
119
+ try { return fs.readdirSync(dir); } catch { return []; } /* expected: the session store may be pruned between readdir and read */
120
+ }
121
+
122
+ // Every `table` token of one assistant text block. The `|` pre-check only skips blocks that cannot
123
+ // hold a GFM table -- the decision is `marked`'s, never a local grammar.
124
+ function lexTables(text) {
125
+ if (!text.includes('|')) return [];
126
+ let tokens = [];
127
+ try { tokens = marked.lexer(text, { gfm: true }); } catch { return []; } // an unlexable block holds no table we can census
128
+ return tokens.filter((token) => token.type === 'table');
129
+ }
130
+
131
+ // A cell's plain text: `label (href)` for a link (R-3 -- the rendering, not the raw markdown, is
132
+ // what the layout measures) and the literal text everywhere else.
133
+ function cellText(tokens) {
134
+ let out = '';
135
+ for (const token of tokens || []) {
136
+ switch (token.type) {
137
+ case 'text': case 'escape': out += token.text; break;
138
+ case 'codespan': out += token.text; break;
139
+ case 'strong': case 'em': case 'del': out += cellText(token.tokens); break;
140
+ case 'link': {
141
+ const label = cellText(token.tokens);
142
+ out += label === token.href ? label : `${label} (${token.href})`;
143
+ break;
144
+ }
145
+ case 'image': out += `${token.text} (${token.href})`; break;
146
+ case 'br': out += ' '; break;
147
+ case 'checkbox': break;
148
+ default: out += token.text != null ? token.text : (token.raw || '');
149
+ }
150
+ }
151
+ return out;
152
+ }
153
+
154
+ // The form the LIVE screen drew, read off the hit row and its neighbours: a row starting with the
155
+ // box border is `bordered`; otherwise a 40-dash card rule or a `Header:` label within
156
+ // `2 * columns + 2` rows of the hit says `cards`; anything else is `other` (a drawing we could not
157
+ // classify -- counted, never predicted against).
158
+ function observedForm(hit, painted, labels) {
159
+ if (/^\s*│/.test(hit.t)) return 'bordered';
160
+ const near = painted
161
+ .filter((p) => p.f === hit.f && p.r >= hit.r - 2 && p.r <= hit.r + 2 * labels.length + 2)
162
+ .map((p) => p.t)
163
+ .join('\n');
164
+ if (near.includes(CARD_RULE) || labels.some((label) => label && near.includes(`${label}:`))) return 'cards';
165
+ return 'other';
166
+ }
167
+
168
+ // The re-draw: the shipped renderer at `cols`, SGR-stripped and right-trimmed, exactly as the
169
+ // painted rows are.
170
+ function renderRows(markdown, cols) {
171
+ return emitMarkdownRows(markdown, cols, { markdown: marked, style: MD_STYLE })
172
+ .map((line) => trimEnd(stripSgr(line)));
173
+ }
174
+
175
+ // The prediction for one drawing: try every width the session's stretch headers recorded, align the
176
+ // emitted rows to the painted window by the offset with the most identical rows, keep the best
177
+ // width. `predicted` is the form the re-draw took (no `│` row => the stacked cards).
178
+ function predict(raw, widths, window) {
179
+ let best = null;
180
+ for (const cols of widths) {
181
+ const lines = renderRows(raw, cols);
182
+ let identical = 0;
183
+ for (let off = 0; off < window.length; off += 1) {
184
+ let same = 0;
185
+ for (let i = 0; i < lines.length; i += 1) if (lines[i] === window[off + i]) same += 1;
186
+ if (same > identical) identical = same;
187
+ }
188
+ const candidate = {
189
+ cols,
190
+ predicted: lines.some((line) => line.includes('│')) ? 'bordered' : 'cards',
191
+ identical,
192
+ emitted: lines.length,
193
+ };
194
+ if (!best || candidate.identical > best.identical) best = candidate;
195
+ }
196
+ return best;
197
+ }
198
+
199
+ // One table token -> its record(s): one per stretch file that holds the probe, or a single
200
+ // `notfound` record when no cell is long enough / no painted row matches.
201
+ function locateTable({ sid, index, token, painted, widths }) {
202
+ const header = (token.header || []).map((cell) => cellText(cell.tokens));
203
+ const rows = (token.rows || []).map((row) => row.map((cell) => cellText(cell.tokens)));
204
+ const base = { sid: sid.slice(0, 8), table: index, columns: header.length, rows: rows.length };
205
+ const probe = (rows[0] || []).find((cell) => cell && cell.length >= PROBE_MIN)
206
+ || header.find((cell) => cell && cell.length >= PROBE_MIN)
207
+ || '';
208
+ if (!probe) return [{ ...base, form: 'notfound', predicted: null, identical: 0, emitted: 0, cols: null }];
209
+ const key = probe.slice(0, PROBE_LEN);
210
+ const hits = new Map(); // file -> the first painted row of that file holding the probe
211
+ for (const row of painted) if (!hits.has(row.f) && row.t.includes(key)) hits.set(row.f, row);
212
+ if (hits.size === 0) return [{ ...base, form: 'notfound', predicted: null, identical: 0, emitted: 0, cols: null }];
213
+ const out = [];
214
+ for (const hit of hits.values()) {
215
+ const window = painted
216
+ .filter((p) => p.f === hit.f && p.r >= hit.r - WINDOW_BEFORE && p.r <= hit.r + WINDOW_AFTER)
217
+ .map((p) => p.t);
218
+ const candidates = widths.length ? widths : (hit.cols ? [hit.cols] : []);
219
+ const best = predict(token.raw, candidates, window) || { cols: hit.cols, predicted: null, identical: 0, emitted: 0 };
220
+ out.push({ ...base, at: hit.r, form: observedForm(hit, painted, header), ...best });
221
+ }
222
+ return out;
223
+ }
224
+
225
+ function census(opts) {
226
+ const storeDirs = listDirs(opts.painted);
227
+ const projectDirs = listDirs(opts.projects);
228
+ const records = [];
229
+ let sessionsWithStore = 0;
230
+ let sessionsPaired = 0;
231
+ let tables = 0;
232
+ for (const sid of storeDirs) {
233
+ sessionsWithStore += 1;
234
+ const transcript = findTranscript(opts.projects, projectDirs, sid);
235
+ if (!transcript) continue;
236
+ sessionsPaired += 1;
237
+ const painted = readPainted(path.join(opts.painted, sid));
238
+ const widths = [...new Set(painted.map((row) => row.cols).filter(Boolean))];
239
+ let index = 0;
240
+ for (const line of readText(transcript).split('\n')) {
241
+ const message = parseLine(line);
242
+ if (!message || !message.message || message.message.role !== 'assistant') continue;
243
+ const parts = Array.isArray(message.message.content)
244
+ ? message.message.content
245
+ : [{ type: 'text', text: message.message.content }];
246
+ for (const part of parts) {
247
+ if (!part || part.type !== 'text' || typeof part.text !== 'string') continue;
248
+ for (const token of lexTables(part.text)) {
249
+ index += 1;
250
+ tables += 1;
251
+ records.push(...locateTable({ sid, index, token, painted, widths }));
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ const observed = { bordered: 0, cards: 0, other: 0, notfound: 0 };
258
+ for (const record of records) observed[record.form] = (observed[record.form] || 0) + 1;
259
+ // `drawings` = the drawings whose form we could read (bordered + cards) -- the denominator of
260
+ // predictedAgree, the number the design's floor (313 of 328, §1.2) is stated against. `other`
261
+ // hits and `notfound` tables are carried in `observed` and never predicted against.
262
+ const drawings = observed.bordered + observed.cards;
263
+ const found = records.filter((record) => record.form === 'bordered' || record.form === 'cards');
264
+ const predictedAgree = found.filter((record) => record.predicted === record.form).length;
265
+ const byteIdentical = found.filter((record) => record.emitted > 0 && record.identical === record.emitted).length;
266
+ const within2 = found.filter((record) => record.emitted > 0 && record.identical >= record.emitted - 2).length;
267
+ return {
268
+ summary: {
269
+ sessionsWithStore,
270
+ sessionsPaired,
271
+ tables,
272
+ drawings,
273
+ observed,
274
+ predictedAgree,
275
+ predictedDisagree: found.length - predictedAgree,
276
+ byteIdentical,
277
+ within2,
278
+ },
279
+ records,
280
+ };
281
+ }
282
+
283
+ function main() {
284
+ const opts = parseArgs(process.argv.slice(2));
285
+ const { summary, records } = census(opts);
286
+ process.stdout.write(`${JSON.stringify(summary)}\n`);
287
+ if (opts.json) return;
288
+ // Disagreement list: the observed form the prediction missed, or a re-draw that lost more than
289
+ // two rows of the painted drawing. Numbers and an 8-character session prefix only -- no cell.
290
+ const disagreements = records
291
+ .filter((record) => record.form !== 'notfound'
292
+ && (record.predicted !== record.form || record.identical < record.emitted - 2))
293
+ .sort((a, b) => (a.sid === b.sid ? a.table - b.table : a.sid.localeCompare(b.sid)));
294
+ for (const record of disagreements) {
295
+ process.stdout.write(`${record.sid} table#${record.table} cols=${record.cols} `
296
+ + `observed=${record.form} predicted=${record.predicted} identical=${record.identical}/${record.emitted}\n`);
297
+ }
298
+ }
299
+
300
+ try {
301
+ main();
302
+ } catch (error) {
303
+ // The census is a report, not a test: an unexpected failure is printed on stderr and still
304
+ // exits 0 (numbers only -- never a painted row or a cell).
305
+ process.stderr.write(`claude-table-parity: ${error && error.message ? error.message : String(error)}\n`);
306
+ }
307
+ process.exitCode = 0;
@@ -15,6 +15,9 @@ const {
15
15
  bindObservedDatabaseGenerations,
16
16
  } = require('./conversation-storage-generation-evidence');
17
17
  const { inspectCompletedMarker } = require('./conversation-storage-completed-marker');
18
+ // WP-3 (clock-free r5 R-4..R-8): walle liveness from the write-lock identity record, the
19
+ // proven-owned daemon stop before the first walle write, and a fail-closed foreign holder.
20
+ const { ensureWalleOwnerStopped } = require('./conversation-storage-walle-owner');
18
21
  // No-op when options.profile is absent so behaviour is byte-identical to today: this
19
22
  // module only ever calls profile.measure/measureAsync around existing calls, never adds
20
23
  // new work, so an absent profile just means the timing calls disappear.
@@ -475,7 +478,13 @@ function runAutomaticConversationMigration(options = {}) {
475
478
  }
476
479
  }
477
480
  if (!state.irreversibleAt) {
478
- profile.measure('assert_databases_offline', () => _assertDatabasesOffline(specs));
481
+ // R-5/R-8: stop this instance's PROVEN-OWNED Wall-E before the first walle write, and
482
+ // probe only the databases this run will mutate -- `specs` included a live walle in a
483
+ // CTM-only rerun, which blocked the boot on a database it was never going to touch.
484
+ const walleSpecForPreflight = specs.find((spec) => spec.id === 'walle');
485
+ const walleOwner = walleSpecForPreflight && plannedDatabaseIds.has('walle')
486
+ ? ensureWalleOwnerStopped(walleSpecForPreflight.path, options.walleOwner || {}) : null;
487
+ profile.measure('assert_databases_offline', () => _assertDatabasesOffline(plannedSpecs()));
479
488
  const headroomDatabaseIds = new Set(plannedDatabaseIds);
480
489
  if (headroomDatabaseIds.has('ctm') && specs.some((spec) => spec.id === 'walle')) {
481
490
  headroomDatabaseIds.add('walle');
@@ -486,6 +495,7 @@ function runAutomaticConversationMigration(options = {}) {
486
495
  options,
487
496
  )),
488
497
  databaseOwnership: 'offline',
498
+ ...(walleOwner ? { walleOwner } : {}),
489
499
  };
490
500
  checkpoint('preflight');
491
501
  checkpoint('inventory');
@@ -654,6 +664,11 @@ function runAutomaticConversationMigration(options = {}) {
654
664
  expandWalleDependency = true;
655
665
  } finally { ctmDb.close(); walleDb.close(); }
656
666
  if (expandWalleDependency) {
667
+ // R-5/R-8 (still inside the !state.irreversibleAt guard above): the expansion adds
668
+ // walle AFTER the preflight probe, so the owner must be proven stopped here too,
669
+ // before rebuild_walle -- and walle alone is re-probed.
670
+ state.preflight.walleOwner = ensureWalleOwnerStopped(walleSpec.path, options.walleOwner || {});
671
+ _assertDatabasesOffline([walleSpec]);
657
672
  plannedDatabaseIds.add('walle');
658
673
  delete state.cleanBackups.walle;
659
674
  state.restartPreflight.plannedDatabaseIds = specs
@@ -5,6 +5,7 @@ const Database = require('better-sqlite3');
5
5
  const migration = require('../../scripts/conversation-storage-v2-migration');
6
6
  const {
7
7
  sanitizeInspectionViolations,
8
+ inspectionErrorClass,
8
9
  } = require('../../shared/conversation-storage-v2-contract');
9
10
 
10
11
  // Moved from conversation-storage-auto-migration-lib.js (WP-5 un-golf/split: keeping the
@@ -170,8 +171,14 @@ function inspectCompletedMarkerSpec(spec, expectedGeneration, generationEvidence
170
171
  violations,
171
172
  ...(generationChanged ? { generationChanged: true } : {}),
172
173
  };
173
- } catch {
174
- return { clean: false, reasonCodes: ['inspection_failed'], violations: [] };
174
+ } catch (error) {
175
+ // REPORT: an inspection that throws still plans a rerun (RERUN_CLASS, unchanged), but the
176
+ // boot log now names the error class instead of an empty violation list (clock-free r6 OQ-5).
177
+ return {
178
+ clean: false,
179
+ reasonCodes: ['inspection_failed'],
180
+ violations: [{ code: 'inspection_failed', objectId: inspectionErrorClass(error), count: 1 }],
181
+ };
175
182
  }
176
183
  }
177
184
 
@@ -12,6 +12,7 @@ const {
12
12
  buildConversationMigrationStatus,
13
13
  } = require('../lib/conversation-storage-migration-status');
14
14
  const { rebindCompletedGenerations } = require('./conversation-storage-generation-evidence');
15
+ const { resolveInstanceTag } = require('../lib/instance-tag');
15
16
  const { createPhaseProfile } = require('../lib/phase-profile');
16
17
  const {
17
18
  sanitizeInspectionViolations,
@@ -102,7 +103,13 @@ function buildConversationStorageServiceLog(result = {}) {
102
103
  recoveryArtifactCount: status.recoveryArtifactCount,
103
104
  cleanupPendingCount: status.cleanupPendingCount,
104
105
  };
105
- const restartPreflight = _restartPreflightForServiceLog(result.restartPreflight);
106
+ // R-6 (clock-free r4): restartPreflight describes a migration that was PLANNED by the
107
+ // boot that ran it. An already_complete / generation_rebound boot (result.skipped set)
108
+ // only re-reads the journal, so re-emitting the recorded object would print a stale
109
+ // re-run plan for a boot that ran no migration -- exactly what misled r3's census.
110
+ const restartPreflight = result.skipped == null
111
+ ? _restartPreflightForServiceLog(result.restartPreflight)
112
+ : null;
106
113
  if (restartPreflight) summary.restartPreflight = restartPreflight;
107
114
  return summary;
108
115
  }
@@ -129,6 +136,14 @@ function resolveAutomaticConversationMigrationOptions(env = process.env, homedir
129
136
  },
130
137
  batchLimit: Number(env.CTM_CONVERSATION_MIGRATION_BATCH_ROWS) || undefined,
131
138
  batchByteLimit: Number(env.CTM_CONVERSATION_MIGRATION_BATCH_BYTES) || undefined,
139
+ // WP-3 (clock-free r5 R-5/R-6): the kill authority the boot migration uses to prove and
140
+ // stop THIS instance's Wall-E before it touches wall-e-brain.db. Same tag derivation the
141
+ // supervisor uses (server.js:5897), so a dev/test instance can never signal the primary's
142
+ // daemon. Kept out of buildConversationStorageServiceLog, which never serializes options.
143
+ walleOwner: {
144
+ configDir: ctmDataDir,
145
+ instanceTag: resolveInstanceTag({ env, configDir: ctmDataDir, port: parseInt(env.CTM_PORT || '3456', 10) }),
146
+ },
132
147
  };
133
148
  }
134
149
 
@@ -0,0 +1,247 @@
1
+ 'use strict';
2
+
3
+ // WP-3 / R-4..R-8 of docs/clock-free-tests-followups-r5-2026-10-25.html §5.
4
+ //
5
+ // Liveness for the boot migration's destructive walle re-run comes from Wall-E's own
6
+ // write-lock identity record — `<db>.write-lock`, a 1,024-byte space-padded JSON
7
+ // `{pid,...}` that PERSISTS after release (shared/sqlite-write-lock.js:1-40,250, D-3) —
8
+ // not from a `BEGIN IMMEDIATE` probe. Probe P1 showed that an owner which is alive with an
9
+ // idle connection (or no connection at all) passes the transaction probe, and the purge
10
+ // then rewrites the database under it (the 2026-07-20 column-loss incident, r4 §1.1).
11
+ //
12
+ // The module is synchronous on purpose: bin/ctm-service-entry.js runs this migration
13
+ // BEFORE it requires ../server, and the stop has to finish before the first walle write.
14
+ //
15
+ // R-6: nothing here is ever signalled on a name match. The one pid that may receive a
16
+ // signal is the daemon pid read from `<configDir>/walle.pid`, and only after a single
17
+ // `ps -p <pid>` row proves it is this instance's own Wall-E (lib/walle-kill-authority.js).
18
+ // R-7: any other live write-lock holder stays fail-closed (`conversation_migration_database_busy`),
19
+ // which the caller maps to exit 78 exactly as before.
20
+
21
+ const fs = require('node:fs');
22
+ const path = require('node:path');
23
+ const { execFileSync } = require('node:child_process');
24
+ const { processTitleCandidates } = require('../lib/process-title');
25
+ const { isOwnedRow } = require('../lib/walle-kill-authority');
26
+ const { intFlag } = require('../lib/env-flags');
27
+
28
+ const WALLE_PID_FILE = 'walle.pid';
29
+ const WRITE_LOCK_SUFFIX = '.write-lock';
30
+ const PS_TIMEOUT_MS = 5000;
31
+ const PS_MAX_BUFFER = 1024 * 1024;
32
+ // One SIGKILL grace window after the graceful budget is spent; a killed-but-unreaped
33
+ // process is still a live pid until the kernel reaps it.
34
+ const SIGKILL_GRACE_MS = 1000;
35
+ // Same budget, default and clamp as lib/walle-supervisor.js:333,389 — a Wall-E that is
36
+ // legitimately draining inside its own WALLE_SHUTDOWN_DEADLINE_MS gets to finish.
37
+ const CHILD_EXIT_WAIT_ENV = 'WALLE_CHILD_EXIT_WAIT_MS';
38
+ const DEFAULT_CHILD_EXIT_WAIT_MS = 16000;
39
+ const MIN_CHILD_EXIT_WAIT_MS = 5000;
40
+ const MAX_CHILD_EXIT_WAIT_MS = 120000;
41
+ const DEFAULT_LOCK_RELEASE_WAIT_MS = 2000;
42
+ const DEFAULT_POLL_MS = 100;
43
+
44
+ function _childExitWaitMs() {
45
+ return Math.max(MIN_CHILD_EXIT_WAIT_MS, Math.min(MAX_CHILD_EXIT_WAIT_MS,
46
+ intFlag(CHILD_EXIT_WAIT_ENV, DEFAULT_CHILD_EXIT_WAIT_MS)));
47
+ }
48
+
49
+ // R-4: the owner identity, read off disk. null covers ENOENT (nothing ever wrote here),
50
+ // EISDIR and a torn read of a v2 in-place write — every one of those means "no usable
51
+ // identity", never "throw". Shape copied from shared/sqlite-write-lock.js:250 (_readIdentity)
52
+ // and wall-e/bin/brain-purge-legacy-ctm.js:236-245 (liveReason's first check).
53
+ function readWriteLockOwnerPid(dbPath) {
54
+ let record;
55
+ try {
56
+ record = JSON.parse(fs.readFileSync(`${dbPath}${WRITE_LOCK_SUFFIX}`, 'utf8'));
57
+ } catch {
58
+ /* expected: ENOENT (no writer yet), EISDIR (a directory sits at the path) and a torn
59
+ read of an in-place v2 write all mean "no identity readable right now". */
60
+ return null;
61
+ }
62
+ const pid = Number(record?.pid);
63
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
64
+ }
65
+
66
+ // R-4: only ESRCH proves the pid is gone. EPERM means the pid EXISTS but is owned by
67
+ // another user — kill(pid,0) probes existence, not killability — so every non-ESRCH
68
+ // error fails toward "alive" (shared/sqlite-write-lock.js:231-245).
69
+ function pidAlive(pid, kill = process.kill) {
70
+ const value = Number(pid);
71
+ if (!Number.isFinite(value) || value <= 0) return false;
72
+ try {
73
+ kill(value, 0);
74
+ return true;
75
+ } catch (error) {
76
+ return error?.code !== 'ESRCH';
77
+ }
78
+ }
79
+
80
+ // A pid that has exited but has not been REAPED is still a live pid for kill(pid,0): the
81
+ // kernel keeps a zombie's pid until its parent waits. libuv only waits when the event loop
82
+ // runs, and this migration blocks the loop for its whole duration (see the module comment:
83
+ // it runs before ../server is required), so a daemon this process spawned -- ppid === myPid,
84
+ // which lib/walle-kill-authority.js:17 explicitly accepts as owned -- stays kill(pid,0)-alive
85
+ // forever after it has exited. Measured on this host: `kill(pid,0)` says alive while
86
+ // `ps -o stat=` says "Z" for the whole blocked window, then the pid disappears once the loop
87
+ // turns. A zombie has exited, so it must read as gone -- otherwise the SIGTERM we just sent
88
+ // would be reported back to the caller as "a live holder that is not this instance's Wall-E".
89
+ // Only consulted when kill(pid,0) already says alive, so the common reparented-to-launchd
90
+ // daemon (ppid 1, reaped by init) never pays for it.
91
+ function _isZombiePid(pid) {
92
+ try {
93
+ const stdout = execFileSync('ps', ['-p', String(pid), '-o', 'stat='], {
94
+ encoding: 'utf8',
95
+ timeout: PS_TIMEOUT_MS,
96
+ maxBuffer: 4096,
97
+ });
98
+ return String(stdout).trim().startsWith('Z');
99
+ } catch {
100
+ /* expected: ps exits non-zero when the pid has no row -- it is gone, or it was reaped
101
+ between the two probes. Either way there is no zombie to report. */
102
+ return false;
103
+ }
104
+ }
105
+
106
+ function _readWalleDaemonPid(configDir) {
107
+ if (!configDir) return null;
108
+ try {
109
+ const pid = Number.parseInt(fs.readFileSync(path.join(String(configDir), WALLE_PID_FILE), 'utf8').trim(), 10);
110
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
111
+ } catch {
112
+ /* expected: no pid file (Wall-E never started for this config dir) or an unreadable
113
+ one both mean "no daemon to stop" — R-7 still fails closed on the write-lock owner. */
114
+ return null;
115
+ }
116
+ }
117
+
118
+ // The same synchronous sleep idiom the write-lock module uses (shared/sqlite-write-lock.js:226-229).
119
+ function sleepSync(ms) {
120
+ const buffer = new SharedArrayBuffer(4);
121
+ Atomics.wait(new Int32Array(buffer), 0, 0, ms);
122
+ }
123
+
124
+ let _parsePsWithPpid = null;
125
+
126
+ // Rows for ONE pid, parsed by the supervisor's own parser so ownership sees an identical
127
+ // row shape (lib/walle-supervisor.js:166-184,205-212). Loaded lazily: the migration runs
128
+ // before ../server boots, and the ps path is only reached when a live owner record exists.
129
+ function _psRow(pid) {
130
+ if (!_parsePsWithPpid) ({ parsePsWithPpid: _parsePsWithPpid } = require('../lib/walle-supervisor'));
131
+ try {
132
+ const stdout = execFileSync('ps', ['-p', String(pid), '-o', 'pid=,ppid=,command='], {
133
+ encoding: 'utf8',
134
+ timeout: PS_TIMEOUT_MS,
135
+ maxBuffer: PS_MAX_BUFFER,
136
+ });
137
+ return _parsePsWithPpid(stdout);
138
+ } catch (error) {
139
+ // A non-zero exit is ps answering "no such process" -> an empty row list.
140
+ // Anything else (ps un-runnable, timed out) is the probe itself failing -> null, which
141
+ // ownership must read as "not proven owned", never as "there is no process".
142
+ return Number.isInteger(error?.status) ? [] : null;
143
+ }
144
+ }
145
+
146
+ // R-4/R-5/R-7 liveness, one predicate: gone (ESRCH) or exited-and-not-yet-reaped (zombie).
147
+ function _hasExited(pid, ops) {
148
+ return !pidAlive(pid, ops.kill) || ops.zombie(pid);
149
+ }
150
+
151
+ // Poll until the pid is gone, bounded by BOTH the deadline and an iteration cap: a caller
152
+ // that injects a `now` which does not advance must not spin this loop forever.
153
+ function _waitForExit(pid, timeoutMs, pollMs, ops) {
154
+ if (_hasExited(pid, ops)) return true;
155
+ const budget = Math.max(0, Number(timeoutMs) || 0);
156
+ const poll = Math.max(1, Number(pollMs) || DEFAULT_POLL_MS);
157
+ const deadline = ops.now() + budget;
158
+ const maxIterations = Math.ceil(budget / poll) + 1;
159
+ for (let index = 0; index < maxIterations; index += 1) {
160
+ if (ops.now() >= deadline) break;
161
+ ops.sleep(Math.max(1, Math.min(poll, deadline - ops.now())));
162
+ if (_hasExited(pid, ops)) return true;
163
+ }
164
+ return _hasExited(pid, ops);
165
+ }
166
+
167
+ // R-5/R-6/R-7. Returns { stopped, pid, waitedMs, signal }; throws
168
+ // `conversation_migration_database_busy` when a live holder survives (R-7).
169
+ function ensureWalleOwnerStopped(walleDbPath, options = {}) {
170
+ const injected = options.ops || {};
171
+ const ops = {
172
+ kill: typeof injected.kill === 'function' ? injected.kill : process.kill,
173
+ psRow: typeof injected.psRow === 'function' ? injected.psRow : _psRow,
174
+ sleep: typeof injected.sleep === 'function' ? injected.sleep : sleepSync,
175
+ now: typeof injected.now === 'function' ? injected.now : Date.now,
176
+ myPid: Number.isInteger(injected.myPid) ? injected.myPid : process.pid,
177
+ // A caller that injects its own process table owns liveness entirely -- probing the
178
+ // real ps for a fake pid would be meaningless (and would fork a process per poll). The
179
+ // real zombie probe is only armed on the real process.kill path.
180
+ zombie: typeof injected.zombie === 'function' ? injected.zombie
181
+ : (typeof injected.kill === 'function' ? () => false : _isZombiePid),
182
+ };
183
+ const instanceTag = options.instanceTag ? String(options.instanceTag) : null;
184
+ const childExitWaitMs = Number.isFinite(options.childExitWaitMs)
185
+ ? Math.max(0, Number(options.childExitWaitMs)) : _childExitWaitMs();
186
+ const lockReleaseWaitMs = Number.isFinite(options.lockReleaseWaitMs)
187
+ ? Math.max(0, Number(options.lockReleaseWaitMs)) : DEFAULT_LOCK_RELEASE_WAIT_MS;
188
+ const pollMs = Number.isFinite(options.pollMs) ? Math.max(1, Number(options.pollMs)) : DEFAULT_POLL_MS;
189
+ const startedAt = ops.now();
190
+
191
+ const ownerPid = readWriteLockOwnerPid(walleDbPath);
192
+ if (ownerPid === null || ownerPid === ops.myPid || _hasExited(ownerPid, ops)) {
193
+ // No usable record, our own pid, or a provably dead writer: nothing to stop, and no
194
+ // ownership probe is even attempted.
195
+ return { stopped: false, pid: null, waitedMs: 0, signal: null };
196
+ }
197
+
198
+ let daemonPid = null;
199
+ let signal = null;
200
+ if (options.configDir && instanceTag) {
201
+ const candidate = _readWalleDaemonPid(options.configDir);
202
+ if (candidate !== null && !_hasExited(candidate, ops)) daemonPid = candidate;
203
+ }
204
+ if (daemonPid !== null) {
205
+ const rows = ops.psRow(daemonPid);
206
+ const row = (Array.isArray(rows) ? rows : []).find((entry) => Number(entry?.pid) === daemonPid) || null;
207
+ const owned = isOwnedRow(row, {
208
+ titles: new Set(processTitleCandidates('walle', instanceTag)),
209
+ myPid: ops.myPid,
210
+ });
211
+ if (owned) {
212
+ ops.kill(daemonPid, 'SIGTERM');
213
+ signal = 'SIGTERM';
214
+ if (!_waitForExit(daemonPid, childExitWaitMs, pollMs, ops)) {
215
+ ops.kill(daemonPid, 'SIGKILL');
216
+ signal = 'SIGKILL';
217
+ _waitForExit(daemonPid, SIGKILL_GRACE_MS, pollMs, ops);
218
+ }
219
+ }
220
+ }
221
+ // The write-lock owner may be a different pid than the daemon (a worker, or a foreign
222
+ // process). It gets the same short grace — the lock is released by process exit — and
223
+ // then fails closed below. It is never signalled unless it IS the proven daemon pid.
224
+ if (!_hasExited(ownerPid, ops)) _waitForExit(ownerPid, lockReleaseWaitMs, pollMs, ops);
225
+
226
+ if (!_hasExited(ownerPid, ops)) {
227
+ const error = new Error(
228
+ `database is owned by a live process that is not this instance's Wall-E: walle pid=${ownerPid}`,
229
+ );
230
+ error.code = 'conversation_migration_database_busy';
231
+ error.database = 'walle';
232
+ error.ownerPid = ownerPid;
233
+ throw error;
234
+ }
235
+ return {
236
+ stopped: signal !== null,
237
+ pid: daemonPid,
238
+ waitedMs: Math.max(0, ops.now() - startedAt),
239
+ signal,
240
+ };
241
+ }
242
+
243
+ module.exports = {
244
+ ensureWalleOwnerStopped,
245
+ pidAlive,
246
+ readWriteLockOwnerPid,
247
+ };