create-walle 0.10.0 → 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 (381) hide show
  1. package/README.md +88 -41
  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 +7 -5
  6. package/package.json +10 -4
  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/capture-codex-rollout-records.js +417 -0
  11. package/template/coding-task-manager/bin/census-codex-lead-history.js +325 -0
  12. package/template/coding-task-manager/bin/claude-hook.js +7 -3
  13. package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
  14. package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
  15. package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
  16. package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
  17. package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
  18. package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
  19. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  20. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  21. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  22. package/template/coding-task-manager/bin/seam-replay-codex.js +301 -0
  23. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  24. package/template/coding-task-manager/db.js +705 -286
  25. package/template/coding-task-manager/git-utils.js +44 -0
  26. package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
  27. package/template/coding-task-manager/lib/agent-presets.js +15 -0
  28. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  29. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  30. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  31. package/template/coding-task-manager/lib/api/ingress.js +35 -12
  32. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  33. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  34. package/template/coding-task-manager/lib/api/machines.js +461 -0
  35. package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
  36. package/template/coding-task-manager/lib/api/providers.js +53 -6
  37. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  38. package/template/coding-task-manager/lib/api/sessions.js +160 -8
  39. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  40. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  41. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  42. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  43. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  44. package/template/coding-task-manager/lib/auth-context.js +32 -2
  45. package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
  46. package/template/coding-task-manager/lib/auth-rules.js +186 -45
  47. package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
  48. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  49. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  50. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  51. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  52. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  53. package/template/coding-task-manager/lib/client-diag-handler.js +25 -0
  54. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  55. package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
  56. package/template/coding-task-manager/lib/codex-history-terminal-renderer.js +33 -33
  57. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  58. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  59. package/template/coding-task-manager/lib/codex-local-links.js +267 -0
  60. package/template/coding-task-manager/lib/codex-markdown-lists.js +160 -0
  61. package/template/coding-task-manager/lib/codex-native-history-prefix.js +4 -0
  62. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  63. package/template/coding-task-manager/lib/codex-source-handoff.js +186 -0
  64. package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
  65. package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
  66. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  67. package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
  68. package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
  69. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  70. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  71. package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
  72. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  73. package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
  74. package/template/coding-task-manager/lib/db-domains.js +21 -2
  75. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  76. package/template/coding-task-manager/lib/db-owner-worker-client.js +22 -0
  77. package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
  78. package/template/coding-task-manager/lib/feature-manifest.js +1 -3
  79. package/template/coding-task-manager/lib/fs-watcher.js +3 -2
  80. package/template/coding-task-manager/lib/harness-history-transition.js +41 -14
  81. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  82. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  83. package/template/coding-task-manager/lib/headless-term-service.js +60 -23
  84. package/template/coding-task-manager/lib/history-transition-read-op.js +179 -23
  85. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  86. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  87. package/template/coding-task-manager/lib/host-handoff.js +51 -13
  88. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  89. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  90. package/template/coding-task-manager/lib/host-stats.js +328 -0
  91. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  92. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  93. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  94. package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
  95. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
  96. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  97. package/template/coding-task-manager/lib/instance-manifest.js +13 -1
  98. package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
  99. package/template/coding-task-manager/lib/kernel/diag-log.js +12 -0
  100. package/template/coding-task-manager/lib/launch-model-override.js +160 -1
  101. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  102. package/template/coding-task-manager/lib/lead-model-observer.js +17 -2
  103. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
  104. package/template/coding-task-manager/lib/lead-model-switch.js +11 -2
  105. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +42 -4
  106. package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
  107. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  108. package/template/coding-task-manager/lib/login-tripwire.js +18 -0
  109. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  110. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  111. package/template/coding-task-manager/lib/machine-registry.js +749 -0
  112. package/template/coding-task-manager/lib/machine-self.js +66 -0
  113. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +56 -4
  114. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  115. package/template/coding-task-manager/lib/media-journal.js +84 -27
  116. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  117. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  118. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  119. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +16 -3
  120. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  121. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  122. package/template/coding-task-manager/lib/model-proxy/collaboration-codec.js +94 -0
  123. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  124. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  125. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  126. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  127. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  128. package/template/coding-task-manager/lib/model-proxy/responses-face.js +24 -2
  129. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  130. package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
  131. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  132. package/template/coding-task-manager/lib/native-history-control.js +62 -14
  133. package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
  134. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  135. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
  136. package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
  137. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  138. package/template/coding-task-manager/lib/prompt-admission-trace.js +69 -0
  139. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  140. package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
  141. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  142. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  143. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  144. package/template/coding-task-manager/lib/raw-history-block.js +3 -1
  145. package/template/coding-task-manager/lib/read-pool-client.js +36 -24
  146. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  147. package/template/coding-task-manager/lib/read-pool-telemetry.js +14 -5
  148. package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
  149. package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
  150. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  151. package/template/coding-task-manager/lib/remote-command-runtime.js +18 -5
  152. package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
  153. package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
  154. package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
  155. package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
  156. package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
  157. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  158. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  159. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  160. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  161. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  162. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  163. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  164. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  165. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  166. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  167. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  168. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  169. package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
  170. package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
  171. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  172. package/template/coding-task-manager/lib/root-prompt-admission.js +43 -11
  173. package/template/coding-task-manager/lib/root-prompt-flow.js +175 -38
  174. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  175. package/template/coding-task-manager/lib/server-memory-sampler-runtime.js +10 -0
  176. package/template/coding-task-manager/lib/server-memory-sampler.js +41 -2
  177. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  178. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  179. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  180. package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
  181. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  182. package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
  183. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  184. package/template/coding-task-manager/lib/session-create/provider-process.js +38 -39
  185. package/template/coding-task-manager/lib/session-create/response.js +30 -12
  186. package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
  187. package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
  188. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  189. package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
  190. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  191. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  192. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  193. package/template/coding-task-manager/lib/session-host-manager.js +152 -206
  194. package/template/coding-task-manager/lib/session-jobs.js +17 -9
  195. package/template/coding-task-manager/lib/session-lifecycle.js +20 -50
  196. package/template/coding-task-manager/lib/session-model-action-preconditions.js +68 -3
  197. package/template/coding-task-manager/lib/session-resource-sampler.js +67 -9
  198. package/template/coding-task-manager/lib/session-standup.js +67 -4
  199. package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
  200. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  201. package/template/coding-task-manager/lib/session-status.js +2 -2
  202. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  203. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  204. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  205. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  206. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
  207. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  208. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  209. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  210. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  211. package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
  212. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  213. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  214. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  215. package/template/coding-task-manager/lib/transcript-source.js +8 -3
  216. package/template/coding-task-manager/lib/transcript-store.js +40 -0
  217. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  218. package/template/coding-task-manager/lib/typing-interaction-trace.js +136 -2
  219. package/template/coding-task-manager/lib/typing-worker-stages.js +208 -1
  220. package/template/coding-task-manager/lib/usage-ledger/claude-parse.js +53 -2
  221. package/template/coding-task-manager/lib/usage-ledger/ingest.js +13 -4
  222. package/template/coding-task-manager/lib/usage-ledger/read.js +14 -0
  223. package/template/coding-task-manager/lib/usage-ledger/schema.js +49 -1
  224. package/template/coding-task-manager/lib/usage-ledger/store.js +337 -6
  225. package/template/coding-task-manager/lib/usage-ledger/task-read.js +123 -5
  226. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  227. package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
  228. package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
  229. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  230. package/template/coding-task-manager/lib/ws/input.js +1 -0
  231. package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
  232. package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
  233. package/template/coding-task-manager/lib/ws-router.js +68 -1
  234. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  235. package/template/coding-task-manager/oauth-proxy.js +99 -55
  236. package/template/coding-task-manager/package-lock.json +24 -0
  237. package/template/coding-task-manager/package.json +3 -0
  238. package/template/coding-task-manager/protocol/message-table.json +0 -10
  239. package/template/coding-task-manager/protocol/messages.js +0 -10
  240. package/template/coding-task-manager/protocol/ws-schema.json +0 -20
  241. package/template/coding-task-manager/protocol/ws-schema.md +0 -2
  242. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  243. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  244. package/template/coding-task-manager/public/css/app.css +279 -2
  245. package/template/coding-task-manager/public/css/reviews.css +4 -0
  246. package/template/coding-task-manager/public/index.html +297 -240
  247. package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
  248. package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
  249. package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
  250. package/template/coding-task-manager/public/js/background-work.js +13 -7
  251. package/template/coding-task-manager/public/js/client-health-core.js +81 -0
  252. package/template/coding-task-manager/public/js/client-health-main.js +61 -0
  253. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
  254. package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
  255. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  256. package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
  257. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  258. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  259. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  260. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  261. package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
  262. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  263. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -4
  264. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  265. package/template/coding-task-manager/public/js/protocol.js +0 -10
  266. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  267. package/template/coding-task-manager/public/js/reviews.js +7 -3
  268. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  269. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  270. package/template/coding-task-manager/public/js/row-join.js +18 -3
  271. package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
  272. package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
  273. package/template/coding-task-manager/public/js/session-usage-view.js +1 -0
  274. package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
  275. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  276. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  277. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  278. package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
  279. package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
  280. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  281. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  282. package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
  283. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  284. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  285. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -3
  286. package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
  287. package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
  288. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  289. package/template/coding-task-manager/public/m/app.css +226 -8
  290. package/template/coding-task-manager/public/m/app.js +502 -9
  291. package/template/coding-task-manager/public/m/index.html +38 -1
  292. package/template/coding-task-manager/public/m/machines.js +1313 -0
  293. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  294. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  295. package/template/coding-task-manager/public/m/sw.js +5 -0
  296. package/template/coding-task-manager/queue-engine.js +13 -0
  297. package/template/coding-task-manager/server/approvals.js +5 -6
  298. package/template/coding-task-manager/server/boot.js +136 -17
  299. package/template/coding-task-manager/server.js +964 -425
  300. package/template/coding-task-manager/session-integrity.js +7 -9
  301. package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
  302. package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
  303. package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
  304. package/template/coding-task-manager/workers/session-host-process.js +14 -3
  305. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  306. package/template/coding-task-manager/workers/worktree-worker.js +34 -0
  307. package/template/package-lock.json +2 -2
  308. package/template/package.json +1 -1
  309. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  310. package/template/shared/conversation-storage-v2-contract.js +42 -6
  311. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  312. package/template/shared/git-remote-key.js +83 -0
  313. package/template/shared/host-identity.js +98 -2
  314. package/template/shared/owner-write-safety.js +135 -0
  315. package/template/shared/public-runtime.js +13 -5
  316. package/template/shared/release-schema.json +3 -2
  317. package/template/shared/report-citations.js +14 -0
  318. package/template/shared/sqlite-write-lock.js +361 -223
  319. package/template/shared/storage/contract.js +35 -3
  320. package/template/wall-e/agent.js +21 -0
  321. package/template/wall-e/api-walle.js +5 -0
  322. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  323. package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
  324. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  325. package/template/wall-e/brain/migrations.js +51 -22
  326. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  327. package/template/wall-e/brain/owner-call-whitelist.js +16 -0
  328. package/template/wall-e/brain/schema-ensures.js +22 -0
  329. package/template/wall-e/brain.js +1 -1
  330. package/template/wall-e/coding/host-capabilities.js +2 -2
  331. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  332. package/template/wall-e/coding/session-journal.js +28 -40
  333. package/template/wall-e/coding-orchestrator.js +23 -26
  334. package/template/wall-e/fly.toml +1 -1
  335. package/template/wall-e/http/auth.js +18 -1
  336. package/template/wall-e/http/blog-views-api.js +318 -0
  337. package/template/wall-e/http/helper-rules-api.js +321 -0
  338. package/template/wall-e/http/model-proxy-providers.js +3 -1
  339. package/template/wall-e/http/telemetry-api.js +12 -0
  340. package/template/wall-e/lib/blog-view-policy.js +88 -0
  341. package/template/wall-e/lib/blog-view-store.js +169 -0
  342. package/template/wall-e/lib/client-config-lock.js +2 -1
  343. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  344. package/template/wall-e/lib/error-kinds.js +10 -1
  345. package/template/wall-e/lib/feature-manifest.js +1 -1
  346. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  347. package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
  348. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  349. package/template/wall-e/lib/helper-rules/grade.js +277 -0
  350. package/template/wall-e/lib/helper-rules/label.js +247 -0
  351. package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
  352. package/template/wall-e/lib/helper-rules/store.js +667 -0
  353. package/template/wall-e/lib/mcp-integration.js +5 -6
  354. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  355. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
  356. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  357. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  358. package/template/wall-e/llm/default-fallback.js +4 -0
  359. package/template/wall-e/llm/model-catalog.js +13 -4
  360. package/template/wall-e/llm/model-roles.js +6 -0
  361. package/template/wall-e/llm/portkey-http.js +150 -0
  362. package/template/wall-e/llm/portkey-sync.js +15 -145
  363. package/template/wall-e/llm/worker-mcp-routing.js +61 -0
  364. package/template/wall-e/llm/worker-route.js +43 -1
  365. package/template/wall-e/loops/brain-custom-writes.js +45 -0
  366. package/template/wall-e/mcp-protocol-contract.js +41 -1
  367. package/template/wall-e/mcp-server.js +13 -2
  368. package/template/wall-e/mcp-worker-schemas.js +20 -2
  369. package/template/wall-e/memory/ctm-context-client.js +25 -5
  370. package/template/wall-e/shared/release-schema.json +3 -2
  371. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  372. package/template/wall-e/shared/storage/contract.js +35 -3
  373. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  374. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
  375. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
  376. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
  377. package/template/wall-e/tools/local-tools.js +12 -5
  378. package/template/wall-e/workers/brain-owner-worker.js +9 -0
  379. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  380. package/template/wall-e/workers/telemetry-worker.js +8 -0
  381. package/template/coding-task-manager/lib/giant-freeze-park.js +0 -31
@@ -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,
@@ -197,10 +199,6 @@ const {
197
199
  } = require('./lib/deferred-input-generation');
198
200
  const { createResumeReadyEstimator } = require('./lib/resume-ready-history');
199
201
  const { resumeTranscriptPaths } = require('./lib/resume-transcript-stat');
200
- const {
201
- DEFAULT_GIANT_ROLLOUT_BYTES,
202
- shouldFreezeParkGiant,
203
- } = require('./lib/giant-freeze-park');
204
202
 
205
203
  const http = require('http');
206
204
  const https = require('https');
@@ -228,6 +226,8 @@ function _readPerfCertificationLedger() {
228
226
  } catch { return {}; } // expected: missing/corrupt ledger surfaces as empty, not a 500
229
227
  }
230
228
  const { createTypingInteractionTraceStore, noteWorkerStageMessage, pendingInteractionIdsFor } = require('./lib/typing-interaction-trace');
229
+ // WP-3 (§5 D-C2): the single-grapheme input class that can carry an echo prediction -- see lib/typing-worker-stages.js.
230
+ const { isSingleGraphemeEcho } = require('./lib/typing-worker-stages');
231
231
  const { getWorkerLoopLag, buildRollupExtras, noteIfWorkerLoopLag } = require('./lib/state-sync/worker-loop-lag-state');
232
232
  const { createVisibleRevisionWatchdog } = require('./lib/visible-revision-watchdog');
233
233
  const { createClientDiagHandler } = require('./lib/client-diag-handler');
@@ -455,7 +455,7 @@ if (_shadowBootSafetyMs > 0) {
455
455
  if (t && typeof t.unref === 'function') t.unref();
456
456
  }
457
457
 
458
- const { mirrorSessionTitle, fillClaudeAgentTitle } = require('./lib/session-title-mirror');
458
+ const { mirrorSessionTitle, applyProviderTitle } = require('./lib/session-title-mirror');
459
459
  const streamResizePolicy = require('./public/js/stream-resize-policy');
460
460
  const { turnSettleQuietResizeAction, deferredResizeReleaseDecision } = require('./lib/deferred-resize-quiet-policy');
461
461
  const { rowsFlapResizeDecision, DEBOUNCE_MS: ROWS_FLAP_DEBOUNCE_MS, MAX_HOLD_MS: ROWS_FLAP_MAX_HOLD_MS } = require('./lib/rows-flap-debounce');
@@ -651,6 +651,8 @@ const { isCrashCapTripped: _isHostCrashCapTripped } = require('./lib/host-crash-
651
651
  // _shutdownOrphanHost below).
652
652
  const { createHostHealthLedger, readStallFromDataDir } = require('./lib/host-health-ledger');
653
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');
654
656
  const {
655
657
  createPtyStartupReservation,
656
658
  ptyStartupCommitComplete,
@@ -699,6 +701,7 @@ const _typingInteractionTraces = createTypingInteractionTraceStore({
699
701
  publicLimit: 128,
700
702
  ttlMs: 60_000,
701
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
702
705
  // D-1 (composer-typing-freeze-keystroke-slo-2026-09-11.html §5 D-1, §8 WP-0): persist the
703
706
  // keystroke-to-paint distribution past the 60s in-memory ring. _eventLoopIncidents is
704
707
  // resolved lazily here (this callback only ever runs later, on the flush timer below) even
@@ -1507,17 +1510,7 @@ _perfTracker.namedInterval('timer:visible-revision-watchdog', () => {
1507
1510
  recordDiagnostic: recordSessionDiagnostic,
1508
1511
  coalescer: _visibleRevisionMarginalCoalescer,
1509
1512
  });
1510
- const session = sessions.get(incident.sessionId);
1511
- const target = (session?.clients || []).find((client) => (
1512
- client.clientId === incident.clientId
1513
- && Number(client.socketGeneration || 0) === Number(incident.socketGeneration || 0)
1514
- ));
1515
- if (target?.readyState !== 1 || incident.recoveryAction === 'report-only') continue;
1516
- // WP-3b (delivery-path reset): the 'fence-generation' recovery action used to fence the
1517
- // client here (server-side belief the client's baseline generation is invalid); the client
1518
- // fence is deleted, so this incident now only ever asks for a keyframe, same as any other
1519
- // recovery action.
1520
- _requestKeyframeGrant(target, incident.sessionId, { priority: true });
1513
+ // Report-only (lib/visible-revision-watchdog.js): a late boundary never forces a keyframe.
1521
1514
  }
1522
1515
  }, 25).unref?.();
1523
1516
  // WP-3b (delivery-path reset): the fence-breaker sweep (lib/state-sync/fence-breaker.js) existed
@@ -1666,13 +1659,11 @@ const {
1666
1659
  computeParkSweepMaxParks,
1667
1660
  classifyWakeOutcome,
1668
1661
  describePressureCause,
1669
- evaluateFreeze,
1670
1662
  evaluateManualPark,
1671
1663
  evaluatePark,
1672
1664
  lifecycleConfigFromEnv,
1673
1665
  normalizeActivationSource,
1674
1666
  normalizeClientKind,
1675
- selectFreezeVictim,
1676
1667
  selectParkVictim,
1677
1668
  summarizeParkEligibility,
1678
1669
  } = require('./lib/session-lifecycle');
@@ -2000,6 +1991,7 @@ const { splitMirrorAndBrowserChunks } = require('./lib/mirror-feed-sanitize');
2000
1991
  const {
2001
1992
  buildSessionStandupSnapshot,
2002
1993
  isWalleOwnedSession,
1994
+ remoteStandupSession,
2003
1995
  selectUnmatchedStartupTaskPlaceholders,
2004
1996
  isStandupStartupTaskSeedEnabled,
2005
1997
  } = require('./lib/session-standup');
@@ -2088,7 +2080,7 @@ const {
2088
2080
  const {
2089
2081
  createCodingAgentModelSync,
2090
2082
  resolveModelToRegistry,
2091
- shouldApplyCodexAutoTitle,
2083
+ getCodexThreadAgentName,
2092
2084
  } = require('./lib/coding-agent-models');
2093
2085
  const { MODEL_RESULT_PAGE_SIZE, MAX_MODEL_RESULT_BYTES } = require('./lib/coding-session-model-projection'); const { createCodingSessionModelRuntime } = require('./lib/coding-session-model-runtime');
2094
2086
  const { createCodingSessionGenerationRuntime } = require('./lib/coding-session-generation-runtime');
@@ -2145,6 +2137,10 @@ const { evaluateWaitState, shouldClearWaitCard, deriveChoiceLabel, shouldSuperse
2145
2137
  const { selectionCardForViewport } = require('./lib/terminal-interaction-policy');
2146
2138
  const promptIntent = require('./lib/prompt-intent');
2147
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');
2148
2144
  const { classifyResumeNoise, resumeFailureDetail } = require('./lib/resume-noise');
2149
2145
  const { createResumeAttemptTracker } = require('./lib/resume-attempts');
2150
2146
  const {
@@ -2194,6 +2190,60 @@ const resumeFromStoreApi = require('./lib/api/resume-from-store');
2194
2190
  const servicesApi = require('./lib/api/services'); const devFaultApi = require('./lib/api/dev-fault');
2195
2191
  const setupNetworkApi = require('./lib/api/setup-network'); const runtimeApprovalsApi = require('./lib/api/runtime-approvals');
2196
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
+ });
2197
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
2198
2248
  const wsRouter = require('./lib/ws-router');
2199
2249
  const { runWsMessageWithTrace } = require('./lib/ws-causal-context');
@@ -2494,7 +2544,7 @@ const {
2494
2544
  loadConfig,
2495
2545
  ensureWalleToken,
2496
2546
  } = serverConfig;
2497
- 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
2498
2548
  const bestOfAttempts = createBestOfAttemptService({
2499
2549
  statePath: path.join(CONFIG_DIR, 'best-of-attempts.json'),
2500
2550
  launchAttempt: _launchBestOfAttempt,
@@ -2668,9 +2718,9 @@ const _diagBatcher = createSessionDiagnosticBatcher({
2668
2718
  },
2669
2719
  flush: (batch) => {
2670
2720
  // Off-thread: route the durable batch write through the db-owner worker (the designated
2671
- // serial writer) so the cross-process SQLite write-lock claim (_writeLockClaim's sync
2672
- // writeFileSync/linkSync) never blocks the main event loop — it was a ~110ms main-thread
2673
- // 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.
2674
2724
  _runCtmDbWorkerOp('flushSessionDiagnostics', { entries: batch }, { bestEffort: true })
2675
2725
  .catch(() => {});
2676
2726
  },
@@ -4890,7 +4940,7 @@ async function _launchBestOfAttempt(spec) {
4890
4940
  const created = await handleCreatePrepared(null, {
4891
4941
  id: sessionId,
4892
4942
  title,
4893
- label: title,
4943
+ label: title, titleIntent: 'default', // D-13: `_bestOfAttemptTitle` is CTM-composed (tier 3), never a user name
4894
4944
  cmd: source.cmd || agentType,
4895
4945
  args: bestOfFreshAgentArgs(agentType, source.args, { approvalHookToken: source._approvalHookToken || '' }),
4896
4946
  cwd: workspace.cwd,
@@ -7478,19 +7528,11 @@ function _applyBootRepairsResult(repairs) {
7478
7528
  const timer = setTimeout(continueWalleIdentityRekey, 250);
7479
7529
  timer.unref?.();
7480
7530
  }
7481
- const repairedTitles = repairs?.titleRepair;
7482
- if (repairedTitles?.promoted) {
7483
- console.log(`[session-title] promoted ${repairedTitles.promoted} active startup title(s) into ctm_sessions.title`);
7484
- telemetry.track('session_title_startup_repair', {
7485
- promoted: repairedTitles.promoted,
7486
- scanned: repairedTitles.scanned || 0,
7487
- });
7488
- }
7489
7531
  return _refreshControlSnapshot({ reason: 'db-owner-ready' });
7490
7532
  }
7491
7533
 
7492
7534
  // WP-B / B1 (2026-07-31 boot-wedge outage): runBootRepairs performs REPAIRS
7493
- // (session-title repair, codex identity/metadata repair, conversation-source
7535
+ // (codex identity/metadata repair, conversation-source
7494
7536
  // migration, order-key backfill, Wall-E identity rekey) — none of it is
7495
7537
  // load-bearing for serving HTTP. The old code awaited this with no
7496
7538
  // try/catch, so a single rejection (that night: a worker-request timeout)
@@ -7857,7 +7899,10 @@ const sessionHostManager = createSessionHostManager({
7857
7899
  // WP-0 (pty-host-threadpool-stranded-waiters-2026-09-25.html, D-3/D-6): report-only
7858
7900
  // seam for the pool/dedicated-host threadpool monitor's frames -- see
7859
7901
  // _recordThreadpoolDiagnostic above and apiServicesStatus's golden-signals feed below.
7860
- 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); },
7861
7906
  // WP-1 (pty-host-threadpool-stranded-waiters-2026-09-25.html, D-2): the manager has no
7862
7907
  // DB/session access of its own -- forward every input-ack frame to the one function
7863
7908
  // that turns it into typing-trace/durable-phase truth.
@@ -7865,7 +7910,7 @@ const sessionHostManager = createSessionHostManager({
7865
7910
  // Probe (mouse-mode handoff): host-reported per-session diagnostic rows (host-handoff-mirror-seed/-restored).
7866
7911
  onSessionDiagnostic: (sessionId, event, details) => recordSessionDiagnostic(sessionId, event, details),
7867
7912
  });
7868
- _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
7869
7914
  if (serverConfig.CTM_SESSION_HOST_PROCESS !== '0' && process.env.CTM_SESSION_HOST_POOL_PREWARM !== '0') {
7870
7915
  try { sessionHostManager.prewarmPool?.(); }
7871
7916
  catch (e) { console.error('[session-host-pool] prewarm error:', e.message); }
@@ -7876,8 +7921,16 @@ if (serverConfig.CTM_SESSION_HOST_PROCESS !== '0' && process.env.CTM_SESSION_HOS
7876
7921
  // process globals inside lib/host-health-ledger.js itself); its default
7877
7922
  // D-2(b) input is the WP-0 stall file a stalled pool host writes for its
7878
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'.
7879
7930
  const hostHealth = createHostHealthLedger({
7880
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),
7881
7934
  });
7882
7935
  // Final-review fix I6: a host that exits (normally OR by crash) must leave the
7883
7936
  // ledger, or its miss history outlives it -- and a later host reusing the same
@@ -10460,41 +10513,25 @@ setDbMaintenanceRunner(runOnlineDbBackup);
10460
10513
  setImageMetricsRecorder(event => _uploadMetrics.recordUpload(event));
10461
10514
  // WP-0 §6.3/S-8: tpOldestMs on the [img-upload] line comes from the main-process monitor above.
10462
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
+ });
10463
10521
  async function _prepareMediaStore() {
10464
- if (!_mediaWriteBehindEnabled) return null;
10522
+ if (!_mediaWriteBehindEnabled) { await _legacyDraftCancellationRecovery.prepare(); return null; }
10465
10523
  if (_mediaStore) return _mediaStore;
10466
10524
  if (_mediaStorePromise) return _mediaStorePromise;
10467
- _mediaStorePromise = (async () => {
10468
- const registry = await _runCtmDbWorkerOp('loadMediaStoreRegistry', {}, {
10469
- // Listener activation depends on this registry. It is boot-critical
10470
- // maintenance, while the journal projections below remain background work.
10471
- priority: 'high',
10472
- requestClass: 'maintenance',
10473
- timeoutMs: 30_000,
10474
- });
10475
- return createMediaStore(buildMediaStoreOptions({
10476
- dataDir: serverConfig.CTM_DATA_DIR || path.dirname(dbModule.getDbPath()),
10477
- 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,
10478
10528
  runDbWorkerOp: _runCtmDbWorkerOp,
10479
10529
  onDrainProgress: () => { try { _loadPressure.recordProducerProgress('media-journal'); } catch {} },
10480
- // WP-1 (docs/image-draft-event-ordering-and-verified-adoption-2026-10-14.html §6.4): the store
10481
- // is the single draft version allocator. It recovers the highest pending journal version and
10482
- // then applies verified owner-lineage normalization as one journaled mutation — the draft
10483
- // restamp was removed from db.js restampOwnerLineage so the two can never allocate
10484
- // concurrently.
10485
- //
10486
- // Review finding 9: the identity source is the LAZY, MEMOIZED accessor
10487
- // (lib/instance-identity-access.js), not a synchronous main-thread `dbModule.…()` read on the
10488
- // store-preparation path. getInstanceIdentity() derives at most once per process, only after
10489
- // the DB is open, and returns null instead of ever inventing a hostname/port-derived default;
10490
- // isSameOwnerLineage is the existing dbModule-free lineage snapshot used by the session-owner
10491
- // hot path. Neither is a new DB boundary — both are already the reviewed identity seams.
10492
- currentOwnerId: _instanceIdentityAccess.getInstanceIdentity({ required: false })?.instanceId
10530
+ getCurrentOwnerId: () => _instanceIdentityAccess.getInstanceIdentity({ required: false })?.instanceId
10493
10531
  || _instanceIdentityAccess.getCachedInstanceId()
10494
10532
  || serverConfig.CTM_INSTANCE_ID || '',
10495
10533
  isSameOwnerLineage: _isSameOwnerLineageSnapshot,
10496
- }));
10497
- })().then(store => {
10534
+ }).then(store => {
10498
10535
  // Review finding 6: permanent-delete receipts observed while this store was preparing are
10499
10536
  // applied FIRST — before `_mediaStore` is assigned and therefore before any reader (or the boot
10500
10537
  // reconciler below) can observe the registry.
@@ -10517,6 +10554,7 @@ async function _prepareMediaStore() {
10517
10554
  }
10518
10555
 
10519
10556
  async function _closeMediaStore(timeoutMs = 2_000) {
10557
+ await _legacyDraftCancellationRecovery.close();
10520
10558
  const store = _mediaStore;
10521
10559
  if (!store) return;
10522
10560
  _mediaStore = null;
@@ -10538,7 +10576,8 @@ setImageSaveRunner(async ({ promptId, buffer, filename, mimeType, deferRow, requ
10538
10576
  dbModule.saveImageDecoupled(promptId, buffer, filename, mimeType, { deferRow: !!deferRow }));
10539
10577
  });
10540
10578
 
10541
- function _runLegacyTerminalDraftDbAction(action, args) {
10579
+ async function _runLegacyTerminalDraftDbAction(action, args) {
10580
+ await _legacyDraftCancellationRecovery.prepare();
10542
10581
  return _runCtmDbWorkerOp('runDbMutation', { action, args }, {
10543
10582
  priority: 'urgent',
10544
10583
  requestClass: 'user-blocking',
@@ -11753,7 +11792,11 @@ function _auditAuthDecision({ req, auth, rule, decision, route, wsType, action,
11753
11792
  function _rateLimitDecision(auth, rule, keyPath) {
11754
11793
  if (auth?.isLoopback) return { ok: true };
11755
11794
  const key = auth?.deviceId || auth?.tokenHashPrefix || 'anonymous';
11756
- 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);
11757
11800
  const decision = authRateLimiter.check(kind, key);
11758
11801
  return decision.ok ? decision : { status: 429, ...decision };
11759
11802
  }
@@ -14689,6 +14732,23 @@ async function _observeRemoteSubmission(context, attempt = 0) {
14689
14732
  return;
14690
14733
  }
14691
14734
 
14735
+ // The status-bus busy stamp is in memory; the durable match below resolves transcript files on
14736
+ // the read pool, which can queue for minutes under load (2026-10-21: a prompt the agent took up
14737
+ // 0.2 s after Enter stayed "Still confirming..." for 90 s behind resolveSessionFiles). An idle
14738
+ // submission is confirmed by the busy stamp the moment it exists. Mid-turn submissions are not:
14739
+ // the session was busy anyway (D-3).
14740
+ const submittedWhileGenerating = !!context.submittedWhileGenerating;
14741
+ if (!submittedWhileGenerating && Number(session?._lastBusyStatusAt || 0) >= Number(startedAt || 0)) {
14742
+ storeStatus('response_started', {
14743
+ reason: 'session_marked_busy_after_submit',
14744
+ message: `The agent started working after the ${submissionLabel} was submitted.`,
14745
+ attempts: attempt,
14746
+ source: submissionSource,
14747
+ evidenceSource: 'status-bus',
14748
+ });
14749
+ return;
14750
+ }
14751
+
14692
14752
  const durableMatch = await _remoteSubmissionDurablePromptMatch(sessionId, text, {
14693
14753
  minTimestampMs: Number(startedAt || 0) || 0,
14694
14754
  timestampToleranceMs: 120000,
@@ -14712,7 +14772,6 @@ async function _observeRemoteSubmission(context, attempt = 0) {
14712
14772
  // ONLY by its on-screen echo or a durable transcript row (checked above) —
14713
14773
  // "status bus went busy" and "an agent progress line appeared" are both
14714
14774
  // true anyway mid-turn, so they cannot be used as delivery evidence here.
14715
- const submittedWhileGenerating = !!context.submittedWhileGenerating;
14716
14775
  // Fix round 1, finding 3: the tri-state ('composer'/'echoed'/'absent', ❯-aware)
14717
14776
  // drives ONLY the mid-turn path below. The idle path's `promptStillVisible`
14718
14777
  // (fed into markerEvidence, enter-retry, and needs_attention) must keep using
@@ -14737,7 +14796,6 @@ async function _observeRemoteSubmission(context, attempt = 0) {
14737
14796
  promptStillVisible,
14738
14797
  });
14739
14798
  const elapsedMs = Date.now() - Number(startedAt || Date.now());
14740
- const busyAt = Number(session?._lastBusyStatusAt || 0) || 0;
14741
14799
 
14742
14800
  if (!isCurrent()) return;
14743
14801
  recordSessionDiagnostic(sessionId, 'remote-submission-observe-state', {
@@ -14780,17 +14838,6 @@ async function _observeRemoteSubmission(context, attempt = 0) {
14780
14838
  return;
14781
14839
  }
14782
14840
  } else {
14783
- if (busyAt >= Number(startedAt || 0)) {
14784
- storeStatus('response_started', {
14785
- reason: 'session_marked_busy_after_submit',
14786
- message: `The agent started working after the ${submissionLabel} was submitted.`,
14787
- attempts: attempt,
14788
- source: submissionSource,
14789
- evidenceSource: 'status-bus',
14790
- });
14791
- return;
14792
- }
14793
-
14794
14841
  if (legacyDetail.agentProgressAfterPrompt && elapsedMs >= 500) {
14795
14842
  storeStatus('response_started', {
14796
14843
  reason: 'terminal_progress_after_prompt',
@@ -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
 
@@ -27382,9 +27547,6 @@ function _markMeaningfulActivity(session, source = 'activity', timestamp = Date.
27382
27547
  function _markLifecycleFocus(sessionId, timestamp = Date.now()) {
27383
27548
  const session = sessions.get(String(sessionId || ''));
27384
27549
  if (!session) return 0;
27385
- if (session._lifecycleState === 'frozen') {
27386
- _thawLifecycleSession(session, { source: 'focus' });
27387
- }
27388
27550
  _claimLifecyclePrewarm(session, 'focus');
27389
27551
  const at = Math.max(Number(session._lastFocusedAt || 0), Number(timestamp) || Date.now());
27390
27552
  session._lastFocusedAt = at;
@@ -27519,7 +27681,7 @@ function _sessionLifecycleView(session, options = {}) {
27519
27681
  state: session?._lifecycleState || (session?._restoreDeferred ? 'deferred' : (session?.ptyProcess ? 'live' : 'unavailable')),
27520
27682
  liveStatus: session?._restoreDeferred
27521
27683
  ? 'parked'
27522
- : (session?._lifecycleState === 'frozen' ? 'idle' : _standupLiveStatusForSession(session, now)),
27684
+ : _standupLiveStatusForSession(session, now),
27523
27685
  hasPendingApproval: !!(
27524
27686
  _standupIsBlockingWaitingReason(waitingReason)
27525
27687
  || approver?.reviewing
@@ -27536,7 +27698,7 @@ function _sessionLifecycleView(session, options = {}) {
27536
27698
  pinned: !!session?._lifecyclePinned,
27537
27699
  isActiveTab: !!(session?.id && _sessionIsDeclaredShown(session.id)),
27538
27700
  isMru: !!(session?.id && mruIds.has(session.id)),
27539
- hasBackgroundShell: !!(_statusLedgerFor(session)?.backgroundLive(now)) || (!_statusLedgerFor(session)?.backgroundDeclared && _backgroundAgentsActive(session, now)),
27701
+ hasBackgroundShell: !!(_statusLedgerFor(session)?.backgroundActive(now)) || (!_statusLedgerFor(session)?.backgroundDeclared && _backgroundAgentsActive(session, now)),
27540
27702
  isRestoring: _inRestoreStorm() || restoreRuntime.restoreStartingActive(session),
27541
27703
  recentNonStatusActivityAt: Number(session?._lastMeaningfulNonStatusAt || session?.lastMeaningfulActivityAt || 0),
27542
27704
  lastActivatedAt: Number(session?._lastActivatedAt || 0),
@@ -27916,6 +28078,25 @@ function _lifecycleProvider(session) {
27916
28078
  return detectAgentType(session?.cmd || '') || session?.agentType || session?.type || 'unknown';
27917
28079
  }
27918
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
+
27919
28100
  function _canonicalLifecycleWakePhases(phases = {}) {
27920
28101
  return {
27921
28102
  admitted: Number(phases.gateAdmittedAt || phases.activationRequestedAt || 0),
@@ -28158,12 +28339,23 @@ function _lifecycleOrphanTotals() {
28158
28339
  }
28159
28340
 
28160
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
+ }
28161
28353
  for (const [pgid, prev] of [..._lifecycleOrphanGroups]) {
28162
28354
  try {
28163
28355
  // D-4/D-6 (session-park-busy-background-shell-truth-2026-10-14): carry the stored survivor set
28164
28356
  // forward, so a survivor that leads its OWN process group (invisible to the pgid rollup) is not
28165
28357
  // forgotten by the next sweep while it is still alive.
28166
- const group = await _sessionResourceSampler.sampleProcessGroup(pgid, prev?.survivorPids || []);
28358
+ const group = await _sessionResourceSampler.sampleProcessGroup(pgid, prev?.survivorPids || [], { rows });
28167
28359
  if (group.procCount > 0) _lifecycleOrphanGroups.set(pgid, group);
28168
28360
  else _lifecycleOrphanGroups.delete(pgid);
28169
28361
  } catch {}
@@ -28208,76 +28400,6 @@ function _lifecycleProcessGroupId(session) {
28208
28400
  return Number.isInteger(pgid) && pgid > 1 ? pgid : 0;
28209
28401
  }
28210
28402
 
28211
- function _freezeLifecycleSession(sessionId, options = {}) {
28212
- const session = sessions.get(String(sessionId || ''));
28213
- if (!session || !session.ptyProcess || session._restoreDeferred || session._closing || session._exited) {
28214
- return { ok: false, code: 'session_not_live' };
28215
- }
28216
- if (!sessionPolicy(session).parkable) return { ok: false, code: 'session_not_parkable' };
28217
- if (session._lifecycleState === 'frozen') return { ok: true, alreadyFrozen: true };
28218
- const now = Number(options.now) || Date.now();
28219
- const decision = evaluateFreeze(
28220
- _sessionLifecycleView(session, { now, mruIds: options.mruIds }),
28221
- { now, connectedClients: Number(options.connectedClients ?? _connectedLifecycleClientCount()) },
28222
- options.policyConfig || _lifecycleConfig,
28223
- );
28224
- if (!decision.eligible) {
28225
- return { ok: false, code: 'session_protected', protections: decision.protections };
28226
- }
28227
- const pgid = _lifecycleProcessGroupId(session);
28228
- if (!pgid) return { ok: false, code: 'process_group_unavailable' };
28229
- try {
28230
- process.kill(-pgid, 'SIGSTOP');
28231
- } catch (error) {
28232
- _lifecycleDiagnostics.recordFreeze({ id: session.id, action: 'failed', reason: error.code || error.message });
28233
- return { ok: false, code: 'freeze_signal_failed', error: error.message };
28234
- }
28235
- session._lifecycleState = 'frozen';
28236
- session._frozenAt = now;
28237
- session._freezePgid = pgid;
28238
- session._frozenInputQueue = [];
28239
- _lifecycleDiagnostics.recordFreeze({ id: session.id, action: 'frozen', idleMs: decision.idleMs });
28240
- broadcastToSession(session.id, session, { type: 'session-frozen', id: session.id, frozenAt: now });
28241
- broadcastSessionList(true);
28242
- console.log(`[lifecycle] freeze id=${String(session.id).slice(0, 8)} idleMin=${Math.floor(decision.idleMs / 60_000)} pgid=${pgid}`);
28243
- return { ok: true, sessionId: session.id, frozenAt: now };
28244
- }
28245
-
28246
- function _thawLifecycleSession(sessionOrId, options = {}) {
28247
- const session = typeof sessionOrId === 'string' ? sessions.get(sessionOrId) : sessionOrId;
28248
- if (!session) return { ok: false, code: 'session_not_found' };
28249
- if (session._lifecycleState !== 'frozen') return { ok: true, alreadyLive: true };
28250
- const pgid = Number(session._freezePgid || _lifecycleProcessGroupId(session));
28251
- if (!Number.isInteger(pgid) || pgid <= 1) return { ok: false, code: 'process_group_unavailable' };
28252
- try {
28253
- process.kill(-pgid, 'SIGCONT');
28254
- } catch (error) {
28255
- _lifecycleDiagnostics.recordFreeze({ id: session.id, action: 'thaw-failed', reason: error.code || error.message });
28256
- return { ok: false, code: 'thaw_signal_failed', error: error.message };
28257
- }
28258
- const now = Date.now();
28259
- const frozenForMs = Math.max(0, now - Number(session._frozenAt || now));
28260
- const queuedInput = Array.isArray(session._frozenInputQueue) ? session._frozenInputQueue.splice(0) : [];
28261
- session._lifecycleState = 'live';
28262
- session._frozenAt = 0;
28263
- session._freezePgid = 0;
28264
- session._frozenInputQueue = [];
28265
- if (options.preserveIdle !== true) session._lastActivatedAt = now;
28266
- _lifecycleDiagnostics.recordFreeze({
28267
- id: session.id,
28268
- action: 'thawed',
28269
- idleMs: frozenForMs,
28270
- reason: String(options.source || 'activation'),
28271
- });
28272
- for (const data of queuedInput) _writeLifecyclePty(session, data, 'frozen-thaw');
28273
- if (options.broadcast !== false) {
28274
- broadcastToSession(session.id, session, { type: 'session-thawed', id: session.id, source: String(options.source || 'activation') });
28275
- broadcastSessionList(true);
28276
- }
28277
- console.log(`[lifecycle] thaw id=${String(session.id).slice(0, 8)} source=${String(options.source || 'activation')} frozenMs=${frozenForMs}`);
28278
- return { ok: true, sessionId: session.id, flushedInputs: queuedInput.length };
28279
- }
28280
-
28281
28403
  function _recordDroppedDeviceReply(session, source, classification) {
28282
28404
  if (!session?.id || !classification) return;
28283
28405
  recordSessionDiagnostic(session.id, 'input-dropped-device-reply', {
@@ -28353,7 +28475,7 @@ function _stripPtyOutputReplies(session, sessionId, data, site) {
28353
28475
  }
28354
28476
 
28355
28477
  function _writeLifecyclePty(session, data, source = 'input', options = {}) {
28356
- if (!session?.ptyProcess) return { posted: false, inputSeq: 0 };
28478
+ if (!session?.ptyProcess) return { posted: false, inputSeq: 0, dispatchDisposition: 'preflight-refused' };
28357
28479
  // Image, typing and lifecycle frames share one monotonic transport sequence. Typing keeps its
28358
28480
  // separate echo counter and passes the exact allocated frame sequence through options.inputSeq.
28359
28481
  // WP-2 (§6.2, D6): ONE monotonic allocator for every host frame. Keystrokes pass the exact seq they
@@ -28372,7 +28494,7 @@ function _writeLifecyclePty(session, data, source = 'input', options = {}) {
28372
28494
  // user-input funnel (source 'input', the ONE path _writeInputWithEchoTracking uses for
28373
28495
  // every keystroke and forwarded mouse report) stamps mouse/non-mouse recency for the
28374
28496
  // mouse-provoked-output gate. Every other source here is CTM-generated (auto-answer,
28375
- // dialog-skip, device-query reply, restore/frozen/park replay) or an authoritative device
28497
+ // dialog-skip, device-query reply, restore/park replay) or an authoritative device
28376
28498
  // reply -- never "the user just did X" -- and must not move either clock.
28377
28499
  if (source === 'input' && !authoritativeDeviceReply) {
28378
28500
  const stampAt = Date.now();
@@ -28384,35 +28506,23 @@ function _writeLifecyclePty(session, data, source = 'input', options = {}) {
28384
28506
  if (data === '\x1b[I' || data === '\x1b[O') session._lastViewerProvokeAt = stampAt; // focus in/out
28385
28507
  }
28386
28508
  }
28387
- if (session._lifecycleState === 'frozen') {
28388
- const classification = authoritativeDeviceReply ? null : classifyHostBoundReply(data);
28389
- if (classification) {
28390
- _recordDroppedDeviceReply(session, source, classification);
28391
- return { posted: false, inputSeq };
28392
- }
28393
- if (!Array.isArray(session._frozenInputQueue)) session._frozenInputQueue = [];
28394
- session._frozenInputQueue.push(data);
28395
- const result = _thawLifecycleSession(session, { source });
28396
- return { posted: !!result.ok, inputSeq };
28397
- }
28398
28509
  session._lifecycleInputInFlight = true;
28510
+ let dispatchDisposition = 'preflight-refused';
28399
28511
  try {
28400
- // WP-1 (D-2): `session.ptyProcess.write` is HostedPtyAdapter.write, which returns
28401
- // whether the frame was POSTED to the host -- never whether the byte was written.
28402
- // "written" is a fact that arrives later as an 'input-ack' frame (see
28403
- // _onHostInputAck below); nothing here may treat a truthy synchronous return as
28404
- // 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.
28405
28515
  const posted = writeWithDeviceReplyFirewall({
28406
28516
  data,
28407
28517
  exempt: authoritativeDeviceReply,
28408
- // A real HostedPtyAdapter.write() returns an explicit boolean ("posted to the
28409
- // host", never "written"). Legacy/non-hosted/test-double PTYs whose write() has no
28410
- // return value (undefined) keep today's behavior: not throwing means posted; only
28411
- // an EXPLICIT `false` return means the frame never left this process.
28412
- 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
+ },
28413
28523
  onDrop: classification => _recordDroppedDeviceReply(session, source, classification),
28414
28524
  });
28415
- return { posted: posted === true, inputSeq };
28525
+ return { posted: posted === true, inputSeq, dispatchDisposition };
28416
28526
  }
28417
28527
  finally { session._lifecycleInputInFlight = false; }
28418
28528
  }
@@ -28432,7 +28542,7 @@ function _lifecycleBackgroundLease(session, now = Date.now()) {
28432
28542
  const ledger = session ? _statusLedgerFor(session) : null;
28433
28543
  const snap = ledger && typeof ledger.backgroundSnapshot === 'function' ? ledger.backgroundSnapshot(now) : null;
28434
28544
  if (!snap || !Array.isArray(snap.entries) || !snap.entries.length) return null;
28435
- 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;
28436
28546
  for (const e of snap.entries) {
28437
28547
  if (e.type === 'shell') shells += 1; else tasks += 1;
28438
28548
  firstSeenAt = Math.min(firstSeenAt, Number(e.firstSeenAt) || 0);
@@ -28440,8 +28550,10 @@ function _lifecycleBackgroundLease(session, now = Date.now()) {
28440
28550
  expiresAt = Math.min(expiresAt, Number(e.expiresAt) || 0);
28441
28551
  renewals += Number(e.renewals) || 0;
28442
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);
28443
28555
  }
28444
- return { shells, tasks, firstSeenAt, renewedAt, expiresAt, renewals, capCrossed };
28556
+ return { shells, tasks, firstSeenAt, renewedAt, expiresAt, renewals, capCrossed, quiet, activeAt };
28445
28557
  }
28446
28558
 
28447
28559
  function _lifecycleJournalGate(session, decision, configName) {
@@ -28632,54 +28744,15 @@ async function parkSession(sessionId, options = {}) {
28632
28744
  protections: preparedDecision.protections }, preparedDecision);
28633
28745
  if (transcriptBefore.unknown) return finishParkAttempt({ ok: false, code: 'transcript_verification_unavailable', statusCode: 503 }, preparedDecision);
28634
28746
  if (!transcriptBefore.missing && !resumeSpec) return finishParkAttempt({ ok: false, code: 'session_not_resumable', statusCode: 409 }, preparedDecision);
28747
+ // WP-2 (docs/composer-freeze-followups-load-sigstop-attribution-2026-10-20.html §5 D-B1..D-B4): a giant rollout used to divert
28748
+ // here into the deleted SIGSTOP "frozen" state. There is no such state any more -- the park
28749
+ // falls through to the one kill ladder below (SIGTERM -> parkTermGraceMs -> SIGKILL).
28635
28750
  const rolloutSizeBytes = Math.max(
28636
28751
  0,
28637
28752
  Number(session._resumeLoadingFileSize || session.fileSize || session.file_size) || 0,
28638
28753
  Number(prepared.transcript?.size || 0),
28639
28754
  Number(transcriptBefore.size || 0),
28640
28755
  );
28641
- if (shouldFreezeParkGiant({
28642
- enabled: FREEZE_PARK_GIANT_ENABLED,
28643
- fileSizeBytes: rolloutSizeBytes,
28644
- pressure: _lifecyclePressure.level,
28645
- reason,
28646
- alreadyFrozen: session._lifecycleState === 'frozen',
28647
- })) {
28648
- const latestDecision = parkDecision(Date.now());
28649
- if (!latestDecision.eligible) {
28650
- return finishParkAttempt({
28651
- ok: false,
28652
- code: 'session_protected',
28653
- statusCode: 409,
28654
- protections: latestDecision.protections,
28655
- }, latestDecision);
28656
- }
28657
- const frozen = _freezeLifecycleSession(session.id, {
28658
- policyConfig: {
28659
- ...policyConfig,
28660
- freezeTier: true,
28661
- freezeAfterMs: Math.max(1, Number(policyConfig.idleParkAfterMs) || 1),
28662
- },
28663
- });
28664
- if (frozen.ok) {
28665
- recordSessionDiagnostic(session.id, 'giant-rollout-freeze-park', {
28666
- reason,
28667
- fileSizeBytes: rolloutSizeBytes,
28668
- pressure: _lifecyclePressure.level,
28669
- });
28670
- return finishParkAttempt({ ...frozen, lifecycleState: 'frozen', reason }, latestDecision);
28671
- }
28672
- recordSessionDiagnostic(session.id, 'giant-rollout-freeze-fallback', {
28673
- reason,
28674
- fileSizeBytes: rolloutSizeBytes,
28675
- pressure: _lifecyclePressure.level,
28676
- code: frozen.code || 'freeze_failed',
28677
- });
28678
- }
28679
- if (session._lifecycleState === 'frozen') {
28680
- const thaw = _thawLifecycleSession(session, { source: 'park', preserveIdle: true, broadcast: false });
28681
- if (!thaw.ok) return finishParkAttempt({ ok: false, code: thaw.code, statusCode: 500 }, decision);
28682
- }
28683
28756
 
28684
28757
  if (options.prewarmExpiry === true && session._prewarmExpiryTimer) {
28685
28758
  clearTimeout(session._prewarmExpiryTimer);
@@ -29136,6 +29209,17 @@ async function _sampleLifecycleResources(options = {}) {
29136
29209
  _sessionResourceSampler.warnedPressureFailure = true;
29137
29210
  console.warn('[lifecycle] memory-pressure probe was unparseable; treating pressure as normal');
29138
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
+ });
29139
29223
  for (const resource of snapshot.sessions || []) {
29140
29224
  const session = sessions.get(resource.id);
29141
29225
  if (!session || session._restoreDeferred) continue;
@@ -29152,7 +29236,7 @@ async function _sampleLifecycleResources(options = {}) {
29152
29236
  // matches every other footprint field's "sampled vs not yet" contract in this file.
29153
29237
  session.footprintMB = resource.footprintMB == null ? null : resource.footprintMB;
29154
29238
  _statusBindings.applyCpuSample(session, resource);
29155
- 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
29156
29240
  }
29157
29241
  const previousPressure = _lifecyclePressure.level;
29158
29242
  const pressureResult = _lifecyclePressure.observe(snapshot.signals || snapshot.pressure, {
@@ -29223,7 +29307,7 @@ async function _runLifecycleSweep() {
29223
29307
  const shouldTrim = pressure === 'warn' && _lifecycleConfig.pressureTrimFirst;
29224
29308
  let trimFreedMB = 0;
29225
29309
  let lastTrim = null;
29226
- if (shouldTrim && !_lifecycleConfig.freezeTier) {
29310
+ if (shouldTrim) {
29227
29311
  // AC-4d: awaited so `trim.mirrorRowsTrimmed` is real by the time it is journaled --
29228
29312
  // the mirror shrink is an IPC round-trip into the pooled host process.
29229
29313
  // WP-3/S7: shouldTrim is only true when pressure === 'warn' (see above), so this is
@@ -29245,7 +29329,6 @@ async function _runLifecycleSweep() {
29245
29329
  const policy = automaticParkSweepPolicy(_lifecycleConfig, {
29246
29330
  pressure,
29247
29331
  liveCount: _liveLifecycleSessions().length,
29248
- freezeTier: _lifecycleConfig.freezeTier,
29249
29332
  warnPersistedMs,
29250
29333
  trimFreedMB,
29251
29334
  });
@@ -29311,7 +29394,7 @@ async function _runLifecycleSweep() {
29311
29394
  ? 'pressure'
29312
29395
  : (pressure === 'warn' && policy.warnPark === true
29313
29396
  ? 'warn'
29314
- : (budget.actionable ? 'rss-budget' : (victim.state === 'frozen' ? 'freeze-expired' : 'idle')));
29397
+ : (budget.actionable ? 'rss-budget' : 'idle'));
29315
29398
  if (_lifecycleConfig.idleParkShadow) {
29316
29399
  const shadowSession = sessions.get(victim.id);
29317
29400
  if (shadowSession && !shadowSession._lifecycleShadowCandidateAt) {
@@ -29353,26 +29436,6 @@ async function _runLifecycleSweep() {
29353
29436
  }
29354
29437
  if (pressureMode && parked + 1 < maxParks) await new Promise(resolve => setTimeout(resolve, 1_000));
29355
29438
  }
29356
- if (_lifecycleConfig.freezeTier) {
29357
- const mruIds = _lifecycleMruIds();
29358
- for (let frozen = 0; frozen < _lifecycleConfig.parkMaxPerSweep; frozen += 1) {
29359
- const views = _liveLifecycleSessions().map(session => _sessionLifecycleView(session, { now, mruIds }));
29360
- const victim = selectFreezeVictim(views, { now, connectedClients }, _lifecycleConfig);
29361
- if (!victim) break;
29362
- const result = _freezeLifecycleSession(victim.id, { now, connectedClients, mruIds });
29363
- if (!result.ok) break;
29364
- if (shouldTrim && _lifecycleConfig.parkJournal) {
29365
- appendParkRecord({
29366
- event: 'pressure-freeze',
29367
- id: victim.id,
29368
- reason: 'pressure',
29369
- gate: _lifecycleJournalGate(sessions.get(victim.id), evaluateFreeze(victim, { now, connectedClients }, _lifecycleConfig), 'pressure'),
29370
- host: _lifecycleJournalHost(),
29371
- victimRssMB: Number(victim.rssMB) || 0,
29372
- });
29373
- }
29374
- }
29375
- }
29376
29439
  if (snapshot?.pressure && snapshot.pressure !== pressure) {
29377
29440
  console.log(`[lifecycle] pressure hysteresis observed=${snapshot.pressure} effective=${pressure}`);
29378
29441
  }
@@ -30039,7 +30102,12 @@ wss.on('connection', async (ws, req) => {
30039
30102
  const url = new URL(req.url, `http://${req.headers.host}`);
30040
30103
  const reqSnapshot = {
30041
30104
  socket: { remoteAddress: _remoteIpFromReq(req) },
30042
- 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
+ },
30043
30111
  };
30044
30112
  const cfDecision = cloudflareAccessVerifier.verifyRequestSync(req, { isLocalhost });
30045
30113
  if (!cfDecision.ok) {
@@ -30291,6 +30359,7 @@ wss.on('connection', async (ws, req) => {
30291
30359
  let msg;
30292
30360
  try { msg = JSON.parse(raw); } catch { return; }
30293
30361
  if (msg.type === 'input') msg._typingWsReceivedAt = wsReceivedAt;
30362
+ if (msg.type === 'submit-prompt') msg._wsReceivedAt = wsReceivedAt;
30294
30363
  // WP-4f (terminal-delivery-contract-followups-2026-09-23.html §Step 2, fix round 1): mark +
30295
30364
  // gate move earlier, synchronously, pre-await, so a same-tick client-info/attach race can't
30296
30365
  // drop a compatible attach as terminal-protocol-incompatible (the "viewer B" flake).
@@ -31532,6 +31601,9 @@ headlessWorker.on('message', _perfTracker.wrap('worker:headless-terminal-message
31532
31601
  ? _recordInputEchoFromFrame(session, msg.echoedThroughSeq, Date.now(), {
31533
31602
  targetClientId: String(msg.targetClientId || ''),
31534
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,
31535
31607
  })
31536
31608
  : [];
31537
31609
  const holdingRestoreFrames = _stateSyncHoldingRestoreFrames(session);
@@ -34039,6 +34111,35 @@ function _sendWalleSessionCreated(ws, session, extra = {}) {
34039
34111
  return true;
34040
34112
  }
34041
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
+
34042
34143
  // WP-3 (design/walle-conv-order-authority-2026-08-21 D-5, §6): min/max order key over a
34043
34144
  // served page's messages, for the envelope's keyRange. Min/max (not first/last array
34044
34145
  // position) because callers serve pages in different physical row orders (e.g. a
@@ -35641,7 +35742,6 @@ const RESUME_LIFECYCLE_V2_ENABLED = serverConfig.CTM_RESUME_LIFECYCLE_V2 !== '0'
35641
35742
  const RESUME_VIEWPORT_READINESS_ENABLED = process.env.CTM_RESUME_VIEWPORT_READINESS !== '0';
35642
35743
  const INPUT_READINESS_ENABLED = (process.env.CTM_INPUT_READINESS || '') !== '0';
35643
35744
  const INPUT_READY_GATE_ENABLED = (process.env.CTM_INPUT_READY_GATE || '') !== '0';
35644
- const FREEZE_PARK_GIANT_ENABLED = process.env.CTM_FREEZE_PARK_GIANT !== '0';
35645
35745
  // Refuse to convert an unreadable terminal mirror into a "the agent never became interactive"
35646
35746
  // verdict. Set to '0' to restore the pre-2026-07-19 behavior where an unreadable mirror killed
35647
35747
  // the resume.
@@ -35859,6 +35959,8 @@ function _noteInputEchoSequence(session, inputSeq, acceptedAt = Date.now()) {
35859
35959
  // before that synchronous write, then establish the worker barrier before a later PTY data
35860
35960
  // callback can enqueue output attributed to this input.
35861
35961
  session._flushHeadlessFeed?.();
35962
+ // WP-3 D-C2 (§6 W-1): the per-key payload the mirror's shadow echo matchers arm on.
35963
+ const tracedInput = session._typingInteractionByInputSeq?.get(inputSeq);
35862
35964
  headlessWorker.postMessage({
35863
35965
  type: 'note-input',
35864
35966
  sessionId: session.id,
@@ -35866,6 +35968,7 @@ function _noteInputEchoSequence(session, inputSeq, acceptedAt = Date.now()) {
35866
35968
  // WP-0b fix round 1 (S-10): 'note-input', not 'feed', is the op that reaches BOTH transports for every keystroke
35867
35969
  // (session-host-manager.js:1815-1834); a pool-hosted session's 'feed' never leaves that child process.
35868
35970
  interactionIds: pendingInteractionIdsFor(session._typingInteractionByInputSeq),
35971
+ echo: tracedInput?.echoChar ? { interactionId: tracedInput.interactionId, char: tracedInput.echoChar } : undefined,
35869
35972
  });
35870
35973
  } catch (error) {
35871
35974
  recordSessionDiagnostic(session.id, 'resume-input-seq-note-failed', {
@@ -35958,6 +36061,8 @@ function _writeInputWithEchoTracking(session, data, source = 'input', trace = {}
35958
36061
  clientId: String(trace.clientId || ''),
35959
36062
  socketGeneration: Number(trace.socketGeneration || 0),
35960
36063
  mirrored: false,
36064
+ // WP-3 D-C2: the predicted echo char for this key; '' for anything but one printable grapheme.
36065
+ echoChar: printable && isSingleGraphemeEcho(data) ? data : '',
35961
36066
  });
35962
36067
  while (session._typingInteractionByInputSeq.size > 64) {
35963
36068
  session._typingInteractionByInputSeq.delete(session._typingInteractionByInputSeq.keys().next().value);
@@ -36052,6 +36157,37 @@ function _onHostInputAck(session, ack, origin = null) {
36052
36157
  }
36053
36158
  }
36054
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
+
36055
36191
  function _recordInputEchoFromFrame(session, echoedThroughSeq, at = Date.now(), target = {}) {
36056
36192
  if (!session || !(session._inputEchoPending instanceof Map)) return [];
36057
36193
  const echoed = Math.max(0, Number(echoedThroughSeq) || 0);
@@ -36069,6 +36205,10 @@ function _recordInputEchoFromFrame(session, echoedThroughSeq, at = Date.now(), t
36069
36205
  }
36070
36206
  const targetClientId = String(target.targetClientId || '');
36071
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));
36072
36212
  for (const [seq, rawRecord] of session._typingInteractionByInputSeq || []) {
36073
36213
  if (seq > echoed) continue;
36074
36214
  const record = typeof rawRecord === 'string'
@@ -36077,6 +36217,14 @@ function _recordInputEchoFromFrame(session, echoedThroughSeq, at = Date.now(), t
36077
36217
  if (!record?.interactionId) continue;
36078
36218
  if (!record.mirrored) {
36079
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
+ }
36080
36228
  record.mirrored = true;
36081
36229
  session._typingInteractionByInputSeq.set(seq, record);
36082
36230
  }
@@ -36434,7 +36582,7 @@ function _terminateResumeLifecycle(session, failure) {
36434
36582
  agentType: session._resumeAgentType || detectAgentType(session.cmd) || '',
36435
36583
  generation,
36436
36584
  };
36437
- markSessionResumeBlocked(session.id, info);
36585
+ markSessionResumeBlocked(session.id, info, 'lifecycle-terminate');
36438
36586
  session._resumeAttemptFailed = true;
36439
36587
  // This is a system-enforced provider termination, not an explicit user close. The PTY exit
36440
36588
  // handler already suppresses auto-restart while `resumeBlocked` is set, and
@@ -36985,6 +37133,10 @@ const _DETERMINISTIC_PREFLIGHT_CODES = new Set([
36985
37133
  'missing_claude_transcript',
36986
37134
  'missing_codex_rollout',
36987
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',
36988
37140
  ]);
36989
37141
 
36990
37142
  // Pure decision (test seam): is a resume-preflight failure DETERMINISTIC (quarantine) or
@@ -37010,7 +37162,7 @@ function _shouldRefreshLaunchReadinessForBlock(code, blockDeterministic) {
37010
37162
  return code === 'launch_rollout_index_pending' && blockDeterministic !== true;
37011
37163
  }
37012
37164
 
37013
- function markSessionResumeBlocked(sessionId, info = {}) {
37165
+ function markSessionResumeBlocked(sessionId, info = {}, site = 'unspecified') {
37014
37166
  const id = String(sessionId || '');
37015
37167
  if (!id) return null;
37016
37168
  const record = {
@@ -37031,24 +37183,18 @@ function markSessionResumeBlocked(sessionId, info = {}) {
37031
37183
  checkedAt: info.checkedAt || '',
37032
37184
  // WP-2: 'removed'|'never_seen'; only set on the two missing-transcript codes.
37033
37185
  localHistory: info.localHistory || (['missing_claude_transcript', 'missing_codex_rollout'].includes(info.code) ? 'unknown' : ''), lastSeenBytes: Number(info.lastSeenBytes) || 0,
37034
- 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 || '',
37035
37187
  // WP-6: the resume ladder's answer, only on the two missing-transcript codes.
37036
37188
  resumeRung: info.resumeRung || '', coverage: info.coverage || null, offerStoreFirst: info.offerStoreFirst === true, restoreError: info.restoreError || '',
37037
37189
  };
37038
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));
37039
37194
  // WP-2: callers that built an info object inline get the enriched record back instead of re-reading the map.
37040
37195
  return record;
37041
37196
  }
37042
37197
 
37043
- // The ladder fields every resume-blocked message for a missing transcript carries (S-33).
37044
- function _resumeBlockedRungWire(info) {
37045
- return {
37046
- resumeRung: info?.resumeRung || '', coverage: info?.coverage || null, offerStoreFirst: info?.offerStoreFirst === true, restoreError: info?.restoreError || '',
37047
- deterministic: info?.deterministic === true, origin: info?.origin || 'unknown',
37048
- storeTurns: normalizeStoreTurns(info?.storeTurns), checkedAt: info?.checkedAt || '',
37049
- };
37050
- }
37051
-
37052
37198
  // WP-2 (multi-machine §C): the pure relative-origin rule now lives in lib/resume-origin-facts.js, so this
37053
37199
  // launch-plan projection, the resume-blocked record and the transcripts route share exactly one rule.
37054
37200
  // Re-exported here under the name every existing call site in this file already uses.
@@ -37083,9 +37229,17 @@ function isSessionResumeBlocked(sessionId) {
37083
37229
  // explicit user action (attach, click, "Start fresh here") — those paths consult the block
37084
37230
  // directly (see `_startDeferredRestoreSession`), not this drain-eligibility predicate.
37085
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.
37086
37234
  if (!session || !session._restoreDeferred || session._restoreStarting || session._exited) return false;
37087
- if (!sessionPolicy(session).relaunchFromRestore) return false;
37088
- 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
+ }
37089
37243
  return true;
37090
37244
  }
37091
37245
 
@@ -37160,16 +37314,16 @@ function _notifyResumePreflightFailure(id, ws, agentType, result) {
37160
37314
  let blockedInfo = null;
37161
37315
  if (deterministic) {
37162
37316
  blockedInfo = {
37163
- ..._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,
37164
37318
  origin: result.origin || _resumeOriginForSession(id), storeTurns: result.storeTurns, checkedAt: result.checkedAt || new Date().toISOString(),
37165
- 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,
37166
37320
  };
37167
- 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)
37168
37322
  }
37169
37323
  if (!ws) return deterministic;
37170
37324
  if (deterministic) {
37171
37325
  try {
37172
- _wsSend(ws, JSON.stringify(resumeBlockedWire(id, blockedInfo, { agentType, rungWire: _resumeBlockedRungWire(blockedInfo) })));
37326
+ _wsSend(ws, JSON.stringify(resumeBlockedWire(id, blockedInfo, { agentType, rungWire: resumeBlockedRungWire(blockedInfo) })));
37173
37327
  } catch {}
37174
37328
  } else {
37175
37329
  try {
@@ -37230,7 +37384,7 @@ function _notifyPreparedResumePreflightFailure(id, ws, agentType, resumePrefligh
37230
37384
  if (MISSING_TRANSCRIPT_CODES.has(resumePreflight.code) && existingSession) {
37231
37385
  existingSession._resumeRung = {
37232
37386
  code: resumePreflight.code, message: resumePreflight.message || '', resumeId: resumePreflight.resumeId || '',
37233
- ..._resumeBlockedRungWire(resumePreflight), origin: resumePreflight.origin || _resumeOriginForSession(id),
37387
+ ...resumeBlockedRungWire(resumePreflight), origin: resumePreflight.origin || _resumeOriginForSession(id),
37234
37388
  };
37235
37389
  }
37236
37390
  _notifyResumePreflightFailure(id, ws, agentType, resumePreflight);
@@ -37268,7 +37422,7 @@ function _blockResumeAttempt(session, code, message, detail) {
37268
37422
  attempts: Number(attemptState.failures || attemptState.attempt || 1),
37269
37423
  agentType: session._resumeAgentType || detectAgentType(session.cmd) || '',
37270
37424
  };
37271
- markSessionResumeBlocked(session.id, info);
37425
+ markSessionResumeBlocked(session.id, info, 'attempt-block');
37272
37426
  markResumeFailureExit(session, { code, source: code });
37273
37427
  _clearResumeLoading(session, code);
37274
37428
  recordSessionDiagnostic(session.id, 'resume-blocked', info);
@@ -37502,6 +37656,9 @@ const preparedSessionCreate = require('./lib/session-create/prepared').createPre
37502
37656
  ptyStartupCommitComplete,
37503
37657
  _rollbackPtyStartupAttempt,
37504
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,
37505
37662
  get _ownerRecovery() { return _ownerRecovery; },
37506
37663
  });
37507
37664
 
@@ -37830,6 +37987,8 @@ sessionCreateProvider.configure(() => ({
37830
37987
  parseSessionStartMs,
37831
37988
  path,
37832
37989
  recordSessionDiagnostic,
37990
+ // WP-1 (§5 D-4): the deterministic refusal hook the launch-time native-provider gate uses.
37991
+ markSessionResumeBlocked,
37833
37992
  resolveResumeGeneration,
37834
37993
  // WP-2 (default-subagent-model-delegate-pick-2026-10-13.html §5 D-1..D-3): the Wall-E delegate
37835
37994
  // pick reader for a lead launch. Injected here (not required in provider-process) so the launch
@@ -38108,6 +38267,40 @@ function apiResumeSession(req, res) {
38108
38267
  if (_ownerDecisionInvalidRequest(ownerDecision)) return sendJsonResponse(res, 400, { ok: false, error: 'SESSION_ID_REQUIRED' });
38109
38268
  _emitOwnerMismatchDiagnostic(ownerDecision);
38110
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
+ }
38111
38304
  await handleCreatePrepared(null, {
38112
38305
  id: launch.id,
38113
38306
  title: launch.title,
@@ -38985,6 +39178,32 @@ const _serverMemorySampler = startServerMemorySamplerRuntime({
38985
39178
  }
38986
39179
  return out;
38987
39180
  },
39181
+ // WP-A (freeze-probes) P-2/P-3: what was in flight when this row was taken -- the read-pool workers
39182
+ // currently occupied, both db-owner clients' oldest pending ops, and the main loop's recent spans --
39183
+ // plus the CATEGORIZED footprint regions of the main pid, so a 4<->9 GB "Malloc Large" step is
39184
+ // attributed instead of merely recorded. Measurement only: no gates, no behaviour change.
39185
+ inFlightSource: () => {
39186
+ const rp = _ctmReadPool?.getStatus?.();
39187
+ const readPool = [];
39188
+ for (const [i, w] of (rp?.workers || []).entries()) {
39189
+ if (!w?.currentOp) continue;
39190
+ readPool.push({ w: w.id ?? i, op: w.currentOp, ageMs: w.currentOpAgeMs, n: w.occupiedCount });
39191
+ }
39192
+ return {
39193
+ readPool,
39194
+ dbOwner: {
39195
+ base: _ctmDbWorker?.getStatus?.()?.pendingOps ?? [],
39196
+ maintenance: _ctmDbMaintenanceWorker?.getStatus?.()?.pendingOps ?? [],
39197
+ },
39198
+ // getRecentSpans() -> { recent: [{name, dur, start, end}], top: [{name, ms}] }; only the
39199
+ // main-loop spans worth naming (>= 50 ms), newest first, capped at 8.
39200
+ mainSpans: (_perfTracker?.getRecentSpans?.()?.recent || [])
39201
+ .map((span) => ({ name: span.name, ms: Math.round(span.dur) }))
39202
+ .filter((span) => span.ms >= 50)
39203
+ .slice(0, 8),
39204
+ };
39205
+ },
39206
+ regionSampler: _sessionResourceSampler,
38988
39207
  sessionHostManager,
38989
39208
  desktopIndex: _desktopIndex,
38990
39209
  footprintSampler: _sessionResourceSampler,
@@ -39132,7 +39351,7 @@ function _terminalSnapshotProtected(session) {
39132
39351
  const hasViewer = !!session?._primaryWs
39133
39352
  || (Array.isArray(session?.clients) && session.clients.some((client) => client?.readyState === 1));
39134
39353
  const resumable = session?._restoreDeferred === true
39135
- || ['parked', 'frozen'].includes(String(session?._lifecycleState || ''));
39354
+ || String(session?._lifecycleState || '') === 'parked';
39136
39355
  return hasViewer || resumable;
39137
39356
  }
39138
39357
 
@@ -40176,7 +40395,9 @@ function sendDeferredRestoreStatus(ws, session, sessionId, cols, rows, source, o
40176
40395
  id: sessionId,
40177
40396
  cached: served,
40178
40397
  starting,
40179
- 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 || ''),
40180
40401
  message: opts.message || (starting
40181
40402
  ? 'Starting terminal process for the visible session.'
40182
40403
  : 'Session terminal process is deferred until this tab is activated.'),
@@ -40186,9 +40407,10 @@ function sendDeferredRestoreStatus(ws, session, sessionId, cols, rows, source, o
40186
40407
  }
40187
40408
 
40188
40409
  // Extracted to lib/restore-prepare-retry.js (createScheduleDeferredRestoreStart); semantics unchanged.
40189
- 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 });
40190
40412
 
40191
- 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)
40192
40414
 
40193
40415
  function _maybeAutoStartDeferredRestore(session, msg, source, ws) {
40194
40416
  if (!session?._restoreDeferred || !msg?.autoStartRestore) return false;
@@ -40201,6 +40423,9 @@ function _maybeAutoStartDeferredRestore(session, msg, source, ws) {
40201
40423
  reason: msg.prewarm === true ? 'prewarm-disabled' : 'background-client',
40202
40424
  clientKind: _lifecycleClientKind(ws, msg),
40203
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' });
40204
40429
  return false;
40205
40430
  }
40206
40431
  if (!shouldAutoStartDeferredRestore(session, msg, source)) {
@@ -41082,12 +41307,12 @@ function handleResumeRetry(ws, msg) {
41082
41307
  // A quarantine record gets its full facts again; no record means there is nothing blocking.
41083
41308
  const record = id ? getSessionResumeBlocked(id) : null;
41084
41309
  const send = (reply) => { if (ws) try { _wsSend(ws, JSON.stringify(reply)); } catch { /* expected: the asking socket closed; the next attach replays the record */ } };
41085
- 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) });
41086
41311
  if (record) { // Check again re-asks the ladder for any blocked record; a stale record is never replayed as the answer
41087
41312
  Promise.resolve().then(() => { // every outcome replies: a throw anywhere below answers with the unchanged (stale) record
41088
41313
  const agent = dbModule.getSessionIdentity(id)?.primaryAgent || {};
41089
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 || '') } });
41090
- }).then((fresh) => { markSessionResumeBlocked(id, fresh); send(wire(getSessionResumeBlocked(id) || fresh)); })
41315
+ }).then((fresh) => { markSessionResumeBlocked(id, fresh, 'resume-retry-recheck'); send(wire(getSessionResumeBlocked(id) || fresh)); })
41091
41316
  .catch((error) => { console.warn(`[resume-retry] recheck failed for ${id.slice(0, 8)}: ${error && error.message}`); send(wire(record)); });
41092
41317
  } else send(record ? wire(record) : { type: 'resume-retry-noop', id, checkedAt: new Date().toISOString() });
41093
41318
  return false;
@@ -41693,11 +41918,11 @@ function _imageDeliveryAckWindowMs() {
41693
41918
  }
41694
41919
 
41695
41920
  // The classified failure reason a refused/no-op image write leaves on the draft. Kept next to the
41696
- // delivery path so the rollback reason names the real cause (a post that never left the process, or a
41697
- // 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.
41698
41923
  function _imageWriteFailureReason(error) {
41699
41924
  const code = String((error && error.code) || '');
41700
- if (code === 'pty_write_not_posted') return code;
41925
+ if (code === 'pty_write_not_posted' || code === 'pty_dispatch_uncertain') return code;
41701
41926
  if (code.startsWith('pty_write_ack_')) return code.slice(0, 64);
41702
41927
  return 'Provider delivery could not be started';
41703
41928
  }
@@ -41781,14 +42006,14 @@ async function _deliverImageDraftAttempt(sessionId, attachmentId, deliveryAttemp
41781
42006
  // draw from, so this frame's ack number is unique for the session and can never be confused with a
41782
42007
  // keystroke's echo identity (which is tracked by map membership, not by a numeric range).
41783
42008
  const writeResult = _writeLifecyclePty(session, payload, 'image-input');
41784
- // A POST is not a write: HostedPtyAdapter.write only queues the frame, and the byte-write fact
41785
- // arrives later as this inputSeq's host 'input-ack'. A frame that never left this process
41786
- // (posted === false — the device-reply firewall dropped it, the PTY rejected it, or the frozen
41787
- // branch discarded it) can never be acked, and the draft must not conclude that it was delivered:
41788
- // 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".
41789
42012
  if (!writeResult || writeResult.posted !== true) {
41790
- const error = new Error('Image paste frame was not posted to the host PTY');
41791
- 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';
41792
42017
  throw error;
41793
42018
  }
41794
42019
  // §6.3/D6: a PATH-mode draft reaches 'attached' only on the host's actual written ack — a
@@ -44792,7 +45017,7 @@ function handleKill(ws, msg) {
44792
45017
  async function handleKillAsync(ws, msg) {
44793
45018
  if (_ownerRecovery.rejectRequest(ws, msg)) return null; const session = sessions.get(msg.id);
44794
45019
  if (!session) return;
44795
- const ownerDecision = await _sessionOwnerDecisionAsync(msg.id);
45020
+ const ownerDecision = _sessionOwnerDecision(msg.id);
44796
45021
  if (!ownerDecision.ok) {
44797
45022
  if (_ownerDecisionInvalidRequest(ownerDecision)) return;
44798
45023
  _emitOwnerMismatchDiagnostic(ownerDecision);
@@ -44824,18 +45049,10 @@ async function handleKillAsync(ws, msg) {
44824
45049
  broadcastSessionList(true);
44825
45050
  return;
44826
45051
  }
44827
- if (session._lifecycleState === 'frozen') {
44828
- const thaw = _thawLifecycleSession(session, {
44829
- source: 'user-close',
44830
- preserveIdle: true,
44831
- broadcast: false,
44832
- });
44833
- if (!thaw.ok) {
44834
- console.warn(`[lifecycle] close-thaw failed id=${String(msg.id).slice(0, 8)} code=${thaw.code || 'unknown'}`);
44835
- }
44836
- }
44837
45052
  session._intentionalKill = true;
44838
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);
44839
45056
  if (RESUME_LIFECYCLE_V2_ENABLED && session._resumeAttemptState) {
44840
45057
  _resumeLifecycleCoordinator.dispatch(session, {
44841
45058
  type: RESUME_EVENT.CANCEL,
@@ -44855,14 +45072,12 @@ async function handleKillAsync(ws, msg) {
44855
45072
  broadcastSessionList(true);
44856
45073
  return;
44857
45074
  }
44858
- // Retire the startup_task durably on an explicit user close, BEFORE killing the
44859
- // PTY. Don't rely solely on the PTY onExit handler to retire it: onExit
44860
- // early-returns during shutdown (`if (_isShuttingDown) return`), so closing a
44861
- // session and then restarting CTM before the PTY finishes exiting would leave
44862
- // the row at status='running' and the session would be resurrected by restore on
44863
- // the next boot. The onExit retire stays as an idempotent backstop.
44864
- 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');
44865
45079
  try { _killSessionProcess(session, 'user_close'); } catch { /* expected: a PTY already gone on user close must not fail the close handler */ }
45080
+ await retire;
44866
45081
  }
44867
45082
 
44868
45083
  // Delegates to lib/session-workspace-binding.js's TTL-memoized safeRealpath (60s TTL, bounded
@@ -47115,7 +47330,10 @@ function handleList(ws) {
47115
47330
  const listPayload = JSON.stringify({
47116
47331
  type: 'sessions',
47117
47332
  restoring: _restoreInProgress,
47118
- 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 }))),
47119
47337
  });
47120
47338
  _wsSend(ws, listPayload, { class: 'interactive', channel: 'sessions' });
47121
47339
  _kickPendingAutoResumeDeferred('client-list');
@@ -48761,7 +48979,7 @@ function _sessionPayload(s, options = {}) {
48761
48979
  // WP-2: only set on the two missing-transcript codes.
48762
48980
  resumeBlockedLocalHistory: resumeBlocked?.localHistory || '', resumeBlockedLastSeenBytes: Math.max(0, Number(resumeBlocked?.lastSeenBytes || 0)),
48763
48981
  resumeBlockedLastSeenAt: resumeBlocked?.lastSeenAt || '', resumeBlockedTrashPath: resumeBlocked?.trashPath || '',
48764
- resumeBlockedOriginalPath: resumeBlocked?.originalPath || '',
48982
+ resumeBlockedOriginalPath: resumeBlocked?.originalPath || '', resumeBlockedTrashReason: resumeBlocked?.trashReason || '', resumeBlockedTrashedAt: resumeBlocked?.trashedAt || '',
48765
48983
  resumeBlockedRung: resumeBlocked?.resumeRung || '', resumeBlockedCoverage: resumeBlocked?.coverage || null,
48766
48984
  resumeBlockedOfferStoreFirst: resumeBlocked?.offerStoreFirst === true, resumeBlockedRestoreError: resumeBlocked?.restoreError || '',
48767
48985
  resumeBlockedOrigin: resumeBlocked?.origin || 'unknown',
@@ -50290,8 +50508,13 @@ const _broadcastPayload = require('./lib/broadcast-serialize').createBroadcastPa
50290
50508
  // `arr` is the session snapshot the payloads were built from (used for the elision signature so it stays
50291
50509
  // consistent with `built` even if the live sessions Map mutated during a sliced build's yields).
50292
50510
  function _finishBroadcast(arr, built, fresh) {
50293
- return _broadcastPayload.broadcast({ type: 'sessions', restoring: _restoreInProgress, sessions: built }, wss.clients,
50294
- { 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) });
50295
50518
  }
50296
50519
 
50297
50520
  function _maybePrewarmRestoreBroadcastPayloadCaches(sessionList) {
@@ -51287,9 +51510,6 @@ async function _quiesceSessionsForProcessExit(reason = 'shutdown') {
51287
51510
  // REPORT: best-effort diagnostic write -- must never block shutdown teardown.
51288
51511
  errorSink.report(`${reason}:persist-last-footprint`, error, { sessionId: session.id });
51289
51512
  }
51290
- if (session._lifecycleState === 'frozen') {
51291
- _thawLifecycleSession(session, { source: reason, preserveIdle: true, broadcast: false });
51292
- }
51293
51513
  // R-2: do NOT mislabel a hosted session we are deliberately preserving across a
51294
51514
  // restart as intentionally killed -- that flag is read by the next boot's exit-vs-
51295
51515
  // crash classification, and this session's process never actually died here.
@@ -51385,7 +51605,8 @@ async function _quiesceSessionsForProcessExit(reason = 'shutdown') {
51385
51605
  }
51386
51606
 
51387
51607
  // --- Graceful Shutdown ---
51388
- 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) {
51389
51610
  if (_isShuttingDown) return;
51390
51611
  console.log('\n Shutting down...');
51391
51612
  _isShuttingDown = true;
@@ -51467,6 +51688,8 @@ function shutdown() {
51467
51688
  new Promise((resolve) => { const t = setTimeout(resolve, 3000); if (t && typeof t.unref === 'function') t.unref(); }),
51468
51689
  ]);
51469
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 */ }
51470
51693
  await _quiesceSessionsForProcessExit('shutdown');
51471
51694
  try { await headlessWorker.terminate({ intent: 'restart' }); } catch {} // hosts linger through SIGTERM (WP-2)
51472
51695
  _ctmDbClosingForProcessExit = true;
@@ -51480,6 +51703,9 @@ function shutdown() {
51480
51703
  if (_imageWriteProofs) _imageWriteProofs.dispose();
51481
51704
  try { await _desktopIndex.stop(); } catch {}
51482
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 });
51483
51709
  try { await managedBrowserBroker.close(); } catch (e) { console.error('[shutdown] managed browser close failed:', e.message); }
51484
51710
  try { await runtimeGoalStore.close?.(); } catch (e) { console.error('[shutdown] runtime goal store close failed:', e.message); }
51485
51711
  // Kill Wall-E child so it doesn't outlive us (detached + unref'd = orphan
@@ -51664,7 +51890,21 @@ function _startSdWatchdog() {
51664
51890
  function _startHostedSyncWorker() {
51665
51891
  if (_hostedSyncWorker) return;
51666
51892
  try {
51667
- 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
+ });
51668
51908
  _hostedSyncWorker = worker;
51669
51909
  if (worker.started) console.log(`[sync] hosted sync worker started for ${worker.peers.length} peer(s)`);
51670
51910
  } catch (err) {
@@ -51907,10 +52147,20 @@ function _executeHook(hook, data) {
51907
52147
  }
51908
52148
  }
51909
52149
 
51910
- 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 } = {}) {
51911
52159
  let walleStatus;
51912
52160
  try {
51913
- walleStatus = await walleSupervisor.getStatus();
52161
+ walleStatus = snapshot
52162
+ ? await walleSupervisor.getStatusSnapshot()
52163
+ : await walleSupervisor.getStatus();
51914
52164
  } catch (err) {
51915
52165
  walleStatus = { running: false, pid: null, error: err.message };
51916
52166
  }
@@ -51936,6 +52186,9 @@ function _buildInstanceManifest(walleStatus, ctmUptime) {
51936
52186
  golivePlan: _golivePlanCache,
51937
52187
  observedOrigin: _lastMicrosoftTunnelOrigin,
51938
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(),
51939
52192
  });
51940
52193
  }
51941
52194
 
@@ -52208,7 +52461,7 @@ function _storageRiskSnapshot() {
52208
52461
 
52209
52462
  async function _buildHostedRuntimeContextUncached() {
52210
52463
  const ctmUptime = Math.floor((Date.now() - _ctmStartTime) / 1000);
52211
- const walleStatus = await _getWalleStatusSafe();
52464
+ const walleStatus = await _getWalleStatusSafe({ snapshot: true }); // R-4: never the full ownership probe or the recovery await
52212
52465
  const instance = _buildInstanceManifest(walleStatus, ctmUptime);
52213
52466
  const envWorkspaceBindings = buildWorkspaceBindingsStatus({ manifest: instance, env: process.env });
52214
52467
  void _detachOwnerMutation('hosted-workspace-bindings-seed', () =>
@@ -52223,7 +52476,9 @@ async function _buildHostedRuntimeContextUncached() {
52223
52476
  const localEdge = buildLocalEdgeStatus({ manifest: instance, env: process.env });
52224
52477
  const syncSchema = hostedStorage.syncSchema;
52225
52478
  syncSchema.worker = _hostedSyncWorkerStatus();
52226
- 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 }) });
52227
52482
  const security = await _buildRemoteSecuritySummary(instance);
52228
52483
  const terminalUx = buildTerminalRemoteUxStatus({ manifest: instance, env: process.env });
52229
52484
  const deadMan = buildDeadManStatus({ env: process.env });
@@ -52306,7 +52561,7 @@ function _manifestCorsHeaders(req) {
52306
52561
 
52307
52562
  async function apiInstanceManifest(req, res) {
52308
52563
  const ctmUptime = Math.floor((Date.now() - _ctmStartTime) / 1000);
52309
- const walleStatus = await _getWalleStatusSafe();
52564
+ const walleStatus = await _getWalleStatusSafe({ snapshot: true }); // D-7 (WP-5): status GETs read the snapshot, not the process table
52310
52565
  const manifest = _buildInstanceManifest(walleStatus, ctmUptime);
52311
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
52312
52567
  }
@@ -53014,8 +53269,6 @@ function fetchMcpGatewayLedgerStatus(timeoutMs = 300) {
53014
53269
  async function apiServicesStatus(req, res) {
53015
53270
  const {
53016
53271
  ctmUptime,
53017
- walleStatus,
53018
- instance,
53019
53272
  sync,
53020
53273
  workspaceBindings,
53021
53274
  localEdge,
@@ -53024,6 +53277,8 @@ async function apiServicesStatus(req, res) {
53024
53277
  deadMan,
53025
53278
  hostedReadiness,
53026
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
53027
53282
  const mcpGateway = await fetchMcpGatewayStatus();
53028
53283
  const walleRuntimeStatus = walleStatus.running
53029
53284
  ? await getWalleStatusDataOrNull(500)
@@ -53664,6 +53919,22 @@ function _recordWorktreeCreateJobProgress(job, event) {
53664
53919
  };
53665
53920
  job.progress = progress;
53666
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
+ }
53667
53938
  job.events.push({
53668
53939
  at: now,
53669
53940
  stage: progress.stage,
@@ -54307,15 +54578,24 @@ function apiAdoptBranch(req, res) {
54307
54578
  });
54308
54579
  }
54309
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
+
54310
54588
  function apiCreateWorktree(req, res) {
54311
54589
  _worktreeReadJsonBody(req, res, async (payload) => {
54312
54590
  const reqUrl = new URL(req.url, 'http://localhost');
54313
54591
  const { name, base_branch, cwd: reqCwd, namespace, agentType } = payload;
54314
54592
  if (!name || !/^[a-zA-Z0-9_-]+$/.test(name)) {
54593
+ _logWorktreeCreateJobRejected('invalid_name', 'Invalid worktree name. Use alphanumeric, hyphens, underscores only.');
54315
54594
  res.writeHead(400, WORKTREE_JSON_HEADERS);
54316
54595
  return res.end(JSON.stringify({ error: 'Invalid worktree name. Use alphanumeric, hyphens, underscores only.' }));
54317
54596
  }
54318
54597
  if (base_branch && !VALID_GIT_REF.test(base_branch)) {
54598
+ _logWorktreeCreateJobRejected('invalid_base_branch', 'Invalid base branch name');
54319
54599
  res.writeHead(400, WORKTREE_JSON_HEADERS);
54320
54600
  return res.end(JSON.stringify({ error: 'Invalid base branch name' }));
54321
54601
  }
@@ -54327,6 +54607,7 @@ function apiCreateWorktree(req, res) {
54327
54607
  const rawRequestId = payload.request_id || payload.requestId || '';
54328
54608
  const requestId = normalizeWorktreeCreateRequestId(rawRequestId);
54329
54609
  if (rawRequestId && !requestId) {
54610
+ _logWorktreeCreateJobRejected('invalid_request_id', 'Invalid worktree create request id.');
54330
54611
  res.writeHead(400, WORKTREE_JSON_HEADERS);
54331
54612
  return res.end(JSON.stringify({ error: 'Invalid worktree create request id.' }));
54332
54613
  }
@@ -54342,6 +54623,7 @@ function apiCreateWorktree(req, res) {
54342
54623
  job = findReusableWorktreeCreateJob(_worktreeCreateJobs, createParams)
54343
54624
  || _startWorktreeCreateJob(createParams);
54344
54625
  } catch (e) {
54626
+ _logWorktreeCreateJobRejected(e.code || 'WORKTREE_CREATE_FAILED', e.message);
54345
54627
  res.writeHead(e.statusCode || 500, WORKTREE_JSON_HEADERS);
54346
54628
  return res.end(JSON.stringify({ error: e.message, code: e.code || 'WORKTREE_CREATE_FAILED' }));
54347
54629
  }
@@ -54701,7 +54983,10 @@ function apiSyncAllWorktrees(req, res) {
54701
54983
  });
54702
54984
  }
54703
54985
 
54704
- 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 } = {}) {
54705
54990
  return summarizeRestartGuard(sessions.values(), {
54706
54991
  statusForSession: (session) => _standupLiveStatusForSession(session, now),
54707
54992
  isWaitingForInput: (session) => _isServerWaitingForInput(session.id, session, now),
@@ -54710,7 +54995,7 @@ function _restartGuardSnapshot(now = Date.now()) {
54710
54995
  // WP-4 (S-16): reuse the existing hosted-session discriminator (WP-2's
54711
54996
  // `_sessionIsHosted`, defined above at _quiesceSessionsForProcessExit) --
54712
54997
  // do not redefine it here.
54713
- 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
54714
54999
  });
54715
55000
  }
54716
55001
 
@@ -55546,6 +55831,9 @@ async function _adoptStartupTaskSession(task, record, aliveHostIndex, tried) {
55546
55831
  codexHomePath: adoptedStorageRoots.CODEX_HOME || (adoptedHome ? path.join(adoptedHome, '.codex') : null),
55547
55832
  sessionsDir: adoptedSessionsDir || undefined,
55548
55833
  }) : null,
55834
+ // The launch-time config.toml receipt rides the host recipe; a host spawned before it did
55835
+ // carries none, and its lead switch keeps refusing until the session is relaunched.
55836
+ nativeConfigReceipt: adoptedRecipe?.nativeConfigReceipt || { state: 'unknown' },
55549
55837
  },
55550
55838
  // P2-3: restores seed activity clocks from the task, not "now" -- matches
55551
55839
  // _createDeferredRestoreSession (70328-70329) / handleCreate's restore branch;
@@ -56004,6 +56292,7 @@ function _announceDeferredRestoreRetry(session, opts) {
56004
56292
  // Extracted to lib/restore-backoff-gate.js (semantics unchanged; 'prepare-retry' is user-initiated per design D-4).
56005
56293
  const _awaitRestoreBackoffOutsideGate = createAwaitRestoreBackoffOutsideGate({
56006
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
56007
56296
  });
56008
56297
 
56009
56298
  function _startDeferredRestoreSession(session, opts = {}) {
@@ -56077,12 +56366,18 @@ async function _startDeferredRestoreSessionAttempt(session, opts = {}) {
56077
56366
  }
56078
56367
  }
56079
56368
  if (sharedCapacityAdmission) {
56080
- 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
+ }
56081
56375
  if (session._activationAdmissionPromise) { _endInteractiveActivationIfNeeded(); return session._activationAdmissionPromise; }
56082
56376
  }
56083
56377
  if (!capacity.ok) {
56084
56378
  session._lifecycleState = session._parkedAt ? 'parked' : 'deferred';
56085
56379
  _notifyLifecycleWakeBlocked(session, capacity);
56380
+ _noteWakeSkip(session, 'WakeBlocked', {}, id); // WP-3 (§9 S-12)
56086
56381
  console.log(`[lifecycle] wake-blocked id=${id.slice(0, 8)} reason=${capacity.code || 'live-session-cap'}`);
56087
56382
  _endInteractiveActivationIfNeeded();
56088
56383
  return false;
@@ -56158,7 +56453,10 @@ async function _startDeferredRestoreSessionAttempt(session, opts = {}) {
56158
56453
  session._lifecycleState = session._parkedAt ? 'parked' : 'deferred';
56159
56454
  _scheduleStandupMaterializedRefresh(`lifecycle:deferred:${id.slice(0, 8)}`, 0, { markDirty: true });
56160
56455
  _clearLifecyclePrewarm(session);
56161
- _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);
56162
56460
  console.log(`[lifecycle] prewarm-skip id=${id.slice(0, 8)} reason=gate-busy`);
56163
56461
  _endInteractiveActivationIfNeeded();
56164
56462
  return false;
@@ -56256,6 +56554,16 @@ function _setDeferredRestoreStarting(session, value, reason = '') {
56256
56554
  const next = value === true;
56257
56555
  if (session._restoreStarting === next) return false;
56258
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
+ }
56259
56567
  _markHeartbeatDirty(session);
56260
56568
  return true;
56261
56569
  }
@@ -56267,7 +56575,12 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56267
56575
  return false;
56268
56576
  }
56269
56577
  let task = session._restoreTask || {};
56270
- 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
+ }
56271
56584
  // WI-2/WI-6: quarantine short-circuit. A session already marked resume_blocked (deterministic
56272
56585
  // prerequisite failure from a prior attempt — missing transcript/rollout, agent CLI
56273
56586
  // unresolvable) must never be re-driven by ANY of the auto-retry triggers that funnel through
@@ -56279,7 +56592,10 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56279
56592
  // id stays quarantined until the deterministic cause changes or CTM restarts.
56280
56593
  const _blockedInfo = getSessionResumeBlocked(task.session_id);
56281
56594
  if (_blockedInfo) {
56282
- 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);
56283
56599
  return false;
56284
56600
  }
56285
56601
  if (task._codexLinkDeferred) {
@@ -56288,7 +56604,11 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56288
56604
  // operation instead of falling back to cwd/creation-time discovery.
56289
56605
  const codexLink = await _ensureCodexThreadLinkOffThread(task.session_id, 'deferred-restore-codex',
56290
56606
  { selectedIdentity: task._codexRestoreTarget || null });
56291
- 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
+ }
56292
56612
  if (codexLink?.status === 'unavailable') {
56293
56613
  recordSessionDiagnostic(task.session_id, 'codex-link-resolution-deferred', {
56294
56614
  source: String(opts.source || ''),
@@ -56300,6 +56620,8 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56300
56620
  detail: codexLink.reason || 'Codex thread resolution is temporarily unavailable.',
56301
56621
  retryable: true,
56302
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);
56303
56625
  return false;
56304
56626
  }
56305
56627
  if (codexLink?.blocked) {
@@ -56307,13 +56629,15 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56307
56629
  code: 'codex_thread_owned_by_other_ctm',
56308
56630
  message: 'The saved Codex thread is already attached to another CTM session.',
56309
56631
  agentType: 'codex',
56310
- });
56632
+ }, 'codex-thread-owned');
56311
56633
  _recordRuntimeSessionLifecycle(session, 'degraded', {
56312
56634
  adapter: 'codex',
56313
56635
  reason: 'codex_thread_owned_by_other_ctm',
56314
56636
  detail: 'The saved Codex thread is already attached to another CTM session.',
56315
56637
  retryable: false,
56316
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);
56317
56641
  return false;
56318
56642
  }
56319
56643
  if (codexLink?.agentSessionId) {
@@ -56349,7 +56673,10 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56349
56673
  sessionLaunchOverrides: session._launchEnvOverrides, options: opts,
56350
56674
  });
56351
56675
  if (restoreLaunchProjection.sourceRecoveryEnvironmentInvalid) {
56352
- _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);
56353
56680
  recordSessionDiagnostic(task.session_id, 'source-recovery-environment-rejected', {
56354
56681
  reason: 'captured_source_environment_owner_mismatch',
56355
56682
  });
@@ -56399,7 +56726,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56399
56726
  let launchGeneration = suppliedPlan.generation;
56400
56727
  if (launchGeneration <= 0) {
56401
56728
  const reserved = _codingSessionGenerationRuntime.revalidateReservation(await _codingSessionGenerationRuntime.reserveSessionGeneration(task.session_id, Math.max(inputGeneration - 1, Number.isSafeInteger(session._launchPlanGeneration) ? session._launchPlanGeneration : 0)));
56402
- 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; }
56403
56730
  launchGeneration = reserved.generation;
56404
56731
  }
56405
56732
  session._launchPlanGeneration = launchGeneration;
@@ -56477,7 +56804,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56477
56804
  : _hydrateCanonicalAgentEventCaches([task.session_id]),
56478
56805
  ]);
56479
56806
  } catch (error) {
56480
- _setDeferredRestoreStarting(session, false, 'prepare-failed');
56807
+ _setDeferredRestoreStarting(session, false, 'PrepareFailed');
56481
56808
  // round-3 WP-3 T5 (S-24, I-8b): a read-pool ADMISSION failure (the lane was full/paused, or
56482
56809
  // the pool itself is disabled) is a fact about the POOL at this instant, never about this
56483
56810
  // session's data or its agent -- the prior code folded it into the same generic
@@ -56543,23 +56870,23 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56543
56870
  return false;
56544
56871
  }
56545
56872
  if (launchPlanResult?.status === 'stale') {
56546
- _setDeferredRestoreStarting(session, false, 'launch-plan-stale');
56873
+ _setDeferredRestoreStarting(session, false, 'LaunchPlanStale');
56547
56874
  return false;
56548
56875
  }
56549
56876
  if (sessions.get(task.session_id) !== session || !session._restoreDeferred) {
56550
- _setDeferredRestoreStarting(session, false, 'session-replaced-or-not-deferred');
56877
+ _setDeferredRestoreStarting(session, false, 'SessionReplaced');
56551
56878
  return false;
56552
56879
  }
56553
56880
  const rungHook = await _resolveMissingTranscriptRung(task.session_id, launchPlanResult.plan, detectAgentType(task.cmd));
56554
56881
  if (rungHook.restored) {
56555
56882
  // The archived copy is back on disk: prepare the launch again and continue natively.
56556
56883
  try { launchPlanResult = await runDeferredLaunchPlan(launchPlanResult.plan); } catch (error) {
56557
- _setDeferredRestoreStarting(session, false, 'prepare-failed');
56884
+ _setDeferredRestoreStarting(session, false, 'PrepareFailed');
56558
56885
  errorSink.report('deferred-restore:replan-after-restore', error);
56559
56886
  return false;
56560
56887
  }
56561
56888
  if (launchPlanResult?.status === 'stale') {
56562
- _setDeferredRestoreStarting(session, false, 'launch-plan-stale');
56889
+ _setDeferredRestoreStarting(session, false, 'LaunchPlanStale');
56563
56890
  return false;
56564
56891
  }
56565
56892
  }
@@ -56573,7 +56900,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56573
56900
  rungHook.restored ? launchPlanResult.plan.resume?.preflight : (rungHook.preflight || launchPlanResult.plan.resume?.preflight),
56574
56901
  );
56575
56902
  const _planBlocked = getSessionResumeBlocked(task.session_id);
56576
- 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);
56577
56904
  recordSessionDiagnostic(task.session_id, 'deferred-restore-blocked', {
56578
56905
  code,
56579
56906
  source: _restoreSource,
@@ -56584,7 +56911,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56584
56911
  detail: launchPlanResult.plan.blockReason.message || 'Session launch is blocked.',
56585
56912
  retryable: code === 'launch_rollout_index_pending' && launchPlanResult.plan.blockReason.deterministic !== true,
56586
56913
  });
56587
- _setDeferredRestoreStarting(session, false, 'launch-blocked');
56914
+ _setDeferredRestoreStarting(session, false, 'LaunchBlocked');
56588
56915
  if (_shouldRefreshLaunchReadinessForBlock(code, launchPlanResult.plan.blockReason.deterministic === true)) {
56589
56916
  _refreshLaunchReadiness().catch((e) => errorSink.report('launch-readiness:refresh', e));
56590
56917
  }
@@ -56619,7 +56946,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56619
56946
  transition_id: String(pendingTransition.transition_id || '').slice(0, 16),
56620
56947
  reason: targetAuthorization.reason || 'host_target_receipt_mismatch',
56621
56948
  });
56622
- _setDeferredRestoreStarting(session, false, 'lead-transition-target-owner-mismatch');
56949
+ _setDeferredRestoreStarting(session, false, 'LeadTransitionTargetOwnerMismatch');
56623
56950
  return false;
56624
56951
  }
56625
56952
  }
@@ -56638,7 +56965,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56638
56965
  transition_id: String(pendingTransition.transition_id || '').slice(0, 16),
56639
56966
  reason: 'startup_task_not_exact_source',
56640
56967
  });
56641
- _setDeferredRestoreStarting(session, false, 'lead-transition-source-owner-mismatch');
56968
+ _setDeferredRestoreStarting(session, false, 'LeadTransitionSourceOwnerMismatch');
56642
56969
  return false;
56643
56970
  }
56644
56971
  const reconciled = await leadModelSwitch.reconcileUnownedTransition({ sessionId: task.session_id,
@@ -56648,7 +56975,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56648
56975
  transition_id: String(pendingTransition.transition_id || '').slice(0, 16),
56649
56976
  reason: reconciled.reason || 'transition_reconciliation_failed',
56650
56977
  });
56651
- _setDeferredRestoreStarting(session, false, 'lead-transition-reconciliation-failed');
56978
+ _setDeferredRestoreStarting(session, false, 'LeadTransitionReconciliationFailed');
56652
56979
  return false;
56653
56980
  }
56654
56981
  }
@@ -56657,7 +56984,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56657
56984
  recordSessionDiagnostic(task.session_id, 'lead-transition-source-restore-refused', {
56658
56985
  reason: 'transition_reconciliation_unavailable', message: String(error?.message || error).slice(0, 160),
56659
56986
  });
56660
- _setDeferredRestoreStarting(session, false, 'lead-transition-reconciliation-unavailable');
56987
+ _setDeferredRestoreStarting(session, false, 'LeadTransitionReconciliationUnavailable');
56661
56988
  return false;
56662
56989
  }
56663
56990
  if (opts.expectedProviderThreadId) {
@@ -56670,7 +56997,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56670
56997
  expectedThreadPrefix: String(opts.expectedProviderThreadId).slice(0, 8),
56671
56998
  actualThreadPrefix: String(resolvedThreadId).slice(0, 8),
56672
56999
  });
56673
- _setDeferredRestoreStarting(session, false, 'lead-source-recovery-resume-mismatch');
57000
+ _setDeferredRestoreStarting(session, false, 'LeadSourceRecoveryResumeMismatch');
56674
57001
  return false;
56675
57002
  }
56676
57003
  }
@@ -56690,7 +57017,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56690
57017
  throw new Error(`backup restore not possible: ${decision?.reason || 'unavailable'}`);
56691
57018
  }
56692
57019
  } catch (error) {
56693
- _setDeferredRestoreStarting(session, false, 'transcript-restore-failed');
57020
+ _setDeferredRestoreStarting(session, false, 'TranscriptRestoreFailed');
56694
57021
  _launchPlanCoordinator.markFailed(task.session_id, launchGeneration, 'launch_transcript_restore_failed', launchPlanResult.plan);
56695
57022
  recordSessionDiagnostic(task.session_id, 'deferred-restore-transcript-copy-failed', { error: error.message });
56696
57023
  _recordRuntimeSessionLifecycle(session, 'degraded', {
@@ -56714,7 +57041,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56714
57041
  }),
56715
57042
  });
56716
57043
  if (sessions.get(task.session_id) !== session || !session._restoreDeferred) {
56717
- _setDeferredRestoreStarting(session, false, 'mcp-wait-stale');
57044
+ _setDeferredRestoreStarting(session, false, 'McpWaitStale');
56718
57045
  return false;
56719
57046
  }
56720
57047
  // WI-8: gate a large-transcript resume BEFORE committing to spawn. `source` here is
@@ -56757,7 +57084,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56757
57084
  // immediately — nothing downstream will ever reach _clearResumeLoading for it.
56758
57085
  if (_resumeStormAdmission.gated) _resumeStormBudget.release(_resumeStormAdmission.ticket);
56759
57086
  session._resumeStormBudgetTicket = null;
56760
- _setDeferredRestoreStarting(session, false, 'resume-storm-stale');
57087
+ _setDeferredRestoreStarting(session, false, 'ResumeStormStale');
56761
57088
  return false;
56762
57089
  }
56763
57090
  session._resumeStormBudgetHeld = _resumeStormAdmission.gated;
@@ -56774,7 +57101,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56774
57101
  const geometryLeaseBeforeRestore = _geometryLease(session, task.session_id);
56775
57102
  if (!_launchPlanCoordinator.markSpawning(task.session_id, launchGeneration, launchPlanResult.plan)) {
56776
57103
  _releaseResumeStormBudgetIfHeld(session);
56777
- _setDeferredRestoreStarting(session, false, 'launch-plan-not-spawning');
57104
+ _setDeferredRestoreStarting(session, false, 'LaunchPlanNotSpawning');
56778
57105
  return false;
56779
57106
  }
56780
57107
  // D-8 (WP-3): capture the placeholder's cached-snapshot fields BEFORE _destroySession
@@ -56797,7 +57124,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56797
57124
  });
56798
57125
  if (!issued) {
56799
57126
  _releaseResumeStormBudgetIfHeld(session);
56800
- _setDeferredRestoreStarting(session, false, 'lead-transition-attempt-issue-refused');
57127
+ _setDeferredRestoreStarting(session, false, 'LeadTransitionAttemptIssueRefused');
56801
57128
  recordSessionDiagnostic(task.session_id, 'lead-transition-attempt-issue-refused', {
56802
57129
  generation: launchGeneration,
56803
57130
  });
@@ -56971,7 +57298,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
56971
57298
  // finds this session not-live) instead of waiting out its deadline for a wake-ready that
56972
57299
  // will never come, and push visibility unconditionally.
56973
57300
  _announceRestoredSessionLive(session, 'create-rollback');
56974
- _setDeferredRestoreStarting(session, false, 'create-rollback');
57301
+ _setDeferredRestoreStarting(session, false, 'CreateRollback');
56975
57302
  recordSessionDiagnostic(task.session_id, 'deferred-restore-create-rollback', {
56976
57303
  source: _restoreSource,
56977
57304
  code: createError
@@ -57001,7 +57328,7 @@ async function _startDeferredRestoreSessionNow(session, opts = {}) {
57001
57328
  // a missing file will not appear on the next tick, so there is nothing to wait out (and
57002
57329
  // every future call into this function short-circuits at the top before reaching here).
57003
57330
  if (_newlyBlocked) {
57004
- 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);
57005
57332
  } else {
57006
57333
  for (const client of existingClients) {
57007
57334
  if (client && client.readyState === 1) {
@@ -57183,6 +57510,8 @@ function _autoResumeDeferredSessions(source = 'auto-resume') {
57183
57510
  idleHours: diet.idleHours,
57184
57511
  bytes: diet.bytes,
57185
57512
  });
57513
+ // WP-3 (§9 S-19): beside the existing diag; the latch keeps a long diet to one skip.
57514
+ _noteWakeSkip(s, 'DrainPressureSkipped');
57186
57515
  }
57187
57516
  continue;
57188
57517
  }
@@ -57230,9 +57559,11 @@ function _autoResumeDeferredSessions(source = 'auto-resume') {
57230
57559
  const resumeIds = warmDrain.selected;
57231
57560
  // S-18/skipReason: a session skipped this pass stays a lazy placeholder as before, but
57232
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.
57233
57563
  for (const skip of warmDrain.skipped) {
57234
57564
  const s = sessions.get(skip.id);
57235
57565
  if (s) s._resumeDrainSkipReason = skip.reason;
57566
+ _noteWakeSkip(s, 'DrainPressureSkipped');
57236
57567
  }
57237
57568
  for (const id of resumeIds) {
57238
57569
  const s = sessions.get(id);
@@ -57329,6 +57660,7 @@ function _autoResumeDeferredSessions(source = 'auto-resume') {
57329
57660
  coalesceBroadcast: true, // batch emits a single list refresh per tick
57330
57661
  }),
57331
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
57332
57664
  broadcast: () => broadcastSessionList(true),
57333
57665
  scheduleGap: _scheduleResumeGap,
57334
57666
  // WI-7(d): yield the whole drain to an in-flight interactive activation.
@@ -57412,13 +57744,18 @@ const _reviveUnresponsiveHost = createReviveUnresponsiveHost({
57412
57744
  recordSessionDiagnostic,
57413
57745
  log: (msg) => console.warn(msg),
57414
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 })); }
57415
57752
 
57416
57753
  // Reconcile previous-boot hosts before classifying startup tasks. Incompatible
57417
57754
  // hosts are quiesced, then restored through the ordinary provider-resume path.
57418
57755
  const RECONCILE_PROBE_CONCURRENCY = 4;
57419
57756
  async function _reconcileSurvivingHostsForRestore(startupTasks) {
57420
57757
  const aliveHostIndex = new Map();
57421
- 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
57422
57759
  const dataDir = require('./lib/data-dir').getDataDir();
57423
57760
  // WP-4 (D-9 crash cap): >= 3 unexpected host deaths inside the trailing 10-
57424
57761
  // minute window means the topology itself is unhealthy right now -- skip
@@ -57436,7 +57773,7 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
57436
57773
  await _restartOutcomeTruth.killHostsForCrashCap({ records: await _listHostRecords(dataDir), rollup: _hostAdoptionRollup, isAlive: _isProcessAlive, shutdownHost: _shutdownOrphanHost,
57437
57774
  removeRecord: (r) => _removeHostRecordFile(dataDir, r.hostId), skip: (r) => r.serverBootId === _serverBootId || sessionHostManager.ownsHostId(r.hostId), errorSink });
57438
57775
  } catch (e) { errorSink.report('boot-reconcile:crash-cap-kill', e); }
57439
- _restartOutcomeTruth.recordCrashCapRows({ recordSessionDiagnostic, startupTasks, deaths }); return { aliveHostIndex, hostRecordsById };
57776
+ _restartOutcomeTruth.recordCrashCapRows({ recordSessionDiagnostic, startupTasks, deaths }); return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
57440
57777
  }
57441
57778
  } catch (e) {
57442
57779
  errorSink.report('boot-reconcile:read-host-crashes', e);
@@ -57446,9 +57783,9 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
57446
57783
  records = await _listHostRecords(dataDir);
57447
57784
  } catch (e) {
57448
57785
  errorSink.report('boot-reconcile:list-host-records', e);
57449
- return { aliveHostIndex, hostRecordsById };
57786
+ return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
57450
57787
  }
57451
- if (!records.length) return { aliveHostIndex, hostRecordsById };
57788
+ if (!records.length) return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
57452
57789
  const startupSessionIds = new Set(startupTasks.map((t) => t.session_id));
57453
57790
  const aliveHosts = []; // { record, matching } per probed-alive host; indexed AFTER all probes, hostId order
57454
57791
  let cursor = 0;
@@ -57510,7 +57847,7 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
57510
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`);
57511
57848
  for (const sessionId of matching) recordSessionDiagnostic(sessionId, 'host-upgrade-pending', { hostId: record.hostId, upgrade: probed.upgrade });
57512
57849
  }
57513
- 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
57514
57851
  aliveHosts.push({ record, matching });
57515
57852
  }
57516
57853
  }
@@ -57527,7 +57864,7 @@ async function _reconcileSurvivingHostsForRestore(startupTasks) {
57527
57864
  if (aliveHostIndex.size) {
57528
57865
  console.log(` [boot-reconcile] ${aliveHostIndex.size} session(s) have a surviving host — will adopt instead of respawning`);
57529
57866
  }
57530
- return { aliveHostIndex, hostRecordsById };
57867
+ return { aliveHostIndex, hostRecordsById, threadpoolByHostId };
57531
57868
  }
57532
57869
 
57533
57870
  // D-2: one-line stamp for restore/adopt/placeholder sites (idempotent; no row = untouched).
@@ -57581,6 +57918,54 @@ function _reloadWorkerSessionMarkersAtBoot() {
57581
57918
  if (rearmed > 0) console.log(` [restore] re-armed ${rearmed} worker retirement timer(s) from worker_sessions.hide_at.`);
57582
57919
  }
57583
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
+
57584
57969
  async function _restoreSessionsAsync() {
57585
57970
  await _ownerRecovery.beforeRestore();
57586
57971
  _hostAdoptionRollup.markRestoreStarted(); _restoreAdmission.markTierAActive('restore-start');
@@ -57599,11 +57984,11 @@ async function _restoreSessionsAsync() {
57599
57984
  );
57600
57985
  _listStartupTasksMs = Date.now() - _listStartupTasksStartedAt;
57601
57986
  _restoreWindowVerdict.fillRestorePendingIds(_startupTasks.map((t) => t.session_id));
57602
- let _restoreAliveHostIndex = new Map(), _restoreHostRecordsById = new Map();
57987
+ let _restoreAliveHostIndex = new Map(), _restoreHostRecordsById = new Map(), _restoreThreadpoolByHostId = new Map(); // WP-3 (§9 S-8)
57603
57988
  const _earlyAdopted = new Set(); const _earlyAdoptStartedAt = Date.now();
57604
57989
  if (_startupTasks.length) {
57605
57990
  try {
57606
- ({ aliveHostIndex: _restoreAliveHostIndex, hostRecordsById: _restoreHostRecordsById } = await _reconcileSurvivingHostsForRestore(_startupTasks));
57991
+ ({ aliveHostIndex: _restoreAliveHostIndex, hostRecordsById: _restoreHostRecordsById, threadpoolByHostId: _restoreThreadpoolByHostId } = await _reconcileSurvivingHostsForRestore(_startupTasks));
57607
57992
  } catch (e) { errorSink.report('boot-reconcile:reconcile', e); }
57608
57993
  await Promise.all(_startupTasks.filter((t) => _restoreAliveHostIndex.has(t.session_id)).map((t) => _adoptStartupTaskSession(t, _restoreAliveHostIndex.get(t.session_id).record, _restoreAliveHostIndex)
57609
57994
  .then((adopted) => { if (adopted) _earlyAdopted.add(t.session_id); }, (e) => errorSink.report('boot-adopt:restore-early', e, { sessionId: t.session_id }))));
@@ -57759,6 +58144,11 @@ async function _restoreSessionsAsync() {
57759
58144
  try { await dbModule.clearScrollbackIdentities(); } catch {}
57760
58145
  _resumeShadowAfterBoot('no-sessions'); // nothing to restore — resume mirroring now
57761
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 }));
57762
58152
  broadcastToAll({ type: 'server-ready' });
57763
58153
  return;
57764
58154
  }
@@ -57810,6 +58200,16 @@ async function _restoreSessionsAsync() {
57810
58200
  // auto-resume below runs shadowed; only the restore loop itself was the
57811
58201
  // suppressed window.)
57812
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();
57813
58213
  // WP-3 (design doc §5 D-4, §8 WP-3, §9 S-18): force one real sampler snapshot
57814
58214
  // before draining so the warm-drain budget's pressure/free/inactive reflect the
57815
58215
  // ACTUAL host state at the exact moment deferred sessions would auto-resume,
@@ -57819,7 +58219,7 @@ async function _restoreSessionsAsync() {
57819
58219
  try { await _sampleLifecycleResources({ force: true }); }
57820
58220
  catch (error) { errorSink.report('restore:warm-drain-pre-sample', error); }
57821
58221
  // Bring deferred placeholders back to life by themselves (staggered).
57822
- _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
57823
58223
  // Clean orphan scrollback identities — entries for sessions no longer in
57824
58224
  // startup_tasks (e.g. a hard crash where pty.onExit cleanup didn't run).
57825
58225
  // (scrollback_log was dropped in schema v10; snapshot files are reaped by the
@@ -58170,7 +58570,7 @@ async function _restoreSessionsAsync() {
58170
58570
  // broadcastToAll, isCtmInternalSession, callClaudeAsync, _detachOwnerMutation,
58171
58571
  // _inRestoreStorm, _offthreadRead) is already initialized. See server/boot.js's own
58172
58572
  // header comment for the TDZ-safety argument.
58173
- const { createBootJobs, readPackageVersion } = require('./server/boot');
58573
+ const { createBootJobs, readPackageVersion, isHumanPromptCandidate, cleanFirstMessage } = require('./server/boot');
58174
58574
  const _bootJobs = createBootJobs({
58175
58575
  installRoot: path.join(__dirname, '..'),
58176
58576
  telemetry,
@@ -60318,7 +60718,6 @@ async function apiDiagnosticsLifecycle(req, res, url) {
60318
60718
  activationGate: _lifecycleActivationGate.snapshot(),
60319
60719
  counts: {
60320
60720
  live: rows.filter(row => row.state === 'live').length,
60321
- frozen: rows.filter(row => row.state === 'frozen').length,
60322
60721
  parked: rows.filter(row => row.state === 'parked').length,
60323
60722
  deferred: rows.filter(row => row.state === 'deferred').length,
60324
60723
  waking: rows.filter(row => row.state === 'waking').length,
@@ -61649,6 +62048,129 @@ servicesApi.configure({
61649
62048
  });
61650
62049
  servicesApi.register();
61651
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
+
61652
62174
  // WP-8 slice 3: setup/diagnostics/misc-ops route clusters.
61653
62175
  const setupApi = require('./lib/api/setup');
61654
62176
  setupApi.configure({
@@ -61743,6 +62265,7 @@ miscopsApi.configure({
61743
62265
  apiListRules,
61744
62266
  apiDeleteHook,
61745
62267
  apiRestartCtm, restartGuardSnapshot: _restartGuardSnapshot,
62268
+ proxyPreflight: () => oauthProxySupervisor.proxyPreflight(), // WP-3 D-10: the live model-proxy verdict
61746
62269
  wss,
61747
62270
  clientBuildDiagnosticDetails,
61748
62271
  attachedSessionIds,
@@ -61811,6 +62334,7 @@ ingressApi.configure({
61811
62334
  observeWorkspaceActivity: _observeWorkspaceActivity,
61812
62335
  dbModule,
61813
62336
  delegateWorker,
62337
+ recordSessionDiagnostic,
61814
62338
  });
61815
62339
  ingressApi.register();
61816
62340
  const { createMicrosoftOwnerServer } = require('./lib/microsoft-owner-server');
@@ -62133,10 +62657,10 @@ const rootPromptFlow = createRootPromptFlow({
62133
62657
  }, Date.now(),
62134
62658
  ),
62135
62659
  sendWsJson: (ws, frame) => _sendWsJsonBounded(ws, frame),
62136
- reportDiagnostic: ({ phase, code, returnedRecordConfirmed, canonicalRecordConfirmed,
62660
+ reportDiagnostic: ({ phase, code, sessionId, clientMessageId, returnedRecordConfirmed, canonicalRecordConfirmed,
62137
62661
  recordFieldsMatch, errorKind, sourceFile, sourceLine, nativeHandleOpen, nativeHandleClosed, failureClass }) =>
62138
62662
  recordSessionDiagnostic('__host__', 'root-prompt-flow-suppressed-failure', {
62139
- phase, code, returnedRecordConfirmed, canonicalRecordConfirmed, recordFieldsMatch,
62663
+ phase, code, sessionId, clientMessageId, returnedRecordConfirmed, canonicalRecordConfirmed, recordFieldsMatch,
62140
62664
  errorKind, sourceFile, sourceLine, nativeHandleOpen, nativeHandleClosed, failureClass,
62141
62665
  }),
62142
62666
  markDirty: (sessionId, reason) => {
@@ -62702,7 +63226,7 @@ function _runBootSequence() {
62702
63226
  // configDir/instanceTag/port are passed explicitly so a dev/test instance
62703
63227
  // (its own CONFIG_DIR, per lib/server-config.js) never lands its journal
62704
63228
  // under the real ~/.walle — see lib/ctm-boot-lifecycle.js.
62705
- appendCtmBootLine({ configDir: CONFIG_DIR, instanceTag: INSTANCE_TAG, port: PORT });
63229
+ appendCtmBootLine({ configDir: CONFIG_DIR, instanceTag: INSTANCE_TAG, port: PORT, enteredAtServer: require.main === module });
62706
63230
 
62707
63231
  // Stale CTM cleanup: with launchd KeepAlive, only one instance runs.
62708
63232
  // No pgrep needed — launchd manages the lifecycle.
@@ -62727,8 +63251,13 @@ function _runBootSequence() {
62727
63251
  // Wall-E daemon and surfaced through its HTTP API after startup.
62728
63252
  const shouldStartOauthProxy = (() => {
62729
63253
  if (isWalleSupervisorDisabled()) return false;
62730
- // Sync boot path: Wall-E's stored provider is not readable here; the auth method alone gates the proxy.
62731
- 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();
62732
63261
  })();
62733
63262
  if (shouldStartOauthProxy) {
62734
63263
  try {
@@ -63368,6 +63897,16 @@ function _runBootSequence() {
63368
63897
  // coalescing keeps at most one queued behind the in-flight one).
63369
63898
  _transcriptDeltaCoalescer.request(filePath, payload);
63370
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
+ }
63371
63910
  } catch (e) {
63372
63911
  fallback('pipeline-error');
63373
63912
  console.error(`[event-delta] ${String(sessionId).slice(0, 8)} pipeline error:`, e.message);