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
@@ -0,0 +1,167 @@
1
+ 'use strict';
2
+
3
+ // The session-host control-socket dial, extracted from lib/session-host-manager.js
4
+ // (session-stops-mid-job-2026-10-24.html WP-3 freed the manager's file-size budget this way).
5
+ // Behaviour is unchanged; tests/host-dial-stall-clock.test.js pins it from this file.
6
+
7
+ const fs = require('node:fs');
8
+ const net = require('node:net');
9
+ const { CONTROL_CONNECTION_ROLE, encodeFrame, createFrameDecoder, PROTOCOL_VERSION, FLOOR_VERSION } = require('./host-control-protocol');
10
+ const { hostRuntimeCompatibilityDecision } = require('./host-runtime-compatibility');
11
+ const { hashClosureFiles } = require('./host-closure-hash');
12
+ const { resolveDialRetryDeadline, DIAL_MAX_STALL_EXTENSION_RATIO } = require('./host-dial-stall-clock');
13
+
14
+ // Dial a host control socket within a stall-aware deadline. The first state frame
15
+ // is judged by hostRuntimeCompatibilityDecision (a differing closure hash is adoptable); later frames
16
+ // route to onFrame, while onState exposes adoptable session metadata.
17
+ function dialControlSocketWithRetry(socketPath, {
18
+ serverBootId, onFrame, onState, timeoutMs = 5000, retryDelayMs = 200,
19
+ protocolVersion = PROTOCOL_VERSION,
20
+ } = {}) {
21
+ return new Promise((resolve, reject) => {
22
+ const budgetMs = Math.max(0, Number(timeoutMs) || 5000);
23
+ // Stall-aware, not fixed wall clock: see lib/host-dial-stall-clock.js for the incident where
24
+ // this deadline drained during the server's OWN boot freeze and a healthy pooled host was
25
+ // SIGTERM'd (taking its live sessions with it). `deadline` is reassigned as stall is credited
26
+ // back; `_stallExtendedMs` is the running total and is capped, so a permanently stalled
27
+ // server still times out rather than waiting forever.
28
+ let deadline = Date.now() + budgetMs;
29
+ let _stallExtendedMs = 0;
30
+ const maxStallExtensionMs = budgetMs * DIAL_MAX_STALL_EXTENSION_RATIO;
31
+ let settled = false;
32
+ let retryTimer = null, deadlineTimer = null, activeConn = null;
33
+ function finishOk(conn) {
34
+ if (settled) return;
35
+ settled = true;
36
+ clearTimeout(deadlineTimer);
37
+ resolve(conn);
38
+ }
39
+ function finishFail(err) {
40
+ if (settled) return;
41
+ settled = true;
42
+ clearTimeout(retryTimer); clearTimeout(deadlineTimer);
43
+ try { activeConn?.destroy(); } catch { /* expected: connection may already be gone */ }
44
+ reject(err || new Error('control socket dial failed'));
45
+ }
46
+ function armDeadlineWatchdog(scheduledAt = Date.now()) {
47
+ const delayMs = Math.max(1, deadline - scheduledAt);
48
+ deadlineTimer = setTimeout(() => {
49
+ const firedAt = Date.now();
50
+ const credited = resolveDialRetryDeadline({ deadline, scheduledAt, firedAt, retryDelayMs: delayMs,
51
+ extendedMs: _stallExtendedMs, maxExtensionMs: maxStallExtensionMs });
52
+ deadline = credited.deadline; _stallExtendedMs = credited.extendedMs;
53
+ if (firedAt < deadline) armDeadlineWatchdog(firedAt); else finishFail(new Error(`control socket handshake timed out after ${timeoutMs}ms`));
54
+ }, delayMs);
55
+ deadlineTimer.unref?.();
56
+ }
57
+ function scheduleRetry() {
58
+ if (settled) return;
59
+ if (Date.now() >= deadline) {
60
+ finishFail(new Error(
61
+ `control socket dial timed out after ${timeoutMs}ms`
62
+ + (_stallExtendedMs > 0 ? ` (+${_stallExtendedMs}ms credited for main-loop stall)` : ''),
63
+ ));
64
+ return;
65
+ }
66
+ const scheduledAt = Date.now();
67
+ retryTimer = setTimeout(() => {
68
+ // A retry that fires far later than it was armed for means OUR event loop was starved --
69
+ // that time is not evidence about the child, so give it back before re-testing the
70
+ // deadline above. Bounded by maxStallExtensionMs.
71
+ const credited = resolveDialRetryDeadline({
72
+ deadline,
73
+ scheduledAt,
74
+ firedAt: Date.now(),
75
+ retryDelayMs,
76
+ extendedMs: _stallExtendedMs,
77
+ maxExtensionMs: maxStallExtensionMs,
78
+ });
79
+ deadline = credited.deadline;
80
+ _stallExtendedMs = credited.extendedMs;
81
+ attempt();
82
+ }, retryDelayMs);
83
+ retryTimer.unref?.();
84
+ }
85
+ function attempt() {
86
+ if (settled) return;
87
+ // Do NOT dial before the host has created its socket. A connect to a missing unix socket
88
+ // makes node build an ExceptionWithHostPort and capture its stack, and this loop runs every
89
+ // retryDelayMs across every concurrent dial (pool + per-session + registry probes). Measured
90
+ // 2026-08-23 with the server's own auto-CPU-profiler, during a boot where a pooled host was
91
+ // being started:
92
+ // top self-time frames: ExceptionWithHostPort (errors:765) 3290ms
93
+ // | defaultPrepareStackTrace (errors:95) 3256ms
94
+ // | (garbage collector) 752ms | checkError (destroy:33) 512ms
95
+ // | scheduleRetry (session-host-manager.js:89) 192ms
96
+ // i.e. ~6.5s of main-loop CPU spent building the retry loop's OWN connection errors. That
97
+ // block (`[perf-lag] Event loop blocked 6102ms`) is what then starved the very handshake it
98
+ // was polling for, until the dial "timed out" and the manager SIGTERM'd a healthy pooled
99
+ // host, killing its live sessions. A self-inflicted starvation loop.
100
+ //
101
+ // fs.access (async -- never sync I/O on the main loop) is a cheap stat that produces no
102
+ // exception object. This is a fast-path skip only: once the socket exists the dial below is
103
+ // byte-identical to before, and a socket that appears between this check and the connect
104
+ // still takes the ordinary error path.
105
+ fs.access(socketPath, fs.constants.F_OK, (missing) => {
106
+ if (settled) return;
107
+ if (missing) { scheduleRetry(); return; }
108
+ _connect();
109
+ });
110
+ }
111
+ function _connect() {
112
+ if (settled) return;
113
+ let conn;
114
+ let helloAcked = false;
115
+ try {
116
+ conn = net.createConnection(socketPath);
117
+ activeConn = conn;
118
+ } catch {
119
+ scheduleRetry();
120
+ return;
121
+ }
122
+ const decoder = createFrameDecoder({
123
+ onFrame: (msg) => {
124
+ if (!helloAcked) {
125
+ if (msg && msg.type === 'state') {
126
+ const diskClosureHash = hashClosureFiles(Array.isArray(msg.closureFiles) ? msg.closureFiles : []), hostClosureHash = String(msg.closureHash || '');
127
+ const compatibility = { ...hostRuntimeCompatibilityDecision({
128
+ expectedProtocol: protocolVersion, actualProtocol: msg.protocolVersion,
129
+ expectedFloor: FLOOR_VERSION, actualFloor: msg.floorVersion, diskClosureHash, hostClosureHash,
130
+ }), hostClosureHash, diskClosureHash };
131
+ if (!compatibility.compatible) {
132
+ helloAcked = true;
133
+ try { conn.destroy(); } catch { /* expected: already gone */ }
134
+ finishFail(Object.assign(
135
+ new Error(`control socket host compatibility mismatch (${compatibility.reason})`),
136
+ { code: 'HOST_INCOMPATIBLE', compatibilityReason: compatibility.reason },
137
+ ));
138
+ return;
139
+ }
140
+ helloAcked = true;
141
+ try { onState?.(msg, compatibility); } catch { /* expected: caller's onState must not wedge the dial */ }
142
+ finishOk(conn);
143
+ }
144
+ return;
145
+ }
146
+ if (typeof onFrame === 'function') onFrame(msg);
147
+ },
148
+ onError: () => { try { conn.destroy(); } catch { /* expected: already gone */ } },
149
+ });
150
+ conn.on('data', (chunk) => decoder.push(chunk));
151
+ conn.on('connect', () => {
152
+ try {
153
+ conn.write(encodeFrame({ type: 'hello', serverBootId, connectionRole: CONTROL_CONNECTION_ROLE.CONTROLLER, lastSeq: 0, protocolVersion, floorVersion: FLOOR_VERSION }));
154
+ } catch {
155
+ try { conn.destroy(); } catch { /* expected: already gone */ }
156
+ }
157
+ });
158
+ conn.on('error', () => { /* expected: close follows error and owns the sole retry */ });
159
+ conn.on('close', () => { if (!helloAcked) scheduleRetry(); });
160
+ }
161
+
162
+ attempt();
163
+ armDeadlineWatchdog();
164
+ });
165
+ }
166
+
167
+ module.exports = { dialControlSocketWithRetry };
@@ -173,10 +173,17 @@ async function probeSuccessor(ch, spawned, hostId, deadlineAt) {
173
173
  }
174
174
 
175
175
  // One handoff attempt. Resolves { ok:true, ... } or { ok:false, reason, attempted }; never rejects.
176
+ //
177
+ // WP-3 (D-3/S-3): opts.reason is 'upgrade' (default) or 'evacuate-threadpool'.
178
+ // An evacuation is not an upgrade -- the host is being replaced because its
179
+ // libuv pool is dead, so it does not need an 'upgrade-pending' compat entry to
180
+ // be eligible (any entry, or none, passes). The recovery-protected check stays
181
+ // first for both reasons, and 'upgrade' keeps today's check byte-for-byte.
176
182
  async function handoffHost(manager, hostId, opts = {}) {
183
+ const reason = opts.reason === 'evacuate-threadpool' ? 'evacuate-threadpool' : 'upgrade';
177
184
  if (manager.isRecoveryProtectedHost?.(hostId)) return { ok: false, attempted: false, reason: 'owner-recovery-pending' };
178
185
  const compat = manager.hostCompatibility.get(hostId);
179
- if (!compat || compat.reason !== 'upgrade-pending') return { ok: false, attempted: false, reason: 'not-upgrade-pending' };
186
+ if (reason === 'upgrade' && (!compat || compat.reason !== 'upgrade-pending')) return { ok: false, attempted: false, reason: 'not-upgrade-pending' };
180
187
  const adapters = liveAdapters(manager, hostId);
181
188
  const first = adapters.values().next().value;
182
189
  const conn = first && first._socketConn && !first._socketConn.destroyed ? first._socketConn : null;
@@ -331,11 +338,18 @@ async function handoffHostWithRetry(manager, hostId, opts = {}) {
331
338
  }
332
339
 
333
340
  // Restore-loop tail (server.js): upgrade every upgrade-pending host, serially. Never throws.
334
- async function runUpgradePendingHandoffs({ manager, log = console.log, ...opts } = {}) {
341
+ // WP-3 (S-4): skipHostIds names hosts the boot-time stall escalation (D-4) has
342
+ // ALREADY decided to evacuate -- they are logged and left alone here, so no
343
+ // rollup/stale flag/10-minute retry can be armed against a stalled host.
344
+ async function runUpgradePendingHandoffs({ manager, skipHostIds = new Set(), log = console.log, ...opts } = {}) {
335
345
  const results = [];
336
346
  try {
337
347
  const hostIds = [...manager.hostCompatibility].filter(([, c]) => c && c.reason === 'upgrade-pending').map(([id]) => id);
338
348
  for (const hostId of hostIds) {
349
+ if (skipHostIds.has(hostId)) {
350
+ log(`[host-handoff] ${hostId} skipped: evacuating`);
351
+ continue;
352
+ }
339
353
  const result = await handoffHostWithRetry(manager, hostId, { ...opts, log });
340
354
  results.push(result);
341
355
  if (!result.attempted) {
@@ -33,6 +33,22 @@ function modelProcessGenerationMetadata(value) {
33
33
  return validModelProcessGeneration(value) ? { modelProcessGeneration: value } : {};
34
34
  }
35
35
  const MODEL_STORAGE_ROOT_KEYS = Object.freeze(['HOME', 'CODEX_HOME', 'CTM_CODEX_SESSIONS_DIR', 'CLAUDE_CONFIG_DIR', 'GEMINI_CLI_HOME', 'OPENCODE_DATA_DIR', 'XDG_DATA_HOME']);
36
+ // The keys a lead-model switch re-projects into the launch context (lib/lead-model-restore-reconciliation.js
37
+ // projectDeferredRestoreLaunch: HOME / CODEX_HOME from the launch context, TMPDIR alongside them). They are
38
+ // PROJECTED launch context, not per-session private overrides: a spawn still receives them, but their presence
39
+ // must never turn an otherwise empty override set into a private environment that only a surviving owner can
40
+ // reproduce. Anything else in the env is private and is captured by value.
41
+ const PROJECTED_LAUNCH_CONTEXT_KEYS = Object.freeze(['HOME', 'CODEX_HOME', 'TMPDIR']);
42
+ // Returns a NEW object holding only the non-projected entries of `env`; never mutates its input and maps a
43
+ // non-object to `{}`.
44
+ function privateOverridesFromEnv(env) {
45
+ if (!env || typeof env !== 'object' || Array.isArray(env)) return {};
46
+ const out = {};
47
+ for (const [key, value] of Object.entries(env)) {
48
+ if (!PROJECTED_LAUNCH_CONTEXT_KEYS.includes(key)) out[key] = value;
49
+ }
50
+ return out;
51
+ }
36
52
  function modelStorageRootsFromEnv(env) {
37
53
  const roots = {};
38
54
  for (const key of MODEL_STORAGE_ROOT_KEYS) {
@@ -111,6 +127,10 @@ function isAlive(pid) {
111
127
  // listSessions() -> [{ sessionId, ptyProcess, ptyClosed, cols, rows, seq, cmd, cwd, name, outputEpoch,
112
128
  // workerDisplayContext, suppressPtyHeadlessUntilInput, mirrorFeed }]; serialize(sessionId) -> {data,cols,rows}|null.
113
129
  const REPAINT_NUDGE_MS = 80;
130
+ // D-1: the old host's painted-seal await is bounded. A seal runs on its session's own op chain; a wedged chain
131
+ // (a dead libuv pool) never comes back, and no deadline is long enough to wait it out. Losing the race costs a
132
+ // restart break in that session's painted rows (an unsealed stretch cannot be joined), never the whole handoff.
133
+ const HANDOFF_SEAL_TIMEOUT_MS = 1000;
114
134
 
115
135
  function createHandoff({ hostId, workerPath, controlSocketPath, listSessions, serialize, mirrorStatus, headless, send, log = () => {}, setTimer = setTimeout, spawnSuccessor = spawnSuccessorHost }) {
116
136
  let active = null;
@@ -140,6 +160,26 @@ function createHandoff({ hostId, workerPath, controlSocketPath, listSessions, se
140
160
  (reply || send)({ type: 'handoff-aborted', handoffId: cur.handoffId, reason });
141
161
  }
142
162
 
163
+ // One session's painted-seal, raced against HANDOFF_SEAL_TIMEOUT_MS: resolves when the seal completes (its
164
+ // timer then cleared) or when the bound expires, in which case the handoff proceeds and the loss is reported.
165
+ // A rejecting seal still rejects (the begin() catch aborts the handoff, unchanged).
166
+ function raceSeal(sessionId) {
167
+ return new Promise((resolve, reject) => {
168
+ let settled = false;
169
+ const timer = setTimer(() => {
170
+ if (settled) return;
171
+ settled = true;
172
+ diagnostic(sessionId, 'host-handoff-seal-timeout', { timeoutMs: HANDOFF_SEAL_TIMEOUT_MS, oldHostId: hostId });
173
+ resolve();
174
+ }, HANDOFF_SEAL_TIMEOUT_MS);
175
+ timer?.unref?.();
176
+ Promise.resolve(headless.handleMessage({ type: 'painted-seal', sessionId })).then(
177
+ () => { if (settled) return; settled = true; clearTimeout(timer); resolve(); },
178
+ (err) => { if (settled) return; settled = true; clearTimeout(timer); reject(err); },
179
+ );
180
+ });
181
+ }
182
+
143
183
  async function begin(msg, reply) {
144
184
  const handoffId = String(msg.handoffId || '');
145
185
  if (active) { reply({ type: 'handoff-aborted', handoffId, reason: 'already-in-progress' }); return; }
@@ -153,11 +193,11 @@ function createHandoff({ hostId, workerPath, controlSocketPath, listSessions, se
153
193
  s.ptyProcess.pause();
154
194
  cur.paused.push(s);
155
195
  }
156
- const seeds = new Map();
157
- for (const s of cur.paused) {
196
+ // D-1: every seed is issued concurrently -- each `serialize` keeps its own 2 000 ms cap, so N sessions cost
197
+ // one cap, not N. The mirrorFeed flush still lands immediately before its own session's serialize.
198
+ const seeds = new Map(await Promise.all(cur.paused.map(async (s) => {
158
199
  s.mirrorFeed?.flush?.({ force: true });
159
200
  const snap = await serialize(s.sessionId);
160
- seeds.set(s.sessionId, snap);
161
201
  // Probe (observe only): what the seed carries vs what the mirror believed at the same instant.
162
202
  const seedData = String((snap && snap.data) || '');
163
203
  diagnostic(s.sessionId, 'host-handoff-mirror-seed', {
@@ -168,18 +208,16 @@ function createHandoff({ hostId, workerPath, controlSocketPath, listSessions, se
168
208
  seedBytes: Buffer.byteLength(seedData),
169
209
  oldHostId: hostId,
170
210
  });
171
- }
211
+ return [s.sessionId, snap];
212
+ })));
172
213
  if (active !== cur) return; // aborted (deadline) while serializing
173
214
  // This host exits without its headless close(): seal each painted-row stretch (with its last screen) and flush the
174
215
  // log now, before the successor exists to load it -- an unsealed stretch cannot be joined (a restart break).
175
- // Each id is listed BEFORE its seal is queued, so an abort that lands mid-seal still reopens it (the per-session op
176
- // queue runs that reopen after the seal).
177
- cur.sealed = [];
178
- for (const s of cur.paused) {
179
- if (active !== cur) return; // aborted (deadline) while sealing
180
- cur.sealed.push(s.sessionId);
181
- await headless.handleMessage({ type: 'painted-seal', sessionId: s.sessionId });
182
- }
216
+ // D-1: every id is listed BEFORE any seal is queued (so an abort that lands mid-seal still reopens all of them,
217
+ // the per-session op queue running each reopen after its seal), and every seal is raced against
218
+ // HANDOFF_SEAL_TIMEOUT_MS, so one wedged op chain cannot hold the handoff past its bound.
219
+ cur.sealed = cur.paused.map((s) => s.sessionId);
220
+ await Promise.all(cur.paused.map((s) => raceSeal(s.sessionId)));
183
221
  if (active !== cur) return; // aborted (deadline) while sealing
184
222
  const running = cur.paused.filter((s) => !s.ptyClosed);
185
223
  const entries = running.map((s, i) => {
@@ -306,4 +344,4 @@ function createHandoff({ hostId, workerPath, controlSocketPath, listSessions, se
306
344
  return { intercept, restore, resumeRestored, isActive: () => !!active };
307
345
  }
308
346
 
309
- module.exports = { handoffManifestFromArgv, writeHandoffManifest, readHandoffManifest, spawnSuccessorHost, createHandoff, validModelProcessGeneration, modelProcessGenerationFromEnv, modelProcessGenerationMetadata, modelStorageRootsFromEnv, modelStorageRootsMetadata };
347
+ module.exports = { HANDOFF_SEAL_TIMEOUT_MS, handoffManifestFromArgv, writeHandoffManifest, readHandoffManifest, spawnSuccessorHost, createHandoff, validModelProcessGeneration, modelProcessGenerationFromEnv, modelProcessGenerationMetadata, modelStorageRootsFromEnv, modelStorageRootsMetadata, PROJECTED_LAUNCH_CONTEXT_KEYS, privateOverridesFromEnv };
@@ -11,6 +11,12 @@
11
11
  // module's), no further RPCs go to that host; markCapped freezes the verdict
12
12
  // until forgetHost. Pure/in-memory except the injectable readStall hook.
13
13
  //
14
+ // WP-3 (session-stops-mid-job-2026-10-24.html, D-10..D-12): a hard serialize
15
+ // miss on a host that sent ANY frame after that request was issued (the
16
+ // injectable lastFrameAt hook) is readiness, not liveness -- it latches
17
+ // 'degraded' (reported once, cleared by a reply, never a kill). Only a silent
18
+ // host still reaches 'unresponsive'; the stall-file shortcut is unchanged.
19
+ //
14
20
  // No deps beyond node:fs/node:path for the default readStall -- kept free of
15
21
  // process globals (server.js binds dataDir into the readStall it injects).
16
22
 
@@ -31,23 +37,77 @@ function readStallFromDataDir(dataDir, hostId) {
31
37
  }
32
38
  }
33
39
 
40
+ // WP-3 (stale-pty-host-evacuation design doc, D-2, §6): the evacuation deadline
41
+ // for a continuously stalled libuv threadpool. Value provenance (§4 D-2): every
42
+ // recovered stall in the log evidence lasted <= 1.7 s, all 6 unrecovered ones
43
+ // longer; 30 s equals the existing hard-miss floor below (line 69).
44
+ const THREADPOOL_EVACUATE_MS = 30000;
45
+
34
46
  function createHostHealthLedger({
35
47
  missThreshold = 2,
36
48
  stallThresholdMs = 30000,
37
49
  readStall = null,
50
+ // WP-3 (D-10): lastFrameAt(hostId) -> number, the manager's per-host frame
51
+ // clock ("the host sent any frame", any transport). Optional: without it the
52
+ // ledger behaves exactly as it did before WP-3.
53
+ lastFrameAt = null,
38
54
  now = Date.now,
55
+ // WP-3 (D-2): the stall-evacuation episode's clock/timers/action seam. All
56
+ // three default to the real thing, so every existing caller is unaffected.
57
+ setTimer = setTimeout,
58
+ clearTimer = clearTimeout,
59
+ onEvacuate = null,
60
+ log = console.warn,
39
61
  } = {}) {
40
- const hosts = new Map(); // hostId -> { misses, lastMissAt, lastReplyAt, verdict }
62
+ // hostId -> { misses, lastMissAt, lastReplyAt, verdict, degraded, ...threadpool episode }
63
+ const hosts = new Map();
41
64
 
42
65
  function entryFor(hostId) {
43
66
  let entry = hosts.get(hostId);
44
67
  if (!entry) {
45
- entry = { misses: 0, lastMissAt: 0, lastReplyAt: 0, verdict: 'ok', cappedUntil: 0 };
68
+ entry = {
69
+ misses: 0, lastMissAt: 0, lastReplyAt: 0, verdict: 'ok', cappedUntil: 0, degraded: false,
70
+ // D-2 (WP-3) per-host stall episode: 0 = no episode in progress.
71
+ threadpoolStalledSince: 0, threadpoolOldestPendingMs: 0, evacuateFired: false, threadpoolTimer: null,
72
+ };
46
73
  hosts.set(hostId, entry);
47
74
  }
48
75
  return entry;
49
76
  }
50
77
 
78
+ function _cancelThreadpoolTimer(entry) {
79
+ if (!entry || !entry.threadpoolTimer) return;
80
+ clearTimer(entry.threadpoolTimer);
81
+ entry.threadpoolTimer = null;
82
+ }
83
+
84
+ function _threadpoolResult(entry, id) {
85
+ return {
86
+ action: 'evacuate',
87
+ hostId: id,
88
+ oldestPendingMs: Number(entry.threadpoolOldestPendingMs) || 0,
89
+ stalledForMs: now() - entry.threadpoolStalledSince,
90
+ };
91
+ }
92
+
93
+ // Fires an episode exactly once: marks it fired BEFORE calling out, so a
94
+ // re-entrant recordThreadpool from onEvacuate cannot double-fire.
95
+ function _markFired(entry, id) {
96
+ entry.evacuateFired = true;
97
+ entry.threadpoolTimer = null;
98
+ return _threadpoolResult(entry, id);
99
+ }
100
+
101
+ // The armed-timer path is the ONLY one that calls out: an immediate verdict is
102
+ // returned to the caller of recordThreadpool instead (§6 -- one action per
103
+ // episode, never both).
104
+ function _fireEvacuate(entry, id) {
105
+ const result = _markFired(entry, id);
106
+ if (typeof onEvacuate !== 'function') return; // no action bound -- the episode is still marked fired
107
+ try { onEvacuate(result); }
108
+ catch (err) { log(`[host-health] host=${id} onEvacuate threw: ${err?.message || err}`); } // REPORT: a timer callback must never throw into the event loop
109
+ }
110
+
51
111
  // Final-review fix C4: 'unresponsive-capped' used to be permanent -- markCapped
52
112
  // set it forever and recordMiss short-circuited on any non-'ok' verdict, so
53
113
  // EVERY snapshot for EVERY session on that host was answered timedOut for the
@@ -63,7 +123,7 @@ function createHostHealthLedger({
63
123
  entry.cappedUntil = 0;
64
124
  }
65
125
 
66
- function recordMiss(hostId, { sessionId, kind, deadlineMs } = {}) {
126
+ function recordMiss(hostId, { sessionId, kind, deadlineMs, issuedAt } = {}) {
67
127
  const id = String(hostId || '');
68
128
  if (!id) return null;
69
129
  // Soft (short-deadline, or explicitly non-'hard') misses never count -- D-2
@@ -74,14 +134,37 @@ function createHostHealthLedger({
74
134
  // Once a host already carries a verdict, further misses are idempotent --
75
135
  // no state change, verdict() keeps reporting it until forgetHost.
76
136
  if (entry.verdict !== 'ok') return null;
77
- entry.misses += 1;
78
- entry.lastMissAt = now();
79
137
  let stall = null;
80
138
  if (typeof readStall === 'function') {
81
139
  try { stall = readStall(id); } catch { stall = null; }
82
140
  }
83
- const stalled = stall && Number(stall.durationMs) >= stallThresholdMs;
84
- if (stalled || entry.misses >= missThreshold) {
141
+ // D-2(b), unchanged: the host's own stall report is proof it is wedged, and
142
+ // it still wins over any frame (a stalled host can relay PTY bytes).
143
+ if (stall && Number(stall.durationMs) >= stallThresholdMs) {
144
+ entry.misses += 1;
145
+ entry.lastMissAt = now();
146
+ entry.verdict = 'unresponsive';
147
+ return { verdict: 'unresponsive', misses: entry.misses, stall };
148
+ }
149
+ // D-11 (WP-3): the host sent a frame at or after this request was issued --
150
+ // a deadlined request family timing out on a TALKING host is readiness, not
151
+ // liveness. Report 'degraded' once per host since the last reply; never
152
+ // count it toward the miss threshold, never verdict the host unresponsive.
153
+ if (typeof lastFrameAt === 'function' && Number.isFinite(issuedAt)) {
154
+ const frameAt = Number(lastFrameAt(id));
155
+ if (Number.isFinite(frameAt) && frameAt >= issuedAt) {
156
+ if (entry.degraded !== true) {
157
+ entry.degraded = true;
158
+ return { verdict: 'degraded', misses: entry.misses, lastFrameAgoMs: now() - frameAt };
159
+ }
160
+ return null;
161
+ }
162
+ }
163
+ // D-12: no frame of any kind since the first missed request was issued --
164
+ // today's path. Two such misses (missThreshold) is a silent host.
165
+ entry.misses += 1;
166
+ entry.lastMissAt = now();
167
+ if (entry.misses >= missThreshold) {
85
168
  entry.verdict = 'unresponsive';
86
169
  return { verdict: 'unresponsive', misses: entry.misses, stall: stall || null };
87
170
  }
@@ -94,6 +177,9 @@ function createHostHealthLedger({
94
177
  const entry = entryFor(id);
95
178
  entry.misses = 0;
96
179
  entry.lastReplyAt = now();
180
+ // D-11: the host answered, so whatever readiness conclusion was latched is
181
+ // stale evidence -- the next talking miss reports once again.
182
+ entry.degraded = false;
97
183
  // C4: the host answered -- whatever the cap concluded is stale evidence.
98
184
  if (entry.verdict === 'unresponsive-capped') {
99
185
  entry.verdict = 'ok';
@@ -116,17 +202,78 @@ function createHostHealthLedger({
116
202
  entry.cappedUntil = now() + CRASH_CAP_WINDOW_MS;
117
203
  }
118
204
 
205
+ // WP-3 (D-2, §6): the host's own threadpool snapshot as a verdict, not a
206
+ // banner. Returns { action:'evacuate', hostId, oldestPendingMs, stalledForMs }
207
+ // exactly ONCE per stall episode, and only for a host whose pool is either
208
+ // reported stalled with an already-overdue op (oldestPendingMs >= 30 s) or
209
+ // has been continuously stalled for >= 30 s with no 'ok' frame in between.
210
+ function recordThreadpool(hostId, snapshot) {
211
+ const id = String(hostId || '');
212
+ const state = snapshot && snapshot.state;
213
+ if (!id || (state !== 'ok' && state !== 'stalled')) return null; // unknown shape -- no state change
214
+ if (state === 'ok') {
215
+ const entry = hosts.get(id);
216
+ if (!entry) return null; // nothing was in progress -- no entry to create
217
+ _cancelThreadpoolTimer(entry);
218
+ entry.threadpoolStalledSince = 0;
219
+ entry.threadpoolOldestPendingMs = 0;
220
+ entry.evacuateFired = false;
221
+ return null;
222
+ }
223
+ const entry = entryFor(id);
224
+ if (!entry.threadpoolStalledSince) entry.threadpoolStalledSince = now(); // first stalled frame starts the episode
225
+ if (entry.evacuateFired) return null; // already acted on this episode
226
+ entry.threadpoolOldestPendingMs = Number(snapshot.oldestPendingMs) || 0;
227
+ const stalledForMs = now() - entry.threadpoolStalledSince;
228
+ if (entry.threadpoolOldestPendingMs >= THREADPOOL_EVACUATE_MS || stalledForMs >= THREADPOOL_EVACUATE_MS) {
229
+ _cancelThreadpoolTimer(entry);
230
+ return _markFired(entry, id);
231
+ }
232
+ // Not yet due: arm ONE timer for the remainder of the window (never stack --
233
+ // an existing timer already targets the same deadline and its own `hosts.get`
234
+ // liveness check keeps it honest across forget/move).
235
+ if (!entry.threadpoolTimer) {
236
+ const timer = setTimer(() => {
237
+ if (hosts.get(id) !== entry || entry.threadpoolStalledSince <= 0 || entry.evacuateFired) return;
238
+ _fireEvacuate(entry, id);
239
+ }, Math.max(1, THREADPOOL_EVACUATE_MS - stalledForMs));
240
+ timer?.unref?.();
241
+ entry.threadpoolTimer = timer || null;
242
+ }
243
+ return null;
244
+ }
245
+
246
+ // WP-3: re-arm RE-EVALUATION without forgetting the episode (used when an
247
+ // evacuation was deferred -- owner recovery pending): the next stalled frame
248
+ // re-decides immediately, keeping the episode's original stalledSince.
249
+ function resetThreadpoolEpisode(hostId) {
250
+ const entry = hosts.get(String(hostId || ''));
251
+ if (!entry) return;
252
+ _cancelThreadpoolTimer(entry);
253
+ entry.evacuateFired = false;
254
+ }
255
+
119
256
  function forgetHost(hostId) {
120
- hosts.delete(String(hostId || ''));
257
+ const id = String(hostId || '');
258
+ _cancelThreadpoolTimer(hosts.get(id));
259
+ hosts.delete(id);
121
260
  }
122
261
 
123
- // WP-4: a handoff replaces the host process, not its health history -- carry the entry over.
262
+ // WP-4: a handoff replaces the host process, not its health history -- carry
263
+ // the entry over. WP-3 (D-2): the successor is a fresh process with a fresh
264
+ // libuv pool, so its stall episode is dropped -- its own frames decide.
124
265
  function moveHost(fromId, toId) {
125
266
  const from = String(fromId || ''); const to = String(toId || '');
126
267
  if (!from || !to || from === to) return;
127
268
  const entry = hosts.get(from);
128
269
  hosts.delete(from);
129
- if (entry) hosts.set(to, entry);
270
+ if (!entry) return;
271
+ _cancelThreadpoolTimer(entry);
272
+ entry.threadpoolStalledSince = 0;
273
+ entry.threadpoolOldestPendingMs = 0;
274
+ entry.evacuateFired = false;
275
+ _cancelThreadpoolTimer(hosts.get(to)); // a superseded entry's armed timer must never fire for its successor
276
+ hosts.set(to, entry);
130
277
  }
131
278
 
132
279
  function snapshot() {
@@ -138,12 +285,17 @@ function createHostHealthLedger({
138
285
  lastReplyAt: entry.lastReplyAt,
139
286
  verdict: entry.verdict,
140
287
  cappedUntil: entry.cappedUntil || 0,
288
+ threadpoolStalledSince: entry.threadpoolStalledSince || 0,
289
+ evacuateFired: entry.evacuateFired === true,
290
+ // D-11 (WP-3): a talking host that missed a hard deadline since its last
291
+ // reply -- readiness, reported once, never a kill.
292
+ degraded: entry.degraded === true,
141
293
  };
142
294
  }
143
295
  return { hosts: out };
144
296
  }
145
297
 
146
- return { recordMiss, recordReply, verdict, markCapped, forgetHost, moveHost, snapshot };
298
+ return { recordMiss, recordReply, recordThreadpool, verdict, markCapped, forgetHost, moveHost, resetThreadpoolEpisode, snapshot };
147
299
  }
148
300
 
149
- module.exports = { createHostHealthLedger, readStallFromDataDir };
301
+ module.exports = { createHostHealthLedger, readStallFromDataDir, THREADPOOL_EVACUATE_MS };
@@ -34,6 +34,10 @@ const HOST_WATCHDOG_OP_TABLE = [
34
34
  'getViewportText', 'composer-probe', 'getRows', 'reset', 'flush-archive',
35
35
  'mirror-status', 'mirrors-report', 'trim-mirrors', 'flush-tail', 'destroy',
36
36
  'painted-read', 'drop-painted',
37
+ // WP-0 (D-6/S-13): appended ONLY -- an entry's index IS its opCode, so
38
+ // reordering or removing any of the rows above would relabel every stamp a
39
+ // still-running host writes. These three were missing and stamped as 'none'.
40
+ 'serialize-bootstrap', 'painted-seal', 'painted-reopen',
37
41
  ];
38
42
 
39
43
  function _hostCpuUsageMs() {