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
@@ -0,0 +1,461 @@
1
+ 'use strict';
2
+
3
+ // WP-0 (remote-machine-hub-sessions-2026-10-22.html §6.1): the hub machine-registry routes.
4
+ //
5
+ // GET /api/machines -> { machines:[{instanceId, displayName, url, urls, health,
6
+ // lastSeenAt, favorite, hidden, stats, lastUsedAt, os, arch,
7
+ // hostname, peerCredential, agents, …}], hubNetworkSuspect }
8
+ // `peerCredential` (WP-1 D-4, WP-2a D-8 rev 2) is
9
+ // 'pool' | 'env' | 'missing' — a hub-local fact repeated on
10
+ // every row, never the token.
11
+ // `agents` (WP-3) is the box's last-read coding-CLI sign-in
12
+ // verdict: { claude, codex, checkedAt } with each agent
13
+ // 'signed_in' | 'signed_out' | 'unknown'. The all-'unknown'
14
+ // default means "never fetched" — a hub that has not probed
15
+ // this box yet must not imply either answer.
16
+ // POST /api/machines -> register { url, displayName?, walleUrl? } (one manifest
17
+ // probe IS the identity pin, D-3)
18
+ // POST /api/machines/pair -> WP-3 §6.3: join a pool by redeeming the box's pairing code
19
+ // POST /api/machines/:id/remove -> unregister; WP-3 §6.3 extends it to a pool member
20
+ //
21
+ // Unlike the lib/api/*.js extraction slices, this module is NEW surface (WP-0), so the handler
22
+ // bodies live here rather than in server.js's legacy handleApi chain — there is no legacy branch
23
+ // to delete, and tests-integration/api-router-parity.test.js's no-dual-dispatch rule is satisfied
24
+ // by construction. Handlers are thin: the policy (pin/mismatch/probe) is lib/machine-registry.js,
25
+ // the SQL is db.js's owner zone, and the pairing codec/verifier is lib/machine-pairing.js.
26
+ //
27
+ // Auth is NOT evaluated here: lib/auth-rules.js carries the rows (loopbackOnly + admin, modeled on
28
+ // the /api/setup/* rules), and a route with no rule is 403 auth_rule_missing by design — so the
29
+ // rule rows and these registrations must land together.
30
+
31
+ const { registerApiRoutes } = require('../api-router');
32
+ const machinePairing = require('../machine-pairing');
33
+
34
+ // §6.3: the identity probe is a reachability check, not a data fetch — it must not hold a request
35
+ // open on a box that is merely slow to answer.
36
+ const PAIR_PROBE_TIMEOUT_MS = 5_000;
37
+ // §6.3: roster pushes and rotation calls are best-effort against a possibly-dead member. Ten seconds
38
+ // matches lib/remote-machine-proxy.js's DEFAULT_TIMEOUT_MS, the proxy's own box budget.
39
+ const PAIR_PUSH_TIMEOUT_MS = 10_000;
40
+
41
+ let _deps = null;
42
+
43
+ function configure({
44
+ db,
45
+ registry,
46
+ pool,
47
+ readJsonRequestBody,
48
+ sendJsonResponse,
49
+ ready = null,
50
+ fetchImpl = null,
51
+ } = {}) {
52
+ _deps = { db, registry, pool, readJsonRequestBody, sendJsonResponse, ready, fetchImpl };
53
+ }
54
+
55
+ /**
56
+ * The boot read's promise (server.js `_machinePoolReady`) — pair/remove MUTATE the pool. Accepts
57
+ * either the promise itself or a thunk returning it: server.js configures this cluster (and its
58
+ * `machines` route group) BEFORE the pool block is evaluated, so the value can only be read lazily.
59
+ */
60
+ function ready() {
61
+ const source = _deps && _deps.ready;
62
+ if (typeof source === 'function') return Promise.resolve(source());
63
+ return source || Promise.resolve();
64
+ }
65
+
66
+ function _send(res, status, payload) {
67
+ if (_deps && typeof _deps.sendJsonResponse === 'function') return _deps.sendJsonResponse(res, status, payload);
68
+ const body = JSON.stringify(payload || {});
69
+ res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
70
+ res.end(body);
71
+ }
72
+
73
+ /** This machine's own minted id — '' rather than a throw before boot has primed identity. */
74
+ function _instanceId() {
75
+ try {
76
+ const identity = require('../instance-identity-access').getInstanceIdentity({ required: false });
77
+ return (identity && identity.instanceId) || '';
78
+ } catch {
79
+ return ''; // expected: instance identity is not ready yet (early boot / pre-initDb)
80
+ }
81
+ }
82
+
83
+ /** The pool store — the singleton by default, or whatever the injected thunk returns (see ready()). */
84
+ function _pool() {
85
+ const source = _deps && _deps.pool;
86
+ if (typeof source === 'function') return source();
87
+ return source || require('../machine-pool').getMachinePool();
88
+ }
89
+
90
+ /**
91
+ * WP-2a D-8: the credential this member dials ONE target with (pool for a pool member, env
92
+ * otherwise). Used for OPERATING calls — the P-5/P-6 roster push and rotate — where the target is a
93
+ * machine this pool already holds. The identity PROBE and the JOIN hand-off deliberately do NOT use
94
+ * it: neither carries an Authorization header (see `_probeInstanceId` and step 5 below), because the
95
+ * box being paired has not issued this member anything to authenticate with.
96
+ */
97
+ function _peerHeadersFor(instanceId) {
98
+ try {
99
+ return require('../remote-machine-create').buildPeerControlHeaders(process.env, { instanceId }) || {};
100
+ } catch {
101
+ return {}; // expected: no credential configured — the call goes out anonymous and is refused
102
+ }
103
+ }
104
+
105
+ /**
106
+ * One JSON round trip with a deadline. `AbortSignal.timeout(ms)` is the codebase's sanctioned
107
+ * non-timer deadline (lib/api/setup.js:168,190,209; lib/api/painted-rows.js:28; lib/oauth-proxy-
108
+ * supervisor.js) — this file therefore adds NO `setTimeout` site for the stabilization lint to own.
109
+ */
110
+ function _fetchJson(url, { method = 'GET', headers = {}, body = null, timeoutMs = PAIR_PUSH_TIMEOUT_MS } = {}) {
111
+ const fetchFn = (_deps && _deps.fetchImpl) || global.fetch;
112
+ if (typeof fetchFn !== 'function') return Promise.resolve({ ok: false, status: 0, body: null });
113
+ const signal = typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
114
+ ? AbortSignal.timeout(timeoutMs)
115
+ : undefined;
116
+ return fetchFn(url, {
117
+ method,
118
+ headers: {
119
+ Accept: 'application/json',
120
+ ...(body === null ? {} : { 'Content-Type': 'application/json' }),
121
+ ...headers,
122
+ },
123
+ body: body === null ? undefined : JSON.stringify(body),
124
+ signal,
125
+ }).then(async (res) => {
126
+ let parsed = null;
127
+ try {
128
+ parsed = await res.json();
129
+ } catch {
130
+ parsed = null; // expected: a non-JSON body is reported by status alone, never as a transport fault
131
+ }
132
+ return { ok: !!res.ok, status: Number(res.status) || 0, body: parsed };
133
+ }).catch(() => ({ ok: false, status: 0, body: null }));
134
+ }
135
+
136
+ /** The identity answer's instance id — the same three shapes lib/machine-registry.js accepts. */
137
+ function _manifestInstanceId(body) {
138
+ const manifest = (body && (body.manifest || body.instance)) || body || {};
139
+ return String(
140
+ manifest.instanceId || manifest.instance_id || manifest.id
141
+ || (body && (body.instanceId || body.instance_id)) || '',
142
+ ).trim();
143
+ }
144
+
145
+ /**
146
+ * §6.3 step 2 (D-9): the identity probe, aimed at the box's PRE-AUTH `GET /api/machine-pairing/
147
+ * identity`. Reachable-and-matching / reachable-but-different / unreachable are three DIFFERENT
148
+ * answers — collapsing them is what makes a mis-pasted pairing string silently pair the wrong
149
+ * machine — and the identity comparison itself stays with the caller.
150
+ *
151
+ * WHY NOT /api/sync/manifest. That route is `sync`-scoped (lib/auth-rules.js), and a machine that has
152
+ * not paired yet holds no credential of this box at all: on a real LAN the probe would 401 and the
153
+ * pair would die as `machine_unreachable` before it began. The pairing routes are dispatched with
154
+ * phase 'api-cloudflare', BEFORE the auth wall, precisely so an unauthenticated member can ask this.
155
+ *
156
+ * NO AUTHORIZATION HEADER, on purpose. The box is not a pool member yet, so D-8 would offer the
157
+ * shared pool secret here — a secret handed to an address that came out of a pasted pairing string or
158
+ * a typed field, for a call that cannot be verified by the receiver anyway. The probe asks a
159
+ * BOOLEAN question ("is a code showing here") and needs no credential to do it.
160
+ */
161
+ async function _probeInstanceId(url) {
162
+ const res = await _fetchJson(`${url}/api/machine-pairing/identity`, {
163
+ headers: {},
164
+ timeoutMs: PAIR_PROBE_TIMEOUT_MS,
165
+ });
166
+ if (!res.ok) return { ok: false };
167
+ const instanceId = _manifestInstanceId(res.body);
168
+ if (!instanceId) return { ok: false };
169
+ return { ok: true, instanceId };
170
+ }
171
+
172
+ /**
173
+ * P-1: this machine's roster as the wire carries it — every OTHER POOL MEMBER row plus SELF (last),
174
+ * each `{instanceId, urls}`.
175
+ *
176
+ * F2: ONE implementation, owned by lib/api/machine-pool.js (the module that owns the pool store) and
177
+ * consumed here. The local copy this file used to carry mapped EVERY machine row, so a legacy
178
+ * env-token row (P-8: a machine this hub dials with CTM_SYNC_PEER_TOKEN, never a pool member) was
179
+ * pushed to a newly paired box as a FELLOW MEMBER of a pool it had not joined — the box would then
180
+ * dial it with the pool bearer and be refused.
181
+ */
182
+ function _rosterMembers() {
183
+ return require('./machine-pool').rosterMembers();
184
+ }
185
+
186
+ /**
187
+ * P-4 dial order, for one POST: try every URL the target advertises, in order, and stop at the first
188
+ * one that ANSWERS. A transport failure (status 0) moves on to the next URL; an HTTP answer — even a
189
+ * refusal — ends the attempt, because a second URL that answers differently is a different machine.
190
+ */
191
+ async function _postToTarget(target, pathname, body, headers) {
192
+ const urls = (Array.isArray(target.urls) && target.urls.length ? target.urls : [String(target.url || '')])
193
+ .map((url) => String(url || '').trim().replace(/\/+$/, ''))
194
+ .filter(Boolean);
195
+ for (const base of urls) {
196
+ const res = await _fetchJson(`${base}${pathname}`, { method: 'POST', body, headers: headers || {} });
197
+ if (res.ok) return true;
198
+ if (res.status) return false;
199
+ }
200
+ return false;
201
+ }
202
+
203
+ async function handleMachinesList(req, res) {
204
+ const registry = _deps && _deps.registry;
205
+ const { machines, hubNetworkSuspect } = registry.listMachines({ db: _deps && _deps.db });
206
+ // WP-1 (D-4) / P-8: which credential this hub will actually dial THIS row with — computed per row
207
+ // from the row's instanceId, because a pool hub sends the pool secret only to pool members and the
208
+ // env bearer to a legacy box that never joined (see peerCredentialState). The token itself is
209
+ // never read into this response — only which class is in play.
210
+ const peerCredentialFor = require('../remote-machine-create').peerCredentialState;
211
+ // WP-3: the box's coding-CLI sign-in truth the probe sweep last read. Guarded by typeof so a test
212
+ // registry that only carries listMachines still serves rows (they just read 'unknown').
213
+ const agentAuthFor = registry && typeof registry.agentAuthFor === 'function' ? registry.agentAuthFor : null;
214
+ _send(res, 200, {
215
+ machines: machines.map((machine) => ({
216
+ instanceId: machine.instanceId,
217
+ displayName: machine.displayName,
218
+ url: machine.url,
219
+ walleUrl: machine.walleUrl,
220
+ enabled: machine.enabled,
221
+ health: machine.health,
222
+ healthDetail: machine.healthDetail,
223
+ lastSeenAt: machine.lastSeenAt,
224
+ pickerState: machine.pickerState,
225
+ visible: machine.visible,
226
+ // WP-2 (P-1/P-4): the dial list, in dial order — the roster's urls, `url` first when the roster
227
+ // is empty. The picker and the Machines page dial/render these; `url` stays the row's cached
228
+ // working address.
229
+ urls: Array.isArray(machine.urls) ? machine.urls : [],
230
+ // WP-2 (D-4 rev 2): machine-owned settings, read from the box's own manifest by the sweep.
231
+ // `favorite` is the picker star and is NEVER the identity pin (that word is reserved).
232
+ favorite: machine.favorite ? 1 : 0,
233
+ hidden: machine.hidden ? 1 : 0,
234
+ // WP-2 (D-12/D-16): the box's own host sample + the hub-clock stamp of the read (null = never
235
+ // read), and the newest session create on that machine (0 = none). Readers label staleness.
236
+ stats: machine.stats || null,
237
+ lastUsedAt: Number(machine.lastUsedAt) || 0,
238
+ os: machine.os || '',
239
+ arch: machine.arch || '',
240
+ hostname: machine.hostname || '',
241
+ peerCredential: peerCredentialFor(process.env, { instanceId: machine.instanceId }),
242
+ agents: (agentAuthFor ? agentAuthFor.call(registry, machine.instanceId) : null)
243
+ || { claude: 'unknown', codex: 'unknown', checkedAt: 0 },
244
+ })),
245
+ hubNetworkSuspect,
246
+ });
247
+ }
248
+
249
+ async function handleMachinesCreate(req, res) {
250
+ let body = {};
251
+ try {
252
+ body = await _deps.readJsonRequestBody(req);
253
+ } catch {
254
+ return _send(res, 400, { ok: false, error: 'invalid_json' });
255
+ }
256
+ const result = await _deps.registry.registerMachine({
257
+ url: body && (body.url || body.baseUrl || body.base_url),
258
+ displayName: body && (body.displayName || body.display_name || body.name),
259
+ walleUrl: body && (body.walleUrl || body.walle_url),
260
+ }, {
261
+ db: _deps.db,
262
+ // The hub's peer credential on the identity probe, so a box behind a relay or tunnel answers.
263
+ // P-8: the builder is PER TARGET in WP-2a, but the box's instance id is not known until the
264
+ // probe answers — so registration uses the no-target form, which is exactly today's behaviour
265
+ // (pool-if-pool, else the env bearer).
266
+ peerHeadersFor: () => require('../remote-machine-create').buildPeerControlHeaders(process.env) || {},
267
+ });
268
+ if (!result.ok) {
269
+ // machine_unreachable / machine_identity_mismatch / machine_manifest_invalid / machine_url_required
270
+ const status = result.code === 'machine_identity_mismatch' ? 409 : 502;
271
+ return _send(res, status, { ok: false, error: result.code, detail: result.error || '' });
272
+ }
273
+ return _send(res, 200, { ok: true, machine: result.machine });
274
+ }
275
+
276
+ /**
277
+ * WP-3 §6.3 — the MEMBER side of pairing. Steps 1-7 in the doc's order, with the doc's verbatim
278
+ * error codes. The token minted here is handed to the box ONCE and never echoed, logged or stored
279
+ * anywhere except lib/machine-pool.js's 0600 file.
280
+ */
281
+ async function handleMachinesPair(req, res) {
282
+ let body = {};
283
+ try {
284
+ body = await _deps.readJsonRequestBody(req);
285
+ } catch {
286
+ return _send(res, 400, { ok: false, error: 'invalid_json' });
287
+ }
288
+ // 1. parse — a `ctmpair1:` container, or the `code` + `url` pair the UI's manual form collects.
289
+ const parsed = machinePairing.decodePairingString(body && body.pairingString);
290
+ const code = parsed ? parsed.code : machinePairing.normalizeCode(body && body.code);
291
+ const claimedId = parsed ? parsed.instanceId : '';
292
+ const candidates = [];
293
+ if (parsed) candidates.push(...parsed.urls);
294
+ if (body && body.url) candidates.push(String(body.url).trim().replace(/\/+$/, ''));
295
+ const seen = new Set();
296
+ const dialOrder = candidates.filter((url) => {
297
+ if (!url || seen.has(url)) return false;
298
+ seen.add(url);
299
+ return true;
300
+ });
301
+ if (code.length !== machinePairing.CODE_LENGTH || !dialOrder.length) {
302
+ return _send(res, 400, { ok: false, error: 'pairing_string_invalid' });
303
+ }
304
+
305
+ // 2. identity-probe the candidates until one names the id the pairing string claimed.
306
+ let boxUrl = '';
307
+ let boxInstanceId = '';
308
+ let mismatched = '';
309
+ for (const candidate of dialOrder) {
310
+ const probe = await _probeInstanceId(candidate);
311
+ if (!probe.ok) continue;
312
+ if (claimedId && probe.instanceId !== claimedId) {
313
+ mismatched = probe.instanceId;
314
+ continue;
315
+ }
316
+ boxUrl = candidate;
317
+ boxInstanceId = probe.instanceId;
318
+ break;
319
+ }
320
+ if (!boxUrl) {
321
+ return mismatched
322
+ ? _send(res, 409, { ok: false, error: 'machine_identity_mismatch', detail: `manifest ${mismatched} != ${claimedId}` })
323
+ : _send(res, 502, { ok: false, error: 'machine_unreachable' });
324
+ }
325
+
326
+ await ready();
327
+ const pool = _pool();
328
+ const selfId = _instanceId();
329
+ // 3. mint the pool if this member has none yet (§6.3 — the first pairing creates the shared secret).
330
+ const token = await pool.ensurePool();
331
+ // 4. bump the roster, then build the membership set the box will store: every POOL MEMBER we
332
+ // already hold, plus the box itself (P-1's roster already carries SELF last). The box stores
333
+ // this MINUS itself (P-8). F2: the set comes from lib/api/machine-pool.js's `rosterMembers()`,
334
+ // so a legacy env-token row — a registry row that is NOT a pool member — is never advertised to
335
+ // the new box as one of its peers.
336
+ const version = await pool.bumpRoster(selfId);
337
+ const members = [
338
+ ..._rosterMembers().filter((member) => member.instanceId !== boxInstanceId),
339
+ { instanceId: boxInstanceId, urls: [boxUrl] },
340
+ ];
341
+
342
+ // 5. hand the token to the box. The response is the box's own identity + dial list; its error
343
+ // codes are relayed VERBATIM (§6.3) so the UI can say exactly what happened.
344
+ //
345
+ // F1: NO Authorization header. The join is the PRE-AUTH act that establishes this member's
346
+ // credential on that box, so there is nothing meaningful to present — and everything D-8 could
347
+ // pick here (the pool secret, or a legacy env bearer) is a secret sent to an unverified address
348
+ // for a call the box cannot authenticate either way. The pool secret is proven by the CODE and
349
+ // by the body's `token`, which is what the box adopts.
350
+ const join = await _fetchJson(`${boxUrl}/api/machine-pairing/join`, {
351
+ method: 'POST',
352
+ headers: {},
353
+ body: { code: machinePairing.formatCode(code), token, version, members, primary: { instanceId: selfId } },
354
+ });
355
+ if (!join.ok) {
356
+ const relayed = String((join.body && join.body.error) || '');
357
+ if (relayed === 'pairing_code_invalid') return _send(res, 410, { ok: false, error: 'pairing_code_invalid' });
358
+ if (relayed === 'rate_limited') return _send(res, 429, { ok: false, error: 'rate_limited' });
359
+ if (relayed === 'pool_member_exists') return _send(res, 409, { ok: false, error: 'pool_member_exists' });
360
+ if (relayed === 'pool_token_invalid') return _send(res, 400, { ok: false, error: 'pool_token_invalid' });
361
+ return _send(res, 502, { ok: false, error: 'machine_unreachable', detail: relayed || `http_${join.status}` });
362
+ }
363
+
364
+ // 6. record the box locally. `url` is the address we just PROVED reachable (the probe's winner) and
365
+ // it leads the dial list; the box's own advertised URLs follow as fallbacks (P-4). A row whose
366
+ // first URL is a LAN address this member cannot reach would leave the new member unusable.
367
+ const advertised = Array.isArray(join.body && join.body.urls) ? join.body.urls.map((url) => String(url || '')) : [];
368
+ const boxUrls = [boxUrl, ...advertised.filter((url) => url && url !== boxUrl)];
369
+ await _deps.db.upsertMachineRow({
370
+ instanceId: boxInstanceId,
371
+ displayName: String((join.body && join.body.displayName) || '').slice(0, 200),
372
+ url: boxUrl,
373
+ urls: boxUrls,
374
+ });
375
+ await pool.addMember(boxInstanceId);
376
+ if (_deps.registry && typeof _deps.registry.noteRegistryChanged === 'function') _deps.registry.noteRegistryChanged();
377
+
378
+ // 7. P-5: push the roster to every OTHER member, best-effort. The box is skipped — it was just
379
+ // handed the same set (and the same version) inside the join request. A failure is not an error
380
+ // here: the next sweep pull (lib/machine-registry.js) converges the same state.
381
+ const roster = _rosterMembers();
382
+ const targets = roster.filter((member) => member.instanceId !== selfId && member.instanceId !== boxInstanceId);
383
+ const pushed = [];
384
+ const unreachable = [];
385
+ for (const target of targets) {
386
+ const ok = await _postToTarget(target, '/api/pool/roster', { version, members: roster }, _peerHeadersFor(target.instanceId));
387
+ if (ok) pushed.push(target.instanceId);
388
+ else unreachable.push(target.instanceId);
389
+ }
390
+ return _send(res, 201, { ok: true, machine: { instanceId: boxInstanceId, url: boxUrl, urls: boxUrls }, pushed, unreachable });
391
+ }
392
+
393
+ /**
394
+ * §6.3: unregister a machine. A POOL member also rotates: the removed machine holds the shared
395
+ * secret, so nothing but a new one keeps it out. A legacy env-token row (P-8) holds no pool secret
396
+ * and keeps the plain delete.
397
+ */
398
+ async function handleMachineRemove(req, res, url, params) {
399
+ const instanceId = decodeURIComponent((params && (params['1'] || params.id)) || '');
400
+ if (!instanceId) return _send(res, 400, { ok: false, error: 'machine_id_required' });
401
+ const pool = _pool();
402
+ const rows = _deps.db.listMachineRows() || [];
403
+ if (!pool.hasPool() || !pool.isMember(instanceId)) {
404
+ // WP-2: the machine writes are DB-owner mutations (db.js's DB_OWNER_MUTATION_ACTIONS), so the
405
+ // handle here is the owner-client Proxy and this call is a PROMISE. Await it before computing
406
+ // `removed` — a synchronous read of the result would report "0 removed" for every real delete.
407
+ const removed = await _deps.db.deleteMachineRow(instanceId);
408
+ if (_deps.registry && typeof _deps.registry.noteRegistryChanged === 'function') _deps.registry.noteRegistryChanged();
409
+ return _send(res, 200, { ok: true, removed: Number(removed) || 0 });
410
+ }
411
+ await ready();
412
+ const selfId = _instanceId();
413
+ // P-6: capture the OLD credential headers BEFORE rotate(). Every remaining member still holds the
414
+ // old secret, so it is the only credential that can authenticate the rotation itself.
415
+ const oldHeaders = new Map();
416
+ for (const row of rows) {
417
+ if (row.instanceId && row.instanceId !== instanceId) oldHeaders.set(row.instanceId, _peerHeadersFor(row.instanceId));
418
+ }
419
+ const removed = await _deps.db.deleteMachineRow(instanceId);
420
+ await pool.removeMember(instanceId);
421
+ const version = await pool.bumpRoster(selfId);
422
+ const roster = _rosterMembers();
423
+ const targets = roster.filter((member) => member.instanceId !== selfId);
424
+ // P-5 first: the remaining members learn the new membership set while the old secret still works.
425
+ for (const target of targets) {
426
+ await _postToTarget(target, '/api/pool/roster', { version, members: roster }, oldHeaders.get(target.instanceId));
427
+ }
428
+ // P-6: then the secret itself changes, and each reachable member is told (with the OLD credential).
429
+ const newToken = await pool.rotate();
430
+ const rotated = [];
431
+ const unreachable = [];
432
+ for (const target of targets) {
433
+ const ok = await _postToTarget(target, '/api/pool/rotate', { token: newToken }, oldHeaders.get(target.instanceId));
434
+ if (ok) rotated.push(target.instanceId);
435
+ else unreachable.push(target.instanceId);
436
+ }
437
+ if (_deps.registry && typeof _deps.registry.noteRegistryChanged === 'function') _deps.registry.noteRegistryChanged();
438
+ // §6.3/AC-3d: `unreachable` members are the ones the UI warns about — they still hold the OLD
439
+ // secret and must re-pair (the old one stops being accepted after lib/machine-pool.js's 10 min
440
+ // rotation grace, ROTATION_GRACE_MS).
441
+ return _send(res, 200, { ok: true, removed: Number(removed) || 0, rotated, unreachable });
442
+ }
443
+
444
+ function register() {
445
+ registerApiRoutes('machines', [
446
+ { method: 'GET', path: '/api/machines', handler: (req, res) => handleMachinesList(req, res) },
447
+ { method: 'POST', path: '/api/machines', handler: (req, res) => handleMachinesCreate(req, res) },
448
+ { method: 'POST', path: '/api/machines/pair', handler: (req, res) => handleMachinesPair(req, res) },
449
+ { method: 'POST', regex: /^\/api\/machines\/([^/]+)\/remove$/, handler: (req, res, url, params) => handleMachineRemove(req, res, url, params) },
450
+ ]);
451
+ }
452
+
453
+ module.exports = {
454
+ configure,
455
+ register,
456
+ ready,
457
+ handleMachinesList,
458
+ handleMachinesCreate,
459
+ handleMachinesPair,
460
+ handleMachineRemove,
461
+ };
@@ -32,9 +32,19 @@
32
32
  // rounds out the DISPATCH-ONLY additions.
33
33
 
34
34
  const { registerApiRoutes } = require('../api-router');
35
+ const remoteMachineProxy = require('../remote-machine-proxy');
35
36
 
36
37
  let _deps = null;
37
38
 
39
+ // Remote machine hub WP-4 (§6.5): a picker request naming another machine (?machine= or
40
+ // X-CTM-Machine) is answered from that box; every other request is the unchanged local handler.
41
+ function _machineRouted(route, local) {
42
+ return (req, res, url) => (remoteMachineProxy.isMachineRouted(req, url)
43
+ ? remoteMachineProxy.maybeProxyMachineRequest(req, res, url, { route })
44
+ .then((handled) => (handled ? undefined : local(req, res, url)))
45
+ : local(req, res, url));
46
+ }
47
+
38
48
  function configure({
39
49
  apiCtmCoopJobsStatus,
40
50
  apiCtmSessionRowsStatus,
@@ -59,6 +69,7 @@ function configure({
59
69
  apiDeleteHook,
60
70
  apiRestartCtm,
61
71
  restartGuardSnapshot,
72
+ proxyPreflight,
62
73
  wss,
63
74
  clientBuildDiagnosticDetails,
64
75
  attachedSessionIds,
@@ -93,6 +104,7 @@ function configure({
93
104
  apiDeleteHook,
94
105
  apiRestartCtm,
95
106
  restartGuardSnapshot,
107
+ proxyPreflight,
96
108
  wss,
97
109
  clientBuildDiagnosticDetails,
98
110
  attachedSessionIds,
@@ -213,9 +225,12 @@ async function handleConversationDebtRedrive(req, res, url) {
213
225
 
214
226
  function register() {
215
227
  registerApiRoutes('restart', [
216
- { method: 'GET', path: '/api/restart/ctm/preflight', handler: (req, res) => {
228
+ { method: 'GET', path: '/api/restart/ctm/preflight', handler: async (req, res) => {
229
+ // WP-3 D-10 (model-proxy-survives-ctm-restart): the model-proxy verdict is a live probe of the
230
+ // proxy's record and /_health, so it is awaited here and injected into the otherwise-sync snapshot.
231
+ const proxy = await _deps.proxyPreflight();
217
232
  res.writeHead(200, { 'Content-Type': 'application/json' });
218
- res.end(JSON.stringify({ ok: true, preflight: _deps.restartGuardSnapshot().preflight }));
233
+ res.end(JSON.stringify({ ok: true, preflight: _deps.restartGuardSnapshot(Date.now(), { proxy }).preflight }));
219
234
  } },
220
235
  ]);
221
236
  registerApiRoutes('misc-ops', [
@@ -248,9 +263,9 @@ function register() {
248
263
  handler: (req, res, url) => handleWorkspaceSessionsDelete(req, res, url),
249
264
  },
250
265
  { method: 'DELETE', prefix: '/api/workspaces/', handler: (req, res, url) => handleWorkspaceDelete(req, res, url) },
251
- { method: 'GET', path: '/api/projects', handler: (req, res) => _deps.apiGetProjects(req, res) },
252
- { method: 'GET', path: '/api/path-suggest', handler: (req, res, url) => _deps.apiPathSuggest(req, res, url) },
253
- { method: 'POST', path: '/api/path-status', handler: (req, res) => _deps.apiPathStatus(req, res) },
266
+ { method: 'GET', path: '/api/projects', handler: _machineRouted('GET /api/projects', (req, res) => _deps.apiGetProjects(req, res)) },
267
+ { method: 'GET', path: '/api/path-suggest', handler: _machineRouted('GET /api/path-suggest', (req, res, url) => _deps.apiPathSuggest(req, res, url)) },
268
+ { method: 'POST', path: '/api/path-status', handler: _machineRouted('POST /api/path-status', (req, res) => _deps.apiPathStatus(req, res)) },
254
269
  { method: 'GET', path: '/api/rules', handler: (req, res, url) => _deps.apiGetRules(req, res, url) },
255
270
  { method: 'PUT', path: '/api/rules', handler: (req, res) => _deps.apiPutRules(req, res) },
256
271
  { method: 'GET', path: '/api/rules/list', handler: (req, res) => _deps.apiListRules(req, res) },
@@ -2,21 +2,24 @@
2
2
 
3
3
  // Round-3 WP-5 T3 group 5 (design doc §5 D-1/D-3/D-8, §8 WP-5, §9.5 S-42):
4
4
  // small NEW module for the coding-CLI provider OAuth login broker cluster --
5
- // GET /api/providers/login-state and POST /api/providers/:id/login. This is
5
+ // GET /api/providers/login-state, POST /api/providers/:id/login and
6
+ // POST /api/providers/:id/login/code. This is
6
7
  // a distinct "providers" concept from lib/api/setup.js's LLM-provider
7
8
  // API-key setup (setup/providers, setup/save-provider, etc.): providerLoginBroker
8
9
  // tracks OAuth login flows for coding-agent CLIs (claude/codex/gemini), not
9
10
  // API-key-based model providers, so it gets its own module rather than
10
- // extending setup.js. DISPATCH-ONLY: both routes call providerLoginBroker
11
- // methods directly, no server.js function bodies to move.
11
+ // extending setup.js. DISPATCH-ONLY: every route calls a providerLoginBroker
12
+ // method directly, no server.js function bodies to move.
12
13
  //
13
- // The POST route carries a regex (`/^\/api\/providers\/([^/]+)\/login$/`)
14
+ // The POST routes carry a regex (`/^\/api\/providers\/([^/]+)\/login$/` and
15
+ // `/^\/api\/providers\/([^/]+)\/login\/code$/`)
14
16
  // the legacy chain's own scanner never counted (the `[^/]` character class
15
17
  // breaks bin/generate-route-golden.js's REGEX_RE, so this spec never showed
16
18
  // up in lint/route-golden.json's chain -- see lib/api/sessions.js's group-5
17
19
  // header for the same gap on /api/sessions/:id/interrupt etc.).
18
20
 
19
21
  const { registerApiRoutes } = require('../api-router');
22
+ const remoteMachineProxy = require('../remote-machine-proxy');
20
23
 
21
24
  let _deps = null;
22
25
 
@@ -24,6 +27,24 @@ function configure({ providerLoginBroker, readJsonRequestBody } = {}) {
24
27
  _deps = { providerLoginBroker, readJsonRequestBody };
25
28
  }
26
29
 
30
+ // WP-3 (remote-agent-sign-in-from-hub-browser-2026-10-25.html §5 D-7/D-9, §6): a sign-in request
31
+ // that names ANOTHER machine (`?machine=` / `X-CTM-Machine`) is answered from that box over the
32
+ // peer-bearer leg — the wrapper shape lib/api/misc-ops.js:41 uses. A request naming no machine
33
+ // (or THIS instance) is the unchanged local handler, byte for byte.
34
+ //
35
+ // Unlike misc-ops' routes (all exact paths), two of these are regex routes, so the wrapper
36
+ // forwards the router's `params` (api-router.js:136) as well: dropping it would read `params['1']`
37
+ // off undefined. `...args` keeps the local call identical to a directly-registered handler.
38
+ function _machineRouted(route, local) {
39
+ return (...args) => {
40
+ const [req, res, url] = args;
41
+ return remoteMachineProxy.isMachineRouted(req, url)
42
+ ? remoteMachineProxy.maybeProxyMachineRequest(req, res, url, { route })
43
+ .then((handled) => (handled ? undefined : local(...args)))
44
+ : local(...args);
45
+ };
46
+ }
47
+
27
48
  // WP-0: getState() now awaits the CLI account probe (lib/coding-agent-auth), so both
28
49
  // handlers are async — the api-router awaits handler promises before falling through.
29
50
  async function handleProvidersLoginState(req, res) {
@@ -52,13 +73,39 @@ async function handleProviderLogin(req, res, providerIdRaw) {
52
73
  res.end(JSON.stringify(result));
53
74
  }
54
75
 
76
+ // WP-1 (D-4/D-5/D-6): the code-return route. Body shaping only — the broker owns every
77
+ // validation (open flow, live tab, `codeEntry === 'paste'`, normalisation, state match) and
78
+ // the single PTY write, so this handler never decides anything about a code. An unreadable or
79
+ // oversized body simply arrives as `code: ''`, which the broker refuses as `code_format_invalid`.
80
+ async function handleProviderLoginCode(req, res, providerIdRaw) {
81
+ const providerId = decodeURIComponent(providerIdRaw || '');
82
+ let body = {};
83
+ if (_deps.readJsonRequestBody) {
84
+ try {
85
+ body = (await _deps.readJsonRequestBody(req, { limitBytes: 16 * 1024 })) || {};
86
+ } catch (error) {
87
+ // HANDLE: an unreadable/oversized body is not a 500 — the missing code is the refusal.
88
+ body = {};
89
+ }
90
+ }
91
+ const code = body && typeof body === 'object' && typeof body.code === 'string' ? body.code : '';
92
+ const result = await _deps.providerLoginBroker.submitCode(providerId, { code });
93
+ res.writeHead(result.status || 500, { 'Content-Type': 'application/json' });
94
+ res.end(JSON.stringify(result));
95
+ }
96
+
55
97
  function register() {
56
98
  registerApiRoutes('providers', [
57
- { method: 'GET', path: '/api/providers/login-state', handler: (req, res) => handleProvidersLoginState(req, res) },
99
+ { method: 'GET', path: '/api/providers/login-state', handler: _machineRouted('GET /api/providers/login-state', (req, res) => handleProvidersLoginState(req, res)) },
58
100
  {
59
101
  method: 'POST',
60
102
  regex: /^\/api\/providers\/([^/]+)\/login$/,
61
- handler: (req, res, url, params) => handleProviderLogin(req, res, params['1']),
103
+ handler: _machineRouted('POST /api/providers/:id/login', (req, res, url, params) => handleProviderLogin(req, res, params['1'])),
104
+ },
105
+ {
106
+ method: 'POST',
107
+ regex: /^\/api\/providers\/([^/]+)\/login\/code$/,
108
+ handler: _machineRouted('POST /api/providers/:id/login/code', (req, res, url, params) => handleProviderLoginCode(req, res, params['1'])),
62
109
  },
63
110
  ]);
64
111
  }
@@ -56,6 +56,31 @@ async function handleResumeFromStore(req, res, deps = _deps) {
56
56
  const cwd = _firstText(ctm.cwd, ctm.project_path, agent.project_path, body.cwd);
57
57
  if (!cwd) return send(res, 409, { error: 'no_cwd', message: 'No working directory is known for this session.' });
58
58
 
59
+ // D-5 (session-exit-close-truth §5): the rebuild is proven safe, never assumed. Ask the resume
60
+ // ladder -- exactly as the resume-rung route does -- whether the original transcript is still on
61
+ // this machine, and refuse the lossy reconstruction when it is. Only the ladder's refusal to
62
+ // answer (a throw) refuses too; an unrecognized rung is not evidence of an intact transcript.
63
+ try {
64
+ const resolve = deps.resolveResumeRung || resolveResumeRungOffThread;
65
+ const ladder = await resolve({ ..._ladderInput(identity, ctm.id, provider), dbPath: deps.getDbPath(), trashRoot: deps.trashRoot });
66
+ const rung = String((ladder && ladder.rung) || '');
67
+ if ((rung === 'native' && ladder.offerStoreFirst !== true) || rung === 'restore' || rung === 'trash') {
68
+ return send(res, 409, {
69
+ error: 'native_transcript_available',
70
+ rung,
71
+ message: 'The original transcript is still on this machine. Resume the session instead.',
72
+ });
73
+ }
74
+ } catch (error) {
75
+ // HANDLE: the ladder could not answer (read pool down, probes failed). Refuse rather than
76
+ // reconstruct on a guess; the card's "Check again" re-asks.
77
+ console.error('[resume-from-store] rung check failed:', String((error && error.message) || error));
78
+ return send(res, 409, {
79
+ error: 'rung_unknown',
80
+ message: 'CTM could not check whether the original transcript still exists. Try again.',
81
+ });
82
+ }
83
+
59
84
  try {
60
85
  const build = deps.buildResumeContext || buildResumeContextOffThread;
61
86
  const plan = await build({ dbPath: deps.getDbPath(), ctmSessionId: ctm.id });