create-walle 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/README.md +4 -0
  2. package/lib/owner-write-safety.js +135 -0
  3. package/lib/public-runtime.js +13 -5
  4. package/lib/release-schema.json +3 -2
  5. package/lib/runtime-closure-policy.json +6 -4
  6. package/package.json +1 -1
  7. package/template/bin/deps-doctor.js +1 -1
  8. package/template/bin/node-bin.sh +11 -0
  9. package/template/coding-task-manager/api-prompts.js +10 -22
  10. package/template/coding-task-manager/bin/claude-hook.js +7 -3
  11. package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
  12. package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
  13. package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
  14. package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
  15. package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
  16. package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
  17. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  18. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  19. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  20. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  21. package/template/coding-task-manager/db.js +705 -286
  22. package/template/coding-task-manager/git-utils.js +44 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
  24. package/template/coding-task-manager/lib/agent-presets.js +15 -0
  25. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  26. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  27. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  28. package/template/coding-task-manager/lib/api/ingress.js +35 -12
  29. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  30. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  31. package/template/coding-task-manager/lib/api/machines.js +461 -0
  32. package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
  33. package/template/coding-task-manager/lib/api/providers.js +53 -6
  34. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  35. package/template/coding-task-manager/lib/api/sessions.js +160 -8
  36. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  37. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  38. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  39. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  40. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  41. package/template/coding-task-manager/lib/auth-context.js +32 -2
  42. package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
  43. package/template/coding-task-manager/lib/auth-rules.js +186 -45
  44. package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
  45. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  46. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  47. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  48. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  49. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  50. package/template/coding-task-manager/lib/client-diag-handler.js +5 -0
  51. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  52. package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
  53. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  54. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  55. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  56. package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
  57. package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
  58. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  59. package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
  60. package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
  61. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  62. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  63. package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
  64. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  65. package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
  66. package/template/coding-task-manager/lib/db-domains.js +21 -2
  67. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  68. package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
  69. package/template/coding-task-manager/lib/feature-manifest.js +1 -1
  70. package/template/coding-task-manager/lib/fs-watcher.js +3 -2
  71. package/template/coding-task-manager/lib/harness-history-transition.js +3 -3
  72. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  73. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  74. package/template/coding-task-manager/lib/headless-term-service.js +55 -17
  75. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  76. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  77. package/template/coding-task-manager/lib/host-handoff.js +51 -13
  78. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  79. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  80. package/template/coding-task-manager/lib/host-stats.js +328 -0
  81. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  82. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  83. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  84. package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
  85. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
  86. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  87. package/template/coding-task-manager/lib/instance-manifest.js +13 -1
  88. package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
  89. package/template/coding-task-manager/lib/launch-model-override.js +160 -1
  90. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  91. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
  92. package/template/coding-task-manager/lib/lead-model-switch.js +3 -2
  93. package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
  94. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  95. package/template/coding-task-manager/lib/login-tripwire.js +1 -0
  96. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  97. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  98. package/template/coding-task-manager/lib/machine-registry.js +749 -0
  99. package/template/coding-task-manager/lib/machine-self.js +66 -0
  100. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +52 -4
  101. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  102. package/template/coding-task-manager/lib/media-journal.js +84 -27
  103. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  104. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  105. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  106. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  107. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  108. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  109. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  110. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  111. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  112. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  113. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  114. package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
  115. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  116. package/template/coding-task-manager/lib/native-history-control.js +2 -2
  117. package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
  118. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  119. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
  120. package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
  121. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  122. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  123. package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
  124. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  125. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  126. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  127. package/template/coding-task-manager/lib/read-pool-client.js +5 -2
  128. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  129. package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
  130. package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
  131. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  132. package/template/coding-task-manager/lib/remote-command-runtime.js +13 -1
  133. package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
  134. package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
  135. package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
  136. package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
  137. package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
  138. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  139. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  140. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  141. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  142. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  143. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  144. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  145. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  146. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  147. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  148. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  149. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  150. package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
  151. package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
  152. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  153. package/template/coding-task-manager/lib/root-prompt-admission.js +33 -2
  154. package/template/coding-task-manager/lib/root-prompt-flow.js +167 -33
  155. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  156. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  157. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  158. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  159. package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
  160. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  161. package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
  162. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  163. package/template/coding-task-manager/lib/session-create/provider-process.js +34 -37
  164. package/template/coding-task-manager/lib/session-create/response.js +30 -12
  165. package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
  166. package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
  167. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  168. package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
  169. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  170. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  171. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  172. package/template/coding-task-manager/lib/session-host-manager.js +152 -206
  173. package/template/coding-task-manager/lib/session-jobs.js +17 -9
  174. package/template/coding-task-manager/lib/session-lifecycle.js +18 -0
  175. package/template/coding-task-manager/lib/session-model-action-preconditions.js +9 -0
  176. package/template/coding-task-manager/lib/session-resource-sampler.js +19 -9
  177. package/template/coding-task-manager/lib/session-standup.js +67 -4
  178. package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
  179. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  180. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  181. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  182. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  183. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  184. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
  185. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  186. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  187. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  188. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  189. package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
  190. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  191. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  192. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  193. package/template/coding-task-manager/lib/transcript-source.js +8 -3
  194. package/template/coding-task-manager/lib/transcript-store.js +40 -0
  195. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  196. package/template/coding-task-manager/lib/typing-interaction-trace.js +92 -0
  197. package/template/coding-task-manager/lib/typing-worker-stages.js +60 -0
  198. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  199. package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
  200. package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
  201. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  202. package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
  203. package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
  204. package/template/coding-task-manager/lib/ws-router.js +68 -1
  205. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  206. package/template/coding-task-manager/oauth-proxy.js +99 -55
  207. package/template/coding-task-manager/package-lock.json +24 -0
  208. package/template/coding-task-manager/package.json +3 -0
  209. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  210. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  211. package/template/coding-task-manager/public/css/app.css +279 -1
  212. package/template/coding-task-manager/public/css/reviews.css +4 -0
  213. package/template/coding-task-manager/public/index.html +294 -223
  214. package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
  215. package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
  216. package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
  217. package/template/coding-task-manager/public/js/background-work.js +13 -7
  218. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
  219. package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
  220. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  221. package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
  222. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  223. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  224. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  225. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  226. package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
  227. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  228. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -8
  229. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  230. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  231. package/template/coding-task-manager/public/js/reviews.js +7 -3
  232. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  233. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  234. package/template/coding-task-manager/public/js/row-join.js +4 -2
  235. package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
  236. package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
  237. package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
  238. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  239. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  240. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  241. package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
  242. package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
  243. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  244. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  245. package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
  246. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  247. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  248. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -1
  249. package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
  250. package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
  251. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  252. package/template/coding-task-manager/public/m/app.css +226 -8
  253. package/template/coding-task-manager/public/m/app.js +502 -9
  254. package/template/coding-task-manager/public/m/index.html +38 -1
  255. package/template/coding-task-manager/public/m/machines.js +1313 -0
  256. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  257. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  258. package/template/coding-task-manager/public/m/sw.js +5 -0
  259. package/template/coding-task-manager/queue-engine.js +13 -0
  260. package/template/coding-task-manager/server/approvals.js +5 -6
  261. package/template/coding-task-manager/server/boot.js +136 -17
  262. package/template/coding-task-manager/server.js +902 -239
  263. package/template/coding-task-manager/session-integrity.js +7 -9
  264. package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
  265. package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
  266. package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
  267. package/template/coding-task-manager/workers/session-host-process.js +14 -3
  268. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  269. package/template/coding-task-manager/workers/worktree-worker.js +34 -0
  270. package/template/package-lock.json +2 -2
  271. package/template/package.json +1 -1
  272. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  273. package/template/shared/conversation-storage-v2-contract.js +42 -6
  274. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  275. package/template/shared/git-remote-key.js +83 -0
  276. package/template/shared/host-identity.js +98 -2
  277. package/template/shared/owner-write-safety.js +135 -0
  278. package/template/shared/public-runtime.js +13 -5
  279. package/template/shared/release-schema.json +3 -2
  280. package/template/shared/report-citations.js +14 -0
  281. package/template/shared/sqlite-write-lock.js +361 -223
  282. package/template/shared/storage/contract.js +35 -3
  283. package/template/wall-e/agent.js +21 -0
  284. package/template/wall-e/api-walle.js +5 -0
  285. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  286. package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
  287. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  288. package/template/wall-e/brain/migrations.js +51 -22
  289. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  290. package/template/wall-e/brain/owner-call-whitelist.js +16 -0
  291. package/template/wall-e/brain/schema-ensures.js +22 -0
  292. package/template/wall-e/brain.js +1 -1
  293. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  294. package/template/wall-e/coding/session-journal.js +28 -40
  295. package/template/wall-e/coding-orchestrator.js +23 -26
  296. package/template/wall-e/http/auth.js +18 -1
  297. package/template/wall-e/http/blog-views-api.js +318 -0
  298. package/template/wall-e/http/helper-rules-api.js +321 -0
  299. package/template/wall-e/http/model-proxy-providers.js +3 -1
  300. package/template/wall-e/http/telemetry-api.js +12 -0
  301. package/template/wall-e/lib/blog-view-policy.js +88 -0
  302. package/template/wall-e/lib/blog-view-store.js +169 -0
  303. package/template/wall-e/lib/client-config-lock.js +2 -1
  304. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  305. package/template/wall-e/lib/error-kinds.js +10 -1
  306. package/template/wall-e/lib/feature-manifest.js +1 -1
  307. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  308. package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
  309. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  310. package/template/wall-e/lib/helper-rules/grade.js +277 -0
  311. package/template/wall-e/lib/helper-rules/label.js +247 -0
  312. package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
  313. package/template/wall-e/lib/helper-rules/store.js +667 -0
  314. package/template/wall-e/lib/mcp-integration.js +5 -6
  315. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  316. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
  317. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  318. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  319. package/template/wall-e/llm/default-fallback.js +4 -0
  320. package/template/wall-e/llm/model-catalog.js +13 -4
  321. package/template/wall-e/llm/model-roles.js +6 -0
  322. package/template/wall-e/llm/portkey-http.js +150 -0
  323. package/template/wall-e/llm/portkey-sync.js +15 -145
  324. package/template/wall-e/llm/worker-mcp-routing.js +61 -0
  325. package/template/wall-e/llm/worker-route.js +43 -1
  326. package/template/wall-e/loops/brain-custom-writes.js +45 -0
  327. package/template/wall-e/mcp-protocol-contract.js +41 -1
  328. package/template/wall-e/mcp-server.js +13 -2
  329. package/template/wall-e/mcp-worker-schemas.js +20 -2
  330. package/template/wall-e/memory/ctm-context-client.js +25 -5
  331. package/template/wall-e/shared/release-schema.json +3 -2
  332. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  333. package/template/wall-e/shared/storage/contract.js +35 -3
  334. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  335. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
  336. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
  337. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
  338. package/template/wall-e/tools/local-tools.js +6 -4
  339. package/template/wall-e/workers/brain-owner-worker.js +9 -0
  340. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  341. package/template/wall-e/workers/telemetry-worker.js +8 -0
@@ -0,0 +1,749 @@
1
+ 'use strict';
2
+
3
+ // WP-0 (remote-machine-hub-sessions-2026-10-22.html §5 D-3/D-4, §6.1): the hub-side machine
4
+ // registry — the durable list of boxes this CTM can drive sessions on, plus the probe loop
5
+ // that keeps their liveness honest.
6
+ //
7
+ // Three rules from the design doc drive every decision in this file:
8
+ // D-3 identity is the box's MINTED instance id, pinned on first contact. The transport URL
9
+ // is replaceable config (it will be a 127.0.0.1 forward at home and a relay URL away),
10
+ // so a URL whose manifest instance id disagrees with the pinned row for that URL is
11
+ // refused (`machine_identity_mismatch`) rather than silently re-pointed.
12
+ // D-4 liveness is HUB-probed and persisted with the HUB clock. The box never dials us
13
+ // (NAT/corp networks); the hub already both pulls and pushes on its own tick, so the
14
+ // probe rides that tick.
15
+ // P3 Unknown != Dead. A machine keeps its previous health until 2 consecutive missed
16
+ // probes (`unknown`), and only reads `unreachable` after 60 s without a success. If
17
+ // EVERY enabled machine misses in one sweep, the sweep ALSO reports hubNetworkSuspect
18
+ // (far more likely this host's network) — advisory context for the UI, never a
19
+ // write-suppressor: the grace walk still runs, because suppressing it would freeze a
20
+ // stale `reachable` forever in the common away-from-home case (every box behind the
21
+ // home LAN), and with one registered machine `unreachable` could never be recorded at
22
+ // all. Blip protection comes from the grace thresholds themselves, not suppression.
23
+ //
24
+ // All durable writes go through the db owner (db.js's DB_OWNER_MUTATION_ACTIONS map) exactly
25
+ // like every other lib-side mutation; this module never opens a handle and never runs raw SQL.
26
+ // The consecutive-miss counters are deliberately MODULE state, not columns: they are probe
27
+ // bookkeeping, they die with the process, and they must never look like fact to a reader.
28
+ const { dispatchOwnerMutation } = require('./owner-mutation-disposition');
29
+
30
+ const DEFAULT_TIMEOUT_MS = 3000;
31
+ // D-4/P3 grace: a machine with no successful probe for this long reads `unreachable`.
32
+ const UNREACHABLE_AFTER_MS = 60_000;
33
+ // P3: `unknown` starts at the SECOND consecutive miss, so one dropped probe is not news.
34
+ const UNKNOWN_AFTER_MISSES = 2;
35
+
36
+ const HEALTH_STATES = Object.freeze(['reachable', 'unknown', 'unreachable', 'unhealthy']);
37
+
38
+ // WP-3 (DGX Spark coding readiness §2): how long a box's CLI sign-in verdict is trusted before the
39
+ // next probe re-reads it. Sign-in changes only when a human acts, so a minute is plenty; the sweep
40
+ // rides the same tick as the health probe and adds no timer of its own.
41
+ const AGENT_AUTH_REFRESH_MS = 60_000;
42
+ // The three states a box's coding agent can be in, as the hub reads them: 'signed_in' |
43
+ // 'signed_out' | 'unknown'. 'unknown' is the honest answer for "never fetched", "the box did not
44
+ // answer", and "the box answered something we do not recognize" alike (P3 Unknown != Dead): only
45
+ // an explicit `signedIn:false` refuses a forward.
46
+
47
+ // WP-2 (machine-pool-picker-pairing-dashboard-2026-10-24.html D-12/D-16, §6.3): how long a box's
48
+ // own host sample (load, memory, disk, GPU, tasks, lastUsedAt) is trusted before the next probe
49
+ // re-reads it. The SAME 60 s cadence as the sign-in read, on the SAME tick, in the SAME loop — this
50
+ // module still owns exactly ONE timer site (`_getJsonBox`), so no new stabilization-lint surface.
51
+ const HOST_STATS_REFRESH_MS = 60_000;
52
+
53
+ // instance_id -> consecutive missed probes. Module state by design (see the header).
54
+ const _missStreak = new Map();
55
+ // instance_id -> { claude, codex, checkedAt } — the box's coding-CLI sign-in verdict as the hub
56
+ // last read it (WP-3). Module state for the same reason as _missStreak: it is probe bookkeeping
57
+ // with a freshness stamp, it dies with the process, and a stale copy must never look like fact to a
58
+ // reader (a restart reads `unknown`, which never refuses a forward).
59
+ const _agentAuth = new Map();
60
+ // instance_id -> { sample, checkedAt } (D-12, D-16). The box's own host sample as the hub last read
61
+ // it from ITS /api/host/stats?latest=1, plus the hub-clock stamp that gates the next re-read. Same
62
+ // module-state contract as _agentAuth: a stale number must never look fresh, and a box that stops
63
+ // answering keeps its last entry with a FROZEN checkedAt (never advanced without an answer).
64
+ const _hostStats = new Map();
65
+ // instance_id -> the `executionHost` block of the last manifest this hub read from the box (D-4
66
+ // rev 2). The manifest is the ONLY place a member's user settings (displayName/favorite/hidden) and
67
+ // its os/arch/hostname live — the roster deliberately carries none of them (P-1) — so the sweep
68
+ // caches what the probe already fetched instead of asking again.
69
+ const _manifestHost = new Map();
70
+ // The last sweep's self-suspicion verdict, surfaced by GET /api/machines. Not persisted per
71
+ // machine (design §7 "not persisted per machine") — it is a hub-wide observation about one sweep.
72
+ let _lastSweep = { hubNetworkSuspect: false, at: 0 };
73
+ // Bumped by every registry write this process makes (register, remove, seed, a probe that changed
74
+ // a row). Readers that keep a snapshot (lib/remote-machine-catalog.js) compare it instead of
75
+ // trusting a TTL alone: a snapshot taken a moment before a registration must not hide the new
76
+ // machine from the relay and the merged list.
77
+ let _revision = 0;
78
+
79
+ /** The registry's write revision (see above). */
80
+ function registryRevision() {
81
+ return _revision;
82
+ }
83
+
84
+ /** Record a registry write made outside this module (lib/api/machines.js's remove route). */
85
+ function noteRegistryChanged() {
86
+ _revision += 1;
87
+ }
88
+
89
+ function _string(value, max = 1000) {
90
+ return String(value == null ? '' : value).trim().slice(0, max);
91
+ }
92
+
93
+ function _now(deps = {}) {
94
+ return typeof deps.now === 'function' ? Number(deps.now()) || 0 : Date.now();
95
+ }
96
+
97
+ function _trimUrl(url) {
98
+ return _string(url, 1000).replace(/\/+$/, '');
99
+ }
100
+
101
+ // The manifest's instance id, from the same three shapes server.js's golive probe accepts
102
+ // (server.js:13009): {manifest:{instanceId}}, {instance:{…}} and a bare manifest body.
103
+ function _manifestInstanceId(body) {
104
+ const manifest = (body && (body.manifest || body.instance)) || body || {};
105
+ return _string(
106
+ manifest.instanceId || manifest.instance_id || manifest.id
107
+ || (body && (body.instanceId || body.instance_id)) || '',
108
+ 160,
109
+ );
110
+ }
111
+
112
+ /**
113
+ * D-4 rev 2 / P-1: the machine-owned settings and host identity, read off the manifest's
114
+ * `executionHost` block. Display fields NEVER come from the roster (P-1) — the manifest is the
115
+ * authority, and this is the one place the sweep reads them.
116
+ */
117
+ function _manifestHostBlock(body) {
118
+ const manifest = (body && (body.manifest || body.instance)) || body || {};
119
+ const host = manifest.executionHost || manifest.execution_host || {};
120
+ return {
121
+ displayName: _string(host.displayName || host.display_name || host.hostLabel || host.hostname || '', 160),
122
+ favorite: host.favorite === true || host.favorite === 1,
123
+ hidden: host.hidden === true || host.hidden === 1,
124
+ os: _string(host.os || host.platform || '', 40),
125
+ arch: _string(host.arch || '', 40),
126
+ hostname: _string(host.hostname || '', 160),
127
+ };
128
+ }
129
+
130
+ function _manifestDisplayName(body) {
131
+ return _manifestHostBlock(body).displayName;
132
+ }
133
+
134
+ function _hostLabel(url) {
135
+ try { return _string(new URL(url).host, 160); } catch { return _string(url, 160); }
136
+ }
137
+
138
+ // The hub's own peer credential on the probe, the same headers lib/hosted-sync-worker.js sends to
139
+ // /api/sync/manifest: a box behind a relay or tunnel sees forwarding markers, stops trusting the
140
+ // caller as loopback and answers a credential-less probe 401. INJECTED by the caller (requiring the
141
+ // header builder from here would be a dependency cycle, as with parseSyncPeers below); none
142
+ // configured probes without one (a same-host box still answers over loopback).
143
+ //
144
+ // WP-2 / P-8: the credential is a PER-TARGET decision on a pool member — the shared pool secret goes
145
+ // to a member, the legacy env bearer to a box that never joined — so the caller may inject
146
+ // `peerHeadersFor(instanceId)` instead of one fixed object. The fixed `peerHeaders` object is kept
147
+ // for callers (and tests) that dial with a single credential; when neither is given the probe goes
148
+ // out credential-less, exactly as before.
149
+ function _peerHeadersFor(deps = {}) {
150
+ const builder = deps.peerHeadersFor;
151
+ const fixed = deps.peerHeaders && typeof deps.peerHeaders === 'object' ? deps.peerHeaders : {};
152
+ if (typeof builder !== 'function') return () => fixed;
153
+ return (instanceId) => {
154
+ try {
155
+ const built = builder(instanceId);
156
+ return built && typeof built === 'object' ? built : {};
157
+ } catch {
158
+ return {}; // expected: a credential-builder fault degrades to an anonymous probe, never a failed sweep
159
+ }
160
+ };
161
+ }
162
+
163
+ /**
164
+ * P-4: the URLs to dial for a machine, in order — the roster's `urls` (LAN first, then the relay
165
+ * origin), falling back to the single pinned `url` when the row predates the roster (P-8 legacy rows
166
+ * and seeded placeholders).
167
+ */
168
+ function _dialUrls(machine = {}) {
169
+ const list = (Array.isArray(machine.urls) ? machine.urls : []).map((u) => _trimUrl(u)).filter(Boolean);
170
+ if (list.length) return list;
171
+ const pinned = _trimUrl(machine.url);
172
+ return pinned ? [pinned] : [];
173
+ }
174
+
175
+ // One JSON GET against a box, with the AbortController + unref'd timer shape server.js's golive
176
+ // probe uses, so a hung box can never hold the tick open past timeoutMs. Shared by the manifest and
177
+ // the agent-sign-in probes: this is the ONE timer site in the file (a second copy would be a new
178
+ // stabilization-lint ownership surface for no gain).
179
+ async function _getJsonBox(url, pathname, { fetchImpl, timeoutMs = DEFAULT_TIMEOUT_MS, peerHeaders = {} } = {}) {
180
+ const fetchFn = fetchImpl || global.fetch;
181
+ if (typeof fetchFn !== 'function') return { ok: false, error: 'fetch_unavailable', status: null, body: null };
182
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
183
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
184
+ if (timer && typeof timer.unref === 'function') timer.unref();
185
+ try {
186
+ const res = await fetchFn(`${url}${pathname}`, {
187
+ method: 'GET',
188
+ headers: { ...peerHeaders, Accept: 'application/json' },
189
+ signal: controller ? controller.signal : undefined,
190
+ });
191
+ if (!res || res.ok === false) {
192
+ return { ok: false, error: `http_${res ? res.status : 'unknown'}`, status: res ? res.status : null, body: null };
193
+ }
194
+ let body = null;
195
+ try {
196
+ body = await res.json();
197
+ } catch {
198
+ // expected: a non-JSON 200 body is reachable-but-degraded, not unreachable — each caller
199
+ // reports it with its own verdict (manifest_invalid / an 'unknown' agent state).
200
+ body = null;
201
+ }
202
+ return { ok: true, error: '', status: res.status, body };
203
+ } catch (err) {
204
+ return { ok: false, error: err?.message || String(err), status: null, body: null };
205
+ } finally {
206
+ if (timer) clearTimeout(timer);
207
+ }
208
+ }
209
+
210
+ // One manifest read, in the shape server.js's golive probe expects.
211
+ async function _probeManifest(url, options = {}) {
212
+ const fetched = await _getJsonBox(url, '/api/sync/manifest', options);
213
+ if (!fetched.ok) return { ok: false, code: 'machine_unreachable', error: fetched.error };
214
+ const instanceId = _manifestInstanceId(fetched.body);
215
+ // Reachable but degraded: the box answered and told us nothing we can pin or trust.
216
+ if (!instanceId) return { ok: false, code: 'machine_manifest_invalid', error: 'manifest_instance_id_missing', body: fetched.body };
217
+ const host = _manifestHostBlock(fetched.body);
218
+ return { ok: true, instanceId, displayName: host.displayName, host };
219
+ }
220
+
221
+ /**
222
+ * P-4 dial order: try the machine's URLs in order and use the first whose manifest instance id is
223
+ * the row's. A URL that answers with ANOTHER box's id is refused for that URL (D-3) and the next one
224
+ * is dialled — an SSH forward that was re-pointed must not silently re-key the row.
225
+ *
226
+ * `machine_unreachable` is reported only when no URL answered at all: the sweep's "every box silent ⇒
227
+ * hubNetworkSuspect" verdict (P3) must not fire for a box that answered on one of its URLs.
228
+ */
229
+ async function _probeMachineManifest(machine = {}, options = {}) {
230
+ const urls = _dialUrls(machine);
231
+ let answered = null; // the last attempt that got an HTTP answer (mismatch / no instance id)
232
+ let last = null;
233
+ for (const url of urls) {
234
+ const probe = await _probeManifest(url, options);
235
+ if (probe.ok && probe.instanceId === machine.instanceId) return { ...probe, url };
236
+ if (probe.ok) {
237
+ answered = { ok: false, code: 'machine_identity_mismatch', error: `manifest ${probe.instanceId} != pinned ${machine.instanceId}` };
238
+ last = answered;
239
+ continue;
240
+ }
241
+ if (probe.code === 'machine_manifest_invalid') {
242
+ answered = probe;
243
+ last = probe;
244
+ continue;
245
+ }
246
+ last = probe;
247
+ }
248
+ return answered || last || { ok: false, code: 'machine_unreachable', error: 'machine_has_no_url' };
249
+ }
250
+
251
+ /**
252
+ * D-12/D-16: read a box's own latest host sample from ITS `/api/host/stats?latest=1` (WP-1). Never
253
+ * throws and never reports anything but the sample or null: a non-2xx, an unparseable body, a
254
+ * missing `latest` and a transport throw alike read "no sample this time" and leave the previous
255
+ * entry (and its `checkedAt`) exactly where they were.
256
+ */
257
+ async function _probeHostStats(url, options = {}) {
258
+ const fetched = await _getJsonBox(url, '/api/host/stats?latest=1', options);
259
+ if (!fetched.ok) return null;
260
+ const latest = fetched.body && fetched.body.latest;
261
+ return latest && typeof latest === 'object' ? latest : null;
262
+ }
263
+
264
+ /**
265
+ * P-5: read a member's roster version + membership from ITS `/api/pool/roster`. Returns
266
+ * `{ version, members }` or null (a version-less / unreachable / unparseable answer is "nothing to
267
+ * adopt", never an error).
268
+ */
269
+ async function _pullRoster(url, options = {}) {
270
+ const fetched = await _getJsonBox(url, '/api/pool/roster', options);
271
+ if (!fetched.ok) return null;
272
+ const body = fetched.body;
273
+ if (!body || typeof body !== 'object') return null;
274
+ const version = body.version && typeof body.version === 'object' ? body.version : null;
275
+ if (!version) return null;
276
+ return { version, members: Array.isArray(body.members) ? body.members : [] };
277
+ }
278
+
279
+ /** The pool store: injected in tests, the machine-pool leaf singleton in production. Never throws. */
280
+ function _machinePool(deps = {}) {
281
+ if (deps.machinePool && typeof deps.machinePool.hasPool === 'function') return deps.machinePool;
282
+ try {
283
+ return require('./machine-pool').getMachinePool();
284
+ } catch {
285
+ return null; // expected: machine-pool is a pure leaf; an unreadable store means "no pool"
286
+ }
287
+ }
288
+
289
+ /**
290
+ * This machine's own minted id, or '' when the caller has not injected one (the P-5 delete leg is
291
+ * then skipped by db.js — an upsert-only convergence, never a wrong deletion).
292
+ *
293
+ * INJECTED, never required: `lib/instance-identity-access.js` reaches db.js, and db.js reaches
294
+ * lib/hosted-sync-worker.js, which reaches this module — so requiring it here closes a lib/**
295
+ * dependency cycle (dependency-lint ceiling 0; verified, not assumed: the first cut of this file did
296
+ * exactly that and the lint reported
297
+ * `lib/instance-identity-access.js -> lib/machine-registry.js -> lib/hosted-sync-worker.js -> db.js`).
298
+ * `selfInstanceId` may be a string or a `() => string` getter, because identity is only ready after
299
+ * boot while a sweep repeats for the life of the process.
300
+ */
301
+ function _selfInstanceId(deps = {}) {
302
+ const source = deps.selfInstanceId;
303
+ try {
304
+ return _string(typeof source === 'function' ? source() : source, 160);
305
+ } catch {
306
+ return ''; // expected: identity is not ready yet (pre-boot / pre-DB) — never a sweep failure
307
+ }
308
+ }
309
+
310
+ /**
311
+ * P-5 sweep pull: a member that was offline during a roster push converges on its next sweep. Adopt
312
+ * only a strictly NEWER version (revision, then updatedAt, then `by` — machine-pool.isNewer); an
313
+ * equal or older one is ignored (the receiver answers with its own version, and the pusher decides).
314
+ * Self is never written into its own roster (db.js's replaceRosterMachines also refuses it).
315
+ */
316
+ async function _maybePullRoster(machine, url, { deps = {}, fetchImpl, timeoutMs, peerHeaders = {} } = {}) {
317
+ const pool = _machinePool(deps);
318
+ if (!pool || typeof pool.hasPool !== 'function' || !pool.hasPool()) return null;
319
+ const pulled = await _pullRoster(url, { fetchImpl, timeoutMs, peerHeaders });
320
+ if (!pulled) return null;
321
+ if (typeof pool.isNewer !== 'function' || !pool.isNewer(pulled.version, pool.rosterVersion())) return null;
322
+
323
+ const self = _selfInstanceId(deps);
324
+ const members = pulled.members
325
+ .map((member) => ({
326
+ instanceId: _string(member && (member.instanceId || member.instance_id), 160),
327
+ urls: (Array.isArray(member && member.urls) ? member.urls : []).map((u) => _trimUrl(u)).filter(Boolean),
328
+ }))
329
+ .filter((member) => member.instanceId && member.instanceId !== self);
330
+
331
+ // Only PREVIOUS pool members may be deleted (a legacy env-token row is never one, P-8).
332
+ const removable = typeof pool.members === 'function' ? pool.members() : [];
333
+ await dispatchOwnerMutation(deps.db, 'replaceRosterMachines', [{ members, selfInstanceId: self, removable }], 'machine registry roster pull');
334
+ if (typeof pool.setRosterVersion === 'function') await pool.setRosterVersion(pulled.version);
335
+ if (typeof pool.setMembers === 'function') await pool.setMembers(members.map((member) => member.instanceId));
336
+ noteRegistryChanged();
337
+ return { version: pulled.version, members };
338
+ }
339
+
340
+ /** D-4: cache the manifest's executionHost block; a display/settings CHANGE is registry news. */
341
+ function _cacheManifestHost(instanceId, host) {
342
+ const id = _string(instanceId, 160);
343
+ if (!id || !host) return;
344
+ const previous = _manifestHost.get(id) || null;
345
+ _manifestHost.set(id, host);
346
+ if (!previous) return; // a first read is not news (the registration already bumped the revision)
347
+ if (previous.displayName !== host.displayName || previous.favorite !== host.favorite
348
+ || previous.hidden !== host.hidden || previous.os !== host.os
349
+ || previous.arch !== host.arch || previous.hostname !== host.hostname) {
350
+ noteRegistryChanged();
351
+ }
352
+ }
353
+
354
+ /** A box's logged-in flag -> the hub's three-state verdict (unknown for anything unexpected). */
355
+ function _agentLoginState(signedIn) {
356
+ if (signedIn === true) return 'signed_in';
357
+ if (signedIn === false) return 'signed_out';
358
+ return 'unknown';
359
+ }
360
+
361
+ /**
362
+ * WP-3: read a box's own coding-CLI sign-in truth from ITS `/api/providers/login-state` (the route
363
+ * a signed-out box answers with `providers.<agent>.account.signedIn: false`). It NEVER throws and
364
+ * never reports anything but a three-state verdict: a non-2xx, an unparseable body, an unexpected
365
+ * shape and a transport throw all read `unknown` — only an explicit false is a refusal.
366
+ */
367
+ async function _probeAgentAuth(url, options = {}) {
368
+ const fetched = await _getJsonBox(url, '/api/providers/login-state', options);
369
+ const unknown = { claude: 'unknown', codex: 'unknown' };
370
+ if (!fetched.ok) return unknown;
371
+ const providers = (fetched.body && typeof fetched.body === 'object' && fetched.body.providers) || {};
372
+ const signedInFor = (agent) => {
373
+ const entry = providers[agent];
374
+ if (!entry || typeof entry !== 'object') return undefined;
375
+ const account = entry.account;
376
+ return account && typeof account === 'object' ? account.signedIn : undefined;
377
+ };
378
+ return { claude: _agentLoginState(signedInFor('claude')), codex: _agentLoginState(signedInFor('codex')) };
379
+ }
380
+
381
+ function _rows(deps = {}) {
382
+ const db = deps.db;
383
+ if (!db || typeof db.listMachineRows !== 'function') return [];
384
+ return db.listMachineRows() || [];
385
+ }
386
+
387
+ function _isPinned(row = {}) {
388
+ // A row is pinned once something observed the box: registerMachine stamps both fields, and
389
+ // the probe loop keeps last_seen_at moving. A seeded row (enabled=0, never probed) is NOT
390
+ // pinned — it carries no evidence about which box its URL points at.
391
+ // S-9 (machine-pool-picker-pairing-dashboard-2026-10-24.html §9): a dead `row.pinned` field was
392
+ // dropped here — nothing ever wrote it. The picker's star is `favorite`, a machine-owned setting
393
+ // (D-4, lib/machine-self.js + the manifest), which is NOT this identity pin and never was.
394
+ return Number(row.lastSeenAt || row.last_seen_at || 0) > 0;
395
+ }
396
+
397
+ function _machineView(row = {}) {
398
+ const lastSeenAt = Number(row.lastSeenAt ?? row.last_seen_at ?? 0) || 0;
399
+ const enabled = Number(row.enabled ?? 1) !== 0;
400
+ const health = _string(row.health, 40) || 'unknown';
401
+ const instanceId = _string(row.instanceId || row.instance_id, 160);
402
+ // D-4 rev 2: the machine owns its display name / favorite / hidden and serves them in its own
403
+ // manifest, which the sweep caches per instance. The registry row's own label is the fallback for
404
+ // a box this hub has not read a manifest from yet.
405
+ const manifest = _manifestHost.get(instanceId) || null;
406
+ // D-12/D-16: the box's own host sample (load, memory, disk, GPU, tasks) and its newest session
407
+ // create. `checkedAt` is the HUB clock stamp of the read, so a reader can label a stale number
408
+ // instead of showing it as current.
409
+ const stats = _hostStats.get(instanceId) || null;
410
+ return {
411
+ instanceId,
412
+ displayName: _string((manifest && manifest.displayName) || row.displayName || row.display_name, 200),
413
+ url: _string(row.url, 1000),
414
+ urls: (Array.isArray(row.urls) ? row.urls : []).map((u) => _string(u, 1000)).filter(Boolean),
415
+ walleUrl: _string(row.walleUrl || row.walle_url, 1000),
416
+ enabled,
417
+ health,
418
+ healthDetail: _string(row.healthDetail || row.health_detail, 500),
419
+ lastSeenAt,
420
+ createdAt: _string(row.createdAt || row.created_at, 60),
421
+ updatedAt: _string(row.updatedAt || row.updated_at, 60),
422
+ // D-4 rev 2: machine-owned settings, read from the last manifest this hub probed.
423
+ favorite: !!(manifest && manifest.favorite),
424
+ hidden: !!(manifest && manifest.hidden),
425
+ os: _string(manifest && manifest.os, 40),
426
+ arch: _string(manifest && manifest.arch, 40),
427
+ hostname: _string(manifest && manifest.hostname, 160),
428
+ // null = this hub has never read a sample for this box (never a zeroed-out sample).
429
+ stats: stats ? { sample: stats.sample, checkedAt: stats.checkedAt } : null,
430
+ lastUsedAt: Number((stats && stats.sample && stats.sample.lastUsedAt) || 0) || 0,
431
+ // D-5: "unknown" until the box has ever answered; a disabled row is never a picker
432
+ // candidate, and only reachable/unhealthy ever render as cards.
433
+ pickerState: !enabled ? 'unknown' : health,
434
+ visible: enabled && (health === 'reachable' || health === 'unhealthy'),
435
+ };
436
+ }
437
+
438
+ function listMachines(deps = {}) {
439
+ const machines = _rows(deps).map((row) => _machineView(row));
440
+ return { machines, hubNetworkSuspect: !!_lastSweep.hubNetworkSuspect };
441
+ }
442
+
443
+ /**
444
+ * Register (or re-point) a machine by URL. One successful manifest probe IS the pin (D-3):
445
+ * an unreachable box cannot be registered, because there would be nothing to key it on.
446
+ *
447
+ * @returns {{ok: true, machine: object}|{ok: false, code: string, error: string}}
448
+ */
449
+ async function registerMachine({ url, displayName, walleUrl } = {}, deps = {}) {
450
+ const baseUrl = _trimUrl(url);
451
+ if (!baseUrl) return { ok: false, code: 'machine_url_required', error: 'url is required' };
452
+ const probe = await _probeManifest(baseUrl, {
453
+ fetchImpl: deps.fetchImpl || deps.fetch,
454
+ timeoutMs: Number(deps.timeoutMs || DEFAULT_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS,
455
+ // P-8: registration probes the URL BEFORE the box's identity is known, so the caller's
456
+ // no-target builder is used (the pool-vs-env split needs an instanceId the hub does not have yet).
457
+ peerHeaders: _peerHeadersFor(deps)(''),
458
+ });
459
+ if (!probe.ok) return { ok: false, code: probe.code, error: probe.error };
460
+
461
+ const rows = _rows(deps).map((row) => _machineView(row));
462
+ const byUrl = rows.find((row) => row.url === baseUrl);
463
+ if (byUrl && byUrl.instanceId !== probe.instanceId && _isPinned(byUrl)) {
464
+ // The URL already points at a DIFFERENT box (or the forward was re-pointed). Refuse
465
+ // instead of silently re-keying: sessions already routed to the pinned id would move.
466
+ return {
467
+ ok: false,
468
+ code: 'machine_identity_mismatch',
469
+ error: `manifest ${probe.instanceId} != pinned ${byUrl.instanceId}`,
470
+ };
471
+ }
472
+ const byId = rows.find((row) => row.instanceId === probe.instanceId);
473
+ if (byId && byId.url !== baseUrl) {
474
+ return {
475
+ ok: false,
476
+ code: 'machine_identity_mismatch',
477
+ error: `instance ${probe.instanceId} already registered at ${byId.url}`,
478
+ };
479
+ }
480
+
481
+ // A seeded placeholder for this URL (enabled=0, never probed — S-19's env-seeded row) holds
482
+ // the URL but not the identity: replace it with the pinned row rather than keying on a guess.
483
+ if (byUrl && byUrl.instanceId !== probe.instanceId) {
484
+ await dispatchOwnerMutation(deps.db, 'deleteMachineRow', [byUrl.instanceId], 'machine registry reseed');
485
+ }
486
+ const now = _now(deps);
487
+ await dispatchOwnerMutation(deps.db, 'upsertMachineRow', [{
488
+ instanceId: probe.instanceId,
489
+ displayName: _string(displayName, 200) || probe.displayName || _hostLabel(baseUrl),
490
+ url: baseUrl,
491
+ walleUrl: _trimUrl(walleUrl),
492
+ enabled: 1,
493
+ }], 'machine registry register');
494
+ _missStreak.delete(probe.instanceId);
495
+ await dispatchOwnerMutation(deps.db, 'recordMachineProbe', [probe.instanceId, {
496
+ health: 'reachable', healthDetail: '', lastSeenAt: now,
497
+ }], 'machine registry register probe');
498
+ noteRegistryChanged();
499
+ const machine = _rows(deps).map((row) => _machineView(row)).find((row) => row.instanceId === probe.instanceId);
500
+ return { ok: true, machine: machine || null };
501
+ }
502
+
503
+ /**
504
+ * One probe sweep over every ENABLED machine. Zero machines ⇒ zero fetches (AC-0d).
505
+ *
506
+ * Grace walk (D-4/P3), per machine:
507
+ * probe ok -> health 'reachable', last_seen_at = hub now
508
+ * manifest ok, no instance id -> health 'unhealthy' + detail (the box answered; it is
509
+ * degraded, not gone — so last_seen_at still moves)
510
+ * probe failed, 1st miss -> previous health kept
511
+ * probe failed, >= 2 misses -> health 'unknown'
512
+ * probe failed, no success for >= 60 s (hub clock) -> health 'unreachable'
513
+ * If EVERY enabled machine failed in this sweep, the sweep ADDITIONALLY reports
514
+ * hubNetworkSuspect: true — the UI's cue to say "network?" rather than "all machines down".
515
+ * The walk above still runs: a hub-side blip is absorbed by the grace thresholds (one miss
516
+ * keeps the previous health and writes nothing; `unreachable` needs 60 s of silence), while a
517
+ * genuine all-boxes-down (laptop away from home) converges to the truth instead of freezing a
518
+ * stale `reachable` forever.
519
+ *
520
+ * The returned {reachable, unknown, unreachable, unhealthy} are the RESULTING-HEALTH histogram
521
+ * (the same states lib/instance-sync-status.js reports through summarizeMachines), so a machine
522
+ * still inside its grace window counts under the health it kept, not under "missed".
523
+ */
524
+ async function probeMachinesOnce(options = {}, deps = {}) {
525
+ const fetchImpl = options.fetchImpl || deps.fetchImpl || deps.fetch;
526
+ const timeoutMs = Number(options.timeoutMs || DEFAULT_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS;
527
+ const now = _now(deps);
528
+ const enabled = _rows(deps).map((row) => _machineView(row)).filter((row) => row.enabled);
529
+ if (!enabled.length) {
530
+ _lastSweep = { hubNetworkSuspect: false, at: now };
531
+ return { ok: true, probed: 0, reachable: 0, unknown: 0, unreachable: 0, unhealthy: 0, hubNetworkSuspect: false, results: [] };
532
+ }
533
+
534
+ // P-8: one credential PER TARGET (pool secret for a member, the legacy env bearer for a box that
535
+ // never joined), so the header builder is resolved per machine, not once per sweep.
536
+ const headersFor = _peerHeadersFor(deps);
537
+ const results = await Promise.all(enabled.map(async (machine) => {
538
+ const probe = await _probeMachineManifest(machine, { fetchImpl, timeoutMs, peerHeaders: headersFor(machine.instanceId) });
539
+ // P-4: the URL that actually answered is the row's dial address from here on. A row whose
540
+ // pinned url went dead (an SSH forward torn down, a relay origin that took over) is re-pointed
541
+ // through the existing owner write path rather than re-dialled to nowhere every sweep.
542
+ if (probe.ok && _trimUrl(machine.url) !== probe.url) {
543
+ await dispatchOwnerMutation(deps.db, 'upsertMachineRow', [{
544
+ instanceId: machine.instanceId,
545
+ displayName: machine.displayName,
546
+ url: probe.url,
547
+ walleUrl: machine.walleUrl,
548
+ enabled: machine.enabled ? 1 : 0,
549
+ }], 'machine registry dial order');
550
+ noteRegistryChanged();
551
+ }
552
+ return { machine, probe };
553
+ }));
554
+
555
+ // P3 self-suspicion is ADVISORY: all boxes silent at once is most likely THIS host's network,
556
+ // so the sweep says so — but the grace walk below still runs (see the function comment).
557
+ const hubNetworkSuspect = results.every((row) => !row.probe.ok && row.probe.code === 'machine_unreachable');
558
+ _lastSweep = { hubNetworkSuspect, at: now };
559
+
560
+ const counts = { reachable: 0, unknown: 0, unreachable: 0, unhealthy: 0 };
561
+ for (const { machine, probe } of results) {
562
+ let health;
563
+ let detail = '';
564
+ if (probe.ok) {
565
+ health = 'reachable';
566
+ _missStreak.delete(machine.instanceId);
567
+ } else if (probe.code === 'machine_manifest_invalid') {
568
+ // Reachable-but-degraded: the box is up, so this is not a liveness miss either.
569
+ health = 'unhealthy';
570
+ detail = probe.error || 'manifest_invalid';
571
+ _missStreak.delete(machine.instanceId);
572
+ } else {
573
+ const misses = (_missStreak.get(machine.instanceId) || 0) + 1;
574
+ _missStreak.set(machine.instanceId, misses);
575
+ // The 60 s rule is measured FROM A RECORDED SUCCESS. A machine the hub has never seen
576
+ // answer (enabled before its first probe, so last_seen_at is still 0) is `unknown` after
577
+ // 2 misses — never `unreachable`, which would be a claim about silence we never measured
578
+ // (P3 Unknown != Dead).
579
+ const silentMs = machine.lastSeenAt > 0 ? now - machine.lastSeenAt : 0;
580
+ if (machine.lastSeenAt > 0 && silentMs >= UNREACHABLE_AFTER_MS) health = 'unreachable';
581
+ else if (misses >= UNKNOWN_AFTER_MISSES) health = 'unknown';
582
+ else health = machine.health;
583
+ // The detail describes the STANDING verdict: a first miss inside the grace window keeps
584
+ // the previous health AND its detail (so a blip sweep writes nothing at all).
585
+ detail = health === machine.health ? machine.healthDetail : (probe.error || '');
586
+ }
587
+ counts[_string(health, 40)] = (counts[_string(health, 40)] || 0) + 1;
588
+ const next = { health, healthDetail: detail, lastSeenAt: probe.ok || probe.code === 'machine_manifest_invalid' ? now : machine.lastSeenAt };
589
+ if (next.health !== machine.health || next.healthDetail !== machine.healthDetail || next.lastSeenAt !== machine.lastSeenAt) {
590
+ await dispatchOwnerMutation(deps.db, 'recordMachineProbe', [machine.instanceId, next], 'machine registry probe');
591
+ noteRegistryChanged();
592
+ }
593
+ }
594
+
595
+ // WP-3: the box's CLI sign-in truth, refreshed on the SAME tick as its health — never on its own
596
+ // timer. Only a box whose manifest probe just answered is asked: a box that did not answer tells
597
+ // us nothing about its CLIs, so its stored entry is left exactly as it was (never blanked to
598
+ // 'unknown', which would weaken a verdict we already hold). A refresh older than
599
+ // AGENT_AUTH_REFRESH_MS (or a box never fetched) is re-read; anything fresher is reused.
600
+ for (const { machine, probe } of results) {
601
+ if (!probe.ok) continue;
602
+ const machineHeaders = headersFor(machine.instanceId);
603
+ const url = probe.url || machine.url;
604
+ // D-4: the manifest we just read IS the machine's settings + host identity. Cached here so
605
+ // /api/machines serves the machine-owned displayName/favorite/hidden and os/arch/hostname.
606
+ _cacheManifestHost(machine.instanceId, probe.host);
607
+
608
+ const previous = _agentAuth.get(machine.instanceId);
609
+ if (!previous || now - previous.checkedAt >= AGENT_AUTH_REFRESH_MS) {
610
+ const probed = await _probeAgentAuth(url, { fetchImpl, timeoutMs, peerHeaders: machineHeaders });
611
+ _agentAuth.set(machine.instanceId, { ...probed, checkedAt: now });
612
+ // A sign-in CHANGE is registry news (a reader keeping a snapshot must re-read); a first read
613
+ // or an identical re-read is not.
614
+ if (previous && (previous.claude !== probed.claude || previous.codex !== probed.codex)) noteRegistryChanged();
615
+ }
616
+
617
+ // D-12/P-5: the box's own host sample and its roster version, refreshed on the SAME tick and
618
+ // under the SAME per-instance 60 s stamp as the sign-in read — one pass over `results`, no
619
+ // timer of this module's own. Both are asked ONLY of a box whose manifest probe just answered
620
+ // (a silent box teaches us nothing, and its stored entry keeps its FROZEN checkedAt — AC-2b).
621
+ const statsEntry = _hostStats.get(machine.instanceId);
622
+ if (statsEntry && now - (statsEntry.attemptedAt || statsEntry.checkedAt) < HOST_STATS_REFRESH_MS) continue;
623
+ const sample = await _probeHostStats(url, { fetchImpl, timeoutMs, peerHeaders: machineHeaders });
624
+ // Two stamps: `attemptedAt` gates the 60 s refresh on every attempt; `checkedAt` moves ONLY when
625
+ // a sample arrived. A failed stats read keeps the sample we hold AND its old checkedAt, so the
626
+ // picker's "stats stale" (checkedAt older than 180 s) is driven by the stats read itself.
627
+ _hostStats.set(machine.instanceId, sample
628
+ ? { sample, checkedAt: now, attemptedAt: now }
629
+ : { sample: (statsEntry && statsEntry.sample) || null, checkedAt: (statsEntry && statsEntry.checkedAt) || 0, attemptedAt: now });
630
+ await _maybePullRoster(machine, url, { deps, fetchImpl, timeoutMs, peerHeaders: machineHeaders });
631
+ }
632
+
633
+ return {
634
+ ok: true,
635
+ probed: results.length,
636
+ reachable: counts.reachable || 0,
637
+ unknown: counts.unknown || 0,
638
+ unreachable: counts.unreachable || 0,
639
+ unhealthy: counts.unhealthy || 0,
640
+ hubNetworkSuspect,
641
+ results: results.map((row) => ({
642
+ instanceId: row.machine.instanceId,
643
+ url: row.machine.url,
644
+ ok: !!row.probe.ok,
645
+ ...(row.probe.ok ? {} : { error: row.probe.error }),
646
+ })),
647
+ };
648
+ }
649
+
650
+ /**
651
+ * Boot seeding (§8 WP-0 "seed-from-CTM_SYNC_PEERS"): every configured sync peer that is not
652
+ * already registered becomes a DISABLED machines row — visible, never probed, so the user can
653
+ * enable it in Settings → Machines (WP-5). Env is a seed, never an authority: it never
654
+ * auto-enables a machine.
655
+ */
656
+ function seedMachinesFromSyncPeers(deps = {}) {
657
+ // The peer list (or the parser) is INJECTED by the caller — lib/hosted-sync-worker.js passes
658
+ // its own parsed peers. Requiring hosted-sync-worker from here would be a dependency cycle
659
+ // (it requires this module for the probe ride), so this module never imports it.
660
+ let peers = Array.isArray(deps.peers) ? deps.peers : [];
661
+ if (!peers.length && typeof deps.parseSyncPeers === 'function') {
662
+ try { peers = deps.parseSyncPeers(deps.env || process.env) || []; } catch { peers = []; } // expected: malformed CTM_SYNC_PEERS seeds nothing
663
+ }
664
+ if (!peers.length) return { seeded: 0, skipped: 0 };
665
+ // No owner handle (unit tests, receiver-only boots) ⇒ nothing to seed; report the peers as
666
+ // skipped rather than counting writes that never happened.
667
+ if (!deps.db || typeof deps.db.upsertMachineRow !== 'function') return { seeded: 0, skipped: peers.length };
668
+ const known = new Set();
669
+ for (const row of _rows(deps).map((r) => _machineView(r))) {
670
+ if (row.instanceId) known.add(row.instanceId);
671
+ if (row.url) known.add(row.url);
672
+ }
673
+ let seeded = 0;
674
+ let skipped = 0;
675
+ for (const peer of peers) {
676
+ const url = _trimUrl(peer.url);
677
+ if (!url) { skipped += 1; continue; }
678
+ const instanceId = _string(peer.instanceId, 160);
679
+ if ((instanceId && known.has(instanceId)) || known.has(url)) { skipped += 1; continue; }
680
+ const key = instanceId || `unpinned:${url}`;
681
+ if (known.has(key)) { skipped += 1; continue; }
682
+ // void: boot seeding is fire-and-forget by contract (a failed seed is a missing card the
683
+ // next boot retries, surfaced via status().machineSeed) — never a boot failure.
684
+ void dispatchOwnerMutation(deps.db, 'upsertMachineRow', [{
685
+ instanceId: key,
686
+ displayName: instanceId || _hostLabel(url),
687
+ url,
688
+ walleUrl: _trimUrl(peer.walleUrl),
689
+ enabled: 0,
690
+ }], 'machine registry seed');
691
+ known.add(key);
692
+ known.add(url);
693
+ seeded += 1;
694
+ }
695
+ if (seeded) noteRegistryChanged();
696
+ return { seeded, skipped };
697
+ }
698
+
699
+ /** S-30: the machines section of /api/instance/sync-status (lib/instance-sync-status.js). */
700
+ function summarizeMachines(deps = {}) {
701
+ const machines = _rows(deps).map((row) => _machineView(row));
702
+ const summary = { count: machines.length, reachable: 0, unknown: 0, unreachable: 0, unhealthy: 0, hubNetworkSuspect: !!_lastSweep.hubNetworkSuspect };
703
+ for (const machine of machines) {
704
+ if (Object.prototype.hasOwnProperty.call(summary, machine.health)) summary[machine.health] += 1;
705
+ }
706
+ return summary;
707
+ }
708
+
709
+ /** WP-3: the box's last-read coding-CLI sign-in verdict, or null when it was never fetched. */
710
+ function agentAuthFor(instanceId) {
711
+ return _agentAuth.get(_string(instanceId, 160)) || null;
712
+ }
713
+
714
+ /**
715
+ * D-12/D-16: the box's own host sample as this hub last read it, with the hub-clock stamp of that
716
+ * read, or null when the box has never been asked successfully. Readers use `checkedAt` to label a
717
+ * stale number (the UI says "stats stale" past 180 s) instead of printing it as current.
718
+ */
719
+ function hostStatsFor(instanceId) {
720
+ const entry = _hostStats.get(_string(instanceId, 160));
721
+ return entry ? { sample: entry.sample, checkedAt: entry.checkedAt } : null;
722
+ }
723
+
724
+ /** Test seam: the miss counters, the agent-auth/host-stats/manifest caches and the sweep verdict. */
725
+ function _resetMachineRegistryStateForTests() {
726
+ _missStreak.clear();
727
+ _agentAuth.clear();
728
+ _hostStats.clear();
729
+ _manifestHost.clear();
730
+ _lastSweep = { hubNetworkSuspect: false, at: 0 };
731
+ }
732
+
733
+ module.exports = {
734
+ UNREACHABLE_AFTER_MS,
735
+ UNKNOWN_AFTER_MISSES,
736
+ HEALTH_STATES,
737
+ AGENT_AUTH_REFRESH_MS,
738
+ HOST_STATS_REFRESH_MS,
739
+ agentAuthFor,
740
+ hostStatsFor,
741
+ listMachines,
742
+ registryRevision,
743
+ noteRegistryChanged,
744
+ registerMachine,
745
+ probeMachinesOnce,
746
+ seedMachinesFromSyncPeers,
747
+ summarizeMachines,
748
+ _resetMachineRegistryStateForTests,
749
+ };