create-walle 0.10.1 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (341) hide show
  1. package/README.md +4 -0
  2. package/lib/owner-write-safety.js +135 -0
  3. package/lib/public-runtime.js +13 -5
  4. package/lib/release-schema.json +3 -2
  5. package/lib/runtime-closure-policy.json +6 -4
  6. package/package.json +1 -1
  7. package/template/bin/deps-doctor.js +1 -1
  8. package/template/bin/node-bin.sh +11 -0
  9. package/template/coding-task-manager/api-prompts.js +10 -22
  10. package/template/coding-task-manager/bin/claude-hook.js +7 -3
  11. package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
  12. package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
  13. package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
  14. package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
  15. package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
  16. package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
  17. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  18. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  19. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  20. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  21. package/template/coding-task-manager/db.js +705 -286
  22. package/template/coding-task-manager/git-utils.js +44 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
  24. package/template/coding-task-manager/lib/agent-presets.js +15 -0
  25. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  26. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  27. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  28. package/template/coding-task-manager/lib/api/ingress.js +35 -12
  29. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  30. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  31. package/template/coding-task-manager/lib/api/machines.js +461 -0
  32. package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
  33. package/template/coding-task-manager/lib/api/providers.js +53 -6
  34. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  35. package/template/coding-task-manager/lib/api/sessions.js +160 -8
  36. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  37. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  38. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  39. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  40. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  41. package/template/coding-task-manager/lib/auth-context.js +32 -2
  42. package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
  43. package/template/coding-task-manager/lib/auth-rules.js +186 -45
  44. package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
  45. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  46. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  47. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  48. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  49. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  50. package/template/coding-task-manager/lib/client-diag-handler.js +5 -0
  51. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  52. package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
  53. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  54. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  55. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  56. package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
  57. package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
  58. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  59. package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
  60. package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
  61. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  62. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  63. package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
  64. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  65. package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
  66. package/template/coding-task-manager/lib/db-domains.js +21 -2
  67. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  68. package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
  69. package/template/coding-task-manager/lib/feature-manifest.js +1 -1
  70. package/template/coding-task-manager/lib/fs-watcher.js +3 -2
  71. package/template/coding-task-manager/lib/harness-history-transition.js +3 -3
  72. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  73. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  74. package/template/coding-task-manager/lib/headless-term-service.js +55 -17
  75. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  76. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  77. package/template/coding-task-manager/lib/host-handoff.js +51 -13
  78. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  79. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  80. package/template/coding-task-manager/lib/host-stats.js +328 -0
  81. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  82. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  83. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  84. package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
  85. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
  86. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  87. package/template/coding-task-manager/lib/instance-manifest.js +13 -1
  88. package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
  89. package/template/coding-task-manager/lib/launch-model-override.js +160 -1
  90. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  91. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
  92. package/template/coding-task-manager/lib/lead-model-switch.js +3 -2
  93. package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
  94. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  95. package/template/coding-task-manager/lib/login-tripwire.js +1 -0
  96. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  97. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  98. package/template/coding-task-manager/lib/machine-registry.js +749 -0
  99. package/template/coding-task-manager/lib/machine-self.js +66 -0
  100. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +52 -4
  101. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  102. package/template/coding-task-manager/lib/media-journal.js +84 -27
  103. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  104. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  105. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  106. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  107. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  108. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  109. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  110. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  111. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  112. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  113. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  114. package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
  115. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  116. package/template/coding-task-manager/lib/native-history-control.js +2 -2
  117. package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
  118. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  119. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
  120. package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
  121. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  122. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  123. package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
  124. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  125. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  126. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  127. package/template/coding-task-manager/lib/read-pool-client.js +5 -2
  128. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  129. package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
  130. package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
  131. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  132. package/template/coding-task-manager/lib/remote-command-runtime.js +13 -1
  133. package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
  134. package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
  135. package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
  136. package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
  137. package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
  138. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  139. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  140. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  141. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  142. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  143. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  144. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  145. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  146. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  147. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  148. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  149. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  150. package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
  151. package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
  152. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  153. package/template/coding-task-manager/lib/root-prompt-admission.js +33 -2
  154. package/template/coding-task-manager/lib/root-prompt-flow.js +167 -33
  155. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  156. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  157. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  158. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  159. package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
  160. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  161. package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
  162. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  163. package/template/coding-task-manager/lib/session-create/provider-process.js +34 -37
  164. package/template/coding-task-manager/lib/session-create/response.js +30 -12
  165. package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
  166. package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
  167. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  168. package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
  169. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  170. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  171. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  172. package/template/coding-task-manager/lib/session-host-manager.js +152 -206
  173. package/template/coding-task-manager/lib/session-jobs.js +17 -9
  174. package/template/coding-task-manager/lib/session-lifecycle.js +18 -0
  175. package/template/coding-task-manager/lib/session-model-action-preconditions.js +9 -0
  176. package/template/coding-task-manager/lib/session-resource-sampler.js +19 -9
  177. package/template/coding-task-manager/lib/session-standup.js +67 -4
  178. package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
  179. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  180. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  181. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  182. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  183. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  184. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
  185. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  186. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  187. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  188. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  189. package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
  190. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  191. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  192. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  193. package/template/coding-task-manager/lib/transcript-source.js +8 -3
  194. package/template/coding-task-manager/lib/transcript-store.js +40 -0
  195. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  196. package/template/coding-task-manager/lib/typing-interaction-trace.js +92 -0
  197. package/template/coding-task-manager/lib/typing-worker-stages.js +60 -0
  198. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  199. package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
  200. package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
  201. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  202. package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
  203. package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
  204. package/template/coding-task-manager/lib/ws-router.js +68 -1
  205. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  206. package/template/coding-task-manager/oauth-proxy.js +99 -55
  207. package/template/coding-task-manager/package-lock.json +24 -0
  208. package/template/coding-task-manager/package.json +3 -0
  209. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  210. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  211. package/template/coding-task-manager/public/css/app.css +279 -1
  212. package/template/coding-task-manager/public/css/reviews.css +4 -0
  213. package/template/coding-task-manager/public/index.html +294 -223
  214. package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
  215. package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
  216. package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
  217. package/template/coding-task-manager/public/js/background-work.js +13 -7
  218. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
  219. package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
  220. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  221. package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
  222. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  223. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  224. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  225. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  226. package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
  227. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  228. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -8
  229. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  230. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  231. package/template/coding-task-manager/public/js/reviews.js +7 -3
  232. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  233. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  234. package/template/coding-task-manager/public/js/row-join.js +4 -2
  235. package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
  236. package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
  237. package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
  238. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  239. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  240. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  241. package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
  242. package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
  243. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  244. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  245. package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
  246. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  247. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  248. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -1
  249. package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
  250. package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
  251. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  252. package/template/coding-task-manager/public/m/app.css +226 -8
  253. package/template/coding-task-manager/public/m/app.js +502 -9
  254. package/template/coding-task-manager/public/m/index.html +38 -1
  255. package/template/coding-task-manager/public/m/machines.js +1313 -0
  256. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  257. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  258. package/template/coding-task-manager/public/m/sw.js +5 -0
  259. package/template/coding-task-manager/queue-engine.js +13 -0
  260. package/template/coding-task-manager/server/approvals.js +5 -6
  261. package/template/coding-task-manager/server/boot.js +136 -17
  262. package/template/coding-task-manager/server.js +902 -239
  263. package/template/coding-task-manager/session-integrity.js +7 -9
  264. package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
  265. package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
  266. package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
  267. package/template/coding-task-manager/workers/session-host-process.js +14 -3
  268. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  269. package/template/coding-task-manager/workers/worktree-worker.js +34 -0
  270. package/template/package-lock.json +2 -2
  271. package/template/package.json +1 -1
  272. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  273. package/template/shared/conversation-storage-v2-contract.js +42 -6
  274. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  275. package/template/shared/git-remote-key.js +83 -0
  276. package/template/shared/host-identity.js +98 -2
  277. package/template/shared/owner-write-safety.js +135 -0
  278. package/template/shared/public-runtime.js +13 -5
  279. package/template/shared/release-schema.json +3 -2
  280. package/template/shared/report-citations.js +14 -0
  281. package/template/shared/sqlite-write-lock.js +361 -223
  282. package/template/shared/storage/contract.js +35 -3
  283. package/template/wall-e/agent.js +21 -0
  284. package/template/wall-e/api-walle.js +5 -0
  285. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  286. package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
  287. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  288. package/template/wall-e/brain/migrations.js +51 -22
  289. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  290. package/template/wall-e/brain/owner-call-whitelist.js +16 -0
  291. package/template/wall-e/brain/schema-ensures.js +22 -0
  292. package/template/wall-e/brain.js +1 -1
  293. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  294. package/template/wall-e/coding/session-journal.js +28 -40
  295. package/template/wall-e/coding-orchestrator.js +23 -26
  296. package/template/wall-e/http/auth.js +18 -1
  297. package/template/wall-e/http/blog-views-api.js +318 -0
  298. package/template/wall-e/http/helper-rules-api.js +321 -0
  299. package/template/wall-e/http/model-proxy-providers.js +3 -1
  300. package/template/wall-e/http/telemetry-api.js +12 -0
  301. package/template/wall-e/lib/blog-view-policy.js +88 -0
  302. package/template/wall-e/lib/blog-view-store.js +169 -0
  303. package/template/wall-e/lib/client-config-lock.js +2 -1
  304. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  305. package/template/wall-e/lib/error-kinds.js +10 -1
  306. package/template/wall-e/lib/feature-manifest.js +1 -1
  307. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  308. package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
  309. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  310. package/template/wall-e/lib/helper-rules/grade.js +277 -0
  311. package/template/wall-e/lib/helper-rules/label.js +247 -0
  312. package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
  313. package/template/wall-e/lib/helper-rules/store.js +667 -0
  314. package/template/wall-e/lib/mcp-integration.js +5 -6
  315. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  316. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
  317. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  318. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  319. package/template/wall-e/llm/default-fallback.js +4 -0
  320. package/template/wall-e/llm/model-catalog.js +13 -4
  321. package/template/wall-e/llm/model-roles.js +6 -0
  322. package/template/wall-e/llm/portkey-http.js +150 -0
  323. package/template/wall-e/llm/portkey-sync.js +15 -145
  324. package/template/wall-e/llm/worker-mcp-routing.js +61 -0
  325. package/template/wall-e/llm/worker-route.js +43 -1
  326. package/template/wall-e/loops/brain-custom-writes.js +45 -0
  327. package/template/wall-e/mcp-protocol-contract.js +41 -1
  328. package/template/wall-e/mcp-server.js +13 -2
  329. package/template/wall-e/mcp-worker-schemas.js +20 -2
  330. package/template/wall-e/memory/ctm-context-client.js +25 -5
  331. package/template/wall-e/shared/release-schema.json +3 -2
  332. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  333. package/template/wall-e/shared/storage/contract.js +35 -3
  334. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  335. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
  336. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
  337. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
  338. package/template/wall-e/tools/local-tools.js +6 -4
  339. package/template/wall-e/workers/brain-owner-worker.js +9 -0
  340. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  341. package/template/wall-e/workers/telemetry-worker.js +8 -0
@@ -36,7 +36,8 @@ process.env.CTM_PROCESS_ROLE = process.env.CTM_PROCESS_ROLE || 'ctm-server';
36
36
  // get a second tick to retry (see shared/storage/PERF_SPEC_R6.md V2).
37
37
  process.env.CTM_DB_SHADOW_REPLAY = process.env.CTM_DB_SHADOW_REPLAY || '1';
38
38
  const serverConfig = require('./lib/server-config');
39
- const { buildMediaStoreOptions } = require('./lib/media-store-options');
39
+ const { prepareMainMediaStore, createMediaJournalProjector } = require('./lib/media-store-options');
40
+ const { createLegacyDraftCancellationRecovery } = require('./lib/legacy-draft-cancellation-recovery');
40
41
  const { runLegacyDraftRequestCancellation } = require('./lib/terminal-draft-request-cancellations');
41
42
  try { require('./lib/timestamped-output').installTimestampedOutput(); } catch {}
42
43
 
@@ -66,7 +67,7 @@ const { DURABLE_KINDS } = require('../shared/conversation-admission');
66
67
  const { scanTuiInputModes, tuiModesChanged } = require('./lib/tui-input-modes');
67
68
  const { resumeFirstFrameWatchdogDecision } = require('./lib/resume-firstframe-watchdog');
68
69
  const { waitProjection } = require('./lib/session-wait-projection');
69
- const { normalizeStoreTurns, resumeBlockedWire, sendResumeBlockedToClients } = require('./lib/resume-blocked-wire');
70
+ const { normalizeStoreTurns, resumeBlockedRungWire, resumeBlockSetDetails, resumeBlockedWire, sendResumeBlockedToClients } = require('./lib/resume-blocked-wire');
70
71
  const { persistCodexStartupResumeFromLink } = require('./lib/session-create/codex-startup-pointer');
71
72
  const {
72
73
  resumeHardCeilingEnabled,
@@ -102,6 +103,7 @@ const {
102
103
  stripRepliesStateful,
103
104
  writeWithDeviceReplyFirewall,
104
105
  } = require('./lib/terminal-query-protocol');
106
+ const { observePtyWrite: _observePtyWrite } = require('./lib/hosted-input-dispatch');
105
107
  const {
106
108
  classifyPtyChunk,
107
109
  ptyRuntimeActivityDecision,
@@ -453,7 +455,7 @@ if (_shadowBootSafetyMs > 0) {
453
455
  if (t && typeof t.unref === 'function') t.unref();
454
456
  }
455
457
 
456
- const { mirrorSessionTitle, fillClaudeAgentTitle } = require('./lib/session-title-mirror');
458
+ const { mirrorSessionTitle, applyProviderTitle } = require('./lib/session-title-mirror');
457
459
  const streamResizePolicy = require('./public/js/stream-resize-policy');
458
460
  const { turnSettleQuietResizeAction, deferredResizeReleaseDecision } = require('./lib/deferred-resize-quiet-policy');
459
461
  const { rowsFlapResizeDecision, DEBOUNCE_MS: ROWS_FLAP_DEBOUNCE_MS, MAX_HOLD_MS: ROWS_FLAP_MAX_HOLD_MS } = require('./lib/rows-flap-debounce');
@@ -649,6 +651,8 @@ const { isCrashCapTripped: _isHostCrashCapTripped } = require('./lib/host-crash-
649
651
  // _shutdownOrphanHost below).
650
652
  const { createHostHealthLedger, readStallFromDataDir } = require('./lib/host-health-ledger');
651
653
  const { createReviveUnresponsiveHost } = require('./lib/host-health-revive');
654
+ // WP-3 (stale-pty-host-evacuation design doc, §6): evacuate first, revive second.
655
+ const { createEvacuateStalledHost, evacuateStalledAdoptedHosts } = require('./lib/host-threadpool-evacuation');
652
656
  const {
653
657
  createPtyStartupReservation,
654
658
  ptyStartupCommitComplete,
@@ -697,6 +701,7 @@ const _typingInteractionTraces = createTypingInteractionTraceStore({
697
701
  publicLimit: 128,
698
702
  ttlMs: 60_000,
699
703
  getRollupExtras: () => buildRollupExtras(_loadPressure.getStatus()), // WP-0 D-4/D-5: report-only
704
+ mainSpansForWindow: (startMs, endMs) => _mainSpansForWindow(startMs, endMs), // WP-4 r4 R-8: report-only
700
705
  // D-1 (composer-typing-freeze-keystroke-slo-2026-09-11.html §5 D-1, §8 WP-0): persist the
701
706
  // keystroke-to-paint distribution past the 60s in-memory ring. _eventLoopIncidents is
702
707
  // resolved lazily here (this callback only ever runs later, on the flush timer below) even
@@ -1505,17 +1510,7 @@ _perfTracker.namedInterval('timer:visible-revision-watchdog', () => {
1505
1510
  recordDiagnostic: recordSessionDiagnostic,
1506
1511
  coalescer: _visibleRevisionMarginalCoalescer,
1507
1512
  });
1508
- const session = sessions.get(incident.sessionId);
1509
- const target = (session?.clients || []).find((client) => (
1510
- client.clientId === incident.clientId
1511
- && Number(client.socketGeneration || 0) === Number(incident.socketGeneration || 0)
1512
- ));
1513
- if (target?.readyState !== 1 || incident.recoveryAction === 'report-only') continue;
1514
- // WP-3b (delivery-path reset): the 'fence-generation' recovery action used to fence the
1515
- // client here (server-side belief the client's baseline generation is invalid); the client
1516
- // fence is deleted, so this incident now only ever asks for a keyframe, same as any other
1517
- // recovery action.
1518
- _requestKeyframeGrant(target, incident.sessionId, { priority: true });
1513
+ // Report-only (lib/visible-revision-watchdog.js): a late boundary never forces a keyframe.
1519
1514
  }
1520
1515
  }, 25).unref?.();
1521
1516
  // WP-3b (delivery-path reset): the fence-breaker sweep (lib/state-sync/fence-breaker.js) existed
@@ -1996,6 +1991,7 @@ const { splitMirrorAndBrowserChunks } = require('./lib/mirror-feed-sanitize');
1996
1991
  const {
1997
1992
  buildSessionStandupSnapshot,
1998
1993
  isWalleOwnedSession,
1994
+ remoteStandupSession,
1999
1995
  selectUnmatchedStartupTaskPlaceholders,
2000
1996
  isStandupStartupTaskSeedEnabled,
2001
1997
  } = require('./lib/session-standup');
@@ -2084,7 +2080,7 @@ const {
2084
2080
  const {
2085
2081
  createCodingAgentModelSync,
2086
2082
  resolveModelToRegistry,
2087
- shouldApplyCodexAutoTitle,
2083
+ getCodexThreadAgentName,
2088
2084
  } = require('./lib/coding-agent-models');
2089
2085
  const { MODEL_RESULT_PAGE_SIZE, MAX_MODEL_RESULT_BYTES } = require('./lib/coding-session-model-projection'); const { createCodingSessionModelRuntime } = require('./lib/coding-session-model-runtime');
2090
2086
  const { createCodingSessionGenerationRuntime } = require('./lib/coding-session-generation-runtime');
@@ -2141,6 +2137,10 @@ const { evaluateWaitState, shouldClearWaitCard, deriveChoiceLabel, shouldSuperse
2141
2137
  const { selectionCardForViewport } = require('./lib/terminal-interaction-policy');
2142
2138
  const promptIntent = require('./lib/prompt-intent');
2143
2139
  const { canResumeAgent, getAgentCapabilities, getAgentSurfaceIds, normalizeAgentType } = require('./lib/agent-capabilities');
2140
+ // WP-4 (docs/remote-hub-followups-credential-projects-rows-idempotency-2026-10-23.html §5 D-12): the
2141
+ // SAME `created`-frame builder a fresh spawn uses, so a duplicate create is re-answered with one
2142
+ // frame shape, never a second one hand-rolled here.
2143
+ const { buildCreatedSessionPayload } = require('./lib/coding-session-model-public');
2144
2144
  const { classifyResumeNoise, resumeFailureDetail } = require('./lib/resume-noise');
2145
2145
  const { createResumeAttemptTracker } = require('./lib/resume-attempts');
2146
2146
  const {
@@ -2190,6 +2190,60 @@ const resumeFromStoreApi = require('./lib/api/resume-from-store');
2190
2190
  const servicesApi = require('./lib/api/services'); const devFaultApi = require('./lib/api/dev-fault');
2191
2191
  const setupNetworkApi = require('./lib/api/setup-network'); const runtimeApprovalsApi = require('./lib/api/runtime-approvals');
2192
2192
  const approvalDeliveryActionApi = require('./lib/api/approval-delivery-action');
2193
+ // WP-0 (remote-machine-hub-sessions-2026-10-22.html §6.1): the machine registry — routes in
2194
+ // lib/api/machines.js, policy/probe loop in lib/machine-registry.js.
2195
+ const machinesApi = require('./lib/api/machines');
2196
+ const machineRegistry = require('./lib/machine-registry');
2197
+ // WP-1 (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.1): this machine's own host
2198
+ // stats. The sampler is ONE per process and is fed by the existing 60s resource tick (no new
2199
+ // timer); lib/api/host-stats.js is the read surface the picker and the hub's ?machine= proxy use.
2200
+ const { createHostStats } = require('./lib/host-stats');
2201
+ const hostStatsApi = require('./lib/api/host-stats');
2202
+ // WP-6 (remote-machine-hub-sessions-2026-10-22.html §6.6/S-3/S-4): the remote catalog. It is the
2203
+ // thin seam behind the two session-list projections below and the resume-on-origin branch; the
2204
+ // lookup, the merge and the wire fields all live in lib/remote-machine-catalog.js.
2205
+ const remoteMachineCatalog = require('./lib/remote-machine-catalog');
2206
+ // WP-6: the catalog's peers are wired HERE. Each of them transitively requires the catalog (the
2207
+ // link's relay hook, the create post-hook, the sync worker's probe hook all call into it), so the
2208
+ // catalog cannot `require` them without closing a lib/** dependency cycle (lint ceiling: 0). The
2209
+ // getters keep them lazy, so this changes no load order — the modules load on the catalog's first
2210
+ // use exactly as they did through the removed requires.
2211
+ // WP-3 (remote-hub-followups D-9/D-10): the catalog's change signal (lib/remote-machine-catalog.js
2212
+ // `_notifyChanged` — a box session-list change, or a sweep that flipped a machine's health). The
2213
+ // WS re-broadcast already rides it (`onChange` below); these two additions invalidate the HTTP
2214
+ // projections that never call `mergeSessionRows`: the materialized STANDUP snapshot (the phone home
2215
+ // list is built ONLY from /api/sessions/standup, so a box card would otherwise never appear or
2216
+ // disappear for the phone) and the published recent catalog (whose next build reads a fresh
2217
+ // `machineTagInputs()`). Recent rows come from the LOCAL DB and their tags depend only on the
2218
+ // registry, so the recent catalog is rebuilt only when the tag inputs change — never on every
2219
+ // probe sweep's box session-list refresh (a full read-pool rebuild per sweep).
2220
+ let _lastRecentMachineTagSignature = null;
2221
+ function _onRemoteMachineCatalogChanged() {
2222
+ _markStandupMaterializedDirty('remote-catalog');
2223
+ const signature = JSON.stringify(remoteMachineCatalog.machineTagInputs());
2224
+ if (signature === _lastRecentMachineTagSignature) return;
2225
+ _lastRecentMachineTagSignature = signature;
2226
+ _advanceRecentCatalogGeneration();
2227
+ }
2228
+
2229
+ remoteMachineCatalog.configureProduction({
2230
+ link: () => require('./lib/remote-machine-link'),
2231
+ create: () => require('./lib/remote-machine-create'),
2232
+ proxy: () => require('./lib/remote-machine-proxy'),
2233
+ originFacts: () => require('./lib/resume-origin-facts'),
2234
+ db: () => require('./db-owner-client'),
2235
+ // debounced; an unchanged merge is elided by its signature. The getter is called exactly once per
2236
+ // _notifyChanged, so marking the standup snapshot and the recent catalog dirty here changes no
2237
+ // catalog notify semantics.
2238
+ onChange: () => { _onRemoteMachineCatalogChanged(); return broadcastSessionList; },
2239
+ // 2026-10-23 relay-hijack guard (lib/remote-machine-catalog.js _localSessionLookup): a session this
2240
+ // instance holds locally — live or a deferred boot placeholder — is never a box mirror.
2241
+ localSession: () => (ctmSessionId) => !!getSession(ctmSessionId),
2242
+ // WP-1 ingest fence (D-1/D-2): the HUB's own ownership truth — its instance id and its lineage
2243
+ // closure — which no row a box sends can forge. A missing/throwing peer fails OPEN.
2244
+ localInstanceId: () => () => dbModule.getCurrentHostedInstanceId?.() || '',
2245
+ ownerLineage: () => (instanceId) => dbModule.isSameOwnerLineage?.(instanceId) === true,
2246
+ });
2193
2247
  const { sendConditionalJson } = require('./lib/api/conditional-json'), { manifestEtagBody: _manifestEtagBody } = require('./lib/instance-manifest-runtime'); // devtunnel-egress D-7: ETag/304 for the identity reads
2194
2248
  const wsRouter = require('./lib/ws-router');
2195
2249
  const { runWsMessageWithTrace } = require('./lib/ws-causal-context');
@@ -2490,7 +2544,7 @@ const {
2490
2544
  loadConfig,
2491
2545
  ensureWalleToken,
2492
2546
  } = serverConfig;
2493
- const providerLoginBroker = new ProviderLoginBroker({ configDir: CONFIG_DIR, createSession: (m) => handleCreatePrepared(null, m), getSession: (id) => getSession(id), resolveCli: (bin) => resolveAgentCliCommand(bin) }); // WP-1: sign-in opens a real terminal tab via the prepared-create entry point; all three args are hoisted functions, resolved at call time
2547
+ const providerLoginBroker = new ProviderLoginBroker({ configDir: CONFIG_DIR, createSession: (m) => handleCreatePrepared(null, m), getSession: (id) => getSession(id), resolveCli: (bin) => resolveAgentCliCommand(bin), writeToLoginTab: (s, d) => _writeLifecyclePty(s, d, 'provider-login-code') }); // WP-1: sign-in opens a real terminal tab via the prepared-create entry point; all args are hoisted functions, resolved at call time. The code-return write uses source 'provider-login-code', so it is CTM-generated (never 'input') and cannot move the user-recency clocks
2494
2548
  const bestOfAttempts = createBestOfAttemptService({
2495
2549
  statePath: path.join(CONFIG_DIR, 'best-of-attempts.json'),
2496
2550
  launchAttempt: _launchBestOfAttempt,
@@ -2664,9 +2718,9 @@ const _diagBatcher = createSessionDiagnosticBatcher({
2664
2718
  },
2665
2719
  flush: (batch) => {
2666
2720
  // Off-thread: route the durable batch write through the db-owner worker (the designated
2667
- // serial writer) so the cross-process SQLite write-lock claim (_writeLockClaim's sync
2668
- // writeFileSync/linkSync) never blocks the main event loop — it was a ~110ms main-thread
2669
- // block under multi-session contention. Best-effort: the in-memory ring still serves the UI.
2721
+ // serial writer) so the cross-process SQLite write lock (and any wait for it) never blocks
2722
+ // the main event loop — the old link-claim lock was a ~110ms main-thread block under
2723
+ // multi-session contention. Best-effort: the in-memory ring still serves the UI.
2670
2724
  _runCtmDbWorkerOp('flushSessionDiagnostics', { entries: batch }, { bestEffort: true })
2671
2725
  .catch(() => {});
2672
2726
  },
@@ -4886,7 +4940,7 @@ async function _launchBestOfAttempt(spec) {
4886
4940
  const created = await handleCreatePrepared(null, {
4887
4941
  id: sessionId,
4888
4942
  title,
4889
- label: title,
4943
+ label: title, titleIntent: 'default', // D-13: `_bestOfAttemptTitle` is CTM-composed (tier 3), never a user name
4890
4944
  cmd: source.cmd || agentType,
4891
4945
  args: bestOfFreshAgentArgs(agentType, source.args, { approvalHookToken: source._approvalHookToken || '' }),
4892
4946
  cwd: workspace.cwd,
@@ -7474,19 +7528,11 @@ function _applyBootRepairsResult(repairs) {
7474
7528
  const timer = setTimeout(continueWalleIdentityRekey, 250);
7475
7529
  timer.unref?.();
7476
7530
  }
7477
- const repairedTitles = repairs?.titleRepair;
7478
- if (repairedTitles?.promoted) {
7479
- console.log(`[session-title] promoted ${repairedTitles.promoted} active startup title(s) into ctm_sessions.title`);
7480
- telemetry.track('session_title_startup_repair', {
7481
- promoted: repairedTitles.promoted,
7482
- scanned: repairedTitles.scanned || 0,
7483
- });
7484
- }
7485
7531
  return _refreshControlSnapshot({ reason: 'db-owner-ready' });
7486
7532
  }
7487
7533
 
7488
7534
  // WP-B / B1 (2026-07-31 boot-wedge outage): runBootRepairs performs REPAIRS
7489
- // (session-title repair, codex identity/metadata repair, conversation-source
7535
+ // (codex identity/metadata repair, conversation-source
7490
7536
  // migration, order-key backfill, Wall-E identity rekey) — none of it is
7491
7537
  // load-bearing for serving HTTP. The old code awaited this with no
7492
7538
  // try/catch, so a single rejection (that night: a worker-request timeout)
@@ -7853,7 +7899,10 @@ const sessionHostManager = createSessionHostManager({
7853
7899
  // WP-0 (pty-host-threadpool-stranded-waiters-2026-09-25.html, D-3/D-6): report-only
7854
7900
  // seam for the pool/dedicated-host threadpool monitor's frames -- see
7855
7901
  // _recordThreadpoolDiagnostic above and apiServicesStatus's golden-signals feed below.
7856
- onThreadpoolReport: (snapshot) => _recordThreadpoolDiagnostic(snapshot),
7902
+ // WP-3 (§9 S-7): the record-only sink becomes the escalation's ingress too. `hostHealth` and
7903
+ // `_evacuateStalledHostSafe` are declared later in module evaluation -- this callback only ever
7904
+ // runs after it completes, and the function declaration below hoists.
7905
+ onThreadpoolReport: (snapshot) => { _recordThreadpoolDiagnostic(snapshot); const v = hostHealth.recordThreadpool(snapshot && snapshot.hostId, snapshot); if (v) _evacuateStalledHostSafe(v); },
7857
7906
  // WP-1 (pty-host-threadpool-stranded-waiters-2026-09-25.html, D-2): the manager has no
7858
7907
  // DB/session access of its own -- forward every input-ack frame to the one function
7859
7908
  // that turns it into typing-trace/durable-phase truth.
@@ -7861,7 +7910,7 @@ const sessionHostManager = createSessionHostManager({
7861
7910
  // Probe (mouse-mode handoff): host-reported per-session diagnostic rows (host-handoff-mirror-seed/-restored).
7862
7911
  onSessionDiagnostic: (sessionId, event, details) => recordSessionDiagnostic(sessionId, event, details),
7863
7912
  });
7864
- _ownerRecovery = require('./lib/surviving-owner-bootstrap').createSurvivingOwnerBootstrap({ dataDir: require('./lib/data-dir').getDataDir(), bootId: _serverBootId, manager: sessionHostManager, getTasks: () => dbModule.listStartupTasks(), getSession: (id) => sessions.get(id), send: (ws, reply) => _wsSend(ws, JSON.stringify(reply)) });
7913
+ _ownerRecovery = require('./lib/surviving-owner-bootstrap').createSurvivingOwnerBootstrap({ dataDir: require('./lib/data-dir').getDataDir(), bootId: _serverBootId, manager: sessionHostManager, getTasks: () => dbModule.listStartupTasks(), getSession: (id) => sessions.get(id), send: (ws, reply) => _wsSend(ws, JSON.stringify(reply)), onSkip: _restoreWakeSkipSink }); // WP-3 (§9 S-20): wakeSkips('OwnerRecoveryFence'); the fence verdict is untouched
7865
7914
  if (serverConfig.CTM_SESSION_HOST_PROCESS !== '0' && process.env.CTM_SESSION_HOST_POOL_PREWARM !== '0') {
7866
7915
  try { sessionHostManager.prewarmPool?.(); }
7867
7916
  catch (e) { console.error('[session-host-pool] prewarm error:', e.message); }
@@ -7872,8 +7921,16 @@ if (serverConfig.CTM_SESSION_HOST_PROCESS !== '0' && process.env.CTM_SESSION_HOS
7872
7921
  // process globals inside lib/host-health-ledger.js itself); its default
7873
7922
  // D-2(b) input is the WP-0 stall file a stalled pool host writes for its
7874
7923
  // own hostId.
7924
+ // WP-3 (session-stops-mid-job-2026-10-24.html, §5 D-10/D-11): liveness is "the
7925
+ // host sent any frame". The manager's own per-host frame clock is this ledger's
7926
+ // one liveness input: a hard serialize miss on a host whose last frame is newer
7927
+ // than the request was issued is 'degraded' (readiness -- reported, never
7928
+ // killed), and only a host silent since the first missed request reaches
7929
+ // 'unresponsive'.
7875
7930
  const hostHealth = createHostHealthLedger({
7876
7931
  readStall: (hostId) => readStallFromDataDir(require('./lib/data-dir').getDataDir(), hostId),
7932
+ onEvacuate: (v) => _evacuateStalledHostSafe(v), // WP-3 (§9 S-7): the armed-timer half of the same escalation
7933
+ lastFrameAt: (hostId) => sessionHostManager.lastFrameAt(hostId),
7877
7934
  });
7878
7935
  // Final-review fix I6: a host that exits (normally OR by crash) must leave the
7879
7936
  // ledger, or its miss history outlives it -- and a later host reusing the same
@@ -10456,41 +10513,25 @@ setDbMaintenanceRunner(runOnlineDbBackup);
10456
10513
  setImageMetricsRecorder(event => _uploadMetrics.recordUpload(event));
10457
10514
  // WP-0 §6.3/S-8: tpOldestMs on the [img-upload] line comes from the main-process monitor above.
10458
10515
  setThreadpoolStatusReader(() => _mainThreadpoolMonitor.snapshot());
10516
+ const _legacyDraftCancellationRecovery = createLegacyDraftCancellationRecovery({
10517
+ getDirectory: () => path.join(serverConfig.CTM_DATA_DIR || path.dirname(dbModule.getDbPath()), 'media-journal'),
10518
+ getContext: () => _ctmDbClosingForProcessExit ? null : dbModule.getDb(),
10519
+ projectEntry: createMediaJournalProjector(_runCtmDbWorkerOp),
10520
+ });
10459
10521
  async function _prepareMediaStore() {
10460
- if (!_mediaWriteBehindEnabled) return null;
10522
+ if (!_mediaWriteBehindEnabled) { await _legacyDraftCancellationRecovery.prepare(); return null; }
10461
10523
  if (_mediaStore) return _mediaStore;
10462
10524
  if (_mediaStorePromise) return _mediaStorePromise;
10463
- _mediaStorePromise = (async () => {
10464
- const registry = await _runCtmDbWorkerOp('loadMediaStoreRegistry', {}, {
10465
- // Listener activation depends on this registry. It is boot-critical
10466
- // maintenance, while the journal projections below remain background work.
10467
- priority: 'high',
10468
- requestClass: 'maintenance',
10469
- timeoutMs: 30_000,
10470
- });
10471
- return createMediaStore(buildMediaStoreOptions({
10472
- dataDir: serverConfig.CTM_DATA_DIR || path.dirname(dbModule.getDbPath()),
10473
- bootId: _serverBootId, registry, assertDraftOwner: _assertTerminalDraftOwnerAsync,
10525
+ _mediaStorePromise = prepareMainMediaStore({
10526
+ createStore: createMediaStore, getDataDir: () => serverConfig.CTM_DATA_DIR || path.dirname(dbModule.getDbPath()),
10527
+ bootId: _serverBootId, assertDraftOwner: _assertTerminalDraftOwnerAsync,
10474
10528
  runDbWorkerOp: _runCtmDbWorkerOp,
10475
10529
  onDrainProgress: () => { try { _loadPressure.recordProducerProgress('media-journal'); } catch {} },
10476
- // WP-1 (docs/image-draft-event-ordering-and-verified-adoption-2026-10-14.html §6.4): the store
10477
- // is the single draft version allocator. It recovers the highest pending journal version and
10478
- // then applies verified owner-lineage normalization as one journaled mutation — the draft
10479
- // restamp was removed from db.js restampOwnerLineage so the two can never allocate
10480
- // concurrently.
10481
- //
10482
- // Review finding 9: the identity source is the LAZY, MEMOIZED accessor
10483
- // (lib/instance-identity-access.js), not a synchronous main-thread `dbModule.…()` read on the
10484
- // store-preparation path. getInstanceIdentity() derives at most once per process, only after
10485
- // the DB is open, and returns null instead of ever inventing a hostname/port-derived default;
10486
- // isSameOwnerLineage is the existing dbModule-free lineage snapshot used by the session-owner
10487
- // hot path. Neither is a new DB boundary — both are already the reviewed identity seams.
10488
- currentOwnerId: _instanceIdentityAccess.getInstanceIdentity({ required: false })?.instanceId
10530
+ getCurrentOwnerId: () => _instanceIdentityAccess.getInstanceIdentity({ required: false })?.instanceId
10489
10531
  || _instanceIdentityAccess.getCachedInstanceId()
10490
10532
  || serverConfig.CTM_INSTANCE_ID || '',
10491
10533
  isSameOwnerLineage: _isSameOwnerLineageSnapshot,
10492
- }));
10493
- })().then(store => {
10534
+ }).then(store => {
10494
10535
  // Review finding 6: permanent-delete receipts observed while this store was preparing are
10495
10536
  // applied FIRST — before `_mediaStore` is assigned and therefore before any reader (or the boot
10496
10537
  // reconciler below) can observe the registry.
@@ -10513,6 +10554,7 @@ async function _prepareMediaStore() {
10513
10554
  }
10514
10555
 
10515
10556
  async function _closeMediaStore(timeoutMs = 2_000) {
10557
+ await _legacyDraftCancellationRecovery.close();
10516
10558
  const store = _mediaStore;
10517
10559
  if (!store) return;
10518
10560
  _mediaStore = null;
@@ -10534,7 +10576,8 @@ setImageSaveRunner(async ({ promptId, buffer, filename, mimeType, deferRow, requ
10534
10576
  dbModule.saveImageDecoupled(promptId, buffer, filename, mimeType, { deferRow: !!deferRow }));
10535
10577
  });
10536
10578
 
10537
- function _runLegacyTerminalDraftDbAction(action, args) {
10579
+ async function _runLegacyTerminalDraftDbAction(action, args) {
10580
+ await _legacyDraftCancellationRecovery.prepare();
10538
10581
  return _runCtmDbWorkerOp('runDbMutation', { action, args }, {
10539
10582
  priority: 'urgent',
10540
10583
  requestClass: 'user-blocking',
@@ -11749,7 +11792,11 @@ function _auditAuthDecision({ req, auth, rule, decision, route, wsType, action,
11749
11792
  function _rateLimitDecision(auth, rule, keyPath) {
11750
11793
  if (auth?.isLoopback) return { ok: true };
11751
11794
  const key = auth?.deviceId || auth?.tokenHashPrefix || 'anonymous';
11752
- const kind = rateKindForRule(rule, keyPath);
11795
+ // WP-3 (docs/remote-machine-hub-sessions-2026-10-22.html §6.4): the authenticated principal is
11796
+ // handed to the bucket selector so a `peer-control` hub relay is charged to the wide `peer`
11797
+ // bucket instead of `admin`/`mutation`. Every other caller's bucket is unchanged (rateKindForRule
11798
+ // ignores the argument without that scope).
11799
+ const kind = rateKindForRule(rule, keyPath, auth);
11753
11800
  const decision = authRateLimiter.check(kind, key);
11754
11801
  return decision.ok ? decision : { status: 429, ...decision };
11755
11802
  }
@@ -17204,22 +17251,30 @@ async function handleApi(req, res, url) {
17204
17251
 
17205
17252
  function apiGetProjects(req, res) {
17206
17253
  const claudeProjectsDir = path.join(serverConfig.HOME, '.claude', 'projects');
17207
- const projects = [];
17254
+
17255
+ const send = (projects) => {
17256
+ res.writeHead(200, { 'Content-Type': 'application/json' });
17257
+ res.end(JSON.stringify(projects));
17258
+ };
17208
17259
 
17209
17260
  if (fs.existsSync(claudeProjectsDir)) {
17210
- for (const entry of readClaudeProjectEntries(claudeProjectsDir)) {
17211
- // Claude encodes paths by replacing / with -
17212
- const projectPath = entry.startsWith('-') ? entry.replace(/-/g, '/') : entry;
17213
- projects.push({
17214
- id: entry,
17215
- path: projectPath,
17216
- exists: fs.existsSync(projectPath),
17261
+ // Remote hub follow-ups WP-2 (D-5..D-8, §9 S-8/S-9): the naive `entry.replace(/-/g, '/')`
17262
+ // decode used to name a path that does not exist for most entries (Claude encodes EVERY
17263
+ // non-alphanumeric character as '-'), and every row was listed regardless. The lossless decode,
17264
+ // the existence stat, the gate-scratch filter and the normalized origin key all run in ONE
17265
+ // worktree-worker op now (214 ms cold for 540 entries — never on main). Row shape is unchanged:
17266
+ // {id, path, exists, gitRemoteKey}, with `exists` always true because a missing path is dropped.
17267
+ _runWorktreeWorkerOp('projectRows', { entries: readClaudeProjectEntries(claudeProjectsDir) })
17268
+ .then(({ rows } = {}) => send(Array.isArray(rows) ? rows : []), (error) => {
17269
+ console.warn('[api/projects] project row decode failed:', error?.message || error);
17270
+ res.writeHead(503, { 'Content-Type': 'application/json' });
17271
+ res.end(JSON.stringify({ ok: false, error: 'projects_unavailable' }));
17217
17272
  });
17218
- }
17273
+ return;
17219
17274
  }
17220
17275
 
17221
- res.writeHead(200, { 'Content-Type': 'application/json' });
17222
- res.end(JSON.stringify(projects));
17276
+ // No ~/.claude/projects at all: today's empty answer, no worker round trip.
17277
+ send([]);
17223
17278
  }
17224
17279
 
17225
17280
  async function apiPathStatus(req, res) {
@@ -17335,6 +17390,22 @@ async function _handleClaudeWatcherNew(evt) {
17335
17390
  const db = dbModule.getDomainDb('core');
17336
17391
  const indexedOwner = (agentId) => db.prepare('SELECT agent_session_id, ctm_session_id FROM agent_sessions WHERE agent_session_id = ?').get(agentId)?.ctm_session_id || '';
17337
17392
  const owner = indexedOwner(sessionId);
17393
+ // WP-0 (D-6): every early return between here and the owner's upsert drops that row's
17394
+ // transcript link silently. Name each skip so a missing link is diagnosable.
17395
+ const linkSkipped = (reason) => {
17396
+ if (!owner) return; // only an owner (claimed) row has a link to lose
17397
+ console.log(`[jsonl-watcher] link-skipped id=${String(sessionId).slice(0, 8)} reason=${reason}`);
17398
+ recordSessionDiagnostic(owner, 'transcript-link-skipped', { reason, filePath: String(filePath || '') });
17399
+ };
17400
+ // WP-1 (D-2): the link is an identity fact known before the observation — filePath + owner.
17401
+ // Write it first so an early return below (superseded / owner-changed / observation-null)
17402
+ // can no longer drop the row's transcript link. projectPath is deliberately omitted here:
17403
+ // upsertSession COALESCEs an empty project_path, so the metadata write below still owns it
17404
+ // (and this stays free of the decode path).
17405
+ if (owner) {
17406
+ await dbModule.upsertSession(owner, { agentSessionId: sessionId, provider: 'claude',
17407
+ jsonlPath: filePath, hostname: HOSTNAME });
17408
+ }
17338
17409
  const collect = () => {
17339
17410
  const candidates = [];
17340
17411
  for (const [id, session] of sessions) {
@@ -17362,8 +17433,8 @@ async function _handleClaudeWatcherNew(evt) {
17362
17433
  ctmSessionId: id, cwd: session.cwd || '', createdAt: session.createdAt,
17363
17434
  })) },
17364
17435
  });
17365
- if (!prepared) return;
17366
- if (indexedOwner(sessionId) !== owner) return;
17436
+ if (!prepared) { linkSkipped('observation-null'); return; }
17437
+ if (indexedOwner(sessionId) !== owner) { linkSkipped('owner-changed'); return; }
17367
17438
  const { observation, projectPath, parsed } = prepared;
17368
17439
  const metadata = () => currentMetadata() ? {
17369
17440
  fileSize: observation.fileSize, compactBakSize: observation.compactBakSize, modifiedAt: observation.modifiedAt,
@@ -17372,7 +17443,7 @@ async function _handleClaudeWatcherNew(evt) {
17372
17443
  if (currentMetadata()) _serverCompactBakSizeCache.seed(new Map([[observation.bak.path, observation.bak]]));
17373
17444
  };
17374
17445
  if (owner) {
17375
- if (!currentMetadata()) return;
17446
+ if (!currentMetadata()) { linkSkipped('superseded'); return; }
17376
17447
  await dbModule.upsertSession(owner, { agentSessionId: sessionId, provider: 'claude', projectPath,
17377
17448
  jsonlPath: filePath, ...metadata(), hostname: HOSTNAME });
17378
17449
  seedBak();
@@ -17404,8 +17475,7 @@ async function _handleClaudeWatcherNew(evt) {
17404
17475
  seedBak();
17405
17476
  broadcastToAll({ type: 'agent-linked', ctmSessionId: chosen.id, agentSessionId: sessionId });
17406
17477
  if (_sessionStream) _sessionStream.linkSession(chosen.id, sessionId, filePath);
17407
- void fillClaudeAgentTitle({ db: dbModule, sessionId: chosen.id, provider: 'claude', claudeSessionId: sessionId,
17408
- dir: projectPath, recordSessionDiagnostic });
17478
+ // WP-2 (D-6): the one-shot SDK title fill is deleted — the agent's own name arrives with the transcript ingest.
17409
17479
  return;
17410
17480
  }
17411
17481
  if (!parsed) return;
@@ -17431,10 +17501,8 @@ async function _handleClaudeWatcherNew(evt) {
17431
17501
  console.log(`[jsonl-watcher] Stitched compact-continuation ${sessionId.slice(0,8)} → ctm ${stitchedToCtmId.slice(0,8)}`);
17432
17502
  broadcastToAll({ type: 'agent-linked', ctmSessionId: stitchedToCtmId, agentSessionId: sessionId, stitched: true });
17433
17503
  if (_sessionStream) _sessionStream.linkSession(stitchedToCtmId, sessionId, filePath);
17434
- } else {
17435
- void fillClaudeAgentTitle({ db: dbModule, sessionId, provider: parsed.agent || 'claude', claudeSessionId: sessionId,
17436
- dir: parsed.cwd || projectPath, recordSessionDiagnostic });
17437
17504
  }
17505
+ // WP-2 (D-6/C-9): no title fill here — the continuation's own ingest resolves the predecessor's row as owner.
17438
17506
  } catch (error) {
17439
17507
  console.error('[jsonl-watcher] Error handling new file:', error.message);
17440
17508
  } finally {
@@ -17460,11 +17528,15 @@ async function _handleClaudeWatcherChange(evt, transcriptDeltaCoalescer) {
17460
17528
  const mtime = new Date(mtimeMs).toISOString();
17461
17529
  const db = dbModule.getDomainDb('core');
17462
17530
  let didShrink = false;
17531
+ let previous = null;
17463
17532
  try {
17464
- const previous = db.prepare('SELECT file_size, compact_bak_size FROM agent_sessions WHERE agent_session_id = ?').get(sessionId);
17533
+ previous = db.prepare('SELECT file_size, compact_bak_size, jsonl_path FROM agent_sessions WHERE agent_session_id = ?').get(sessionId);
17465
17534
  const priorSize = previousJsonlSizeForShrink(previous);
17466
17535
  didShrink = !!(previous && priorSize && size < priorSize);
17467
17536
  } catch { /* metadata lookup failure retains the existing non-shrink policy */ }
17537
+ // WP-1 (D-3): re-link on every file-change while the row has no link. A file-change is proof
17538
+ // the transcript exists, so an unlinked row is repaired here even if its file-new was dropped.
17539
+ const relink = previous && !String(previous.jsonl_path || '').trim() ? filePath : '';
17468
17540
  const statPath = claudeDesktopSessions.sourcePathForStat(filePath);
17469
17541
  if (didShrink) _serverCompactBakSizeCache.clear(filePath, statPath);
17470
17542
  if (_serverCompactBakSizeCache.peekFresh(filePath, statPath) === undefined) {
@@ -17479,9 +17551,14 @@ async function _handleClaudeWatcherChange(evt, transcriptDeltaCoalescer) {
17479
17551
  if (!currentMetadata()) return;
17480
17552
  const fileMeta = _claudeEffectiveFileMeta(filePath, size || 0);
17481
17553
  await _runCtmDbWorkerOp('updateAgentSessionFileMetadata', {
17482
- sessionId, fileSize: fileMeta.fileSize, compactBakSize: fileMeta.compactBakSize, modifiedAt: mtime,
17554
+ sessionId, fileSize: fileMeta.fileSize, compactBakSize: fileMeta.compactBakSize, modifiedAt: mtime, jsonlPath: relink,
17483
17555
  });
17484
17556
  if (!currentMetadata()) return;
17557
+ // WP-1 (D-3b): a no-jsonl-path park is the exact debt this write resolves — but only when the
17558
+ // row now has a link (just written, or already present). Other park causes stay parked.
17559
+ const debt = _conversationFreshnessDebt.get(sessionId);
17560
+ if (debt && debt.parked === true && /no-jsonl-path|E_NO_JSONL_PATH/.test(String(debt.cause || ''))
17561
+ && (relink || String(previous?.jsonl_path || '').trim())) _clearConversationFreshnessDebt(sessionId);
17485
17562
  publishWatcherFileChange({ sourceId: sessionId, ctmSessionId: sessionId, fileSize: fileMeta.fileSize,
17486
17563
  mtimeMs: Number(mtimeMs || 0) });
17487
17564
  if (didShrink && filePath && fileMeta.compactBakSize > 0) {
@@ -17781,6 +17858,10 @@ function _primaryApiAgentSession(row, agentSessions) {
17781
17858
  return agentSessions.find(agent => agent.agentSessionId === primaryId) || agentSessions[0];
17782
17859
  }
17783
17860
 
17861
+ // WP-3 (D-10): the ONE machine-tag rule for a recent row, shared with the CPU/worker projection
17862
+ // (lib/recent-catalog-query.js) so the legacy and worker outputs can never disagree.
17863
+ const { machineFactsForRecentRow: _machineFactsForRecentRow } = require('./lib/recent-catalog-query');
17864
+
17784
17865
  function _recentSessionFromDbRow(row, options = {}) {
17785
17866
  const agentSessions = _apiAgentSessionsFromDbRow(row, options);
17786
17867
  const primaryAgent = _primaryApiAgentSession(row, agentSessions);
@@ -17833,6 +17914,9 @@ function _recentSessionFromDbRow(row, options = {}) {
17833
17914
  parentAgentSessionId: row.parent_agent_session_id || primaryAgent?.parentAgentSessionId || '',
17834
17915
  agentNickname: row.agent_nickname || primaryAgent?.agentNickname || '',
17835
17916
  agentRole: row.agent_role || primaryAgent?.agentRole || '',
17917
+ // WP-3 (D-10): the machine tag, only when this row's execution host is a registered box — no
17918
+ // machines registered ⇒ no key ⇒ byte-identical to the pre-WP-3 projection (AC-3a).
17919
+ ...(_machineFactsForRecentRow(row, options) || {}),
17836
17920
  };
17837
17921
  return sessionEmpty.applyRecentSessionEmptyState(session, row);
17838
17922
  }
@@ -18175,6 +18259,9 @@ function _scheduleRecentCatalogBuild() {
18175
18259
  hostname: HOSTNAME,
18176
18260
  dbPath: dbModule.getDbPath(),
18177
18261
  liveSessionIds: [...sessions.keys()],
18262
+ // WP-3 (D-10): main's cheap registry snapshot, so the worker can stamp machine/machineName
18263
+ // without any registry read of its own (the read-pool worker has no registry).
18264
+ machineTags: remoteMachineCatalog.machineTagInputs(),
18178
18265
  }, {
18179
18266
  timeoutMs: Math.max(5000, Number(process.env.CTM_RECENT_CATALOG_BUILD_TIMEOUT_MS) || 30_000),
18180
18267
  telemetryReason: 'recent-catalog-publish',
@@ -18363,6 +18450,9 @@ async function _mapRecentSessionRows(rows) {
18363
18450
  hostname: HOSTNAME,
18364
18451
  sliceMs: _SESSION_CATALOG_SLICE_MS,
18365
18452
  liveSessionIds: new Set(sessions.keys()),
18453
+ // WP-3 (D-10): this is the CPU projection the legacy path serves and persists from, so it
18454
+ // stamps the same machine tag the read-pool worker does (both from main's registry snapshot).
18455
+ machineTags: remoteMachineCatalog.machineTagInputs(),
18366
18456
  });
18367
18457
  }
18368
18458
  // Shared read-pool boundary. onFail must serve cache/empty/degraded state; it must never run a
@@ -19941,6 +20031,7 @@ const _statusBindings = statusRuntime.createBindings({
19941
20031
  restoreStarting: (session, now) => restoreRuntime.restoreStartingActive(session, now) || (_restoreInProgress && session._restoredFromStartup),
19942
20032
  isLiveTerminal: (session) => _isLiveTerminalSession(session),
19943
20033
  hasPendingWake: (session, now) => _sessionHasPendingScheduledWake(session, now),
20034
+ taskOutputPathFor: (s, id) => statusRuntime.claudeTaskOutputPathFor(s, id, { uid: process.getuid(), encodeClaudeProjectEntry }),
19944
20035
  fsp: fs.promises, backgroundStore: require('./lib/status-background-store').createStore({ dir: path.join(require('./lib/data-dir').getDataDir(), 'status-background') }), isAdopted: (s) => s.ptyProcess?._adopted === true, pidOf: (s) => s.pid,
19945
20036
  processEvidence: require('./lib/env-flags').boolFlag('CTM_STATUS_PROCESS_EVIDENCE', true), sampler: _sessionResourceSampler, resourceRoots: _lifecycleResourceRoots, liveSessions: _liveLifecycleSessions, getSession: (id) => sessions.get(id), baselineMs: () => _lifecycleConfig.resourceSampleMs,
19946
20037
  dropWaitFingerprint: (s) => _waitStateDelete(s.id, s), probeIdle: (s) => checkIdleNotify(s.id, s, '', 250), clearWaitFields: (s) => _clearServerWaitingForInput(s.id, s), isBlockingWaitReason: (r) => _standupIsBlockingWaitingReason(r), evaluateWait: (id, opts) => reconcileSessionWaitState(id, opts).catch((error) => { errorSink.report('wait-reconcile:evaluate', error, { sessionId: id }); return { read: 'failed', kind: '', raised: false, cleared: false }; }), reconcileQuietMs: () => RECONCILE_QUIET_MS, pressureLevel: () => _lifecyclePressure.level, broadcastPressure: () => _broadcastLifecyclePressureStatus(),
@@ -19952,6 +20043,47 @@ const {
19952
20043
  recordWaitCleared: _recordWaitCleared, syncWaitFields: _syncWaitFields, composerMayClose: _composerMayClose,
19953
20044
  } = _statusBindings;
19954
20045
 
20046
+ // WP-1 (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.1, D-15/D-16): the process's ONE
20047
+ // host-stats sampler. It is constructed HERE rather than beside _sessionResourceSampler because its
20048
+ // two injected readers close over the status bindings directly above and over _liveLifecycleSessions
20049
+ // (the accessor that owns the raw session map — session-access lint), so building it after them
20050
+ // removes any TDZ window rather than relying on record() only ever running post-boot.
20051
+ // countTasks (D-15): the SAME wire status the sidebar shows (liveStatus/verdictFor), never a new
20052
+ // heuristic — running ∪ stalled ∪ resuming → running, waiting_input → needsYou, idle → idle; plus
20053
+ // the worker count. A session with no status row ('' — non-live PTY, nothing running) lands in no
20054
+ // bucket, matching the pill it renders.
20055
+ // lastCreateAt (D-16): the newest create among LIVE sessions, 0 when none. Live-only is deliberate:
20056
+ // D-16's "use" is a session the machine is actually holding, so an exited tab cannot make the row
20057
+ // look freshly used.
20058
+ const _hostStats = createHostStats({
20059
+ db: dbModule,
20060
+ countTasks: () => {
20061
+ const now = Date.now();
20062
+ const tasks = { running: 0, needsYou: 0, idle: 0, workers: 0 };
20063
+ for (const session of _liveLifecycleSessions()) {
20064
+ if (isWorkerSessionObj(session)) tasks.workers += 1;
20065
+ let status = '';
20066
+ try {
20067
+ status = (_ledgerLiveStatus(session, _statusLedgerVerdict(session, now), now) || {}).status || '';
20068
+ } catch {
20069
+ status = ''; // REPORT-lite: a session mid-teardown has no status row; it counts in no bucket
20070
+ }
20071
+ if (status === 'running' || status === 'stalled' || status === 'resuming') tasks.running += 1;
20072
+ else if (status === 'waiting_input') tasks.needsYou += 1;
20073
+ else if (status === 'idle') tasks.idle += 1;
20074
+ }
20075
+ return tasks;
20076
+ },
20077
+ lastCreateAt: () => {
20078
+ let newest = 0;
20079
+ for (const session of _liveLifecycleSessions()) {
20080
+ const createdAt = Number(session.createdAt) || 0;
20081
+ if (createdAt > newest) newest = createdAt;
20082
+ }
20083
+ return newest;
20084
+ },
20085
+ });
20086
+
19955
20087
  // A wheel-forwarded repaint is not agent output (the classifier's mouse veto, term-scroll-lag D-5).
19956
20088
  function _mouseProvokedNow(session, now) {
19957
20089
  return outputIsMouseProvoked({ lastMouseInputAtMs: session._lastMouseInputAt || 0, lastNonMouseInputAtMs: session._lastNonMouseInputAt || 0, nowMs: now });
@@ -20469,6 +20601,11 @@ async function _buildStandupSnapshot(options = {}) {
20469
20601
  const standupPairs = [];
20470
20602
  const now = Date.now();
20471
20603
  const activeSessions = [];
20604
+ // WP-3 (D-9): the machine tag inputs, computed ONCE per rebuild (cheap: the registry's TTL
20605
+ // snapshot). With ZERO registered machines nothing below changes a byte — no key is stamped on a
20606
+ // local card and no remote card is appended — mirroring mergeSessionRows' byte-identity rule.
20607
+ const machineTags = remoteMachineCatalog.machineTagInputs();
20608
+ const machinesRegistered = machineTags.machines.length > 0;
20472
20609
 
20473
20610
  // WP-A6: split the per-session work into a cheap synchronous pass (everything below is
20474
20611
  // in-memory/sync) and the one slow await — the timeline-summary lookup. Sequentially
@@ -20544,6 +20681,9 @@ async function _buildStandupSnapshot(options = {}) {
20544
20681
  waitingForInput: wallePendingPermission ? true : _isServerWaitingForInput(session.id, session),
20545
20682
  waitingForInputAt: wallePendingPermission?.waitingForInputAt || session._waitingForInputAt || null,
20546
20683
  waitingReason: wallePendingPermission?.waitingReason || session._waitingForInputReason || '',
20684
+ // WP-3 (D-9): a LOCAL card carries the three routing fields EMPTY — but only once a machine is
20685
+ // registered, so a single-machine hub's card keeps its exact pre-WP-3 key set (AC-3a).
20686
+ ...(machinesRegistered ? { machine: '', machineName: '', machineHealth: '' } : {}),
20547
20687
  };
20548
20688
  activeSessions.push(standupSession);
20549
20689
  standupPairs.push({ session: standupSession, summary });
@@ -20553,13 +20693,13 @@ async function _buildStandupSnapshot(options = {}) {
20553
20693
  // matches the sidebar's (DB-backed, /api/recent-sessions) from the very first snapshot after
20554
20694
  // boot, instead of only catching up ~13s later as the in-memory map fills (R1/R5). Read
20555
20695
  // dynamically (not cached at module load) so the kill switch takes effect without a
20556
- // restart. This is the ONE synchronous DB read in the whole rebuild — it runs here, inside
20557
- // the materialized rebuild (already off the hot per-request path; see
20558
- // _standupSnapshotForRequest), never per-request.
20696
+ // restart. These are the only synchronous DB reads in the whole rebuild — the task list plus ONE
20697
+ // batched durable-title read (WP-5/D-13: the sidebar's ctm_sessions title) — inside the
20698
+ // materialized rebuild (already off the hot per-request path; see _standupSnapshotForRequest).
20559
20699
  if (isStandupStartupTaskSeedEnabled(process.env)) {
20560
20700
  try {
20561
20701
  const startupTasks = dbModule.listStartupTasks();
20562
- for (const placeholder of selectUnmatchedStartupTaskPlaceholders(activeSessions, startupTasks)) {
20702
+ for (const placeholder of selectUnmatchedStartupTaskPlaceholders(activeSessions, startupTasks, dbModule.getSessionTitlesByIds)) {
20563
20703
  activeSessions.push(placeholder);
20564
20704
  standupPairs.push({ session: placeholder, summary: null });
20565
20705
  }
@@ -20568,6 +20708,26 @@ async function _buildStandupSnapshot(options = {}) {
20568
20708
  }
20569
20709
  }
20570
20710
 
20711
+ // WP-3 (D-9): the box sessions. A LOCAL id always wins (the same rule mergeSessionRows enforces):
20712
+ // an id already present — a live session or a startup-task placeholder — is never re-added. A
20713
+ // remote card is classified from the box row's OWN status fields (lib/session-standup.js
20714
+ // remoteStandupSession); no local PTY helper is ever consulted for a session this host does not
20715
+ // own, and it carries no local stream summary (summary: null). Deliberately NOT pushed into
20716
+ // standupPairs: attention classification reads local scrollback/terminal state by session id.
20717
+ if (machinesRegistered) {
20718
+ const seenIds = new Set();
20719
+ for (const session of activeSessions) {
20720
+ const id = String(session?.id || session?.sessionId || '').trim();
20721
+ if (id) seenIds.add(id);
20722
+ }
20723
+ for (const row of remoteMachineCatalog.remoteRows()) {
20724
+ const id = String(row?.id || row?.sessionId || '').trim();
20725
+ if (!id || seenIds.has(id)) continue;
20726
+ seenIds.add(id);
20727
+ activeSessions.push(remoteStandupSession(row));
20728
+ }
20729
+ }
20730
+
20571
20731
  if (includeAttention) await _applyStandupAttention(standupPairs, now);
20572
20732
 
20573
20733
  const snapshot = buildSessionStandupSnapshot({
@@ -20949,6 +21109,11 @@ async function apiRecentSessions(req, res, url) {
20949
21109
  async function _apiRecentSessionsLegacy(req, res, url, options = {}) {
20950
21110
  const limit = parseInt(url.searchParams.get('limit') || '50', 10);
20951
21111
  const forceRescan = url.searchParams.get('rescan') === '1';
21112
+ // WP-3 (D-10): main's cheap registry snapshot for the DB-only projection below — the same input
21113
+ // the read-pool op carries, so the legacy and published projections stamp identically. Deliberately
21114
+ // read HERE, never in apiRecentSessions (tests/r4-9-recent-sessions-caps.test.js:144 pins that
21115
+ // handler body to a pure published-buffer lookup).
21116
+ const machineTags = remoteMachineCatalog.machineTagInputs();
20952
21117
 
20953
21118
  // First access after (a cold) boot: seed the in-memory cache from the persisted snapshot so this
20954
21119
  // request serves the last good list immediately instead of an empty one. It's marked stale, so
@@ -21117,7 +21282,7 @@ async function _apiRecentSessionsLegacy(req, res, url, options = {}) {
21117
21282
  if (_isProviderChildSessionRow(row)) continue;
21118
21283
  if (existingSessionIds.has(row.id)) continue;
21119
21284
  if (dbRowAgentSessionIds(row).some(agentId => existingSessionIds.has(agentId))) continue;
21120
- const session = _recentSessionFromDbRow(row, { fileStats });
21285
+ const session = _recentSessionFromDbRow(row, { fileStats, machineTags });
21121
21286
  if (!_shouldIncludeDbOnlyRecentSession(row, session, fileStats)) continue;
21122
21287
  result.push(session);
21123
21288
  existingSessionIds.add(row.id);
@@ -24860,15 +25025,21 @@ function _resolveCodexOwnerCtmSessionId(requestedSessionId, agentSessionId) {
24860
25025
  return requestedSessionId || agentSessionId || '';
24861
25026
  }
24862
25027
 
24863
- function _codexIndexTitleForSession(ctmSessionId, candidateTitle) {
24864
- let existingTitle = null;
24865
- try { existingTitle = dbModule.getSessionTitleNew(ctmSessionId); } catch {}
24866
- const liveSession = sessions.get(ctmSessionId) || null;
24867
- const candidate = (candidateTitle || '').trim();
24868
- if (candidate && shouldApplyCodexAutoTitle({ session: liveSession || { title: existingTitle?.title || '', label: existingTitle?.title || '' }, existingTitle })) {
24869
- return candidate;
24870
- }
24871
- return existingTitle?.title || liveSession?.title || liveSession?.label || candidate;
25028
+ // WP-4/D-10: read the Codex thread's OWN name (threads.name) and apply it through the shared
25029
+ // applyProviderTitle path (tier rules live there, never here) — the ONE entry point for the link
25030
+ // path and every Codex rollout event, never a timer. Main-thread-only effects: live label, the
25031
+ // startup-task label and one session-list broadcast.
25032
+ function _refreshCodexThreadName(ref, liveSession = findLiveSessionByAnyId(ref)) {
25033
+ const threadId = String(liveSession?._claudeSessionId || liveSession?._providerResumeId || '');
25034
+ const agentName = threadId ? getCodexThreadAgentName(threadId) : '';
25035
+ if (!liveSession || !agentName) return { applied: 'none' };
25036
+ const outcome = applyProviderTitle({ db: dbModule, sessionId: liveSession.id, provider: 'codex', agentName });
25037
+ if (outcome.applied !== 'agent') return outcome;
25038
+ const label = `Codex: ${agentName}`;
25039
+ if (liveSession.label !== label) { liveSession.label = label; _invalidateTitleInfoCache(liveSession); }
25040
+ void _detachOwnerMutation('agent-title-label', () => dbModule.updateStartupTaskLabel(liveSession.id, agentName));
25041
+ broadcastSessionList();
25042
+ return outcome;
24872
25043
  }
24873
25044
 
24874
25045
  function _shouldReclaimStandaloneAgentOwner(agentSessionId, ctmSessionId) {
@@ -25005,15 +25176,9 @@ function _linkCodexThreadToSession(ctmSessionId, liveSession, thread, source, re
25005
25176
  }
25006
25177
 
25007
25178
  const title = _codexThreadTitle(thread);
25008
- try {
25009
- const titleInfo = dbModule.getSessionTitleNew(ctmSessionId);
25010
- if (title && shouldApplyCodexAutoTitle({ session: liveSession, existingTitle: titleInfo })) {
25011
- liveSession.label = `Codex: ${title}`;
25012
- liveSession._codexTitleSynced = true;
25013
- } else if (title) {
25014
- liveSession._codexTitleSynced = true;
25015
- }
25016
- } catch {}
25179
+ // WP-4/D-10: the thread's OWN name (threads.name) is read at link through the shared
25180
+ // applyProviderTitle path; no `_codexTitleSynced` latch is set from the index title any more (S-23).
25181
+ try { _refreshCodexThreadName(ctmSessionId, liveSession); } catch (e) { console.error('[codex-link] thread name read error:', e.message); }
25017
25182
 
25018
25183
  const candidateTransitionId = String(liveSession._modelExecutionRecipe?.transitionId || '');
25019
25184
  const startupPointerWritten = persistCodexStartupResumeFromLink({ session: liveSession, dbModule,
@@ -26666,11 +26831,21 @@ function apiGenerateTitles(req, res) {
26666
26831
  if (existing && (existing.userRenamed || existing.titleSource === 'provider_title' || existing.title)) return false;
26667
26832
  return !!s.firstMessage;
26668
26833
  });
26669
- const skippedInternal = candidates.filter(s => isCtmInternalSession(s.firstMessage));
26670
- for (const s of skippedInternal) {
26671
- persistTitleStatus(s.sessionId, 'skipped', 'ctm_internal');
26834
+ // D-7 (WP-3): harness/provider boilerplate (<local-command-caveat>, <task-notification>,
26835
+ // "# AGENTS.md instructions for", a compaction summary) is not a human prompt — the same
26836
+ // predicate server/boot.js's background auto-title job gates its candidates on.
26837
+ const needTitles = [];
26838
+ for (const s of candidates) {
26839
+ if (isCtmInternalSession(s.firstMessage)) {
26840
+ persistTitleStatus(s.sessionId, 'skipped', 'ctm_internal');
26841
+ continue;
26842
+ }
26843
+ if (!isHumanPromptCandidate(s.provider, s.firstMessage)) {
26844
+ persistTitleStatus(s.sessionId, 'skipped', 'not_human_prompt');
26845
+ continue;
26846
+ }
26847
+ needTitles.push(s);
26672
26848
  }
26673
- const needTitles = candidates.filter(s => !isCtmInternalSession(s.firstMessage));
26674
26849
 
26675
26850
  if (needTitles.length === 0) {
26676
26851
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -26686,19 +26861,9 @@ function apiGenerateTitles(req, res) {
26686
26861
  // provider credentials itself; a credential-less setup surfaces as a normal
26687
26862
  // callClaudeAsync failure below, classified by titleGenerationFailureInfo().
26688
26863
 
26689
- // Strip resume/continuation boilerplate from first messages
26690
- function cleanFirstMessage(msg) {
26691
- if (!msg) return '';
26692
- return msg
26693
- .replace(/^This session is being continued from a previous conversation[^\n]*\n*/i, '')
26694
- .replace(/^Continue the conversation[^\n]*\n*/i, '')
26695
- .replace(/^Resume:?\s*/i, '')
26696
- .replace(/^Summary:?\s*\n?/i, '')
26697
- .replace(/^The summary below covers[^\n]*\n*/i, '')
26698
- .replace(/^\[(?:Request interrupted|Tool use|Error|Retrying)[^\]]*\]\s*/g, '')
26699
- .trim();
26700
- }
26701
-
26864
+ // Resume/continuation boilerplate is stripped by server/boot.js's cleanFirstMessage, shared
26865
+ // with the background auto-title job (its `This session is being continued…` line is gone:
26866
+ // isHumanPromptCandidate already rejects that compaction row).
26702
26867
  const prompt = `Generate short, descriptive titles (max 60 chars) for these Claude Code sessions.
26703
26868
  Each session has an ID and the user's first message. Create a title that summarizes what the user was working on.
26704
26869
 
@@ -27533,7 +27698,7 @@ function _sessionLifecycleView(session, options = {}) {
27533
27698
  pinned: !!session?._lifecyclePinned,
27534
27699
  isActiveTab: !!(session?.id && _sessionIsDeclaredShown(session.id)),
27535
27700
  isMru: !!(session?.id && mruIds.has(session.id)),
27536
- hasBackgroundShell: !!(_statusLedgerFor(session)?.backgroundLive(now)) || (!_statusLedgerFor(session)?.backgroundDeclared && _backgroundAgentsActive(session, now)),
27701
+ hasBackgroundShell: !!(_statusLedgerFor(session)?.backgroundActive(now)) || (!_statusLedgerFor(session)?.backgroundDeclared && _backgroundAgentsActive(session, now)),
27537
27702
  isRestoring: _inRestoreStorm() || restoreRuntime.restoreStartingActive(session),
27538
27703
  recentNonStatusActivityAt: Number(session?._lastMeaningfulNonStatusAt || session?.lastMeaningfulActivityAt || 0),
27539
27704
  lastActivatedAt: Number(session?._lastActivatedAt || 0),
@@ -27913,6 +28078,25 @@ function _lifecycleProvider(session) {
27913
28078
  return detectAgentType(session?.cmd || '') || session?.agentType || session?.type || 'unknown';
27914
28079
  }
27915
28080
 
28081
+ // WP-3 (§5 D-5/D-6, D-9): the one recorder for a skip that does NOT flow through the
28082
+ // _setDeferredRestoreStarting funnel. Per-session latch: a session skipped for the SAME reason
28083
+ // (drain passes, background-client storms) records once per episode; the claim clears it.
28084
+ function _noteWakeSkip(session, reason, detail = {}, id = '') {
28085
+ if (!reason) return;
28086
+ if (session && session._lastRestoreSkip?.reason === reason) return;
28087
+ if (session) session._lastRestoreSkip = { reason, at: Date.now() };
28088
+ const sid = String(id || session?.id || '');
28089
+ if (sid) {
28090
+ try { recordSessionDiagnostic(sid, 'deferred-restore-skip', { reason, ...detail }); }
28091
+ catch { /* expected: session diagnostics ring is not constructed in sliced unit harnesses */ }
28092
+ }
28093
+ try { _lifecycleDiagnostics.recordWakeSkip({ id: sid, provider: _lifecycleProvider(session), reason: String(reason) }); }
28094
+ catch { /* expected: lifecycle diagnostics are not constructed in sliced unit harnesses */ }
28095
+ }
28096
+
28097
+ // WP-3 (§9 S-10..S-20): the `onSkip(reason, id, session)` dep the extracted restore helpers use.
28098
+ function _restoreWakeSkipSink(reason, id, session) { _noteWakeSkip(session, reason, {}, id); }
28099
+
27916
28100
  function _canonicalLifecycleWakePhases(phases = {}) {
27917
28101
  return {
27918
28102
  admitted: Number(phases.gateAdmittedAt || phases.activationRequestedAt || 0),
@@ -28155,12 +28339,23 @@ function _lifecycleOrphanTotals() {
28155
28339
  }
28156
28340
 
28157
28341
  async function _refreshLifecycleOrphanTotals() {
28342
+ if (_lifecycleOrphanGroups.size === 0) return _lifecycleOrphanTotals();
28343
+ // D-9 (WP-6, clock-free-tests-followups-2026-10-23 §5 D-9): ONE whole-table `ps` for the whole
28344
+ // sweep. N orphan groups used to mean N table reads; the rows are identical within a sweep.
28345
+ let rows;
28346
+ try {
28347
+ rows = await _sessionResourceSampler.sampleProcessTable();
28348
+ } catch {
28349
+ /* expected: best-effort sweep — a failed read skips this refresh and keeps the stored groups for
28350
+ one more sweep, exactly like the per-group catch below, instead of dropping known orphans. */
28351
+ return _lifecycleOrphanTotals();
28352
+ }
28158
28353
  for (const [pgid, prev] of [..._lifecycleOrphanGroups]) {
28159
28354
  try {
28160
28355
  // D-4/D-6 (session-park-busy-background-shell-truth-2026-10-14): carry the stored survivor set
28161
28356
  // forward, so a survivor that leads its OWN process group (invisible to the pgid rollup) is not
28162
28357
  // forgotten by the next sweep while it is still alive.
28163
- const group = await _sessionResourceSampler.sampleProcessGroup(pgid, prev?.survivorPids || []);
28358
+ const group = await _sessionResourceSampler.sampleProcessGroup(pgid, prev?.survivorPids || [], { rows });
28164
28359
  if (group.procCount > 0) _lifecycleOrphanGroups.set(pgid, group);
28165
28360
  else _lifecycleOrphanGroups.delete(pgid);
28166
28361
  } catch {}
@@ -28280,7 +28475,7 @@ function _stripPtyOutputReplies(session, sessionId, data, site) {
28280
28475
  }
28281
28476
 
28282
28477
  function _writeLifecyclePty(session, data, source = 'input', options = {}) {
28283
- if (!session?.ptyProcess) return { posted: false, inputSeq: 0 };
28478
+ if (!session?.ptyProcess) return { posted: false, inputSeq: 0, dispatchDisposition: 'preflight-refused' };
28284
28479
  // Image, typing and lifecycle frames share one monotonic transport sequence. Typing keeps its
28285
28480
  // separate echo counter and passes the exact allocated frame sequence through options.inputSeq.
28286
28481
  // WP-2 (§6.2, D6): ONE monotonic allocator for every host frame. Keystrokes pass the exact seq they
@@ -28312,23 +28507,22 @@ function _writeLifecyclePty(session, data, source = 'input', options = {}) {
28312
28507
  }
28313
28508
  }
28314
28509
  session._lifecycleInputInFlight = true;
28510
+ let dispatchDisposition = 'preflight-refused';
28315
28511
  try {
28316
- // WP-1 (D-2): `session.ptyProcess.write` is HostedPtyAdapter.write, which returns
28317
- // whether the frame was POSTED to the host -- never whether the byte was written.
28318
- // "written" is a fact that arrives later as an 'input-ack' frame (see
28319
- // _onHostInputAck below); nothing here may treat a truthy synchronous return as
28320
- // delivery proof.
28512
+ // A1: preserve the selected writer's legacy result while observing its actual route.
28513
+ // Neither a Boolean nor this local disposition is byte-write or withdrawal proof;
28514
+ // the separate input-ack is still the host's byte-write observation.
28321
28515
  const posted = writeWithDeviceReplyFirewall({
28322
28516
  data,
28323
28517
  exempt: authoritativeDeviceReply,
28324
- // A real HostedPtyAdapter.write() returns an explicit boolean ("posted to the
28325
- // host", never "written"). Legacy/non-hosted/test-double PTYs whose write() has no
28326
- // return value (undefined) keep today's behavior: not throwing means posted; only
28327
- // an EXPLICIT `false` return means the frame never left this process.
28328
- write: value => session.ptyProcess.write(value, { inputSeq }) !== false,
28518
+ write: value => {
28519
+ const observed = _observePtyWrite(session.ptyProcess, value, { inputSeq });
28520
+ dispatchDisposition = observed.disposition;
28521
+ return observed.posted;
28522
+ },
28329
28523
  onDrop: classification => _recordDroppedDeviceReply(session, source, classification),
28330
28524
  });
28331
- return { posted: posted === true, inputSeq };
28525
+ return { posted: posted === true, inputSeq, dispatchDisposition };
28332
28526
  }
28333
28527
  finally { session._lifecycleInputInFlight = false; }
28334
28528
  }
@@ -28348,7 +28542,7 @@ function _lifecycleBackgroundLease(session, now = Date.now()) {
28348
28542
  const ledger = session ? _statusLedgerFor(session) : null;
28349
28543
  const snap = ledger && typeof ledger.backgroundSnapshot === 'function' ? ledger.backgroundSnapshot(now) : null;
28350
28544
  if (!snap || !Array.isArray(snap.entries) || !snap.entries.length) return null;
28351
- let shells = 0, tasks = 0, firstSeenAt = Infinity, renewedAt = 0, expiresAt = Infinity, renewals = 0, capCrossed = 0;
28545
+ let shells = 0, tasks = 0, firstSeenAt = Infinity, renewedAt = 0, expiresAt = Infinity, renewals = 0, capCrossed = 0, quiet = 0, activeAt = 0;
28352
28546
  for (const e of snap.entries) {
28353
28547
  if (e.type === 'shell') shells += 1; else tasks += 1;
28354
28548
  firstSeenAt = Math.min(firstSeenAt, Number(e.firstSeenAt) || 0);
@@ -28356,8 +28550,10 @@ function _lifecycleBackgroundLease(session, now = Date.now()) {
28356
28550
  expiresAt = Math.min(expiresAt, Number(e.expiresAt) || 0);
28357
28551
  renewals += Number(e.renewals) || 0;
28358
28552
  if (Number(e.capCrossedAt) > 0) capCrossed += 1;
28553
+ if (typeof ledger.backgroundQuiet === 'function' && ledger.backgroundQuiet(e, now)) quiet += 1; // D-5: no classifier, nothing is quiet
28554
+ activeAt = Math.max(activeAt, Number(e.activeAt) || 0);
28359
28555
  }
28360
- return { shells, tasks, firstSeenAt, renewedAt, expiresAt, renewals, capCrossed };
28556
+ return { shells, tasks, firstSeenAt, renewedAt, expiresAt, renewals, capCrossed, quiet, activeAt };
28361
28557
  }
28362
28558
 
28363
28559
  function _lifecycleJournalGate(session, decision, configName) {
@@ -29013,6 +29209,17 @@ async function _sampleLifecycleResources(options = {}) {
29013
29209
  _sessionResourceSampler.warnedPressureFailure = true;
29014
29210
  console.warn('[lifecycle] memory-pressure probe was unparseable; treating pressure as normal');
29015
29211
  }
29212
+ // WP-1 (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.1/D-11): one host sample per
29213
+ // RESOURCE SAMPLE, hooked here rather than in _runLifecycleSweep on purpose. This function's own
29214
+ // due() gate (CTM_RESOURCE_SAMPLE_MS, default 60 s) is what makes host_samples hold "one row per
29215
+ // minute, ≤1440 rows/machine" -- the same number db.js's listHostSamples caps at, so a 24 h read
29216
+ // always fits. Hooking the sweep instead would record on the 30 s maintenance tick and halve the
29217
+ // readable window (the read takes the OLDEST `limit` rows in the window). No new timer either
29218
+ // way. Fire-and-forget: record() is defined never to throw and a failed sub-read degrades to null
29219
+ // fields, but a rejection here would still be a bug, so it is reported rather than swallowed.
29220
+ void _hostStats.record(snapshot).catch((error) => {
29221
+ errorSink.report('host-stats:record', error, { at: Number(snapshot && snapshot.sampledAt) || 0 });
29222
+ });
29016
29223
  for (const resource of snapshot.sessions || []) {
29017
29224
  const session = sessions.get(resource.id);
29018
29225
  if (!session || session._restoreDeferred) continue;
@@ -29029,7 +29236,7 @@ async function _sampleLifecycleResources(options = {}) {
29029
29236
  // matches every other footprint field's "sampled vs not yet" contract in this file.
29030
29237
  session.footprintMB = resource.footprintMB == null ? null : resource.footprintMB;
29031
29238
  _statusBindings.applyCpuSample(session, resource);
29032
- if (resource.agentProcCount > 0) { session.descendantGroupLeaderCount = resource.descendantGroupLeaderCount; _statusBindings.applyProcessShape(session, resource); } // D-10: unknown agent pid is not zero
29239
+ if (resource.agentProcCount > 0) { session.descendantGroupLeaderCount = resource.descendantGroupLeaderCount; _statusBindings.applyProcessShape(session, resource); void _statusBindings.observeTaskOutputs(session, resource.sampledAt); } // D-10: unknown agent pid is not zero
29033
29240
  }
29034
29241
  const previousPressure = _lifecyclePressure.level;
29035
29242
  const pressureResult = _lifecyclePressure.observe(snapshot.signals || snapshot.pressure, {
@@ -29895,7 +30102,12 @@ wss.on('connection', async (ws, req) => {
29895
30102
  const url = new URL(req.url, `http://${req.headers.host}`);
29896
30103
  const reqSnapshot = {
29897
30104
  socket: { remoteAddress: _remoteIpFromReq(req) },
29898
- headers: { 'user-agent': req.headers?.['user-agent'] || '' },
30105
+ // X-CTM-Forwarded-By (WP-2a P-3) joins the snapshot so the WS create path can apply the
30106
+ // one-hop guard (lib/remote-machine-create.js forwardRemoteCreate reads it off ctx.ws here).
30107
+ headers: {
30108
+ 'user-agent': req.headers?.['user-agent'] || '',
30109
+ 'x-ctm-forwarded-by': req.headers?.['x-ctm-forwarded-by'] || '',
30110
+ },
29899
30111
  };
29900
30112
  const cfDecision = cloudflareAccessVerifier.verifyRequestSync(req, { isLocalhost });
29901
30113
  if (!cfDecision.ok) {
@@ -31389,6 +31601,9 @@ headlessWorker.on('message', _perfTracker.wrap('worker:headless-terminal-message
31389
31601
  ? _recordInputEchoFromFrame(session, msg.echoedThroughSeq, Date.now(), {
31390
31602
  targetClientId: String(msg.targetClientId || ''),
31391
31603
  targetSocketGeneration: Number(msg.targetSocketGeneration || 0),
31604
+ // WP-4 (R-7): session-host-manager.js's `_stampMainArrival` stamped this frame object
31605
+ // when it first touched the main thread; 0 when the sender predates the stamp.
31606
+ frameArrivedAt: Number(msg._mainArrivedAt) || 0,
31392
31607
  })
31393
31608
  : [];
31394
31609
  const holdingRestoreFrames = _stateSyncHoldingRestoreFrames(session);
@@ -33896,6 +34111,35 @@ function _sendWalleSessionCreated(ws, session, extra = {}) {
33896
34111
  return true;
33897
34112
  }
33898
34113
 
34114
+ // WP-4 (§5 D-12): the `created` frame a DUPLICATE same-id create for an ALREADY LIVE native session
34115
+ // is re-answered with. Byte-for-byte the frame the spawn path sends (lib/session-create/response.js's
34116
+ // sendCreated): the SAME builder, the SAME `_sessionPayload` projection, the same arguments -- read
34117
+ // off the LIVE session object instead of the spawn's locals. No attach, no snapshot, no
34118
+ // client-list mutation, no state-sync subscription: the client attaches itself on `created`, exactly
34119
+ // as it does after a fresh create (public/index.html's onCreated replaces the tab in place).
34120
+ function _sendNativeSessionCreated(ws, session) {
34121
+ if (!ws || ws.readyState !== 1 || !session || typeof session !== 'object') return false;
34122
+ const cmd = session.cmd || session.shell || '';
34123
+ const createAgentType = session.agentType || detectAgentType(cmd);
34124
+ _wsSend(ws, JSON.stringify(buildCreatedSessionPayload({
34125
+ sessionPayload: _sessionPayload(session, { lightweight: true, stormSafe: true }),
34126
+ session,
34127
+ id: session.id,
34128
+ // The session's CURRENT pid. On a hosted PTY the adapter re-points `ptyProcess.pid` from the
34129
+ // wrapper to the real child once the host receipt lands, so this can differ from the pid the
34130
+ // first `created` named; the re-answer reports the live truth, not a replay.
34131
+ pid: session.ptyProcess.pid,
34132
+ cwd: session.cwd || '',
34133
+ cmd,
34134
+ args: Array.isArray(session.args) ? session.args : [],
34135
+ branch: session.branch ?? null,
34136
+ createAgentType,
34137
+ projectDirName: session._claudeProjectDir ? path.basename(session._claudeProjectDir) : null,
34138
+ getAgentCapabilities,
34139
+ })));
34140
+ return true;
34141
+ }
34142
+
33899
34143
  // WP-3 (design/walle-conv-order-authority-2026-08-21 D-5, §6): min/max order key over a
33900
34144
  // served page's messages, for the envelope's keyRange. Min/max (not first/last array
33901
34145
  // position) because callers serve pages in different physical row orders (e.g. a
@@ -35913,6 +36157,37 @@ function _onHostInputAck(session, ack, origin = null) {
35913
36157
  }
35914
36158
  }
35915
36159
 
36160
+ // WP-4 (R-8): the top ≤3 recent main-thread spans overlapping [startMs, endMs], by overlapped ms
36161
+ // descending, as `{ name, overlapMs }` -- "what was main running while this frame was in flight".
36162
+ // Reads the same recent-span ring lib/perf-tracker.js's selectBlockCoverage reads (via its
36163
+ // getRecentSpans accessor; `eventLoopBlockOverlap` is the aggregate view of the same ring and
36164
+ // names no op, so it cannot answer this). A pure read + compute: no span is recorded, no counter
36165
+ // moves, and a caller can never make this loop do work it would not otherwise do. Spans on a
36166
+ // different clock (or absent) simply contribute nothing.
36167
+ function _mainSpansForWindow(startMs, endMs) {
36168
+ const start = Number(startMs);
36169
+ const end = Number(endMs);
36170
+ if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return [];
36171
+ // The window's own start can be older than the ring's default 3 s lookback; ask for the ring's
36172
+ // full 24-span depth and let the overlap test below decide.
36173
+ const spans = _perfTracker.getRecentSpans({ maxAgeMs: Math.max(3000, Date.now() - start + 1000), limit: 24 }).recent;
36174
+ const byName = new Map();
36175
+ for (const span of spans) {
36176
+ if (!span) continue;
36177
+ const spanStart = Number(span.start);
36178
+ const spanEnd = Number(span.end);
36179
+ if (!Number.isFinite(spanStart) || !Number.isFinite(spanEnd)) continue;
36180
+ const overlap = Math.min(spanEnd, end) - Math.max(spanStart, start);
36181
+ if (overlap <= 0) continue;
36182
+ const name = String(span.name || '').slice(0, 120);
36183
+ if (!name) continue;
36184
+ byName.set(name, (byName.get(name) || 0) + overlap);
36185
+ }
36186
+ return Array.from(byName, ([name, overlapMs]) => ({ name, overlapMs: Math.max(0, Math.floor(overlapMs)) }))
36187
+ .sort((a, b) => b.overlapMs - a.overlapMs)
36188
+ .slice(0, 3);
36189
+ }
36190
+
35916
36191
  function _recordInputEchoFromFrame(session, echoedThroughSeq, at = Date.now(), target = {}) {
35917
36192
  if (!session || !(session._inputEchoPending instanceof Map)) return [];
35918
36193
  const echoed = Math.max(0, Number(echoedThroughSeq) || 0);
@@ -35930,6 +36205,10 @@ function _recordInputEchoFromFrame(session, echoedThroughSeq, at = Date.now(), t
35930
36205
  }
35931
36206
  const targetClientId = String(target.targetClientId || '');
35932
36207
  const targetSocketGeneration = Number(target.targetSocketGeneration || 0);
36208
+ // WP-4 (clock-free-tests-followups-r4-2026-10-24.html §5 R-7/R-8, §6.5): the frame's arrival
36209
+ // stamp, set by session-host-manager.js's `_stampMainArrival` at the first main-thread touch.
36210
+ // 0 (an older/unstamped sender) means "no arrival leg to price" -- never a real epoch 0.
36211
+ const frameArrivedAt = Math.max(0, Math.floor(Number(target.frameArrivedAt) || 0));
35933
36212
  for (const [seq, rawRecord] of session._typingInteractionByInputSeq || []) {
35934
36213
  if (seq > echoed) continue;
35935
36214
  const record = typeof rawRecord === 'string'
@@ -35938,6 +36217,14 @@ function _recordInputEchoFromFrame(session, echoedThroughSeq, at = Date.now(), t
35938
36217
  if (!record?.interactionId) continue;
35939
36218
  if (!record.mirrored) {
35940
36219
  _noteTypingInteraction({ interactionId: record.interactionId, stage: 'mirror-revision', at });
36220
+ // R-7/R-8: exactly the interactions that just got `mirror-revision` also get
36221
+ // `frame-arrived`, at the arrival instant rather than at the instant main finished
36222
+ // stamping -- the difference between the two IS the receiver-loop leg this measures.
36223
+ // R-8's mainSpans are attached by the trace store once BOTH this stamp and the worker's
36224
+ // `frame-posted` are in (either order; see mainSpansForWindow at its construction).
36225
+ if (frameArrivedAt > 0) {
36226
+ _noteTypingInteraction({ interactionId: record.interactionId, stage: 'frame-arrived', at: frameArrivedAt });
36227
+ }
35941
36228
  record.mirrored = true;
35942
36229
  session._typingInteractionByInputSeq.set(seq, record);
35943
36230
  }
@@ -36295,7 +36582,7 @@ function _terminateResumeLifecycle(session, failure) {
36295
36582
  agentType: session._resumeAgentType || detectAgentType(session.cmd) || '',
36296
36583
  generation,
36297
36584
  };
36298
- markSessionResumeBlocked(session.id, info);
36585
+ markSessionResumeBlocked(session.id, info, 'lifecycle-terminate');
36299
36586
  session._resumeAttemptFailed = true;
36300
36587
  // This is a system-enforced provider termination, not an explicit user close. The PTY exit
36301
36588
  // handler already suppresses auto-restart while `resumeBlocked` is set, and
@@ -36846,6 +37133,10 @@ const _DETERMINISTIC_PREFLIGHT_CODES = new Set([
36846
37133
  'missing_claude_transcript',
36847
37134
  'missing_codex_rollout',
36848
37135
  'agent_cli_not_found',
37136
+ // WP-1 (docs/codex-routed-fork-cold-restore-guard-2026-10-22.html §5 D-4, §9 S-7): a Codex
37137
+ // thread created on a gateway provider no native config load can resolve. Retrying cannot heal it
37138
+ // — the picker (or the journal-backed repair, D-3) is the only way out — so it quarantines.
37139
+ 'codex_provider_missing',
36849
37140
  ]);
36850
37141
 
36851
37142
  // Pure decision (test seam): is a resume-preflight failure DETERMINISTIC (quarantine) or
@@ -36871,7 +37162,7 @@ function _shouldRefreshLaunchReadinessForBlock(code, blockDeterministic) {
36871
37162
  return code === 'launch_rollout_index_pending' && blockDeterministic !== true;
36872
37163
  }
36873
37164
 
36874
- function markSessionResumeBlocked(sessionId, info = {}) {
37165
+ function markSessionResumeBlocked(sessionId, info = {}, site = 'unspecified') {
36875
37166
  const id = String(sessionId || '');
36876
37167
  if (!id) return null;
36877
37168
  const record = {
@@ -36892,24 +37183,18 @@ function markSessionResumeBlocked(sessionId, info = {}) {
36892
37183
  checkedAt: info.checkedAt || '',
36893
37184
  // WP-2: 'removed'|'never_seen'; only set on the two missing-transcript codes.
36894
37185
  localHistory: info.localHistory || (['missing_claude_transcript', 'missing_codex_rollout'].includes(info.code) ? 'unknown' : ''), lastSeenBytes: Number(info.lastSeenBytes) || 0,
36895
- lastSeenAt: info.lastSeenAt || '', trashPath: info.trashPath || '', originalPath: info.originalPath || '',
37186
+ lastSeenAt: info.lastSeenAt || '', trashPath: info.trashPath || '', originalPath: info.originalPath || '', trashReason: info.trashReason || '', trashedAt: info.trashedAt || '',
36896
37187
  // WP-6: the resume ladder's answer, only on the two missing-transcript codes.
36897
37188
  resumeRung: info.resumeRung || '', coverage: info.coverage || null, offerStoreFirst: info.offerStoreFirst === true, restoreError: info.restoreError || '',
36898
37189
  };
36899
37190
  _resumeBlockedSessions.set(id, record);
37191
+ // WP-0 (D-8): one diagnostic names the site that set this block, so a card the user saw is always
37192
+ // attributable to a code path -- including the sites that used to log nothing at all.
37193
+ recordSessionDiagnostic(id, 'resume-blocked-set', resumeBlockSetDetails(site, record));
36900
37194
  // WP-2: callers that built an info object inline get the enriched record back instead of re-reading the map.
36901
37195
  return record;
36902
37196
  }
36903
37197
 
36904
- // The ladder fields every resume-blocked message for a missing transcript carries (S-33).
36905
- function _resumeBlockedRungWire(info) {
36906
- return {
36907
- resumeRung: info?.resumeRung || '', coverage: info?.coverage || null, offerStoreFirst: info?.offerStoreFirst === true, restoreError: info?.restoreError || '',
36908
- deterministic: info?.deterministic === true, origin: info?.origin || 'unknown',
36909
- storeTurns: normalizeStoreTurns(info?.storeTurns), checkedAt: info?.checkedAt || '',
36910
- };
36911
- }
36912
-
36913
37198
  // WP-2 (multi-machine §C): the pure relative-origin rule now lives in lib/resume-origin-facts.js, so this
36914
37199
  // launch-plan projection, the resume-blocked record and the transcripts route share exactly one rule.
36915
37200
  // Re-exported here under the name every existing call site in this file already uses.
@@ -36944,9 +37229,17 @@ function isSessionResumeBlocked(sessionId) {
36944
37229
  // explicit user action (attach, click, "Start fresh here") — those paths consult the block
36945
37230
  // directly (see `_startDeferredRestoreSession`), not this drain-eligibility predicate.
36946
37231
  function _sessionEligibleForAutoResumeDrain(session, isBlockedFn) {
37232
+ // WP-3 (§9 S-19): only the two NON-normal-state arms below are skips (a session merely not being a
37233
+ // drain candidate is not). Zero return-value change; sliced into bare vm contexts by unit tests.
36947
37234
  if (!session || !session._restoreDeferred || session._restoreStarting || session._exited) return false;
36948
- if (!sessionPolicy(session).relaunchFromRestore) return false;
36949
- if (typeof isBlockedFn === 'function' && isBlockedFn(session.id)) return false;
37235
+ if (!sessionPolicy(session).relaunchFromRestore) {
37236
+ _noteWakeSkip(session, 'DrainIneligible');
37237
+ return false;
37238
+ }
37239
+ if (typeof isBlockedFn === 'function' && isBlockedFn(session.id)) {
37240
+ _noteWakeSkip(session, 'ResumeBlocked');
37241
+ return false;
37242
+ }
36950
37243
  return true;
36951
37244
  }
36952
37245
 
@@ -37021,16 +37314,16 @@ function _notifyResumePreflightFailure(id, ws, agentType, result) {
37021
37314
  let blockedInfo = null;
37022
37315
  if (deterministic) {
37023
37316
  blockedInfo = {
37024
- ..._resumeBlockedRungWire(result), code: result.code || 'resume_blocked', message, agentType, detail: result.detail || '', attempts: Number(result.attempts || 0), deterministic: true,
37317
+ ...resumeBlockedRungWire(result), code: result.code || 'resume_blocked', message, agentType, detail: result.detail || '', attempts: Number(result.attempts || 0), deterministic: true,
37025
37318
  origin: result.origin || _resumeOriginForSession(id), storeTurns: result.storeTurns, checkedAt: result.checkedAt || new Date().toISOString(),
37026
- localHistory: result.localHistory, lastSeenBytes: result.lastSeenBytes, lastSeenAt: result.lastSeenAt, trashPath: result.trashPath, originalPath: result.originalPath,
37319
+ localHistory: result.localHistory, lastSeenBytes: result.lastSeenBytes, lastSeenAt: result.lastSeenAt, trashPath: result.trashPath, originalPath: result.originalPath, trashReason: result.trashReason, trashedAt: result.trashedAt,
37027
37320
  };
37028
- blockedInfo = markSessionResumeBlocked(id, blockedInfo) || blockedInfo; // WP-2: the wire carries the enriched record (host + peer + transcript state)
37321
+ blockedInfo = markSessionResumeBlocked(id, blockedInfo, 'preflight') || blockedInfo; // WP-2: the wire carries the enriched record (host + peer + transcript state)
37029
37322
  }
37030
37323
  if (!ws) return deterministic;
37031
37324
  if (deterministic) {
37032
37325
  try {
37033
- _wsSend(ws, JSON.stringify(resumeBlockedWire(id, blockedInfo, { agentType, rungWire: _resumeBlockedRungWire(blockedInfo) })));
37326
+ _wsSend(ws, JSON.stringify(resumeBlockedWire(id, blockedInfo, { agentType, rungWire: resumeBlockedRungWire(blockedInfo) })));
37034
37327
  } catch {}
37035
37328
  } else {
37036
37329
  try {
@@ -37091,7 +37384,7 @@ function _notifyPreparedResumePreflightFailure(id, ws, agentType, resumePrefligh
37091
37384
  if (MISSING_TRANSCRIPT_CODES.has(resumePreflight.code) && existingSession) {
37092
37385
  existingSession._resumeRung = {
37093
37386
  code: resumePreflight.code, message: resumePreflight.message || '', resumeId: resumePreflight.resumeId || '',
37094
- ..._resumeBlockedRungWire(resumePreflight), origin: resumePreflight.origin || _resumeOriginForSession(id),
37387
+ ...resumeBlockedRungWire(resumePreflight), origin: resumePreflight.origin || _resumeOriginForSession(id),
37095
37388
  };
37096
37389
  }
37097
37390
  _notifyResumePreflightFailure(id, ws, agentType, resumePreflight);
@@ -37129,7 +37422,7 @@ function _blockResumeAttempt(session, code, message, detail) {
37129
37422
  attempts: Number(attemptState.failures || attemptState.attempt || 1),
37130
37423
  agentType: session._resumeAgentType || detectAgentType(session.cmd) || '',
37131
37424
  };
37132
- markSessionResumeBlocked(session.id, info);
37425
+ markSessionResumeBlocked(session.id, info, 'attempt-block');
37133
37426
  markResumeFailureExit(session, { code, source: code });
37134
37427
  _clearResumeLoading(session, code);
37135
37428
  recordSessionDiagnostic(session.id, 'resume-blocked', info);
@@ -37363,6 +37656,9 @@ const preparedSessionCreate = require('./lib/session-create/prepared').createPre
37363
37656
  ptyStartupCommitComplete,
37364
37657
  _rollbackPtyStartupAttempt,
37365
37658
  console,
37659
+ // WP-4 (§5 D-12): the duplicate-create re-answer frame, next to the walle one identity.js already
37660
+ // owns; the module sends it, server.js only builds it.
37661
+ sendNativeSessionCreated: _sendNativeSessionCreated,
37366
37662
  get _ownerRecovery() { return _ownerRecovery; },
37367
37663
  });
37368
37664
 
@@ -37691,6 +37987,8 @@ sessionCreateProvider.configure(() => ({
37691
37987
  parseSessionStartMs,
37692
37988
  path,
37693
37989
  recordSessionDiagnostic,
37990
+ // WP-1 (§5 D-4): the deterministic refusal hook the launch-time native-provider gate uses.
37991
+ markSessionResumeBlocked,
37694
37992
  resolveResumeGeneration,
37695
37993
  // WP-2 (default-subagent-model-delegate-pick-2026-10-13.html §5 D-1..D-3): the Wall-E delegate
37696
37994
  // pick reader for a lead launch. Injected here (not required in provider-process) so the launch
@@ -37969,6 +38267,40 @@ function apiResumeSession(req, res) {
37969
38267
  if (_ownerDecisionInvalidRequest(ownerDecision)) return sendJsonResponse(res, 400, { ok: false, error: 'SESSION_ID_REQUIRED' });
37970
38268
  _emitOwnerMismatchDiagnostic(ownerDecision);
37971
38269
  }
38270
+ // WP-6 (S-8/S-25, AC-6d): a session whose ORIGIN is a registered, REACHABLE machine is
38271
+ // resumed ON that machine. The box owns the session and its transcript (D-2), and this hub
38272
+ // has no local copy to launch from — the pre-WP-6 code checked the local sessions Map and
38273
+ // 500'd ("Resume launch did not start"). The forward is a WP-2 create carrying the resume
38274
+ // args through the EXISTING allowlist; not registered / not reachable keeps today's flow,
38275
+ // byte-for-byte (resumeMachineFor returns null).
38276
+ const resumeMachine = remoteMachineCatalog.resumeMachineFor(launch.ctmSessionId || launch.id);
38277
+ if (resumeMachine) {
38278
+ const forwarded = await remoteMachineCatalog.forwardResumeOnMachine({ launch, machine: resumeMachine });
38279
+ if (!forwarded.ok) {
38280
+ res.writeHead(502, { 'Content-Type': 'application/json' });
38281
+ res.end(JSON.stringify({
38282
+ error: `Machine ${resumeMachine.displayName || resumeMachine.instanceId} could not start this resume`,
38283
+ code: forwarded.code || 'machine_unreachable',
38284
+ machine: resumeMachine.instanceId,
38285
+ machineName: resumeMachine.displayName || '',
38286
+ }));
38287
+ return;
38288
+ }
38289
+ res.writeHead(201, { 'Content-Type': 'application/json' });
38290
+ res.end(JSON.stringify({
38291
+ ok: true,
38292
+ id: forwarded.sessionId,
38293
+ tab_id: forwarded.sessionId,
38294
+ source_ctm_session_id: launch.ctmSessionId,
38295
+ session_id: launch.resumeSessionId,
38296
+ agent: launch.agentType,
38297
+ cwd: launch.cwd,
38298
+ title: launch.title,
38299
+ machine: resumeMachine.instanceId,
38300
+ machineName: resumeMachine.displayName || '',
38301
+ }));
38302
+ return;
38303
+ }
37972
38304
  await handleCreatePrepared(null, {
37973
38305
  id: launch.id,
37974
38306
  title: launch.title,
@@ -40063,7 +40395,9 @@ function sendDeferredRestoreStatus(ws, session, sessionId, cols, rows, source, o
40063
40395
  id: sessionId,
40064
40396
  cached: served,
40065
40397
  starting,
40066
- reason: String(opts.reason || ''),
40398
+ // WP-3 (§5 D-7): fill the existing, always-'' field with the session's last skip reason
40399
+ // (no new wire name, no client change; an explicit caller reason still wins).
40400
+ reason: String(opts.reason || session?._lastRestoreSkip?.reason || ''),
40067
40401
  message: opts.message || (starting
40068
40402
  ? 'Starting terminal process for the visible session.'
40069
40403
  : 'Session terminal process is deferred until this tab is activated.'),
@@ -40073,9 +40407,10 @@ function sendDeferredRestoreStatus(ws, session, sessionId, cols, rows, source, o
40073
40407
  }
40074
40408
 
40075
40409
  // Extracted to lib/restore-prepare-retry.js (createScheduleDeferredRestoreStart); semantics unchanged.
40076
- const _scheduleDeferredRestoreStart = createScheduleDeferredRestoreStart({ getSession, startDeferredRestoreSession: (session, opts) => _startDeferredRestoreSession(session, opts) });
40410
+ // WP-3 (§9 S-16): onSkip is the wakeSkips sink -- reason plumbing only, no return value changes.
40411
+ const _scheduleDeferredRestoreStart = createScheduleDeferredRestoreStart({ getSession, startDeferredRestoreSession: (session, opts) => _startDeferredRestoreSession(session, opts), onSkip: _restoreWakeSkipSink });
40077
40412
 
40078
- const _scheduleDeferredRestoreRetry = createScheduleDeferredRestoreRetry({ getSession, recordSessionDiagnostic, scheduleStart: _scheduleDeferredRestoreStart }); // WP-2 (D-4)
40413
+ const _scheduleDeferredRestoreRetry = createScheduleDeferredRestoreRetry({ getSession, recordSessionDiagnostic, scheduleStart: _scheduleDeferredRestoreStart, onSkip: _restoreWakeSkipSink }); // WP-2 (D-4)
40079
40414
 
40080
40415
  function _maybeAutoStartDeferredRestore(session, msg, source, ws) {
40081
40416
  if (!session?._restoreDeferred || !msg?.autoStartRestore) return false;
@@ -40088,6 +40423,9 @@ function _maybeAutoStartDeferredRestore(session, msg, source, ws) {
40088
40423
  reason: msg.prewarm === true ? 'prewarm-disabled' : 'background-client',
40089
40424
  clientKind: _lifecycleClientKind(ws, msg),
40090
40425
  });
40426
+ // WP-3 (§9 S-19 / D-8 AutoStartNotAttach): same latch as every other skip -- a background
40427
+ // client that keeps re-attaching records once per episode, not once per message.
40428
+ _noteWakeSkip(session, 'AutoStartNotAttach', { autostartBlock: msg.prewarm === true ? 'prewarm-disabled' : 'background-client' });
40091
40429
  return false;
40092
40430
  }
40093
40431
  if (!shouldAutoStartDeferredRestore(session, msg, source)) {
@@ -40969,12 +41307,12 @@ function handleResumeRetry(ws, msg) {
40969
41307
  // A quarantine record gets its full facts again; no record means there is nothing blocking.
40970
41308
  const record = id ? getSessionResumeBlocked(id) : null;
40971
41309
  const send = (reply) => { if (ws) try { _wsSend(ws, JSON.stringify(reply)); } catch { /* expected: the asking socket closed; the next attach replays the record */ } };
40972
- const wire = (r) => resumeBlockedWire(id, r, { agentType: session?.agentType, rungWire: _resumeBlockedRungWire(r) });
41310
+ const wire = (r) => resumeBlockedWire(id, r, { agentType: session?.agentType, rungWire: resumeBlockedRungWire(r) });
40973
41311
  if (record) { // Check again re-asks the ladder for any blocked record; a stale record is never replayed as the answer
40974
41312
  Promise.resolve().then(() => { // every outcome replies: a throw anywhere below answers with the unchanged (stale) record
40975
41313
  const agent = dbModule.getSessionIdentity(id)?.primaryAgent || {};
40976
41314
  return require('./lib/resume-preflight').recheckResumeBlockedRecord(record, { resolveRung: (input) => require('./lib/resume-from-store-client').resolveResumeRungOffThread({ ...input, dbPath: dbModule.getDbPath(), trashRoot: SESSION_TRANSCRIPT_TRASH_ROOT }), input: { ctmSessionId: id, agentSessionId: String(agent.agent_session_id || ''), provider: record.agentType || session?.agentType, jsonlPath: String(agent.jsonl_path || '') } });
40977
- }).then((fresh) => { markSessionResumeBlocked(id, fresh); send(wire(getSessionResumeBlocked(id) || fresh)); })
41315
+ }).then((fresh) => { markSessionResumeBlocked(id, fresh, 'resume-retry-recheck'); send(wire(getSessionResumeBlocked(id) || fresh)); })
40978
41316
  .catch((error) => { console.warn(`[resume-retry] recheck failed for ${id.slice(0, 8)}: ${error && error.message}`); send(wire(record)); });
40979
41317
  } else send(record ? wire(record) : { type: 'resume-retry-noop', id, checkedAt: new Date().toISOString() });
40980
41318
  return false;
@@ -41580,11 +41918,11 @@ function _imageDeliveryAckWindowMs() {
41580
41918
  }
41581
41919
 
41582
41920
  // The classified failure reason a refused/no-op image write leaves on the draft. Kept next to the
41583
- // delivery path so the rollback reason names the real cause (a post that never left the process, or a
41584
- // host ack that never arrived) instead of a generic "could not be started".
41921
+ // delivery path so refusal, uncertain dispatch and a missing host ack stay distinct rather than
41922
+ // claiming that every failed publication was known not to have left this process.
41585
41923
  function _imageWriteFailureReason(error) {
41586
41924
  const code = String((error && error.code) || '');
41587
- if (code === 'pty_write_not_posted') return code;
41925
+ if (code === 'pty_write_not_posted' || code === 'pty_dispatch_uncertain') return code;
41588
41926
  if (code.startsWith('pty_write_ack_')) return code.slice(0, 64);
41589
41927
  return 'Provider delivery could not be started';
41590
41928
  }
@@ -41668,14 +42006,14 @@ async function _deliverImageDraftAttempt(sessionId, attachmentId, deliveryAttemp
41668
42006
  // draw from, so this frame's ack number is unique for the session and can never be confused with a
41669
42007
  // keystroke's echo identity (which is tracked by map membership, not by a numeric range).
41670
42008
  const writeResult = _writeLifecyclePty(session, payload, 'image-input');
41671
- // A POST is not a write: HostedPtyAdapter.write only queues the frame, and the byte-write fact
41672
- // arrives later as this inputSeq's host 'input-ack'. A frame that never left this process
41673
- // (posted === false — the device-reply firewall dropped it, the PTY rejected it, or the frozen
41674
- // branch discarded it) can never be acked, and the draft must not conclude that it was delivered:
41675
- // take the EXISTING failure transition (the catch below rolls the attempt back to 'failed').
42009
+ // A POST is not a byte write. Only an observed preflight refusal means this invocation
42010
+ // did not enter a transport; an exception after publication may still have effects.
42011
+ // Keep the existing failure transition, but never label uncertain dispatch "not posted".
41676
42012
  if (!writeResult || writeResult.posted !== true) {
41677
- const error = new Error('Image paste frame was not posted to the host PTY');
41678
- error.code = 'pty_write_not_posted';
42013
+ const refused = writeResult?.dispatchDisposition === 'preflight-refused';
42014
+ const error = new Error(refused ? 'Image paste frame was not posted to the host PTY'
42015
+ : 'Image paste dispatch could not be confirmed');
42016
+ error.code = refused ? 'pty_write_not_posted' : 'pty_dispatch_uncertain';
41679
42017
  throw error;
41680
42018
  }
41681
42019
  // §6.3/D6: a PATH-mode draft reaches 'attached' only on the host's actual written ack — a
@@ -44679,7 +45017,7 @@ function handleKill(ws, msg) {
44679
45017
  async function handleKillAsync(ws, msg) {
44680
45018
  if (_ownerRecovery.rejectRequest(ws, msg)) return null; const session = sessions.get(msg.id);
44681
45019
  if (!session) return;
44682
- const ownerDecision = await _sessionOwnerDecisionAsync(msg.id);
45020
+ const ownerDecision = _sessionOwnerDecision(msg.id);
44683
45021
  if (!ownerDecision.ok) {
44684
45022
  if (_ownerDecisionInvalidRequest(ownerDecision)) return;
44685
45023
  _emitOwnerMismatchDiagnostic(ownerDecision);
@@ -44713,6 +45051,8 @@ async function handleKillAsync(ws, msg) {
44713
45051
  }
44714
45052
  session._intentionalKill = true;
44715
45053
  session._userCloseAccepted = true; // inventory stops listing it now; the kill always ends in exit + delete
45054
+ // D-6: announce the close before anything is awaited, so no racing list can put the tab back.
45055
+ broadcastSessionList(true);
44716
45056
  if (RESUME_LIFECYCLE_V2_ENABLED && session._resumeAttemptState) {
44717
45057
  _resumeLifecycleCoordinator.dispatch(session, {
44718
45058
  type: RESUME_EVENT.CANCEL,
@@ -44732,14 +45072,12 @@ async function handleKillAsync(ws, msg) {
44732
45072
  broadcastSessionList(true);
44733
45073
  return;
44734
45074
  }
44735
- // Retire the startup_task durably on an explicit user close, BEFORE killing the
44736
- // PTY. Don't rely solely on the PTY onExit handler to retire it: onExit
44737
- // early-returns during shutdown (`if (_isShuttingDown) return`), so closing a
44738
- // session and then restarting CTM before the PTY finishes exiting would leave
44739
- // the row at status='running' and the session would be resurrected by restore on
44740
- // the next boot. The onExit retire stays as an idempotent backstop.
44741
- await _retireStartupTaskForUserClose(msg.id, 'kill');
45075
+ // D-6: submit the durable startup_task retire FIRST — the op FIFO-queues on the DB owner, so a
45076
+ // restart racing this exit still finds it queued — then signal the PTY at once and await the
45077
+ // retire after. The PTY onExit retire stays as the idempotent backstop.
45078
+ const retire = _retireStartupTaskForUserClose(msg.id, 'kill');
44742
45079
  try { _killSessionProcess(session, 'user_close'); } catch { /* expected: a PTY already gone on user close must not fail the close handler */ }
45080
+ await retire;
44743
45081
  }
44744
45082
 
44745
45083
  // Delegates to lib/session-workspace-binding.js's TTL-memoized safeRealpath (60s TTL, bounded
@@ -46992,7 +47330,10 @@ function handleList(ws) {
46992
47330
  const listPayload = JSON.stringify({
46993
47331
  type: 'sessions',
46994
47332
  restoring: _restoreInProgress,
46995
- sessions: _listableSessions().map(s => _sessionPayload(s, { lightweight: true })),
47333
+ // WP-6 (S-3/S-4, AC-6c): local live rows ∪ remote rows, deduped by ctm session id with a LOCAL
47334
+ // id always winning. With zero registered machines mergeSessionRows returns this exact array, so
47335
+ // the payload is byte-identical to the pre-WP-6 one.
47336
+ sessions: remoteMachineCatalog.mergeSessionRows(_listableSessions().map(s => _sessionPayload(s, { lightweight: true }))),
46996
47337
  });
46997
47338
  _wsSend(ws, listPayload, { class: 'interactive', channel: 'sessions' });
46998
47339
  _kickPendingAutoResumeDeferred('client-list');
@@ -48638,7 +48979,7 @@ function _sessionPayload(s, options = {}) {
48638
48979
  // WP-2: only set on the two missing-transcript codes.
48639
48980
  resumeBlockedLocalHistory: resumeBlocked?.localHistory || '', resumeBlockedLastSeenBytes: Math.max(0, Number(resumeBlocked?.lastSeenBytes || 0)),
48640
48981
  resumeBlockedLastSeenAt: resumeBlocked?.lastSeenAt || '', resumeBlockedTrashPath: resumeBlocked?.trashPath || '',
48641
- resumeBlockedOriginalPath: resumeBlocked?.originalPath || '',
48982
+ resumeBlockedOriginalPath: resumeBlocked?.originalPath || '', resumeBlockedTrashReason: resumeBlocked?.trashReason || '', resumeBlockedTrashedAt: resumeBlocked?.trashedAt || '',
48642
48983
  resumeBlockedRung: resumeBlocked?.resumeRung || '', resumeBlockedCoverage: resumeBlocked?.coverage || null,
48643
48984
  resumeBlockedOfferStoreFirst: resumeBlocked?.offerStoreFirst === true, resumeBlockedRestoreError: resumeBlocked?.restoreError || '',
48644
48985
  resumeBlockedOrigin: resumeBlocked?.origin || 'unknown',
@@ -50167,8 +50508,13 @@ const _broadcastPayload = require('./lib/broadcast-serialize').createBroadcastPa
50167
50508
  // `arr` is the session snapshot the payloads were built from (used for the elision signature so it stays
50168
50509
  // consistent with `built` even if the live sessions Map mutated during a sliced build's yields).
50169
50510
  function _finishBroadcast(arr, built, fresh) {
50170
- return _broadcastPayload.broadcast({ type: 'sessions', restoring: _restoreInProgress, sessions: built }, wss.clients,
50171
- { fresh, elideNoop: _BROADCAST_ELIDE_NOOP, sourceSessions: arr, isMemoBypass: s => _lightPayloadMemo.isMemoBypass(s) });
50511
+ // WP-6 (S-3/S-4): the same merge as handleList, so a broadcast row set matches a requested list.
50512
+ // `sourceSessions: arr` stays the LOCAL snapshot (it is the elision signature's subject); the
50513
+ // remote rows are appended after the merge and are cheap, already-built objects.
50514
+ const merged = remoteMachineCatalog.mergeSessionRows(built);
50515
+ return _broadcastPayload.broadcast({ type: 'sessions', restoring: _restoreInProgress, sessions: merged }, wss.clients,
50516
+ { fresh, elideNoop: _BROADCAST_ELIDE_NOOP, sourceSessions: arr, extraSignature: remoteMachineCatalog.lastMergeSignature(),
50517
+ isMemoBypass: s => _lightPayloadMemo.isMemoBypass(s) });
50172
50518
  }
50173
50519
 
50174
50520
  function _maybePrewarmRestoreBroadcastPayloadCaches(sessionList) {
@@ -51259,7 +51605,8 @@ async function _quiesceSessionsForProcessExit(reason = 'shutdown') {
51259
51605
  }
51260
51606
 
51261
51607
  // --- Graceful Shutdown ---
51262
- function shutdown() {
51608
+ // R3 WP-2: `signal` ('SIGTERM'/'SIGINT', passed by Node) is recorded in the clean-shutdown marker's reason.
51609
+ function shutdown(signal) {
51263
51610
  if (_isShuttingDown) return;
51264
51611
  console.log('\n Shutting down...');
51265
51612
  _isShuttingDown = true;
@@ -51341,6 +51688,8 @@ function shutdown() {
51341
51688
  new Promise((resolve) => { const t = setTimeout(resolve, 3000); if (t && typeof t.unref === 'function') t.unref(); }),
51342
51689
  ]);
51343
51690
  } catch (e) { console.error('[shutdown] walle-transcript flush failed:', e.message); }
51691
+ let inFlight;
51692
+ try { inFlight = _collectInFlightSessionsForShutdown(); } catch { inFlight = undefined; /* expected: exit must never be blocked; marker omits inFlight */ }
51344
51693
  await _quiesceSessionsForProcessExit('shutdown');
51345
51694
  try { await headlessWorker.terminate({ intent: 'restart' }); } catch {} // hosts linger through SIGTERM (WP-2)
51346
51695
  _ctmDbClosingForProcessExit = true;
@@ -51354,6 +51703,9 @@ function shutdown() {
51354
51703
  if (_imageWriteProofs) _imageWriteProofs.dispose();
51355
51704
  try { await _desktopIndex.stop(); } catch {}
51356
51705
  const dbExit = await _closeDbForProcessExit('shutdown'); // D-7
51706
+ // R3 WP-2: a graceful signal stop is a clean exit, marked after the DB close; fatal/SIGKILL stay unmarked.
51707
+ const reason = `shutdown:${typeof signal === 'string' && signal ? signal : 'unknown'}`;
51708
+ _restartLedger.writeCleanShutdownMarker({ exitCode: 0, reason, oldPid: process.pid, dbClose: _dbCloseOutcome(dbExit), inFlight });
51357
51709
  try { await managedBrowserBroker.close(); } catch (e) { console.error('[shutdown] managed browser close failed:', e.message); }
51358
51710
  try { await runtimeGoalStore.close?.(); } catch (e) { console.error('[shutdown] runtime goal store close failed:', e.message); }
51359
51711
  // Kill Wall-E child so it doesn't outlive us (detached + unref'd = orphan
@@ -51538,7 +51890,21 @@ function _startSdWatchdog() {
51538
51890
  function _startHostedSyncWorker() {
51539
51891
  if (_hostedSyncWorker) return;
51540
51892
  try {
51541
- const worker = startHostedSyncWorker({ env: process.env, db: dbModule, fetch: global.fetch });
51893
+ const worker = startHostedSyncWorker({
51894
+ env: process.env,
51895
+ db: dbModule,
51896
+ fetch: global.fetch,
51897
+ // WP-2 / P-8: the machine probe's credential is chosen PER TARGET — the shared pool secret for
51898
+ // a pool member, the legacy env bearer for a box that never joined. The builder is injected
51899
+ // from here because lib/remote-machine-create.js requires lib/hosted-sync-worker.js (for
51900
+ // parseSyncPeers), so the worker requiring it back would close a lib/** dependency cycle
51901
+ // (dependency-lint ceiling 0).
51902
+ peerHeadersFor: (instanceId) => require('./lib/remote-machine-create').buildPeerControlHeaders(process.env, { instanceId }) || {},
51903
+ // WP-2 / P-5: this machine's own minted id for the sweep's roster pull (a member never writes
51904
+ // its own row). Injected from here for the same cycle reason; read lazily so it is the id the
51905
+ // boot ladder actually minted, not a pre-DB guess.
51906
+ selfInstanceId: () => _instanceIdentityAccess.getInstanceIdentity({ required: false })?.instanceId || '',
51907
+ });
51542
51908
  _hostedSyncWorker = worker;
51543
51909
  if (worker.started) console.log(`[sync] hosted sync worker started for ${worker.peers.length} peer(s)`);
51544
51910
  } catch (err) {
@@ -51781,10 +52147,20 @@ function _executeHook(hook, data) {
51781
52147
  }
51782
52148
  }
51783
52149
 
51784
- async function _getWalleStatusSafe() {
52150
+ // D-7 (WP-5, docs/clock-free-tests-followups-2026-10-23.html §6) + r3 R-3/R-4
52151
+ // (docs/clock-free-tests-followups-r3-2026-10-24.html §5): EVERY GET path that reports whether
52152
+ // Wall-E is up — /api/services/status, /api/instance/manifest, /api/instance/sync-status and the
52153
+ // hosted runtime context behind /readyz, setup-status and friends — passes { snapshot: true } and
52154
+ // gets the supervisor's cached status (revalidated with kill(pid, 0), single-flight on a miss)
52155
+ // instead of running kill-authority discovery — a whole-table `ps` — on every request. The default
52156
+ // getStatus() is left to the paths that need fresh ownership: kill and repair (lib/walle-repair.js)
52157
+ // and the walle-bridge unavailable context (getWalleSupervisorStatus), which is a failure path.
52158
+ async function _getWalleStatusSafe({ snapshot = false } = {}) {
51785
52159
  let walleStatus;
51786
52160
  try {
51787
- walleStatus = await walleSupervisor.getStatus();
52161
+ walleStatus = snapshot
52162
+ ? await walleSupervisor.getStatusSnapshot()
52163
+ : await walleSupervisor.getStatus();
51788
52164
  } catch (err) {
51789
52165
  walleStatus = { running: false, pid: null, error: err.message };
51790
52166
  }
@@ -51810,6 +52186,9 @@ function _buildInstanceManifest(walleStatus, ctmUptime) {
51810
52186
  golivePlan: _golivePlanCache,
51811
52187
  observedOrigin: _lastMicrosoftTunnelOrigin,
51812
52188
  nowMs: Date.now(),
52189
+ // WP-2a D-4: the machine-owned settings come from the in-memory cache (lib/machine-self.js),
52190
+ // never a DB read on the manifest/ETag path.
52191
+ machineSelf: require('./lib/machine-self').current(),
51813
52192
  });
51814
52193
  }
51815
52194
 
@@ -52082,7 +52461,7 @@ function _storageRiskSnapshot() {
52082
52461
 
52083
52462
  async function _buildHostedRuntimeContextUncached() {
52084
52463
  const ctmUptime = Math.floor((Date.now() - _ctmStartTime) / 1000);
52085
- const walleStatus = await _getWalleStatusSafe();
52464
+ const walleStatus = await _getWalleStatusSafe({ snapshot: true }); // R-4: never the full ownership probe or the recovery await
52086
52465
  const instance = _buildInstanceManifest(walleStatus, ctmUptime);
52087
52466
  const envWorkspaceBindings = buildWorkspaceBindingsStatus({ manifest: instance, env: process.env });
52088
52467
  void _detachOwnerMutation('hosted-workspace-bindings-seed', () =>
@@ -52097,7 +52476,9 @@ async function _buildHostedRuntimeContextUncached() {
52097
52476
  const localEdge = buildLocalEdgeStatus({ manifest: instance, env: process.env });
52098
52477
  const syncSchema = hostedStorage.syncSchema;
52099
52478
  syncSchema.worker = _hostedSyncWorkerStatus();
52100
- const sync = buildInstanceSyncStatus({ manifest: instance, workspaceBindings, localEdge, env: process.env, schema: syncSchema });
52479
+ // S-30 (WP-0): the machine registry's rollup rides the sync status. Machine rows are read on
52480
+ // the main thread (they are one indexed SELECT), so no owner round-trip is needed here.
52481
+ const sync = buildInstanceSyncStatus({ manifest: instance, workspaceBindings, localEdge, env: process.env, schema: syncSchema, machines: machineRegistry.summarizeMachines({ db: dbModule }) });
52101
52482
  const security = await _buildRemoteSecuritySummary(instance);
52102
52483
  const terminalUx = buildTerminalRemoteUxStatus({ manifest: instance, env: process.env });
52103
52484
  const deadMan = buildDeadManStatus({ env: process.env });
@@ -52180,7 +52561,7 @@ function _manifestCorsHeaders(req) {
52180
52561
 
52181
52562
  async function apiInstanceManifest(req, res) {
52182
52563
  const ctmUptime = Math.floor((Date.now() - _ctmStartTime) / 1000);
52183
- const walleStatus = await _getWalleStatusSafe();
52564
+ const walleStatus = await _getWalleStatusSafe({ snapshot: true }); // D-7 (WP-5): status GETs read the snapshot, not the process table
52184
52565
  const manifest = _buildInstanceManifest(walleStatus, ctmUptime);
52185
52566
  sendConditionalJson(req, res, 200, manifest, _manifestCorsHeaders(req), { etagBody: _manifestEtagBody(manifest) }); // D-7 fix A: the tag skips per-read noise the phone never reads
52186
52567
  }
@@ -52888,8 +53269,6 @@ function fetchMcpGatewayLedgerStatus(timeoutMs = 300) {
52888
53269
  async function apiServicesStatus(req, res) {
52889
53270
  const {
52890
53271
  ctmUptime,
52891
- walleStatus,
52892
- instance,
52893
53272
  sync,
52894
53273
  workspaceBindings,
52895
53274
  localEdge,
@@ -52898,6 +53277,8 @@ async function apiServicesStatus(req, res) {
52898
53277
  deadMan,
52899
53278
  hostedReadiness,
52900
53279
  } = await _buildHostedRuntimeContext();
53280
+ const walleStatus = await _getWalleStatusSafe({ snapshot: true }); // R-3: per request, never the 20 s hosted context
53281
+ const instance = _buildInstanceManifest(walleStatus, ctmUptime); // R-3: the manifest block uses the same snapshot
52901
53282
  const mcpGateway = await fetchMcpGatewayStatus();
52902
53283
  const walleRuntimeStatus = walleStatus.running
52903
53284
  ? await getWalleStatusDataOrNull(500)
@@ -53538,6 +53919,22 @@ function _recordWorktreeCreateJobProgress(job, event) {
53538
53919
  };
53539
53920
  job.progress = progress;
53540
53921
  job.updatedAt = now;
53922
+ // WP-4 (nightly-browser-tier-round2-2026-10-21 §6): this is the ONE chokepoint every
53923
+ // job transition passes through, so the log lines live here instead of in a response
53924
+ // observer in lib/api/worktrees.js -- that observer only saw the two dispatch routes,
53925
+ // so the GET poll route and 9 of 13 transitions never logged at all. One line per
53926
+ // distinct (stage, status); the transition is recorded on the job so a repeated
53927
+ // progress event (same stage+status) does not repeat the line.
53928
+ if (job._loggedStage !== progress.stage || job._loggedStatus !== progress.status) {
53929
+ job._loggedStage = progress.stage;
53930
+ job._loggedStatus = progress.status;
53931
+ const line = `[worktree-job] id=${job.id} stage=${progress.stage} status=${progress.status} elapsedMs=${progress.elapsedMs}`
53932
+ + (progress.stage === 'queued'
53933
+ ? ` name=${JSON.stringify(job.name)} namespace=${job.namespace || ''} cwd=${JSON.stringify(job.cwd || '')}`
53934
+ : '');
53935
+ if (progress.status === 'failed' || progress.status === 'cancelled') console.warn(line);
53936
+ else console.log(line);
53937
+ }
53541
53938
  job.events.push({
53542
53939
  at: now,
53543
53940
  stage: progress.stage,
@@ -54181,15 +54578,24 @@ function apiAdoptBranch(req, res) {
54181
54578
  });
54182
54579
  }
54183
54580
 
54581
+ // A dispatch refused before a job exists never reaches _recordWorktreeCreateJobProgress,
54582
+ // so it logs its own one-liner with the same [worktree-job] prefix: a red browser artifact
54583
+ // can then tell "CTM refused the dispatch" apart from "the job started and never advanced".
54584
+ function _logWorktreeCreateJobRejected(code, message) {
54585
+ console.warn(`[worktree-job] rejected code=${code} error=${JSON.stringify(String(message || ''))}`);
54586
+ }
54587
+
54184
54588
  function apiCreateWorktree(req, res) {
54185
54589
  _worktreeReadJsonBody(req, res, async (payload) => {
54186
54590
  const reqUrl = new URL(req.url, 'http://localhost');
54187
54591
  const { name, base_branch, cwd: reqCwd, namespace, agentType } = payload;
54188
54592
  if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) {
54593
+ _logWorktreeCreateJobRejected('invalid_name', 'Invalid worktree name. Use alphanumeric, hyphens, underscores only.');
54189
54594
  res.writeHead(400, WORKTREE_JSON_HEADERS);
54190
54595
  return res.end(JSON.stringify({ error: 'Invalid worktree name. Use alphanumeric, hyphens, underscores only.' }));
54191
54596
  }
54192
54597
  if (base_branch && !VALID_GIT_REF.test(base_branch)) {
54598
+ _logWorktreeCreateJobRejected('invalid_base_branch', 'Invalid base branch name');
54193
54599
  res.writeHead(400, WORKTREE_JSON_HEADERS);
54194
54600
  return res.end(JSON.stringify({ error: 'Invalid base branch name' }));
54195
54601
  }
@@ -54201,6 +54607,7 @@ function apiCreateWorktree(req, res) {
54201
54607
  const rawRequestId = payload.request_id || payload.requestId || '';
54202
54608
  const requestId = normalizeWorktreeCreateRequestId(rawRequestId);
54203
54609
  if (rawRequestId && !requestId) {
54610
+ _logWorktreeCreateJobRejected('invalid_request_id', 'Invalid worktree create request id.');
54204
54611
  res.writeHead(400, WORKTREE_JSON_HEADERS);
54205
54612
  return res.end(JSON.stringify({ error: 'Invalid worktree create request id.' }));
54206
54613
  }
@@ -54216,6 +54623,7 @@ function apiCreateWorktree(req, res) {
54216
54623
  job = findReusableWorktreeCreateJob(_worktreeCreateJobs, createParams)
54217
54624
  || _startWorktreeCreateJob(createParams);
54218
54625
  } catch (e) {
54626
+ _logWorktreeCreateJobRejected(e.code || 'WORKTREE_CREATE_FAILED', e.message);
54219
54627
  res.writeHead(e.statusCode || 500, WORKTREE_JSON_HEADERS);
54220
54628
  return res.end(JSON.stringify({ error: e.message, code: e.code || 'WORKTREE_CREATE_FAILED' }));
54221
54629
  }
@@ -54575,7 +54983,10 @@ function apiSyncAllWorktrees(req, res) {
54575
54983
  });
54576
54984
  }
54577
54985
 
54578
- function _restartGuardSnapshot(now = Date.now()) {
54986
+ // WP-3 D-10 (model-proxy-survives-ctm-restart): `proxy` is the model-proxy verdict from
54987
+ // oauthProxySupervisor.proxyPreflight() -- a LIVE probe, so only async callers can supply it
54988
+ // (GET /api/restart/ctm/preflight does); the sync callers get the preflight's own default.
54989
+ function _restartGuardSnapshot(now = Date.now(), { proxy = null } = {}) {
54579
54990
  return summarizeRestartGuard(sessions.values(), {
54580
54991
  statusForSession: (session) => _standupLiveStatusForSession(session, now),
54581
54992
  isWaitingForInput: (session) => _isServerWaitingForInput(session.id, session, now),
@@ -54584,7 +54995,7 @@ function _restartGuardSnapshot(now = Date.now()) {
54584
54995
  // WP-4 (S-16): reuse the existing hosted-session discriminator (WP-2's
54585
54996
  // `_sessionIsHosted`, defined above at _quiesceSessionsForProcessExit) --
54586
54997
  // do not redefine it here.
54587
- isHostedSession: _sessionIsHosted, preflight: _computeRestartPreflight({ sessions: sessions.values(), sessionHostManager, isHosted: _sessionIsHosted, isRunning: (s) => !!s.ptyProcess, isExited: (s) => _standupLiveStatusForSession(s, now) === 'exited', hostCodeDiffers: (hostId) => !!sessionHostManager?.hostCodeDiffersFromDisk?.(hostId) }), // WP-6 (S-22); WP-2: loaded code vs disk
54998
+ isHostedSession: _sessionIsHosted, preflight: _computeRestartPreflight({ sessions: sessions.values(), sessionHostManager, isHosted: _sessionIsHosted, isRunning: (s) => !!s.ptyProcess, isExited: (s) => _standupLiveStatusForSession(s, now) === 'exited', hostCodeDiffers: (hostId) => !!sessionHostManager?.hostCodeDiffersFromDisk?.(hostId), proxy }), // WP-6 (S-22); WP-2: loaded code vs disk; WP-3 D-10: proxy verdict
54588
54999
  });
54589
55000
  }
54590
55001
 
@@ -55881,6 +56292,7 @@ function _announceDeferredRestoreRetry(session, opts) {
55881
56292
  // Extracted to lib/restore-backoff-gate.js (semantics unchanged; 'prepare-retry' is user-initiated per design D-4).
55882
56293
  const _awaitRestoreBackoffOutsideGate = createAwaitRestoreBackoffOutsideGate({
55883
56294
  enabled: _RESTORE_BACKOFF_ENABLED, restoreBackoff: _restoreBackoff, recordSessionDiagnostic, getSession, maxCooldownMs: _RESTORE_BACKOFF_MAX_MS,
56295
+ onSkip: _restoreWakeSkipSink, // WP-3 (§9 S-10/S-11): BackoffTaskMissingSessionId / BackoffSessionReplaced
55884
56296
  });
55885
56297
 
55886
56298
  function _startDeferredRestoreSession(session, opts = {}) {
@@ -55954,12 +56366,18 @@ async function _startDeferredRestoreSessionAttempt(session, opts = {}) {
55954
56366
  }
55955
56367
  }
55956
56368
  if (sharedCapacityAdmission) {
55957
- if (!capacity.ok) { _endInteractiveActivationIfNeeded(); return false; }
56369
+ // WP-3 (§9 S-12): a shared admission that came back refused is a skip of THIS activation.
56370
+ if (!capacity.ok) {
56371
+ _noteWakeSkip(session, 'CapacityRejected', {}, id);
56372
+ _endInteractiveActivationIfNeeded();
56373
+ return false;
56374
+ }
55958
56375
  if (session._activationAdmissionPromise) { _endInteractiveActivationIfNeeded(); return session._activationAdmissionPromise; }
55959
56376
  }
55960
56377
  if (!capacity.ok) {
55961
56378
  session._lifecycleState = session._parkedAt ? 'parked' : 'deferred';
55962
56379
  _notifyLifecycleWakeBlocked(session, capacity);
56380
+ _noteWakeSkip(session, 'WakeBlocked', {}, id); // WP-3 (§9 S-12)
55963
56381
  console.log(`[lifecycle] wake-blocked id=${id.slice(0, 8)} reason=${capacity.code || 'live-session-cap'}`);
55964
56382
  _endInteractiveActivationIfNeeded();
55965
56383
  return false;
@@ -56035,7 +56453,10 @@ async function _startDeferredRestoreSessionAttempt(session, opts = {}) {
56035
56453
  session._lifecycleState = session._parkedAt ? 'parked' : 'deferred';
56036
56454
  _scheduleStandupMaterializedRefresh(`lifecycle:deferred:${id.slice(0, 8)}`, 0, { markDirty: true });
56037
56455
  _clearLifecyclePrewarm(session);
56038
- _lifecycleDiagnostics.recordPrewarm({ id, outcome: 'skipped', reason: 'gate-busy' });
56456
+ // WP-3 (§9 S-17, AC-3d): recordPrewarm fires ONLY for a real prewarm; the non-prewarm case is a
56457
+ // genuine restore skip, counted GateBusy instead of polluting prewarmByOutcome.
56458
+ if (prewarm) _lifecycleDiagnostics.recordPrewarm({ id, outcome: 'skipped', reason: 'gate-busy' });
56459
+ else _noteWakeSkip(session, 'GateBusy', {}, id);
56039
56460
  console.log(`[lifecycle] prewarm-skip id=${id.slice(0, 8)} reason=gate-busy`);
56040
56461
  _endInteractiveActivationIfNeeded();
56041
56462
  return false;
@@ -56133,6 +56554,16 @@ function _setDeferredRestoreStarting(session, value, reason = '') {
56133
56554
  const next = value === true;
56134
56555
  if (session._restoreStarting === next) return false;
56135
56556
  session._restoreStarting = next;
56557
+ // WP-3 (§5 D-5, §9 S-15): this is the release funnel -- the ~20 sites that bail after claiming a
56558
+ // placeholder pass a reason that used to be discarded. Record it on all three surfaces.
56559
+ if (next) session._lastRestoreSkip = null; // the claim ends any skip episode (the D-7 wire reads it)
56560
+ if (!next && reason) {
56561
+ session._lastRestoreSkip = { reason, at: Date.now() };
56562
+ try { recordSessionDiagnostic(session.id, 'deferred-restore-skip', { reason }); }
56563
+ catch { /* expected: session diagnostics ring is not constructed in sliced unit harnesses */ }
56564
+ try { _lifecycleDiagnostics.recordWakeSkip({ id: String(session.id || ''), provider: _lifecycleProvider(session), reason: String(reason) }); }
56565
+ catch { /* expected: lifecycle diagnostics are not constructed in sliced unit harnesses */ }
56566
+ }
56136
56567
  _markHeartbeatDirty(session);
56137
56568
  return true;
56138
56569
  }
@@ -56144,7 +56575,12 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56144
56575
  return false;
56145
56576
  }
56146
56577
  let task = session._restoreTask || {};
56147
- if (!task.session_id) return false;
56578
+ // WP-3 (§9 S-14): a placeholder with no durable task row can never be claimed. (The
56579
+ // `_restoreStarting` re-entry above records nothing -- the in-flight attempt owns the outcome.)
56580
+ if (!task.session_id) {
56581
+ _noteWakeSkip(session, 'BackoffTaskMissingSessionId', {}, session.id);
56582
+ return false;
56583
+ }
56148
56584
  // WI-2/WI-6: quarantine short-circuit. A session already marked resume_blocked (deterministic
56149
56585
  // prerequisite failure from a prior attempt — missing transcript/rollout, agent CLI
56150
56586
  // unresolvable) must never be re-driven by ANY of the auto-retry triggers that funnel through
@@ -56156,7 +56592,10 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56156
56592
  // id stays quarantined until the deterministic cause changes or CTM restarts.
56157
56593
  const _blockedInfo = getSessionResumeBlocked(task.session_id);
56158
56594
  if (_blockedInfo) {
56159
- sendResumeBlockedToClients(session.clients, resumeBlockedWire(task.session_id, _blockedInfo, { agentType: session.agentType, rungWire: _resumeBlockedRungWire(_blockedInfo) }), _wsSend);
56595
+ // WP-0 (S-3): this replay re-sends an existing record without going through markSessionResumeBlocked,
56596
+ // so it writes the same diagnostic itself -- otherwise this card is unattributable.
56597
+ recordSessionDiagnostic(task.session_id, 'resume-blocked-set', resumeBlockSetDetails('deferred-restore-replay', _blockedInfo));
56598
+ sendResumeBlockedToClients(session.clients, resumeBlockedWire(task.session_id, _blockedInfo, { agentType: session.agentType, rungWire: resumeBlockedRungWire(_blockedInfo) }), _wsSend);
56160
56599
  return false;
56161
56600
  }
56162
56601
  if (task._codexLinkDeferred) {
@@ -56165,7 +56604,11 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56165
56604
  // operation instead of falling back to cwd/creation-time discovery.
56166
56605
  const codexLink = await _ensureCodexThreadLinkOffThread(task.session_id, 'deferred-restore-codex',
56167
56606
  { selectedIdentity: task._codexRestoreTarget || null });
56168
- if (codexLink?.status === 'stale') return false;
56607
+ if (codexLink?.status === 'stale') {
56608
+ // WP-3 (§9 S-13): carry the linker's own conflict reason (lib/codex-restore-linker.js).
56609
+ _noteWakeSkip(session, 'CodexLinkStale', { linkerReason: codexLink.reason || '' }, task.session_id);
56610
+ return false;
56611
+ }
56169
56612
  if (codexLink?.status === 'unavailable') {
56170
56613
  recordSessionDiagnostic(task.session_id, 'codex-link-resolution-deferred', {
56171
56614
  source: String(opts.source || ''),
@@ -56177,6 +56620,8 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56177
56620
  detail: codexLink.reason || 'Codex thread resolution is temporarily unavailable.',
56178
56621
  retryable: true,
56179
56622
  });
56623
+ // WP-3 (§9 S-13 / D-8 CodexLinkUnavailable): count the skip beside the existing records.
56624
+ _noteWakeSkip(session, 'CodexLinkUnavailable', { linkerReason: codexLink.reason || '' }, task.session_id);
56180
56625
  return false;
56181
56626
  }
56182
56627
  if (codexLink?.blocked) {
@@ -56184,13 +56629,15 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56184
56629
  code: 'codex_thread_owned_by_other_ctm',
56185
56630
  message: 'The saved Codex thread is already attached to another CTM session.',
56186
56631
  agentType: 'codex',
56187
- });
56632
+ }, 'codex-thread-owned');
56188
56633
  _recordRuntimeSessionLifecycle(session, 'degraded', {
56189
56634
  adapter: 'codex',
56190
56635
  reason: 'codex_thread_owned_by_other_ctm',
56191
56636
  detail: 'The saved Codex thread is already attached to another CTM session.',
56192
56637
  retryable: false,
56193
56638
  });
56639
+ // WP-3 (§9 S-13 / D-8 CodexLinkBlocked): a hard block is still a skip of this activation.
56640
+ _noteWakeSkip(session, 'CodexLinkBlocked', { linkerReason: codexLink.reason || '' }, task.session_id);
56194
56641
  return false;
56195
56642
  }
56196
56643
  if (codexLink?.agentSessionId) {
@@ -56226,7 +56673,10 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56226
56673
  sessionLaunchOverrides: session._launchEnvOverrides, options: opts,
56227
56674
  });
56228
56675
  if (restoreLaunchProjection.sourceRecoveryEnvironmentInvalid) {
56229
- _setDeferredRestoreStarting(session, false, 'source-recovery-environment-invalid');
56676
+ _setDeferredRestoreStarting(session, false, 'SourceRecoveryEnvironmentInvalid');
56677
+ // WP-3 (§9 S-15): PRE-CLAIM -- the funnel call above is a no-op here (_restoreStarting is already
56678
+ // false), so record the skip directly; the funnel call stays (D-9, no control-flow change).
56679
+ _noteWakeSkip(session, 'SourceRecoveryEnvironmentInvalid', {}, task.session_id);
56230
56680
  recordSessionDiagnostic(task.session_id, 'source-recovery-environment-rejected', {
56231
56681
  reason: 'captured_source_environment_owner_mismatch',
56232
56682
  });
@@ -56276,7 +56726,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56276
56726
  let launchGeneration = suppliedPlan.generation;
56277
56727
  if (launchGeneration <= 0) {
56278
56728
  const reserved = _codingSessionGenerationRuntime.revalidateReservation(await _codingSessionGenerationRuntime.reserveSessionGeneration(task.session_id, Math.max(inputGeneration - 1, Number.isSafeInteger(session._launchPlanGeneration) ? session._launchPlanGeneration : 0)));
56279
- if (!reserved.ok) { recordSessionDiagnostic(task.session_id, 'launch-generation-refused', { code: reserved.code, message: reserved.message }); _setDeferredRestoreStarting(session, false, 'launch-generation-unavailable'); _releaseResumeStormBudgetIfHeld(session); return false; }
56729
+ if (!reserved.ok) { recordSessionDiagnostic(task.session_id, 'launch-generation-refused', { code: reserved.code, message: reserved.message }); _setDeferredRestoreStarting(session, false, 'LaunchGenerationUnavailable'); _releaseResumeStormBudgetIfHeld(session); return false; }
56280
56730
  launchGeneration = reserved.generation;
56281
56731
  }
56282
56732
  session._launchPlanGeneration = launchGeneration;
@@ -56354,7 +56804,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56354
56804
  : _hydrateCanonicalAgentEventCaches([task.session_id]),
56355
56805
  ]);
56356
56806
  } catch (error) {
56357
- _setDeferredRestoreStarting(session, false, 'prepare-failed');
56807
+ _setDeferredRestoreStarting(session, false, 'PrepareFailed');
56358
56808
  // round-3 WP-3 T5 (S-24, I-8b): a read-pool ADMISSION failure (the lane was full/paused, or
56359
56809
  // the pool itself is disabled) is a fact about the POOL at this instant, never about this
56360
56810
  // session's data or its agent -- the prior code folded it into the same generic
@@ -56420,23 +56870,23 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56420
56870
  return false;
56421
56871
  }
56422
56872
  if (launchPlanResult?.status === 'stale') {
56423
- _setDeferredRestoreStarting(session, false, 'launch-plan-stale');
56873
+ _setDeferredRestoreStarting(session, false, 'LaunchPlanStale');
56424
56874
  return false;
56425
56875
  }
56426
56876
  if (sessions.get(task.session_id) !== session || !session._restoreDeferred) {
56427
- _setDeferredRestoreStarting(session, false, 'session-replaced-or-not-deferred');
56877
+ _setDeferredRestoreStarting(session, false, 'SessionReplaced');
56428
56878
  return false;
56429
56879
  }
56430
56880
  const rungHook = await _resolveMissingTranscriptRung(task.session_id, launchPlanResult.plan, detectAgentType(task.cmd));
56431
56881
  if (rungHook.restored) {
56432
56882
  // The archived copy is back on disk: prepare the launch again and continue natively.
56433
56883
  try { launchPlanResult = await runDeferredLaunchPlan(launchPlanResult.plan); } catch (error) {
56434
- _setDeferredRestoreStarting(session, false, 'prepare-failed');
56884
+ _setDeferredRestoreStarting(session, false, 'PrepareFailed');
56435
56885
  errorSink.report('deferred-restore:replan-after-restore', error);
56436
56886
  return false;
56437
56887
  }
56438
56888
  if (launchPlanResult?.status === 'stale') {
56439
- _setDeferredRestoreStarting(session, false, 'launch-plan-stale');
56889
+ _setDeferredRestoreStarting(session, false, 'LaunchPlanStale');
56440
56890
  return false;
56441
56891
  }
56442
56892
  }
@@ -56450,7 +56900,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56450
56900
  rungHook.restored ? launchPlanResult.plan.resume?.preflight : (rungHook.preflight || launchPlanResult.plan.resume?.preflight),
56451
56901
  );
56452
56902
  const _planBlocked = getSessionResumeBlocked(task.session_id);
56453
- if (_planBlocked) sendResumeBlockedToClients(session.clients, resumeBlockedWire(task.session_id, _planBlocked, { agentType: session.agentType, rungWire: _resumeBlockedRungWire(_planBlocked) }), _wsSend);
56903
+ if (_planBlocked) sendResumeBlockedToClients(session.clients, resumeBlockedWire(task.session_id, _planBlocked, { agentType: session.agentType, rungWire: resumeBlockedRungWire(_planBlocked) }), _wsSend);
56454
56904
  recordSessionDiagnostic(task.session_id, 'deferred-restore-blocked', {
56455
56905
  code,
56456
56906
  source: _restoreSource,
@@ -56461,7 +56911,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56461
56911
  detail: launchPlanResult.plan.blockReason.message || 'Session launch is blocked.',
56462
56912
  retryable: code === 'launch_rollout_index_pending' && launchPlanResult.plan.blockReason.deterministic !== true,
56463
56913
  });
56464
- _setDeferredRestoreStarting(session, false, 'launch-blocked');
56914
+ _setDeferredRestoreStarting(session, false, 'LaunchBlocked');
56465
56915
  if (_shouldRefreshLaunchReadinessForBlock(code, launchPlanResult.plan.blockReason.deterministic === true)) {
56466
56916
  _refreshLaunchReadiness().catch((e) => errorSink.report('launch-readiness:refresh', e));
56467
56917
  }
@@ -56496,7 +56946,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56496
56946
  transition_id: String(pendingTransition.transition_id || '').slice(0, 16),
56497
56947
  reason: targetAuthorization.reason || 'host_target_receipt_mismatch',
56498
56948
  });
56499
- _setDeferredRestoreStarting(session, false, 'lead-transition-target-owner-mismatch');
56949
+ _setDeferredRestoreStarting(session, false, 'LeadTransitionTargetOwnerMismatch');
56500
56950
  return false;
56501
56951
  }
56502
56952
  }
@@ -56515,7 +56965,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56515
56965
  transition_id: String(pendingTransition.transition_id || '').slice(0, 16),
56516
56966
  reason: 'startup_task_not_exact_source',
56517
56967
  });
56518
- _setDeferredRestoreStarting(session, false, 'lead-transition-source-owner-mismatch');
56968
+ _setDeferredRestoreStarting(session, false, 'LeadTransitionSourceOwnerMismatch');
56519
56969
  return false;
56520
56970
  }
56521
56971
  const reconciled = await leadModelSwitch.reconcileUnownedTransition({ sessionId: task.session_id,
@@ -56525,7 +56975,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56525
56975
  transition_id: String(pendingTransition.transition_id || '').slice(0, 16),
56526
56976
  reason: reconciled.reason || 'transition_reconciliation_failed',
56527
56977
  });
56528
- _setDeferredRestoreStarting(session, false, 'lead-transition-reconciliation-failed');
56978
+ _setDeferredRestoreStarting(session, false, 'LeadTransitionReconciliationFailed');
56529
56979
  return false;
56530
56980
  }
56531
56981
  }
@@ -56534,7 +56984,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56534
56984
  recordSessionDiagnostic(task.session_id, 'lead-transition-source-restore-refused', {
56535
56985
  reason: 'transition_reconciliation_unavailable', message: String(error?.message || error).slice(0, 160),
56536
56986
  });
56537
- _setDeferredRestoreStarting(session, false, 'lead-transition-reconciliation-unavailable');
56987
+ _setDeferredRestoreStarting(session, false, 'LeadTransitionReconciliationUnavailable');
56538
56988
  return false;
56539
56989
  }
56540
56990
  if (opts.expectedProviderThreadId) {
@@ -56547,7 +56997,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56547
56997
  expectedThreadPrefix: String(opts.expectedProviderThreadId).slice(0, 8),
56548
56998
  actualThreadPrefix: String(resolvedThreadId).slice(0, 8),
56549
56999
  });
56550
- _setDeferredRestoreStarting(session, false, 'lead-source-recovery-resume-mismatch');
57000
+ _setDeferredRestoreStarting(session, false, 'LeadSourceRecoveryResumeMismatch');
56551
57001
  return false;
56552
57002
  }
56553
57003
  }
@@ -56567,7 +57017,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56567
57017
  throw new Error(`backup restore not possible: ${decision?.reason || 'unavailable'}`);
56568
57018
  }
56569
57019
  } catch (error) {
56570
- _setDeferredRestoreStarting(session, false, 'transcript-restore-failed');
57020
+ _setDeferredRestoreStarting(session, false, 'TranscriptRestoreFailed');
56571
57021
  _launchPlanCoordinator.markFailed(task.session_id, launchGeneration, 'launch_transcript_restore_failed', launchPlanResult.plan);
56572
57022
  recordSessionDiagnostic(task.session_id, 'deferred-restore-transcript-copy-failed', { error: error.message });
56573
57023
  _recordRuntimeSessionLifecycle(session, 'degraded', {
@@ -56591,7 +57041,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56591
57041
  }),
56592
57042
  });
56593
57043
  if (sessions.get(task.session_id) !== session || !session._restoreDeferred) {
56594
- _setDeferredRestoreStarting(session, false, 'mcp-wait-stale');
57044
+ _setDeferredRestoreStarting(session, false, 'McpWaitStale');
56595
57045
  return false;
56596
57046
  }
56597
57047
  // WI-8: gate a large-transcript resume BEFORE committing to spawn. `source` here is
@@ -56634,7 +57084,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56634
57084
  // immediately — nothing downstream will ever reach _clearResumeLoading for it.
56635
57085
  if (_resumeStormAdmission.gated) _resumeStormBudget.release(_resumeStormAdmission.ticket);
56636
57086
  session._resumeStormBudgetTicket = null;
56637
- _setDeferredRestoreStarting(session, false, 'resume-storm-stale');
57087
+ _setDeferredRestoreStarting(session, false, 'ResumeStormStale');
56638
57088
  return false;
56639
57089
  }
56640
57090
  session._resumeStormBudgetHeld = _resumeStormAdmission.gated;
@@ -56651,7 +57101,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56651
57101
  const geometryLeaseBeforeRestore = _geometryLease(session, task.session_id);
56652
57102
  if (!_launchPlanCoordinator.markSpawning(task.session_id, launchGeneration, launchPlanResult.plan)) {
56653
57103
  _releaseResumeStormBudgetIfHeld(session);
56654
- _setDeferredRestoreStarting(session, false, 'launch-plan-not-spawning');
57104
+ _setDeferredRestoreStarting(session, false, 'LaunchPlanNotSpawning');
56655
57105
  return false;
56656
57106
  }
56657
57107
  // D-8 (WP-3): capture the placeholder's cached-snapshot fields BEFORE _destroySession
@@ -56674,7 +57124,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56674
57124
  });
56675
57125
  if (!issued) {
56676
57126
  _releaseResumeStormBudgetIfHeld(session);
56677
- _setDeferredRestoreStarting(session, false, 'lead-transition-attempt-issue-refused');
57127
+ _setDeferredRestoreStarting(session, false, 'LeadTransitionAttemptIssueRefused');
56678
57128
  recordSessionDiagnostic(task.session_id, 'lead-transition-attempt-issue-refused', {
56679
57129
  generation: launchGeneration,
56680
57130
  });
@@ -56848,7 +57298,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56848
57298
  // finds this session not-live) instead of waiting out its deadline for a wake-ready that
56849
57299
  // will never come, and push visibility unconditionally.
56850
57300
  _announceRestoredSessionLive(session, 'create-rollback');
56851
- _setDeferredRestoreStarting(session, false, 'create-rollback');
57301
+ _setDeferredRestoreStarting(session, false, 'CreateRollback');
56852
57302
  recordSessionDiagnostic(task.session_id, 'deferred-restore-create-rollback', {
56853
57303
  source: _restoreSource,
56854
57304
  code: createError
@@ -56878,7 +57328,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56878
57328
  // a missing file will not appear on the next tick, so there is nothing to wait out (and
56879
57329
  // every future call into this function short-circuits at the top before reaching here).
56880
57330
  if (_newlyBlocked) {
56881
- sendResumeBlockedToClients(existingClients, resumeBlockedWire(task.session_id, _newlyBlocked, { agentType: session.agentType, rungWire: _resumeBlockedRungWire(_newlyBlocked) }), _wsSend);
57331
+ sendResumeBlockedToClients(existingClients, resumeBlockedWire(task.session_id, _newlyBlocked, { agentType: session.agentType, rungWire: resumeBlockedRungWire(_newlyBlocked) }), _wsSend);
56882
57332
  } else {
56883
57333
  for (const client of existingClients) {
56884
57334
  if (client && client.readyState === 1) {
@@ -57060,6 +57510,8 @@ function _autoResumeDeferredSessions(source = 'auto-resume') {
57060
57510
  idleHours: diet.idleHours,
57061
57511
  bytes: diet.bytes,
57062
57512
  });
57513
+ // WP-3 (§9 S-19): beside the existing diag; the latch keeps a long diet to one skip.
57514
+ _noteWakeSkip(s, 'DrainPressureSkipped');
57063
57515
  }
57064
57516
  continue;
57065
57517
  }
@@ -57107,9 +57559,11 @@ function _autoResumeDeferredSessions(source = 'auto-resume') {
57107
57559
  const resumeIds = warmDrain.selected;
57108
57560
  // S-18/skipReason: a session skipped this pass stays a lazy placeholder as before, but
57109
57561
  // now carries WHY -- surfaced on its eventual activation log (grep deferredForMin).
57562
+ // WP-3 (§9 S-19): the warm-drain policy skip (budget/pressure/limit) also reaches wakeSkips.
57110
57563
  for (const skip of warmDrain.skipped) {
57111
57564
  const s = sessions.get(skip.id);
57112
57565
  if (s) s._resumeDrainSkipReason = skip.reason;
57566
+ _noteWakeSkip(s, 'DrainPressureSkipped');
57113
57567
  }
57114
57568
  for (const id of resumeIds) {
57115
57569
  const s = sessions.get(id);
@@ -57206,6 +57660,7 @@ function _autoResumeDeferredSessions(source = 'auto-resume') {
57206
57660
  coalesceBroadcast: true, // batch emits a single list refresh per tick
57207
57661
  }),
57208
57662
  onError: (id, e) => console.error(`[auto-resume] ${String(id).slice(0, 8)} error: ${e.message}`),
57663
+ onSkip: _restoreWakeSkipSink, // WP-3 (§9 S-18): DrainPaused, once per pause transition
57209
57664
  broadcast: () => broadcastSessionList(true),
57210
57665
  scheduleGap: _scheduleResumeGap,
57211
57666
  // WI-7(d): yield the whole drain to an in-flight interactive activation.
@@ -57289,13 +57744,18 @@ const _reviveUnresponsiveHost = createReviveUnresponsiveHost({
57289
57744
  recordSessionDiagnostic,
57290
57745
  log: (msg) => console.warn(msg),
57291
57746
  });
57747
+ // WP-3 (D-3): the stall action -- one handoffHost per episode, the existing revive as its fallback.
57748
+ // `_evacuateStalledHostSafe` is a function declaration so the hoisted references above (the manager's
57749
+ // onThreadpoolReport option and the ledger's onEvacuate) resolve to it.
57750
+ const _evacuateStalledHost = createEvacuateStalledHost({ manager: sessionHostManager, handoffHost: _hostHandoffOrchestrator.handoffHost, reviveUnresponsiveHost: _reviveUnresponsiveHost, recordSessionDiagnostic, hostHealth, log: (m) => console.warn(m), handoffOpts: { errorSink } });
57751
+ function _evacuateStalledHostSafe(v) { _evacuateStalledHost(v.hostId, v).catch((e) => errorSink.report('host-evacuate', e, { hostId: v.hostId })); }
57292
57752
 
57293
57753
  // Reconcile previous-boot hosts before classifying startup tasks. Incompatible
57294
57754
  // hosts are quiesced, then restored through the ordinary provider-resume path.
57295
57755
  const RECONCILE_PROBE_CONCURRENCY = 4;
57296
57756
  async function _reconcileSurvivingHostsForRestore(startupTasks) {
57297
57757
  const aliveHostIndex = new Map();
57298
- const hostRecordsById = new Map();
57758
+ const hostRecordsById = new Map(); const threadpoolByHostId = new Map(); // WP-3 (§9 S-8): adopted hosts' state-reply threadpool snapshots, keyed by hostId
57299
57759
  const dataDir = require('./lib/data-dir').getDataDir();
57300
57760
  // WP-4 (D-9 crash cap): >= 3 unexpected host deaths inside the trailing 10-
57301
57761
  // minute window means the topology itself is unhealthy right now -- skip
@@ -57313,7 +57773,7 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
57313
57773
  await _restartOutcomeTruth.killHostsForCrashCap({ records: await _listHostRecords(dataDir), rollup: _hostAdoptionRollup, isAlive: _isProcessAlive, shutdownHost: _shutdownOrphanHost,
57314
57774
  removeRecord: (r) => _removeHostRecordFile(dataDir, r.hostId), skip: (r) => r.serverBootId === _serverBootId || sessionHostManager.ownsHostId(r.hostId), errorSink });
57315
57775
  } catch (e) { errorSink.report('boot-reconcile:crash-cap-kill', e); }
57316
- _restartOutcomeTruth.recordCrashCapRows({ recordSessionDiagnostic, startupTasks, deaths }); return { aliveHostIndex, hostRecordsById };
57776
+ _restartOutcomeTruth.recordCrashCapRows({ recordSessionDiagnostic, startupTasks, deaths }); return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
57317
57777
  }
57318
57778
  } catch (e) {
57319
57779
  errorSink.report('boot-reconcile:read-host-crashes', e);
@@ -57323,9 +57783,9 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
57323
57783
  records = await _listHostRecords(dataDir);
57324
57784
  } catch (e) {
57325
57785
  errorSink.report('boot-reconcile:list-host-records', e);
57326
- return { aliveHostIndex, hostRecordsById };
57786
+ return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
57327
57787
  }
57328
- if (!records.length) return { aliveHostIndex, hostRecordsById };
57788
+ if (!records.length) return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
57329
57789
  const startupSessionIds = new Set(startupTasks.map((t) => t.session_id));
57330
57790
  const aliveHosts = []; // { record, matching } per probed-alive host; indexed AFTER all probes, hostId order
57331
57791
  let cursor = 0;
@@ -57387,7 +57847,7 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
57387
57847
  console.log(` [boot-reconcile] host ${record.hostId} runs different code (closure hash ${String(probed.hostClosureHash || '').slice(0, 8)}≠${String(probed.diskClosureHash || '').slice(0, 8)}) — adopting now; handoff to new code after restore`);
57388
57848
  for (const sessionId of matching) recordSessionDiagnostic(sessionId, 'host-upgrade-pending', { hostId: record.hostId, upgrade: probed.upgrade });
57389
57849
  }
57390
- hostRecordsById.set(record.hostId, record);
57850
+ hostRecordsById.set(record.hostId, record); threadpoolByHostId.set(record.hostId, probed.state && probed.state.threadpool); // WP-3 (D-4/S-9): the boot-time stall reading, absent on a pre-WP-3 host
57391
57851
  aliveHosts.push({ record, matching });
57392
57852
  }
57393
57853
  }
@@ -57404,7 +57864,7 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
57404
57864
  if (aliveHostIndex.size) {
57405
57865
  console.log(` [boot-reconcile] ${aliveHostIndex.size} session(s) have a surviving host — will adopt instead of respawning`);
57406
57866
  }
57407
- return { aliveHostIndex, hostRecordsById };
57867
+ return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
57408
57868
  }
57409
57869
 
57410
57870
  // D-2: one-line stamp for restore/adopt/placeholder sites (idempotent; no row = untouched).
@@ -57458,6 +57918,54 @@ function _reloadWorkerSessionMarkersAtBoot() {
57458
57918
  if (rearmed > 0) console.log(` [restore] re-armed ${rearmed} worker retirement timer(s) from worker_sessions.hide_at.`);
57459
57919
  }
57460
57920
 
57921
+ // WP-3 (worker-transcript-link-derived-not-discovered-2026-10-25.html §5 D-5): the derived-transcript
57922
+ // boot backfill — one bounded, idempotent, fire-and-forget pass that fills agent_sessions.jsonl_path
57923
+ // for Claude rows whose transcript already sits at the DERIVED path (nothing else ever does: resume,
57924
+ // the targeted import, the review view and usage attribution all come up empty for them).
57925
+ //
57926
+ // The latch is module scope, not a local in _restoreSessionsAsync, so a re-entrant restore cannot
57927
+ // re-run the pass; only a process restart clears it. It means "scheduled this process", not "finished
57928
+ // this process" — the single re-arm below is the only extra attempt. The dispatch lives in this helper
57929
+ // — rather than inline at the call site — because the restore-loop test files slice
57930
+ // _restoreSessionsAsync into a vm sandbox with a curated global list: the call site must read nothing
57931
+ // that sandbox does not inject (see the typeof guard at the call site). Nothing in the restore loop
57932
+ // waits on this pass.
57933
+ let _derivedTranscriptLinkBackfillStarted = false;
57934
+
57935
+ // Why a re-arm exists at all (fix round 2, live defect on a dev restart): the owner worker's
57936
+ // statement lock is fail-fast by design and the dispatcher retries a write-lock-busy op only 3x with
57937
+ // ~60 ms backoff, so an op that lands while the main thread holds `transaction:immediate` rejects in
57938
+ // about a second. The call site was moved after the restore loop drains for exactly that reason; this
57939
+ // single 60 s re-arm covers a burst that outlives the loop (a slow adoption write, a busy disk).
57940
+ // Bounded: attempt 2 is the last one, there is no loop, and the latch above is not consulted again.
57941
+ async function _linkDerivedClaudeTranscriptsOnce(attempt = 1) {
57942
+ if (attempt === 1) {
57943
+ if (_derivedTranscriptLinkBackfillStarted) return;
57944
+ _derivedTranscriptLinkBackfillStarted = true;
57945
+ }
57946
+ try {
57947
+ const r = await _runCtmDbWorkerOp(
57948
+ 'linkDerivedClaudeTranscripts',
57949
+ { limit: 500, homeDir: os.homedir(), claudeConfigDir: serverConfig.CLAUDE_CONFIG_DIR || '' },
57950
+ { timeoutMs: 60 * 1000 },
57951
+ );
57952
+ console.log(`[transcript-link] backfill checked=${r.checked} linked=${r.linked} missing=${r.missing} mismatched=${r.mismatched} unreadable=${r.unreadable} ms=${r.ms}`);
57953
+ } catch (e) {
57954
+ const writeLockBusy = !!e && (e.code === 'SQLITE_WRITE_LOCK_BUSY' || e.code === 'SQLITE_BUSY' || /write lock busy/i.test(String(e.message || '')));
57955
+ if (!writeLockBusy) {
57956
+ console.warn(`[transcript-link] backfill failed: ${e && e.message}`);
57957
+ return;
57958
+ }
57959
+ if (attempt >= 2) {
57960
+ console.warn(`[transcript-link] backfill deferred (attempt 2/2, giving up): ${e && e.message}`);
57961
+ return;
57962
+ }
57963
+ console.warn(`[transcript-link] backfill deferred (attempt 1/2, retry in 60s): ${e && e.message}`);
57964
+ const retry = setTimeout(() => { void _linkDerivedClaudeTranscriptsOnce(2); }, 60 * 1000);
57965
+ if (retry && typeof retry.unref === 'function') retry.unref();
57966
+ }
57967
+ }
57968
+
57461
57969
  async function _restoreSessionsAsync() {
57462
57970
  await _ownerRecovery.beforeRestore();
57463
57971
  _hostAdoptionRollup.markRestoreStarted(); _restoreAdmission.markTierAActive('restore-start');
@@ -57476,11 +57984,11 @@ async function _restoreSessionsAsync() {
57476
57984
  );
57477
57985
  _listStartupTasksMs = Date.now() - _listStartupTasksStartedAt;
57478
57986
  _restoreWindowVerdict.fillRestorePendingIds(_startupTasks.map((t) => t.session_id));
57479
- let _restoreAliveHostIndex = new Map(), _restoreHostRecordsById = new Map();
57987
+ let _restoreAliveHostIndex = new Map(), _restoreHostRecordsById = new Map(), _restoreThreadpoolByHostId = new Map(); // WP-3 (§9 S-8)
57480
57988
  const _earlyAdopted = new Set(); const _earlyAdoptStartedAt = Date.now();
57481
57989
  if (_startupTasks.length) {
57482
57990
  try {
57483
- ({ aliveHostIndex: _restoreAliveHostIndex, hostRecordsById: _restoreHostRecordsById } = await _reconcileSurvivingHostsForRestore(_startupTasks));
57991
+ ({ aliveHostIndex: _restoreAliveHostIndex, hostRecordsById: _restoreHostRecordsById, threadpoolByHostId: _restoreThreadpoolByHostId } = await _reconcileSurvivingHostsForRestore(_startupTasks));
57484
57992
  } catch (e) { errorSink.report('boot-reconcile:reconcile', e); }
57485
57993
  await Promise.all(_startupTasks.filter((t) => _restoreAliveHostIndex.has(t.session_id)).map((t) => _adoptStartupTaskSession(t, _restoreAliveHostIndex.get(t.session_id).record, _restoreAliveHostIndex)
57486
57994
  .then((adopted) => { if (adopted) _earlyAdopted.add(t.session_id); }, (e) => errorSink.report('boot-adopt:restore-early', e, { sessionId: t.session_id }))));
@@ -57636,6 +58144,11 @@ async function _restoreSessionsAsync() {
57636
58144
  try { await dbModule.clearScrollbackIdentities(); } catch {}
57637
58145
  _resumeShadowAfterBoot('no-sessions'); // nothing to restore — resume mirroring now
57638
58146
  _reloadWorkerSessionMarkersAtBoot(); // once per boot in either branch (quota holds, worker markers, lead notices)
58147
+ // WP-4 (model-proxy-survives-ctm-restart D-10): a boot with nothing to restore still ledgers its
58148
+ // restore_complete row -- the model proxy's adopt/handoff/spawn outcome is per RESTART, not per
58149
+ // session -- written after the proxy's adoption decision (waitForHealth awaits it, bounded 2 s) so
58150
+ // adoption.proxy names what happened, never the in-flight fallback. Not awaited: boot never waits on it.
58151
+ oauthProxySupervisor.waitForHealth().catch(() => { /* expected: waitForHealth rejects only on an aborted signal and none is passed; the row is ledgered either way */ }).then(() => _restartOutcomeTruth.appendRestoreCompleteLedger({ build: _bootBuildSha, summary: _hostAdoptionMetricsSummary, errorSink }));
57639
58152
  broadcastToAll({ type: 'server-ready' });
57640
58153
  return;
57641
58154
  }
@@ -57687,6 +58200,16 @@ async function _restoreSessionsAsync() {
57687
58200
  // auto-resume below runs shadowed; only the restore loop itself was the
57688
58201
  // suppressed window.)
57689
58202
  _resumeShadowAfterBoot('restore-complete');
58203
+ // D-5 (worker transcript link): one bounded, idempotent pass that links every Claude row whose
58204
+ // transcript exists at the derived path. Placed here — after the loop drained, not in the boot
58205
+ // write burst — because the owner worker's statement lock is fail-fast: dispatching it while the
58206
+ // main thread holds back-to-back `transaction:immediate` writes (0.8-4.8 s each during restore)
58207
+ // makes the op reject in ~1 s, and the once-latch would then skip it for the whole boot. The
58208
+ // helper re-arms once if a burst still outlives the loop. Fire-and-forget: nothing waits on it.
58209
+ // typeof guard, not a missing dependency: the restore-loop tests evaluate this function in a vm
58210
+ // sandbox whose curated globals do not include the helper, and an unguarded call would throw there.
58211
+ // Same reason as `typeof _markStreamBehind === 'function'` above.
58212
+ if (typeof _linkDerivedClaudeTranscriptsOnce === 'function') void _linkDerivedClaudeTranscriptsOnce();
57690
58213
  // WP-3 (design doc §5 D-4, §8 WP-3, §9 S-18): force one real sampler snapshot
57691
58214
  // before draining so the warm-drain budget's pressure/free/inactive reflect the
57692
58215
  // ACTUAL host state at the exact moment deferred sessions would auto-resume,
@@ -57696,7 +58219,7 @@ async function _restoreSessionsAsync() {
57696
58219
  try { await _sampleLifecycleResources({ force: true }); }
57697
58220
  catch (error) { errorSink.report('restore:warm-drain-pre-sample', error); }
57698
58221
  // Bring deferred placeholders back to life by themselves (staggered).
57699
- _autoResumeDeferredSessions('post-restore-auto-resume'); _hostHandoffOrchestrator.runUpgradePendingHandoffs({ manager: sessionHostManager, rollup: _hostAdoptionRollup, recordSessionDiagnostic, hostHealth, errorSink }).finally(() => _restartOutcomeTruth.appendRestoreCompleteLedger({ build: _bootBuildSha, summary: _hostAdoptionMetricsSummary, errorSink })); // WP-4: upgrade-pending hosts hand off after the loop
58222
+ _autoResumeDeferredSessions('post-restore-auto-resume'); _hostHandoffOrchestrator.runUpgradePendingHandoffs({ manager: sessionHostManager, rollup: _hostAdoptionRollup, recordSessionDiagnostic, hostHealth, errorSink, skipHostIds: evacuateStalledAdoptedHosts({ hostHealth, evacuateStalledHost: _evacuateStalledHost, threadpoolByHostId: _restoreThreadpoolByHostId, ownsHostId: (id) => [...sessionHostManager.hosts.values()].some((a) => a._hostId === id && !a.closed) }) }).finally(() => oauthProxySupervisor.waitForHealth().catch(() => { /* expected: waitForHealth rejects only on an aborted signal and none is passed; the row is ledgered either way */ }).then(() => _restartOutcomeTruth.appendRestoreCompleteLedger({ build: _bootBuildSha, summary: _hostAdoptionMetricsSummary, errorSink }))) // WP-4 D-10: after the proxy's adoption decision, same as the empty branch; // WP-4: upgrade-pending hosts hand off after the loop
57700
58223
  // Clean orphan scrollback identities — entries for sessions no longer in
57701
58224
  // startup_tasks (e.g. a hard crash where pty.onExit cleanup didn't run).
57702
58225
  // (scrollback_log was dropped in schema v10; snapshot files are reaped by the
@@ -58047,7 +58570,7 @@ async function _restoreSessionsAsync() {
58047
58570
  // broadcastToAll, isCtmInternalSession, callClaudeAsync, _detachOwnerMutation,
58048
58571
  // _inRestoreStorm, _offthreadRead) is already initialized. See server/boot.js's own
58049
58572
  // header comment for the TDZ-safety argument.
58050
- const { createBootJobs, readPackageVersion } = require('./server/boot');
58573
+ const { createBootJobs, readPackageVersion, isHumanPromptCandidate, cleanFirstMessage } = require('./server/boot');
58051
58574
  const _bootJobs = createBootJobs({
58052
58575
  installRoot: path.join(__dirname, '..'),
58053
58576
  telemetry,
@@ -61525,6 +62048,129 @@ servicesApi.configure({
61525
62048
  });
61526
62049
  servicesApi.register();
61527
62050
 
62051
+ // WP-0 (remote-machine-hub-sessions-2026-10-22.html §6.1): machines route cluster. Unlike the
62052
+ // extraction slices above there is no legacy handleApi branch to delete — this is NEW surface, so
62053
+ // the handlers live in lib/api/machines.js (its header states the convention) and the policy is
62054
+ // lib/machine-registry.js. Auth comes from lib/auth-rules.js's three /api/machines rows
62055
+ // (loopbackOnly + admin), which is also why nothing here evaluates a token.
62056
+ machinesApi.configure({
62057
+ db: dbModule,
62058
+ registry: machineRegistry,
62059
+ // WP-3 §6.3: the pair/remove handlers join a pool, so they need the pool store and the boot read's
62060
+ // promise — both declared further down this file (the pool cluster below owns them), so they are
62061
+ // passed as LAZY accessors; the cluster is configured here to keep the `machines` route-group
62062
+ // registration order the route golden pins. F2: this machine's own dial list is no longer injected
62063
+ // here — the roster shape (members + self) is lib/api/machine-pool.js's `rosterMembers()` now.
62064
+ pool: () => require('./lib/machine-pool').getMachinePool(),
62065
+ ready: () => _machinePoolReady,
62066
+ readJsonRequestBody,
62067
+ sendJsonResponse,
62068
+ });
62069
+ machinesApi.register();
62070
+
62071
+ // WP-1 (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.1/D-13): host-stats route
62072
+ // cluster. Same shape as the machines cluster above — NEW surface, so no legacy handleApi branch
62073
+ // exists to delete, and the auth row (read + peerControl, NOT loopbackOnly) lives in
62074
+ // lib/auth-rules.js.
62075
+ hostStatsApi.configure({
62076
+ hostStats: _hostStats,
62077
+ sendJsonResponse,
62078
+ });
62079
+ hostStatsApi.register();
62080
+
62081
+ // WP-2a (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.2, D-4/D-13, §9 S-24): the pool
62082
+ // + machine-self route cluster. Same shape as the two clusters above — NEW surface, no legacy
62083
+ // handleApi branch to delete. lib/machine-pool.js's process singleton is the POOL STORE (its
62084
+ // load() ran at boot, above); lib/machine-self.js is the machine-owned settings CACHE (loaded at
62085
+ // boot too, so the manifest build below never reads the DB).
62086
+ const machinePoolApi = require('./lib/api/machine-pool');
62087
+ const machineSelfStore = require('./lib/machine-self');
62088
+ machineSelfStore.configure({ db: dbModule });
62089
+ // §6.2: pool.load() and the machine-self cache load run ONCE at boot, before routes are served.
62090
+ // Kicked here (module load, ahead of every listener's 'listening' / _runBootSequence, which is a
62091
+ // frozen function at its line ceiling) rather than inside the boot sequence. Both are a single
62092
+ // small file read / one-row SELECT; a failure leaves the member pool-less (verify() fails closed
62093
+ // to 401) which is the same state as "never paired", never a broken boot.
62094
+ function _loadMachinePoolState() {
62095
+ const fail = (name) => (error) => console.warn(`[machine-pool] ${name} load failed: ${error?.message || error}`);
62096
+ return Promise.all([
62097
+ require('./lib/machine-pool').getMachinePool().load().catch(fail('pool')),
62098
+ machineSelfStore.load().catch(fail('machine-self')),
62099
+ ]);
62100
+ }
62101
+ // `_machinePoolReady` is that same promise, named: this kick is fire-and-forget (nothing on the
62102
+ // request path may await a boot read), so a handler that MUTATES the pool — WP-3's pair/remove —
62103
+ // awaits it first and never races a load that is still in flight. A read-only handler does not.
62104
+ const _machinePoolReady = _loadMachinePoolState();
62105
+ machinePoolApi.configure({
62106
+ db: dbModule,
62107
+ pool: require('./lib/machine-pool').getMachinePool(),
62108
+ machineSelf: require('./lib/machine-self'),
62109
+ // The boot load's promise, so a pool-MUTATING handler (WP-3's pair/remove) can await it.
62110
+ ready: _machinePoolReady,
62111
+ registry: machineRegistry,
62112
+ readJsonRequestBody,
62113
+ sendJsonResponse,
62114
+ listenPort: PORT,
62115
+ // §6.2 "then executionHost.publicOrigin if set": EXACTLY the origin lib/instance-manifest.js
62116
+ // publishes on executionHost.publicOrigin — read back from the builder rather than off
62117
+ // `serverConfig`.
62118
+ //
62119
+ // Why the builder and not the four names: server-config's seam (RAW_ENV_NAMES) does not carry
62120
+ // CTM_CANONICAL_PUBLIC_ORIGIN / CTM_STABLE_PUBLIC_ORIGIN / CTM_PUBLIC_ORIGIN / CTM_EXTERNAL_ORIGIN,
62121
+ // so every `serverConfig.<name>` read here was `undefined` and this fallback could NEVER fire —
62122
+ // a loopback- or tailnet-bound instance advertised only its LAN addresses, and selfUrls() (P-4)
62123
+ // had nothing dialable to append. WP-3's three-box pairing test hit it head-on: the roster push
62124
+ // to a peer whose only dial URL was a LAN address nothing was bound to never arrived.
62125
+ // lib/instance-manifest-runtime.js spreads `process.env` first, so this reads the same values the
62126
+ // manifest publishes, through the sanctioned seam, with no new raw env member read here.
62127
+ publicOrigin: () => {
62128
+ try {
62129
+ const manifest = _buildInstanceManifest(null, Math.floor((Date.now() - _ctmStartTime) / 1000));
62130
+ return String((manifest && manifest.executionHost && manifest.executionHost.publicOrigin) || '');
62131
+ } catch {
62132
+ return ''; // expected: a failed manifest build just leaves self URLs LAN-only (P-4 order preserved)
62133
+ }
62134
+ },
62135
+ });
62136
+ machinePoolApi.register();
62137
+
62138
+ // WP-3 (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.3, D-6/D-9/D-13, §9 S-15/S-16/S-23):
62139
+ // the pairing route cluster — the routes a JOINING member talks to. Same shape as the three
62140
+ // clusters above: NEW surface, no legacy handleApi branch to delete.
62141
+ //
62142
+ // POST /api/machine-pairing/offers is an ordinary authorised route (admin + step-up remote, row in
62143
+ // lib/auth-rules.js). POST /api/machine-pairing/join AND GET /api/machine-pairing/identity are
62144
+ // registered with phase 'api-cloudflare' — the dispatcher server.js already runs BEFORE
62145
+ // `_authorizeHttpRequest` (line ~12278, the seam lib/api/microsoft-owner.js's public rows use),
62146
+ // because the caller does not hold this box's credential yet; the pool secret in the body IS the
62147
+ // thing being proven, and the identity probe exists exactly so a member can ask "is there a code
62148
+ // here" without one (D-9: /api/sync/manifest is `sync`-scoped and would 401 on a real LAN before the
62149
+ // pair began). The defence on both is the `machine_pair` bucket below plus the single-use code
62150
+ // (lib/api/machine-pairing.js states this in full). The limiter and the remote-IP reader are injected
62151
+ // rather than re-derived, so both routes are charged to the SAME AuthRateLimiter instance every
62152
+ // authorised route uses.
62153
+ const machinePairingApi = require('./lib/api/machine-pairing');
62154
+ const machinePairingStore = require('./lib/machine-pairing').createMachinePairing({ db: dbModule });
62155
+ machinePairingApi.configure({
62156
+ db: dbModule,
62157
+ pool: require('./lib/machine-pool').getMachinePool(),
62158
+ pairing: machinePairingStore,
62159
+ machineSelf: require('./lib/machine-self'),
62160
+ registry: machineRegistry,
62161
+ selfUrls: machinePoolApi.selfUrls,
62162
+ readJsonRequestBody,
62163
+ sendJsonResponse,
62164
+ // The PRE-AUTH rate limit: `_rateLimitDecision`'s own limiter and KDF, keyed by remote address
62165
+ // because there is no principal yet. Same `AuthRateLimiter` instance (`authRateLimiter`).
62166
+ rateLimit: (kind, key) => authRateLimiter.check(kind, key),
62167
+ remoteIp: (req) => _remoteIpFromReq(req),
62168
+ // F5: the offer/identity failure path logs here and echoes nothing to the caller.
62169
+ logError: (...args) => console.error(...args),
62170
+ ready: _machinePoolReady,
62171
+ });
62172
+ machinePairingApi.register();
62173
+
61528
62174
  // WP-8 slice 3: setup/diagnostics/misc-ops route clusters.
61529
62175
  const setupApi = require('./lib/api/setup');
61530
62176
  setupApi.configure({
@@ -61619,6 +62265,7 @@ miscopsApi.configure({
61619
62265
  apiListRules,
61620
62266
  apiDeleteHook,
61621
62267
  apiRestartCtm, restartGuardSnapshot: _restartGuardSnapshot,
62268
+ proxyPreflight: () => oauthProxySupervisor.proxyPreflight(), // WP-3 D-10: the live model-proxy verdict
61622
62269
  wss,
61623
62270
  clientBuildDiagnosticDetails,
61624
62271
  attachedSessionIds,
@@ -61687,6 +62334,7 @@ ingressApi.configure({
61687
62334
  observeWorkspaceActivity: _observeWorkspaceActivity,
61688
62335
  dbModule,
61689
62336
  delegateWorker,
62337
+ recordSessionDiagnostic,
61690
62338
  });
61691
62339
  ingressApi.register();
61692
62340
  const { createMicrosoftOwnerServer } = require('./lib/microsoft-owner-server');
@@ -62009,10 +62657,10 @@ const rootPromptFlow = createRootPromptFlow({
62009
62657
  }, Date.now(),
62010
62658
  ),
62011
62659
  sendWsJson: (ws, frame) => _sendWsJsonBounded(ws, frame),
62012
- reportDiagnostic: ({ phase, code, returnedRecordConfirmed, canonicalRecordConfirmed,
62660
+ reportDiagnostic: ({ phase, code, sessionId, clientMessageId, returnedRecordConfirmed, canonicalRecordConfirmed,
62013
62661
  recordFieldsMatch, errorKind, sourceFile, sourceLine, nativeHandleOpen, nativeHandleClosed, failureClass }) =>
62014
62662
  recordSessionDiagnostic('__host__', 'root-prompt-flow-suppressed-failure', {
62015
- phase, code, returnedRecordConfirmed, canonicalRecordConfirmed, recordFieldsMatch,
62663
+ phase, code, sessionId, clientMessageId, returnedRecordConfirmed, canonicalRecordConfirmed, recordFieldsMatch,
62016
62664
  errorKind, sourceFile, sourceLine, nativeHandleOpen, nativeHandleClosed, failureClass,
62017
62665
  }),
62018
62666
  markDirty: (sessionId, reason) => {
@@ -62578,7 +63226,7 @@ function _runBootSequence() {
62578
63226
  // configDir/instanceTag/port are passed explicitly so a dev/test instance
62579
63227
  // (its own CONFIG_DIR, per lib/server-config.js) never lands its journal
62580
63228
  // under the real ~/.walle — see lib/ctm-boot-lifecycle.js.
62581
- appendCtmBootLine({ configDir: CONFIG_DIR, instanceTag: INSTANCE_TAG, port: PORT });
63229
+ appendCtmBootLine({ configDir: CONFIG_DIR, instanceTag: INSTANCE_TAG, port: PORT, enteredAtServer: require.main === module });
62582
63230
 
62583
63231
  // Stale CTM cleanup: with launchd KeepAlive, only one instance runs.
62584
63232
  // No pgrep needed — launchd manages the lifecycle.
@@ -62603,8 +63251,13 @@ function _runBootSequence() {
62603
63251
  // Wall-E daemon and surfaced through its HTTP API after startup.
62604
63252
  const shouldStartOauthProxy = (() => {
62605
63253
  if (isWalleSupervisorDisabled()) return false;
62606
- // Sync boot path: Wall-E's stored provider is not readable here; the auth method alone gates the proxy.
62607
- return serverConfig.WALLE_AUTH_METHOD === 'oauth_proxy';
63254
+ // Sync boot path: Wall-E's stored provider is not readable here; the auth method gates a fresh spawn.
63255
+ if (serverConfig.WALLE_AUTH_METHOD === 'oauth_proxy') return true;
63256
+ // WP-4 AC-4e (model-proxy-survives-ctm-restart D-4): a proxy that outlived the previous CTM (live
63257
+ // process named by this data dir's instance record) is adopted by start() regardless of the auth
63258
+ // method -- the primary's proxy is started by the setup surface, not this env var, and a survivor
63259
+ // left un-adopted keeps serving but no longer gets this CTM's lead tokens.
63260
+ return oauthProxySupervisor.hasLiveSurvivor();
62608
63261
  })();
62609
63262
  if (shouldStartOauthProxy) {
62610
63263
  try {
@@ -63244,6 +63897,16 @@ function _runBootSequence() {
63244
63897
  // coalescing keeps at most one queued behind the in-flight one).
63245
63898
  _transcriptDeltaCoalescer.request(filePath, payload);
63246
63899
  }
63900
+ // WP-2 (D-11): publish the title the db-owner worker wrote; a drifted tier-1 row asks for one re-assert.
63901
+ const titleApply = applied.titleApply;
63902
+ if (titleApply?.applied === 'agent') {
63903
+ if (liveSession) { liveSession.title = titleApply.title; _invalidateTitleInfoCache(liveSession); }
63904
+ void _detachOwnerMutation('agent-title-label', () => dbModule.updateStartupTaskLabel(ownerSessionId, titleApply.title));
63905
+ broadcastSessionList();
63906
+ }
63907
+ if (titleApply?.reassert) {
63908
+ void mirrorSessionTitle({ db: dbModule, sessionId: ownerSessionId, force: true }).catch(() => { /* expected: exit/relink and the next observation retry it */ });
63909
+ }
63247
63910
  } catch (e) {
63248
63911
  fallback('pipeline-error');
63249
63912
  console.error(`[event-delta] ${String(sessionId).slice(0, 8)} pipeline error:`, e.message);