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
@@ -9,9 +9,16 @@
9
9
  // about it — a keystroke, a prompt, an approval verdict, a resize — must be carried to the box and
10
10
  // every frame the box produces must be carried back. This module owns that carry:
11
11
  //
12
- // LINK one upstream WS per machine, lazy, opened on the first viewed session and closed
13
- // after IDLE_CLOSE_MS with no viewers. `listLinks().length <= 1` per machine by
14
- // construction (a Map keyed by the machine's minted instance id).
12
+ // LINK two kinds, both lazy, both in ONE Map (WP-1 D-1):
13
+ // * VIEWER link — one upstream WS per (machine, hub socket), keyed by
14
+ // `machineId\0clientId:socketGeneration`. Opened on the FIRST viewed session of that
15
+ // hub socket and closed immediately when the socket goes away (or after
16
+ // IDLE_CLOSE_MS once it stops viewing every session on that machine). It carries
17
+ // that one socket's session traffic and nobody else's, so the box runs one delivery
18
+ // (one in-flight frame, acked by that one viewer) per hub viewer — exactly like a
19
+ // local client.
20
+ // * CONTROL link — one per machine, keyed by machineId, created ONLY by openLink()
21
+ // (the catalog `list` lane + onLinkReady). It never carries a session view.
15
22
  // INTERCEPT the single dispatch seam (lib/ws-router.js's pre-dispatch hook, consulted at
16
23
  // `dispatchWs` before ANY local handler). A message naming a MIRROR session is
17
24
  // diverted here; a message naming a local session returns false immediately and takes
@@ -19,29 +26,52 @@
19
26
  // the byte-identical existing path").
20
27
  // FORWARD hub browser -> box, VERBATIM. `clientMessageId` (S-24) and every other id is the
21
28
  // ORIGINAL one the browser minted — this module never re-mints an idempotency key.
22
- // FAN-OUT box -> every hub browser socket viewing that session (S-22). Reply frames
23
- // (submit-prompt-status, approval-resolution-result, model-change-result, parked,
24
- // killed, …) go to the ORIGINATING hub socket ONLY, matched by the original
25
- // `clientMessageId` when the frame carries one.
29
+ // ONE exception, and only for the geometry family (WP-3 D-4/D-5): `socketGeneration`
30
+ // is a PER-CONNECTION identity minted by whichever server accepted the socket, so the
31
+ // relay rewrites it per hop to the identity of the socket on the receiving side (RFC
32
+ // 9110 §7.6.1's per-hop field model) — outbound `_toBoxHop` sets link.boxSocketGeneration
33
+ // on terminal-geometry-claim-v2 (and its `measurement.socketGeneration`) and on a v2
34
+ // `resize`; inbound `_toHubHop` sets the receiving hub socket's generation on
35
+ // terminal-geometry-claim-ack-v2 / -fenced / -owner-revoked. `clientMessageId` and every
36
+ // other id stay verbatim; the browser's object is never mutated.
37
+ // FAN-OUT box -> the hub browser sockets VIEWING that session (S-22). WP-1 D-2: a viewer link
38
+ // delivers to its OWN ctx only, so co-viewers each receive their own copy on their own
39
+ // link. WP-1 D-3: a content payload (`type:'frame'`, or `type:'frame-v2'` since WP-2) is
40
+ // written with `sendFrame` (ctx.ws) — the LOCAL path — bypassing hub send admission, and
41
+ // WP-2 R-3 projects a `frame-v2` per RECEIVING socket (v2 verbatim, v1 through
42
+ // projectFrameForProtocol; a projection throw is counted and skips just that frame).
43
+ // Every other type keeps `ctx.send` (admission + per-hop geometry identity). Reply
44
+ // frames (submit-prompt-status,
45
+ // approval-resolution-result, model-change-result, session-lifecycle-result, …) go to
46
+ // the ORIGINATING hub socket ONLY, matched by the original `clientMessageId`/`requestId`
47
+ // when the frame carries one.
26
48
  // RECONNECT full-jitter backoff 500 ms -> 30 s cap, reset on success; on every (re)connect the
27
49
  // link re-attaches every viewed session and issues `keyframe-req`, so the box's own
28
50
  // seq/keyframe authority is what re-syncs content (no duplicate/no gap by design).
29
51
  // PINGS two consecutive missed WS pings force a reconnect (the link owns its own liveness —
30
52
  // the peer-control allowlist deliberately has no CTM `ping` message).
31
53
  //
32
- // BACKPRESSURE (§10 risk 2), the choice this module makes: the box sees exactly ONE attached
33
- // client — the link — so its credit/window is the LINK's, not any single hub browser's. Fan-out to
34
- // hub sockets is a local, unconditional, non-blocking send per socket, so a slow/stalled hub viewer
35
- // can never stall the frames the OTHER viewers of the same session receive; the hub socket acks
36
- // (`ack`/`nack`/`frame-receipt`) are forwarded upstream VERBATIM, per session, which is the credit
37
- // signal the box already understands (lib/ws/state-sync.js keys acks by session). Nothing is
38
- // re-minted, no local credit is invented.
54
+ // BACKPRESSURE (§10 risk 2), the choice this module makes: with WP-1 D-1 each hub socket has its OWN
55
+ // upstream link, so the box runs one delivery per hub viewer (one in-flight frame, acked by that one
56
+ // socket) — the local client contract. A slow/stalled hub viewer can never stall the frames the OTHER
57
+ // viewers of the same session receive; each hub socket's acks (`ack`/`nack`/`frame-receipt`) go up
58
+ // its own link, which is the credit signal the box already understands (lib/ws/state-sync.js keys
59
+ // acks by session). Nothing is re-minted, no local credit is invented.
39
60
  //
40
61
  // No durable state (lead ruling 4): links and viewer refcounts are in-memory and rebuilt lazily
41
62
  // from viewed sessions after a hub restart. Everything is injectable for tests.
42
63
 
43
64
  const { registerWsPreDispatchHook } = require('./ws-router');
44
65
  const remoteMachineCreate = require('./remote-machine-create');
66
+ const { sendFrame } = require('./ws-send-admission');
67
+ // WP-2 (docs/relay-frame-v2-negotiation-box-session-mouse-2026-10-31.html §5 R-3/R-6): the box now
68
+ // answers the hub's peer client-info with `frame-v2` (the frame that carries surface.mouse), so the
69
+ // hub projects it down per RECEIVING hub socket — exactly the box's own fan-out contract.
70
+ const { FRAME_PROTOCOL_V1, FRAME_PROTOCOL_V2, projectFrameForProtocol } = require('./state-sync/frame-protocol');
71
+
72
+ // Content frame types that take the direct path (sendFrame, no hub admission) on a viewer link.
73
+ // Both speak the same delivery semantics; only the projection differs per receiving socket (R-3).
74
+ const RELAYED_CONTENT_FRAME_TYPES = new Set(['frame', 'frame-v2']);
45
75
 
46
76
  const BACKOFF_BASE_MS = 500;
47
77
  const BACKOFF_CAP_MS = 30000;
@@ -58,7 +88,12 @@ const RELAY_TYPES = Object.freeze(new Set([
58
88
  // receipt of the cold bootstrap) — both cross the hop, so both are relayed.
59
89
  'attach', 'view', 'keyframe-req', 'snapshot', 'terminal-bootstrap', 'terminal-bootstrap-ack', 'wake-resync',
60
90
  // typing + geometry
91
+ // WP-3 (docs/remote-terminal-cursor-and-geometry-claim-relay-2026-10-27.html §5 D-3/D-4, §9 S-4):
92
+ // `terminal-geometry-claim-v2` is a viewer's size claim for a MIRROR session — the box owns the
93
+ // PTY, so without this the claim never crosses the hop and the box PTY keeps its spawn size. Its
94
+ // `socketGeneration` is rewritten per hop (see the FORWARD note above).
61
95
  'input', 'resize', 'reflow', 'reconcile', 'flow', 'detach', 'interrupt-session',
96
+ 'terminal-geometry-claim-v2',
62
97
  // prompts + approvals + model (S-23/S-24). `image-deliver-now` is WP-3 of
63
98
  // docs/remote-session-image-upload-forwarding-2026-10-24.html §6: the viewer asks the BOX that
64
99
  // owns the session to deliver the already-uploaded draft, so the frame must cross the hop.
@@ -102,6 +137,10 @@ const REPLY_TYPES = Object.freeze(new Set([
102
137
  'session-lifecycle-result',
103
138
  'session-interrupt-result',
104
139
  'resume-prompt-result',
140
+ // WP-3 (docs/remote-terminal-cursor-and-geometry-claim-relay-2026-10-27.html §5 D-6, §9 S-5): the
141
+ // box's verdict on a relayed geometry claim. Routed to the CLAIMING socket only — every other hub
142
+ // viewer of the same mirror session must not see a lease outcome it never asked for.
143
+ 'terminal-geometry-claim-ack-v2',
105
144
  'request-error',
106
145
  'error',
107
146
  ]));
@@ -115,12 +154,22 @@ const REPLY_TYPES = Object.freeze(new Set([
115
154
  const REMEMBER_TYPES = Object.freeze(new Set([
116
155
  'submit-prompt', 'approval-resolve', 'model-change', 'park', 'wake', 'kill', 'interrupt-session',
117
156
  'resume-prompt-select',
157
+ // WP-3 (§5 D-6): a claim carries no `clientMessageId`, so its pending entry is matched by
158
+ // `replyType` (`terminal-geometry-claim-ack-v2`) alone — `_rememberPending` falls back to a
159
+ // per-session seq key when `clientMessageId` is absent, which is exactly what `_takePending`'s
160
+ // replyType predicate consumes.
161
+ 'terminal-geometry-claim-v2',
118
162
  ]));
119
163
  const PENDING_TTL_MS = 60000;
120
164
  const PENDING_MAX = 512;
121
165
  // Bounded queue of frames sent to a link that is still dialing (see forward()):
122
166
  const PENDING_SEND_MAX = 64;
123
167
 
168
+ // WP-0 (docs/relay-per-viewer-delivery-contract-2026-10-27.html §5 D-5, §6, §8 WP-0): at most one
169
+ // `relay-frame-undelivered` diagnostic per (session, reason) window. The counter itself is
170
+ // unthrottled; only the report is.
171
+ const UNDELIVERED_REPORT_INTERVAL_MS = 10000;
172
+
124
173
  // WP-6: the request -> reply-type pairs the box's handlers really implement (see REPLY_TYPES).
125
174
  const REPLY_FOR_REQUEST = Object.freeze({
126
175
  'submit-prompt': 'submit-prompt-status',
@@ -130,6 +179,8 @@ const REPLY_FOR_REQUEST = Object.freeze({
130
179
  'wake': 'session-lifecycle-result',
131
180
  'interrupt-session': 'session-interrupt-result',
132
181
  'resume-prompt-select': 'resume-prompt-result',
182
+ // WP-3 (§5 D-6, §9 S-6): the box's geometry-claim verdict.
183
+ 'terminal-geometry-claim-v2': 'terminal-geometry-claim-ack-v2',
133
184
  });
134
185
 
135
186
  // WP-6: how long a catalog `list` request waits for the box's `sessions` reply before giving up.
@@ -138,12 +189,25 @@ const CATALOG_TIMEOUT_MS = 5000;
138
189
  let _injected = null;
139
190
  let _uninstallIntercept = null;
140
191
  let _productionCache = null;
192
+ // WP-0 (D-5): the hub's session-diagnostics reporter, wired by lib/ws/terminal-attach.js's
193
+ // configure() to the SAME `remoteCreateDeps.recordSessionDiagnostic` the rest of the hub uses (so
194
+ // this needs no server.js edit). Module state, not a deps field, because the production dep object
195
+ // below is memoized once and must read the sink at CALL time (the sink is set at boot).
196
+ let _diagnosticsSink = null;
141
197
 
142
198
  /** Test/host seam: override any production dependency (registry, WebSocket, timers, clock, log). */
143
199
  function configure(deps) {
144
200
  _injected = deps && typeof deps === 'object' ? deps : null;
145
201
  }
146
202
 
203
+ /**
204
+ * WP-0 (D-5): set the diagnostics sink the production `recordDiagnostic` dep forwards to. A
205
+ * non-function clears it, so a `configure()` without a reporter can never leave a stale one behind.
206
+ */
207
+ function setDiagnosticsSink(fn) {
208
+ _diagnosticsSink = typeof fn === 'function' ? fn : null;
209
+ }
210
+
147
211
  function _string(value, max = 200) {
148
212
  return String(value == null ? '' : value).trim().slice(0, max);
149
213
  }
@@ -181,6 +245,13 @@ function _productionDeps() {
181
245
  onSessionList: (link, rows, frame) => { try { require('./remote-machine-catalog').recordSessionList(link.machineInstanceId, rows); } catch { /* expected: ditto — the box's list is a freshness bonus, not a link obligation */ } },
182
246
  onRelayedKill: (link) => { try { require('./remote-machine-catalog').fetchCatalog(link.machineInstanceId); } catch { /* expected: ditto — the kill already crossed; the refresh is best effort */ } },
183
247
  catalogTimeoutMs: CATALOG_TIMEOUT_MS,
248
+ // WP-0 (D-5, §9 S-2): the report path for a relayed frame a hub viewer did not get. Forwards to
249
+ // whatever lib/ws/terminal-attach.js's configure() installed via setDiagnosticsSink(); a hub with
250
+ // no sink records nothing but still counts (undeliveredCounters()). Tests override this dep
251
+ // directly with `configure({ recordDiagnostic })`.
252
+ recordDiagnostic: (sessionId, event, details) => {
253
+ if (_diagnosticsSink) _diagnosticsSink(sessionId, event, details);
254
+ },
184
255
  log: console,
185
256
  idleCloseMs: IDLE_CLOSE_MS,
186
257
  heartbeatMs: HEARTBEAT_MS,
@@ -225,35 +296,259 @@ function sessionIdsOf(msg) {
225
296
  }
226
297
 
227
298
  function _frameSessionId(frame) {
299
+ // WP-0 (docs/hub-viewed-box-conv-freshness-and-paste-render-2026-10-31.html §5 D-1, §6, §9 S-1):
300
+ // `ctmSessionId` is the spelling the box's per-conversation broadcasts use (`conversation-updated`,
301
+ // `session-image-refs-updated` — server.js:9165/9192), so reading only `id`/`sessionId` dropped
302
+ // every one of them on the floor at :811. All three spellings name the same session.
228
303
  if (!frame || typeof frame !== 'object') return '';
229
- return _string(frame.id || frame.sessionId, 200);
304
+ return _string(frame.id || frame.sessionId || frame.ctmSessionId, 200);
305
+ }
306
+
307
+ // ---------------------------------------------------------------------------------------------
308
+ // WP-0 (docs/relay-per-viewer-delivery-contract-2026-10-27.html §5 D-5, §6, §8 WP-0, §9 S-1..S-3):
309
+ // the per-session relay UNDELIVERED counter.
310
+ //
311
+ // The local delivery contract (docs/terminal-delivery-contract-simplification-2026-09-21.html D-4)
312
+ // bypasses send admission, so a frame that reached nobody is counted locally. The relay did NOT:
313
+ // every hub-browser copy goes through `ctx.send` = `_wsSend`, whose ADMISSION_RESULTS value was
314
+ // discarded, so a throttled / stalled / coalesced viewer silently missed frames the box believed it
315
+ // had delivered (and acked). This counts every non-'sent' downstream send and reports it, throttled
316
+ // so one stalled viewer cannot flood the diagnostics table. WP-1 then makes the frame path report
317
+ // only send-error / socket-not-open, which is the live proof of D-3.
318
+ // ---------------------------------------------------------------------------------------------
319
+
320
+ // `${sessionId}\u0000${reason}` -> { count, total, sinceMs, lastReportMs, type, clientId }
321
+ // `count` is the accumulation since the last report; `total` is the lifetime count.
322
+ const _undelivered = new Map();
323
+
324
+ /** Plain snapshot of the undelivered counters, for tests and a hub diagnostics read. */
325
+ function undeliveredCounters() {
326
+ const rows = [];
327
+ for (const [key, entry] of _undelivered) {
328
+ const sep = key.indexOf('\u0000');
329
+ rows.push({
330
+ sessionId: key.slice(0, sep),
331
+ reason: key.slice(sep + 1),
332
+ count: entry.count,
333
+ total: entry.total,
334
+ type: entry.type,
335
+ });
336
+ }
337
+ return rows;
338
+ }
339
+
340
+ /**
341
+ * Record one downstream relayed send this viewer did not get as sent. Reason is an ADMISSION_RESULTS
342
+ * value ('dropped-backpressure' | 'closed-backpressure' | 'dropped-diet' | 'coalesced') or our own
343
+ * 'send-error' / 'socket-not-open'. At most one `relay-frame-undelivered` diagnostic per
344
+ * (sessionId, reason) per UNDELIVERED_REPORT_INTERVAL_MS, carrying the count accumulated in the
345
+ * window. A sink fault is swallowed: the COUNTER is the contract, and a diagnostics failure must
346
+ * never break relayed frame delivery.
347
+ */
348
+ function _noteUndelivered(ctx, frame, reason, d) {
349
+ const sessionId = _frameSessionId(frame);
350
+ const key = `${sessionId}\u0000${reason}`;
351
+ const now = d.now();
352
+ let entry = _undelivered.get(key);
353
+ if (!entry) {
354
+ entry = {
355
+ count: 0,
356
+ total: 0,
357
+ sinceMs: now,
358
+ lastReportMs: null,
359
+ type: frame && frame.type,
360
+ clientId: String((ctx && ctx.ws && ctx.ws.clientId) || ''),
361
+ };
362
+ _undelivered.set(key, entry);
363
+ }
364
+ entry.count += 1;
365
+ entry.total += 1;
366
+ entry.type = frame && frame.type;
367
+ if (entry.lastReportMs != null && now - entry.lastReportMs < UNDELIVERED_REPORT_INTERVAL_MS) return;
368
+ try {
369
+ if (typeof d.recordDiagnostic === 'function') {
370
+ d.recordDiagnostic(sessionId, 'relay-frame-undelivered', {
371
+ reason, type: entry.type, count: entry.count, sinceMs: entry.sinceMs, clientId: entry.clientId,
372
+ });
373
+ }
374
+ } catch {
375
+ // expected: a diagnostics-sink fault must never break relayed frame delivery
376
+ }
377
+ entry.count = 0;
378
+ entry.lastReportMs = now;
379
+ entry.sinceMs = now;
380
+ }
381
+
382
+ // ---------------------------------------------------------------------------------------------
383
+ // WP-0 (docs/remote-relay-ack-rate-limit-typing-delay-2026-10-28.html §5 D-4b, §6, §8 WP-0,
384
+ // §9 S-7): the upstream twin of the counter above.
385
+ //
386
+ // A box refusing a RELAYED frame answers `auth-error {messageType, code}`. `_routeAuthError` can
387
+ // only hand that verdict to the socket that asked, so a refusal with NO pending origin — exactly the
388
+ // shape of a rate-limited `ack`/`frame-receipt`, which is never remembered as pending — was dropped
389
+ // on the floor: the hub browser waited for a verdict that no one ever saw (design doc §5 D-4b).
390
+ // Count every such refusal and report it, throttled the same way and for the same reason.
391
+ // ---------------------------------------------------------------------------------------------
392
+
393
+ // `${machineInstanceId}\u0000${sessionId}\u0000${messageType}` -> { count, total, code, sinceMs,
394
+ // lastReportMs }. `count` is the accumulation since the last report; `total` is the lifetime count.
395
+ const _upstreamRefused = new Map();
396
+
397
+ /** Plain snapshot of the upstream-refusal counters, for tests and a hub diagnostics read. */
398
+ function upstreamRefusedCounters() {
399
+ const rows = [];
400
+ for (const [key, entry] of _upstreamRefused) {
401
+ const [machineInstanceId, sessionId, messageType] = key.split('\u0000');
402
+ rows.push({
403
+ machineInstanceId, sessionId, messageType, code: entry.code, count: entry.count, total: entry.total,
404
+ });
405
+ }
406
+ return rows;
407
+ }
408
+
409
+ /**
410
+ * Record one box `auth-error` refusing a RELAYED frame that no pending origin could claim. At most
411
+ * one `relay-upstream-refused` diagnostic per (machine, session, messageType) per
412
+ * UNDELIVERED_REPORT_INTERVAL_MS, carrying the count accumulated in the window. A sink fault is
413
+ * swallowed: the COUNTER is the contract, and a diagnostics failure must never break relayed frames.
414
+ */
415
+ function _noteUpstreamRefused(link, frame, messageType, d) {
416
+ const machineInstanceId = _string(link && link.machineInstanceId, 200);
417
+ const sessionId = _frameSessionId(frame);
418
+ const key = `${machineInstanceId}\u0000${sessionId}\u0000${messageType}`;
419
+ const now = d.now();
420
+ let entry = _upstreamRefused.get(key);
421
+ if (!entry) {
422
+ entry = {
423
+ count: 0,
424
+ total: 0,
425
+ code: _string(frame && frame.code, 40),
426
+ sinceMs: now,
427
+ lastReportMs: null,
428
+ };
429
+ _upstreamRefused.set(key, entry);
430
+ }
431
+ entry.count += 1;
432
+ entry.total += 1;
433
+ entry.code = _string(frame && frame.code, 40);
434
+ if (entry.lastReportMs != null && now - entry.lastReportMs < UNDELIVERED_REPORT_INTERVAL_MS) return;
435
+ try {
436
+ if (typeof d.recordDiagnostic === 'function') {
437
+ d.recordDiagnostic(sessionId, 'relay-upstream-refused', {
438
+ messageType, code: entry.code, count: entry.count, machineInstanceId,
439
+ });
440
+ }
441
+ } catch {
442
+ // expected: a diagnostics-sink fault must never break relayed frame delivery
443
+ }
444
+ entry.count = 0;
445
+ entry.lastReportMs = now;
446
+ entry.sinceMs = now;
230
447
  }
231
448
 
232
449
  /** Deliver an already-serialized frame to one hub browser socket (fan-out or reply routing). */
233
- function _sendToCtx(ctx, payload) {
450
+ function _sendToCtx(ctx, payload, frame, d) {
234
451
  try {
235
- if (ctx && ctx.ws && ctx.ws.readyState === 1 && typeof ctx.send === 'function') ctx.send(payload);
452
+ if (!ctx || typeof ctx.send !== 'function') return;
453
+ if (!ctx.ws || ctx.ws.readyState !== 1) {
454
+ // WP-0 (D-5): the hub viewer's socket is gone — this frame reached nobody. Count it instead of
455
+ // dropping it silently; the box owns the session and the viewer re-subscribes on reconnect.
456
+ _noteUndelivered(ctx, frame, 'socket-not-open', d);
457
+ return;
458
+ }
459
+ const result = ctx.send(payload);
460
+ // WP-0 (D-5): `ctx.send` is `_wsSend(ws, payload)` in production, which returns an
461
+ // ADMISSION_RESULTS value. Anything but 'sent' means THIS viewer did not get the frame as sent.
462
+ // `undefined` (a test ctx whose send returns nothing) is not a verdict and records nothing.
463
+ if (typeof result === 'string' && result !== 'sent') _noteUndelivered(ctx, frame, result, d);
236
464
  } catch {
237
465
  // expected: the hub browser socket died while a relayed frame was in flight — the box owns the
238
466
  // session and WP-5's UI re-subscribes on the next connect.
467
+ _noteUndelivered(ctx, frame, 'send-error', d);
468
+ }
469
+ }
470
+
471
+ function _sendFrame(ctx, frame, d) {
472
+ _sendToCtx(ctx, JSON.stringify(_toHubHop(frame, ctx)), frame, d);
473
+ }
474
+
475
+ // ---------------------------------------------------------------------------------------------
476
+ // Per-hop identity (WP-3, docs/remote-terminal-cursor-and-geometry-claim-relay-2026-10-27.html
477
+ // §5 D-4/D-5, RFC 9110 §7.6.1). `socketGeneration` is minted by whichever server accepted the WS
478
+ // (server.js's per-connection `ws.socketGeneration`) and the box checks it against the accepting
479
+ // connection (lib/terminal-geometry-claim.js:59-61/106-108). The hub browser's generation is
480
+ // meaningless to the box, and several hub sockets share ONE box generation (they share one link),
481
+ // so the relay owns hop identity: it rewrites ONLY `socketGeneration` (and, for the claim, the
482
+ // nested `measurement.socketGeneration`) on the geometry family, per direction. `clientMessageId`
483
+ // and every other id stay verbatim, and the caller's object is never mutated.
484
+ // ---------------------------------------------------------------------------------------------
485
+
486
+ // Box -> hub: the geometry family, whose `socketGeneration` must name the RECEIVING hub socket.
487
+ const GEOMETRY_INBOUND_TYPES = Object.freeze(new Set([
488
+ 'terminal-geometry-claim-ack-v2', 'terminal-geometry-fenced', 'terminal-geometry-owner-revoked',
489
+ ]));
490
+
491
+ /** True for a hub-browser frame whose outbound `socketGeneration` must name the LINK's box socket. */
492
+ function _isOutboundGeometry(msg) {
493
+ if (!msg || typeof msg !== 'object') return false;
494
+ if (msg.type === 'terminal-geometry-claim-v2') return true;
495
+ // A v2 resize is the other per-connection identity carrier (a legacy v1 resize has no generation).
496
+ return msg.type === 'resize' && msg.geometryProtocolVersion === 2;
497
+ }
498
+
499
+ /**
500
+ * Outbound hop rewrite (D-4). Returns `msg` ITSELF unless it is a geometry message; otherwise a
501
+ * SHALLOW COPY with `socketGeneration = link.boxSocketGeneration` and, for the claim, a copied
502
+ * `measurement` with `measurement.socketGeneration` rewritten too. No other field changes.
503
+ */
504
+ function _toBoxHop(link, msg) {
505
+ if (!_isOutboundGeometry(msg)) return msg;
506
+ const generation = Number(link && link.boxSocketGeneration) || 0;
507
+ const hop = { ...msg, socketGeneration: generation };
508
+ if (msg.type === 'terminal-geometry-claim-v2' && msg.measurement && typeof msg.measurement === 'object') {
509
+ hop.measurement = { ...msg.measurement, socketGeneration: generation };
239
510
  }
511
+ return hop;
240
512
  }
241
513
 
242
- function _sendFrame(ctx, frame) {
243
- _sendToCtx(ctx, JSON.stringify(frame));
514
+ /**
515
+ * Inbound hop rewrite (D-5). Returns `frame` ITSELF unless it is one of GEOMETRY_INBOUND_TYPES;
516
+ * otherwise a SHALLOW COPY whose `socketGeneration` names the RECEIVING hub socket. Applied at
517
+ * `_sendFrame`, so both the reply-to-origin path and the fan-out path are covered per receiving
518
+ * socket.
519
+ */
520
+ function _toHubHop(frame, ctx) {
521
+ if (!frame || typeof frame !== 'object' || !GEOMETRY_INBOUND_TYPES.has(frame.type)) return frame;
522
+ return { ...frame, socketGeneration: Number(ctx && ctx.ws && ctx.ws.socketGeneration) || 0 };
244
523
  }
245
524
 
246
525
  // ---------------------------------------------------------------------------------------------
247
526
  // Links
248
527
  // ---------------------------------------------------------------------------------------------
249
528
 
250
- const _links = new Map(); // machineInstanceId -> link
529
+ const _links = new Map(); // key -> link (control key = machineInstanceId; viewer key = _viewerKey(...))
530
+
531
+ /**
532
+ * WP-1 (D-1): the VIEWER-link key — one upstream link per (machine, hub socket). `clientId` and
533
+ * `socketGeneration` are the hub-side per-connection identity (server.js's own `ws.clientId` /
534
+ * `ws.socketGeneration`), so two tabs on the same machine get two links and therefore two box
535
+ * deliveries. A missing ctx/ws degrades to the `':0'` suffix (the caller guards it anyway).
536
+ */
537
+ function _viewerKey(machineId, ctx) {
538
+ const ws = ctx && ctx.ws;
539
+ return `${machineId}\u0000${String((ws && ws.clientId) || '')}:${Number(ws && ws.socketGeneration) || 0}`;
540
+ }
251
541
 
252
- function _newLink(machineId, machineName, url) {
542
+ function _newLink(machineId, machineName, url, kind = 'control', viewerCtx = null) {
253
543
  return {
544
+ key: kind === 'viewer' ? _viewerKey(machineId, viewerCtx) : machineId,
254
545
  machineInstanceId: machineId,
255
546
  machineName,
256
547
  url,
548
+ // WP-1 (D-1): 'control' (key = machineId, the catalog lane) | 'viewer' (key = _viewerKey, one per
549
+ // hub socket). `viewerCtx` is the hub ctx a viewer link delivers to; null on a control link.
550
+ kind,
551
+ viewerCtx,
257
552
  socket: null,
258
553
  ready: false,
259
554
  closed: false,
@@ -264,6 +559,10 @@ function _newLink(machineId, machineName, url) {
264
559
  state: 'idle',
265
560
  attempts: 0,
266
561
  generation: 0,
562
+ // WP-3 (D-4): the BOX-side socket identity the box's `hello` reports, recorded on every hello.
563
+ // Outbound geometry frames carry THIS value (the box checks it against its own accepting socket);
564
+ // 0 until the first hello, which is why a pre-hello geometry frame is queued (flushed with it).
565
+ boxSocketGeneration: 0,
267
566
  pingMisses: 0,
268
567
  heartbeatTimer: null,
269
568
  reconnectTimer: null,
@@ -283,8 +582,14 @@ function _newLink(machineId, machineName, url) {
283
582
  };
284
583
  }
285
584
 
286
- function linkFor(machineId) {
287
- return _links.get(_string(machineId, 160)) || null;
585
+ /**
586
+ * WP-1 (D-1): resolve the link for `machineId` — with a `ctx`, that ctx's VIEWER link; without, the
587
+ * machine's CONTROL link. Returns null when no such link exists.
588
+ */
589
+ function linkFor(machineId, ctx) {
590
+ const id = _string(machineId, 160);
591
+ if (ctx) return _links.get(_viewerKey(id, ctx)) || null;
592
+ return _links.get(id) || null;
288
593
  }
289
594
 
290
595
  function listLinks() {
@@ -292,9 +597,10 @@ function listLinks() {
292
597
  }
293
598
 
294
599
  /**
295
- * Open (or reuse) the one link to a machine. `machine` is a registry row or a machine instance id.
296
- * Never throws: a machine that is not registered / disabled / url-less yields null and the caller
297
- * degrades to the existing "no frames" UX.
600
+ * Open (or reuse) the CONTROL link to a machine — the catalog `list`/onLinkReady lane, keyed by the
601
+ * machine id. `machine` is a registry row or a machine instance id. Never throws: a machine that is
602
+ * not registered / disabled / url-less yields null and the caller degrades to the existing "no
603
+ * frames" UX. A viewing hub socket gets its OWN link via `_ensureViewerLink` (WP-1 D-1).
298
604
  */
299
605
  function openLink(machine, deps) {
300
606
  const d = _d(deps);
@@ -302,7 +608,18 @@ function openLink(machine, deps) {
302
608
  if (!row) return null;
303
609
  const machineId = _string(row.instanceId, 160);
304
610
  if (!machineId) return null;
305
- const existing = _links.get(machineId);
611
+ return _openKeyed(machineId, row, d, 'control', null);
612
+ }
613
+
614
+ /**
615
+ * WP-1 (D-1): the shared open/reuse body for BOTH link kinds. `key` is the Map key (machineId for a
616
+ * control link, `_viewerKey` for a viewer link); the enabled/url/health checks and `_dial` are
617
+ * identical for both, so only the key/kind/viewerCtx differ.
618
+ */
619
+ function _openKeyed(key, row, d, kind, viewerCtx) {
620
+ const machineId = _string(row.instanceId, 160);
621
+ if (!machineId) return null;
622
+ const existing = _links.get(key);
306
623
  if (existing && !existing.closed) {
307
624
  // D-3: a link parked on a missing credential is retried by the NEXT acquire — that is the
308
625
  // "hub restarted with CTM_SYNC_PEER_TOKEN set" path, and it is the only thing that can clear
@@ -316,8 +633,8 @@ function openLink(machine, deps) {
316
633
  const url = _string(row.url, 1000);
317
634
  if (!url) return null;
318
635
  if (_string(row.health, 40) === 'unreachable') return null;
319
- const link = _newLink(machineId, _string(row.displayName, 200), url);
320
- _links.set(machineId, link);
636
+ const link = _newLink(machineId, _string(row.displayName, 200), url, kind, viewerCtx);
637
+ _links.set(key, link);
321
638
  _dial(link, d);
322
639
  return link;
323
640
  }
@@ -473,12 +790,35 @@ function _onUpstreamFrame(link, raw, d) {
473
790
  // tolerated exactly as WP-2's dial tolerates it. A verdict that NAMES a relayed message type
474
791
  // is the box refusing THAT frame, so it is reported to the socket that sent it instead of
475
792
  // being swallowed (a silent refusal is indistinguishable from a broken relay).
476
- _routeAuthError(link, frame);
793
+ _routeAuthError(link, frame, d);
794
+ return;
795
+ }
796
+ if (frame.type === 'hub-bridge-request') {
797
+ // WP-0 (docs/remote-session-hub-bridge-2026-10-30.html §6.1, §8 WP-0): the box -> hub request
798
+ // lane. The BOX asks the hub that dialled it to run an op; the reply rides the SAME CONTROL
799
+ // socket. Route BEFORE the session-frame branches: the frame names no session and is not a
800
+ // relay frame. `handleRequest` never throws and never rejects, and it writes EXACTLY ONE
801
+ // response through the upstream send (`_sendUpstream`) — never a local run (R-7).
802
+ try {
803
+ const hubBridge = require('./hub-bridge');
804
+ const pending = hubBridge.handleRequest(link, frame, (reply) => _sendUpstream(link, reply, d));
805
+ if (pending && typeof pending.catch === 'function') {
806
+ pending.catch((error) => _report(d, `link ${link.machineInstanceId}: hub-bridge failed: ${error?.message || error}`));
807
+ }
808
+ } catch (error) {
809
+ _report(d, `link ${link.machineInstanceId}: hub-bridge failed: ${error?.message || error}`);
810
+ }
477
811
  return;
478
812
  }
479
813
 
480
814
  const sessionId = _frameSessionId(frame);
481
- if (!sessionId) return;
815
+ if (!sessionId) {
816
+ // WP-0 (D-4, §9 S-2): an upstream frame that still names no session is reported once per
817
+ // (link, type) rather than vanishing — this exact silent `return` hid the conversation-updated
818
+ // drop for four minutes of live traffic.
819
+ _reportUnroutedOnce(link, frame.type, d);
820
+ return;
821
+ }
482
822
  if (_routeReplyToOrigin(link, frame, sessionId, d)) return;
483
823
  _fanout(link, frame, sessionId, d);
484
824
  }
@@ -493,7 +833,24 @@ function _onHello(link, frame, d) {
493
833
  link.ready = true;
494
834
  link.dialing = false;
495
835
  link.attempts = 0; // backoff resets ON SUCCESS (full-jitter contract)
836
+ // WP-3 (D-4, §9 S-7): the box's `hello` carries the identity of THIS (box-side) connection — the
837
+ // value every outbound geometry frame must present. Recorded before the pending flush below, so a
838
+ // queued pre-hello claim goes out with this generation.
839
+ link.boxSocketGeneration = Number(frame.socketGeneration) || 0;
496
840
  _sendUpstream(link, remoteMachineCreate.peerClientInfoFrame(frame), d);
841
+ // WP-0 (docs/remote-session-hub-bridge-2026-10-30.html §5 R-2, §6.1, §8 WP-0): a CONTROL link
842
+ // identifies ITSELF to the box once per (re)connect, right after the client-info, so the box knows
843
+ // which hub dialled it and can route a `hub-bridge-request` back down this very socket. Sent ONLY
844
+ // on the control link (a viewer link carries one socket's session traffic, never the lane), and
845
+ // only when this process has an identity to name — `controlHelloFrame()` returns null pre-boot.
846
+ if (link.kind === 'control') {
847
+ try {
848
+ const helloFrame = require('./hub-bridge').controlHelloFrame();
849
+ if (helloFrame) _sendUpstream(link, helloFrame, d);
850
+ } catch (error) {
851
+ _report(d, `link ${link.machineInstanceId}: hub-bridge hello failed: ${error?.message || error}`);
852
+ }
853
+ }
497
854
  // Re-attach AFTER the client-info above: the box's protocol gate drops a terminal-protocol frame
498
855
  // that arrives before a compatible client-info (the dial-order rule the create lane learned).
499
856
  link.pingMisses = 0;
@@ -501,7 +858,9 @@ function _onHello(link, frame, d) {
501
858
  for (const sessionId of link.viewers.keys()) _attachUpstream(link, sessionId, d, alreadyReady ? 'link-reconnect' : 'link-ready');
502
859
  // WP-6: a catalog `list` requested while the link was still dialing is flushed HERE, after the
503
860
  // client-info above — the box's protocol gate drops a terminal-protocol frame that arrives first.
504
- for (const queued of link.pendingSends.splice(0)) _sendUpstream(link, queued, d);
861
+ // WP-3 (D-4): the hop rewrite is applied at FLUSH time (not enqueue time), so a geometry frame
862
+ // queued before this hello carries the generation the hello just reported.
863
+ for (const queued of link.pendingSends.splice(0)) _sendUpstream(link, _toBoxHop(link, queued), d);
505
864
  // A catalog request that was queued while this link was dialing sends its `list` HERE, so this
506
865
  // hello has already asked the box for its list — skip the refresh hook below rather than paying a
507
866
  // second identical round-trip on the same connection.
@@ -509,7 +868,9 @@ function _onHello(link, frame, d) {
509
868
  for (const finish of [...link.catalogWaiters.values()]) _sendCatalogList(link, finish, d);
510
869
  // WP-6 §8: "on link (re)connect" is one of the catalog's refresh triggers. Injected (not
511
870
  // required) so this module never has to depend on the catalog module it feeds.
512
- if (!catalogAsked && typeof d.onLinkReady === 'function') {
871
+ // WP-1 (D-1/S-9): the CONTROL link owns the catalog lane — a viewer link never fires this, so a
872
+ // per-socket viewing link never pays a catalog round-trip.
873
+ if (!catalogAsked && link.kind === 'control' && typeof d.onLinkReady === 'function') {
513
874
  try { d.onLinkReady(link); } catch { /* expected: a catalog hook fault must not break the link */ }
514
875
  }
515
876
  }
@@ -634,11 +995,17 @@ function _takePending(link, match) {
634
995
  * Deliver it to the originating socket; the routine verdict on our own client-info carries no
635
996
  * `messageType` and stays noise.
636
997
  */
637
- function _routeAuthError(link, frame) {
998
+ function _routeAuthError(link, frame, d) {
638
999
  const messageType = _string(frame.messageType, 40);
639
1000
  if (!messageType) return;
640
1001
  const entry = _takePending(link, (e) => e.requestType === messageType);
641
- if (entry) _sendFrame(entry.ctx, frame);
1002
+ if (entry) {
1003
+ _sendFrame(entry.ctx, frame, d);
1004
+ return;
1005
+ }
1006
+ // WP-0 (D-4b): a refusal of a RELAYED type with no pending origin is the silent case — count it
1007
+ // (the reply routing above is unchanged, so this is diagnostics only).
1008
+ if (RELAY_TYPES.has(messageType)) _noteUpstreamRefused(link, frame, messageType, d);
642
1009
  }
643
1010
 
644
1011
  /**
@@ -659,7 +1026,7 @@ function _routeReplyToOrigin(link, frame, sessionId, d) {
659
1026
  if (requestId) {
660
1027
  const entry = _takePending(link, (e) => e.sessionId === sessionId
661
1028
  && e.replyType === frame.type && e.requestId === requestId);
662
- if (entry) _sendFrame(entry.ctx, frame);
1029
+ if (entry) _sendFrame(entry.ctx, frame, d);
663
1030
  return true;
664
1031
  }
665
1032
  // A result carrying no requestId keeps the legacy clientMessageId/type fallback below.
@@ -670,21 +1037,89 @@ function _routeReplyToOrigin(link, frame, sessionId, d) {
670
1037
  : null)
671
1038
  || _takePending(link, (e) => e.sessionId === sessionId && e.replyType === frame.type);
672
1039
  if (!entry) return false;
673
- _sendFrame(entry.ctx, frame);
1040
+ _sendFrame(entry.ctx, frame, d);
674
1041
  return true;
675
1042
  }
676
1043
 
1044
+ /**
1045
+ * WP-1 (D-3/D-4): deliver one box content frame straight to the viewer's hub socket through
1046
+ * `sendFrame` — the SAME no-admission primitive server.js's local frame channel uses. A viewer
1047
+ * whose socket is gone (or whose write throws) did not get this frame, so it is counted (WP-0/D-5)
1048
+ * and THAT viewer's link is closed (D-4): the box detaches that delivery and the socket's own
1049
+ * reconnect re-attaches + re-bootstraps. No other viewer is affected.
1050
+ *
1051
+ * WP-2 (R-3/R-6): both content frame types (`frame`, `frame-v2`) take this direct path. A
1052
+ * `frame-v2` is projected to v1 for a hub socket that did not negotiate v2 — per-HOP versioning,
1053
+ * the hub speaks v2 to the box on its own behalf and v1 to a V1 viewer. The projection runs
1054
+ * WITHOUT validation for a V2 socket (verbatim; the browser validates), and a projection THROW
1055
+ * counts as `projection-error` and skips just that frame for that viewer — the link stays OPEN,
1056
+ * matching the box's own fan-out.
1057
+ */
1058
+ function _deliverFrameDirect(ctx, frame, link, d) {
1059
+ if (!ctx || !ctx.ws || ctx.ws.readyState !== 1) {
1060
+ _noteUndelivered(ctx, frame, 'socket-not-open', d);
1061
+ closeLink(link, d);
1062
+ return;
1063
+ }
1064
+ let payload = frame;
1065
+ if (frame.type === 'frame-v2' && ctx.ws.frameProtocol !== FRAME_PROTOCOL_V2) {
1066
+ try {
1067
+ // R-3: NARROW the v2 frame to what this v1 socket understands. The v2 projection is the
1068
+ // step that VALIDATES the authority (projectFrameForProtocol's v1 branch is a pure
1069
+ // field-strip and refuses nothing on its own), so a malformed frame-v2 is skipped here for
1070
+ // THIS viewer instead of being handed down half-understood.
1071
+ payload = projectFrameForProtocol(projectFrameForProtocol(frame, FRAME_PROTOCOL_V2), FRAME_PROTOCOL_V1);
1072
+ } catch {
1073
+ // expected: a v2 frame this hub cannot project (invalid authority) is skipped for THIS viewer
1074
+ // only; the counter is the contract and the link must survive (R-6).
1075
+ _noteUndelivered(ctx, frame, 'projection-error', d);
1076
+ return;
1077
+ }
1078
+ }
1079
+ if (!sendFrame(ctx.ws, JSON.stringify(payload))) {
1080
+ _noteUndelivered(ctx, frame, 'send-error', d);
1081
+ closeLink(link, d);
1082
+ }
1083
+ }
1084
+
677
1085
  function _fanout(link, frame, sessionId, d) {
678
1086
  const viewers = link.viewers.get(sessionId);
679
1087
  if (!viewers || viewers.size === 0) return;
1088
+ // WP-1 (D-2/D-3): a viewer link holds just its own ctx, and a content payload (`frame` or, WP-2,
1089
+ // `frame-v2`) goes straight to that socket with `sendFrame` (the local path — no hub admission),
1090
+ // projected per receiving socket inside _deliverFrameDirect. Every other frame type keeps
1091
+ // the existing paths: a geometry frame carries the RECEIVING hub socket's own generation, so it is
1092
+ // still serialized per viewer via _sendFrame.
1093
+ if (RELAYED_CONTENT_FRAME_TYPES.has(frame.type) && link.kind === 'viewer') {
1094
+ for (const ctx of viewers) _deliverFrameDirect(ctx, frame, link, d);
1095
+ return;
1096
+ }
1097
+ if (GEOMETRY_INBOUND_TYPES.has(frame.type)) {
1098
+ for (const ctx of viewers) _sendFrame(ctx, frame, d);
1099
+ return;
1100
+ }
680
1101
  const payload = JSON.stringify(frame);
681
- for (const ctx of viewers) _sendToCtx(ctx, payload);
1102
+ for (const ctx of viewers) _sendToCtx(ctx, payload, frame, d);
682
1103
  }
683
1104
 
684
1105
  // ---------------------------------------------------------------------------------------------
685
1106
  // Downstream (hub browser -> box)
686
1107
  // ---------------------------------------------------------------------------------------------
687
1108
 
1109
+ /**
1110
+ * WP-1 (D-4): install the ONCE-per-hub-socket close hook that releases every bit of this socket's
1111
+ * relay state — its own viewer links close outright, and its viewer entries on any other link drop
1112
+ * (arming the idle close). Installed from BOTH the viewing path (`viewSession`) and the forward-only
1113
+ * path (`_ensureViewerLink`): a socket that only ever relays a lifecycle/prompt frame for a mirror it
1114
+ * never viewed still owns a viewer link, and without this hook that link would outlive the socket.
1115
+ * Idempotent per socket, because the relay must not need an edit to server.js's own `ws.on('close')`.
1116
+ */
1117
+ function _hookSocketClose(ctx, d) {
1118
+ if (!ctx || !ctx.ws || typeof ctx.ws.once !== 'function' || ctx.ws._remoteRelayCloseHooked) return;
1119
+ ctx.ws._remoteRelayCloseHooked = true;
1120
+ ctx.ws.once('close', () => releaseSocket(ctx, d));
1121
+ }
1122
+
688
1123
  /**
689
1124
  * Register `ctx` as a viewer of `sessionId` on this link. The FIRST viewer of a session makes the
690
1125
  * link attach upstream and ask for a keyframe, so the box pushes that session's frames to the link.
@@ -706,12 +1141,8 @@ function viewSession(link, sessionId, ctx, deps) {
706
1141
  if (ctx) {
707
1142
  viewers.add(ctx);
708
1143
  // AC-3d: a hub browser tab closing RELEASES its viewers (and only then arms the machine's
709
- // 60 s idle close) — it never closes the upstream link directly. Hooked here, once per socket,
710
- // because the relay must not need an edit to server.js's own `ws.on('close')`.
711
- if (ctx.ws && typeof ctx.ws.once === 'function' && !ctx.ws._remoteRelayCloseHooked) {
712
- ctx.ws._remoteRelayCloseHooked = true;
713
- ctx.ws.once('close', () => releaseSocket(ctx, d));
714
- }
1144
+ // 60 s idle close) — it never closes the upstream link directly.
1145
+ _hookSocketClose(ctx, d);
715
1146
  }
716
1147
  if (first && link.ready) _attachUpstream(link, id, d, 'hub-view');
717
1148
  return first;
@@ -727,10 +1158,18 @@ function releaseViewer(link, sessionId, ctx, deps) {
727
1158
  _maybeScheduleIdleClose(link, d);
728
1159
  }
729
1160
 
730
- /** A hub socket went away: drop it from every session's viewer set on every link. */
1161
+ /** A hub socket went away: close ITS viewer links outright, and drop it from any other link's viewers. */
731
1162
  function releaseSocket(ctx, deps) {
732
1163
  const d = _d(deps);
733
- for (const link of _links.values()) {
1164
+ // Snapshot: closeLink mutates _links while we iterate (WP-1 D-4).
1165
+ for (const link of [..._links.values()]) {
1166
+ // WP-1 (D-4): a viewer link belongs to exactly ONE hub socket, so that socket going away closes
1167
+ // the link IMMEDIATELY — no 60 s idle. The box then detaches that delivery, and the socket's own
1168
+ // reconnect re-attaches + re-bootstraps. (A LIVE socket that merely stops viewing keeps the idle.)
1169
+ if (link.kind === 'viewer' && link.viewerCtx === ctx) {
1170
+ closeLink(link, d);
1171
+ continue;
1172
+ }
734
1173
  for (const [sessionId, viewers] of [...link.viewers]) {
735
1174
  if (viewers.delete(ctx) && viewers.size === 0) link.viewers.delete(sessionId);
736
1175
  }
@@ -777,7 +1216,7 @@ function _clearIdleClose(link, d) {
777
1216
  * re-syncs via keyframe.
778
1217
  */
779
1218
  function forward(link, msg, ctx, deps) {
780
- if (!link || link.closed) return false;
1219
+ if (!link || link.closed) { if (msg?.type === 'kill') console.warn(`[kill-drop] site=relay-link-closed id=${sessionIdsOf(msg)[0] || ''}`); return false; }
781
1220
  const d = _d(deps);
782
1221
  const sessionId = sessionIdsOf(msg)[0] || '';
783
1222
  _rememberPending(link, sessionId, msg, ctx, d);
@@ -789,7 +1228,7 @@ function forward(link, msg, ctx, deps) {
789
1228
  if (link.pendingSends.length < PENDING_SEND_MAX) link.pendingSends.push(msg);
790
1229
  return true;
791
1230
  }
792
- const sent = _sendUpstream(link, msg, d);
1231
+ const sent = _sendUpstream(link, _toBoxHop(link, msg), d);
793
1232
  // WP-6: a relayed kill changes the box's session list, so the catalog re-reads it right away
794
1233
  // rather than waiting out the ~15 s probe cadence (the row becomes exited on the box), and the
795
1234
  // in-memory mirror registration for the killed id is dropped (the catalog keeps routing it).
@@ -828,15 +1267,21 @@ function interceptRelay(ctx, msg, deps) {
828
1267
  const mirror = d.mirrorFor(id);
829
1268
  if (mirror) mirrors.push({ id, mirror });
830
1269
  }
831
- if (mirrors.length === 0) return false;
1270
+ if (mirrors.length === 0) { if (type === 'kill') console.warn(`[kill-drop] site=relay-no-mirror id=${ids[0]}`); return false; }
832
1271
  // A `view` frame declaring BOTH local and remote sessions is left to the local handler (it owns
833
1272
  // the local viewer set); the mirror ids are still registered below.
834
1273
  const mixedView = type === 'view' && mirrors.length !== ids.length;
835
1274
 
836
1275
  let handled = true;
1276
+ // The viewer links this frame touched, so their idle close can be re-evaluated once per frame
1277
+ // (see the forward-only note below) instead of at every early `continue`.
1278
+ const touched = [];
837
1279
  for (const { id, mirror } of mirrors) {
838
- const link = _ensureLink(mirror, d);
839
- if (!link) { handled = false; continue; }
1280
+ // WP-1 (D-1): the link is resolved from the mirror's machine AND the calling hub socket, so each
1281
+ // hub socket relays on its OWN upstream link (one box delivery per viewer).
1282
+ const link = _ensureViewerLink(mirror, ctx, d);
1283
+ if (!link) { if (type === 'kill') console.warn(`[kill-drop] site=relay-no-link id=${id}`); handled = false; continue; }
1284
+ touched.push(link);
840
1285
  if (VIEW_TYPES.has(type)) {
841
1286
  const first = viewSession(link, id, ctx, d);
842
1287
  // `attach`/`view` are synthesized upstream by viewSession; `keyframe-req` and the rest are
@@ -850,17 +1295,35 @@ function interceptRelay(ctx, msg, deps) {
850
1295
  }
851
1296
  forward(link, msg, ctx, d);
852
1297
  }
1298
+ // WP-1 (D-4): a FORWARD-ONLY viewer link — a lifecycle/prompt frame for a mirror this socket never
1299
+ // viewed — registers no viewer, so nothing above would ever arm its idle close and it would hold an
1300
+ // upstream socket open forever. Arm it here (a no-op for a link that does hold a viewer, and
1301
+ // idempotent when releaseViewer already armed it). A later attach cancels it via _clearIdleClose.
1302
+ for (const link of touched) _maybeScheduleIdleClose(link, d);
853
1303
  return mixedView ? false : handled;
854
1304
  }
855
1305
 
856
- function _ensureLink(mirror, d) {
857
- const machineId = _string(mirror.machineInstanceId, 160);
858
- const existing = _links.get(machineId);
859
- if (existing && !existing.closed) {
860
- _clearIdleClose(existing, d);
861
- return existing;
1306
+ /**
1307
+ * WP-1 (D-1/D-2): the VIEWER link for (mirror.machineInstanceId, ctx) — replaces the old machine-only
1308
+ * `_ensureLink`, so each hub socket gets its own upstream link and therefore its own box delivery.
1309
+ * Reuse clears the idle close. A missing ctx (or a ctx with no ws) has no per-socket identity and
1310
+ * falls back to the control link, silently.
1311
+ */
1312
+ function _ensureViewerLink(mirror, ctx, d) {
1313
+ const machineId = _string(mirror && mirror.machineInstanceId, 160);
1314
+ if (!machineId) return null;
1315
+ if (!ctx || !ctx.ws) return openLink(machineId, d);
1316
+ const row = d.machineRow(machineId, d);
1317
+ if (!row) return null;
1318
+ const link = _openKeyed(_viewerKey(machineId, ctx), row, d, 'viewer', ctx);
1319
+ if (link) {
1320
+ // WP-1 (D-4): this socket now owns a viewer link — hook its close here too, not only in
1321
+ // viewSession, so a FORWARD-ONLY relay (a lifecycle/prompt frame for a mirror it never viewed)
1322
+ // is still released the moment the socket goes away. Idempotent per socket.
1323
+ _hookSocketClose(ctx, d);
1324
+ _clearIdleClose(link, d);
862
1325
  }
863
- return openLink(machineId, d);
1326
+ return link;
864
1327
  }
865
1328
 
866
1329
  function installIntercept() {
@@ -884,7 +1347,10 @@ function closeLink(link, deps) {
884
1347
  link.ready = false;
885
1348
  link.generation += 1;
886
1349
  try { socket?.close?.(); } catch { /* expected: the upstream socket is already gone */ }
887
- if (_links.get(link.machineInstanceId) === link) _links.delete(link.machineInstanceId);
1350
+ // WP-1 (D-1): the Map is keyed by `link.key` (machineId for a control link, _viewerKey for a
1351
+ // viewer link) — never by machineId alone, or closing one viewer's link would evict another's.
1352
+ const key = link.key || link.machineInstanceId;
1353
+ if (_links.get(key) === link) _links.delete(key);
888
1354
  }
889
1355
 
890
1356
  function _report(d, message) {
@@ -892,6 +1358,19 @@ function _report(d, message) {
892
1358
  if (d && d.log && typeof d.log.warn === 'function') d.log.warn(`[remote-machine-link] ${message}`);
893
1359
  }
894
1360
 
1361
+ /**
1362
+ * WP-0 (docs/hub-viewed-box-conv-freshness-and-paste-render-2026-10-31.html §5 D-4): report an
1363
+ * upstream frame the relay cannot route — once per (link, type), so a repeating unnamed frame
1364
+ * cannot flood the log while the first occurrence still shows up in `ctm.log`.
1365
+ */
1366
+ function _reportUnroutedOnce(link, type, d) {
1367
+ const key = _string(type, 60) || '(none)';
1368
+ if (!link._unroutedTypes) link._unroutedTypes = new Set();
1369
+ if (link._unroutedTypes.has(key)) return;
1370
+ link._unroutedTypes.add(key);
1371
+ _report(d, `link ${link.machineInstanceId}: dropped upstream frame type=${key} (names no session)`);
1372
+ }
1373
+
895
1374
  /** Test seam: links and their timers are module state and must never leak between tests. */
896
1375
  function _resetForTests() {
897
1376
  for (const link of [..._links.values()]) {
@@ -906,6 +1385,10 @@ function _resetForTests() {
906
1385
  try { link.heartbeatTimer && clearInterval(link.heartbeatTimer); } catch { /* expected: test teardown */ }
907
1386
  }
908
1387
  _links.clear();
1388
+ // WP-0 (D-5): the undelivered counters are module state too — a test that drove drops must not
1389
+ // leak its (session, reason) rows into the next one. Same for the WP-0 upstream-refusal counters.
1390
+ _undelivered.clear();
1391
+ _upstreamRefused.clear();
909
1392
  _injected = null;
910
1393
  }
911
1394
 
@@ -915,12 +1398,16 @@ module.exports = {
915
1398
  IDLE_CLOSE_MS,
916
1399
  PING_MISS_LIMIT,
917
1400
  CATALOG_TIMEOUT_MS,
1401
+ UNDELIVERED_REPORT_INTERVAL_MS,
918
1402
  RELAY_TYPES,
919
1403
  VIEW_TYPES,
920
1404
  REPLY_TYPES,
921
1405
  REPLY_FOR_REQUEST,
922
1406
  REMEMBER_TYPES,
923
1407
  configure,
1408
+ setDiagnosticsSink,
1409
+ undeliveredCounters,
1410
+ upstreamRefusedCounters,
924
1411
  installIntercept,
925
1412
  openLink,
926
1413
  linkFor,