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,494 @@
1
+ 'use strict';
2
+
3
+ // §5 D-7 / §5 D-9 / §9 S-6, S-10, S-13 (docs/model-proxy-survives-ctm-restart-2026-10-22.html): the
4
+ // UPGRADE HANDOFF -- both halves of it, in one place.
5
+ //
6
+ // The proxy's code changed on disk (CTM adopted it with `upgrade-pending`), so the running proxy cannot
7
+ // keep serving new code it never loaded, and CTM must not simply kill it: every in-flight model stream of
8
+ // every live CLI dies with it. Instead the OLD proxy spawns its own path from disk -- its successor --
9
+ // passes over the two LISTENING sockets it owns (plaintext HTTP, and the native mTLS listener when it is
10
+ // up), the memory-only state that has no other authority (the token registry, the per-token last lead
11
+ // model), and then stops ACCEPTING, drains its in-flight streams, and exits 0.
12
+ //
13
+ // The pillars this is built from (§3 P-1..P-4): the fd is passed, never re-bound (no EADDRINUSE window,
14
+ // no dropped syscall between two binds); the successor's `ready` is a handshake BEFORE the predecessor
15
+ // stops accepting; stop-accepting is split from stop-serving (`native-lead-listener.stopAccepting()`,
16
+ // `wrapHttpServerClose(..., { drain:true })`); and the drain is BOUNDED -- at `DRAIN_DEADLINE_MS` the
17
+ // remaining streams are cut and the process still exits 0 so nothing is left half-upgraded.
18
+ //
19
+ // What this module deliberately does NOT do: it never picks a different port, never retries a failed
20
+ // handoff (the predecessor keeps serving and CTM's next adoption tries again), and never writes the
21
+ // instance record of its successor -- the successor publishes its own identity, and a predecessor that
22
+ // is still draining must not unlink it (`isDraining()` is what keeps its SIGTERM handler off that file).
23
+
24
+ const { spawn } = require('node:child_process');
25
+
26
+ const tokens = require('./tokens');
27
+ const leadWorkerPicks = require('./lead-worker-picks');
28
+ const instanceRecord = require('./proxy-instance-record');
29
+ const streamAccounting = require('./stream-accounting');
30
+ const { wrapHttpServerClose } = require('./native-lead-listener');
31
+
32
+ // §5 D-7: the successor must answer `ready` within this; the old proxy keeps serving unchanged if it does
33
+ // not. Host handoffs measured 179-256 ms for five sessions, and the successor also loads the TLS
34
+ // authority, so 15 s is two orders of magnitude of headroom and still bounded.
35
+ const HANDOFF_READY_DEADLINE_MS = 15_000;
36
+ // §5 D-7: the failsafe only -- a drain normally ends the moment the counter reaches zero. 45 real
37
+ // streamed requests exceeded 10 minutes (§1.3), so the ceiling is above the longest observed turn.
38
+ const DRAIN_DEADLINE_MS = 1_800_000;
39
+
40
+ const HANDOFF_MESSAGE = 'handoff/v1';
41
+ const HANDOFF_NATIVE_MESSAGE = 'handoff/v1/native';
42
+ const HANDOFF_STATE_MESSAGE = 'handoff/v1/state';
43
+ const READY_MESSAGE = 'ready';
44
+ const HANDOFF_INHERITANCE_TIMEOUT_CODE = 'handoff_inheritance_timeout';
45
+
46
+ // The successor is always THIS file's sibling from disk, never a caller-supplied path.
47
+ const PROXY_SCRIPT = require.resolve('../../oauth-proxy.js');
48
+
49
+ // Whether THIS process has taken itself out of the accepting role: the record on disk now belongs to the
50
+ // successor, so a SIGTERM arriving after step 6 must not unlink it (WP-0's handler asks exactly this).
51
+ let _predecessorState = null;
52
+
53
+ function isDraining() {
54
+ return _predecessorState !== null;
55
+ }
56
+
57
+ // ── §5 D-8: the proxy's own linger ────────────────────────────────────────────────────────────────────
58
+ // A proxy outlives the CTM that spawned it (§5 D-4) on purpose: a restart is a reconnect, and the
59
+ // in-flight streams of every live CLI survive it. The other half of that bargain is that a proxy which
60
+ // outlives CTM FOR GOOD must not serve forever -- it is a credential-holding listener on a loopback port.
61
+ // So it keeps serving a while after the last sign of life from above and then leaves by itself, exactly
62
+ // as a session host does (workers/session-host-pool-process.js:49 `HOST_LINGER_MS`, same value on
63
+ // purpose: one linger policy to reason about, and an operator reads "15 minutes" once, in both logs).
64
+ const PROXY_LINGER_MS = 15 * 60_000; // === HOST_LINGER_MS
65
+
66
+ // "Contact" is exactly one thing: a request that passed the admin-secret check in oauth-proxy.js's
67
+ // `_handleAdmin` -- CTM's boot probe, its lead-worker-picks read, a token mint, a handoff request. The
68
+ // unauthenticated surfaces (`/_health`, `/_status`, `/r/*`) are NOT contact: anyone on the loopback
69
+ // interface can poll them, so counting them would let a spectator keep a stale proxy alive for ever.
70
+ let _lastAdminContactAt = null;
71
+ // The ONE timer, armed by the CLI's start() only (see armLinger): null means "this process never lingers".
72
+ let _lingerTimer = null;
73
+
74
+ /** §5 D-8: record contact from above. Called once per authenticated admin request, and nowhere else. */
75
+ function noteAdminContact({ now = Date.now() } = {}) {
76
+ _lastAdminContactAt = now;
77
+ }
78
+
79
+ /** The moment of the last authenticated admin request, or null when this process has never had one. */
80
+ function lastAdminContactAt() {
81
+ return _lastAdminContactAt;
82
+ }
83
+
84
+ /** Test seam (§6): whether a linger timer is armed. Nothing in production reads it. */
85
+ function _lingerArmedForTest() {
86
+ return _lingerTimer !== null;
87
+ }
88
+
89
+ function _armLingerTimer({ lingerMs, ctx }) {
90
+ _lingerTimer = setTimeout(() => {
91
+ _lingerTimer = null;
92
+ _lingerTick({ lingerMs, ctx });
93
+ }, lingerMs);
94
+ // §5 D-8: never a reason for this process to stay alive -- the listeners and the sockets are.
95
+ _lingerTimer.unref();
96
+ }
97
+
98
+ /**
99
+ * §5 D-8: one tick. The window is the cadence as well as the test, so there is one constant here, not two.
100
+ * Anything that could still be serving -- an in-flight stream, an open connection on either lane, a
101
+ * handoff being brought up -- re-arms and the process stays.
102
+ */
103
+ function _lingerTick({ lingerMs, ctx }) {
104
+ const idleMs = _lastAdminContactAt === null ? 0 : Date.now() - _lastAdminContactAt;
105
+ const rearm = () => _armLingerTimer({ lingerMs, ctx });
106
+ // A predecessor mid-handoff is not idle: its successor is starting, and its own drain owns its exit.
107
+ if (idleMs < lingerMs || isDraining()) { rearm(); return; }
108
+ // The counter outlives the transfer for an upgraded stream, so it is checked as well as the sockets.
109
+ if (ctx.streamAccounting.count() > 0) { rearm(); return; }
110
+ const nativeCount = ctx.nativeListener && typeof ctx.nativeListener.socketCount === 'function'
111
+ ? ctx.nativeListener.socketCount()
112
+ : 0;
113
+ if (nativeCount > 0) { rearm(); return; }
114
+ ctx.httpServer.getConnections((err, count) => {
115
+ // REPORT: a count that cannot be read is treated as busy -- an unanswerable question must never be
116
+ // the reason a proxy holding live credentials disappears.
117
+ if (err || count > 0) { rearm(); return; }
118
+ _exitOnLinger({ idleMs, ctx });
119
+ });
120
+ }
121
+
122
+ /** §5 D-8: the exit itself -- record it, remove this process's own record, and leave 0. */
123
+ function _exitOnLinger({ idleMs, ctx }) {
124
+ console.log(`[model-proxy] exit reason=linger idle_ms=${idleMs}`);
125
+ // The record names a process that is about to be gone, so the next CTM spawns rather than adopts. It is
126
+ // removed BEFORE the exit record is written: that one is the ledger's, and it is written once.
127
+ instanceRecord.unlinkInstanceRecord({ dataDir: ctx.dataDir });
128
+ streamAccounting.writeExitRecord({ dataDir: ctx.dataDir, reason: 'linger', openStreams: 0 })
129
+ .catch((err) => {
130
+ // REPORT: the exit code below still classifies this exit, so a failed write is not fatal.
131
+ console.error(`[model-proxy] exit record not written: ${(err && err.code) || 'unknown'}`);
132
+ })
133
+ .then(() => process.exit(0));
134
+ }
135
+
136
+ /**
137
+ * §5 D-8: arm the linger timer for THIS process. Only the CLI calls this (proxy-cli.js, with an optional
138
+ * `CTM_TEST_PROXY_LINGER_MS` override) -- an embedded start() from a test or another module passes no
139
+ * `lingerMs`, so no test process can ever be exited by a proxy's own linger.
140
+ *
141
+ * "Contact" starts at arming time: a process that has just started (a handoff SUCCESSOR above all) has
142
+ * been in contact by construction -- its predecessor only hands over after an authenticated request.
143
+ *
144
+ * @param {object} input
145
+ * @param {number} [input.lingerMs] the idle window, defaulting to PROXY_LINGER_MS
146
+ * @param {object} input.ctx this server's context (`httpServer`, `nativeListener`, `streamAccounting`,
147
+ * `dataDir` -- everything the tick needs to prove nothing is being served)
148
+ */
149
+ function armLinger({ lingerMs = PROXY_LINGER_MS, ctx } = {}) {
150
+ if (!ctx || !ctx.httpServer || !ctx.streamAccounting) return false;
151
+ if (_lingerTimer) clearTimeout(_lingerTimer);
152
+ _lastAdminContactAt = _lastAdminContactAt === null ? Date.now() : _lastAdminContactAt;
153
+ _armLingerTimer({ lingerMs: _positiveIntOr(lingerMs, PROXY_LINGER_MS), ctx });
154
+ return true;
155
+ }
156
+
157
+ /** A bounded deadline comes from the admin body or not at all: anything that is not a positive integer
158
+ * (a float, a string, 0, a negative, a missing field) is IGNORED and the constant stands. */
159
+ function _positiveIntOr(value, fallback) {
160
+ return Number.isSafeInteger(value) && value > 0 ? value : fallback;
161
+ }
162
+
163
+ /**
164
+ * §5 D-7 steps 1-6: hand this proxy over to a successor spawned from disk.
165
+ *
166
+ * @param {object} input
167
+ * @param {object} input.ctx the running server's context (its `streamAccounting` is the drain signal)
168
+ * @param {import('http').Server} input.httpServer the plaintext server whose listening socket is passed
169
+ * @param {object|null} [input.nativeListener] the mTLS controller; its server is passed only when ready
170
+ * @param {string} input.dataDir the canonical data directory (record, secret and log live under it)
171
+ * @param {number} input.port the bound plaintext port
172
+ * @param {number|null} [input.nativePort] the bound native port, when the listener was up
173
+ * @param {string|null} [input.adminSecret] this process's admin secret, re-used by the successor
174
+ * @param {number} [input.generation] this process's generation; the successor is +1
175
+ * @param {number} [input.readyDeadlineMs] test override (admin body only, never env)
176
+ * @param {number} [input.drainDeadlineMs] test override (admin body only, never env)
177
+ * @param {object} [input.log] `log`/`error` sink, default console
178
+ * @returns {Promise<{ok:true, generation:number, successorPid:number, handoffMs:number, armDrain:Function}
179
+ * | {ok:false, status:409, reason:string}>} the caller's `armDrain()` runs the drain (step 7) once its
180
+ * own 202 has been written, so the exit this schedules can never preempt the response.
181
+ */
182
+ async function initiateHandoff({
183
+ ctx = null,
184
+ httpServer = null,
185
+ nativeListener = null,
186
+ dataDir = null,
187
+ port = null,
188
+ nativePort = null,
189
+ adminSecret = null,
190
+ generation = 1,
191
+ readyDeadlineMs,
192
+ drainDeadlineMs,
193
+ log = console,
194
+ } = {}) {
195
+ // step 1 -- one handoff per process, and the first one is final: a drained predecessor has nothing
196
+ // left to hand over, and a second caller must not spawn a second successor.
197
+ if (_predecessorState) return { ok: false, status: 409, reason: 'already_draining' };
198
+ _predecessorState = 'draining';
199
+ const startedAt = Date.now();
200
+ const readyMs = _positiveIntOr(readyDeadlineMs, HANDOFF_READY_DEADLINE_MS);
201
+ const drainMs = _positiveIntOr(drainDeadlineMs, DRAIN_DEADLINE_MS);
202
+ const accounting = ctx && ctx.streamAccounting ? ctx.streamAccounting : null;
203
+
204
+ const abort = (reason) => {
205
+ // §5 D-7 step 5 ("abort restores"): the predecessor kept serving, so both truths it wrote must say
206
+ // so again -- the record stops claiming a drain, and /_health stops reporting one. The IDENTITY is
207
+ // restored with them: a successor that got as far as publishing its own record and then never
208
+ // answered is killed here, so the record must name the process that is really serving rather than
209
+ // the one that just died (otherwise the next boot reads a dead pid and spawns a second proxy onto a
210
+ // port this process still holds).
211
+ _predecessorState = null;
212
+ instanceRecord.updateInstanceRecord({
213
+ dataDir, patch: { pid: process.pid, generation, predecessorPid: null, draining: false },
214
+ });
215
+ if (accounting && typeof accounting.setDraining === 'function') accounting.setDraining(false);
216
+ return { ok: false, status: 409, reason };
217
+ };
218
+
219
+ // step 2 -- the record is truthful BEFORE the successor exists: a predecessor that dies mid-handoff
220
+ // leaves a record that says a handoff was in progress rather than one that silently claims to serve.
221
+ instanceRecord.updateInstanceRecord({ dataDir, patch: { draining: true } });
222
+ if (accounting && typeof accounting.setDraining === 'function') accounting.setDraining(true);
223
+
224
+ // step 3 -- detached, its own log file, an IPC channel for the two handles, and this process's own
225
+ // environment (the successor is the same proxy with the same configurability).
226
+ let logFd = null;
227
+ let child = null;
228
+ try {
229
+ logFd = instanceRecord.openProxyLogFd(dataDir);
230
+ child = spawn(process.execPath, [PROXY_SCRIPT], {
231
+ detached: true,
232
+ stdio: ['ignore', logFd, logFd, 'ipc'],
233
+ env: process.env,
234
+ serialization: 'json',
235
+ });
236
+ } catch (err) {
237
+ // REPORT: a successor that could not be created is the same outcome as one that never answered.
238
+ log.error(`[model-proxy] handoff spawn failed: ${(err && err.code) || 'unknown'}`);
239
+ instanceRecord.closeProxyLogFd(logFd);
240
+ return abort('spawn_failed');
241
+ }
242
+ instanceRecord.closeProxyLogFd(logFd); // the successor holds its own copy of the descriptor
243
+ child.unref(); // CTM owns neither proxy; a successor must not be reaped with this process
244
+
245
+ // steps 4 + 5 -- the handshake. The wait is armed BEFORE the first send, so a successor that answers
246
+ // instantly cannot be missed.
247
+ const ready = _waitForReady(child, readyMs);
248
+ const handed = _sendHandoff(child, httpServer, nativeListener, {
249
+ adminSecret, dataDir, port, nativePort, generation: generation + 1,
250
+ });
251
+ const outcome = handed ? await ready : { ok: false, reason: 'successor_not_ready' };
252
+ if (!outcome.ok) {
253
+ try {
254
+ child.kill('SIGKILL');
255
+ } catch (err) {
256
+ // expected: the successor may already be gone; the predecessor's own state is what matters.
257
+ void err;
258
+ }
259
+ log.error(`[model-proxy] handoff aborted: ${outcome.reason}`);
260
+ return abort(outcome.reason);
261
+ }
262
+
263
+ // step 6 -- stop ACCEPTING (never destroy): the plaintext listening socket is closed here and the
264
+ // native listener releases its own, while every connection either one already accepted keeps serving.
265
+ const handoffMs = Date.now() - startedAt;
266
+ if (nativeListener) wrapHttpServerClose(httpServer, nativeListener, { drain: true });
267
+ httpServer.close();
268
+ if (typeof httpServer.closeIdleConnections === 'function') httpServer.closeIdleConnections();
269
+ _predecessorState = 'drained';
270
+ log.log(`[model-proxy] handoff successor_pid=${outcome.pid} generation=${generation + 1} handoff_ms=${handoffMs}`);
271
+ return {
272
+ ok: true,
273
+ generation: generation + 1,
274
+ successorPid: outcome.pid,
275
+ handoffMs,
276
+ // step 7 is armed by the CALLER, once its own 202 is on the wire.
277
+ armDrain: () => startDrain({ accounting, httpServer, nativeListener, drainMs, log }),
278
+ };
279
+ }
280
+
281
+ /**
282
+ * §5 D-7 step 5 (successor side): everything that must be true before `ready` may be sent. The inherited
283
+ * TLS socket is re-bound first -- when the predecessor had one, its listener was serving, so the
284
+ * successor must be serving on the same socket before the predecessor stops accepting on it.
285
+ * @returns {Promise<void>} resolves when this process is genuinely serving
286
+ */
287
+ function announceInheritedStart({ ctx, inherited, record }) {
288
+ if (!inherited.nativeHandle || !ctx || !ctx.nativeListener) return Promise.resolve(sendReady(inherited, record));
289
+ return ctx.nativeListener.ensureReady().then(
290
+ () => sendReady(inherited, record),
291
+ (err) => {
292
+ // REPORT: the inherited TLS socket could not be re-bound; the plaintext lane is unaffected and the
293
+ // native lane falls back to its own bind on the next registration.
294
+ process.stderr.write(`[model-proxy] inherited native listener unavailable: ${(err && err.code) || 'unknown'}\n`);
295
+ return sendReady(inherited, record);
296
+ },
297
+ );
298
+ }
299
+
300
+ /** §6 test seam (AC-2b): a successor that is held back does everything else and never answers. */
301
+ function sendReady(inherited, record) {
302
+ if (inherited.holdReady) return;
303
+ process.send({ t: READY_MESSAGE, pid: process.pid, closureHash: record ? record.closureHash : null });
304
+ }
305
+
306
+ /**
307
+ * §5 D-7 step 4: the three messages, in the order the successor expects them. The plaintext server goes
308
+ * with the first one, the TLS server with the second -- and only when it is really up, because a
309
+ * listener that never bound has no socket to give (the successor binds the port itself in that case).
310
+ * @returns {boolean} false when the channel refused a send, which aborts the handoff
311
+ */
312
+ function _sendHandoff(child, httpServer, nativeListener, payload) {
313
+ try {
314
+ child.send({ t: HANDOFF_MESSAGE, ...payload, predecessorPid: process.pid }, httpServer);
315
+ if (nativeListener && nativeListener.isReady() && typeof nativeListener.server === 'function') {
316
+ child.send({ t: HANDOFF_NATIVE_MESSAGE }, nativeListener.server());
317
+ }
318
+ child.send({
319
+ t: HANDOFF_STATE_MESSAGE,
320
+ tokens: tokens.exportAll(),
321
+ lastLeadModel: leadWorkerPicks.exportLastLeadModel(),
322
+ });
323
+ return true;
324
+ } catch (err) {
325
+ // REPORT: the successor never became reachable, so the caller kills it and keeps serving.
326
+ process.stderr.write(`[model-proxy] handoff send failed: ${(err && err.code) || 'unknown'}\n`);
327
+ return false;
328
+ }
329
+ }
330
+
331
+ /**
332
+ * §5 D-7 step 5: `{ t:'ready', pid, closureHash }` within the deadline; a successor that exits first is
333
+ * the same failure as one that never answers.
334
+ * @returns {Promise<{ok:true, pid:number, closureHash:string}|{ok:false, reason:'successor_not_ready'}>}
335
+ */
336
+ function _waitForReady(child, readyMs) {
337
+ return new Promise((resolve) => {
338
+ let settled = false;
339
+ let timer = null;
340
+ const settle = (value) => {
341
+ if (settled) return;
342
+ settled = true;
343
+ clearTimeout(timer);
344
+ child.removeListener('message', onMessage);
345
+ child.removeListener('exit', onExit);
346
+ resolve(value);
347
+ };
348
+ const onMessage = (message) => {
349
+ if (message && message.t === READY_MESSAGE) settle({ ok: true, pid: message.pid, closureHash: message.closureHash });
350
+ };
351
+ const onExit = () => settle({ ok: false, reason: 'successor_not_ready' });
352
+ timer = setTimeout(() => settle({ ok: false, reason: 'successor_not_ready' }), readyMs);
353
+ child.on('message', onMessage);
354
+ child.on('exit', onExit);
355
+ });
356
+ }
357
+
358
+ /**
359
+ * §5 D-7 step 7: exit 0 the moment the counter reaches zero (the decrement notifies -- there is no
360
+ * polling loop), or at the deadline cut whatever is left and exit 0 anyway. `drain_forced_cuts` is the
361
+ * count AT the moment of the cut, so a clean drain logs 0 and the number is never a guess.
362
+ */
363
+ function startDrain({ accounting = null, httpServer = null, nativeListener = null, drainMs = DRAIN_DEADLINE_MS, log = console } = {}) {
364
+ let done = false;
365
+ let timer = null;
366
+ const finish = (forcedCuts) => {
367
+ if (done) return;
368
+ done = true;
369
+ clearTimeout(timer);
370
+ log.log(`[model-proxy] drain_forced_cuts=${forcedCuts}`);
371
+ process.exit(0);
372
+ };
373
+ timer = setTimeout(() => {
374
+ const remaining = accounting ? accounting.count() : 0;
375
+ try {
376
+ if (httpServer && typeof httpServer.closeAllConnections === 'function') httpServer.closeAllConnections();
377
+ } catch (err) {
378
+ // expected: a server that is already closed owns no connection to cut; the exit below still runs.
379
+ void err;
380
+ }
381
+ if (nativeListener && typeof nativeListener.close === 'function') {
382
+ try {
383
+ nativeListener.close();
384
+ } catch (err) {
385
+ // expected: the native listener may already be closed; the exit below still runs.
386
+ void err;
387
+ }
388
+ }
389
+ finish(remaining);
390
+ }, drainMs);
391
+ if (!accounting || accounting.count() === 0) {
392
+ // Nothing is in flight: leave on the next turn, so the caller's own response has been queued first.
393
+ setImmediate(() => finish(0));
394
+ return;
395
+ }
396
+ accounting.onIdle(() => finish(0));
397
+ }
398
+
399
+ /**
400
+ * §5 D-7 (successor side): collect the predecessor's three messages and resolve what they carry. Only a
401
+ * process spawned with an IPC channel calls this -- and it is the SUCCESSOR, so it never reads stdin.
402
+ *
403
+ * Termination is the STATE message, not a count: the native message is optional (it is sent only when
404
+ * the predecessor's TLS listener was up), and IPC preserves order, so the state message proves whether a
405
+ * native handle was coming.
406
+ *
407
+ * @param {{timeoutMs?:number}} [options] bounded wait; a predecessor that dies mid-handshake must not
408
+ * leave a successor waiting forever
409
+ * @returns {Promise<{inherited:object, state:{tokens:Array, lastLeadModel:Array}}>}
410
+ */
411
+ function receiveInheritance({ timeoutMs = HANDOFF_READY_DEADLINE_MS } = {}) {
412
+ return new Promise((resolve, reject) => {
413
+ if (typeof process.send !== 'function' || typeof process.on !== 'function') {
414
+ const err = new Error('handoff inheritance requested without an IPC channel');
415
+ err.code = HANDOFF_INHERITANCE_TIMEOUT_CODE;
416
+ reject(err);
417
+ return;
418
+ }
419
+ let first = null;
420
+ let nativeHandle = null;
421
+ let state = null;
422
+ let settled = false;
423
+ let timer = null;
424
+ const cleanup = () => {
425
+ clearTimeout(timer);
426
+ process.removeListener('message', onMessage);
427
+ };
428
+ const maybe = () => {
429
+ if (settled || !first || !state) return;
430
+ settled = true;
431
+ cleanup();
432
+ resolve({
433
+ inherited: {
434
+ httpHandle: first.httpHandle,
435
+ nativeHandle,
436
+ adminSecret: first.adminSecret,
437
+ dataDir: first.dataDir,
438
+ port: first.port,
439
+ nativePort: first.nativePort === undefined ? null : first.nativePort,
440
+ generation: first.generation,
441
+ predecessorPid: first.predecessorPid,
442
+ },
443
+ state: {
444
+ tokens: Array.isArray(state.tokens) ? state.tokens : [],
445
+ lastLeadModel: Array.isArray(state.lastLeadModel) ? state.lastLeadModel : [],
446
+ },
447
+ });
448
+ };
449
+ const onMessage = (message, handle) => {
450
+ if (!message || typeof message !== 'object') return;
451
+ if (message.t === HANDOFF_MESSAGE) {
452
+ first = { ...message, httpHandle: handle };
453
+ maybe();
454
+ return;
455
+ }
456
+ if (message.t === HANDOFF_NATIVE_MESSAGE) {
457
+ nativeHandle = handle;
458
+ return;
459
+ }
460
+ if (message.t === HANDOFF_STATE_MESSAGE) {
461
+ state = message;
462
+ maybe();
463
+ }
464
+ };
465
+ timer = setTimeout(() => {
466
+ if (settled) return;
467
+ settled = true;
468
+ cleanup();
469
+ const err = new Error('handoff inheritance timed out');
470
+ err.code = HANDOFF_INHERITANCE_TIMEOUT_CODE;
471
+ reject(err);
472
+ }, _positiveIntOr(timeoutMs, HANDOFF_READY_DEADLINE_MS));
473
+ process.on('message', onMessage);
474
+ });
475
+ }
476
+
477
+ module.exports = {
478
+ HANDOFF_READY_DEADLINE_MS,
479
+ DRAIN_DEADLINE_MS,
480
+ // §5 D-8: the proxy's own idle window, and the two functions that implement it. `noteAdminContact` is
481
+ // the ONLY writer of the contact clock; oauth-proxy.js calls it from one place, after the secret check.
482
+ PROXY_LINGER_MS,
483
+ armLinger,
484
+ noteAdminContact,
485
+ lastAdminContactAt,
486
+ _lingerArmedForTest,
487
+ HANDOFF_INHERITANCE_TIMEOUT_CODE,
488
+ initiateHandoff,
489
+ receiveInheritance,
490
+ announceInheritedStart,
491
+ startDrain,
492
+ isDraining,
493
+ _positiveIntOr,
494
+ };
@@ -112,6 +112,29 @@ function forgetToken(token) {
112
112
  _lastLeadModel.delete(token);
113
113
  }
114
114
 
115
+ /** §9 S-10 (WP-2 handoff): the memory-only per-token model memory, as IPC-safe rows. */
116
+ function exportLastLeadModel() {
117
+ return [..._lastLeadModel.entries()];
118
+ }
119
+
120
+ /**
121
+ * Register each exported pair verbatim (§9 S-10). A malformed row is skipped rather than thrown on:
122
+ * one unreadable pair must not keep a successor from serving the pairs that are readable.
123
+ * @returns {number} how many pairs were registered
124
+ */
125
+ function importLastLeadModel(rows) {
126
+ if (!Array.isArray(rows)) return 0;
127
+ let imported = 0;
128
+ for (const row of rows) {
129
+ if (!Array.isArray(row) || row.length !== 2) continue;
130
+ const [token, model] = row;
131
+ if (!_isNonEmptyString(token) || !_isNonEmptyString(model)) continue;
132
+ _lastLeadModel.set(token, model);
133
+ imported += 1;
134
+ }
135
+ return imported;
136
+ }
137
+
115
138
  /**
116
139
  * Resolve a `walle-worker` model request against this lead session's pick table (§5 D-6/D-7).
117
140
  * @returns {{swapped:false} | {swapped:true, alias:string, model:string, source:'pick'|'last_lead_model'} | {error:'subagent_pick_unset'}}
@@ -146,5 +169,7 @@ module.exports = {
146
169
  noteLeadModel,
147
170
  lastLeadModel,
148
171
  forgetToken,
172
+ exportLastLeadModel,
173
+ importLastLeadModel,
149
174
  resolveWorkerAlias,
150
175
  };
@@ -95,6 +95,9 @@ function _peerCertificateFingerprint(socket) {
95
95
 
96
96
  function _deny(res, status, code) {
97
97
  if (res.writableEnded || res.headersSent) return;
98
+ // §9 S-9: one line, the finite reason code only — never the certificate, header value or path that
99
+ // caused the refusal, and never the body this connection never got to send.
100
+ console.log(`[model-proxy] refused status=${status} reason=${code}`);
98
101
  res.writeHead(status, { 'content-type': 'application/json' });
99
102
  res.end(JSON.stringify({ type: 'error', error: { type: 'authentication_error', code, message: code } }));
100
103
  }
@@ -111,7 +114,7 @@ function _deny(res, status, code) {
111
114
  * @param {Function} input.onAuthorizedRequest the host callback into the shared routing pipeline,
112
115
  * invoked as `(req, res, {token, binding, suffix, query, fingerprint})` after authorization
113
116
  */
114
- function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthorizedRequest } = {}) {
117
+ function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthorizedRequest, streamAccounting, inheritedHandle = null } = {}) {
115
118
  // Static refusals, before anything can bind. A port whose successor is not a TCP port is refused
116
119
  // rather than renumbered (D-8: a collision fails this transport explicitly; it never strands an
117
120
  // adopted host on a port nobody recorded).
@@ -253,6 +256,9 @@ function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthoriz
253
256
  server.on('tlsClientError', (_err, socket) => {
254
257
  try { socket.destroy(); } catch { /* expected: the failed handshake socket is already unusable. */ }
255
258
  });
259
+ // §5 D-9: this lane's requests are counted by the SAME counter as the plaintext lane when the host
260
+ // supplies one; a caller that wires none counts none.
261
+ if (streamAccounting) streamAccounting.track(server);
256
262
  server.on('request', (req, res) => {
257
263
  _handleRequest(req, res).catch(() => {
258
264
  if (!res.headersSent && !res.writableEnded) _deny(res, 500, 'native_lead_request_failed');
@@ -274,7 +280,11 @@ function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthoriz
274
280
  server.once('error', onError);
275
281
  server.once('listening', onListening);
276
282
  try {
277
- server.listen(nativePort, LOOPBACK_ADDRESS);
283
+ // §5 D-7 step 4 / S-6: a handoff successor binds the TLS LISTENING SOCKET its predecessor passed
284
+ // over IPC, so the port never has a gap and no second bind can race an EADDRINUSE. Without a
285
+ // handle this is today's lazy port bind, byte for byte.
286
+ if (inheritedHandle) server.listen(inheritedHandle);
287
+ else server.listen(nativePort, LOOPBACK_ADDRESS);
278
288
  } catch {
279
289
  onError();
280
290
  }
@@ -318,6 +328,29 @@ function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthoriz
318
328
  _admissionClosed = true;
319
329
  }
320
330
 
331
+ /**
332
+ * §5 D-7 step 6 / S-6: stop ACCEPTING on this listener without touching a single accepted connection.
333
+ * This is the handoff's half of the stop-accepting/stop-serving split (P-3): the predecessor is on its
334
+ * way out, so it must not take another connection, and every mTLS request already in flight finishes
335
+ * on the socket it arrived on. Idempotent, and a listener that never became ready resolves at once.
336
+ *
337
+ * The listening handle is released SYNCHRONOUSLY by `close()`; close()'s own callback fires only once
338
+ * every open connection has ended, which is exactly what this function must not wait for.
339
+ * @returns {Promise<void>}
340
+ */
341
+ function stopAccepting() {
342
+ _admissionClosed = true;
343
+ const server = _server;
344
+ if (!server) return Promise.resolve();
345
+ try {
346
+ server.close();
347
+ } catch (err) {
348
+ // expected: a server that is already closed owns nothing to release; the refusal flag above stands.
349
+ void err;
350
+ }
351
+ return Promise.resolve();
352
+ }
353
+
321
354
  async function _joinInit() {
322
355
  if (!_readyPromise) return;
323
356
  try {
@@ -364,6 +397,9 @@ function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthoriz
364
397
  ensureReady,
365
398
  close,
366
399
  closeHttpAdmission,
400
+ stopAccepting,
401
+ /** §5 D-7 step 4 / S-6: the LIVE server, for `child.send(msg, listener.server())`. Null until ready. */
402
+ server: () => _server,
367
403
  nativePort,
368
404
  baseUrl: `https://${LOOPBACK_ADDRESS}:${nativePort}/native-lead/v1`,
369
405
  isReady: () => _server !== null,
@@ -372,6 +408,12 @@ function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthoriz
372
408
  };
373
409
  }
374
410
 
411
+ // The un-wrapped close is remembered ON the server (never in a module global: two servers can be alive
412
+ // in one process) so a LATER wrap can replace the behaviour instead of stacking on top of it. The
413
+ // handoff drain (§5 D-7 step 6) installs its wrap after start() already installed the normal-mode one,
414
+ // and a stacked wrap would run the destroying close underneath the non-destroying stop.
415
+ const kOriginalServerClose = Symbol('ctmHttpServerOriginalClose');
416
+
375
417
  /**
376
418
  * Wrap an HTTP server's `close` so the native listener starts closing IMMEDIATELY and the caller's
377
419
  * callback fires only once BOTH joins completed.
@@ -383,15 +425,27 @@ function createNativeLeadListener({ httpPort, dataDir, loadAuthority, onAuthoriz
383
425
  * controller first removes that ordering hazard; the callback still waits for both, and `close()`
384
426
  * returns the server, exactly as Node's does.
385
427
  *
428
+ * `drain:false` (every caller before WP-2, and every caller that passes no options) is today's
429
+ * behaviour byte for byte: admission closed, sockets destroyed. `drain:true` is the handoff's
430
+ * stop-ACCEPTING half — the listener releases its listening socket and every accepted connection keeps
431
+ * serving; the predecessor exits once the counter reaches zero, or at the drain deadline (§5 D-7 step 7).
432
+ *
386
433
  * @param {import('http').Server} server
387
- * @param {{close:Function, closeHttpAdmission:Function}} listener
434
+ * @param {{close:Function, closeHttpAdmission:Function, stopAccepting?:Function}} listener
435
+ * @param {{drain?:boolean}} [options]
388
436
  * @returns {import('http').Server} the same server
389
437
  */
390
- function wrapHttpServerClose(server, listener) {
391
- const originalClose = server.close;
438
+ function wrapHttpServerClose(server, listener, { drain = false } = {}) {
439
+ const originalClose = server[kOriginalServerClose] || server.close;
440
+ server[kOriginalServerClose] = originalClose;
392
441
  server.close = function close(callback) {
393
- listener.closeHttpAdmission();
394
- const nativeDone = listener.close();
442
+ let nativeDone;
443
+ if (drain && typeof listener.stopAccepting === 'function') {
444
+ nativeDone = listener.stopAccepting();
445
+ } else {
446
+ listener.closeHttpAdmission();
447
+ nativeDone = listener.close();
448
+ }
395
449
  const httpDone = new Promise((resolve) => {
396
450
  try {
397
451
  originalClose.call(this, () => resolve());