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,306 @@
1
+ 'use strict';
2
+
3
+ // WP-2 (docs/remote-session-image-upload-forwarding-2026-10-24.html §5 D-1..D-10, §6, §8
4
+ // AC-2a..AC-2f, §9 S-1/S-10) — the hub-side BYTE-FAITHFUL session request forwarder.
5
+ //
6
+ // The hub forwards only `POST /api/sessions/:id/(park|revive|pin|wake)` today
7
+ // (lib/remote-machine-catalog.js's forwardLifecycleRequest). The image-draft family reaches the
8
+ // hub's own handler, and for a MIRROR session that handler can never succeed: the hub does not hold
9
+ // the session in its map, so lib/terminal-draft-owner.js throws a 503
10
+ // CTM_OWNERSHIP_UNAVAILABLE that the browser uploader retries forever (public/js/ctm-uploader.js
11
+ // treats 503 as retryable). This module is the seam that lets that family be answered by the BOX
12
+ // that owns the session — the bytes must land on the PTY host, because the CLI opens the delivered
13
+ // draft path on its own filesystem (§1 "Where the bytes must live").
14
+ //
15
+ // It is deliberately NOT a general reverse proxy, and it is NOT wired into any route here (WP-3
16
+ // adds the one dispatch line in api-prompts.js):
17
+ //
18
+ // D-1 ownership is decided ONCE, by `catalog.getMirrorSessionMachine` — local sessions Map
19
+ // first (the 2026-10-23 relay-hijack fix), then the forwarded creates, the live catalog and
20
+ // the sync mirror. A local id, or an id no registered machine claims, MISSES and the caller
21
+ // runs its local handler with the request stream untouched (byte-identical local path).
22
+ // D-2 a mirror id never reaches the hub's draft owner: this module either proxies it or refuses
23
+ // with the same 503 `machine_unreachable` body forwardLifecycleRequest uses.
24
+ // D-3 the forwarded set is a CLOSED list (FORWARDED_SESSION_ROUTES); every member has a
25
+ // matching `peerControl` row on the box (WP-1). A prefix-forward would 403 on every route
26
+ // nobody flagged (lib/auth-rules.js pins the peer allowlist EXACTLY).
27
+ // D-4 outbound headers are the peer credential plus an allowlist. The inbound `cookie` and
28
+ // `authorization` are NEVER forwarded, and the hub's `token` query parameter is stripped:
29
+ // the box's getAuthToken (server.js:11524) prefers `?token=` over the Bearer, so forwarding
30
+ // it verbatim would both leak a hub credential and 401 the request.
31
+ // D-5 bytes stream both ways with NO hub-side cap: `Readable.toWeb(req)` + `duplex:'half'` out,
32
+ // `pipeline(Readable.fromWeb(response.body), res)` back. The box's own 25 MiB limit
33
+ // (api-prompts.js) is the only limit — an intermediate cap reproduces JupyterHub #2831.
34
+ // D-6 honest status: registered but unreachable, no row, no credential, a connect error, or the
35
+ // headers deadline → 503 `machine_unreachable` naming the machine, with `Retry-After: 2`.
36
+ // Any status the box answered (409/413/503 included) goes through VERBATIM.
37
+ // D-7 the headers deadline is 30 s (= public/js/ctm-uploader.js's attemptTimeoutMs), so the hub
38
+ // never outlives the browser's attempt; a client that goes away aborts the upstream fetch.
39
+ // D-8 the hub never retries: a streamed body is single-use and the client uploader already owns
40
+ // retry with an idempotent `request_id`.
41
+ //
42
+ // Dependency note: this module must NOT require lib/hosted-sync-worker.js (dependency-cycle lint —
43
+ // see lib/remote-machine-proxy.js:30-33). The two requires below are leaves on the way in:
44
+ // lib/remote-machine-catalog.js is a dependency SINK and lib/remote-machine-create.js loads its own
45
+ // heavy peers lazily inside `_productionDeps()`.
46
+
47
+ const { Readable } = require('node:stream');
48
+ const { pipeline } = require('node:stream/promises');
49
+
50
+ const catalogDefault = require('./remote-machine-catalog');
51
+ const { buildPeerControlHeaders: buildPeerControlHeadersDefault } = require('./remote-machine-create');
52
+
53
+ // D-7: equal to public/js/ctm-uploader.js's attemptTimeoutMs — hub and browser agree on how long a
54
+ // single upload attempt may live.
55
+ const FORWARD_DEADLINE_MS = 30_000;
56
+
57
+ // D-4: the hub's own device token and the picker's routing param belong to the FIRST hop. Everything
58
+ // else in the query is the box's business (`provider`, `ordinal`, `request_id`, `filename`, `mode`,
59
+ // `v`, the content route's cache buster).
60
+ const STRIPPED_QUERY_PARAMS = Object.freeze(['token', 'machine']);
61
+
62
+ // D-4: request headers copied from the browser — never `cookie`, never the inbound `authorization`.
63
+ const FORWARDED_REQUEST_HEADERS = Object.freeze(['content-type', 'content-length', 'if-match', 'x-ctm-upload-request-id']);
64
+
65
+ // D-5: response headers copied from the box. `retry-after` is here because the box's own 503 must
66
+ // keep its own retry hint; everything else (set-cookie, server, date) stays at the hop that owns it.
67
+ const FORWARDED_RESPONSE_HEADERS = Object.freeze(['content-type', 'content-length', 'etag', 'cache-control', 'retry-after', 'x-content-type-options']);
68
+
69
+ // D-3: the closed forwarded set. Each entry is [METHOD, RegExp on pathname]; it mirrors the ten
70
+ // image-draft peer-control routes on the box (lib/auth-rules.js:164-179) exactly, and the WP-1
71
+ // pairing meta-test (tests/peer-control-auth.test.js §7) fails when a member here has no flagged
72
+ // row on the box. A member added here without that flag turns the hub's honest 503 into the box's
73
+ // 403 insufficient_scope (the incident-2/incident-3 class).
74
+ const FORWARDED_SESSION_ROUTES = Object.freeze([
75
+ ['GET', /^\/api\/sessions\/[^/]+\/image-drafts$/],
76
+ ['GET', /^\/api\/sessions\/[^/]+\/image-drafts\/[^/]+\/content$/],
77
+ ['POST', /^\/api\/sessions\/[^/]+\/image-drafts$/],
78
+ ['PUT', /^\/api\/sessions\/[^/]+\/image-drafts\/[^/]+$/],
79
+ ['DELETE', /^\/api\/sessions\/[^/]+\/image-drafts$/],
80
+ ['DELETE', /^\/api\/sessions\/[^/]+\/image-drafts\/[^/]+$/],
81
+ ['POST', /^\/api\/sessions\/[^/]+\/image-drafts\/[^/]+\/delivery$/],
82
+ ['POST', /^\/api\/sessions\/[^/]+\/image-drafts\/[^/]+\/freeze$/],
83
+ ['POST', /^\/api\/sessions\/[^/]+\/image-drafts\/[^/]+\/submit$/],
84
+ ['POST', /^\/api\/sessions\/[^/]+\/image-drafts\/[^/]+\/retry$/],
85
+ ]);
86
+
87
+ function _string(value, max = 200) {
88
+ return String(value == null ? '' : value).trim().slice(0, max);
89
+ }
90
+
91
+ /** D-3 as a predicate: is (method, pathname) one of the forwarded routes? Purely lexical. */
92
+ function isForwardedSessionRoute(method, pathname) {
93
+ const verb = _string(method, 12).toUpperCase();
94
+ const target = String(pathname == null ? '' : pathname);
95
+ if (!verb || !target) return false;
96
+ return FORWARDED_SESSION_ROUTES.some(([routeMethod, pattern]) => routeMethod === verb && pattern.test(target));
97
+ }
98
+
99
+ /**
100
+ * D-1 as a SYNCHRONOUS predicate for the dispatch line: is this a forwarded route for a MIRROR id?
101
+ * The caller uses it to keep the local path byte-identical — a local id calls its handler in the same
102
+ * tick, so the handler's body listeners attach before any `aborted`/`data` event can be missed.
103
+ */
104
+ function isMirrorSessionRequest(req, url, sessionId, deps = {}) {
105
+ if (!isForwardedSessionRoute(req && req.method, url && url.pathname)) return false;
106
+ const catalog = (deps && deps.catalog) || catalogDefault;
107
+ if (typeof catalog.getMirrorSessionMachine !== 'function') return false;
108
+ const mirror = catalog.getMirrorSessionMachine(sessionId, deps || {});
109
+ return !!(mirror && mirror.machineInstanceId);
110
+ }
111
+
112
+ /** An allowlist copy of `headers` (values verbatim; a repeated header is comma-joined). */
113
+ function _pickHeaders(headers, names) {
114
+ const out = {};
115
+ if (!headers) return out;
116
+ for (const name of names) {
117
+ const value = headers[name];
118
+ if (value === undefined || value === null) continue;
119
+ out[name] = Array.isArray(value) ? value.join(', ') : String(value);
120
+ }
121
+ return out;
122
+ }
123
+
124
+ /** The same allowlist over a fetch `Headers` object (or a plain object, for an injected fetch). */
125
+ function _pickResponseHeaders(headers) {
126
+ const out = {};
127
+ if (!headers) return out;
128
+ for (const name of FORWARDED_RESPONSE_HEADERS) {
129
+ const value = typeof headers.get === 'function' ? headers.get(name) : headers[name];
130
+ if (value === undefined || value === null || value === '') continue;
131
+ out[name] = String(value);
132
+ }
133
+ return out;
134
+ }
135
+
136
+ /**
137
+ * D-6: the one refusal shape — the same body forwardLifecycleRequest writes, so the client renders
138
+ * one message for both forwarders, plus `Retry-After: 2` (the reader may retry; the box is down, not
139
+ * the request) and an optional machine-readable `detail`.
140
+ *
141
+ * @returns {true} always — the request was answered here (never falls through to a local handler).
142
+ */
143
+ function _refuse(res, mirror, machineName, detail) {
144
+ const label = _string(machineName, 200) || _string(mirror && mirror.machineInstanceId, 160) || 'machine';
145
+ const payload = {
146
+ ok: false,
147
+ code: 'machine_unreachable',
148
+ error: `Machine ${label} is unreachable`,
149
+ machine: _string(mirror && mirror.machineInstanceId, 160),
150
+ machineName: _string(machineName, 200),
151
+ ...(detail ? { detail } : {}),
152
+ };
153
+ const body = JSON.stringify(payload);
154
+ // A client that already hung up (the abort path) has a destroyed res: writing to it would only
155
+ // raise a second, uninteresting error on the way out.
156
+ if (!res.headersSent && !res.writableEnded && !res.destroyed) {
157
+ res.writeHead(503, {
158
+ 'Content-Type': 'application/json',
159
+ 'Content-Length': Buffer.byteLength(body),
160
+ 'Retry-After': '2',
161
+ });
162
+ res.end(body);
163
+ }
164
+ return true;
165
+ }
166
+
167
+ /**
168
+ * D-1/D-2 entry point. Returns `false` (the caller runs its local handler with the request stream
169
+ * untouched) unless `sessionId` is a MIRROR id AND `(req.method, url.pathname)` is in
170
+ * FORWARDED_SESSION_ROUTES. Otherwise it fully answers `res` and resolves `true`.
171
+ *
172
+ * Never throws, and never reads `req` before it has decided (a non-forwarded route leaves
173
+ * `req.readableFlowing === null`).
174
+ *
175
+ * @param {import('http').IncomingMessage} req
176
+ * @param {import('http').ServerResponse} res
177
+ * @param {URL} url
178
+ * @param {string} sessionId already decoded
179
+ * @param {{catalog?, buildPeerControlHeaders?, fetchImpl?, env?, deadlineMs?, setTimeoutFn?,
180
+ * clearTimeoutFn?}} [deps]
181
+ * @returns {Promise<boolean>}
182
+ */
183
+ async function maybeForwardSessionRequest(req, res, url, sessionId, deps = {}) {
184
+ const d = deps || {};
185
+ // 1. A route this module does not own is never even looked up — the request stream stays virgin.
186
+ if (!isForwardedSessionRoute(req && req.method, url && url.pathname)) return false;
187
+
188
+ // 2. D-1: the ONE ownership decision (local Map first). A local or unclaimed id falls through.
189
+ const catalog = d.catalog || catalogDefault;
190
+ const mirror = typeof catalog.getMirrorSessionMachine === 'function'
191
+ ? catalog.getMirrorSessionMachine(sessionId, d)
192
+ : null;
193
+ if (!mirror || !mirror.machineInstanceId) return false;
194
+
195
+ // 3. D-6: a registered machine with no usable row, or one known to be down, is refused here —
196
+ // never acted on locally.
197
+ const machine = typeof catalog.machineRowForMirror === 'function' ? catalog.machineRowForMirror(mirror, d) : null;
198
+ const machineName = _string(machine ? machine.displayName : mirror.machineName, 200);
199
+ if (!machine) return _refuse(res, mirror, machineName, 'machine_row_unavailable');
200
+ if (_string(machine.health, 40) === 'unreachable') return _refuse(res, mirror, machineName, 'machine_unreachable');
201
+
202
+ // 4. D-4: the peer credential is the ONLY authorization on the wire. No credential configured
203
+ // means this hub cannot be a peer of that box — refuse rather than send an unauthenticated
204
+ // request that would come back as an opaque 401.
205
+ const buildHeaders = typeof d.buildPeerControlHeaders === 'function'
206
+ ? d.buildPeerControlHeaders
207
+ : buildPeerControlHeadersDefault;
208
+ const peerHeaders = buildHeaders(d.env || process.env);
209
+ if (!peerHeaders) return _refuse(res, mirror, machineName, 'peer_credential_missing');
210
+
211
+ const fetchImpl = typeof d.fetchImpl === 'function'
212
+ ? d.fetchImpl
213
+ : (typeof d.fetch === 'function' ? d.fetch : globalThis.fetch);
214
+ if (typeof fetchImpl !== 'function') return _refuse(res, mirror, machineName, 'fetch_unavailable');
215
+
216
+ // 5. D-4: the same path and query, minus what belongs to the first hop.
217
+ let target;
218
+ try {
219
+ target = new URL(`${(url && url.pathname) || ''}${(url && url.search) || ''}`, _string(machine.url, 1000));
220
+ for (const key of STRIPPED_QUERY_PARAMS) target.searchParams.delete(key);
221
+ } catch {
222
+ return _refuse(res, mirror, machineName, 'machine_url_invalid'); // expected: a malformed registry url is a refusal, not a throw
223
+ }
224
+
225
+ // 6/7. D-7: the abort path. Two independent signals, because Node emits the REQUEST's `close` on
226
+ // natural completion too (a fully received POST body closes long before the box answers) — so the
227
+ // request's own close only counts while the message is INCOMPLETE, and the response's close only
228
+ // counts while nothing has been written yet. Between them, exactly one shape aborts: the client
229
+ // went away with the answer still owed to it.
230
+ const controller = new AbortController();
231
+ let headersArrived = false;
232
+ const abortIfClientGone = () => {
233
+ if (!headersArrived && !res.writableEnded) controller.abort();
234
+ };
235
+ res.once('close', abortIfClientGone);
236
+ req.once('close', () => { if (!req.complete) abortIfClientGone(); });
237
+
238
+ const timeoutFn = typeof d.setTimeoutFn === 'function' ? d.setTimeoutFn : setTimeout;
239
+ const clearFn = typeof d.clearTimeoutFn === 'function' ? d.clearTimeoutFn : clearTimeout;
240
+ const deadlineMs = Number(d.deadlineMs) > 0 ? Number(d.deadlineMs) : FORWARD_DEADLINE_MS;
241
+ let timedOut = false;
242
+ const timer = timeoutFn(() => {
243
+ timedOut = true;
244
+ controller.abort();
245
+ }, deadlineMs);
246
+ if (timer && typeof timer.unref === 'function') timer.unref();
247
+
248
+ const method = _string(req.method, 12).toUpperCase();
249
+ const hasBody = method !== 'GET' && method !== 'HEAD';
250
+ let response;
251
+ try {
252
+ response = await fetchImpl(target, {
253
+ method,
254
+ headers: { ...peerHeaders, ..._pickHeaders(req.headers, FORWARDED_REQUEST_HEADERS) },
255
+ redirect: 'manual',
256
+ signal: controller.signal,
257
+ // D-5: the body streams through — no buffering, no hub-side cap, single use (D-8: never retry).
258
+ ...(hasBody ? { body: Readable.toWeb(req), duplex: 'half' } : {}),
259
+ });
260
+ } catch (error) {
261
+ clearFn(timer);
262
+ // D-6: the response never started, so the honest answer is the machine_unreachable refusal. When
263
+ // the client itself is gone this write is skipped (the socket is dead) — `_refuse` owns that.
264
+ const detail = timedOut ? 'timeout' : (_string((error && (error.code || error.name)) || error, 80) || 'fetch_failed');
265
+ return _refuse(res, mirror, machineName, detail);
266
+ }
267
+ clearFn(timer);
268
+ headersArrived = true;
269
+
270
+ if (!response) return _refuse(res, mirror, machineName, 'empty_response');
271
+
272
+ // 8. Status and the response allowlist go through verbatim (D-5/D-6) — the box's own 4xx/5xx
273
+ // included: the client parses the box's answer exactly as it parses a local one.
274
+ const status = Number(response.status) || 502;
275
+ try {
276
+ res.writeHead(status, _pickResponseHeaders(response.headers));
277
+ } catch {
278
+ // A header value the runtime refuses to write (a box bug, not a hub one) must still leave the
279
+ // hook's "never throws" contract intact for its caller: refuse honestly instead of rejecting.
280
+ return _refuse(res, mirror, machineName, 'invalid_response_headers'); // expected: an unwritable header from the box
281
+ }
282
+
283
+ // 9. Stream the body back.
284
+ if (!response.body) {
285
+ res.end();
286
+ return true;
287
+ }
288
+ try {
289
+ await pipeline(Readable.fromWeb(response.body), res, { signal: controller.signal });
290
+ } catch (error) {
291
+ // The headers are already on the wire, so there is no honest status left to send: destroy the
292
+ // response (the client sees a truncated body and a network error) and let its uploader retry
293
+ // with the same idempotent request_id (D-8).
294
+ try { res.destroy(error); } catch { /* expected: a response that is already destroyed */ }
295
+ }
296
+ return true;
297
+ }
298
+
299
+ module.exports = {
300
+ FORWARDED_SESSION_ROUTES,
301
+ FORWARD_DEADLINE_MS,
302
+ STRIPPED_QUERY_PARAMS,
303
+ maybeForwardSessionRequest,
304
+ isForwardedSessionRoute,
305
+ isMirrorSessionRequest,
306
+ };
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { normalizeRecoveryAction } = require('./prompt-submission-contract');
4
+
3
5
  function createRemoteSubmissionStatusWriter(deps = {}) {
4
6
  const required = ['getDb', 'getDbPath', 'getInstanceId', 'getSession', 'updateRemoteSubmission',
5
7
  'publicSubmission', 'remoteSubmissionIdentity', 'broadcastSubmissionStatus',
@@ -67,6 +69,10 @@ function createRemoteSubmissionStatusWriter(deps = {}) {
67
69
  reason: details.reason || '',
68
70
  message: details.message || '',
69
71
  retryable: details.retryable === true,
72
+ // WP-0: persisted next to `retryable` so a reloaded row (or any later
73
+ // frame built from `row.result`) carries the same recovery verdict the
74
+ // direct reply carried. Same normalization as publicSubmissionStatus.
75
+ recoveryAction: normalizeRecoveryAction(details.recoveryAction, details.retryable === true),
70
76
  source: details.source || '',
71
77
  evidence_source: details.evidenceSource || details.evidence_source || '',
72
78
  observed_at: Date.now(),
@@ -63,16 +63,33 @@ function recordLingerEntered({ sessions, isHosted, hostIdForSession, recordSessi
63
63
  }
64
64
  }
65
65
 
66
+ // §5 D-10 / §9 S-12: the model proxy's own restart outcome, as `adoption.proxy`. The default is the
67
+ // real supervisor, required LAZILY (this module loads during boot, before any proxy exists), and a
68
+ // failure is reported and answered as null — a diagnostics row must never break the boot it describes.
69
+ function _proxyOutcomeFor(proxyOutcome, errorSink) {
70
+ try {
71
+ const outcome = (proxyOutcome || (() => require('./oauth-proxy-supervisor').lastRestartOutcome()))();
72
+ return outcome && typeof outcome === 'object' ? outcome : null;
73
+ } catch (e) {
74
+ if (errorSink && typeof errorSink.report === 'function') errorSink.report('restore:proxy-outcome', e);
75
+ return null;
76
+ }
77
+ }
78
+
66
79
  // Ledgering must never break boot: any failure is reported, not thrown.
67
- function appendRestoreCompleteLedger({ build, summary, errorSink, ledger = require('./restart-ledger') }) {
80
+ function appendRestoreCompleteLedger({ build, summary, errorSink, proxyOutcome, ledger = require('./restart-ledger') }) {
81
+ // `proxy` is nested FIRST on purpose: lib/restart-ledger.js:52-63 keeps an object's first 10 keys, and
82
+ // the adoption summary already carries more than that, so a trailing key would be silently dropped from
83
+ // the very row the design is judged on.
84
+ const adoption = () => ({ proxy: _proxyOutcomeFor(proxyOutcome, errorSink), ...summary() });
68
85
  try {
69
86
  const bootRestartId = ledger.getBootRestartId();
70
87
  if (bootRestartId) {
71
- ledger.appendRestartReceiptStage(bootRestartId, 'restore_complete', { newPid: process.pid, build, adoption: summary() });
88
+ ledger.appendRestartReceiptStage(bootRestartId, 'restore_complete', { newPid: process.pid, build, adoption: adoption() });
72
89
  } else {
73
90
  ledger.appendRestartLedgerEntry({
74
91
  method: 'launchd-respawn', requester: { source: 'launchd' }, reason: 'restore complete (no restart id)',
75
- force: false, guardVerdict: { blocked: false, blockers: [] }, build, stage: 'restore_complete', adoption: summary(),
92
+ force: false, guardVerdict: { blocked: false, blockers: [] }, build, stage: 'restore_complete', adoption: adoption(),
76
93
  });
77
94
  }
78
95
  } catch (e) { errorSink.report('restore:ledger', e); }
@@ -11,7 +11,14 @@ function aliveFn(isProcessAlive) {
11
11
 
12
12
  // `hostCodeDiffers(hostId, record)`: true when the live host's LOADED code differs from the files on
13
13
  // disk now -- the next boot will adopt it as upgrade-pending and hand it off, so predict a handoff.
14
- function computeRestartPreflight({ sessions, sessionHostManager, hostRecords, isProcessAlive, isRunning, isHosted, isExited, hostCodeDiffers } = {}) {
14
+ //
15
+ // `proxy` (§5 D-10, WP-3): what the next boot will do with the model proxy, already computed by
16
+ // `lib/oauth-proxy-supervisor.js:proxyPreflight()`. It is passed THROUGH, never recomputed here: this
17
+ // module reads session/host state, and the proxy is a different process with a different truth. A caller
18
+ // with no proxy answer gets the honest default -- 'none', with the three measurements null rather than
19
+ // guessed -- so the surface has one shape and the three session arrays below stay byte-for-byte what they
20
+ // have always been.
21
+ function computeRestartPreflight({ sessions, sessionHostManager, hostRecords, isProcessAlive, isRunning, isHosted, isExited, hostCodeDiffers, proxy = null } = {}) {
15
22
  const alive = aliveFn(isProcessAlive);
16
23
  const differsFn = typeof hostCodeDiffers === 'function' ? hostCodeDiffers : () => false;
17
24
  const differsByHost = new Map(); // one disk hash per host per preflight, not per session
@@ -52,7 +59,7 @@ function computeRestartPreflight({ sessions, sessionHostManager, hostRecords, is
52
59
  if (compat?.reason === 'upgrade-pending' || codeDiffers(String(hostId), record)) willHandoff.push(sessionId);
53
60
  else willAdopt.push(sessionId);
54
61
  }
55
- return { willAdopt, willHandoff, willRevive };
62
+ return { willAdopt, willHandoff, willRevive, proxy: proxy ?? { action: 'none', pid: null, openStreams: null, closureDiffers: null } };
56
63
  }
57
64
 
58
65
  module.exports = { computeRestartPreflight };
@@ -14,6 +14,10 @@
14
14
  // activation): allowed past the auto ceiling, but a cooldown is waited out (bounded by
15
15
  // maxCooldownMs) unless this is an unclaimed prewarm;
16
16
  // - after a wait the session must still be the live, deferred, non-closing one.
17
+ //
18
+ // WP-3 (§9 S-10/S-11): the two silent bails below are pure skips of THIS attempt -- they change no
19
+ // return value, they only report through the injected `onSkip(reason, id, session)` dep (default
20
+ // no-op, so this module stays server-free and its unit tests need no lifecycle singleton).
17
21
 
18
22
  function createAwaitRestoreBackoffOutsideGate({
19
23
  enabled,
@@ -23,11 +27,12 @@ function createAwaitRestoreBackoffOutsideGate({
23
27
  maxCooldownMs,
24
28
  now = Date.now,
25
29
  setTimer = setTimeout,
30
+ onSkip = () => {},
26
31
  }) {
27
32
  return async function awaitRestoreBackoffOutsideGate(session, opts = {}) {
28
33
  if (!enabled) return true;
29
34
  const task = session?._restoreTask || {};
30
- if (!task.session_id) return false;
35
+ if (!task.session_id) { onSkip('BackoffTaskMissingSessionId', task.session_id, session); return false; }
31
36
  const source = String(opts.source || '');
32
37
  const background = /auto-resume/.test(source);
33
38
  const decision = restoreBackoff.shouldAttempt(task.session_id, now(), { userInitiated: !background });
@@ -47,7 +52,10 @@ function createAwaitRestoreBackoffOutsideGate({
47
52
  timer?.unref?.();
48
53
  });
49
54
  const latest = getSession(task.session_id);
50
- if (latest !== session || !session._restoreDeferred || session._closing || session._exited) return false;
55
+ if (latest !== session || !session._restoreDeferred || session._closing || session._exited) {
56
+ onSkip('BackoffSessionReplaced', task.session_id, session);
57
+ return false;
58
+ }
51
59
  }
52
60
  return true;
53
61
  };
@@ -22,9 +22,13 @@ function prepareRetryJitterMs(sessionId) {
22
22
  // The coalescing "start a deferred restore on the next tick" scheduler, extracted verbatim from
23
23
  // server.js (`_scheduleDeferredRestoreStart`) so server.js stays under its file-size ratchet;
24
24
  // the retry timer below is one more caller of it (with source:'prepare-retry').
25
- function createScheduleDeferredRestoreStart({ getSession, startDeferredRestoreSession, log = console.error }) {
25
+ //
26
+ // WP-3 (§9 S-16): the bails report through the injected `onSkip(reason, id, session)` dep (default
27
+ // no-op) and change no return value -- `RetryAlreadyStarting` in particular still returns TRUE: an
28
+ // attempt is already in flight, which is a success-shaped bail, not a skip of the session.
29
+ function createScheduleDeferredRestoreStart({ getSession, startDeferredRestoreSession, log = console.error, onSkip = () => {} }) {
26
30
  return function scheduleDeferredRestoreStart(session, opts = {}) {
27
- if (!session?._restoreDeferred) return false;
31
+ if (!session?._restoreDeferred) { onSkip('RetryNotDeferred', session?.id, session); return false; }
28
32
  if (!session._deferredInputGeneration) {
29
33
  session._deferredInputGeneration = Math.max(1, Number(session._terminalLifecycleCounter || 0) + 1);
30
34
  }
@@ -32,7 +36,7 @@ function createScheduleDeferredRestoreStart({ getSession, startDeferredRestoreSe
32
36
  ...(session._restoreStartOpts || {}),
33
37
  ...(opts || {}),
34
38
  };
35
- if (session._restoreStarting || session._restoreStartTimer) return true;
39
+ if (session._restoreStarting || session._restoreStartTimer) { onSkip('RetryAlreadyStarting', session.id, session); return true; }
36
40
  session._restoreStartTimer = setTimeout(() => {
37
41
  session._restoreStartTimer = null;
38
42
  const latest = getSession(session.id);
@@ -53,19 +57,22 @@ function scheduleDeferredRestoreRetry(session, params, deps) {
53
57
  const {
54
58
  getSession, recordSessionDiagnostic, scheduleStart,
55
59
  setTimer = setTimeout, clearTimer = clearTimeout, jitterMs = prepareRetryJitterMs,
60
+ onSkip = () => {},
56
61
  } = deps;
57
- if (!session?._restoreDeferred || session._closing || session._exited) return false;
62
+ if (!session?._restoreDeferred || session._closing || session._exited) { onSkip('RetryNotDeferred', session?.id, session); return false; }
58
63
  const source = String(opts.source || '');
59
64
  const openClients = (session.clients || []).filter((client) => client?.readyState === 1).length;
60
65
  // No open client and a background origin: nothing is waiting to see the retry, and a
61
66
  // background drain will pick this session up again on its own cadence -- don't arm a timer
62
67
  // that will just fire into an empty room. An 'attach'-sourced failure (or any other
63
68
  // user-initiated source) still schedules: the client may reconnect before the timer fires.
64
- if (openClients === 0 && /auto-resume|prewarm/.test(source)) return false;
69
+ if (openClients === 0 && /auto-resume|prewarm/.test(source)) { onSkip('RetryNoClients', session.id, session); return false; }
65
70
  const delayMs = Math.max(1500, Number(retryAfterMs) || 0) + jitterMs(session.id);
66
71
  if (session._prepareRetryTimer) clearTimer(session._prepareRetryTimer);
67
72
  session._prepareRetryTimer = setTimer(() => {
68
73
  session._prepareRetryTimer = null;
74
+ // WP-3 (§9 S-16): the fire-time guard records NOTHING -- it is an idempotent re-arm guard
75
+ // (something else already started / this session is gone), not a skip predicate.
69
76
  const latest = getSession(session.id);
70
77
  if (!latest?._restoreDeferred || latest._restoreStarting || latest._closing || latest._exited) return;
71
78
  scheduleStart(latest, { ...opts, source: 'prepare-retry' });
@@ -51,6 +51,12 @@
51
51
  // now () => number injectable clock for tests
52
52
  // isDrainPaused () => boolean optional; true pauses the whole drain (WI-7(d))
53
53
  // getTranscriptSize (id) => number optional; sorts one batch's window ascending (WI-8)
54
+ // onSkip (reason, id, session) => void optional; WP-3 (§9 S-18) skip reporting
55
+ //
56
+ // WP-3 (§9 S-18) accounting, zero control-flow change: the requeues of a still-resumable `false`
57
+ // start are counted as a plain NUMBER on the gap metadata (`requeues`) rather than a D-8 reason --
58
+ // the vocabulary is about causes, this is mechanics. A paused tick reports `onSkip('DrainPaused')`
59
+ // once per pause TRANSITION, not per tick, so a long pause cannot flood the lifecycle event ring.
54
60
  function drainDeferredInBatches(deps) {
55
61
  const {
56
62
  ids,
@@ -66,6 +72,7 @@ function drainDeferredInBatches(deps) {
66
72
  onComplete,
67
73
  isDrainPaused,
68
74
  getTranscriptSize,
75
+ onSkip = () => {},
69
76
  } = deps || {};
70
77
  const size = Math.max(1, Math.floor(Number(batchSize)) || 1);
71
78
  const budgetMs = Math.max(0, Math.floor(Number(batchBudgetMs)) || 0);
@@ -75,6 +82,11 @@ function drainDeferredInBatches(deps) {
75
82
  // front (its original relative position is preserved by the filter below).
76
83
  let remaining = Array.isArray(ids) ? ids.slice() : [];
77
84
  const totalCount = remaining.length;
85
+ // WP-3 (§9 S-18): `false` starts that stayed resumable (the id goes back to the front of the
86
+ // queue and is re-attempted after the normal gap) and the pause-transition latch. Neither
87
+ // changes what is started, requeued, broadcast or when the drain completes.
88
+ let requeues = 0;
89
+ let pauseReported = false;
78
90
 
79
91
  async function tick() {
80
92
  if (!remaining.length) {
@@ -82,15 +94,18 @@ function drainDeferredInBatches(deps) {
82
94
  return;
83
95
  }
84
96
  if (typeof isDrainPaused === 'function' && isDrainPaused()) {
97
+ if (!pauseReported) { pauseReported = true; onSkip('DrainPaused', '', null); }
85
98
  scheduleGap(tick, {
86
99
  phase: 'paused',
87
100
  resumedInBatch: 0,
88
101
  elapsedMs: 0,
89
102
  budgetMs,
90
103
  remaining: remaining.length,
104
+ requeues,
91
105
  });
92
106
  return;
93
107
  }
108
+ pauseReported = false;
94
109
  const batchWindow = remaining.slice(0, size);
95
110
  const ordered = typeof getTranscriptSize === 'function'
96
111
  ? batchWindow.slice().sort((a, b) => Number(getTranscriptSize(a) || 0) - Number(getTranscriptSize(b) || 0))
@@ -112,6 +127,11 @@ function drainDeferredInBatches(deps) {
112
127
  if (started !== false || !stillResumable) {
113
128
  processed.add(id);
114
129
  resumedInBatch++;
130
+ } else {
131
+ // WP-3 (§9 S-18): a refused start that is still resumable stays at the front of the
132
+ // queue and will be re-attempted after the gap. Counted, never capped -- the cap
133
+ // decision needs this counter's live data first (design D-9).
134
+ requeues++;
115
135
  }
116
136
  } catch (e) {
117
137
  if (typeof onError === 'function') onError(id, e);
@@ -145,12 +165,13 @@ function drainDeferredInBatches(deps) {
145
165
  elapsedMs,
146
166
  budgetMs,
147
167
  remaining: remaining.length,
168
+ requeues,
148
169
  });
149
170
  }
150
171
 
151
172
  // First tick is scheduled through the same gap (the original kicked off with
152
173
  // setTimeout(resumeNext, gap) after the identity-cache prebuild settled).
153
- scheduleGap(tick, { phase: 'initial', resumedInBatch: 0, elapsedMs: 0, budgetMs, remaining: totalCount });
174
+ scheduleGap(tick, { phase: 'initial', resumedInBatch: 0, elapsedMs: 0, budgetMs, remaining: totalCount, requeues });
154
175
  }
155
176
 
156
177
  module.exports = { drainDeferredInBatches };
@@ -50,6 +50,54 @@ function deriveTranscriptStateFromInfo(info) {
50
50
  });
51
51
  }
52
52
 
53
+ // The ladder fields every resume-blocked message for a missing transcript carries (S-33).
54
+ // WP-0 moved this out of server.js unchanged (same body, same precedence); server.js imports it.
55
+ function resumeBlockedRungWire(info) {
56
+ return {
57
+ resumeRung: info?.resumeRung || '', coverage: info?.coverage || null, offerStoreFirst: info?.offerStoreFirst === true, restoreError: info?.restoreError || '',
58
+ deterministic: info?.deterministic === true, origin: info?.origin || 'unknown',
59
+ storeTurns: normalizeStoreTurns(info?.storeTurns), checkedAt: info?.checkedAt || '',
60
+ };
61
+ }
62
+
63
+ // WP-1 (session-exit-close-truth-2026-10-24.html §5 D-1/D-2, §6): the ONE chooser for the block a
64
+ // deferred exit carries. Both exit handlers defer (keep the startup row); this decides whether the
65
+ // defer may also claim a failure, and WHICH failure, from the exit evidence alone.
66
+ // - A resume that never became ready is a RESUME failure, whatever the exit code says -- checked
67
+ // FIRST, so it is never swallowed by the unknown-exit rule below (S-11 rule 1 beats rule 2).
68
+ // - An unknown exit (`exitSource === 'eof'` + a null code, a handed-off PTY macOS cannot reap) is
69
+ // NOT a crash verdict (D-1): the defer carries no block, so the session presents as exited and
70
+ // the next input resumes it. "Recovery was exhausted" would be a lie -- no restart ran.
71
+ // - `session_crash_loop` ("automatic recovery was exhausted") needs BOTH a known abnormal exit
72
+ // (a finite non-zero code, or a signal) AND a restart that actually ran (D-2).
73
+ // - Anything else known-abnormal is a plain crash with no recovery claim.
74
+ // Returns the block to stamp on a defer, or null when the defer must carry no block.
75
+ function deferBlockForExit({ exitPersistence, latestResumeExit, exitCode, signal, autoRestartCount, label }) {
76
+ const resumeExit = latestResumeExit || {};
77
+ if (resumeExit.failedBeforeReady) {
78
+ return { code: resumeExit.code || 'resume_failed', message: `${label} did not become ready while resuming.` };
79
+ }
80
+ if (exitPersistence?.reason === 'unknown-exit') return null;
81
+ const abnormalExit = (Number.isFinite(exitCode) && exitCode !== 0) || !!signal;
82
+ if (abnormalExit && Number(autoRestartCount) > 0) {
83
+ return { code: 'session_crash_loop', message: `${label} exited unexpectedly and automatic recovery was exhausted.` };
84
+ }
85
+ return { code: 'session_crashed', message: `${label} exited unexpectedly.` };
86
+ }
87
+
88
+ // WP-0 (D-8): the ONE diagnostic payload every resume-block site writes, so a card the user saw can
89
+ // always be attributed to the code path that set it. `site` is always a literal at the call, never
90
+ // derived from the record, because the whole point is to name the caller.
91
+ function resumeBlockSetDetails(site, record) {
92
+ return {
93
+ site: String(site || 'unspecified'),
94
+ code: String(record?.code || ''),
95
+ deterministic: record?.deterministic === true,
96
+ storeHuman: Number(record?.storeTurns?.human) || 0,
97
+ rung: String(record?.resumeRung || ''),
98
+ };
99
+ }
100
+
53
101
  function resumeBlockedWire(sessionId, info, { agentType = '', rungWire = null } = {}) {
54
102
  const i = info || {};
55
103
  return {
@@ -71,11 +119,24 @@ function resumeBlockedWire(sessionId, info, { agentType = '', rungWire = null }
71
119
  lastSeenAt: i.lastSeenAt || '',
72
120
  trashPath: i.trashPath || '',
73
121
  originalPath: i.originalPath || '',
122
+ // WP-3 (D-13): WHY the copy is in CTM's trash and WHEN it went there — recorded in the trash
123
+ // manifest, so the card can say what actually happened instead of guessing. Empty on servers
124
+ // (or rows) that predate the manifest keys.
125
+ trashReason: String(i.trashReason || ''),
126
+ trashedAt: String(i.trashedAt || ''),
74
127
  // WP-2 (multi-machine R-15): the three facts the resume-blocked card needs to say WHERE the transcript
75
128
  // is instead of only that it is missing. Additive keys; every field above keeps its value.
76
129
  transcriptState: deriveTranscriptStateFromInfo(i),
77
130
  originHostname: String(i.originHostname || ''),
78
131
  originIsPeer: i.originIsPeer === true,
132
+ // WP-6 (S-8, AC-6d): the tri-state above is PRESERVED exactly (`on_other_machine` is still the
133
+ // state — "the transcript is over there" is true); this ADDITIVE field is what makes the card
134
+ // actionable: when the origin instance is a registered, reachable machine, the client renders
135
+ // "Open on <displayName>" and posts the resume to the hub, which forwards it to that box. The
136
+ // key is absent (not null) when there is no such machine, so today's payload is byte-identical.
137
+ ...(i.openOnMachine && i.openOnMachine.instanceId
138
+ ? { openOnMachine: { instanceId: String(i.openOnMachine.instanceId), displayName: String(i.openOnMachine.displayName || '') } }
139
+ : {}),
79
140
  ...(rungWire || {}),
80
141
  };
81
142
  }
@@ -95,4 +156,4 @@ function sendResumeBlockedToClients(clients, payload, send) {
95
156
  return sent;
96
157
  }
97
158
 
98
- module.exports = { normalizeStoreTurns, normalizeLocalHistory, deriveTranscriptState, resumeBlockedWire, sendResumeBlockedToClients };
159
+ module.exports = { normalizeStoreTurns, normalizeLocalHistory, deriveTranscriptState, resumeBlockedRungWire, resumeBlockSetDetails, resumeBlockedWire, sendResumeBlockedToClients, deferBlockForExit };