create-walle 0.10.1 → 0.10.3

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 (681) hide show
  1. package/README.md +7 -1
  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 +11 -8
  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 +55 -29
  10. package/template/coding-task-manager/bin/claude-hook.js +62 -4
  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/ctm-relay-client.js +35 -12
  18. package/template/coding-task-manager/bin/land.js +2069 -0
  19. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  20. package/template/coding-task-manager/bin/memory-census.js +4 -2
  21. package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
  22. package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
  23. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  24. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  25. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  26. package/template/coding-task-manager/db.js +860 -317
  27. package/template/coding-task-manager/git-utils.js +44 -0
  28. package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
  29. package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
  30. package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
  31. package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
  32. package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
  33. package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
  34. package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
  35. package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
  36. package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
  37. package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
  38. package/template/coding-task-manager/lib/adopted-host-redial.js +47 -7
  39. package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
  40. package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
  41. package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
  42. package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
  43. package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
  44. package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
  45. package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
  46. package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
  47. package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
  48. package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
  49. package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
  50. package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
  51. package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
  52. package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
  53. package/template/coding-task-manager/lib/agent-definitions.js +19 -0
  54. package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
  55. package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
  56. package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
  57. package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
  58. package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
  59. package/template/coding-task-manager/lib/agent-presets.js +99 -1
  60. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  61. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  62. package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
  63. package/template/coding-task-manager/lib/agent-version-service.js +74 -11
  64. package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
  65. package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
  66. package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
  67. package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
  68. package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
  69. package/template/coding-task-manager/lib/api/fleet.js +118 -0
  70. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  71. package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
  72. package/template/coding-task-manager/lib/api/ingress.js +43 -12
  73. package/template/coding-task-manager/lib/api/landing.js +403 -0
  74. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  75. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  76. package/template/coding-task-manager/lib/api/machines.js +1150 -0
  77. package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
  78. package/template/coding-task-manager/lib/api/misc-ops.js +25 -5
  79. package/template/coding-task-manager/lib/api/providers.js +53 -6
  80. package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
  81. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  82. package/template/coding-task-manager/lib/api/session-attach.js +623 -0
  83. package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
  84. package/template/coding-task-manager/lib/api/sessions.js +237 -59
  85. package/template/coding-task-manager/lib/api/setup-network.js +544 -16
  86. package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
  87. package/template/coding-task-manager/lib/api/setup.js +829 -10
  88. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  89. package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
  90. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  91. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  92. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  93. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  94. package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
  95. package/template/coding-task-manager/lib/auth-context.js +32 -2
  96. package/template/coding-task-manager/lib/auth-rate-limit.js +64 -1
  97. package/template/coding-task-manager/lib/auth-rules.js +474 -62
  98. package/template/coding-task-manager/lib/background-stop.js +369 -0
  99. package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
  100. package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
  101. package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
  102. package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
  103. package/template/coding-task-manager/lib/broadcast-serialize.js +7 -1
  104. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  105. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  106. package/template/coding-task-manager/lib/build-info.js +4 -2
  107. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  108. package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
  109. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  110. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  111. package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
  112. package/template/coding-task-manager/lib/client-diag-handler.js +69 -2
  113. package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
  114. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  115. package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
  116. package/template/coding-task-manager/lib/codex-harness-qualification.js +55 -20
  117. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  118. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  119. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  120. package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
  121. package/template/coding-task-manager/lib/coding-agent-models.js +48 -42
  122. package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
  123. package/template/coding-task-manager/lib/coding-session-model-adapters.js +70 -4
  124. package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
  125. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  126. package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
  127. package/template/coding-task-manager/lib/coding-session-model-runtime.js +24 -6
  128. package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
  129. package/template/coding-task-manager/lib/coding-session-model-state.js +17 -0
  130. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  131. package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
  132. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  133. package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
  134. package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
  135. package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
  136. package/template/coding-task-manager/lib/conversation-sync.js +355 -0
  137. package/template/coding-task-manager/lib/create/resolve-launch-params.js +104 -13
  138. package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
  139. package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
  140. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  141. package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
  142. package/template/coding-task-manager/lib/db-domain-tables.js +8 -0
  143. package/template/coding-task-manager/lib/db-domains.js +22 -2
  144. package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
  145. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  146. package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
  147. package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
  148. package/template/coding-task-manager/lib/db-schema-source-files.js +6 -0
  149. package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
  150. package/template/coding-task-manager/lib/feature-manifest.js +5 -1
  151. package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
  152. package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
  153. package/template/coding-task-manager/lib/fleet-hub.js +762 -0
  154. package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
  155. package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
  156. package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
  157. package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
  158. package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
  159. package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
  160. package/template/coding-task-manager/lib/fleet-update.js +798 -0
  161. package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
  162. package/template/coding-task-manager/lib/fs-watcher.js +61 -44
  163. package/template/coding-task-manager/lib/harness-history-transition.js +52 -11
  164. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  165. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  166. package/template/coding-task-manager/lib/headless-term-service.js +139 -25
  167. package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
  168. package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
  169. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  170. package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
  171. package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
  172. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  173. package/template/coding-task-manager/lib/host-handoff.js +50 -15
  174. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  175. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  176. package/template/coding-task-manager/lib/host-stats.js +475 -0
  177. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  178. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  179. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  180. package/template/coding-task-manager/lib/hosted-sync-worker.js +141 -310
  181. package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
  182. package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
  183. package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
  184. package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
  185. package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
  186. package/template/coding-task-manager/lib/hub-bridge.js +398 -0
  187. package/template/coding-task-manager/lib/idle-notify.js +491 -0
  188. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +12 -0
  189. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  190. package/template/coding-task-manager/lib/instance-manifest.js +18 -2
  191. package/template/coding-task-manager/lib/instance-sync-status.js +33 -8
  192. package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
  193. package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
  194. package/template/coding-task-manager/lib/landing/config.js +536 -0
  195. package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
  196. package/template/coding-task-manager/lib/landing/gate.js +360 -0
  197. package/template/coding-task-manager/lib/landing/git.js +344 -0
  198. package/template/coding-task-manager/lib/landing/lease.js +247 -0
  199. package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
  200. package/template/coding-task-manager/lib/landing/proof.js +223 -0
  201. package/template/coding-task-manager/lib/landing/queue.js +998 -0
  202. package/template/coding-task-manager/lib/landing/radar.js +489 -0
  203. package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
  204. package/template/coding-task-manager/lib/landing/state.js +263 -0
  205. package/template/coding-task-manager/lib/landing/stop.js +49 -0
  206. package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
  207. package/template/coding-task-manager/lib/landing/sync.js +456 -0
  208. package/template/coding-task-manager/lib/landing/train.js +885 -0
  209. package/template/coding-task-manager/lib/landing/window.js +57 -0
  210. package/template/coding-task-manager/lib/launch-environment.js +121 -0
  211. package/template/coding-task-manager/lib/launch-model-override.js +193 -8
  212. package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
  213. package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
  214. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  215. package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
  216. package/template/coding-task-manager/lib/lead-model-options.js +160 -3
  217. package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
  218. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +409 -72
  219. package/template/coding-task-manager/lib/lead-model-switch.js +115 -10
  220. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
  221. package/template/coding-task-manager/lib/lead-model-transition-store.js +85 -9
  222. package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
  223. package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
  224. package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
  225. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  226. package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
  227. package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
  228. package/template/coding-task-manager/lib/login-tripwire.js +1 -0
  229. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  230. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  231. package/template/coding-task-manager/lib/machine-registry.js +1494 -0
  232. package/template/coding-task-manager/lib/machine-self.js +66 -0
  233. package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
  234. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +485 -57
  235. package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
  236. package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
  237. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  238. package/template/coding-task-manager/lib/media-journal.js +84 -27
  239. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  240. package/template/coding-task-manager/lib/memory-join.js +337 -0
  241. package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
  242. package/template/coding-task-manager/lib/memory-sync.js +565 -0
  243. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  244. package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
  245. package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
  246. package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
  247. package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
  248. package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
  249. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  250. package/template/coding-task-manager/lib/model-availability.js +727 -0
  251. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
  252. package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
  253. package/template/coding-task-manager/lib/model-id-rule.js +19 -0
  254. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +8 -0
  255. package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
  256. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +6 -0
  257. package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
  258. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  259. package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
  260. package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
  261. package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
  262. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  263. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  264. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  265. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  266. package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
  267. package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
  268. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  269. package/template/coding-task-manager/lib/model-proxy/tokens.js +147 -5
  270. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +18 -0
  271. package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
  272. package/template/coding-task-manager/lib/native-history-control.js +7 -5
  273. package/template/coding-task-manager/lib/native-lead-settings-client.js +46 -5
  274. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  275. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +776 -75
  276. package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
  277. package/template/coding-task-manager/lib/opencode-cli.js +480 -0
  278. package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
  279. package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
  280. package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
  281. package/template/coding-task-manager/lib/owner-action-manifest-data.json +15 -1
  282. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  283. package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
  284. package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
  285. package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
  286. package/template/coding-task-manager/lib/perf-summary.js +7 -0
  287. package/template/coding-task-manager/lib/perf-tracker.js +63 -2
  288. package/template/coding-task-manager/lib/project-config.js +7 -0
  289. package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
  290. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  291. package/template/coding-task-manager/lib/provider-login-broker.js +264 -24
  292. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  293. package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
  294. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  295. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  296. package/template/coding-task-manager/lib/read-pool-client.js +94 -4
  297. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  298. package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
  299. package/template/coding-task-manager/lib/recent-catalog-query.js +41 -0
  300. package/template/coding-task-manager/lib/recent-session-catalog-response.js +76 -10
  301. package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
  302. package/template/coding-task-manager/lib/relay-client.js +57 -3
  303. package/template/coding-task-manager/lib/relink-audit.js +34 -5
  304. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  305. package/template/coding-task-manager/lib/remote-command-runtime.js +13 -1
  306. package/template/coding-task-manager/lib/remote-machine-catalog.js +1357 -0
  307. package/template/coding-task-manager/lib/remote-machine-create.js +1233 -0
  308. package/template/coding-task-manager/lib/remote-machine-link.js +1426 -0
  309. package/template/coding-task-manager/lib/remote-machine-proxy.js +654 -0
  310. package/template/coding-task-manager/lib/remote-session-forward.js +364 -0
  311. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  312. package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
  313. package/template/coding-task-manager/lib/restart-guard.js +18 -6
  314. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  315. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  316. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  317. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  318. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  319. package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
  320. package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
  321. package/template/coding-task-manager/lib/resume-admission.js +17 -3
  322. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  323. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  324. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  325. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  326. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  327. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  328. package/template/coding-task-manager/lib/review-repo-registry.js +74 -18
  329. package/template/coding-task-manager/lib/root-delivery-runtime.js +74 -17
  330. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  331. package/template/coding-task-manager/lib/root-prompt-admission.js +33 -2
  332. package/template/coding-task-manager/lib/root-prompt-flow.js +168 -34
  333. package/template/coding-task-manager/lib/runtime-contract.js +3 -1
  334. package/template/coding-task-manager/lib/schema-indexes.js +1 -0
  335. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  336. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  337. package/template/coding-task-manager/lib/session-context-locator.js +742 -0
  338. package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
  339. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  340. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  341. package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
  342. package/template/coding-task-manager/lib/session-create/delegate-worker.js +179 -20
  343. package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
  344. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  345. package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
  346. package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
  347. package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
  348. package/template/coding-task-manager/lib/session-create/prepared.js +201 -9
  349. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  350. package/template/coding-task-manager/lib/session-create/provider-process.js +193 -64
  351. package/template/coding-task-manager/lib/session-create/response.js +37 -19
  352. package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
  353. package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
  354. package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
  355. package/template/coding-task-manager/lib/session-create/transcript.js +14 -5
  356. package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
  357. package/template/coding-task-manager/lib/session-create/worker-continuation.js +50 -2
  358. package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
  359. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  360. package/template/coding-task-manager/lib/session-create/worker-observability.js +101 -9
  361. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  362. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  363. package/template/coding-task-manager/lib/session-empty.js +13 -10
  364. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  365. package/template/coding-task-manager/lib/session-host-manager.js +212 -206
  366. package/template/coding-task-manager/lib/session-jobs.js +20 -10
  367. package/template/coding-task-manager/lib/session-lifecycle.js +28 -4
  368. package/template/coding-task-manager/lib/session-model-action-preconditions.js +15 -2
  369. package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
  370. package/template/coding-task-manager/lib/session-park.js +738 -0
  371. package/template/coding-task-manager/lib/session-path-grant.js +13 -4
  372. package/template/coding-task-manager/lib/session-resource-sampler.js +19 -9
  373. package/template/coding-task-manager/lib/session-standup.js +67 -4
  374. package/template/coding-task-manager/lib/session-status-ledger.js +147 -6
  375. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  376. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  377. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  378. package/template/coding-task-manager/lib/session-transport.js +17 -0
  379. package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
  380. package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
  381. package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
  382. package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
  383. package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
  384. package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
  385. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  386. package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
  387. package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
  388. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  389. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +25 -23
  390. package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
  391. package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
  392. package/template/coding-task-manager/lib/submission-ack.js +90 -2
  393. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  394. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  395. package/template/coding-task-manager/lib/systemd-scope.js +115 -0
  396. package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
  397. package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
  398. package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
  399. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  400. package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
  401. package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
  402. package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
  403. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  404. package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
  405. package/template/coding-task-manager/lib/transcript-frame-cache.js +20 -5
  406. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  407. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  408. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  409. package/template/coding-task-manager/lib/transcript-source.js +44 -7
  410. package/template/coding-task-manager/lib/transcript-store.js +49 -0
  411. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  412. package/template/coding-task-manager/lib/typing-interaction-trace.js +198 -3
  413. package/template/coding-task-manager/lib/typing-worker-stages.js +60 -0
  414. package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
  415. package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
  416. package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
  417. package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
  418. package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
  419. package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
  420. package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
  421. package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
  422. package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
  423. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  424. package/template/coding-task-manager/lib/wake-skip-reasons.js +47 -0
  425. package/template/coding-task-manager/lib/walle/turn.js +1 -1
  426. package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
  427. package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
  428. package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
  429. package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
  430. package/template/coding-task-manager/lib/walle-row-history.js +495 -0
  431. package/template/coding-task-manager/lib/walle-supervisor.js +130 -27
  432. package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
  433. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  434. package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
  435. package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
  436. package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
  437. package/template/coding-task-manager/lib/ws/misc.js +1 -1
  438. package/template/coding-task-manager/lib/ws/state-sync.js +125 -6
  439. package/template/coding-task-manager/lib/ws/terminal-attach.js +123 -1
  440. package/template/coding-task-manager/lib/ws-router.js +68 -1
  441. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  442. package/template/coding-task-manager/oauth-proxy.js +211 -170
  443. package/template/coding-task-manager/package-lock.json +34 -94
  444. package/template/coding-task-manager/package.json +7 -3
  445. package/template/coding-task-manager/protocol/message-table.json +10 -0
  446. package/template/coding-task-manager/protocol/messages.js +10 -0
  447. package/template/coding-task-manager/protocol/ws-schema.json +20 -0
  448. package/template/coding-task-manager/protocol/ws-schema.md +2 -0
  449. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  450. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  451. package/template/coding-task-manager/public/css/app.css +449 -1
  452. package/template/coding-task-manager/public/css/reviews.css +4 -0
  453. package/template/coding-task-manager/public/css/walle-session.css +4 -0
  454. package/template/coding-task-manager/public/index.html +766 -465
  455. package/template/coding-task-manager/public/js/agent-command.js +86 -0
  456. package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
  457. package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
  458. package/template/coding-task-manager/public/js/app/machines-page.js +2572 -0
  459. package/template/coding-task-manager/public/js/app/models-tab.js +658 -73
  460. package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
  461. package/template/coding-task-manager/public/js/app/worktrees-panel.js +191 -7
  462. package/template/coding-task-manager/public/js/background-work.js +203 -28
  463. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +181 -13
  464. package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
  465. package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
  466. package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
  467. package/template/coding-task-manager/public/js/core/session-commands.js +847 -5
  468. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  469. package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
  470. package/template/coding-task-manager/public/js/document-review-links.js +9 -1
  471. package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
  472. package/template/coding-task-manager/public/js/history-app-seam.js +48 -4
  473. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  474. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  475. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  476. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  477. package/template/coding-task-manager/public/js/machine-pool-shared.js +991 -0
  478. package/template/coding-task-manager/public/js/message-renderer.js +4 -3
  479. package/template/coding-task-manager/public/js/model-picker.js +3 -1
  480. package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
  481. package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
  482. package/template/coding-task-manager/public/js/pasted-content.js +236 -0
  483. package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
  484. package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
  485. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  486. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -8
  487. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  488. package/template/coding-task-manager/public/js/prompts.js +11 -10
  489. package/template/coding-task-manager/public/js/protocol.js +10 -0
  490. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  491. package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
  492. package/template/coding-task-manager/public/js/reviews.js +7 -3
  493. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  494. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  495. package/template/coding-task-manager/public/js/row-join.js +4 -2
  496. package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
  497. package/template/coding-task-manager/public/js/session-search-utils.js +64 -21
  498. package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
  499. package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
  500. package/template/coding-task-manager/public/js/state-sync-client.js +14 -1
  501. package/template/coding-task-manager/public/js/stream-view.js +6 -5
  502. package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
  503. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  504. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  505. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  506. package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
  507. package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
  508. package/template/coding-task-manager/public/js/terminal/runtime-12.js +60 -3
  509. package/template/coding-task-manager/public/js/terminal/runtime-16.js +16 -12
  510. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  511. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  512. package/template/coding-task-manager/public/js/terminal/runtime-20.js +211 -74
  513. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  514. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  515. package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
  516. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -1
  517. package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
  518. package/template/coding-task-manager/public/js/terminal-wheel.js +62 -5
  519. package/template/coding-task-manager/public/js/walle/chat-conversation.js +15 -7
  520. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  521. package/template/coding-task-manager/public/js/walle.js +6 -0
  522. package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
  523. package/template/coding-task-manager/public/m/app.css +366 -8
  524. package/template/coding-task-manager/public/m/app.js +1006 -74
  525. package/template/coding-task-manager/public/m/index.html +55 -1
  526. package/template/coding-task-manager/public/m/machines.js +2147 -0
  527. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  528. package/template/coding-task-manager/public/m/private-api.js +31 -1
  529. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  530. package/template/coding-task-manager/public/m/sw.js +5 -0
  531. package/template/coding-task-manager/public/usage.html +1 -0
  532. package/template/coding-task-manager/queue-engine.js +13 -0
  533. package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
  534. package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
  535. package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
  536. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
  537. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
  538. package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
  539. package/template/coding-task-manager/server/approvals.js +5 -6
  540. package/template/coding-task-manager/server/boot.js +136 -17
  541. package/template/coding-task-manager/server.js +2819 -11885
  542. package/template/coding-task-manager/session-integrity.js +71 -13
  543. package/template/coding-task-manager/workers/db-owner-worker.js +72 -9
  544. package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
  545. package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
  546. package/template/coding-task-manager/workers/read-pool-worker.js +93 -12
  547. package/template/coding-task-manager/workers/session-host-pool-process.js +24 -10
  548. package/template/coding-task-manager/workers/session-host-process.js +19 -8
  549. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  550. package/template/coding-task-manager/workers/watch-worker.js +105 -0
  551. package/template/coding-task-manager/workers/worktree-worker.js +202 -33
  552. package/template/ctm-relay/lib/protocol.js +14 -1
  553. package/template/package-lock.json +2 -2
  554. package/template/package.json +1 -1
  555. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  556. package/template/shared/backup-retention.js +82 -0
  557. package/template/shared/conversation-admission.js +2 -0
  558. package/template/shared/conversation-provenance.js +3 -7
  559. package/template/shared/conversation-storage-v2-contract.js +47 -6
  560. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  561. package/template/shared/git-remote-key.js +83 -0
  562. package/template/shared/host-identity.js +98 -2
  563. package/template/shared/linked-worktree.js +51 -0
  564. package/template/shared/macos-fda-target.js +57 -0
  565. package/template/shared/orphan-parents.js +55 -0
  566. package/template/shared/owner-write-safety.js +135 -0
  567. package/template/shared/public-runtime.js +13 -5
  568. package/template/shared/release-schema.json +3 -2
  569. package/template/shared/report-citations.js +14 -0
  570. package/template/shared/sqlite-write-lock.js +361 -223
  571. package/template/shared/storage/contract.js +38 -3
  572. package/template/wall-e/adapters/imessage.js +12 -0
  573. package/template/wall-e/agent.js +92 -4
  574. package/template/wall-e/api-walle.js +26 -1
  575. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  576. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
  577. package/template/wall-e/bin/walle-mcp-stdio.js +14 -8
  578. package/template/wall-e/brain/brain-join.js +859 -0
  579. package/template/wall-e/brain/brain-sync-membership.js +20 -0
  580. package/template/wall-e/brain/brain-sync-store.js +131 -77
  581. package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
  582. package/template/wall-e/brain/brain-sync-v2.js +578 -0
  583. package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
  584. package/template/wall-e/brain/deterministic-id.js +51 -0
  585. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  586. package/template/wall-e/brain/memories.js +2 -2
  587. package/template/wall-e/brain/migrations.js +91 -28
  588. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  589. package/template/wall-e/brain/model-identity-migration.js +1 -1
  590. package/template/wall-e/brain/owner-call-whitelist.js +39 -7
  591. package/template/wall-e/brain/schema-ensures.js +22 -0
  592. package/template/wall-e/brain/schema.js +45 -85
  593. package/template/wall-e/brain.js +172 -36
  594. package/template/wall-e/chat.js +3 -3
  595. package/template/wall-e/coding/agent-shell-env.js +7 -2
  596. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  597. package/template/wall-e/coding/prompt-bundle.js +66 -21
  598. package/template/wall-e/coding/session-journal.js +28 -40
  599. package/template/wall-e/coding-orchestrator.js +23 -26
  600. package/template/wall-e/config/main-loop-exemptions.json +2 -2
  601. package/template/wall-e/http/admin-api.js +37 -0
  602. package/template/wall-e/http/auth.js +18 -1
  603. package/template/wall-e/http/blog-views-api.js +318 -0
  604. package/template/wall-e/http/gws-api.js +4 -0
  605. package/template/wall-e/http/helper-rules-api.js +327 -0
  606. package/template/wall-e/http/model-admin.js +8 -1
  607. package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
  608. package/template/wall-e/http/model-proxy-providers.js +177 -2
  609. package/template/wall-e/http/model-route-health.js +157 -0
  610. package/template/wall-e/http/sync-api.js +278 -464
  611. package/template/wall-e/http/telemetry-api.js +12 -0
  612. package/template/wall-e/jobs/job-runner.js +28 -1
  613. package/template/wall-e/lib/blog-view-policy.js +88 -0
  614. package/template/wall-e/lib/blog-view-store.js +169 -0
  615. package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
  616. package/template/wall-e/lib/client-config-lock.js +2 -1
  617. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  618. package/template/wall-e/lib/ctm-data-dir.js +26 -0
  619. package/template/wall-e/lib/derivation-home.js +147 -0
  620. package/template/wall-e/lib/error-kinds.js +10 -1
  621. package/template/wall-e/lib/feature-manifest.js +1 -1
  622. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  623. package/template/wall-e/lib/helper-rules/evaluate.js +398 -0
  624. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  625. package/template/wall-e/lib/helper-rules/grade.js +320 -0
  626. package/template/wall-e/lib/helper-rules/label.js +247 -0
  627. package/template/wall-e/lib/helper-rules/lead-call-hook.js +555 -0
  628. package/template/wall-e/lib/helper-rules/store.js +669 -0
  629. package/template/wall-e/lib/landing-mcp-tools.js +170 -0
  630. package/template/wall-e/lib/mcp-client-config.js +133 -14
  631. package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
  632. package/template/wall-e/lib/mcp-integration.js +63 -19
  633. package/template/wall-e/lib/parent-death.js +7 -1
  634. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  635. package/template/wall-e/lib/scheduler-worker-jobs.js +12 -4
  636. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  637. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  638. package/template/wall-e/llm/default-fallback.js +4 -0
  639. package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
  640. package/template/wall-e/llm/delegate-vendor-default.js +12 -5
  641. package/template/wall-e/llm/model-catalog.js +13 -4
  642. package/template/wall-e/llm/model-roles.js +7 -1
  643. package/template/wall-e/llm/native-delegate-selection.js +56 -8
  644. package/template/wall-e/llm/portkey-http.js +150 -0
  645. package/template/wall-e/llm/portkey-sync.js +15 -145
  646. package/template/wall-e/llm/worker-delegate-args.js +72 -7
  647. package/template/wall-e/llm/worker-mcp-routing.js +117 -7
  648. package/template/wall-e/llm/worker-route.js +43 -1
  649. package/template/wall-e/loops/brain-custom-writes.js +64 -0
  650. package/template/wall-e/loops/ingest.js +17 -1
  651. package/template/wall-e/loops/think.js +19 -4
  652. package/template/wall-e/mcp-contract.js +222 -6
  653. package/template/wall-e/mcp-protocol-contract.js +77 -2
  654. package/template/wall-e/mcp-server.js +141 -31
  655. package/template/wall-e/mcp-worker-schemas.js +25 -7
  656. package/template/wall-e/memory/ctm-context-client.js +110 -5
  657. package/template/wall-e/memory/ctm-session-context.js +9 -3
  658. package/template/wall-e/memory/hub-bridge-client.js +388 -0
  659. package/template/wall-e/memory/replica-origin.js +56 -0
  660. package/template/wall-e/memory/session-pool.js +135 -0
  661. package/template/wall-e/runtime/live-capabilities.js +8 -3
  662. package/template/wall-e/server.js +4 -2
  663. package/template/wall-e/shared/release-schema.json +3 -2
  664. package/template/wall-e/shared/sqlite-file-set.js +39 -0
  665. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  666. package/template/wall-e/shared/storage/contract.js +38 -3
  667. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  668. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +13 -1
  669. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +11 -6
  670. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +13 -8
  671. package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
  672. package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
  673. package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
  674. package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
  675. package/template/wall-e/tools/chrome-automation.js +6 -0
  676. package/template/wall-e/tools/local-tools.js +6 -4
  677. package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
  678. package/template/wall-e/workers/brain-owner-worker.js +27 -4
  679. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  680. package/template/wall-e/workers/mcp-gateway-process.js +5 -5
  681. package/template/wall-e/workers/telemetry-worker.js +8 -0
@@ -0,0 +1,1494 @@
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
+ const { createErrorSink } = require('./error-sink');
30
+ // WP-3 rev 3 (§13.6 D-17): the hub-side `proxy.sh` the installer writes. Node builtins only.
31
+ const fs = require('node:fs');
32
+ const os = require('node:os');
33
+ const path = require('node:path');
34
+ // WP-7 (docs/remote-agent-skills-bundles-2026-10-25.html §5 D-11, §6.5): the pure classification of
35
+ // a box's own skills against the hub's copy and the base the box reported. Required at the top
36
+ // level on purpose — it is a leaf (build/store/codex-install only), so it introduces no cycle, and a
37
+ // probe-time require would hide the edge from the dependency lint.
38
+ const bundleInventory = require('./agent-bundle/inventory');
39
+ // WP-1 (docs/remote-box-model-access-2026-10-26.html §6.2, §5.3 D-2/D-9): the box's OWN model
40
+ // availability, read on the same probe tick as everything else. Only the TTL constant is needed from
41
+ // the module (the block is derived here, on the hub, from the box's §6.1 body) — the module is a
42
+ // leaf (http/https only), so the edge introduces no cycle.
43
+ const { ORIGIN_TTL_MS } = require('./model-availability');
44
+
45
+ /** REPORT sink for a probe-sweep fault that must not fail the sweep (WP-7's hub-side build). */
46
+ const errorSink = createErrorSink({ name: 'machine-registry' });
47
+
48
+ const DEFAULT_TIMEOUT_MS = 3000;
49
+ // D-4/P3 grace: a machine with no successful probe for this long reads `unreachable`.
50
+ const UNREACHABLE_AFTER_MS = 60_000;
51
+ // P3: `unknown` starts at the SECOND consecutive miss, so one dropped probe is not news.
52
+ const UNKNOWN_AFTER_MISSES = 2;
53
+
54
+ const HEALTH_STATES = Object.freeze(['reachable', 'unknown', 'unreachable', 'unhealthy']);
55
+
56
+ // WP-3 (DGX Spark coding readiness §2): how long a box's CLI sign-in verdict is trusted before the
57
+ // next probe re-reads it. Sign-in changes only when a human acts, so a minute is plenty; the sweep
58
+ // rides the same tick as the health probe and adds no timer of its own.
59
+ const AGENT_AUTH_REFRESH_MS = 60_000;
60
+ // The three states a box's coding agent can be in, as the hub reads them: 'signed_in' |
61
+ // 'signed_out' | 'unknown'. 'unknown' is the honest answer for "never fetched", "the box did not
62
+ // answer", and "the box answered something we do not recognize" alike (P3 Unknown != Dead): only
63
+ // an explicit `signedIn:false` refuses a forward.
64
+
65
+ // WP-2 (machine-pool-picker-pairing-dashboard-2026-10-24.html D-12/D-16, §6.3): how long a box's
66
+ // own host sample (load, memory, disk, GPU, tasks, lastUsedAt) is trusted before the next probe
67
+ // re-reads it. The SAME 60 s cadence as the sign-in read, on the SAME tick, in the SAME loop — this
68
+ // module still owns exactly ONE timer site (`_getJsonBox`), so no new stabilization-lint surface.
69
+ const HOST_STATS_REFRESH_MS = 60_000;
70
+
71
+ // instance_id -> consecutive missed probes. Module state by design (see the header).
72
+ const _missStreak = new Map();
73
+ // instance_id -> { claude, codex, checkedAt } — the box's coding-CLI sign-in verdict as the hub
74
+ // last read it (WP-3). Module state for the same reason as _missStreak: it is probe bookkeeping
75
+ // with a freshness stamp, it dies with the process, and a stale copy must never look like fact to a
76
+ // reader (a restart reads `unknown`, which never refuses a forward).
77
+ const _agentAuth = new Map();
78
+ // instance_id -> { presence: { dsh: boolean }, checkedAt } (WP-13b, D-15). The box's OWN
79
+ // available-agents answer as this hub last read it from ITS /api/models/available-agents, reduced to
80
+ // the compact presence the new-session card and the machine row need: whether the box has the
81
+ // DeepSeek Harness. Nothing else from the body is kept — not paths, not versions, not the other
82
+ // agents. Same module-state contract as _agentAuth: a stale copy must never look like fact to a
83
+ // reader (a restart reads "never read", which refuses nothing), and a failed/404/unparseable read
84
+ // KEEPS the previous value (P3, "Unknown != Dead").
85
+ const _agentPresence = new Map();
86
+ // instance_id -> { sample, checkedAt } (D-12, D-16). The box's own host sample as the hub last read
87
+ // it from ITS /api/host/stats?latest=1, plus the hub-clock stamp that gates the next re-read. Same
88
+ // module-state contract as _agentAuth: a stale number must never look fresh, and a box that stops
89
+ // answering keeps its last entry with a FROZEN checkedAt (never advanced without an answer).
90
+ const _hostStats = new Map();
91
+ // instance_id -> the `executionHost` block of the last manifest this hub read from the box (D-4
92
+ // rev 2). The manifest is the ONLY place a member's user settings (displayName/favorite/hidden) and
93
+ // its os/arch/hostname live — the roster deliberately carries none of them (P-1) — so the sweep
94
+ // caches what the probe already fetched instead of asking again.
95
+ const _manifestHost = new Map();
96
+ // instance_id -> the `build` block of the last manifest this hub read from the box (WP-0,
97
+ // docs/fleet-auto-update-pooled-machines-2026-10-26.html S-1/S-2). Same lifetime and the same write
98
+ // sites as `_manifestHost`: the manifest the probe already fetched is the version truth the fleet
99
+ // reconciler and the Machines card read ("which build is that box running?"), so nothing re-asks.
100
+ const _manifestBuild = new Map();
101
+ // instance_id -> the agent-skill push result (WP-3, docs/remote-agent-skills-bundles-2026-10-25.html
102
+ // §6.5). Module state for the same reason as _agentAuth: it describes the LAST push this process
103
+ // made, it is not durable truth about the box, and a restart must read "never pushed" rather than a
104
+ // stale tree. `pushError` is cleared by the next success and kept otherwise; `sentTreeHash`/
105
+ // `sentAt`/`sentCount`/`heldBack` always describe the last SUCCESSFUL push (D-6: a failure never
106
+ // erases what the box is actually running).
107
+ const _agentBundlePush = new Map();
108
+ // instance_id -> { onBox, checkedAt } (WP-7 §6.5/D-11). The box's OWN skill inventory as this hub
109
+ // last read it from ITS /api/agent-skills/inventory, classified against the hub's copy and the base
110
+ // the BOX reported. Module state for the same reason as _agentAuth: it is the last read this process
111
+ // made, and a box that stops answering keeps its entry with a FROZEN onBox — never blanked to "no
112
+ // skills", which would read as "the box lost everything". `checkedAt` is the hub clock stamp that
113
+ // gates the next read (SAME 60 s constant, SAME tick — this module still owns ONE timer site).
114
+ const _agentSkills = new Map();
115
+ // WP-1 §6.2: instance_id -> { block, checkedAt }. The box's OWN model availability (§6.1) as this
116
+ // hub last read it from ITS /api/models/availability, plus the hub-clock stamp that gates the next
117
+ // read. D-9: the read rides the EXISTING 60 s probe tick and is refreshed at most once per
118
+ // ORIGIN_TTL_MS (10 min) per machine — no second timer, no poll. Module state for the same reason as
119
+ // _agentAuth. `block === null` is a REAL answer ("this box predates WP-1"), not "never read" —
120
+ // `checkedAt` is what tells the two apart, and a failed read keeps the previous block untouched.
121
+ const _machineModels = new Map();
122
+ // WP-1 §6.2: how long a box's availability answer is trusted. Same value as the box's own per-origin
123
+ // cache, on purpose: the hub must not re-ask more often than the box would re-probe.
124
+ const MODELS_REFRESH_MS = ORIGIN_TTL_MS;
125
+ // platform -> { items, at }: the hub's own item list (`{agent, kind, name, itemHash}`) as WP-1's
126
+ // builder produced it for that platform. Memoized for AGENT_AUTH_REFRESH_MS so N boxes of one
127
+ // platform cost ONE build per minute. Only `items` is kept — `blobsByHash` is hub-local absolute
128
+ // paths and would pin the whole hub tree in memory for a classification that never reads it.
129
+ const _hubItemCache = new Map();
130
+ // The last sweep's self-suspicion verdict, surfaced by GET /api/machines. Not persisted per
131
+ // machine (design §7 "not persisted per machine") — it is a hub-wide observation about one sweep.
132
+ let _lastSweep = { hubNetworkSuspect: false, at: 0 };
133
+ // WP-3 rev 3 (§13.6 D-17): does THIS hub have an away path? The installer writes its hub half to
134
+ // `~/.config/ctm-away-path/proxy.sh` (bin/away-path.sh cmd_install_hub). Read once per sweep, off
135
+ // the request path (see machineAwayReason); `null` = this process has not swept yet.
136
+ let _awayPathProxyExists = null;
137
+ // Bumped by every registry write this process makes (register, remove, seed, a probe that changed
138
+ // a row). Readers that keep a snapshot (lib/remote-machine-catalog.js) compare it instead of
139
+ // trusting a TTL alone: a snapshot taken a moment before a registration must not hide the new
140
+ // machine from the relay and the merged list.
141
+ let _revision = 0;
142
+
143
+ /** The registry's write revision (see above). */
144
+ function registryRevision() {
145
+ return _revision;
146
+ }
147
+
148
+ /** Record a registry write made outside this module (lib/api/machines.js's remove route). */
149
+ function noteRegistryChanged() {
150
+ _revision += 1;
151
+ }
152
+
153
+ function _string(value, max = 1000) {
154
+ return String(value == null ? '' : value).trim().slice(0, max);
155
+ }
156
+
157
+ function _now(deps = {}) {
158
+ return typeof deps.now === 'function' ? Number(deps.now()) || 0 : Date.now();
159
+ }
160
+
161
+ function _trimUrl(url) {
162
+ return _string(url, 1000).replace(/\/+$/, '');
163
+ }
164
+
165
+ // The manifest's instance id, from the same three shapes server.js's golive probe accepts
166
+ // (server.js:13009): {manifest:{instanceId}}, {instance:{…}} and a bare manifest body.
167
+ function _manifestInstanceId(body) {
168
+ const manifest = (body && (body.manifest || body.instance)) || body || {};
169
+ return _string(
170
+ manifest.instanceId || manifest.instance_id || manifest.id
171
+ || (body && (body.instanceId || body.instance_id)) || '',
172
+ 160,
173
+ );
174
+ }
175
+
176
+ /**
177
+ * D-4 rev 2 / P-1: the machine-owned settings and host identity, read off the manifest's
178
+ * `executionHost` block. Display fields NEVER come from the roster (P-1) — the manifest is the
179
+ * authority, and this is the one place the sweep reads them.
180
+ */
181
+ function _manifestHostBlock(body) {
182
+ const manifest = (body && (body.manifest || body.instance)) || body || {};
183
+ const host = manifest.executionHost || manifest.execution_host || {};
184
+ return {
185
+ displayName: _string(host.displayName || host.display_name || host.hostLabel || host.hostname || '', 160),
186
+ favorite: host.favorite === true || host.favorite === 1,
187
+ hidden: host.hidden === true || host.hidden === 1,
188
+ os: _string(host.os || host.platform || '', 40),
189
+ arch: _string(host.arch || '', 40),
190
+ hostname: _string(host.hostname || '', 160),
191
+ };
192
+ }
193
+
194
+ /**
195
+ * WP-0 (S-1): the manifest's `build` block — the box's own version truth (`lib/instance-manifest.js`
196
+ * publishes `{ version, sha, builtAt, release }`). Each field is bounded; a manifest without the
197
+ * block (an older box) reads as three empty strings, never undefined, so a reader can tell "no
198
+ * build published" from "never probed" (`null`).
199
+ */
200
+ function _manifestBuildBlock(body) {
201
+ const manifest = (body && (body.manifest || body.instance)) || body || {};
202
+ const build = manifest.build || {};
203
+ return {
204
+ sha: _string(build.sha, 64),
205
+ version: _string(build.version, 64),
206
+ release: _string(build.release, 64),
207
+ };
208
+ }
209
+
210
+ function _manifestDisplayName(body) {
211
+ return _manifestHostBlock(body).displayName;
212
+ }
213
+
214
+ /**
215
+ * D-9/WP-3: the cached manifest host block for an instance (the box's `os` is what the bundle
216
+ * build targets), or null when this hub has never read that box's manifest. A COPY: the cache is
217
+ * module state and a reader must not be able to mutate it.
218
+ */
219
+ function manifestHostFor(instanceId) {
220
+ const host = _manifestHost.get(_string(instanceId, 160)) || null;
221
+ return host ? { ...host } : null;
222
+ }
223
+
224
+ function _hostLabel(url) {
225
+ try { return _string(new URL(url).host, 160); } catch { return _string(url, 160); }
226
+ }
227
+
228
+ // The hub's own peer credential on the probe, the same headers lib/hosted-sync-worker.js sends to
229
+ // /api/sync/manifest: a box behind a relay or tunnel sees forwarding markers, stops trusting the
230
+ // caller as loopback and answers a credential-less probe 401. INJECTED by the caller (requiring the
231
+ // header builder from here would be a dependency cycle, as with parseSyncPeers below); none
232
+ // configured probes without one (a same-host box still answers over loopback).
233
+ //
234
+ // WP-2 / P-8: the credential is a PER-TARGET decision on a pool member — the shared pool secret goes
235
+ // to a member, the legacy env bearer to a box that never joined — so the caller may inject
236
+ // `peerHeadersFor(instanceId)` instead of one fixed object. The fixed `peerHeaders` object is kept
237
+ // for callers (and tests) that dial with a single credential; when neither is given the probe goes
238
+ // out credential-less, exactly as before.
239
+ function _peerHeadersFor(deps = {}) {
240
+ const builder = deps.peerHeadersFor;
241
+ const fixed = deps.peerHeaders && typeof deps.peerHeaders === 'object' ? deps.peerHeaders : {};
242
+ if (typeof builder !== 'function') return () => fixed;
243
+ return (instanceId) => {
244
+ try {
245
+ const built = builder(instanceId);
246
+ return built && typeof built === 'object' ? built : {};
247
+ } catch {
248
+ return {}; // expected: a credential-builder fault degrades to an anonymous probe, never a failed sweep
249
+ }
250
+ };
251
+ }
252
+
253
+ /**
254
+ * P-4: the URLs to dial for a machine, in order — the roster's `urls` (LAN first, then the relay
255
+ * origin), falling back to the single pinned `url` when the row predates the roster (P-8 legacy rows
256
+ * and seeded placeholders).
257
+ */
258
+ function _dialUrls(machine = {}) {
259
+ const list = (Array.isArray(machine.urls) ? machine.urls : []).map((u) => _trimUrl(u)).filter(Boolean);
260
+ if (list.length) return list;
261
+ const pinned = _trimUrl(machine.url);
262
+ return pinned ? [pinned] : [];
263
+ }
264
+
265
+ // WP-3 rev 3 (§13.6 D-17): is this host one only the home LAN (or this machine) can name? A URL
266
+ // that does not parse is NOT local-only — "would this work from another network?" would be unknown.
267
+ function _isLocalOnlyHost(hostname) {
268
+ const host = String(hostname || '').toLowerCase();
269
+ if (!host) return false;
270
+ if (host === 'localhost' || host.endsWith('.localhost')) return true;
271
+ if (host.endsWith('.local')) return true;
272
+ // `new URL('http://[::1]:3456').hostname` keeps the brackets; strip them before parsing.
273
+ const ip = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
274
+ if (ip.includes(':')) {
275
+ if (ip === '::1') return true;
276
+ const first = parseInt(ip.split(':')[0] || '', 16);
277
+ if (!Number.isInteger(first)) return false;
278
+ // fc00::/7 (unique-local) and fe80::/10 (link-local).
279
+ return (first >= 0xfc00 && first <= 0xfdff) || (first >= 0xfe80 && first <= 0xfebf);
280
+ }
281
+ const octets = ip.split('.');
282
+ if (octets.length !== 4) return false;
283
+ const nums = octets.map((part) => (/^\d{1,3}$/.test(part) ? Number(part) : NaN));
284
+ if (nums.some((n) => !Number.isInteger(n) || n > 255)) return false;
285
+ if (nums[0] === 127 || nums[0] === 10) return true; // 127/8 loopback, 10/8 private
286
+ if (nums[0] === 172 && nums[1] >= 16 && nums[1] <= 31) return true; // 172.16/12
287
+ return nums[0] === 192 && nums[1] === 168; // 192.168/16
288
+ }
289
+
290
+ /**
291
+ * WP-3 rev 3 (docs/off-lan-hub-to-box-access-2026-10-31.html §13.6 D-17): WHY an `unreachable` box
292
+ * whose every dial URL is local-only cannot be dialled from here, or null when the hub cannot know.
293
+ * The one readable fact is whether this hub has an away path at all — the hub-side
294
+ * `~/.config/ctm-away-path/proxy.sh` (bin/away-path.sh cmd_install_hub): no proxy ⇒ nothing could
295
+ * ever route to the box from off-LAN; proxy present ⇒ this box's tunnel or forward is the down half.
296
+ *
297
+ * That fact is read ONCE PER SWEEP (`probeMachinesOnce`), never on the request path: /api/machines
298
+ * renders on the main thread. `null` (module state) = this process has not swept yet — an honest
299
+ * "not known", and `unreachable` is only ever written BY a sweep.
300
+ */
301
+ function machineAwayReason(machine = {}, deps = {}) {
302
+ const row = machine || {};
303
+ if (row.health !== 'unreachable') return null;
304
+ const urls = _dialUrls(row);
305
+ if (!urls.length) return null;
306
+ const localOnly = urls.every((raw) => {
307
+ try {
308
+ return _isLocalOnlyHost(new URL(raw).hostname);
309
+ } catch {
310
+ return false; // expected: an unparsable URL is not local-only (see _isLocalOnlyHost)
311
+ }
312
+ });
313
+ if (!localOnly) return null;
314
+ const proxyExists = typeof deps.awayPathProxyExists === 'boolean' ? deps.awayPathProxyExists : _awayPathProxyExists;
315
+ if (proxyExists === null) return null; // this hub has not swept yet, so it does not know
316
+ return proxyExists ? 'away_path_down' : 'no_away_path';
317
+ }
318
+
319
+ // One JSON GET against a box, with the AbortController + unref'd timer shape server.js's golive
320
+ // probe uses, so a hung box can never hold the tick open past timeoutMs. Shared by the manifest and
321
+ // the agent-sign-in probes: this is the ONE timer site in the file (a second copy would be a new
322
+ // stabilization-lint ownership surface for no gain).
323
+ async function _getJsonBox(url, pathname, { fetchImpl, timeoutMs = DEFAULT_TIMEOUT_MS, peerHeaders = {} } = {}) {
324
+ const fetchFn = fetchImpl || global.fetch;
325
+ if (typeof fetchFn !== 'function') return { ok: false, error: 'fetch_unavailable', status: null, body: null };
326
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
327
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
328
+ if (timer && typeof timer.unref === 'function') timer.unref();
329
+ try {
330
+ const res = await fetchFn(`${url}${pathname}`, {
331
+ method: 'GET',
332
+ headers: { ...peerHeaders, Accept: 'application/json' },
333
+ signal: controller ? controller.signal : undefined,
334
+ });
335
+ if (!res || res.ok === false) {
336
+ return { ok: false, error: `http_${res ? res.status : 'unknown'}`, status: res ? res.status : null, body: null };
337
+ }
338
+ let body = null;
339
+ try {
340
+ body = await res.json();
341
+ } catch {
342
+ // expected: a non-JSON 200 body is reachable-but-degraded, not unreachable — each caller
343
+ // reports it with its own verdict (manifest_invalid / an 'unknown' agent state).
344
+ body = null;
345
+ }
346
+ return { ok: true, error: '', status: res.status, body };
347
+ } catch (err) {
348
+ return { ok: false, error: err?.message || String(err), status: null, body: null };
349
+ } finally {
350
+ if (timer) clearTimeout(timer);
351
+ }
352
+ }
353
+
354
+ // One manifest read, in the shape server.js's golive probe expects.
355
+ async function _probeManifest(url, options = {}) {
356
+ const fetched = await _getJsonBox(url, '/api/sync/manifest', options);
357
+ if (!fetched.ok) return { ok: false, code: 'machine_unreachable', error: fetched.error };
358
+ const instanceId = _manifestInstanceId(fetched.body);
359
+ // Reachable but degraded: the box answered and told us nothing we can pin or trust.
360
+ if (!instanceId) return { ok: false, code: 'machine_manifest_invalid', error: 'manifest_instance_id_missing', body: fetched.body };
361
+ const host = _manifestHostBlock(fetched.body);
362
+ return { ok: true, instanceId, displayName: host.displayName, host, build: _manifestBuildBlock(fetched.body) };
363
+ }
364
+
365
+ /**
366
+ * P-4 dial order: try the machine's URLs in order and use the first whose manifest instance id is
367
+ * the row's. A URL that answers with ANOTHER box's id is refused for that URL (D-3) and the next one
368
+ * is dialled — an SSH forward that was re-pointed must not silently re-key the row.
369
+ *
370
+ * `machine_unreachable` is reported only when no URL answered at all: the sweep's "every box silent ⇒
371
+ * hubNetworkSuspect" verdict (P3) must not fire for a box that answered on one of its URLs.
372
+ */
373
+ async function _probeMachineManifest(machine = {}, options = {}) {
374
+ const urls = _dialUrls(machine);
375
+ let answered = null; // the last attempt that got an HTTP answer (mismatch / no instance id)
376
+ let last = null;
377
+ for (const url of urls) {
378
+ const probe = await _probeManifest(url, options);
379
+ if (probe.ok && probe.instanceId === machine.instanceId) return { ...probe, url };
380
+ if (probe.ok) {
381
+ answered = { ok: false, code: 'machine_identity_mismatch', error: `manifest ${probe.instanceId} != pinned ${machine.instanceId}` };
382
+ last = answered;
383
+ continue;
384
+ }
385
+ if (probe.code === 'machine_manifest_invalid') {
386
+ answered = probe;
387
+ last = probe;
388
+ continue;
389
+ }
390
+ last = probe;
391
+ }
392
+ return answered || last || { ok: false, code: 'machine_unreachable', error: 'machine_has_no_url' };
393
+ }
394
+
395
+ /**
396
+ * D-12/D-16: read a box's own latest host sample from ITS `/api/host/stats?latest=1` (WP-1). Never
397
+ * throws and never reports anything but the sample or null: a non-2xx, an unparseable body, a
398
+ * missing `latest` and a transport throw alike read "no sample this time" and leave the previous
399
+ * entry (and its `checkedAt`) exactly where they were.
400
+ */
401
+ async function _probeHostStats(url, options = {}) {
402
+ const fetched = await _getJsonBox(url, '/api/host/stats?latest=1', options);
403
+ if (!fetched.ok) return null;
404
+ const latest = fetched.body && fetched.body.latest;
405
+ return latest && typeof latest === 'object' ? latest : null;
406
+ }
407
+
408
+ /**
409
+ * P-5: read a member's roster version + membership from ITS `/api/pool/roster`. Returns
410
+ * `{ version, members }` or null (a version-less / unreachable / unparseable answer is "nothing to
411
+ * adopt", never an error).
412
+ */
413
+ async function _pullRoster(url, options = {}) {
414
+ const fetched = await _getJsonBox(url, '/api/pool/roster', options);
415
+ if (!fetched.ok) return null;
416
+ const body = fetched.body;
417
+ if (!body || typeof body !== 'object') return null;
418
+ const version = body.version && typeof body.version === 'object' ? body.version : null;
419
+ if (!version) return null;
420
+ return { version, members: Array.isArray(body.members) ? body.members : [] };
421
+ }
422
+
423
+ /** The pool store: injected in tests, the machine-pool leaf singleton in production. Never throws. */
424
+ function _machinePool(deps = {}) {
425
+ if (deps.machinePool && typeof deps.machinePool.hasPool === 'function') return deps.machinePool;
426
+ try {
427
+ return require('./machine-pool').getMachinePool();
428
+ } catch {
429
+ return null; // expected: machine-pool is a pure leaf; an unreadable store means "no pool"
430
+ }
431
+ }
432
+
433
+ /**
434
+ * This machine's own minted id, or '' when the caller has not injected one (the P-5 delete leg is
435
+ * then skipped by db.js — an upsert-only convergence, never a wrong deletion).
436
+ *
437
+ * INJECTED, never required: `lib/instance-identity-access.js` reaches db.js, and db.js reaches
438
+ * lib/hosted-sync-worker.js, which reaches this module — so requiring it here closes a lib/**
439
+ * dependency cycle (dependency-lint ceiling 0; verified, not assumed: the first cut of this file did
440
+ * exactly that and the lint reported
441
+ * `lib/instance-identity-access.js -> lib/machine-registry.js -> lib/hosted-sync-worker.js -> db.js`).
442
+ * `selfInstanceId` may be a string or a `() => string` getter, because identity is only ready after
443
+ * boot while a sweep repeats for the life of the process.
444
+ */
445
+ function _selfInstanceId(deps = {}) {
446
+ const source = deps.selfInstanceId;
447
+ try {
448
+ return _string(typeof source === 'function' ? source() : source, 160);
449
+ } catch {
450
+ return ''; // expected: identity is not ready yet (pre-boot / pre-DB) — never a sweep failure
451
+ }
452
+ }
453
+
454
+ /**
455
+ * P-5 sweep pull: a member that was offline during a roster push converges on its next sweep. Adopt
456
+ * only a strictly NEWER version (revision, then updatedAt, then `by` — machine-pool.isNewer); an
457
+ * equal or older one is ignored (the receiver answers with its own version, and the pusher decides).
458
+ * Self is never written into its own roster (db.js's replaceRosterMachines also refuses it).
459
+ */
460
+ async function _maybePullRoster(machine, url, { deps = {}, fetchImpl, timeoutMs, peerHeaders = {} } = {}) {
461
+ const pool = _machinePool(deps);
462
+ if (!pool || typeof pool.hasPool !== 'function' || !pool.hasPool()) return null;
463
+ const pulled = await _pullRoster(url, { fetchImpl, timeoutMs, peerHeaders });
464
+ if (!pulled) return null;
465
+ if (typeof pool.isNewer !== 'function' || !pool.isNewer(pulled.version, pool.rosterVersion())) return null;
466
+
467
+ const self = _selfInstanceId(deps);
468
+ const members = pulled.members
469
+ .map((member) => ({
470
+ instanceId: _string(member && (member.instanceId || member.instance_id), 160),
471
+ urls: (Array.isArray(member && member.urls) ? member.urls : []).map((u) => _trimUrl(u)).filter(Boolean),
472
+ }))
473
+ .filter((member) => member.instanceId && member.instanceId !== self);
474
+
475
+ // Only PREVIOUS pool members may be deleted (a legacy env-token row is never one, P-8).
476
+ const removable = typeof pool.members === 'function' ? pool.members() : [];
477
+ await dispatchOwnerMutation(deps.db, 'replaceRosterMachines', [{ members, selfInstanceId: self, removable }], 'machine registry roster pull');
478
+ if (typeof pool.setRosterVersion === 'function') await pool.setRosterVersion(pulled.version);
479
+ if (typeof pool.setMembers === 'function') await pool.setMembers(members.map((member) => member.instanceId));
480
+ noteRegistryChanged();
481
+ return { version: pulled.version, members };
482
+ }
483
+
484
+ /** D-4: cache the manifest's executionHost block; a display/settings CHANGE is registry news. */
485
+ function _cacheManifestHost(instanceId, host) {
486
+ const id = _string(instanceId, 160);
487
+ if (!id || !host) return;
488
+ const previous = _manifestHost.get(id) || null;
489
+ _manifestHost.set(id, host);
490
+ if (!previous) return; // a first read is not news (the registration already bumped the revision)
491
+ if (previous.displayName !== host.displayName || previous.favorite !== host.favorite
492
+ || previous.hidden !== host.hidden || previous.os !== host.os
493
+ || previous.arch !== host.arch || previous.hostname !== host.hostname) {
494
+ noteRegistryChanged();
495
+ }
496
+ }
497
+
498
+ /**
499
+ * WP-0 (S-2): cache the manifest's `build` block beside `_manifestHost` — written by the same single
500
+ * caller (the sweep), cleared by the same test seam. A build change is deliberately NOT registry
501
+ * news: it is a version stamp the fleet reconciler reads, not machine identity, and bumping the
502
+ * revision on every restart of every box would wake every snapshot reader for nothing.
503
+ */
504
+ function _cacheManifestBuild(instanceId, build) {
505
+ const id = _string(instanceId, 160);
506
+ if (!id || !build) return;
507
+ _manifestBuild.set(id, build);
508
+ }
509
+
510
+ /** A box's logged-in flag -> the hub's three-state verdict (unknown for anything unexpected). */
511
+ function _agentLoginState(signedIn) {
512
+ if (signedIn === true) return 'signed_in';
513
+ if (signedIn === false) return 'signed_out';
514
+ return 'unknown';
515
+ }
516
+
517
+ /**
518
+ * WP-3: read a box's own coding-CLI sign-in truth from ITS `/api/providers/login-state` (the route
519
+ * a signed-out box answers with `providers.<agent>.account.signedIn: false`). It NEVER throws and
520
+ * never reports anything but a three-state verdict: a non-2xx, an unparseable body, an unexpected
521
+ * shape and a transport throw all read `unknown` — only an explicit false is a refusal.
522
+ */
523
+ async function _probeAgentAuth(url, options = {}) {
524
+ const fetched = await _getJsonBox(url, '/api/providers/login-state', options);
525
+ const unknown = { claude: 'unknown', codex: 'unknown' };
526
+ if (!fetched.ok) return unknown;
527
+ const providers = (fetched.body && typeof fetched.body === 'object' && fetched.body.providers) || {};
528
+ const signedInFor = (agent) => {
529
+ const entry = providers[agent];
530
+ if (!entry || typeof entry !== 'object') return undefined;
531
+ const account = entry.account;
532
+ return account && typeof account === 'object' ? account.signedIn : undefined;
533
+ };
534
+ return { claude: _agentLoginState(signedInFor('claude')), codex: _agentLoginState(signedInFor('codex')) };
535
+ }
536
+
537
+ /**
538
+ * WP-13b (D-15): the box's OWN available-agents answer, reduced to the one compact presence the hub
539
+ * surfaces — `{ dsh: boolean }`.
540
+ *
541
+ * The route (`GET /api/models/available-agents`) answers `agentCliCache.getAvailableAgents()`'s
542
+ * body: `{ ok, agents: { <agent>: { available, … } }, <agent>: boolean }`. This read keeps ONLY
543
+ * whether `dsh` is present; paths and every other agent are dropped at the door, so nothing the box
544
+ * printed about its filesystem is ever cached hub-side.
545
+ *
546
+ * Three outcomes, matching the sign-in/skills reads:
547
+ * 'block' — a 200 that carries an explicit dsh verdict: store the compact presence.
548
+ * 'keep' — a failed read, a 404 (a box older than this route), a non-object/unparseable body, or
549
+ * a body that names no dsh at all: KEEP the previous value (P3, "Unknown != Dead"). Only
550
+ * an explicit answer is news; a silent box is never read as "the box lost dsh".
551
+ */
552
+ async function _probeAgentAvailability(url, options = {}) {
553
+ const fetched = await _getJsonBox(url, '/api/models/available-agents', options);
554
+ if (!fetched.ok) return { state: 'keep' };
555
+ const presence = _dshPresenceFromBody(fetched.body);
556
+ if (!presence) return { state: 'keep' };
557
+ return { state: 'block', presence };
558
+ }
559
+
560
+ /**
561
+ * The `dsh` verdict in a `/api/models/available-agents` body, or null when the body carries none.
562
+ * Accepted shapes (the hub must not care which one its box serves): the route's own
563
+ * `{ dsh: <boolean>, agents: { dsh: { available } } }`, and the bare `{ agents: {…} }` / `{ dsh }`.
564
+ */
565
+ function _dshPresenceFromBody(body) {
566
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return null;
567
+ const agents = body.agents && typeof body.agents === 'object' && !Array.isArray(body.agents) ? body.agents : null;
568
+ const detail = agents ? agents.dsh : undefined;
569
+ if (detail !== undefined) {
570
+ if (typeof detail === 'boolean') return { dsh: detail };
571
+ if (detail && typeof detail === 'object' && typeof detail.available === 'boolean') return { dsh: detail.available };
572
+ return null; // present but not a verdict — "no answer", never "not installed"
573
+ }
574
+ if (typeof body.dsh === 'boolean') return { dsh: body.dsh };
575
+ return null;
576
+ }
577
+
578
+ /**
579
+ * Store one box's agent presence. `noteRegistryChanged()` fires only when the SURFACED presence
580
+ * actually moved, exactly like the sign-in read above: a first read is not news (registration already
581
+ * bumped the revision), and an identical re-read under a newer `checkedAt` is not either.
582
+ */
583
+ function _setAgentPresence(instanceId, presence, now) {
584
+ const id = _string(instanceId, 160);
585
+ if (!id || !presence) return null;
586
+ const previous = _agentPresence.get(id) || null;
587
+ const next = { presence: { dsh: presence.dsh === true }, checkedAt: now };
588
+ _agentPresence.set(id, next);
589
+ if (previous && previous.presence.dsh !== next.presence.dsh) noteRegistryChanged();
590
+ return next;
591
+ }
592
+
593
+ /**
594
+ * WP-7 (§6.5/AC-7a): read a box's OWN skill inventory from ITS `GET /api/agent-skills/inventory`.
595
+ *
596
+ * Never throws and never reports anything but the parsed body or null. A box running an older CTM
597
+ * answers 404, a silent box never answers, and an unparseable body is not an inventory — all three
598
+ * read "no inventory this time", which the caller answers with KEEP THE LAST VALUE. It is
599
+ * deliberately not "an empty inventory": that would claim the box lost every skill it had.
600
+ */
601
+ async function _probeAgentSkills(url, options = {}) {
602
+ const fetched = await _getJsonBox(url, '/api/agent-skills/inventory', options);
603
+ if (!fetched.ok) return null;
604
+ const body = fetched.body;
605
+ return body && typeof body === 'object' && !Array.isArray(body) ? body : null;
606
+ }
607
+
608
+ /**
609
+ * WP-1 (§6.2/§6.1): read a box's OWN model availability from ITS `GET /api/models/availability`.
610
+ *
611
+ * Three outcomes, and the difference between them is the whole contract:
612
+ * 'block' — the box answered with a §6.1 body: store the derived §6.2 block.
613
+ * 'null' — the box answered 404: it predates WP-1, so `models` is null and the row renders
614
+ * exactly as it did before (AC-1e). Stamped, so a pre-WP-1 box is asked once per TTL.
615
+ * 'keep' — anything else (silent box, non-JSON answer, transport throw): KEEP the previous block
616
+ * and move only the stamp (the remote-skills WP-7 rule). Never blank a block we hold.
617
+ */
618
+ async function _probeModelsAvailability(url, options = {}) {
619
+ const fetched = await _getJsonBox(url, '/api/models/availability', options);
620
+ if (fetched.ok) {
621
+ const body = fetched.body;
622
+ // A 200 that is not a §6.1 body (no `routes` array) is not an answer to this question: KEEP.
623
+ if (!body || typeof body !== 'object' || Array.isArray(body) || !Array.isArray(body.routes)) {
624
+ return { state: 'keep' };
625
+ }
626
+ return { state: 'block', block: _modelsBlockFrom(body) };
627
+ }
628
+ if (fetched.status === 404) return { state: 'null', block: null };
629
+ return { state: 'keep' };
630
+ }
631
+
632
+ /**
633
+ * §6.2: the block the machine row surfaces, derived from the box's §6.1 body. `usable` is the
634
+ * launch-able count the card states (enabled, and either keyed or a local runtime, and reachable);
635
+ * `unreachable` lists the enabled rows the box cannot get to; `delegate` mirrors the box's own
636
+ * /delegate picks with the verdict of the route each one names.
637
+ */
638
+ function _modelsBlockFrom(body) {
639
+ const routes = (Array.isArray(body.routes) ? body.routes : []).filter((row) => row && typeof row === 'object');
640
+ const enabled = routes.filter((row) => row.enabled !== false);
641
+ const isOllama = (row) => _string(row.type, 60).toLowerCase() === 'ollama';
642
+ const usable = enabled.filter((row) => row.reach === 'reachable' && (row.keyed === true || isOllama(row))).length;
643
+ const unreachable = enabled
644
+ .filter((row) => row.reach === 'unreachable' || row.reach === 'no_server')
645
+ .map((row) => ({
646
+ id: _string(row.id, 200),
647
+ type: _string(row.type, 60),
648
+ origin: row.origin ? _string(row.origin, 300) : null,
649
+ }));
650
+ // WP-5 (§6.4/R-6): the box's own route list, so the hub can offer "Use on <box>" for the routes
651
+ // the box does NOT have and refuse one whose origin that same list calls unreachable. THREE fields
652
+ // only — id, origin and the reach verdict — never a name, a type, a credential flag or a key: the
653
+ // hub needs "does this box already have this id" and "can this box reach this origin", nothing
654
+ // else. A row with no id is not a route the hub can match, so it is skipped rather than surfaced
655
+ // as an empty id.
656
+ const routeRows = routes
657
+ .map((row) => ({
658
+ id: _string(row.id, 200),
659
+ origin: row.origin ? _string(row.origin, 300) : null,
660
+ reach: _string(row.reach, 20) || 'unknown',
661
+ }))
662
+ .filter((row) => row.id);
663
+ const delegate = (Array.isArray(body.delegateDefaults) ? body.delegateDefaults : [])
664
+ .filter((pref) => pref && typeof pref === 'object')
665
+ .map((pref) => ({
666
+ agent: _string(pref.agent, 20),
667
+ routeId: _string(pref.routeId, 200),
668
+ model: _string(pref.model, 200),
669
+ reach: _string(pref.reach, 20) || 'unknown',
670
+ }));
671
+ const nativeOf = (agent) => ({
672
+ signedIn: !!(body.native && body.native[agent] && body.native[agent].signedIn === true),
673
+ });
674
+ return {
675
+ checkedAt: _string(body.checkedAt, 60),
676
+ usable,
677
+ unreachable,
678
+ routes: routeRows,
679
+ delegate,
680
+ native: { claude: nativeOf('claude'), codex: nativeOf('codex') },
681
+ };
682
+ }
683
+
684
+ /**
685
+ * Store one box's availability block. `noteRegistryChanged()` fires only when the SURFACED block
686
+ * moved — a first read or a real change is news, an identical re-read under a newer `checkedAt` is
687
+ * not, or every 60 s tick would invalidate every reader's snapshot for nothing.
688
+ */
689
+ function _setMachineModels(instanceId, block, now) {
690
+ const id = _string(instanceId, 160);
691
+ if (!id) return null;
692
+ const previous = _machineModels.get(id) || null;
693
+ const next = { block, checkedAt: now };
694
+ _machineModels.set(id, next);
695
+ if (!previous || JSON.stringify(previous.block) !== JSON.stringify(next.block)) noteRegistryChanged();
696
+ return next;
697
+ }
698
+
699
+ /** WP-1 §6.2: the box's availability block as this hub last read it, or null (never read / pre-WP-1). */
700
+ function modelsFor(instanceId) {
701
+ const entry = _machineModels.get(_string(instanceId, 160));
702
+ return entry ? entry.block : null;
703
+ }
704
+
705
+ /**
706
+ * WP-7 (§6.5): the hub's own item list for one box platform, memoized for AGENT_AUTH_REFRESH_MS.
707
+ *
708
+ * `deps.buildAgentBundle` is injectable so a unit test never reads the real home; production falls
709
+ * back to WP-1's builder. A build failure is REPORTed and answered with null — "the hub cannot read
710
+ * its own skills" and "the hub has no skills" are DIFFERENT claims, and only the second one may
711
+ * rewrite a box's classification to `new_on_box`.
712
+ *
713
+ * @returns {Promise<{items: object[]}|null>}
714
+ */
715
+ async function _hubItemsForPlatform(platform, { deps = {}, now = Date.now() } = {}) {
716
+ const key = _string(platform, 40) || 'unknown';
717
+ const cached = _hubItemCache.get(key) || null;
718
+ if (cached && now - cached.at < AGENT_AUTH_REFRESH_MS) return { items: cached.items };
719
+ const build = typeof deps.buildAgentBundle === 'function'
720
+ ? deps.buildAgentBundle
721
+ : require('./agent-bundle/build').buildAgentBundle;
722
+ try {
723
+ const bundle = await build({ targetPlatform: key });
724
+ const items = (bundle && Array.isArray(bundle.items) ? bundle.items : []).map((item) => ({
725
+ agent: item.agent, kind: item.kind, name: item.name, itemHash: item.itemHash,
726
+ }));
727
+ _hubItemCache.set(key, { items, at: now });
728
+ return { items };
729
+ } catch (error) {
730
+ // REPORT: a hub-side build fault leaves every box's `onBox` exactly as it was (see the caller).
731
+ errorSink.report('agent-skill-inventory-build', error);
732
+ return null;
733
+ }
734
+ }
735
+
736
+ /**
737
+ * WP-7 (§6.5/AC-7a): classify one box's inventory read. `boxSkills` come from the box, `baseItems`
738
+ * from the base the BOX reported (that is the design adjustment: the hub's push record is
739
+ * in-process, so classification survives a hub restart only if the box carries the base back).
740
+ *
741
+ * @returns {Promise<object[]|null>} the classification rows, or null when the hub could not read its
742
+ * own skills — the caller then keeps the previous `onBox` rather than publishing a wrong one.
743
+ */
744
+ async function _classifyInventory(inventory, { deps = {}, now = Date.now() } = {}) {
745
+ const boxSkills = Array.isArray(inventory.skills) ? inventory.skills : [];
746
+ // §6.5: only a box holding at least one UNMANAGED skill is worth the hub's own build — a box whose
747
+ // skills are all CTM's (AC-7d) has nothing to classify, so the hub never walks its own tree for it.
748
+ const unmanaged = boxSkills.some((skill) => skill && (skill.managed === null || skill.managed === undefined));
749
+ if (!unmanaged) return [];
750
+ const hub = await _hubItemsForPlatform(inventory.platform, { deps, now });
751
+ if (!hub) return null;
752
+ const baseItems = inventory.base && Array.isArray(inventory.base.items) ? inventory.base.items : [];
753
+ return bundleInventory.classifyBoxSkills({ hubItems: hub.items, baseItems, boxSkills });
754
+ }
755
+
756
+ /**
757
+ * Store one box's classification. `noteRegistryChanged()` fires when the SURFACED block actually
758
+ * moved — a first read (the row goes from "no inventory" to a review list) is news, an identical
759
+ * re-read with only a newer `checkedAt` is not, or every 15 s tick would invalidate every snapshot.
760
+ */
761
+ function _setAgentSkills(instanceId, onBox, now) {
762
+ const id = _string(instanceId, 160);
763
+ if (!id) return null;
764
+ const previous = _agentSkills.get(id) || null;
765
+ const next = { onBox, checkedAt: now };
766
+ _agentSkills.set(id, next);
767
+ if (!previous || JSON.stringify(previous.onBox) !== JSON.stringify(next.onBox)) noteRegistryChanged();
768
+ return next;
769
+ }
770
+
771
+ function _rows(deps = {}) {
772
+ const db = deps.db;
773
+ if (!db || typeof db.listMachineRows !== 'function') return [];
774
+ return db.listMachineRows() || [];
775
+ }
776
+
777
+ function _isPinned(row = {}) {
778
+ // A row is pinned once something observed the box: registerMachine stamps both fields, and
779
+ // the probe loop keeps last_seen_at moving. A seeded row (enabled=0, never probed) is NOT
780
+ // pinned — it carries no evidence about which box its URL points at.
781
+ // S-9 (machine-pool-picker-pairing-dashboard-2026-10-24.html §9): a dead `row.pinned` field was
782
+ // dropped here — nothing ever wrote it. The picker's star is `favorite`, a machine-owned setting
783
+ // (D-4, lib/machine-self.js + the manifest), which is NOT this identity pin and never was.
784
+ return Number(row.lastSeenAt || row.last_seen_at || 0) > 0;
785
+ }
786
+
787
+ function _machineView(row = {}, deps = {}) {
788
+ const lastSeenAt = Number(row.lastSeenAt ?? row.last_seen_at ?? 0) || 0;
789
+ const enabled = Number(row.enabled ?? 1) !== 0;
790
+ const health = _string(row.health, 40) || 'unknown';
791
+ const instanceId = _string(row.instanceId || row.instance_id, 160);
792
+ // D-4 rev 2: the machine owns its display name / favorite / hidden and serves them in its own
793
+ // manifest, which the sweep caches per instance. The registry row's own label is the fallback for
794
+ // a box this hub has not read a manifest from yet.
795
+ const manifest = _manifestHost.get(instanceId) || null;
796
+ // WP-0 (S-1/S-2): the build stamp the box published in the manifest this hub last read, or null
797
+ // when it has never answered (never a fabricated {sha:''} row pretending to be a version).
798
+ const build = _manifestBuild.get(instanceId) || null;
799
+ // D-12/D-16: the box's own host sample (load, memory, disk, GPU, tasks) and its newest session
800
+ // create. `checkedAt` is the HUB clock stamp of the read, so a reader can label a stale number
801
+ // instead of showing it as current.
802
+ const stats = _hostStats.get(instanceId) || null;
803
+ const view = {
804
+ instanceId,
805
+ displayName: _string((manifest && manifest.displayName) || row.displayName || row.display_name, 200),
806
+ url: _string(row.url, 1000),
807
+ urls: (Array.isArray(row.urls) ? row.urls : []).map((u) => _string(u, 1000)).filter(Boolean),
808
+ walleUrl: _string(row.walleUrl || row.walle_url, 1000),
809
+ enabled,
810
+ health,
811
+ healthDetail: _string(row.healthDetail || row.health_detail, 500),
812
+ lastSeenAt,
813
+ createdAt: _string(row.createdAt || row.created_at, 60),
814
+ updatedAt: _string(row.updatedAt || row.updated_at, 60),
815
+ // D-4 rev 2: machine-owned settings, read from the last manifest this hub probed.
816
+ favorite: !!(manifest && manifest.favorite),
817
+ hidden: !!(manifest && manifest.hidden),
818
+ os: _string(manifest && manifest.os, 40),
819
+ arch: _string(manifest && manifest.arch, 40),
820
+ hostname: _string(manifest && manifest.hostname, 160),
821
+ // WP-0 (S-1): the box's own version truth (the manifest's build block), null until it answers.
822
+ build,
823
+ // null = this hub has never read a sample for this box (never a zeroed-out sample).
824
+ stats: stats ? { sample: stats.sample, checkedAt: stats.checkedAt } : null,
825
+ // WP-3 (§6.5) + WP-7 (§6.5/D-11) + WP-8 (§6.5): the skills push state for this box — the last
826
+ // successful tree and the last failure if there is one — together with the classification of the
827
+ // box's OWN skills (`onBox`, `checkedAt`) and this machine's per-machine plugin exclusions.
828
+ // null = this process neither pushed to, read this box, nor excluded anything for it.
829
+ skills: _skillsView(instanceId, deps),
830
+ // WP-1 (§6.2): the box's own model availability, read on the existing tick. null = this hub has
831
+ // never read it, or the box predates WP-1 — the row then renders exactly as it did before.
832
+ models: modelsFor(instanceId),
833
+ lastUsedAt: Number((stats && stats.sample && stats.sample.lastUsedAt) || 0) || 0,
834
+ // D-5: "unknown" until the box has ever answered; a disabled row is never a picker
835
+ // candidate, and only reachable/unhealthy ever render as cards.
836
+ pickerState: !enabled ? 'unknown' : health,
837
+ visible: enabled && (health === 'reachable' || health === 'unhealthy'),
838
+ };
839
+ // WP-3 rev 3 (§13.6 D-17): computed LAST, on the view itself, so the reason is derived from the
840
+ // exact `health` / `url` / `urls` the card renders (never a second, possibly-different reading).
841
+ // null for every row the hub cannot speak for; the Machines card then renders no note at all.
842
+ view.awayReason = machineAwayReason(view, deps);
843
+ return view;
844
+ }
845
+
846
+ function listMachines(deps = {}) {
847
+ const machines = _rows(deps).map((row) => _machineView(row, deps));
848
+ return { machines, hubNetworkSuspect: !!_lastSweep.hubNetworkSuspect };
849
+ }
850
+
851
+ /**
852
+ * Register (or re-point) a machine by URL. One successful manifest probe IS the pin (D-3):
853
+ * an unreachable box cannot be registered, because there would be nothing to key it on.
854
+ *
855
+ * @returns {{ok: true, machine: object}|{ok: false, code: string, error: string}}
856
+ */
857
+ async function registerMachine({ url, displayName, walleUrl } = {}, deps = {}) {
858
+ const baseUrl = _trimUrl(url);
859
+ if (!baseUrl) return { ok: false, code: 'machine_url_required', error: 'url is required' };
860
+ const probe = await _probeManifest(baseUrl, {
861
+ fetchImpl: deps.fetchImpl || deps.fetch,
862
+ timeoutMs: Number(deps.timeoutMs || DEFAULT_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS,
863
+ // P-8: registration probes the URL BEFORE the box's identity is known, so the caller's
864
+ // no-target builder is used (the pool-vs-env split needs an instanceId the hub does not have yet).
865
+ peerHeaders: _peerHeadersFor(deps)(''),
866
+ });
867
+ if (!probe.ok) return { ok: false, code: probe.code, error: probe.error };
868
+
869
+ const rows = _rows(deps).map((row) => _machineView(row));
870
+ const byUrl = rows.find((row) => row.url === baseUrl);
871
+ if (byUrl && byUrl.instanceId !== probe.instanceId && _isPinned(byUrl)) {
872
+ // The URL already points at a DIFFERENT box (or the forward was re-pointed). Refuse
873
+ // instead of silently re-keying: sessions already routed to the pinned id would move.
874
+ return {
875
+ ok: false,
876
+ code: 'machine_identity_mismatch',
877
+ error: `manifest ${probe.instanceId} != pinned ${byUrl.instanceId}`,
878
+ };
879
+ }
880
+ const byId = rows.find((row) => row.instanceId === probe.instanceId);
881
+ if (byId && byId.url !== baseUrl) {
882
+ return {
883
+ ok: false,
884
+ code: 'machine_identity_mismatch',
885
+ error: `instance ${probe.instanceId} already registered at ${byId.url}`,
886
+ };
887
+ }
888
+
889
+ // A seeded placeholder for this URL (enabled=0, never probed — S-19's env-seeded row) holds
890
+ // the URL but not the identity: replace it with the pinned row rather than keying on a guess.
891
+ if (byUrl && byUrl.instanceId !== probe.instanceId) {
892
+ await dispatchOwnerMutation(deps.db, 'deleteMachineRow', [byUrl.instanceId], 'machine registry reseed');
893
+ }
894
+ const now = _now(deps);
895
+ await dispatchOwnerMutation(deps.db, 'upsertMachineRow', [{
896
+ instanceId: probe.instanceId,
897
+ displayName: _string(displayName, 200) || probe.displayName || _hostLabel(baseUrl),
898
+ url: baseUrl,
899
+ walleUrl: _trimUrl(walleUrl),
900
+ enabled: 1,
901
+ }], 'machine registry register');
902
+ _missStreak.delete(probe.instanceId);
903
+ await dispatchOwnerMutation(deps.db, 'recordMachineProbe', [probe.instanceId, {
904
+ health: 'reachable', healthDetail: '', lastSeenAt: now,
905
+ }], 'machine registry register probe');
906
+ noteRegistryChanged();
907
+ const machine = _rows(deps).map((row) => _machineView(row)).find((row) => row.instanceId === probe.instanceId);
908
+ return { ok: true, machine: machine || null };
909
+ }
910
+
911
+ /**
912
+ * One probe sweep over every ENABLED machine. Zero machines ⇒ zero fetches (AC-0d).
913
+ *
914
+ * Grace walk (D-4/P3), per machine:
915
+ * probe ok -> health 'reachable', last_seen_at = hub now
916
+ * manifest ok, no instance id -> health 'unhealthy' + detail (the box answered; it is
917
+ * degraded, not gone — so last_seen_at still moves)
918
+ * probe failed, 1st miss -> previous health kept
919
+ * probe failed, >= 2 misses -> health 'unknown'
920
+ * probe failed, no success for >= 60 s (hub clock) -> health 'unreachable'
921
+ * If EVERY enabled machine failed in this sweep, the sweep ADDITIONALLY reports
922
+ * hubNetworkSuspect: true — the UI's cue to say "network?" rather than "all machines down".
923
+ * The walk above still runs: a hub-side blip is absorbed by the grace thresholds (one miss
924
+ * keeps the previous health and writes nothing; `unreachable` needs 60 s of silence), while a
925
+ * genuine all-boxes-down (laptop away from home) converges to the truth instead of freezing a
926
+ * stale `reachable` forever.
927
+ *
928
+ * The returned {reachable, unknown, unreachable, unhealthy} are the RESULTING-HEALTH histogram
929
+ * (the same states lib/instance-sync-status.js reports through summarizeMachines), so a machine
930
+ * still inside its grace window counts under the health it kept, not under "missed".
931
+ */
932
+ async function probeMachinesOnce(options = {}, deps = {}) {
933
+ const fetchImpl = options.fetchImpl || deps.fetchImpl || deps.fetch;
934
+ const timeoutMs = Number(options.timeoutMs || DEFAULT_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS;
935
+ const now = _now(deps);
936
+ const enabled = _rows(deps).map((row) => _machineView(row)).filter((row) => row.enabled);
937
+ if (!enabled.length) {
938
+ _lastSweep = { hubNetworkSuspect: false, at: now };
939
+ return { ok: true, probed: 0, reachable: 0, unknown: 0, unreachable: 0, unhealthy: 0, hubNetworkSuspect: false, results: [] };
940
+ }
941
+
942
+ // WP-3 rev 3 (§13.6 D-17): ONE away-path read per sweep, OFF the request path (see
943
+ // machineAwayReason). A missing proxy.sh IS the false answer; `deps` may inject the verdict.
944
+ _awayPathProxyExists = typeof deps.awayPathProxyExists === 'boolean'
945
+ ? deps.awayPathProxyExists
946
+ : await fs.promises.access(path.join(os.homedir(), '.config', 'ctm-away-path', 'proxy.sh'))
947
+ .then(() => true, () => false /* expected: an absent proxy.sh is the "no away path" answer */);
948
+
949
+ // P-8: one credential PER TARGET (pool secret for a member, the legacy env bearer for a box that
950
+ // never joined), so the header builder is resolved per machine, not once per sweep.
951
+ const headersFor = _peerHeadersFor(deps);
952
+ const results = await Promise.all(enabled.map(async (machine) => {
953
+ const probe = await _probeMachineManifest(machine, { fetchImpl, timeoutMs, peerHeaders: headersFor(machine.instanceId) });
954
+ // P-4: the URL that actually answered is the row's dial address from here on. A row whose
955
+ // pinned url went dead (an SSH forward torn down, a relay origin that took over) is re-pointed
956
+ // through the existing owner write path rather than re-dialled to nowhere every sweep.
957
+ if (probe.ok && _trimUrl(machine.url) !== probe.url) {
958
+ await dispatchOwnerMutation(deps.db, 'upsertMachineRow', [{
959
+ instanceId: machine.instanceId,
960
+ displayName: machine.displayName,
961
+ url: probe.url,
962
+ walleUrl: machine.walleUrl,
963
+ enabled: machine.enabled ? 1 : 0,
964
+ }], 'machine registry dial order');
965
+ noteRegistryChanged();
966
+ }
967
+ return { machine, probe };
968
+ }));
969
+
970
+ // P3 self-suspicion is ADVISORY: all boxes silent at once is most likely THIS host's network,
971
+ // so the sweep says so — but the grace walk below still runs (see the function comment).
972
+ const hubNetworkSuspect = results.every((row) => !row.probe.ok && row.probe.code === 'machine_unreachable');
973
+ _lastSweep = { hubNetworkSuspect, at: now };
974
+
975
+ const counts = { reachable: 0, unknown: 0, unreachable: 0, unhealthy: 0 };
976
+ for (const { machine, probe } of results) {
977
+ let health;
978
+ let detail = '';
979
+ if (probe.ok) {
980
+ health = 'reachable';
981
+ _missStreak.delete(machine.instanceId);
982
+ } else if (probe.code === 'machine_manifest_invalid') {
983
+ // Reachable-but-degraded: the box is up, so this is not a liveness miss either.
984
+ health = 'unhealthy';
985
+ detail = probe.error || 'manifest_invalid';
986
+ _missStreak.delete(machine.instanceId);
987
+ } else {
988
+ const misses = (_missStreak.get(machine.instanceId) || 0) + 1;
989
+ _missStreak.set(machine.instanceId, misses);
990
+ // The 60 s rule is measured FROM A RECORDED SUCCESS. A machine the hub has never seen
991
+ // answer (enabled before its first probe, so last_seen_at is still 0) is `unknown` after
992
+ // 2 misses — never `unreachable`, which would be a claim about silence we never measured
993
+ // (P3 Unknown != Dead).
994
+ const silentMs = machine.lastSeenAt > 0 ? now - machine.lastSeenAt : 0;
995
+ if (machine.lastSeenAt > 0 && silentMs >= UNREACHABLE_AFTER_MS) health = 'unreachable';
996
+ else if (misses >= UNKNOWN_AFTER_MISSES) health = 'unknown';
997
+ else health = machine.health;
998
+ // The detail describes the STANDING verdict: a first miss inside the grace window keeps
999
+ // the previous health AND its detail (so a blip sweep writes nothing at all).
1000
+ detail = health === machine.health ? machine.healthDetail : (probe.error || '');
1001
+ }
1002
+ counts[_string(health, 40)] = (counts[_string(health, 40)] || 0) + 1;
1003
+ const next = { health, healthDetail: detail, lastSeenAt: probe.ok || probe.code === 'machine_manifest_invalid' ? now : machine.lastSeenAt };
1004
+ if (next.health !== machine.health || next.healthDetail !== machine.healthDetail || next.lastSeenAt !== machine.lastSeenAt) {
1005
+ await dispatchOwnerMutation(deps.db, 'recordMachineProbe', [machine.instanceId, next], 'machine registry probe');
1006
+ noteRegistryChanged();
1007
+ }
1008
+ }
1009
+
1010
+ // WP-3: the box's CLI sign-in truth, refreshed on the SAME tick as its health — never on its own
1011
+ // timer. Only a box whose manifest probe just answered is asked: a box that did not answer tells
1012
+ // us nothing about its CLIs, so its stored entry is left exactly as it was (never blanked to
1013
+ // 'unknown', which would weaken a verdict we already hold). A refresh older than
1014
+ // AGENT_AUTH_REFRESH_MS (or a box never fetched) is re-read; anything fresher is reused.
1015
+ for (const { machine, probe } of results) {
1016
+ if (!probe.ok) continue;
1017
+ const machineHeaders = headersFor(machine.instanceId);
1018
+ const url = probe.url || machine.url;
1019
+ // D-4: the manifest we just read IS the machine's settings + host identity. Cached here so
1020
+ // /api/machines serves the machine-owned displayName/favorite/hidden and os/arch/hostname.
1021
+ _cacheManifestHost(machine.instanceId, probe.host);
1022
+ // WP-0 (S-2): the same manifest read carries the box's build stamp; cache it here, the one place
1023
+ // the hub learns it, so /api/machines can serve build.sha without a second fetch.
1024
+ _cacheManifestBuild(machine.instanceId, probe.build);
1025
+
1026
+ const previous = _agentAuth.get(machine.instanceId);
1027
+ if (!previous || now - previous.checkedAt >= AGENT_AUTH_REFRESH_MS) {
1028
+ const probed = await _probeAgentAuth(url, { fetchImpl, timeoutMs, peerHeaders: machineHeaders });
1029
+ _agentAuth.set(machine.instanceId, { ...probed, checkedAt: now });
1030
+ // A sign-in CHANGE is registry news (a reader keeping a snapshot must re-read); a first read
1031
+ // or an identical re-read is not.
1032
+ if (previous && (previous.claude !== probed.claude || previous.codex !== probed.codex)) noteRegistryChanged();
1033
+ }
1034
+
1035
+ // WP-13b (D-15): the box's OWN available-agents answer, on the SAME tick and under the SAME
1036
+ // per-instance 60 s stamp as the sign-in read — the New Session card and the machine row need to
1037
+ // know whether the box has the DeepSeek Harness BEFORE a create is dialled. A failed / 404 /
1038
+ // unparseable read keeps the last value (P3): only an explicit answer moves it.
1039
+ const presenceEntry = _agentPresence.get(machine.instanceId) || null;
1040
+ if (!presenceEntry || now - presenceEntry.checkedAt >= AGENT_AUTH_REFRESH_MS) {
1041
+ const read = await _probeAgentAvailability(url, { fetchImpl, timeoutMs, peerHeaders: machineHeaders });
1042
+ if (read.state === 'block') _setAgentPresence(machine.instanceId, read.presence, now);
1043
+ // A failed / 404 / unparseable read KEEPS the last value and moves only the stamp, so a box
1044
+ // without this route is asked once a minute rather than on every tick. A FIRST failure stores
1045
+ // nothing at all — there is no previous value to keep, and "never read" is the honest answer.
1046
+ else if (presenceEntry) _setAgentPresence(machine.instanceId, presenceEntry.presence, now);
1047
+ }
1048
+
1049
+ // WP-7 (§6.5/AC-7a/AC-7c): the box's own skill inventory, on the SAME tick and under the SAME
1050
+ // per-instance 60 s stamp as the sign-in read — one pass over `results`, no timer of this
1051
+ // module's own (AC-7b), so a box-created skill shows up on the row within 60 s + one 15 s tick.
1052
+ const skillsEntry = _agentSkills.get(machine.instanceId) || null;
1053
+ if (!skillsEntry || now - skillsEntry.checkedAt >= AGENT_AUTH_REFRESH_MS) {
1054
+ const inventory = await _probeAgentSkills(url, { fetchImpl, timeoutMs, peerHeaders: machineHeaders });
1055
+ if (inventory) {
1056
+ const onBox = await _classifyInventory(inventory, { deps, now });
1057
+ // null = the hub could not read its OWN skills: keep what we hold (a build fault is not news
1058
+ // about the box), and move only the stamp so the next read waits its full 60 s.
1059
+ _setAgentSkills(machine.instanceId, onBox === null ? (skillsEntry ? skillsEntry.onBox : null) : onBox, now);
1060
+ } else if (skillsEntry) {
1061
+ // An older box (404) or a failed read: KEEP the last value, no error noise — and move the
1062
+ // stamp, so a box without this route is asked once a minute rather than on every tick.
1063
+ _setAgentSkills(machine.instanceId, skillsEntry.onBox, now);
1064
+ }
1065
+ }
1066
+
1067
+ // WP-1 (§6.2/D-9): the box's OWN model availability, on the SAME tick and under its own per-
1068
+ // instance ORIGIN_TTL_MS (10 min) stamp — the hub must not ask more often than the box would
1069
+ // re-probe. A failure keeps the last block (never blanked), and a 404 records that the box
1070
+ // predates WP-1 (models: null, the row renders exactly as today).
1071
+ const modelsEntry = _machineModels.get(machine.instanceId) || null;
1072
+ if (!modelsEntry || now - modelsEntry.checkedAt >= MODELS_REFRESH_MS) {
1073
+ const read = await _probeModelsAvailability(url, { fetchImpl, timeoutMs, peerHeaders: machineHeaders });
1074
+ if (read.state === 'block') _setMachineModels(machine.instanceId, read.block, now);
1075
+ else if (read.state === 'null') _setMachineModels(machine.instanceId, null, now);
1076
+ else if (modelsEntry) _setMachineModels(machine.instanceId, modelsEntry.block, now);
1077
+ }
1078
+
1079
+ // D-12/P-5: the box's own host sample and its roster version, refreshed on the SAME tick and
1080
+ // under the SAME per-instance 60 s stamp as the sign-in read — one pass over `results`, no
1081
+ // timer of this module's own. Both are asked ONLY of a box whose manifest probe just answered
1082
+ // (a silent box teaches us nothing, and its stored entry keeps its FROZEN checkedAt — AC-2b).
1083
+ const statsEntry = _hostStats.get(machine.instanceId);
1084
+ if (statsEntry && now - (statsEntry.attemptedAt || statsEntry.checkedAt) < HOST_STATS_REFRESH_MS) continue;
1085
+ const sample = await _probeHostStats(url, { fetchImpl, timeoutMs, peerHeaders: machineHeaders });
1086
+ // Two stamps: `attemptedAt` gates the 60 s refresh on every attempt; `checkedAt` moves ONLY when
1087
+ // a sample arrived. A failed stats read keeps the sample we hold AND its old checkedAt, so the
1088
+ // picker's "stats stale" (checkedAt older than 180 s) is driven by the stats read itself.
1089
+ _hostStats.set(machine.instanceId, sample
1090
+ ? { sample, checkedAt: now, attemptedAt: now }
1091
+ : { sample: (statsEntry && statsEntry.sample) || null, checkedAt: (statsEntry && statsEntry.checkedAt) || 0, attemptedAt: now });
1092
+ await _maybePullRoster(machine, url, { deps, fetchImpl, timeoutMs, peerHeaders: machineHeaders });
1093
+ }
1094
+
1095
+ return {
1096
+ ok: true,
1097
+ probed: results.length,
1098
+ reachable: counts.reachable || 0,
1099
+ unknown: counts.unknown || 0,
1100
+ unreachable: counts.unreachable || 0,
1101
+ unhealthy: counts.unhealthy || 0,
1102
+ hubNetworkSuspect,
1103
+ results: results.map((row) => ({
1104
+ instanceId: row.machine.instanceId,
1105
+ url: row.machine.url,
1106
+ ok: !!row.probe.ok,
1107
+ ...(row.probe.ok ? {} : { error: row.probe.error }),
1108
+ })),
1109
+ };
1110
+ }
1111
+
1112
+ /**
1113
+ * Boot seeding (§8 WP-0 "seed-from-CTM_SYNC_PEERS"): every configured sync peer that is not
1114
+ * already registered becomes a DISABLED machines row — visible, never probed, so the user can
1115
+ * enable it in Settings → Machines (WP-5). Env is a seed, never an authority: it never
1116
+ * auto-enables a machine.
1117
+ */
1118
+ function seedMachinesFromSyncPeers(deps = {}) {
1119
+ // The peer list (or the parser) is INJECTED by the caller — lib/hosted-sync-worker.js passes
1120
+ // its own parsed peers. Requiring hosted-sync-worker from here would be a dependency cycle
1121
+ // (it requires this module for the probe ride), so this module never imports it.
1122
+ let peers = Array.isArray(deps.peers) ? deps.peers : [];
1123
+ if (!peers.length && typeof deps.parseSyncPeers === 'function') {
1124
+ try { peers = deps.parseSyncPeers(deps.env || process.env) || []; } catch { peers = []; } // expected: malformed CTM_SYNC_PEERS seeds nothing
1125
+ }
1126
+ if (!peers.length) return { seeded: 0, skipped: 0 };
1127
+ // No owner handle (unit tests, receiver-only boots) ⇒ nothing to seed; report the peers as
1128
+ // skipped rather than counting writes that never happened.
1129
+ if (!deps.db || typeof deps.db.upsertMachineRow !== 'function') return { seeded: 0, skipped: peers.length };
1130
+ const known = new Set();
1131
+ for (const row of _rows(deps).map((r) => _machineView(r))) {
1132
+ if (row.instanceId) known.add(row.instanceId);
1133
+ if (row.url) known.add(row.url);
1134
+ }
1135
+ let seeded = 0;
1136
+ let skipped = 0;
1137
+ for (const peer of peers) {
1138
+ const url = _trimUrl(peer.url);
1139
+ if (!url) { skipped += 1; continue; }
1140
+ const instanceId = _string(peer.instanceId, 160);
1141
+ if ((instanceId && known.has(instanceId)) || known.has(url)) { skipped += 1; continue; }
1142
+ const key = instanceId || `unpinned:${url}`;
1143
+ if (known.has(key)) { skipped += 1; continue; }
1144
+ // void: boot seeding is fire-and-forget by contract (a failed seed is a missing card the
1145
+ // next boot retries, surfaced via status().machineSeed) — never a boot failure.
1146
+ void dispatchOwnerMutation(deps.db, 'upsertMachineRow', [{
1147
+ instanceId: key,
1148
+ displayName: instanceId || _hostLabel(url),
1149
+ url,
1150
+ walleUrl: _trimUrl(peer.walleUrl),
1151
+ enabled: 0,
1152
+ }], 'machine registry seed');
1153
+ known.add(key);
1154
+ known.add(url);
1155
+ seeded += 1;
1156
+ }
1157
+ if (seeded) noteRegistryChanged();
1158
+ return { seeded, skipped };
1159
+ }
1160
+
1161
+ /** S-30: the machines section of /api/instance/sync-status (lib/instance-sync-status.js). */
1162
+ function summarizeMachines(deps = {}) {
1163
+ const machines = _rows(deps).map((row) => _machineView(row));
1164
+ const summary = { count: machines.length, reachable: 0, unknown: 0, unreachable: 0, unhealthy: 0, hubNetworkSuspect: !!_lastSweep.hubNetworkSuspect };
1165
+ for (const machine of machines) {
1166
+ if (Object.prototype.hasOwnProperty.call(summary, machine.health)) summary[machine.health] += 1;
1167
+ }
1168
+ return summary;
1169
+ }
1170
+
1171
+ /** WP-3: the box's last-read coding-CLI sign-in verdict, or null when it was never fetched. */
1172
+ function agentAuthFor(instanceId) {
1173
+ return _agentAuth.get(_string(instanceId, 160)) || null;
1174
+ }
1175
+
1176
+ /**
1177
+ * WP-13b (D-15): the box's last-read agent presence as `{ dsh: { available: boolean } }`, or null
1178
+ * when this process has never read it. The shape is the one `lib/remote-machine-create.js`'s
1179
+ * `_boxAgentMissing` already accepts verbatim — the gate asks THIS module, never the box, so a
1180
+ * never-read (or restarted) hub holds no verdict and refuses nothing (P3).
1181
+ */
1182
+ function agentAvailabilityFor(instanceId) {
1183
+ const entry = _agentPresence.get(_string(instanceId, 160));
1184
+ if (!entry) return null;
1185
+ return { dsh: { available: entry.presence.dsh === true } };
1186
+ }
1187
+
1188
+ /**
1189
+ * The §6.5 `skills` block for a machine row, or null when this process neither pushed nor read an
1190
+ * inventory for this box.
1191
+ *
1192
+ * WP-7: the block is the UNION of the two module truths — the last push (§6.5, WP-3) and the last
1193
+ * inventory read (D-11/§6.5). A box this hub has read an inventory from but never pushed to still
1194
+ * gets a block (`sentTreeHash: null`, `onBox` populated); that is the WP-9 review card's whole input.
1195
+ * `onBox`/`checkedAt` are null — never absent — when only one half is known, so a reader can always
1196
+ * ask "have we read this box's own skills, and when".
1197
+ */
1198
+ /**
1199
+ * WP-8 (§6.5): this machine's per-machine plugin exclusions, read through the caller's provider.
1200
+ *
1201
+ * The provider is INJECTED (`deps.excludePluginsFor`), never imported: the real reader is a hub-side
1202
+ * JSON file that lib/api/machines.js supplies, so this module — which must run in unit tests with a
1203
+ * temp db and no data dir — never reads a real one, and a default `[]` keeps every existing caller
1204
+ * (and the probe sweep) exactly as it was.
1205
+ */
1206
+ function _excludePluginsFor(deps = {}, instanceId) {
1207
+ const provider = deps.excludePluginsFor;
1208
+ if (typeof provider !== 'function') return [];
1209
+ try {
1210
+ const list = provider(instanceId);
1211
+ return Array.isArray(list) ? list.map((id) => _string(id, 200)).filter(Boolean) : [];
1212
+ } catch {
1213
+ return []; // expected: an unreadable exclusion file is "nothing excluded", never a failed row read
1214
+ }
1215
+ }
1216
+
1217
+ /**
1218
+ * WP-9 (§9 S-9): the PLUGIN names the hub's last bundle carried — the Review dialog's exclusion
1219
+ * checklist is "the plugins the hub sends to this box", so it is derived from the push entry's own
1220
+ * item list (never re-read from disk here: this module holds no home dir, and the push already
1221
+ * decided what went out).
1222
+ *
1223
+ * push.js's result reports `sentCount`/`heldBack` and NO items in this release, so the reader accepts
1224
+ * every shape a caller could hand it (`items`, a nested `bundle.items`, or a plain `plugins` list) and
1225
+ * otherwise falls back to what the PREVIOUS push recorded — the same rule the rest of
1226
+ * `recordAgentBundlePush` follows: a failed push must not empty a checklist the user is looking at.
1227
+ */
1228
+ function _pluginNamesFromItems(items) {
1229
+ const names = new Set();
1230
+ for (const item of Array.isArray(items) ? items : []) {
1231
+ if (!item || typeof item !== 'object') continue;
1232
+ if (_string(item.kind, 40) !== 'plugin') continue;
1233
+ const name = _string(item.name, 200);
1234
+ if (name) names.add(name);
1235
+ }
1236
+ return [...names].sort();
1237
+ }
1238
+
1239
+ function _pluginsFromResult(result, previous) {
1240
+ const nested = result && result.bundle && result.bundle.items;
1241
+ if (Array.isArray(result && result.items)) return _pluginNamesFromItems(result.items);
1242
+ if (Array.isArray(nested)) return _pluginNamesFromItems(nested);
1243
+ if (Array.isArray(result && result.plugins)) {
1244
+ const names = new Set();
1245
+ for (const value of result.plugins) {
1246
+ const name = _string(value && typeof value === 'object' ? value.name : value, 200);
1247
+ if (name) names.add(name);
1248
+ }
1249
+ return [...names].sort();
1250
+ }
1251
+ return previous && Array.isArray(previous.plugins) ? previous.plugins.slice() : [];
1252
+ }
1253
+
1254
+ /**
1255
+ * WP-6 D-13 (§6.3): the per-phase timings a push result carries, kept as a COPY of only the keys
1256
+ * whose values are finite numbers — anything else (a missing object, a string, NaN) is dropped, and
1257
+ * a result that kept nothing reads as null. Recorded for an ok AND a failed push alike.
1258
+ */
1259
+ function _pushTimingsOf(result) {
1260
+ const source = result && result.timings;
1261
+ if (!source || typeof source !== 'object') return null;
1262
+ const kept = {};
1263
+ let any = false;
1264
+ for (const key of Object.keys(source)) {
1265
+ const value = source[key];
1266
+ if (typeof value === 'number' && Number.isFinite(value)) {
1267
+ kept[key] = value;
1268
+ any = true;
1269
+ }
1270
+ }
1271
+ return any ? kept : null;
1272
+ }
1273
+
1274
+ function _skillsView(instanceId, deps = {}) {
1275
+ const id = _string(instanceId, 160);
1276
+ return _skillsViewFrom(_agentBundlePush.get(id) || null, _agentSkills.get(id) || null, _excludePluginsFor(deps, id));
1277
+ }
1278
+
1279
+ /** The surfaced `skills` block, one shape for the row and the change check below. */
1280
+ function _skillsViewFrom(pushEntry, inventoryEntry = null, excludePlugins = []) {
1281
+ const exclusions = Array.isArray(excludePlugins) ? excludePlugins : [];
1282
+ // WP-8: a machine with nothing pushed, nothing read AND nothing excluded has no block at all — the
1283
+ // pre-WP-8 `null` (and every test that pins it) is preserved. A machine with ONLY exclusions does get
1284
+ // a block, because the exclusions are the hub's own setting for it and a reader must be able to see
1285
+ // them before the first probe ever answers.
1286
+ if (!pushEntry && !inventoryEntry && !exclusions.length) return null;
1287
+ return {
1288
+ sentTreeHash: pushEntry ? pushEntry.sentTreeHash : null,
1289
+ sentAt: pushEntry ? pushEntry.sentAt : 0,
1290
+ sentCount: pushEntry ? pushEntry.sentCount : 0,
1291
+ heldBack: pushEntry ? pushEntry.heldBack : [],
1292
+ pushError: pushEntry ? pushEntry.pushError : null,
1293
+ // WP-6 D-13 (§6.3): the per-phase timings of the LAST push — which phase cost the caller its wait.
1294
+ // Null when no push result has carried any yet (a pre-WP-6 push, or none at all).
1295
+ pushTimings: pushEntry && pushEntry.pushTimings ? pushEntry.pushTimings : null,
1296
+ onBox: inventoryEntry ? inventoryEntry.onBox : null,
1297
+ checkedAt: inventoryEntry ? inventoryEntry.checkedAt : null,
1298
+ excludePlugins: exclusions.slice(),
1299
+ // WP-9 (§9 S-9): the plugin names the last push carried, for the Review dialog's checklist.
1300
+ // Always an array on a non-null block, like `heldBack` — a reader never has to treat "no push"
1301
+ // and "no plugins" differently.
1302
+ plugins: pushEntry && Array.isArray(pushEntry.plugins) ? pushEntry.plugins.slice() : [],
1303
+ };
1304
+ }
1305
+
1306
+ /** A `skills` change is registry news — a reader keeping a snapshot must re-read (`noteRegistryChanged`). */
1307
+ function _skillsChanged(previousPush, nextPush, inventoryEntry) {
1308
+ return JSON.stringify(_skillsViewFrom(previousPush, inventoryEntry))
1309
+ !== JSON.stringify(_skillsViewFrom(nextPush, inventoryEntry));
1310
+ }
1311
+
1312
+ /** WP-3: the last agent-skill push this process made to `instanceId`, or null when it made none. */
1313
+ function agentBundlePushFor(instanceId) {
1314
+ return _skillsView(instanceId);
1315
+ }
1316
+
1317
+ /**
1318
+ * WP-3 (§6.5): record the outcome of a push to `instanceId`.
1319
+ *
1320
+ * ok -> sentTreeHash/sentAt/sentCount/heldBack describe THIS push and `pushError` is cleared.
1321
+ * ok:false-> the failure is stamped (`pushError: {code, message?, continuing?, at}` — `message` is
1322
+ * the box's own reason, `result.boxCode` from lib/agent-bundle/push.js D-4, present only
1323
+ * when the box named one; `continuing` is present only when the push outlived the
1324
+ * caller's wait and is still running, WP-6 D-11) while the last SUCCESSFUL push's fields
1325
+ * are kept: D-6's whole point is that a failed push changes nothing about the session,
1326
+ * and a reader must not conclude the box stopped running its last tree.
1327
+ *
1328
+ * WP-6 D-13: BOTH outcomes also store `pushTimings`, the per-phase timings the result carried (only
1329
+ * keys whose values are finite numbers; null when it carried none). A LATE ok result (D-11) clears
1330
+ * `pushError` exactly as an on-time one does — it is `recordAgentBundlePush` either way.
1331
+ *
1332
+ * Bumps the registry revision only when the surfaced `skills` block actually changed.
1333
+ */
1334
+ function recordAgentBundlePush(instanceId, result, deps = {}) {
1335
+ const id = _string(instanceId, 160);
1336
+ if (!id) return null;
1337
+ const now = _now(deps);
1338
+ const previous = _agentBundlePush.get(id) || null;
1339
+ const ok = !!(result && result.ok);
1340
+ const pushTimings = _pushTimingsOf(result);
1341
+ const next = ok
1342
+ ? {
1343
+ sentTreeHash: _string(result.treeHash, 80) || null,
1344
+ sentAt: now,
1345
+ sentCount: Number(result.sentCount) || 0,
1346
+ heldBack: Array.isArray(result.heldBack) ? result.heldBack : [],
1347
+ plugins: _pluginsFromResult(result, previous),
1348
+ pushError: null,
1349
+ pushTimings,
1350
+ }
1351
+ : {
1352
+ sentTreeHash: previous ? previous.sentTreeHash : null,
1353
+ sentAt: previous ? previous.sentAt : 0,
1354
+ sentCount: previous ? previous.sentCount : 0,
1355
+ heldBack: previous ? previous.heldBack : [],
1356
+ plugins: _pluginsFromResult(result, previous),
1357
+ pushError: {
1358
+ code: _string(result && result.code, 80) || 'agent_bundle_push_network',
1359
+ // D-4: the box's own reason, so the Machines line can say WHY (public/m/machines.js renders
1360
+ // `error.message || error.code`). Absent when the box named nothing — never an empty string.
1361
+ ...(_string(result && result.boxCode, 80) ? { message: _string(result.boxCode, 80) } : {}),
1362
+ // WP-6 D-11: the push outlived the caller's wait and is running on — its result will arrive
1363
+ // later. Present ONLY when true, like every other optional key here.
1364
+ ...(result && result.continuing === true ? { continuing: true } : {}),
1365
+ at: now,
1366
+ },
1367
+ pushTimings,
1368
+ };
1369
+ _agentBundlePush.set(id, next);
1370
+ if (_skillsChanged(previous, next, _agentSkills.get(id) || null)) noteRegistryChanged();
1371
+ return _skillsViewFrom(next, _agentSkills.get(id) || null);
1372
+ }
1373
+
1374
+ /**
1375
+ * D-12/D-16: the box's own host sample as this hub last read it, with the hub-clock stamp of that
1376
+ * read, or null when the box has never been asked successfully. Readers use `checkedAt` to label a
1377
+ * stale number (the UI says "stats stale" past 180 s) instead of printing it as current.
1378
+ */
1379
+ function hostStatsFor(instanceId) {
1380
+ const entry = _hostStats.get(_string(instanceId, 160));
1381
+ return entry ? { sample: entry.sample, checkedAt: entry.checkedAt } : null;
1382
+ }
1383
+
1384
+ /**
1385
+ * WP-8 §6.5 step 7: drop the memoized hub item lists.
1386
+ *
1387
+ * A skill IMPORT changes the hub's own skill tree, and the cache is keyed only by platform — so without
1388
+ * this the hub would keep classifying every OTHER box against the pre-import list for up to 60 s. The
1389
+ * next read rebuilds from the (now-changed) tree.
1390
+ */
1391
+ function invalidateHubItemCache() {
1392
+ _hubItemCache.clear();
1393
+ }
1394
+
1395
+ /**
1396
+ * WP-8 §6.5 step 7: make ONE box's inventory stale, so the next probe tick re-reads it.
1397
+ *
1398
+ * The stamp (not the entry) is zeroed: the last classification stays visible — a reader sees the
1399
+ * pre-import review list rather than a row blanked to "no skills" — and `probeMachinesOnce`'s
1400
+ * `now - checkedAt >= AGENT_AUTH_REFRESH_MS` gate re-reads on the very next tick. Only a box this
1401
+ * process has actually read is touched; a never-read box has nothing to invalidate.
1402
+ */
1403
+ function markAgentSkillsStale(instanceId) {
1404
+ const id = _string(instanceId, 160);
1405
+ if (!id) return;
1406
+ const entry = _agentSkills.get(id);
1407
+ if (entry) _agentSkills.set(id, { ...entry, checkedAt: 0 });
1408
+ }
1409
+
1410
+ /**
1411
+ * WP-4: routes just deleted ON the box are gone from the hub's cached models block at once (the
1412
+ * Review dialog re-reads the list right after the remove), and the stamp is zeroed so the very next
1413
+ * tick re-reads the box's own truth. The block's other fields are left as read — only the box can
1414
+ * say what the removal did to `usable` or to a /delegate pick that named a removed route.
1415
+ */
1416
+ function forgetMachineModelRoutes(instanceId, routeIds) {
1417
+ const id = _string(instanceId, 160);
1418
+ const entry = id ? _machineModels.get(id) : null;
1419
+ if (!entry) return;
1420
+ const gone = new Set((Array.isArray(routeIds) ? routeIds : []).map((route) => _string(route, 200)));
1421
+ const block = entry.block && Array.isArray(entry.block.unreachable)
1422
+ ? { ...entry.block, unreachable: entry.block.unreachable.filter((row) => !gone.has(row.id)) }
1423
+ : entry.block;
1424
+ _machineModels.set(id, { block, checkedAt: 0 });
1425
+ noteRegistryChanged();
1426
+ }
1427
+
1428
+ function markMachineModelsStale(instanceId) {
1429
+ const id = _string(instanceId, 160);
1430
+ if (!id) return;
1431
+ const entry = _machineModels.get(id);
1432
+ if (!entry) return;
1433
+ _machineModels.set(id, { block: entry.block, checkedAt: 0 });
1434
+ }
1435
+
1436
+ /** Test seam: the miss counters, the agent-auth/host-stats/manifest caches, the skills push state
1437
+ * (WP-3) and the skill inventory + hub-item cache (WP-7), the sweep verdict, and the hub's cached
1438
+ * away-path read (WP-3 rev 3). */
1439
+ function _resetMachineRegistryStateForTests() {
1440
+ _missStreak.clear();
1441
+ _agentAuth.clear();
1442
+ _agentPresence.clear();
1443
+ _hostStats.clear();
1444
+ _manifestHost.clear();
1445
+ _manifestBuild.clear();
1446
+ _agentBundlePush.clear();
1447
+ _agentSkills.clear();
1448
+ _machineModels.clear();
1449
+ _hubItemCache.clear();
1450
+ _lastSweep = { hubNetworkSuspect: false, at: 0 };
1451
+ _awayPathProxyExists = null;
1452
+ }
1453
+
1454
+ module.exports = {
1455
+ UNREACHABLE_AFTER_MS,
1456
+ UNKNOWN_AFTER_MISSES,
1457
+ HEALTH_STATES,
1458
+ AGENT_AUTH_REFRESH_MS,
1459
+ HOST_STATS_REFRESH_MS,
1460
+ agentAuthFor,
1461
+ // WP-13b (D-15): the box's own available-agents presence as this hub last read it
1462
+ // (`{ dsh: { available } }`), or null when it was never read — the gate in
1463
+ // lib/remote-machine-create.js and the machine row both read it through here.
1464
+ agentAvailabilityFor,
1465
+ hostStatsFor,
1466
+ listMachines,
1467
+ // WP-3 rev 3 (§13.6 D-17): why an unreachable box cannot be dialled from here (`no_away_path` /
1468
+ // `away_path_down`), or null when that is not this hub's to say. Read straight off the machine view.
1469
+ machineAwayReason,
1470
+ registryRevision,
1471
+ noteRegistryChanged,
1472
+ registerMachine,
1473
+ probeMachinesOnce,
1474
+ seedMachinesFromSyncPeers,
1475
+ summarizeMachines,
1476
+ // WP-3 (docs/remote-agent-skills-bundles-2026-10-25.html D-9/§6.5): the box's os (from the cached
1477
+ // manifest host block) and the agent-skill push state the machine row surfaces.
1478
+ manifestHostFor,
1479
+ recordAgentBundlePush,
1480
+ agentBundlePushFor,
1481
+ // WP-8 (§6.5 step 7): after an import/retire/exclude-plugins write the hub's own item list and this
1482
+ // box's inventory read are both stale — these two make the next read see the change.
1483
+ invalidateHubItemCache,
1484
+ markAgentSkillsStale,
1485
+ // WP-5 (§6.4): after a copy the box has one more row — zero the stamp so the next tick re-reads
1486
+ // it, without dropping the block (the removal path's `forgetMachineModelRoutes` drops ids, which
1487
+ // is the wrong move for an ADDITION: the hub cannot say what the new row's verdict is).
1488
+ markMachineModelsStale,
1489
+ forgetMachineModelRoutes,
1490
+ // WP-5 (§6.4): the §6.2 block as this hub last read it — the hub's "Use on <box>" list reads it
1491
+ // to drop the routes the box already has and the origins the box already said it cannot reach.
1492
+ modelsFor,
1493
+ _resetMachineRegistryStateForTests,
1494
+ };