create-walle 0.10.2 → 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 (488) hide show
  1. package/README.md +3 -1
  2. package/lib/release-schema.json +2 -2
  3. package/lib/runtime-closure-policy.json +8 -7
  4. package/package.json +1 -1
  5. package/template/coding-task-manager/api-prompts.js +45 -7
  6. package/template/coding-task-manager/bin/claude-hook.js +55 -1
  7. package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
  8. package/template/coding-task-manager/bin/land.js +2069 -0
  9. package/template/coding-task-manager/bin/memory-census.js +4 -2
  10. package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
  11. package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
  12. package/template/coding-task-manager/db.js +156 -32
  13. package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
  14. package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
  15. package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
  16. package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
  17. package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
  18. package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
  19. package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
  20. package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
  21. package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
  22. package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
  24. package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
  25. package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
  26. package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
  27. package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
  28. package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
  29. package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
  30. package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
  31. package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
  32. package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
  33. package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
  34. package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
  35. package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
  36. package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
  37. package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
  38. package/template/coding-task-manager/lib/agent-definitions.js +19 -0
  39. package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
  40. package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
  41. package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
  42. package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
  43. package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
  44. package/template/coding-task-manager/lib/agent-presets.js +84 -1
  45. package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
  46. package/template/coding-task-manager/lib/agent-version-service.js +74 -11
  47. package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
  48. package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
  49. package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
  50. package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
  51. package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
  52. package/template/coding-task-manager/lib/api/fleet.js +118 -0
  53. package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
  54. package/template/coding-task-manager/lib/api/ingress.js +9 -1
  55. package/template/coding-task-manager/lib/api/landing.js +403 -0
  56. package/template/coding-task-manager/lib/api/machines.js +691 -2
  57. package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
  58. package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
  59. package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
  60. package/template/coding-task-manager/lib/api/session-attach.js +623 -0
  61. package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
  62. package/template/coding-task-manager/lib/api/sessions.js +77 -51
  63. package/template/coding-task-manager/lib/api/setup-network.js +544 -16
  64. package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
  65. package/template/coding-task-manager/lib/api/setup.js +829 -10
  66. package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
  67. package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
  68. package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
  69. package/template/coding-task-manager/lib/auth-rules.js +292 -21
  70. package/template/coding-task-manager/lib/background-stop.js +369 -0
  71. package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
  72. package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
  73. package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
  74. package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
  75. package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
  76. package/template/coding-task-manager/lib/build-info.js +4 -2
  77. package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
  78. package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
  79. package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
  80. package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
  81. package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
  82. package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
  83. package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
  84. package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
  85. package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
  86. package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
  87. package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
  88. package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
  89. package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
  90. package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
  91. package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
  92. package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
  93. package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
  94. package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
  95. package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
  96. package/template/coding-task-manager/lib/conversation-sync.js +355 -0
  97. package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
  98. package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
  99. package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
  100. package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
  101. package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
  102. package/template/coding-task-manager/lib/db-domains.js +1 -0
  103. package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
  104. package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
  105. package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
  106. package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
  107. package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
  108. package/template/coding-task-manager/lib/feature-manifest.js +4 -0
  109. package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
  110. package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
  111. package/template/coding-task-manager/lib/fleet-hub.js +762 -0
  112. package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
  113. package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
  114. package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
  115. package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
  116. package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
  117. package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
  118. package/template/coding-task-manager/lib/fleet-update.js +798 -0
  119. package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
  120. package/template/coding-task-manager/lib/fs-watcher.js +58 -42
  121. package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
  122. package/template/coding-task-manager/lib/headless-term-service.js +84 -8
  123. package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
  124. package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
  125. package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
  126. package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
  127. package/template/coding-task-manager/lib/host-handoff.js +16 -19
  128. package/template/coding-task-manager/lib/host-stats.js +150 -3
  129. package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
  130. package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
  131. package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
  132. package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
  133. package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
  134. package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
  135. package/template/coding-task-manager/lib/hub-bridge.js +398 -0
  136. package/template/coding-task-manager/lib/idle-notify.js +491 -0
  137. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
  138. package/template/coding-task-manager/lib/instance-manifest.js +5 -1
  139. package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
  140. package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
  141. package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
  142. package/template/coding-task-manager/lib/landing/config.js +536 -0
  143. package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
  144. package/template/coding-task-manager/lib/landing/gate.js +360 -0
  145. package/template/coding-task-manager/lib/landing/git.js +344 -0
  146. package/template/coding-task-manager/lib/landing/lease.js +247 -0
  147. package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
  148. package/template/coding-task-manager/lib/landing/proof.js +223 -0
  149. package/template/coding-task-manager/lib/landing/queue.js +998 -0
  150. package/template/coding-task-manager/lib/landing/radar.js +489 -0
  151. package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
  152. package/template/coding-task-manager/lib/landing/state.js +263 -0
  153. package/template/coding-task-manager/lib/landing/stop.js +49 -0
  154. package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
  155. package/template/coding-task-manager/lib/landing/sync.js +456 -0
  156. package/template/coding-task-manager/lib/landing/train.js +885 -0
  157. package/template/coding-task-manager/lib/landing/window.js +57 -0
  158. package/template/coding-task-manager/lib/launch-environment.js +121 -0
  159. package/template/coding-task-manager/lib/launch-model-override.js +36 -10
  160. package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
  161. package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
  162. package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
  163. package/template/coding-task-manager/lib/lead-model-options.js +160 -3
  164. package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
  165. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
  166. package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
  167. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
  168. package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
  169. package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
  170. package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
  171. package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
  172. package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
  173. package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
  174. package/template/coding-task-manager/lib/machine-registry.js +750 -5
  175. package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
  176. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
  177. package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
  178. package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
  179. package/template/coding-task-manager/lib/memory-join.js +337 -0
  180. package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
  181. package/template/coding-task-manager/lib/memory-sync.js +565 -0
  182. package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
  183. package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
  184. package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
  185. package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
  186. package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
  187. package/template/coding-task-manager/lib/model-availability.js +727 -0
  188. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
  189. package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
  190. package/template/coding-task-manager/lib/model-id-rule.js +19 -0
  191. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  192. package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
  193. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  194. package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
  195. package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
  196. package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
  197. package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
  198. package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
  199. package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
  200. package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
  201. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  202. package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
  203. package/template/coding-task-manager/lib/native-history-control.js +6 -4
  204. package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
  205. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
  206. package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
  207. package/template/coding-task-manager/lib/opencode-cli.js +480 -0
  208. package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
  209. package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
  210. package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
  211. package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
  212. package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
  213. package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
  214. package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
  215. package/template/coding-task-manager/lib/perf-summary.js +7 -0
  216. package/template/coding-task-manager/lib/perf-tracker.js +63 -2
  217. package/template/coding-task-manager/lib/project-config.js +7 -0
  218. package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
  219. package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
  220. package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
  221. package/template/coding-task-manager/lib/read-pool-client.js +89 -2
  222. package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
  223. package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
  224. package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
  225. package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
  226. package/template/coding-task-manager/lib/relay-client.js +57 -3
  227. package/template/coding-task-manager/lib/relink-audit.js +34 -5
  228. package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
  229. package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
  230. package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
  231. package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
  232. package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
  233. package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
  234. package/template/coding-task-manager/lib/restart-guard.js +18 -6
  235. package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
  236. package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
  237. package/template/coding-task-manager/lib/resume-admission.js +17 -3
  238. package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
  239. package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
  240. package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
  241. package/template/coding-task-manager/lib/runtime-contract.js +3 -1
  242. package/template/coding-task-manager/lib/schema-indexes.js +1 -0
  243. package/template/coding-task-manager/lib/session-context-locator.js +742 -0
  244. package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
  245. package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
  246. package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
  247. package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
  248. package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
  249. package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
  250. package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
  251. package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
  252. package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
  253. package/template/coding-task-manager/lib/session-create/response.js +7 -7
  254. package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
  255. package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
  256. package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
  257. package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
  258. package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
  259. package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
  260. package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
  261. package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
  262. package/template/coding-task-manager/lib/session-empty.js +13 -10
  263. package/template/coding-task-manager/lib/session-host-manager.js +60 -0
  264. package/template/coding-task-manager/lib/session-jobs.js +3 -1
  265. package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
  266. package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
  267. package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
  268. package/template/coding-task-manager/lib/session-park.js +738 -0
  269. package/template/coding-task-manager/lib/session-path-grant.js +13 -4
  270. package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
  271. package/template/coding-task-manager/lib/session-transport.js +17 -0
  272. package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
  273. package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
  274. package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
  275. package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
  276. package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
  277. package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
  278. package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
  279. package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
  280. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
  281. package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
  282. package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
  283. package/template/coding-task-manager/lib/submission-ack.js +90 -2
  284. package/template/coding-task-manager/lib/systemd-scope.js +115 -0
  285. package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
  286. package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
  287. package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
  288. package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
  289. package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
  290. package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
  291. package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
  292. package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
  293. package/template/coding-task-manager/lib/transcript-source.js +36 -4
  294. package/template/coding-task-manager/lib/transcript-store.js +9 -0
  295. package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
  296. package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
  297. package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
  298. package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
  299. package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
  300. package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
  301. package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
  302. package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
  303. package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
  304. package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
  305. package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
  306. package/template/coding-task-manager/lib/walle/turn.js +1 -1
  307. package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
  308. package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
  309. package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
  310. package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
  311. package/template/coding-task-manager/lib/walle-row-history.js +495 -0
  312. package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
  313. package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
  314. package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
  315. package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
  316. package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
  317. package/template/coding-task-manager/lib/ws/misc.js +1 -1
  318. package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
  319. package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
  320. package/template/coding-task-manager/oauth-proxy.js +112 -115
  321. package/template/coding-task-manager/package-lock.json +10 -94
  322. package/template/coding-task-manager/package.json +4 -3
  323. package/template/coding-task-manager/protocol/message-table.json +10 -0
  324. package/template/coding-task-manager/protocol/messages.js +10 -0
  325. package/template/coding-task-manager/protocol/ws-schema.json +20 -0
  326. package/template/coding-task-manager/protocol/ws-schema.md +2 -0
  327. package/template/coding-task-manager/public/css/app.css +170 -0
  328. package/template/coding-task-manager/public/css/walle-session.css +4 -0
  329. package/template/coding-task-manager/public/index.html +473 -243
  330. package/template/coding-task-manager/public/js/agent-command.js +86 -0
  331. package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
  332. package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
  333. package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
  334. package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
  335. package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
  336. package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
  337. package/template/coding-task-manager/public/js/background-work.js +196 -27
  338. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
  339. package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
  340. package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
  341. package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
  342. package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
  343. package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
  344. package/template/coding-task-manager/public/js/document-review-links.js +9 -1
  345. package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
  346. package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
  347. package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
  348. package/template/coding-task-manager/public/js/message-renderer.js +4 -3
  349. package/template/coding-task-manager/public/js/model-picker.js +3 -1
  350. package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
  351. package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
  352. package/template/coding-task-manager/public/js/pasted-content.js +236 -0
  353. package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
  354. package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
  355. package/template/coding-task-manager/public/js/prompts.js +11 -10
  356. package/template/coding-task-manager/public/js/protocol.js +10 -0
  357. package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
  358. package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
  359. package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
  360. package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
  361. package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
  362. package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
  363. package/template/coding-task-manager/public/js/stream-view.js +6 -5
  364. package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
  365. package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
  366. package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
  367. package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
  368. package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
  369. package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
  370. package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
  371. package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
  372. package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
  373. package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
  374. package/template/coding-task-manager/public/js/walle.js +6 -0
  375. package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
  376. package/template/coding-task-manager/public/m/app.css +140 -0
  377. package/template/coding-task-manager/public/m/app.js +507 -68
  378. package/template/coding-task-manager/public/m/index.html +17 -0
  379. package/template/coding-task-manager/public/m/machines.js +838 -4
  380. package/template/coding-task-manager/public/m/private-api.js +31 -1
  381. package/template/coding-task-manager/public/usage.html +1 -0
  382. package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
  383. package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
  384. package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
  385. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
  386. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
  387. package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
  388. package/template/coding-task-manager/server.js +1950 -11679
  389. package/template/coding-task-manager/session-integrity.js +64 -4
  390. package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
  391. package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
  392. package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
  393. package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
  394. package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
  395. package/template/coding-task-manager/workers/session-host-process.js +5 -5
  396. package/template/coding-task-manager/workers/watch-worker.js +105 -0
  397. package/template/coding-task-manager/workers/worktree-worker.js +168 -33
  398. package/template/ctm-relay/lib/protocol.js +14 -1
  399. package/template/package-lock.json +2 -2
  400. package/template/package.json +1 -1
  401. package/template/shared/backup-retention.js +82 -0
  402. package/template/shared/conversation-admission.js +2 -0
  403. package/template/shared/conversation-provenance.js +3 -7
  404. package/template/shared/conversation-storage-v2-contract.js +5 -0
  405. package/template/shared/linked-worktree.js +51 -0
  406. package/template/shared/macos-fda-target.js +57 -0
  407. package/template/shared/orphan-parents.js +55 -0
  408. package/template/shared/release-schema.json +2 -2
  409. package/template/shared/storage/contract.js +3 -0
  410. package/template/wall-e/adapters/imessage.js +12 -0
  411. package/template/wall-e/agent.js +71 -4
  412. package/template/wall-e/api-walle.js +21 -1
  413. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
  414. package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
  415. package/template/wall-e/brain/brain-join.js +859 -0
  416. package/template/wall-e/brain/brain-sync-membership.js +20 -0
  417. package/template/wall-e/brain/brain-sync-store.js +131 -77
  418. package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
  419. package/template/wall-e/brain/brain-sync-v2.js +578 -0
  420. package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
  421. package/template/wall-e/brain/deterministic-id.js +51 -0
  422. package/template/wall-e/brain/memories.js +2 -2
  423. package/template/wall-e/brain/migrations.js +40 -6
  424. package/template/wall-e/brain/model-identity-migration.js +1 -1
  425. package/template/wall-e/brain/owner-call-whitelist.js +23 -7
  426. package/template/wall-e/brain/schema.js +45 -85
  427. package/template/wall-e/brain.js +171 -35
  428. package/template/wall-e/chat.js +3 -3
  429. package/template/wall-e/coding/agent-shell-env.js +7 -2
  430. package/template/wall-e/coding/prompt-bundle.js +66 -21
  431. package/template/wall-e/config/main-loop-exemptions.json +2 -2
  432. package/template/wall-e/http/admin-api.js +37 -0
  433. package/template/wall-e/http/gws-api.js +4 -0
  434. package/template/wall-e/http/helper-rules-api.js +7 -1
  435. package/template/wall-e/http/model-admin.js +8 -1
  436. package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
  437. package/template/wall-e/http/model-proxy-providers.js +174 -1
  438. package/template/wall-e/http/model-route-health.js +157 -0
  439. package/template/wall-e/http/sync-api.js +278 -464
  440. package/template/wall-e/jobs/job-runner.js +28 -1
  441. package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
  442. package/template/wall-e/lib/ctm-data-dir.js +26 -0
  443. package/template/wall-e/lib/derivation-home.js +147 -0
  444. package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
  445. package/template/wall-e/lib/helper-rules/grade.js +51 -8
  446. package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
  447. package/template/wall-e/lib/helper-rules/store.js +4 -2
  448. package/template/wall-e/lib/landing-mcp-tools.js +170 -0
  449. package/template/wall-e/lib/mcp-client-config.js +133 -14
  450. package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
  451. package/template/wall-e/lib/mcp-integration.js +58 -13
  452. package/template/wall-e/lib/parent-death.js +7 -1
  453. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
  454. package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
  455. package/template/wall-e/llm/delegate-vendor-default.js +12 -5
  456. package/template/wall-e/llm/model-roles.js +1 -1
  457. package/template/wall-e/llm/native-delegate-selection.js +56 -8
  458. package/template/wall-e/llm/worker-delegate-args.js +72 -7
  459. package/template/wall-e/llm/worker-mcp-routing.js +66 -17
  460. package/template/wall-e/loops/brain-custom-writes.js +19 -0
  461. package/template/wall-e/loops/ingest.js +17 -1
  462. package/template/wall-e/loops/think.js +19 -4
  463. package/template/wall-e/mcp-contract.js +222 -6
  464. package/template/wall-e/mcp-protocol-contract.js +40 -5
  465. package/template/wall-e/mcp-server.js +128 -29
  466. package/template/wall-e/mcp-worker-schemas.js +5 -5
  467. package/template/wall-e/memory/ctm-context-client.js +85 -0
  468. package/template/wall-e/memory/ctm-session-context.js +9 -3
  469. package/template/wall-e/memory/hub-bridge-client.js +388 -0
  470. package/template/wall-e/memory/replica-origin.js +56 -0
  471. package/template/wall-e/memory/session-pool.js +135 -0
  472. package/template/wall-e/runtime/live-capabilities.js +8 -3
  473. package/template/wall-e/server.js +4 -2
  474. package/template/wall-e/shared/release-schema.json +2 -2
  475. package/template/wall-e/shared/sqlite-file-set.js +39 -0
  476. package/template/wall-e/shared/storage/contract.js +3 -0
  477. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  478. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
  479. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
  480. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
  481. package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
  482. package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
  483. package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
  484. package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
  485. package/template/wall-e/tools/chrome-automation.js +6 -0
  486. package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
  487. package/template/wall-e/workers/brain-owner-worker.js +18 -4
  488. package/template/wall-e/workers/mcp-gateway-process.js +5 -5
@@ -1,5 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ // WP-13 (D-15): the canonical agent resolver, used by `remoteCreateAgentKey` below so a DECLARED
4
+ // `agentType` is classified through the same table server.js's create path resolves the CLI with
5
+ // (aliases such as 'deepseek-harness'/'deepseek_harness'/'claude-code' included). agent-capabilities.js
6
+ // is a leaf module (it requires nothing), so this edge adds no cycle to the lib/** graph.
7
+ const { normalizeAgentType } = require('./agent-capabilities');
8
+
3
9
  // WP-2 (docs/remote-machine-hub-sessions-2026-10-22.html §5 D-6/D-10, §6.3, §9 S-1/S-2/S-26):
4
10
  // hub-side remote create. A `create` frame that names a REGISTERED REMOTE machine is never
5
11
  // prepared locally — it is filtered through the §6.3 allowlist and forwarded to that box over a
@@ -64,8 +70,23 @@ const REMOTE_CREATE_FORWARD_ALLOWLIST = Object.freeze([
64
70
  'chatSessionId',
65
71
  ]);
66
72
 
73
+ // WP-3 (docs/session-create-accept-then-complete-2026-11-01.html §6.3 D-3/D-4): PHASE A — dial to
74
+ // the box's `create-accepted` (or to a final `created`/`error`). Unchanged.
67
75
  const DEFAULT_TIMEOUT_MS = 15000;
68
76
 
77
+ // WP-3 (D-4): PHASE B — `create-accepted` to `created`. The hub must never give up before the box
78
+ // legitimately could, so this ceiling is derived from the BOX's own bounded create phases (97.5 s,
79
+ // §1.1 of the design doc) and is the same 120 s as the box-local create-intent lease
80
+ // (lib/create-intent-lease.js CREATE_INTENT_LEASE_MS) and the push's BACKGROUND_CEILING_MS
81
+ // (lib/agent-bundle/push.js). tests/remote-create-ceiling-covers-box.test.js fails if a box-side
82
+ // bound is raised without raising this one with it.
83
+ const CREATE_ACCEPTED_CEILING_MS = 120000;
84
+
85
+ // WP-3 (D-5): an accepted create that then goes quiet is NOT a failure — the box is still starting
86
+ // the session and the hub never cancels it (D-6). It gets its own code so the client can show a
87
+ // warning and keep the pending create instead of a machine error.
88
+ const MACHINE_CREATE_UNCONFIRMED = 'machine_create_unconfirmed';
89
+
69
90
  // WP-1 (D-2): a missing peer credential is a CONFIGURATION error, not an unreachable box. The box
70
91
  // may be perfectly reachable — the hub simply has nothing to authenticate with, and no retry can
71
92
  // change that (env is read at process start). Every hub→box lane reports this one code, so an
@@ -108,6 +129,21 @@ function _productionDeps() {
108
129
  db: require('../db-owner-client'),
109
130
  registry: require('./machine-registry'),
110
131
  parseSyncPeers: require('./hosted-sync-worker').parseSyncPeers,
132
+ // WP-3 §6.5: the agent-skill push (D-4/D-6). Required lazily like everything else here so a
133
+ // caller that only wants the pure filter never loads the bundle builder.
134
+ pushAgentBundle: (machine, options) => require('./agent-bundle/push').pushBundleToBox(machine, options),
135
+ // The plugins this hub must NOT ship to a box (WP-8 §6.5). The list is per MACHINE and lives in
136
+ // one hub-side JSON file (`<data dir>/agent-bundle-exclude-plugins.json`), read through
137
+ // lib/agent-bundle/skill-transfer.js's in-memory cache — the same reader the Machines row and the
138
+ // PUT route use, so the push and the UI can never disagree about what is excluded. `getDataDir()`
139
+ // is required lazily like everything else here: a caller that only wants the pure filter must not
140
+ // load the data-dir module.
141
+ agentBundleExcludePlugins: async (instanceId) => {
142
+ const skillTransfer = require('./agent-bundle/skill-transfer');
143
+ const dataDir = require('./data-dir').getDataDir();
144
+ await skillTransfer.loadExcludePlugins(dataDir);
145
+ return skillTransfer.excludePluginsFor(dataDir, instanceId);
146
+ },
111
147
  // WP-6 §8: the post-create catalog refresh (lazy: the catalog module requires THIS one).
112
148
  refreshCatalog: (machineId) => {
113
149
  try {
@@ -141,19 +177,29 @@ function filterRemoteCreateForward(msg) {
141
177
  /**
142
178
  * WP-3 (DGX Spark coding readiness §2): which coding agent — if any — a forwarded create is about.
143
179
  * The create path picks the CLI from `msg.agentType || detectAgentType(cmd)` (server.js:34024, whose
144
- * canonical values are exactly 'claude' | 'codex' — lib/agent-capabilities.js:19,45 and
180
+ * canonical values are 'claude' | 'codex' | 'dsh' — lib/agent-capabilities.js and
145
181
  * lib/create/resolve-launch-params.js:692, which also writes the matching bare `cmd` at :772/:824,
146
- * the same shape public/js/reviews.js:858 sends). Only those two can be signed out, so a `null`
147
- * (shell / gemini / walle / anything else) is never refused — the caller checks the sign-in verdict
148
- * for that agent only. Pure and total: any other shape reads `null`.
182
+ * the same shape public/js/reviews.js:858 sends; a dsh create carries `agentType:'dsh'`/`cmd:'dsh'`,
183
+ * public/m/app.js:13868). WP-13 (D-15) adds 'dsh': a forwarded dsh create runs the SAME handleCreate
184
+ * on the box, which resolves its OWN dsh binary, proxy port and route (D-11 box-side), so the hub
185
+ * sends nothing dsh-specific. Only claude/codex can be signed out, so a `null` (shell / gemini /
186
+ * walle / anything else) is never refused — the caller checks the sign-in verdict per agent, and a
187
+ * `'dsh'` verdict is never consulted (there is no CTM sign-in for dsh). Pure and total: any other
188
+ * shape reads `null`.
149
189
  */
190
+ const DSH_AGENT_KEY = 'dsh';
191
+
150
192
  function remoteCreateAgentKey(payload) {
151
- const agentType = _string(payload && payload.agentType, 40).toLowerCase();
152
- if (agentType === 'claude' || agentType === 'codex') return agentType;
193
+ // The DECLARED agentType goes through the canonical normalizer — the same read the box's own
194
+ // create path makes of it — so an alias resolves to the agent the box will actually launch.
195
+ const declared = normalizeAgentType(_string(payload && payload.agentType, 40));
196
+ if (declared === 'claude' || declared === 'codex' || declared === DSH_AGENT_KEY) return declared;
197
+ // The bare `cmd` is matched EXACTLY on its basename: the create path writes that bare token, and a
198
+ // hub must not guess at a path/flag tokenization the box would do differently.
153
199
  const cmd = _string(payload && payload.cmd, 300);
154
200
  if (!cmd) return null;
155
201
  const base = cmd.split(/[\\/]/).pop().toLowerCase();
156
- return base === 'claude' || base === 'codex' ? base : null;
202
+ return base === 'claude' || base === 'codex' || base === DSH_AGENT_KEY ? base : null;
157
203
  }
158
204
 
159
205
  /** The hub's own minted instance id, or '' when identity is not available yet. */
@@ -191,6 +237,42 @@ function _findMachine(machineId, deps) {
191
237
  return machines.find((row) => _string(row.instanceId, 160) === machineId) || null;
192
238
  }
193
239
 
240
+ /**
241
+ * WP-13 (D-15): does the TARGET BOX say it does not have `agentKey`? This is the box's OWN
242
+ * available-agents answer — the same read the New Session card disables the dsh card from — and it
243
+ * is INJECTED (`deps.registry.agentAvailabilityFor(instanceId)`), exactly like the WP-3 sign-in read
244
+ * above and for the same reason: the verdict belongs to the box, and a host/test registry that does
245
+ * not carry the read holds NO verdict.
246
+ *
247
+ * P3 applies to the failure direction: only an explicit "the box does not have it" refuses. A
248
+ * missing provider, a non-object answer, a 404/shape the hub does not recognise, or an entry the
249
+ * block does not carry all read `false` ("no verdict") and the create is dialled exactly as before —
250
+ * the box's own create path is then the authority, and its failure is relayed verbatim.
251
+ *
252
+ * Accepted shapes (the hub must not care which one its registry serves):
253
+ * { agents: { dsh: { available: false, path: null } } } - `/api/models/available-agents`, verbatim
254
+ * { dsh: false } | { dsh: true } | { dsh: { available: false } }
255
+ *
256
+ * @returns {boolean} true only for an explicit "this box does not have it".
257
+ */
258
+ function _boxAgentMissing(d, row, agentKey) {
259
+ const registry = d && d.registry;
260
+ if (!registry || typeof registry.agentAvailabilityFor !== 'function') return false;
261
+ let answer = null;
262
+ try {
263
+ answer = registry.agentAvailabilityFor(_string(row && row.instanceId, 160));
264
+ } catch {
265
+ return false; // REPORT: an unreadable verdict is "no verdict", never a refusal
266
+ }
267
+ if (!answer || typeof answer !== 'object') return false;
268
+ const block = answer.agents && typeof answer.agents === 'object' ? answer.agents : answer;
269
+ const detail = block[agentKey];
270
+ if (detail === undefined || detail === null) return false;
271
+ if (typeof detail === 'boolean') return detail === false;
272
+ if (typeof detail === 'object') return detail.available === false;
273
+ return false;
274
+ }
275
+
194
276
  function _parsePeerTokenMap(raw = '') {
195
277
  const value = _string(raw, 10000);
196
278
  const map = new Map();
@@ -253,13 +335,19 @@ function _ownForwardId(env, deps) {
253
335
  * `machine_forward_loop`. The value is the sender's minted instance id (the same id the
254
336
  * `machine` routing field uses), read through lib/instance-identity-access; it falls back to
255
337
  * the env instance tag so a pre-boot dial still names the sender.
338
+ * D-1 (docs/remote-skills-hub-peer-identity-2026-10-26.html §5 D-1/D-2) the ENV leg's peer id
339
+ * falls back to the sender's minted instance id when the process env names none — a
340
+ * launchd/systemd hub has no shell `HOSTNAME`, and the box keys every peer-scoped route by
341
+ * this header (403 agent_bundle_peer_unidentified otherwise). An env-named id still wins.
256
342
  *
257
343
  * Returns null when NO credential is configured (then the caller reports peer_credential_missing).
258
344
  */
259
345
  function buildPeerControlHeaders(env = process.env, deps = {}) {
260
- const peerId = _string(env.CTM_INSTANCE_ID || env.CTM_INSTANCE_TAG || env.HOSTNAME || '', 160);
261
346
  const forwardedBy = _ownForwardId(env, deps);
262
- const envToken = _envPeerToken(env, peerId);
347
+ // An env-named id wins; else the minted instance id — a launchd/systemd hub has no shell HOSTNAME,
348
+ // and the box keys every peer-scoped route by this header (remote-skills-hub-peer-identity D-1).
349
+ const peerId = _string(env.CTM_INSTANCE_ID || env.CTM_INSTANCE_TAG || env.HOSTNAME || '', 160) || forwardedBy;
350
+ const envToken = _envPeerToken(env, peerId); // D-2: the SAME id for the map lookup
263
351
  const poolToken = _poolToken();
264
352
  // P-8: the per-target decision. A named non-member is dialled with the env bearer so a legacy
265
353
  // box keeps answering; an unnamed target keeps the pool-if-pool behaviour this function had.
@@ -397,35 +485,91 @@ function peerClientInfoFrame(hello) {
397
485
  terminalProtocol: Number((hello && hello.terminalProtocol) || appVersion.terminalProtocol || 0),
398
486
  assetEpoch: String(appVersion.assetEpoch || ''),
399
487
  surface: 'machine-hub',
488
+ // WP-2 (docs/relay-frame-v2-negotiation-box-session-mouse-2026-10-31.html §5 R-4, §6): the hub's
489
+ // peer identity is a hub viewer too, so it negotiates frame protocol v2 on its own behalf and
490
+ // lib/remote-machine-link.js projects each `frame-v2` down per RECEIVING hub socket (R-3).
491
+ terminalCapabilities: { frameContentV2: true },
400
492
  };
401
493
  }
402
494
 
495
+ /**
496
+ * WP-0 (§6.3, D-8): the four dial timings, all measured from the DIAL START and `-1` for a phase
497
+ * that was never reached. WP-3 fills `acceptedMs` in — it is the `create-accepted` frame's offset
498
+ * (D-3) — and it stays `-1` for every dial whose box never accepted, which is exactly the failed
499
+ * phase A the line has to show.
500
+ */
501
+ function _noTimings() {
502
+ return { connectMs: -1, helloMs: -1, acceptedMs: -1, replyMs: -1 };
503
+ }
504
+
403
505
  /**
404
506
  * One short-lived forward: connect, HELLO handshake, send the filtered create, wait for the
405
507
  * box's `created` (or `error`) frame, close. Never throws — the caller turns a failure into the
406
508
  * `machine_unreachable` (or box-error) frame.
407
509
  *
408
- * @returns {Promise<{ok: true, frame: object}|{ok: false, code: string, error: string}>}
510
+ * WP-3 (§6.3 D-3..D-6): the wait is in TWO phases. Phase A (DEFAULT_TIMEOUT_MS, unchanged) runs
511
+ * from the dial to the box's `create-accepted` — or straight through to `created`/`error`, which is
512
+ * what an older box answers. A matching `create-accepted` ends phase A, calls `onAccepted` exactly
513
+ * once and arms phase B (CREATE_ACCEPTED_CEILING_MS). After acceptance, silence, a socket close or a
514
+ * socket error is `machine_create_unconfirmed` — the box owns the create from that moment and the hub
515
+ * never cancels it, so NOTHING is sent to the box on any phase-B exit (D-6).
516
+ *
517
+ * @param {object} options
518
+ * @param {string} options.machineUrl the box's registered url
519
+ * @param {object} options.payload the §6.3-filtered create (its `id` is the acceptance key, D-1)
520
+ * @param {object} options.deps the dial deps (env/registry/authHeaders/WebSocket/timers…)
521
+ * @param {number} [options.acceptedCeilingMs] phase B; also readable as `deps.acceptedCeilingMs`
522
+ * @param {null|function} [options.onAccepted] called once with the accepted frame; also `deps.onAccepted`
523
+ * @returns {Promise<{ok: true, frame: object, accepted: boolean, timings: object}
524
+ * |{ok: false, code: string, error: string, accepted: boolean, timings: object}>}
409
525
  */
410
- function sendRemoteCreate({ machineUrl, payload, deps }) {
526
+ function sendRemoteCreate({ machineUrl, payload, deps, acceptedCeilingMs, onAccepted }) {
411
527
  return new Promise((resolve) => {
412
528
  const dial = preparePeerDial(machineUrl, deps);
413
- if (!dial.ok) return resolve(dial);
529
+ if (!dial.ok) return resolve({ ...dial, accepted: false, timings: _noTimings() });
414
530
  const { url, headers, WebSocketImpl } = dial;
415
531
  const timeoutMs = Number(deps.timeoutMs || DEFAULT_TIMEOUT_MS) || DEFAULT_TIMEOUT_MS;
532
+ // WP-3 (§6.3): phase B's ceiling and the acceptance hook. Both may ride the dial deps instead —
533
+ // the seam every other dial option already uses (`deps.timeoutMs`, `deps.setTimeoutFn`) — so a
534
+ // caller that drives this through forwardRemoteCreate passes neither of them here.
535
+ const ceilingMs = Number(acceptedCeilingMs || (deps && deps.acceptedCeilingMs) || CREATE_ACCEPTED_CEILING_MS) || CREATE_ACCEPTED_CEILING_MS;
536
+ const acceptedHook = typeof onAccepted === 'function'
537
+ ? onAccepted
538
+ : (deps && typeof deps.onAccepted === 'function' ? deps.onAccepted : null);
416
539
  const setTimeoutFn = typeof deps.setTimeoutFn === 'function' ? deps.setTimeoutFn : setTimeout;
417
540
  const clearTimeoutFn = typeof deps.clearTimeoutFn === 'function' ? deps.clearTimeoutFn : clearTimeout;
418
541
 
542
+ // WP-0 (§6.3): the dial clock. `dialStartedAt` is the dial's own origin and every timing below
543
+ // is an offset from it; a phase that never happened stays -1 (see _noTimings).
544
+ const dialStartedAt = Date.now();
545
+ let openedAt = -1;
546
+ let helloAt = -1;
547
+ let acceptedAt = -1;
548
+ let replyAt = -1;
549
+ const _since = (at) => (at < 0 ? -1 : at - dialStartedAt);
550
+ const timings = () => ({
551
+ connectMs: _since(openedAt),
552
+ helloMs: _since(helloAt),
553
+ acceptedMs: _since(acceptedAt), // WP-3 (D-3): the create-accepted offset, -1 when never accepted
554
+ replyMs: _since(replyAt),
555
+ });
556
+
557
+ // D-3/D-1: the create idempotency key this dial is for. A `create-accepted` naming any OTHER id
558
+ // is not this create's acceptance (one box WS endpoint serves many clients) and must not end
559
+ // phase A.
560
+ const createId = _string(payload && payload.id, 200);
561
+
419
562
  let socket = null;
420
563
  let timer = null;
421
564
  let settled = false;
565
+ let accepted = false;
422
566
  let sent = false;
423
567
  const finish = (result) => {
424
568
  if (settled) return;
425
569
  settled = true;
426
570
  if (timer) clearTimeoutFn(timer);
427
571
  try { if (socket) socket.close(); } catch { /* expected: close on an already-dead socket */ }
428
- resolve(result);
572
+ resolve({ ...result, accepted, timings: timings() });
429
573
  };
430
574
  // Only ever after client-info (see the dial-order note above): a create sent earlier is
431
575
  // dropped by the box's protocol gate and the forward would hang until the timeout.
@@ -442,9 +586,25 @@ function sendRemoteCreate({ machineUrl, payload, deps }) {
442
586
  try {
443
587
  socket = new WebSocketImpl(url, { headers });
444
588
  } catch (error) {
445
- return resolve({ ok: false, code: 'machine_unreachable', error: `connect_failed:${error?.message || error}` });
589
+ return resolve({ ok: false, code: 'machine_unreachable', error: `connect_failed:${error?.message || error}`, accepted: false, timings: _noTimings() });
446
590
  }
447
591
 
592
+ // WP-0: the connect phase is the socket's own 'open'. Nothing is SENT here (see the dial-order
593
+ // note below) — the listener exists only to stamp connectMs, and an implementation that never
594
+ // fires it leaves the phase at -1 rather than inventing a number.
595
+ socket.on('open', () => { if (openedAt < 0) openedAt = Date.now(); });
596
+
597
+ // WP-3 (D-4/D-6): phase B. Its expiry ends the WAIT only — the box already owns the create and
598
+ // runs it to completion, so nothing is sent to the box here (D-6).
599
+ const armPhaseB = () => {
600
+ timer = setTimeoutFn(() => {
601
+ try { socket.terminate(); } catch { /* expected: the socket is already gone */ }
602
+ finish({ ok: false, code: MACHINE_CREATE_UNCONFIRMED, error: 'accepted_then_timeout' });
603
+ }, ceilingMs);
604
+ if (timer && typeof timer.unref === 'function') timer.unref();
605
+ };
606
+
607
+ // Phase A (D-3): unchanged 15 s from the dial to the box's acceptance or its final reply.
448
608
  timer = setTimeoutFn(() => {
449
609
  try { socket.terminate(); } catch { /* expected: the socket is already gone */ }
450
610
  finish({ ok: false, code: 'machine_unreachable', error: 'timeout' });
@@ -453,6 +613,9 @@ function sendRemoteCreate({ machineUrl, payload, deps }) {
453
613
 
454
614
  // No create on 'open': the box has not said hello yet, so its protocol gate is not satisfied.
455
615
  socket.on('message', (raw) => {
616
+ // D-6: once this dial is settled nothing may cross to the box again — a frame arriving after
617
+ // the phase-B timeout (a late hello, the box's eventual `created`) is dropped, never answered.
618
+ if (settled) return;
456
619
  let frame = null;
457
620
  try {
458
621
  frame = JSON.parse(raw.toString());
@@ -460,21 +623,52 @@ function sendRemoteCreate({ machineUrl, payload, deps }) {
460
623
  return; // expected: a non-JSON frame (the box's binary/other lanes) is not the reply
461
624
  }
462
625
  if (!frame || typeof frame !== 'object') return;
626
+ // WP-0: a REPLY is any frame that settles this dial (created / box error / the box's protocol
627
+ // or auth verdict on the create). Timeout, socket error and close-before-reply are NOT replies,
628
+ // so replyMs stays -1 there.
629
+ const markReply = () => { if (replyAt < 0) replyAt = Date.now(); };
630
+ if (frame.type === 'create-accepted') {
631
+ // WP-3 (D-3): the box admitted the create and is starting it. Only THIS create's id counts
632
+ // (any other id belongs to another client on the same box endpoint) and only the first one.
633
+ // Phase A is cleared, the caller is told once, and phase B starts.
634
+ if (!createId || _string(frame.id, 200) !== createId) return;
635
+ if (accepted) return;
636
+ accepted = true;
637
+ acceptedAt = Date.now();
638
+ if (timer) clearTimeoutFn(timer);
639
+ timer = null;
640
+ if (acceptedHook) {
641
+ try {
642
+ acceptedHook(frame);
643
+ } catch (error) {
644
+ // REPORT: the caller's own bookkeeping (mirror registration + browser relay) must never
645
+ // break a live dial — the box is starting the session either way (D-6).
646
+ console.warn(`[remote-create] onAccepted failed: ${_string(error && error.message, 200) || 'unknown error'}`);
647
+ }
648
+ }
649
+ armPhaseB();
650
+ return;
651
+ }
463
652
  if (frame.type === 'hello') {
464
653
  // The box gates every terminal-protocol message (including 'create') behind a compatible
465
654
  // client-info, so the hub identifies itself with the epoch the box just advertised — the
466
655
  // same handshake a browser does. Both frames go out in this order on this one socket.
656
+ if (helloAt < 0) helloAt = Date.now();
467
657
  try {
468
658
  socket.send(JSON.stringify(peerClientInfoFrame(frame)));
469
659
  } catch { /* expected: the socket died between hello and client-info */ }
470
660
  sendCreate();
471
661
  return;
472
662
  }
473
- if (frame.type === 'created') return finish({ ok: true, frame });
663
+ if (frame.type === 'created') {
664
+ markReply();
665
+ return finish({ ok: true, frame });
666
+ }
474
667
  if (frame.type === 'terminal-protocol-incompatible') {
475
668
  // The box refused the frame on protocol grounds. Fail now with the reason instead of
476
669
  // waiting out the timeout for a reply that will never come (this is the frame that made a
477
670
  // mis-ordered dial look like an unreachable box).
671
+ markReply();
478
672
  return finish({
479
673
  ok: false,
480
674
  code: 'machine_unreachable',
@@ -492,11 +686,13 @@ function sendRemoteCreate({ machineUrl, payload, deps }) {
492
686
  // non-fatal — treating every auth-error as fatal would break every forward.
493
687
  if (_string(frame.messageType, 40) === 'create') {
494
688
  const code = _string(frame.code, 80) || 'machine_unreachable';
689
+ markReply();
495
690
  return finish({ ok: false, code, error: `box_refused_create:${code}` });
496
691
  }
497
692
  return;
498
693
  }
499
694
  if (frame.type === 'error') {
695
+ markReply();
500
696
  return finish({
501
697
  ok: false,
502
698
  code: _string(frame.code, 80) || 'machine_unreachable',
@@ -507,14 +703,183 @@ function sendRemoteCreate({ machineUrl, payload, deps }) {
507
703
  // handshake or the box's ambient traffic on this lane and is not a reply to the create.
508
704
  });
509
705
  socket.on('error', (error) => {
706
+ // D-5: an error AFTER acceptance is not "unreachable" — the box already claimed the create and
707
+ // is still starting it, so the hub reports it as unconfirmed and keeps the session.
708
+ if (accepted) return finish({ ok: false, code: MACHINE_CREATE_UNCONFIRMED, error: 'accepted_then_socket_error' });
510
709
  finish({ ok: false, code: 'machine_unreachable', error: _string(error?.message || error, 300) });
511
710
  });
512
711
  socket.on('close', () => {
712
+ if (accepted) return finish({ ok: false, code: MACHINE_CREATE_UNCONFIRMED, error: 'accepted_then_closed' });
513
713
  finish({ ok: false, code: 'machine_unreachable', error: 'closed_before_reply' });
514
714
  });
515
715
  });
516
716
  }
517
717
 
718
+ // ---------------------------------------------------------------------------------------------
719
+ // WP-3 (docs/remote-agent-skills-bundles-2026-10-25.html §5 D-4/D-6, §6.5) — the hub's agent-skill
720
+ // push at forwarded create.
721
+ //
722
+ // The push happens BEFORE the dial and can never change its outcome (D-6): the box's container gets
723
+ // the hub's skills and plugins, and a hub that cannot deliver them still forwards the create exactly
724
+ // as it did before this work package existed.
725
+ // ---------------------------------------------------------------------------------------------
726
+
727
+ /** D-9: the box os values a bundle can be built for; anything else means "do not guess". */
728
+ const AGENT_BUNDLE_PLATFORMS = Object.freeze(['darwin', 'linux', 'win32']);
729
+
730
+ /** D-6/D-9: the create-time push failures this lane names itself (push.js owns the transport ones). */
731
+ const BUNDLE_PLATFORM_UNKNOWN = 'agent_bundle_platform_unknown';
732
+ const BUNDLE_NO_CREDENTIAL = 'agent_bundle_push_no_credential';
733
+ const BUNDLE_PUSH_NETWORK = 'agent_bundle_push_network';
734
+
735
+ /**
736
+ * D-9: the platform to build for = the box's `os` from the registry's cached manifest host block.
737
+ * Unknown, unreadable or unlisted reads null — the caller then SKIPS the push rather than guessing a
738
+ * platform (a wrong guess would ship the box a skill its own CLI cannot load).
739
+ */
740
+ function agentBundlePlatformFor(machineId, deps = {}) {
741
+ const d = _deps(deps);
742
+ const registry = d.registry;
743
+ if (!registry || typeof registry.manifestHostFor !== 'function') return null;
744
+ let host = null;
745
+ try {
746
+ host = registry.manifestHostFor(_string(machineId, 160));
747
+ } catch {
748
+ return null; // expected: a registry read fault means "unknown platform", never a failed create
749
+ }
750
+ const platform = _string(host && host.os, 40).toLowerCase();
751
+ return AGENT_BUNDLE_PLATFORMS.includes(platform) ? platform : null;
752
+ }
753
+
754
+ /**
755
+ * The headers the push is authenticated with: exactly the choice `preparePeerDial` makes for the
756
+ * dial of the same target (an injected `authHeaders` builder wins; otherwise
757
+ * `buildPeerControlHeaders(env, {instanceId})`), so the push and the dial can never drift onto
758
+ * different credentials. `null` means no credential is configured — a configuration error the caller
759
+ * reports as `agent_bundle_push_no_credential` (the dial reports its own `peer_credential_missing`).
760
+ */
761
+ function bundlePeerHeadersFor(deps = {}, instanceId) {
762
+ const d = _deps(deps);
763
+ if (typeof d.authHeaders === 'function') {
764
+ try {
765
+ const headers = d.authHeaders();
766
+ return headers && typeof headers === 'object' ? headers : null;
767
+ } catch {
768
+ return null; // expected: a credential-builder fault is a missing credential, not a throw
769
+ }
770
+ }
771
+ return buildPeerControlHeaders(d.env || process.env, { instanceId: _string(instanceId, 160) });
772
+ }
773
+
774
+ /** The sync bookkeeping every outcome shares: the machine row's `skills` block, then the diagnostic. */
775
+ function _recordPushOutcome(d, instanceId, result, payload, platform) {
776
+ const registry = d.registry;
777
+ if (registry && typeof registry.recordAgentBundlePush === 'function') {
778
+ try {
779
+ registry.recordAgentBundlePush(instanceId, result);
780
+ } catch { /* expected: a registry write fault must never fail a create (D-6) */ }
781
+ }
782
+ if (result.ok !== true && typeof d.recordSessionDiagnostic === 'function') {
783
+ try {
784
+ const details = { code: _string(result.code, 80), machine: instanceId, platform: platform || '' };
785
+ // WP-6 D-13: the push now says WHICH phase cost the wait and, when the push ran on without the
786
+ // caller (D-11), that it is still going. Both are present only when the push reported them.
787
+ const phase = _string(result.phase, 40);
788
+ if (phase) details.phase = phase;
789
+ if (result.continuing === true) details.continuing = true;
790
+ if (result.timings && typeof result.timings === 'object') details.timings = result.timings;
791
+ // The session id here is the FORWARDED create's id (the hub has no local session row for it —
792
+ // OQ-4); when the client supplied none, the machine id is the correlation we do have.
793
+ d.recordSessionDiagnostic(_string(payload && payload.id, 200) || instanceId, 'agent-bundle-push-failed', details);
794
+ } catch { /* expected: diagnostic bookkeeping must never fail a create (D-6) */ }
795
+ }
796
+ return result;
797
+ }
798
+
799
+ /**
800
+ * Push this hub's bundle for `row` before the dial, or `null` when there is nothing to push.
801
+ *
802
+ * SYNCHRONOUS DECISION, async only when a push actually runs: a shell create, an unknown platform and
803
+ * a missing credential all resolve here without yielding to the event loop, so the create's critical
804
+ * path has no avoidable interleaving point (and a caller driving this seam in a test can still rely on
805
+ * "nothing has been dialled yet" being observable synchronously).
806
+ *
807
+ * Always resolves (never throws) on the async path: a failure is recorded on the machine row and
808
+ * emitted as the `agent-bundle-push-failed` session diagnostic, and the caller forwards the create
809
+ * regardless (D-6). Only claude/codex creates push (`agentKey`), and `excludePlugins` comes from a
810
+ * dep so WP-8 can wire the per-machine list without touching this call site.
811
+ *
812
+ * @returns {null|Promise<object>} the push result, or null when nothing was pushed
813
+ */
814
+ function pushAgentBundleForCreate(d, row, payload, agentKey) {
815
+ if (!agentKey) return null; // a plain shell create runs no coding CLI, so it ships no skills
816
+ const instanceId = _string(row && row.instanceId, 160);
817
+ const platform = agentBundlePlatformFor(instanceId, d);
818
+ const headers = platform ? bundlePeerHeadersFor(d, instanceId) : null;
819
+ if (!platform) {
820
+ // D-9: guessing a platform would ship the box a skill its own CLI cannot load. Report the skip.
821
+ _recordPushOutcome(d, instanceId, {
822
+ ok: false,
823
+ code: BUNDLE_PLATFORM_UNKNOWN,
824
+ detail: 'the machine has never reported a platform this hub can build a bundle for',
825
+ }, payload, '');
826
+ return null;
827
+ }
828
+ if (!headers) {
829
+ _recordPushOutcome(d, instanceId, {
830
+ ok: false,
831
+ code: BUNDLE_NO_CREDENTIAL,
832
+ detail: 'no peer credential is configured for this machine',
833
+ }, payload, platform);
834
+ return null;
835
+ }
836
+ const machine = { instanceId, url: _string(row.url, 1000), displayName: _string(row.displayName, 200) };
837
+ return (async () => {
838
+ // WP-8: the production exclusion dep loads its store with async fs (never a sync read on the
839
+ // create's main thread) and so answers a promise; a dep that answers a plain list is used as-is,
840
+ // without a yield, so the push still starts in the same synchronous turn as before.
841
+ let excludePlugins = [];
842
+ if (typeof d.agentBundleExcludePlugins === 'function') {
843
+ try {
844
+ const answer = d.agentBundleExcludePlugins(instanceId);
845
+ excludePlugins = (answer && typeof answer.then === 'function' ? await answer : answer) || [];
846
+ } catch {
847
+ excludePlugins = []; // expected: an unreadable exclude list ships every plugin, never nothing
848
+ }
849
+ }
850
+ const options = {
851
+ fetchImpl: d.fetchImpl,
852
+ peerHeaders: headers,
853
+ targetPlatform: platform,
854
+ excludePlugins,
855
+ // WP-6 D-11: the push outlives this create's wait. When it settles late — after the create has
856
+ // already been forwarded — its result still has to reach the row and the diagnostic, exactly
857
+ // once (push.js calls this only for a call that timed out AND started the push). The create
858
+ // itself is NEVER held for it: `_recordPushOutcome` cannot reject and the frame goes out as
859
+ // soon as the budgeted call resolves.
860
+ onLateResult: (late) => _recordPushOutcome(
861
+ d,
862
+ instanceId,
863
+ (late && typeof late === 'object') ? late : { ok: false, code: BUNDLE_PUSH_NETWORK, detail: 'the push returned no result' },
864
+ payload,
865
+ platform,
866
+ ),
867
+ };
868
+ let result;
869
+ try {
870
+ result = await d.pushAgentBundle(machine, options);
871
+ } catch (error) {
872
+ // REPORT: push.js resolves its own failures, so a THROW here is a bug in the seam — it must
873
+ // still not touch the create.
874
+ result = { ok: false, code: BUNDLE_PUSH_NETWORK, detail: _string(error && error.message, 200) || 'push failed' };
875
+ }
876
+ if (!result || typeof result !== 'object') {
877
+ result = { ok: false, code: BUNDLE_PUSH_NETWORK, detail: 'the push returned no result' };
878
+ }
879
+ return _recordPushOutcome(d, instanceId, result, payload, platform);
880
+ })();
881
+ }
882
+
518
883
  function _sendFrame(ctx, frame) {
519
884
  try {
520
885
  if (ctx && ctx.ws && ctx.ws.readyState === 1 && typeof ctx.send === 'function') {
@@ -526,81 +891,242 @@ function _sendFrame(ctx, frame) {
526
891
  }
527
892
  }
528
893
 
529
- function _fail(ctx, code, detail) {
530
- _sendFrame(ctx, {
894
+ /**
895
+ * Refuse a forwarded create on the requesting browser socket (one frame, one result).
896
+ *
897
+ * WP-0 (§6.5): `machine`/`machineName` ride every refusal so it is VISIBLE. Both clients gate the
898
+ * machine-error toast on `/^machine_/.test(code) || !!msg.machine` (public/index.html:4467,
899
+ * public/m/app.js:5638), so a hub code such as `peer_credential_missing`, or any box code relayed
900
+ * from a forwarded create (`rate_limited`, `create_in_progress`, …), was console-only before this.
901
+ * `machine` is the requested instance id as a STRING — the desktop dedupes its toast key on
902
+ * `String(msg.machine)`. Same shape as `forwardLifecycleRequest`'s 503 body
903
+ * (lib/remote-machine-catalog.js). `code`/`message` are untouched.
904
+ *
905
+ * WP-3 (§6.3 S-3): the optional 6th `id` is the create's own idempotency key (D-1). It is added
906
+ * AFTER the machine fields and only when non-empty, so the client can match the refusal to the
907
+ * pending create it is holding (D-7) and an id-less caller's frame keeps its exact key set.
908
+ */
909
+ function _fail(ctx, code, detail, machineId, machineName, id = '') {
910
+ const frame = {
531
911
  type: 'error',
532
912
  code: code || 'machine_unreachable',
533
913
  message: detail || 'The machine could not be reached.',
534
- });
914
+ machine: _string(machineId, 200),
915
+ machineName: _string(machineName, 200),
916
+ };
917
+ const refusalId = _string(id, 200);
918
+ if (refusalId) frame.id = refusalId;
919
+ _sendFrame(ctx, frame);
535
920
  return { ok: false, code: code || 'machine_unreachable', error: detail || '' };
536
921
  }
537
922
 
923
+ /**
924
+ * WP-0 (D-8, §6.3): the hub's ONE line per forwarded create. Keys are a FIXED set in a fixed order,
925
+ * and `-` is the "no value" token (`<n|-1>` for the two durations, where `-1` means the push/dial
926
+ * phase was never reached). The line is built for humans and greps, never parsed by code — the
927
+ * machine-readable form of the same facts is the frame's own `timings` and the `_fail` code.
928
+ *
929
+ * @param {{id: string, machine: string, agent: string,
930
+ * outcome: 'created'|'failed'|'unconfirmed'|'refused',
931
+ * code: string, pushMs: number, pushCode: string, timings: object, totalMs: number}} f
932
+ */
933
+ function formatRemoteCreateLine(f) {
934
+ const fields = f && typeof f === 'object' ? f : {};
935
+ const t = fields.timings && typeof fields.timings === 'object' ? fields.timings : _noTimings();
936
+ const ms = (value) => {
937
+ const n = Number(value);
938
+ return Number.isFinite(n) && n >= 0 ? Math.round(n) : -1;
939
+ };
940
+ const token = (value, max) => _string(value, max) || '-';
941
+ return `[remote-create] id=${_string(fields.id, 200).slice(0, 8) || '-'}`
942
+ + ` machine=${token(fields.machine, 160)} agent=${token(fields.agent, 40)}`
943
+ + ` outcome=${token(fields.outcome, 40)} code=${token(fields.code, 80)}`
944
+ + ` pushMs=${ms(fields.pushMs)} pushCode=${token(fields.pushCode, 80)}`
945
+ + ` connectMs=${ms(t.connectMs)} helloMs=${ms(t.helloMs)} acceptedMs=${ms(t.acceptedMs)} replyMs=${ms(t.replyMs)}`
946
+ + ` totalMs=${ms(fields.totalMs)}`;
947
+ }
948
+
538
949
  /**
539
950
  * Forward an already-classified remote create (S-1). On success the box's own `created` frame is
540
951
  * re-emitted to the requesting browser socket with `machine`/`machineName` added (AC-2a) and the
541
952
  * mirror registration is recorded; on failure the caller's socket gets exactly one typed `error`
542
953
  * frame and NOTHING is created locally (AC-2c).
954
+ *
955
+ * WP-0 (D-8): every exit writes exactly ONE `[remote-create]` line (see formatRemoteCreateLine).
956
+ * `outcome` is `created` on the box's `created`, `refused` for the hub-side refusals taken BEFORE the
957
+ * dial (loop guard, the machine-row verdicts, the dsh/sign-in verdicts, a missing peer credential),
958
+ * `failed` for everything after the dial starts (dial failures, timeouts, box error frames) and
959
+ * `unconfirmed` for an ACCEPTED create the hub stopped waiting for (WP-3 D-5 — the box still has it).
543
960
  */
544
961
  async function forwardRemoteCreate(ctx, msg, deps = {}) {
545
962
  const d = _deps(deps);
963
+ const startedAt = Date.now();
546
964
  const machineId = _string(msg && msg.machine, 160);
965
+ // WP-3 (§6.3 S-3): the create's own idempotency key as the client minted it (D-1). It rides every
966
+ // refusal frame (`_fail`'s 6th argument) so the client can match a failure to its pending create.
967
+ const createId = _string(msg && msg.id, 200);
968
+ // WP-0 bookkeeping, shared by every exit below. `_report` is the ONLY writer of the line, so a new
969
+ // return path cannot silently skip it: route the return through `_refuse`/`_report`.
970
+ let logMachine = machineId;
971
+ let logId = createId;
972
+ let pushMs = -1;
973
+ let pushCode = '-';
974
+ let dialTimings = null;
975
+ const _report = (result, outcome) => {
976
+ console.log(formatRemoteCreateLine({
977
+ id: logId,
978
+ machine: logMachine,
979
+ agent: remoteCreateAgentKey(msg) || '-',
980
+ outcome,
981
+ code: _string(result && result.code, 80),
982
+ pushMs,
983
+ pushCode,
984
+ timings: dialTimings,
985
+ totalMs: Date.now() - startedAt,
986
+ }));
987
+ return result;
988
+ };
989
+ /** A hub-side refusal taken before any dial — never counted as a dial failure. */
990
+ const _refuse = (result) => _report(result, 'refused');
547
991
  // P-3 one-hop guard. This function is reached ONLY when the create names a machine that is not
548
992
  // this instance (lib/ws/terminal-attach.js checks isRemoteMachineTarget first), so a request that
549
993
  // already carries the hop header is by definition a request to forward a forwarded create — the
550
994
  // A→B→C chain (and the A→B→A loop) P-3 forbids. Nothing is dialled and nothing is created.
551
995
  const forwardedBy = forwardedByOf(ctx);
552
996
  if (forwardedBy) {
553
- return _fail(ctx, MACHINE_FORWARD_LOOP, `This create was already forwarded by ${forwardedBy}; a forwarded request is never forwarded again`);
997
+ return _refuse(_fail(ctx, MACHINE_FORWARD_LOOP, `This create was already forwarded by ${forwardedBy}; a forwarded request is never forwarded again`, machineId, '', createId));
554
998
  }
555
999
  const row = _findMachine(machineId, d);
556
- if (!row) return _fail(ctx, 'machine_unreachable', `machine ${machineId} is not registered on this hub`);
557
- if (!row.enabled) return _fail(ctx, 'machine_unreachable', `machine ${machineId} is disabled`);
558
- if (!_string(row.url, 1000)) return _fail(ctx, 'machine_unreachable', `machine ${machineId} has no url`);
1000
+ if (!row) return _refuse(_fail(ctx, 'machine_unreachable', `machine ${machineId} is not registered on this hub`, machineId, '', createId));
1001
+ // The row is authoritative for the line's `machine` once it exists (mirrors `_fail`'s frame field).
1002
+ logMachine = _string(row.instanceId, 160) || logMachine;
1003
+ if (!row.enabled) return _refuse(_fail(ctx, 'machine_unreachable', `machine ${machineId} is disabled`, machineId, row.displayName, createId));
1004
+ if (!_string(row.url, 1000)) return _refuse(_fail(ctx, 'machine_unreachable', `machine ${machineId} has no url`, machineId, row.displayName, createId));
559
1005
  if (_string(row.health, 40) === 'unreachable') {
560
- return _fail(ctx, 'machine_unreachable', `machine ${machineId} is unreachable`);
1006
+ return _refuse(_fail(ctx, 'machine_unreachable', `machine ${machineId} is unreachable`, machineId, row.displayName, createId));
561
1007
  }
562
1008
 
563
1009
  const payload = filterRemoteCreateForward(msg);
1010
+ // WP-13 (D-15): the WP-2 `dsh_remote_pending` refusal is GONE — a forwarded dsh create runs the
1011
+ // same handleCreate on the box, which resolves its own dsh binary, proxy port and route. The ONE
1012
+ // remaining dsh gate is the box's own installation verdict below: CTM never installs dsh on a box
1013
+ // (843 MB), so a hub that holds "this box has no dsh" refuses rather than dialling into a box-side
1014
+ // PATH failure. A box with no verdict at all is dialled exactly like any other agent (P3).
1015
+ const agentKey = remoteCreateAgentKey(payload);
1016
+ if (agentKey === DSH_AGENT_KEY) {
1017
+ const notInstalled = _boxAgentMissing(d, row, DSH_AGENT_KEY);
1018
+ if (notInstalled) {
1019
+ return _refuse(_fail(ctx, 'machine_agent_not_installed', `DeepSeek Harness is not installed on ${_string(row.displayName, 200) || row.instanceId}`, machineId, row.displayName, createId));
1020
+ }
1021
+ }
564
1022
  // WP-3: the box judges a create only by its manifest probe, so a box that is REACHABLE but has no
565
1023
  // signed-in Claude/Codex CLI would happily spawn a session that opens at a login prompt. Refuse at
566
1024
  // the hub instead, naming the box and the URL that fixes it. 'unknown' (never probed, unreachable
567
1025
  // probe, unreadable body) and 'signed_in' both proceed: only an explicit `signedIn:false` refuses.
568
- const agentKey = remoteCreateAgentKey(payload);
569
- const auth = agentKey && d.registry && typeof d.registry.agentAuthFor === 'function'
1026
+ // 'dsh' is skipped by construction: it has no CTM sign-in (S-88), so no verdict is consulted —
1027
+ // `agentAuthFor`'s map is `{claude, codex}` and reading `auth['dsh']` would be meaningless.
1028
+ const auth = agentKey && agentKey !== DSH_AGENT_KEY
1029
+ && d.registry && typeof d.registry.agentAuthFor === 'function'
570
1030
  ? d.registry.agentAuthFor(row.instanceId)
571
1031
  : null;
572
1032
  if (auth && auth[agentKey] === 'signed_out') {
573
1033
  const label = agentKey === 'codex' ? 'Codex' : 'Claude Code';
574
- return _fail(ctx, 'machine_agent_signed_out', `${label} is not signed in on ${_string(row.displayName, 200) || row.instanceId} — open ${row.url} → AI Providers → Coding Agents → Sign in`);
1034
+ return _refuse(_fail(ctx, 'machine_agent_signed_out', `${label} is not signed in on ${_string(row.displayName, 200) || row.instanceId} — open ${row.url} → AI Providers → Coding Agents → Sign in`, machineId, row.displayName, createId));
1035
+ }
1036
+ // WP-3 (D-4/D-6, §6.5): hand the box this hub's skills/plugins BEFORE the dial. A failure here is
1037
+ // recorded on the machine row and emitted as a diagnostic, and the forward below runs either way —
1038
+ // skills are a convenience, and NOTHING about this call may change the create's outcome (AC-3c:
1039
+ // the forwarded frame is untouched by construction; the push never touches `payload`). The decision
1040
+ // is synchronous, so a create with nothing to push (a shell create, an unknown platform, no
1041
+ // credential) reaches the dial without yielding.
1042
+ const bundlePush = pushAgentBundleForCreate(d, row, payload, agentKey);
1043
+ if (bundlePush) {
1044
+ // D-9/OQ-3: the push wait (≤10 s) used to be invisible; it is now pushMs/pushCode on the line.
1045
+ // pushMs prefers push.js's own D-13 `timings.totalMs` (the push's account of itself) and falls
1046
+ // back to the caller's measured wait, so a push that RAN is never reported as `-1`.
1047
+ const pushStartedAt = Date.now();
1048
+ const pushResult = await bundlePush;
1049
+ pushMs = _pushMs(pushResult, Date.now() - pushStartedAt);
1050
+ pushCode = pushResult && pushResult.ok === true ? 'ok' : _string(pushResult && pushResult.code, 80) || '-';
575
1051
  }
576
1052
  // The machine's DISPLAY name rides the dial deps so a credential-less refusal can name the box
577
1053
  // the operator has to configure (D-2), not just "the machine".
578
1054
  const result = await sendRemoteCreate({
579
1055
  machineUrl: row.url,
580
1056
  payload,
1057
+ // WP-3 (§6.3 D-3): the moment the box says it ADMITTED the create (phase A ends here, D-4 arms
1058
+ // phase B). The mirror registration is recorded and the frame is relayed to the browser with the
1059
+ // same machine/machineName shape as the `created` relay below — so the client's pending create
1060
+ // turns into "starting on ‹machine›" instead of waiting out the whole box-side create.
1061
+ onAccepted: (frame) => {
1062
+ recordMirrorSession(frame.id, row);
1063
+ _sendFrame(ctx, {
1064
+ ...frame,
1065
+ machine: _string(row.instanceId, 160),
1066
+ machineName: _string(row.displayName, 200),
1067
+ });
1068
+ },
581
1069
  // P-8: the TARGET's instanceId rides the dial deps so the credential is chosen per machine —
582
1070
  // a legacy env-token box that never joined the pool is dialled with the env bearer, not with a
583
1071
  // pool secret it would 401.
584
1072
  deps: { ...d, machineName: _string(row.displayName, 200), instanceId: _string(row.instanceId, 160) },
585
1073
  });
586
- if (!result.ok) return _fail(ctx, result.code, result.error);
1074
+ dialTimings = result.timings || null;
1075
+ // The box's own refusal (`rate_limited`, `create_in_progress`, `session_exists`, …) and every
1076
+ // local dial failure reach the browser HERE: `_fail` re-emits `code`/`message` verbatim and adds
1077
+ // the machine fields (WP-0 §6.5), so neither a hub code nor a relayed box code is console-only.
1078
+ // A missing peer credential is the one dial-path code that is NOT a dial failure: nothing was
1079
+ // dialled, so it reads as a hub refusal (D-2) exactly like the refusals above.
1080
+ if (!result.ok) {
1081
+ const failure = _fail(ctx, result.code, result.error, machineId, row.displayName, createId);
1082
+ if (result.code === PEER_CREDENTIAL_MISSING) return _refuse(failure);
1083
+ // WP-3 (D-5): the box ACCEPTED this create (hence the code) and then went quiet — a warning, not
1084
+ // a failure. The box runs the create to completion (D-6), so the catalog refresh runs here too,
1085
+ // exactly as it does on the success path: the session must appear the moment the box commits it.
1086
+ if (result.code === MACHINE_CREATE_UNCONFIRMED) {
1087
+ _refreshCatalog(d, row.instanceId);
1088
+ return _report(failure, 'unconfirmed');
1089
+ }
1090
+ return _report(failure, 'failed');
1091
+ }
587
1092
 
588
1093
  const frame = result.frame || {};
589
1094
  const sessionId = _string(frame.id || payload.id, 200);
590
- if (sessionId) recordMirrorSession(sessionId, row);
1095
+ if (sessionId) {
1096
+ logId = sessionId;
1097
+ recordMirrorSession(sessionId, row);
1098
+ }
591
1099
  _sendFrame(ctx, {
592
1100
  ...frame,
593
1101
  machine: _string(row.instanceId, 160),
594
1102
  machineName: _string(row.displayName, 200),
595
1103
  });
596
- // WP-6 §8: "right after a forwarded create" is a catalog refresh trigger — the new session must
597
- // appear in the hub's merged list at once, not on the next ~15 s probe sweep. Fire-and-forget:
598
- // the in-memory registration above already makes the relay work, so a catalog fault is a stale
599
- // list, never a failed create.
600
- if (typeof d.refreshCatalog === 'function') {
601
- try { d.refreshCatalog(_string(row.instanceId, 160)); } catch { /* expected: a catalog fault never fails a create */ }
602
- }
603
- return { ok: true, sessionId, machineInstanceId: _string(row.instanceId, 160) };
1104
+ _refreshCatalog(d, row.instanceId);
1105
+ return _report({ ok: true, sessionId, machineInstanceId: _string(row.instanceId, 160) }, 'created');
1106
+ }
1107
+
1108
+ /**
1109
+ * WP-6 §8 / WP-3 (D-5): the fire-and-forget catalog refresh. "right after a forwarded create" is a
1110
+ * catalog refresh trigger — the new session must appear in the hub's merged list at once, not on the
1111
+ * next ~15 s probe sweep. Fire-and-forget: the in-memory mirror registration already makes the relay
1112
+ * work, so a catalog fault is a stale list, never a failed create. Both the `created` outcome and the
1113
+ * `unconfirmed` one (D-5) take it — the box owns the create in either case.
1114
+ */
1115
+ function _refreshCatalog(d, instanceId) {
1116
+ if (typeof d.refreshCatalog !== 'function') return;
1117
+ try { d.refreshCatalog(_string(instanceId, 160)); } catch { /* expected: a catalog fault never fails a create */ }
1118
+ }
1119
+
1120
+ /**
1121
+ * WP-0 (D-9): the push's own account of how long it cost (D-13 `timings.totalMs`) when it has one,
1122
+ * else the caller's measured wait. Never a negative number: a push that RAN is `>= 0`, and only a
1123
+ * push that never ran is `-1` (which the call site simply does not call this for).
1124
+ */
1125
+ function _pushMs(result, measuredMs) {
1126
+ const total = Number(result && result.timings && result.timings.totalMs);
1127
+ if (Number.isFinite(total) && total >= 0) return Math.round(total);
1128
+ const measured = Number(measuredMs);
1129
+ return Number.isFinite(measured) && measured >= 0 ? Math.round(measured) : -1;
604
1130
  }
605
1131
 
606
1132
  /** Record (or refresh) the in-memory mirror registration for a session this hub created on a box. */
@@ -665,11 +1191,25 @@ function _resetMirrorSessionsForTests() {
665
1191
  module.exports = {
666
1192
  REMOTE_CREATE_FORWARD_ALLOWLIST,
667
1193
  DEFAULT_TIMEOUT_MS,
1194
+ // WP-3 (§6.3 D-3/D-4/D-5): phase B's ceiling, the code an accepted-but-silent box reports, and the
1195
+ // shared dial prologue below. The ceiling's coverage of every box-side bound is enforced by
1196
+ // tests/remote-create-ceiling-covers-box.test.js.
1197
+ CREATE_ACCEPTED_CEILING_MS,
1198
+ MACHINE_CREATE_UNCONFIRMED,
668
1199
  configure,
669
1200
  filterRemoteCreateForward,
670
1201
  remoteCreateAgentKey,
671
1202
  isRemoteMachineTarget,
672
1203
  forwardRemoteCreate,
1204
+ // WP-0 (D-8, §6.3): the hub's one-line-per-create formatter — exported so the line's fixed key
1205
+ // set is assertable without driving a whole dial through a console capture.
1206
+ formatRemoteCreateLine,
1207
+ // WP-3 (docs/remote-agent-skills-bundles-2026-10-25.html §6.5): the platform reader, the peer
1208
+ // header choice the push shares with the dial, and the push call site itself.
1209
+ AGENT_BUNDLE_PLATFORMS,
1210
+ agentBundlePlatformFor,
1211
+ bundlePeerHeadersFor,
1212
+ pushAgentBundleForCreate,
673
1213
  sendRemoteCreate,
674
1214
  buildPeerControlHeaders,
675
1215
  // WP-1 (D-2/D-4): the missing-credential code, its operator copy, and the hub-local state.