create-walle 0.10.0 → 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 (381) hide show
  1. package/README.md +88 -41
  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 +7 -5
  6. package/package.json +10 -4
  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/capture-codex-rollout-records.js +417 -0
  11. package/template/coding-task-manager/bin/census-codex-lead-history.js +325 -0
  12. package/template/coding-task-manager/bin/claude-hook.js +7 -3
  13. package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
  14. package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
  15. package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
  16. package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
  17. package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
  18. package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
  19. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  20. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  21. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  22. package/template/coding-task-manager/bin/seam-replay-codex.js +301 -0
  23. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  24. package/template/coding-task-manager/db.js +705 -286
  25. package/template/coding-task-manager/git-utils.js +44 -0
  26. package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
  27. package/template/coding-task-manager/lib/agent-presets.js +15 -0
  28. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  29. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  30. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  31. package/template/coding-task-manager/lib/api/ingress.js +35 -12
  32. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  33. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  34. package/template/coding-task-manager/lib/api/machines.js +461 -0
  35. package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
  36. package/template/coding-task-manager/lib/api/providers.js +53 -6
  37. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  38. package/template/coding-task-manager/lib/api/sessions.js +160 -8
  39. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  40. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  41. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  42. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  43. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  44. package/template/coding-task-manager/lib/auth-context.js +32 -2
  45. package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
  46. package/template/coding-task-manager/lib/auth-rules.js +186 -45
  47. package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
  48. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  49. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  50. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  51. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  52. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  53. package/template/coding-task-manager/lib/client-diag-handler.js +25 -0
  54. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  55. package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
  56. package/template/coding-task-manager/lib/codex-history-terminal-renderer.js +33 -33
  57. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  58. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  59. package/template/coding-task-manager/lib/codex-local-links.js +267 -0
  60. package/template/coding-task-manager/lib/codex-markdown-lists.js +160 -0
  61. package/template/coding-task-manager/lib/codex-native-history-prefix.js +4 -0
  62. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  63. package/template/coding-task-manager/lib/codex-source-handoff.js +186 -0
  64. package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
  65. package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
  66. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  67. package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
  68. package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
  69. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  70. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  71. package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
  72. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  73. package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
  74. package/template/coding-task-manager/lib/db-domains.js +21 -2
  75. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  76. package/template/coding-task-manager/lib/db-owner-worker-client.js +22 -0
  77. package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
  78. package/template/coding-task-manager/lib/feature-manifest.js +1 -3
  79. package/template/coding-task-manager/lib/fs-watcher.js +3 -2
  80. package/template/coding-task-manager/lib/harness-history-transition.js +41 -14
  81. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  82. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  83. package/template/coding-task-manager/lib/headless-term-service.js +60 -23
  84. package/template/coding-task-manager/lib/history-transition-read-op.js +179 -23
  85. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  86. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  87. package/template/coding-task-manager/lib/host-handoff.js +51 -13
  88. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  89. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  90. package/template/coding-task-manager/lib/host-stats.js +328 -0
  91. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  92. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  93. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  94. package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
  95. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
  96. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  97. package/template/coding-task-manager/lib/instance-manifest.js +13 -1
  98. package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
  99. package/template/coding-task-manager/lib/kernel/diag-log.js +12 -0
  100. package/template/coding-task-manager/lib/launch-model-override.js +160 -1
  101. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  102. package/template/coding-task-manager/lib/lead-model-observer.js +17 -2
  103. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
  104. package/template/coding-task-manager/lib/lead-model-switch.js +11 -2
  105. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +42 -4
  106. package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
  107. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  108. package/template/coding-task-manager/lib/login-tripwire.js +18 -0
  109. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  110. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  111. package/template/coding-task-manager/lib/machine-registry.js +749 -0
  112. package/template/coding-task-manager/lib/machine-self.js +66 -0
  113. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +56 -4
  114. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  115. package/template/coding-task-manager/lib/media-journal.js +84 -27
  116. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  117. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  118. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  119. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +16 -3
  120. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  121. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  122. package/template/coding-task-manager/lib/model-proxy/collaboration-codec.js +94 -0
  123. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  124. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  125. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  126. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  127. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  128. package/template/coding-task-manager/lib/model-proxy/responses-face.js +24 -2
  129. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  130. package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
  131. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  132. package/template/coding-task-manager/lib/native-history-control.js +62 -14
  133. package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
  134. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  135. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
  136. package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
  137. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  138. package/template/coding-task-manager/lib/prompt-admission-trace.js +69 -0
  139. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  140. package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
  141. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  142. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  143. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  144. package/template/coding-task-manager/lib/raw-history-block.js +3 -1
  145. package/template/coding-task-manager/lib/read-pool-client.js +36 -24
  146. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  147. package/template/coding-task-manager/lib/read-pool-telemetry.js +14 -5
  148. package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
  149. package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
  150. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  151. package/template/coding-task-manager/lib/remote-command-runtime.js +18 -5
  152. package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
  153. package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
  154. package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
  155. package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
  156. package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
  157. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  158. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  159. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  160. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  161. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  162. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  163. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  164. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  165. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  166. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  167. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  168. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  169. package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
  170. package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
  171. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  172. package/template/coding-task-manager/lib/root-prompt-admission.js +43 -11
  173. package/template/coding-task-manager/lib/root-prompt-flow.js +175 -38
  174. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  175. package/template/coding-task-manager/lib/server-memory-sampler-runtime.js +10 -0
  176. package/template/coding-task-manager/lib/server-memory-sampler.js +41 -2
  177. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  178. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  179. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  180. package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
  181. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  182. package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
  183. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  184. package/template/coding-task-manager/lib/session-create/provider-process.js +38 -39
  185. package/template/coding-task-manager/lib/session-create/response.js +30 -12
  186. package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
  187. package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
  188. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  189. package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
  190. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  191. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  192. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  193. package/template/coding-task-manager/lib/session-host-manager.js +152 -206
  194. package/template/coding-task-manager/lib/session-jobs.js +17 -9
  195. package/template/coding-task-manager/lib/session-lifecycle.js +20 -50
  196. package/template/coding-task-manager/lib/session-model-action-preconditions.js +68 -3
  197. package/template/coding-task-manager/lib/session-resource-sampler.js +67 -9
  198. package/template/coding-task-manager/lib/session-standup.js +67 -4
  199. package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
  200. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  201. package/template/coding-task-manager/lib/session-status.js +2 -2
  202. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  203. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  204. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  205. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  206. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
  207. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  208. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  209. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  210. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  211. package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
  212. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  213. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  214. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  215. package/template/coding-task-manager/lib/transcript-source.js +8 -3
  216. package/template/coding-task-manager/lib/transcript-store.js +40 -0
  217. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  218. package/template/coding-task-manager/lib/typing-interaction-trace.js +136 -2
  219. package/template/coding-task-manager/lib/typing-worker-stages.js +208 -1
  220. package/template/coding-task-manager/lib/usage-ledger/claude-parse.js +53 -2
  221. package/template/coding-task-manager/lib/usage-ledger/ingest.js +13 -4
  222. package/template/coding-task-manager/lib/usage-ledger/read.js +14 -0
  223. package/template/coding-task-manager/lib/usage-ledger/schema.js +49 -1
  224. package/template/coding-task-manager/lib/usage-ledger/store.js +337 -6
  225. package/template/coding-task-manager/lib/usage-ledger/task-read.js +123 -5
  226. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  227. package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
  228. package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
  229. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  230. package/template/coding-task-manager/lib/ws/input.js +1 -0
  231. package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
  232. package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
  233. package/template/coding-task-manager/lib/ws-router.js +68 -1
  234. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  235. package/template/coding-task-manager/oauth-proxy.js +99 -55
  236. package/template/coding-task-manager/package-lock.json +24 -0
  237. package/template/coding-task-manager/package.json +3 -0
  238. package/template/coding-task-manager/protocol/message-table.json +0 -10
  239. package/template/coding-task-manager/protocol/messages.js +0 -10
  240. package/template/coding-task-manager/protocol/ws-schema.json +0 -20
  241. package/template/coding-task-manager/protocol/ws-schema.md +0 -2
  242. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  243. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  244. package/template/coding-task-manager/public/css/app.css +279 -2
  245. package/template/coding-task-manager/public/css/reviews.css +4 -0
  246. package/template/coding-task-manager/public/index.html +297 -240
  247. package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
  248. package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
  249. package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
  250. package/template/coding-task-manager/public/js/background-work.js +13 -7
  251. package/template/coding-task-manager/public/js/client-health-core.js +81 -0
  252. package/template/coding-task-manager/public/js/client-health-main.js +61 -0
  253. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
  254. package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
  255. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  256. package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
  257. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  258. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  259. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  260. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  261. package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
  262. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  263. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -4
  264. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  265. package/template/coding-task-manager/public/js/protocol.js +0 -10
  266. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  267. package/template/coding-task-manager/public/js/reviews.js +7 -3
  268. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  269. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  270. package/template/coding-task-manager/public/js/row-join.js +18 -3
  271. package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
  272. package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
  273. package/template/coding-task-manager/public/js/session-usage-view.js +1 -0
  274. package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
  275. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  276. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  277. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  278. package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
  279. package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
  280. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  281. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  282. package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
  283. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  284. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  285. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -3
  286. package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
  287. package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
  288. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  289. package/template/coding-task-manager/public/m/app.css +226 -8
  290. package/template/coding-task-manager/public/m/app.js +502 -9
  291. package/template/coding-task-manager/public/m/index.html +38 -1
  292. package/template/coding-task-manager/public/m/machines.js +1313 -0
  293. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  294. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  295. package/template/coding-task-manager/public/m/sw.js +5 -0
  296. package/template/coding-task-manager/queue-engine.js +13 -0
  297. package/template/coding-task-manager/server/approvals.js +5 -6
  298. package/template/coding-task-manager/server/boot.js +136 -17
  299. package/template/coding-task-manager/server.js +964 -425
  300. package/template/coding-task-manager/session-integrity.js +7 -9
  301. package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
  302. package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
  303. package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
  304. package/template/coding-task-manager/workers/session-host-process.js +14 -3
  305. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  306. package/template/coding-task-manager/workers/worktree-worker.js +34 -0
  307. package/template/package-lock.json +2 -2
  308. package/template/package.json +1 -1
  309. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  310. package/template/shared/conversation-storage-v2-contract.js +42 -6
  311. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  312. package/template/shared/git-remote-key.js +83 -0
  313. package/template/shared/host-identity.js +98 -2
  314. package/template/shared/owner-write-safety.js +135 -0
  315. package/template/shared/public-runtime.js +13 -5
  316. package/template/shared/release-schema.json +3 -2
  317. package/template/shared/report-citations.js +14 -0
  318. package/template/shared/sqlite-write-lock.js +361 -223
  319. package/template/shared/storage/contract.js +35 -3
  320. package/template/wall-e/agent.js +21 -0
  321. package/template/wall-e/api-walle.js +5 -0
  322. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  323. package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
  324. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  325. package/template/wall-e/brain/migrations.js +51 -22
  326. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  327. package/template/wall-e/brain/owner-call-whitelist.js +16 -0
  328. package/template/wall-e/brain/schema-ensures.js +22 -0
  329. package/template/wall-e/brain.js +1 -1
  330. package/template/wall-e/coding/host-capabilities.js +2 -2
  331. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  332. package/template/wall-e/coding/session-journal.js +28 -40
  333. package/template/wall-e/coding-orchestrator.js +23 -26
  334. package/template/wall-e/fly.toml +1 -1
  335. package/template/wall-e/http/auth.js +18 -1
  336. package/template/wall-e/http/blog-views-api.js +318 -0
  337. package/template/wall-e/http/helper-rules-api.js +321 -0
  338. package/template/wall-e/http/model-proxy-providers.js +3 -1
  339. package/template/wall-e/http/telemetry-api.js +12 -0
  340. package/template/wall-e/lib/blog-view-policy.js +88 -0
  341. package/template/wall-e/lib/blog-view-store.js +169 -0
  342. package/template/wall-e/lib/client-config-lock.js +2 -1
  343. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  344. package/template/wall-e/lib/error-kinds.js +10 -1
  345. package/template/wall-e/lib/feature-manifest.js +1 -1
  346. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  347. package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
  348. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  349. package/template/wall-e/lib/helper-rules/grade.js +277 -0
  350. package/template/wall-e/lib/helper-rules/label.js +247 -0
  351. package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
  352. package/template/wall-e/lib/helper-rules/store.js +667 -0
  353. package/template/wall-e/lib/mcp-integration.js +5 -6
  354. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  355. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
  356. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  357. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  358. package/template/wall-e/llm/default-fallback.js +4 -0
  359. package/template/wall-e/llm/model-catalog.js +13 -4
  360. package/template/wall-e/llm/model-roles.js +6 -0
  361. package/template/wall-e/llm/portkey-http.js +150 -0
  362. package/template/wall-e/llm/portkey-sync.js +15 -145
  363. package/template/wall-e/llm/worker-mcp-routing.js +61 -0
  364. package/template/wall-e/llm/worker-route.js +43 -1
  365. package/template/wall-e/loops/brain-custom-writes.js +45 -0
  366. package/template/wall-e/mcp-protocol-contract.js +41 -1
  367. package/template/wall-e/mcp-server.js +13 -2
  368. package/template/wall-e/mcp-worker-schemas.js +20 -2
  369. package/template/wall-e/memory/ctm-context-client.js +25 -5
  370. package/template/wall-e/shared/release-schema.json +3 -2
  371. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  372. package/template/wall-e/shared/storage/contract.js +35 -3
  373. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  374. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
  375. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
  376. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
  377. package/template/wall-e/tools/local-tools.js +12 -5
  378. package/template/wall-e/workers/brain-owner-worker.js +9 -0
  379. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  380. package/template/wall-e/workers/telemetry-worker.js +8 -0
  381. package/template/coding-task-manager/lib/giant-freeze-park.js +0 -31
@@ -1,5 +1,25 @@
1
1
  'use strict';
2
2
 
3
+ // Write-lock protocol 2 — the EXCLUSION primitive is a kernel lock, not a file.
4
+ //
5
+ // A write hold is `BEGIN EXCLUSIVE` on `<db>.write-lock.mutex`, a SQLite database
6
+ // that is NEVER written (no table, no pragma that writes, rollback journal — never
7
+ // WAL). SQLite's own fcntl locks are the exclusion, so a SIGKILLed holder's lock is
8
+ // released by the OS and no age-based stealing is needed or allowed. Nothing else in
9
+ // the process may open that .mutex path: POSIX advisory locks are per-process, so an
10
+ // unrelated open()+close() on the same file silently drops the holder's lock (D-6,
11
+ // enforced by bin/lint-shared-copy-parity.js). Holder identity is a fixed 1,024-byte
12
+ // space-padded JSON record at the old claim path, pwritten in place at offset 0
13
+ // through an fd kept open for the life of the handle (D-3). The fd is opened
14
+ // O_RDWR | O_CREAT — never 'a+': on Linux pwrite(2) ignores the offset on an
15
+ // O_APPEND fd and appends, so the record would grow by 1 KiB per acquire (§1.2).
16
+ //
17
+ // The identity record persists after release, so a reader sees "the last writer",
18
+ // not "a write is in flight". `_fence` re-checks the path after every kernel-lock
19
+ // acquire, because a v1 process (or a boot sweep) can replace the record's inode
20
+ // under a live v2 holder (D-8b); a v1 claim found at the path is honored while its
21
+ // pid is alive and reaped once when it is not (D-8c).
22
+
3
23
  const fs = require('node:fs');
4
24
  const path = require('node:path');
5
25
  const crypto = require('node:crypto');
@@ -9,10 +29,15 @@ const { isMainThread, threadId } = require('node:worker_threads');
9
29
  const WRITE_SQL_RE = /^\s*(?:INSERT|UPDATE|DELETE|REPLACE|CREATE|ALTER|DROP|VACUUM|REINDEX|ANALYZE|ATTACH|DETACH|BEGIN|COMMIT|ROLLBACK)\b/i;
10
30
  const WRITE_PRAGMA_RE = /^\s*(?:journal_mode|wal_checkpoint|user_version|foreign_keys|synchronous|locking_mode|auto_vacuum|optimize)\b/i;
11
31
  const DEFAULT_TIMEOUT_MS = 0;
12
- const DEFAULT_STALE_MS = 10 * 60 * 1000;
13
32
  const DEFAULT_POLL_MS = 25;
14
- const DEFAULT_CLAIM_STALE_MS = 1000;
15
- const DEFAULT_STALE_RECOVERY_TIMEOUT_MS = 5000;
33
+ // D-1e: the lock DB is `<db>.write-lock.mutex`, NOT `<db>.write-lock.db` — code that
34
+ // enumerates *.db in a data dir (production-update.js, both test template copiers)
35
+ // would otherwise open and checkpoint the lock file as if it were a database.
36
+ const LOCK_DB_SUFFIX = '.mutex';
37
+ // D-3: the identity record is exactly this many bytes, written in place at offset 0.
38
+ const IDENTITY_RECORD_BYTES = 1024;
39
+ // D-8: the marker that distinguishes a v2 record from a pre-WP-1 claim.
40
+ const LOCK_PROTOCOL = 2;
16
41
  // R3 (round-2 WP-0, §6.2): a foreign (not-this-pid) hold at or beyond this age gets
17
42
  // exactly one incident-log record + /healthz.write_lock projection. A CONSTANT, not
18
43
  // an env flag (F-4) — tests inject a smaller value via opts.foreignHoldIncidentMs.
@@ -33,6 +58,9 @@ const MAIN_THREAD_WRITE_ALLOWANCE_POLICIES = Object.freeze({
33
58
  const globalState = globalThis[GLOBAL_STATE_KEY] || (globalThis[GLOBAL_STATE_KEY] = {
34
59
  token: `${process.pid}:${threadId}:${crypto.randomBytes(8).toString('hex')}`,
35
60
  locks: new Map(),
61
+ // D-7: per-thread, strongly referenced lock handles, keyed by canonical lock path.
62
+ // IndexedDB-style handles ({ db, begin, commit, identityFd, ... }) — see _lockHandle.
63
+ lockDbs: new Map(),
36
64
  mainWriteEnforcement: false,
37
65
  mainWriteAllowanceStorage: new AsyncLocalStorage(),
38
66
  mainWriteInventory: new Set(),
@@ -53,6 +81,7 @@ if (!(globalState.mainWriteInventory instanceof Set)) globalState.mainWriteInven
53
81
  if (!(globalState.mainWriteEscapeLastAt instanceof Map)) globalState.mainWriteEscapeLastAt = new Map();
54
82
  if (!(globalState.mainWriteEscapeBySite instanceof Map)) globalState.mainWriteEscapeBySite = new Map();
55
83
  if (!Number.isFinite(globalState.mainWriteEscapeCount)) globalState.mainWriteEscapeCount = 0;
84
+ if (!(globalState.lockDbs instanceof Map)) globalState.lockDbs = new Map();
56
85
  if (!(globalState.mainWriteAllowanceStorage instanceof AsyncLocalStorage)) {
57
86
  globalState.mainWriteAllowanceStorage = new AsyncLocalStorage();
58
87
  }
@@ -215,37 +244,21 @@ function _pidAlive(pid) {
215
244
  }
216
245
  }
217
246
 
218
- function _readLock(lockPath) {
247
+ // D-3: the holder record, read off disk. null covers ENOENT, an unreadable record and
248
+ // a torn read (a v2 holder pwrites in place, so a concurrent reader can observe a
249
+ // half-updated buffer) — every one of those means "no usable identity", never "throw".
250
+ function _readIdentity(lockPath) {
219
251
  try {
220
252
  return JSON.parse(fs.readFileSync(lockPath, 'utf8'));
221
253
  } catch {
254
+ /* expected: ENOENT (no record yet), EISDIR (a directory sits at the path) and a
255
+ torn read of a v2 in-place write all mean "no identity readable right now" —
256
+ the caller decides via the error code, and a thief that cannot be identified
257
+ must never be granted the lock. */
222
258
  return null;
223
259
  }
224
260
  }
225
261
 
226
- function _inspectLock(lockPath) {
227
- let stat = null;
228
- try {
229
- stat = fs.statSync(lockPath);
230
- } catch (error) {
231
- if (error?.code === 'ENOENT') return { exists: false, stat: null, raw: '', info: null, parseError: null };
232
- return { exists: true, stat: null, raw: '', info: null, parseError: error };
233
- }
234
-
235
- let raw = '';
236
- try {
237
- raw = fs.readFileSync(lockPath, 'utf8');
238
- try {
239
- return { exists: true, stat, raw, info: JSON.parse(raw), parseError: null };
240
- } catch (parseError) {
241
- return { exists: true, stat, raw, info: null, parseError };
242
- }
243
- } catch (error) {
244
- if (error?.code === 'ENOENT') return { exists: false, stat: null, raw: '', info: null, parseError: null };
245
- return { exists: true, stat, raw, info: null, parseError: error };
246
- }
247
- }
248
-
249
262
  // Canonical path is stable for the process lifetime (the parent directory's symlink
250
263
  // chain doesn't change), so cache it. _acquire runs this on every write — without the
251
264
  // cache it pays a realpathSync.native syscall per write, which is pure overhead on the
@@ -283,83 +296,222 @@ function _canonicalLockPath(lockPath) {
283
296
  return result;
284
297
  }
285
298
 
286
- function _releaseLockFile(lockPath, token) {
299
+ // D-7: the per-thread lock handle. Created lazily, once per canonical lock path, and
300
+ // kept strongly referenced in globalState.lockDbs — an unreachable holder connection
301
+ // is garbage-collected and its lock silently released, which is exactly the failure
302
+ // this map exists to rule out.
303
+ //
304
+ // The kernel lock and the identity fd are opened on FIRST ACQUIRE (`_openLockResources`),
305
+ // not here, because installSqliteWriteLock calls this for every wrapped connection —
306
+ // including the no-op stubs the boot-sweep tests install — and opening/creating files
307
+ // at install time would both break those tests and leave a record for a database that
308
+ // was never written. `refs` (D-7b) is therefore live from install onward.
309
+ function _lockHandle(lockPath, canonical, opts) {
310
+ const existing = globalState.lockDbs.get(canonical);
311
+ if (existing) return existing;
312
+ if (typeof opts.Database !== 'function') {
313
+ throw new TypeError('createSqliteWriteLock: opts.Database is required');
314
+ }
315
+ const handle = {
316
+ Database: opts.Database,
317
+ db: null, // D-1: opened on first acquire, from opts.Database (D-5)
318
+ begin: null,
319
+ commit: null,
320
+ identityFd: null, // D-3: O_RDWR|O_CREAT fd on lockPath, never 'a+'
321
+ identityIno: 0,
322
+ acquireSeq: 0, // ++ per acquire → the token suffix
323
+ refs: 0, // wrapped connections currently open on this lock path (D-7b)
324
+ refsWarned: false,
325
+ };
326
+ globalState.lockDbs.set(canonical, handle);
327
+ return handle;
328
+ }
329
+
330
+ // D-3/D-8c: the identity fd. Created with O_EXCL so "we made this record file" is a fact, and
331
+ // creating it is a race we can lose but never a steal: EEXIST means something is already at the
332
+ // path — a v2 record from a previous holder (which _fence adopts; D-3 deliberately keeps the
333
+ // record after release) or a pre-WP-1 claim (which _fence leaves alone and reports busy while
334
+ // its holder lives). An unopenable object at the path (EISDIR: a directory, the fixture the
335
+ // fail-fast tests use) is the same "somebody else's claim" case. Both leave identityFd null, so
336
+ // _fence classifies whatever is there on every acquire it takes. Never O_APPEND (§1.2).
337
+ function _openIdentityFd(handle, lockPath) {
287
338
  try {
288
- const info = _readLock(lockPath);
289
- if (info?.token === token) fs.unlinkSync(lockPath);
290
- } catch {}
339
+ const fd = fs.openSync(lockPath, fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
340
+ handle.identityFd = fd;
341
+ handle.identityIno = fs.fstatSync(fd).ino;
342
+ return;
343
+ } catch (error) {
344
+ if (error?.code !== 'EEXIST' && error?.code !== 'EISDIR') throw error;
345
+ handle.identityFd = null;
346
+ handle.identityIno = -1;
347
+ }
291
348
  }
292
349
 
293
- function _lockAgeMs(inspected, now = Date.now()) {
294
- const createdAt = Number(inspected?.info?.created_at_ms || 0);
295
- const basis = createdAt || Number(inspected?.stat?.mtimeMs || 0);
296
- if (!basis) return Number.POSITIVE_INFINITY;
297
- return Math.max(0, now - basis);
350
+ function _openLockResources(handle, lockPath) {
351
+ if (handle.db) return handle;
352
+ // D-5: the constructor of the connection being wrapped, never a fresh require — a
353
+ // second copy of the SQLite library in one process breaks its in-process lock table.
354
+ handle.db = new handle.Database(`${lockPath}${LOCK_DB_SUFFIX}`, { timeout: 0 });
355
+ handle.begin = handle.db.prepare('BEGIN EXCLUSIVE');
356
+ handle.commit = handle.db.prepare('COMMIT');
357
+ _openIdentityFd(handle, lockPath);
358
+ return handle;
298
359
  }
299
360
 
300
- function _lockUnchanged(a, b) {
301
- if (!a?.exists || !b?.exists) return false;
302
- if (a.info?.token || b.info?.token) return a.info?.token && a.info.token === b.info?.token;
303
- if (!a.stat || !b.stat) return false;
304
- if (a.raw || b.raw) return a.raw === b.raw && a.stat.size === b.stat.size && Math.abs(a.stat.mtimeMs - b.stat.mtimeMs) < 1;
305
- return a.stat.dev === b.stat.dev &&
306
- a.stat.ino === b.stat.ino &&
307
- a.stat.size === b.stat.size &&
308
- Math.abs(a.stat.mtimeMs - b.stat.mtimeMs) < 1 &&
309
- Math.abs(a.stat.ctimeMs - b.stat.ctimeMs) < 1;
361
+ function _retainHandle(handle) {
362
+ handle.refs += 1;
310
363
  }
311
364
 
312
- function _unlinkIfUnchanged(lockPath, observed) {
313
- const current = _inspectLock(lockPath);
314
- if (!current.exists) return true;
315
- if (!_lockUnchanged(observed, current)) return false;
365
+ // D-7b: the handle is released with the LAST wrapped connection, because every test
366
+ // file (and every db.close()/reopen) would otherwise leak two fds and one SQLite
367
+ // connection per database. Closing the lock DB through SQLite while another thread's
368
+ // connection holds the lock is safe — the unix VFS works around POSIX advisory locking
369
+ // quirks (How To Corrupt §2.2).
370
+ function _releaseHandle(canonical) {
371
+ const handle = globalState.lockDbs.get(canonical);
372
+ if (!handle) return false;
373
+ handle.refs = Math.max(0, handle.refs - 1);
374
+ if (handle.refs > 0) return false;
375
+ if (handle.db && handle.db.inTransaction) {
376
+ // A close() while a hold is still open is a caller bug. Dropping the handle here
377
+ // would release a lock somebody still believes they hold; keep it and say so once.
378
+ if (!handle.refsWarned) {
379
+ handle.refsWarned = true;
380
+ try {
381
+ console.warn(`[sqlite-write-lock] close() with an open write-lock hold; keeping the lock handle: ${canonical}`);
382
+ } catch {
383
+ /* expected: console.warn on a torn-down stdio (worker exiting) must not turn a
384
+ caller's close() bug into a different, less legible failure. */
385
+ }
386
+ }
387
+ return false;
388
+ }
389
+ globalState.lockDbs.delete(canonical);
390
+ _closeHandleResources(handle);
391
+ return true;
392
+ }
393
+
394
+ function _closeHandleResources(handle) {
395
+ if (handle.db) {
396
+ try { handle.db.close(); } catch {
397
+ /* expected: the lock DB may already be closed by closeSqliteWriteLocksForTests or
398
+ by an earlier teardown in the same test; the identity fd below is the part
399
+ that must not be forgotten, and it is closed unconditionally. */
400
+ }
401
+ handle.db = null;
402
+ }
403
+ if (handle.identityFd !== null) {
404
+ try { fs.closeSync(handle.identityFd); } catch {
405
+ /* expected: an fd already closed (EBADF) or a double release is not an error —
406
+ the handle is being torn down either way. */
407
+ }
408
+ handle.identityFd = null;
409
+ }
410
+ }
411
+
412
+ // D-8b: adopt the record file at lockPath — either because it went missing (recreate
413
+ // with O_EXCL, so exactly one process wins the race) or because the inode under us was
414
+ // replaced (reopen the existing file). Returns false when another process won the
415
+ // O_EXCL race or removed the path first; the acquire loop then re-fences.
416
+ function _adoptIdentity(handle, lockPath, flags) {
417
+ let fd;
316
418
  try {
317
- fs.unlinkSync(lockPath);
318
- return true;
419
+ fd = fs.openSync(lockPath, flags, 0o600);
319
420
  } catch (error) {
320
- return error?.code === 'ENOENT';
421
+ if (error?.code === 'EEXIST' || error?.code === 'ENOENT') return false;
422
+ throw error;
423
+ }
424
+ try { fs.closeSync(handle.identityFd); } catch {
425
+ /* expected: the old fd is a plain fd on the record file (not the lock DB) and may already
426
+ be closed if a previous adopt lost a race, or be null when the first open found somebody
427
+ else's object at the path (EEXIST/EISDIR) — the new fd is what matters either way. */
428
+ }
429
+ handle.identityFd = fd;
430
+ handle.identityIno = fs.fstatSync(fd).ino;
431
+ return true;
432
+ }
433
+
434
+ // D-1d: a failed BEGIN EXCLUSIVE needs no cleanup — but if the connection is somehow
435
+ // inside a transaction anyway, the state is not one this module understands: roll back
436
+ // and fail loudly rather than loop forever against it.
437
+ function _tryKernelLock(handle) {
438
+ try {
439
+ handle.begin.run();
440
+ return true;
441
+ } catch (e) {
442
+ if (handle.db.inTransaction) {
443
+ handle.db.prepare('ROLLBACK').run();
444
+ throw Object.assign(new Error('write-lock DB left in a transaction'), { code: 'SQLITE_WRITE_LOCK_STATE' });
445
+ }
446
+ if (e.code === 'SQLITE_BUSY') return false;
447
+ throw e;
448
+ }
449
+ }
450
+
451
+ // D-8: what is at the lock path? 'v2' — our record (possibly a torn read of an
452
+ // in-place write, or empty); 'v1-live' — a pre-WP-1 claim whose holder is alive, or
453
+ // anything unparseable that is not a torn v2 record; 'v1-dead' — a v1 claim whose
454
+ // holder is provably gone (the single cutover-only reason this module removes a path).
455
+ function _classifyRecord(lockPath, st) {
456
+ let raw = '';
457
+ try {
458
+ raw = fs.readFileSync(lockPath, 'utf8');
459
+ } catch {
460
+ /* expected: the path can vanish between the fence's statSync and this read; an
461
+ unreadable record is classified as a live foreign claim below, never as ours. */
321
462
  }
463
+ if (st.size === 0) return 'v2';
464
+ let info;
465
+ try {
466
+ info = JSON.parse(raw);
467
+ } catch {
468
+ return st.size === IDENTITY_RECORD_BYTES ? 'v2' : 'v1-live';
469
+ }
470
+ if (info && info.protocol === LOCK_PROTOCOL) return 'v2';
471
+ // v1 writes a complete record before linkSync (sqlite-write-lock-v1.js:381-383), so a
472
+ // v1 claim is never empty or torn — anything else with a dead pid is reapable.
473
+ return _pidAlive(info && info.pid) ? 'v1-live' : 'v1-dead';
322
474
  }
323
475
 
324
- function _staleReason(inspected, staleMs, claimStaleMs, now = Date.now()) {
325
- if (!inspected?.exists) return 'missing';
326
- const ageMs = _lockAgeMs(inspected, now);
327
- if (!inspected.info) {
328
- return ageMs > claimStaleMs ? 'orphaned-claim' : '';
476
+ // D-8b/D-8c: runs only after the kernel lock is held. The inode can change under a live
477
+ // holder (a v1 steal, a boot sweep, another process's adopt), and the first draft's
478
+ // "different inode ⇒ busy" would then spin this process busy until it restarted.
479
+ function _fence(handle, lockPath) {
480
+ let st;
481
+ try {
482
+ st = fs.statSync(lockPath);
483
+ } catch (error) {
484
+ if (error?.code !== 'ENOENT') throw error;
485
+ st = null;
329
486
  }
330
- if (ageMs > staleMs) return 'expired';
331
- const info = inspected.info;
332
- // Holder identity is pid + a per-process-launch token (already written by
333
- // _writeLockClaim). Old-format records (no token) fall straight through to the
334
- // pid-only liveness check below — unchanged behavior, fully backward compatible.
335
- //
336
- // A recycled pid can make `_pidAlive(pid)` return true for a lock a DIFFERENT,
337
- // now-dead process wrote (the OS handed that same pid to an unrelated live
338
- // process). We can only DISPROVE this cheaply — with zero extra syscalls — in
339
- // one specific case: when the recorded pid is OUR OWN pid. If it is, and the
340
- // token doesn't match our own launch token, the record cannot possibly be ours:
341
- // the OS just recycled this exact pid onto us after the original holder (some
342
- // earlier, now-dead process) crashed while holding the lock. That's provable
343
- // with no OS calls at all, so treat it as a dead owner immediately. Proving
344
- // recycling for any OTHER pid would need a real cross-process start-time check
345
- // (e.g. `ps -o lstart`), which is too expensive to pay on this hot path; those
346
- // cases fall back to a plain liveness check and are still bounded by the
347
- // `staleMs` ceiling above if the lock is genuinely abandoned.
348
- // IMPORTANT: tokens are per-THREAD (`pid:threadId:rand`, and globalState
349
- // lives on the per-thread globalThis), so "same pid, different token" is NOT
350
- // proof of a dead owner by itself — a live sibling worker_thread of THIS
351
- // process has our pid and a different token. Only when the record claims our
352
- // exact pid AND threadId with a different launch-random is it provably from
353
- // a previous (dead) process that our pid was recycled from.
354
- if (
355
- info.pid === process.pid
356
- && typeof info.token === 'string'
357
- && info.token !== globalState.token
358
- && info.token.startsWith(`${process.pid}:${threadId}:`)
359
- ) {
360
- return 'dead-owner';
487
+ if (st === null) {
488
+ return _adoptIdentity(handle, lockPath, fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL) ? 'ok' : 'busy';
489
+ }
490
+ if (st.ino === handle.identityIno) return 'ok';
491
+ if (st.isDirectory()) return 'busy';
492
+ switch (_classifyRecord(lockPath, st)) {
493
+ case 'v2':
494
+ return _adoptIdentity(handle, lockPath, fs.constants.O_RDWR) ? 'ok' : 'busy';
495
+ case 'v1-dead':
496
+ try {
497
+ fs.unlinkSync(lockPath);
498
+ } catch {
499
+ /* expected: another process may reap the same dead v1 claim first (ENOENT), or
500
+ the path may have been replaced (a directory) — either way the next loop
501
+ iteration re-fences and the outcome is decided there. */
502
+ }
503
+ return _adoptIdentity(handle, lockPath, fs.constants.O_RDWR | fs.constants.O_CREAT | fs.constants.O_EXCL) ? 'ok' : 'busy';
504
+ default:
505
+ return 'busy';
361
506
  }
362
- return _pidAlive(info.pid) ? '' : 'dead-owner';
507
+ }
508
+
509
+ // D-3: one in-place 1,024-byte write at offset 0 on the fd adopted above. No close, no
510
+ // fsync, no unlink — release does not touch the record.
511
+ function _writeIdentity(handle, info) {
512
+ const buf = Buffer.alloc(IDENTITY_RECORD_BYTES, 0x20);
513
+ buf.write(JSON.stringify({ protocol: LOCK_PROTOCOL, ...info }));
514
+ fs.writeSync(handle.identityFd, buf, 0, buf.length, 0);
363
515
  }
364
516
 
365
517
  // Pure deadline-slicing helper shared by every wait branch in the acquire loop:
@@ -374,31 +526,6 @@ function _remainingWaitMs(deadline, sliceMs, now = Date.now()) {
374
526
  return Math.min(sliceMs, remaining);
375
527
  }
376
528
 
377
- function _writeLockClaim(lockPath, payload) {
378
- const dir = path.dirname(lockPath);
379
- const base = path.basename(lockPath);
380
- const tempPath = path.join(dir, `.${base}.${process.pid}.${threadId}.${Date.now()}.${crypto.randomBytes(4).toString('hex')}.tmp`);
381
- const text = JSON.stringify(payload);
382
- try {
383
- fs.writeFileSync(tempPath, text, { flag: 'wx', mode: 0o600 });
384
- fs.linkSync(tempPath, lockPath);
385
- } catch (error) {
386
- if (error?.code !== 'EEXIST' && ['EPERM', 'ENOTSUP', 'EOPNOTSUPP', 'EXDEV'].includes(error?.code)) {
387
- let fd = null;
388
- try {
389
- fd = fs.openSync(lockPath, 'wx', 0o600);
390
- fs.writeFileSync(fd, text);
391
- return;
392
- } finally {
393
- try { if (fd !== null) fs.closeSync(fd); } catch {}
394
- }
395
- }
396
- throw error;
397
- } finally {
398
- try { fs.unlinkSync(tempPath); } catch {}
399
- }
400
- }
401
-
402
529
  function _durationMs(value, fallback, min = 0) {
403
530
  const n = Number(value);
404
531
  return Number.isFinite(n) ? Math.max(min, Math.trunc(n)) : fallback;
@@ -584,23 +711,14 @@ function _resetWriteLockForeignHoldForTests(store = globalState) {
584
711
 
585
712
  function _acquire(lockPath, opts = {}) {
586
713
  const timeoutMs = _durationMs(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS, DEFAULT_TIMEOUT_MS, 0);
587
- const staleMs = _durationMs(opts.staleMs ?? DEFAULT_STALE_MS, DEFAULT_STALE_MS, 1000);
588
714
  // Floor of 1ms (not 5) so a background writer can poll tightly and reliably win
589
715
  // the lock in the brief window after the main thread releases — the main thread
590
716
  // can never wait, so the worker winning fast is how we avoid starving it.
591
717
  const pollMs = _durationMs(opts.pollMs ?? DEFAULT_POLL_MS, DEFAULT_POLL_MS, 1);
592
- const claimStaleMs = _durationMs(opts.claimStaleMs ?? DEFAULT_CLAIM_STALE_MS, DEFAULT_CLAIM_STALE_MS, 1);
593
- const staleRecoveryTimeoutMs = _durationMs(
594
- opts.staleRecoveryTimeoutMs ?? DEFAULT_STALE_RECOVERY_TIMEOUT_MS,
595
- DEFAULT_STALE_RECOVERY_TIMEOUT_MS,
596
- 0
597
- );
598
718
  const { now = Date.now, sleepSync = _sleepSync } = opts;
599
719
  const startedAt = now();
600
720
  const deadline = startedAt + timeoutMs;
601
- const dir = path.dirname(lockPath);
602
721
  const canonical = _canonicalLockPath(lockPath);
603
- let staleRecoveryDeadline = 0;
604
722
  let contended = false;
605
723
  const held = globalState.locks.get(canonical);
606
724
  if (held) {
@@ -610,94 +728,82 @@ function _acquire(lockPath, opts = {}) {
610
728
  contended: false,
611
729
  release: () => {
612
730
  held.depth -= 1;
613
- if (held.depth <= 0) {
614
- globalState.locks.delete(canonical);
615
- _releaseLockFile(held.lockPath, held.token);
616
- }
617
731
  },
618
732
  };
619
733
  }
620
734
 
621
- try { fs.mkdirSync(dir, { recursive: true }); } catch {}
735
+ const handle = _lockHandle(lockPath, canonical, opts);
736
+ // v1 parity (sqlite-write-lock-v1.js:621): a cached handle can outlive the directory it was
737
+ // created in — coding-task-manager/db.js keeps its per-domain opens by name across initDb()
738
+ // calls — and the identity open below would then fail ENOENT and report a busy lock that names
739
+ // nobody. mkdir on a directory that exists writes nothing, so the steady state is unchanged.
740
+ try {
741
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
742
+ } catch {
743
+ /* expected: a directory that cannot be created (EACCES/ENOTDIR/a removed temp root) is not a
744
+ lock verdict — the identity open below reports the real error, and a live foreign holder is
745
+ still attributed by the fence. */
746
+ }
747
+ _openLockResources(handle, lockPath);
622
748
 
623
749
  while (true) {
624
- try {
625
- const token = globalState.token;
626
- // R3 (round-2 WP-0, §6.2): identity fields on the holder record — role/argv0/
627
- // purpose let a contended waiter (and /healthz) attribute WHO holds the lock,
628
- // not just that it's busy. acquiredAt mirrors created_at_ms (kept for
629
- // _lockAgeMs/_staleReason, which are untouched) under the spec's field name.
630
- const claimedAtMs = now();
631
- _writeLockClaim(lockPath, {
632
- pid: process.pid,
633
- thread_id: threadId,
634
- token,
635
- created_at_ms: claimedAtMs,
636
- acquiredAt: claimedAtMs,
637
- // WALL_E_PROCESS_ROLE (not CTM_PROCESS_ROLE — the §6.2 spec text names the
638
- // latter, but that's CTM's own foreign-owner marker, always unset for wall-e's
639
- // OWN daemon/workers; WALL_E_PROCESS_ROLE is the identity every legitimate
640
- // wall-e opener actually sets — see lib/live-data-role-guard.js's enumeration.
641
- // Also avoids a NEW raw CTM_* env read here, which the flag-read ratchet forbids).
642
- role: opts.role || process.env.WALL_E_PROCESS_ROLE || 'unknown',
643
- argv0: opts.argv0 || path.basename(process.argv[1] || ''),
644
- purpose: String(opts.purpose || opts.label || '').slice(0, 80),
645
- label: opts.label || '',
646
- });
647
- const entry = { depth: 1, lockPath, token };
648
- globalState.locks.set(canonical, entry);
649
- return {
650
- waitedMs: now() - startedAt,
651
- contended,
652
- release: () => {
653
- entry.depth -= 1;
654
- if (entry.depth > 0) return;
655
- globalState.locks.delete(canonical);
656
- _releaseLockFile(lockPath, token);
657
- },
658
- };
659
- } catch (err) {
660
- if (err?.code !== 'EEXIST') throw err;
661
- contended = true;
662
- const observed = _inspectLock(lockPath);
663
- // R3 (round-2 WP-0, §6.2): a FOREIGN (not-this-pid) holder at/beyond the
664
- // incident threshold gets exactly one incident record + /healthz projection,
665
- // independent of staleness (a live foreign hold is never "stale" by the
666
- // 10-minute ceiling below, but 30s is already an incident-worthy hold).
667
- if (observed?.info && observed.info.pid !== process.pid) {
668
- _maybeRecordForeignHold(observed, opts);
669
- }
670
- const staleReason = _staleReason(observed, staleMs, claimStaleMs, now());
671
- if (staleReason) {
672
- if (_unlinkIfUnchanged(lockPath, observed)) continue;
673
- if (!staleRecoveryDeadline) staleRecoveryDeadline = now() + staleRecoveryTimeoutMs;
674
- if (now() < staleRecoveryDeadline) {
675
- // Was: unconditional `_sleepSync(pollMs)`, ignoring the CALLER's deadline —
676
- // a 0ms caller could be dragged through the full staleRecoveryTimeoutMs
677
- // (default 5000ms) when the stale lock couldn't be unlinked (it changed
678
- // under us). _remainingWaitMs makes the caller's deadline always win.
679
- const wait = _remainingWaitMs(deadline, pollMs, now());
680
- if (wait < 0) throw _lockBusyError(lockPath, observed.info);
681
- sleepSync(wait);
682
- continue;
683
- }
684
- } else if (!observed.info) {
685
- // Was: unconditional `_sleepSync(waitMs)` here too — same bypass for a
686
- // still-forming (orphaned) claim.
687
- const slice = Math.min(pollMs, Math.max(1, claimStaleMs - _lockAgeMs(observed, now())));
688
- const wait = _remainingWaitMs(deadline, slice, now());
689
- if (wait < 0) throw _lockBusyError(lockPath, observed.info);
690
- sleepSync(wait);
691
- continue;
692
- } else {
693
- staleRecoveryDeadline = 0;
694
- }
695
- {
696
- const wait = _remainingWaitMs(deadline, pollMs, now());
697
- if (wait < 0) throw _lockBusyError(lockPath, observed.info);
698
- sleepSync(wait);
750
+ if (_tryKernelLock(handle)) {
751
+ if (_fence(handle, lockPath) === 'ok') {
752
+ // R3 (round-2 WP-0, §6.2): identity fields on the holder record — role/argv0/
753
+ // purpose let a contended waiter (and /healthz) attribute WHO holds the lock,
754
+ // not just that it's busy.
755
+ const acquiredAtMs = now();
756
+ _writeIdentity(handle, {
757
+ pid: process.pid,
758
+ thread_id: threadId,
759
+ token: `${globalState.token}:${++handle.acquireSeq}`,
760
+ created_at_ms: acquiredAtMs,
761
+ acquiredAt: acquiredAtMs,
762
+ // WALL_E_PROCESS_ROLE (not CTM_PROCESS_ROLE — the §6.2 spec text names the
763
+ // latter, but that's CTM's own foreign-owner marker, always unset for wall-e's
764
+ // OWN daemon/workers; WALL_E_PROCESS_ROLE is the identity every legitimate
765
+ // wall-e opener actually sets — see lib/live-data-role-guard.js's enumeration.
766
+ // Also avoids a NEW raw CTM_* env read here, which the flag-read ratchet forbids).
767
+ role: opts.role || process.env.WALL_E_PROCESS_ROLE || 'unknown',
768
+ argv0: opts.argv0 || path.basename(process.argv[1] || ''),
769
+ purpose: String(opts.purpose || opts.label || '').slice(0, 80),
770
+ label: opts.label || '',
771
+ });
772
+ const entry = { depth: 1, lockPath, token: globalState.token };
773
+ globalState.locks.set(canonical, entry);
774
+ return {
775
+ waitedMs: now() - startedAt,
776
+ contended,
777
+ // D-1: release is COMMIT — never an unlink, never a record rewrite.
778
+ release: () => {
779
+ entry.depth -= 1;
780
+ if (entry.depth > 0) return;
781
+ globalState.locks.delete(canonical);
782
+ handle.commit.run();
783
+ },
784
+ };
699
785
  }
786
+ // A live foreign claim (a pre-WP-1 process, or a directory) owns the path:
787
+ // give the kernel lock back and wait like any other busy acquirer.
788
+ handle.commit.run();
789
+ contended = true;
790
+ } else {
791
+ contended = true;
792
+ }
793
+ // R3 (round-2 WP-0, §6.2): a FOREIGN (not-this-pid) holder at/beyond the incident
794
+ // threshold gets exactly one incident record + /healthz projection. Both contended
795
+ // branches end in the same busy error below, so both must attribute the holder the same
796
+ // way: under D-3 the record at the path is the identity of whoever is inside its hold,
797
+ // and under D-8c it is the only identity a live pre-WP-1 claim has.
798
+ const info = _readIdentity(lockPath);
799
+ if (info && info.pid !== process.pid) {
800
+ _maybeRecordForeignHold({ info }, opts);
700
801
  }
802
+ const wait = _remainingWaitMs(deadline, pollMs, now());
803
+ // D-4: no age-based stealing. A holder that is alive keeps the lock; the waiter
804
+ // fails busy at its own deadline and the incident log is the escalation.
805
+ if (wait < 0) throw _lockBusyError(lockPath, _readIdentity(lockPath));
806
+ sleepSync(wait);
701
807
  }
702
808
  }
703
809
 
@@ -928,21 +1034,27 @@ function containsLikelyWriteSql(sql) {
928
1034
  });
929
1035
  }
930
1036
 
931
- // One-pass boot sweep for a single lock file: crashed processes leave their
932
- // write-lock file behind forever otherwise (nothing else ever revisits a lock
933
- // path once no writer contends for it). Only ever removes a lock whose holder
934
- // pid is provably dead (ESRCH via _pidAlive) — a live holder, or a record we
935
- // can't parse/attribute, is left alone for the normal acquire-time staleness
936
- // path (staleMs / claimStaleMs) to handle instead. Safe to call repeatedly
937
- // (idempotent) and safe under a real concurrent acquirer (reuses the same
938
- // observed-then-unlink-if-unchanged compare as the acquire loop).
1037
+ // One-pass boot sweep for a single lock file: crashed processes leave their record
1038
+ // behind forever otherwise (nothing else ever revisits a lock path once no writer
1039
+ // contends for it). Only ever removes a record whose holder pid is provably dead
1040
+ // (ESRCH via _pidAlive) — a live holder, or a record we can't parse/attribute, is left
1041
+ // alone. Protocol-agnostic: a dead v2 process's record is removed too, and any live v2
1042
+ // holder of that path adopts the new inode on its next acquire (D-8b). The re-inspect
1043
+ // before the unlink is the race guard that replaces _unlinkIfUnchanged; this never
1044
+ // touches `${lockPath}.mutex`. Safe to call repeatedly (idempotent) and safe under a
1045
+ // real concurrent acquirer.
939
1046
  function _bootSweepLockFile(lockPath, logger = console) {
940
- const inspected = _inspectLock(lockPath);
941
- if (!inspected.exists) return false;
942
- const pid = inspected.info?.pid;
1047
+ const pid = _readIdentity(lockPath)?.pid;
943
1048
  if (pid == null || _pidAlive(pid)) return false;
944
- const removed = _unlinkIfUnchanged(lockPath, inspected);
945
- if (removed && logger && typeof logger.warn === 'function') {
1049
+ // Re-inspect: the record may have been replaced (a new writer, another sweep) since
1050
+ // the read above — only remove the very record whose dead pid we proved.
1051
+ if (Number(_readIdentity(lockPath)?.pid) !== Number(pid)) return false;
1052
+ try {
1053
+ fs.unlinkSync(lockPath);
1054
+ } catch (error) {
1055
+ return error?.code === 'ENOENT';
1056
+ }
1057
+ if (logger && typeof logger.warn === 'function') {
946
1058
  try {
947
1059
  logger.warn(`[sqlite-write-lock] boot sweep removed stale lock (dead pid=${pid}): ${lockPath}`);
948
1060
  } catch {
@@ -951,13 +1063,26 @@ function _bootSweepLockFile(lockPath, logger = console) {
951
1063
  an already-successful boot sweep into a startup failure */
952
1064
  }
953
1065
  }
954
- return removed;
1066
+ return true;
955
1067
  }
956
1068
 
957
1069
  function lockBootSweepEnabled(env = process.env) {
958
1070
  return String(env.WALLE_LOCK_BOOT_SWEEP ?? '1') !== '0';
959
1071
  }
960
1072
 
1073
+ // Test seam only: tests that call createSqliteWriteLock directly have no wrapped
1074
+ // db.close() to release the handle through (D-7b), so they call this in after().
1075
+ function closeSqliteWriteLocksForTests() {
1076
+ const entries = [...globalState.lockDbs.entries()];
1077
+ globalState.lockDbs.clear();
1078
+ for (const [canonical, handle] of entries) {
1079
+ if (handle.db && handle.db.inTransaction) {
1080
+ throw new Error(`closeSqliteWriteLocksForTests: lock DB still in a transaction: ${canonical}`);
1081
+ }
1082
+ _closeHandleResources(handle);
1083
+ }
1084
+ }
1085
+
961
1086
  function installSqliteWriteLock(db, dbPath, opts = {}) {
962
1087
  if (!db || db.__sqliteWriteLockInstalled) return db;
963
1088
  const lockPath = opts.lockPath || `${dbPath}.write-lock`;
@@ -965,12 +1090,24 @@ function installSqliteWriteLock(db, dbPath, opts = {}) {
965
1090
  try {
966
1091
  _bootSweepLockFile(lockPath, opts.logger || console);
967
1092
  } catch {
968
- /* expected: _bootSweepLockFile's own doc comment (above its definition) says a lock it
969
- can't remove is left for the normal acquire-time staleness check to handle instead —
970
- a thrown error here (e.g. an unreadable lock dir) must fall through to that same
971
- acquire-time path rather than abort DB install */
1093
+ /* expected: a record the sweep can't remove is left for the acquire-time fence to
1094
+ handle instead — a thrown error here (e.g. an unreadable lock dir) must fall
1095
+ through to that same path rather than abort DB install */
972
1096
  }
973
1097
  }
1098
+ // D-7b: one handle per (thread, lock path), refcounted per wrapped connection and
1099
+ // released by the LAST db.close(). D-5: the lock DB comes from the constructor of the
1100
+ // connection being wrapped.
1101
+ const canonical = _canonicalLockPath(lockPath);
1102
+ _retainHandle(_lockHandle(lockPath, canonical, { ...opts, Database: db.constructor }));
1103
+ if (typeof db.close === 'function') {
1104
+ const originalClose = db.close.bind(db);
1105
+ db.close = (...a) => {
1106
+ const result = originalClose(...a);
1107
+ _releaseHandle(canonical);
1108
+ return result;
1109
+ };
1110
+ }
974
1111
  const upstreamOnEvent = typeof opts.onEvent === 'function' ? opts.onEvent : null;
975
1112
  let eventSequence = 0;
976
1113
  let lastEvent = null;
@@ -1051,8 +1188,9 @@ module.exports = {
1051
1188
  suspendWriteLockHoldBudget,
1052
1189
  // WP-P3 test seams: internal, hermetic-tests-only. Not part of the public API.
1053
1190
  _remainingWaitMsForTests: _remainingWaitMs,
1054
- _staleReasonForTests: _staleReason,
1055
1191
  _bootSweepLockFileForTests: _bootSweepLockFile,
1192
+ // D-7b: direct createSqliteWriteLock callers (no wrapped db.close) release here.
1193
+ closeSqliteWriteLocksForTests,
1056
1194
  _currentLockTokenForTests: () => globalState.token,
1057
1195
  _resetWriteLockForeignHoldForTests,
1058
1196
  };