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
@@ -4,7 +4,8 @@ const path = require('path');
4
4
  const { captureWorkerProgress } = require('./worker-progress');
5
5
  const { classifyFailure } = require('../worker-failure-classify');
6
6
  const { terminalBudgetFailure, budgetEvidence } = require('../model-proxy/request-budget');
7
- const { enqueueNotice } = require('./worker-notices');
7
+ const { enqueueNotice, markWorkerLive } = require('./worker-notices');
8
+ const { startWaitWatcher } = require('./worker-wait-watcher');
8
9
 
9
10
  const PROGRESS_CACHE_MS = 5000;
10
11
  const EVENT_RECEIPT_LIMIT = 32;
@@ -13,6 +14,10 @@ const NO_WRITE_ADVISORY_TURN_THRESHOLD = 20;
13
14
  const NO_WRITE_TERMINAL_TURN_THRESHOLD = 40;
14
15
  const EVENT_PRECEDENCE = { started: 0, progress: 1, advisory: 2, terminal: 3 };
15
16
  const WORKER_FINAL_RESULT_CAP_BYTES = 64 * 1024;
17
+ // Lead-cost routing round 2 D-3 (§6.2): the ONE report-text byte cap, shared by the status reader
18
+ // (delegate-worker.js counts citations from the bounded text) and the ?include_report=1 reader
19
+ // (lib/api/sessions.js). Same 64 KiB as the final-result cap.
20
+ const WORKER_REPORT_TEXT_CAP_BYTES = 64 * 1024;
16
21
  const FILE_PROBE_TTL_MS = 2000;
17
22
  const THROTTLED_WINDOW_MS = 60000;
18
23
 
@@ -89,10 +94,17 @@ function _removeWaiterRegistration(waiter, ctmSessionId) {
89
94
  if (!set.size) _waiters.delete(ctmSessionId);
90
95
  }
91
96
  const _liveWaiters = new Set(); // every not-yet-resolved waiter, once each (waiterCount counts waiters, not registrations)
92
- function _finishWaiter(waiter, why, workerId) {
97
+ function _finishWaiter(waiter, why, workerId, detail) {
93
98
  if (waiter.done) return;
94
99
  waiter.done = true;
95
100
  clearTimeout(waiter.timer);
101
+ // WP-1 (D-3): a resolved waiter stops its watcher first, so a late model reply is dropped
102
+ // (stop() marks the watcher dead) and no interval timer outlives the wait.
103
+ if (waiter.watch) {
104
+ try { waiter.watch.stop(); }
105
+ catch { /* expected: a watcher stop failure never blocks the resolve */ }
106
+ waiter.watch = null;
107
+ }
96
108
  for (const id of waiter.watchedIds) _removeWaiterRegistration(waiter, id);
97
109
  _liveWaiters.delete(waiter);
98
110
  if (waiter.onHumanSubmit) {
@@ -100,9 +112,16 @@ function _finishWaiter(waiter, why, workerId) {
100
112
  humanInputEvents.removeListener('humanSubmit', waiter.onHumanSubmit);
101
113
  waiter.onHumanSubmit = null;
102
114
  }
103
- waiter.resolve({ why, workerId: workerId != null ? workerId : null });
115
+ const result = { why, workerId: workerId != null ? workerId : null };
116
+ // WP-1: the watcher's own result fields exist only on a waiter that ran one -- a caller with
117
+ // no watcher sees exactly today's `{ why, workerId }` shape.
118
+ if (waiter.decisions) {
119
+ result.detail = detail || null;
120
+ result.decisions = waiter.decisions;
121
+ }
122
+ waiter.resolve(result);
104
123
  }
105
- function waitForWorkers({ workerIds, leadId, waitMs, generationById } = {}) {
124
+ function waitForWorkers({ workerIds, leadId, waitMs, generationById, watchMode, callModel, getStatus, readTail, workerRoute } = {}) {
106
125
  const ids = Array.isArray(workerIds) ? workerIds.map((id) => String(id || '')).filter(Boolean) : [];
107
126
  if (!ids.length) {
108
127
  // No watched workers: the wait is still meaningful for a lead (a user_input wake with no
@@ -111,7 +130,7 @@ function waitForWorkers({ workerIds, leadId, waitMs, generationById } = {}) {
111
130
  const empty = { generationById: {} };
112
131
  return waitForWorkersWithListener([], leadId, waitMs, empty);
113
132
  }
114
- return waitForWorkersWithListener(ids, leadId, waitMs, { generationById });
133
+ return waitForWorkersWithListener(ids, leadId, waitMs, { generationById, watchMode, callModel, getStatus, readTail, workerRoute });
115
134
  }
116
135
  function waitForWorkersWithListener(ids, leadId, waitMs, state) {
117
136
  let release = () => {};
@@ -124,7 +143,7 @@ function waitForWorkersWithListener(ids, leadId, waitMs, state) {
124
143
  done: false,
125
144
  resolve,
126
145
  };
127
- const finish = (why, workerId) => _finishWaiter(waiter, why, workerId);
146
+ const finish = (why, workerId, detail) => _finishWaiter(waiter, why, workerId, detail);
128
147
  waiter.finish = finish;
129
148
  waiter.timer = setTimeout(() => finish('timeout'), Math.max(0, Number(waitMs) || 0));
130
149
  if (typeof waiter.timer.unref === 'function') waiter.timer.unref();
@@ -142,12 +161,64 @@ function waitForWorkersWithListener(ids, leadId, waitMs, state) {
142
161
  humanInputEvents.on('humanSubmit', waiter.onHumanSubmit);
143
162
  }
144
163
  release = () => finish('timeout');
164
+ // WP-1 (D-3/D-4): the watcher parks BESIDE the waiter -- one tick per watched worker, the
165
+ // worker's own pinned model, and a `watcher`/`watcher_unavailable` wake through the same
166
+ // finish gate as every code wake. It is started only when the caller wired one (a caller
167
+ // with no watchMode/callModel keeps today's behaviour untouched).
168
+ if (ids.length && state.watchMode !== 'off' && typeof state.callModel === 'function') {
169
+ waiter.decisions = [];
170
+ waiter.watch = startWaitWatcher({
171
+ workerIds: ids,
172
+ mode: state.watchMode || 'on',
173
+ wake: (why, detail) => finish(why, detail && detail.worker_id, detail),
174
+ onDecision: (d) => waiter.decisions.push(d),
175
+ callModel: state.callModel,
176
+ getStatus: state.getStatus,
177
+ readTail: state.readTail,
178
+ workerRoute: state.workerRoute,
179
+ }) || null;
180
+ }
145
181
  // F1: a waiter registered before the caller knows a worker's current generation (undefined
146
182
  // wakes on any generation notice) is narrowed per worker once the first status read lands.
147
183
  setGeneration = (workerId, value) => { if (!waiter.done) waiter.generationByCtmId.set(String(workerId), value); };
148
184
  });
149
185
  return { promise, release, setGeneration };
150
186
  }
187
+
188
+ // WP-1 (D-3): the watcher's PRODUCTION model call. It rides the lane every other CTM
189
+ // background completion uses (lib/background-llm.js -> Wall-E's background-completion
190
+ // endpoint), so the resolved provider, the fallback chain and the circuit breakers stay the
191
+ // routing authority's and CTM builds no provider client of its own. The pin is the caller's
192
+ // `model` (the worker row's own route_alias + ':' + model, never a second route).
193
+ // A failure -- transport, an unroutable pin (route_throttled / route_unavailable /
194
+ // model_not_found), or a non-2xx decision -- answers `{ ok: false, error }`, which is exactly
195
+ // what the watcher's D-4 consecutive-failure counter reads.
196
+ function createWaitWatcherCallModel() {
197
+ return async function callWaitWatcherModel({ feature, model, system, messages, maxTokens, timeoutMs, sessionId }) {
198
+ let llm;
199
+ try {
200
+ llm = require('../background-llm');
201
+ // D-4: the watcher never takes a second route. background-llm's last-ditch Ollama hop
202
+ // fires on a Wall-E transport error unless the fallback setting is off, so this call
203
+ // carries an env copy with it off -- a transport failure is then `{ ok: false }` and the
204
+ // consecutive-failure counter ends the watcher with `watcher_unavailable`.
205
+ const env = { ...process.env, CTM_BACKGROUND_LLM_FALLBACK_PROVIDER: 'off' };
206
+ // WP-4a (D-9): the tick's `sessionId` is the watched worker's ctm_session_id (WP-1's
207
+ // per-worker tick). background-llm.js forwards it into the POST body only when it is a
208
+ // non-empty string, and Wall-E's ledger then keys the row by it -- so the cell's watcher
209
+ // spend is exactly the ledger rows of that cell's own workers.
210
+ const reply = await llm.callBackgroundLlm(messages, {
211
+ task: feature, model, system, maxTokens, timeoutMs, env,
212
+ ...(typeof sessionId === 'string' && sessionId ? { sessionId } : {}),
213
+ });
214
+ const text = typeof reply?.text === 'string' ? reply.text : llm.extractBackgroundLlmText(reply);
215
+ return { ok: true, text };
216
+ } catch (error) {
217
+ return { ok: false, error: String((error && (error.code || error.message)) || 'watch_call_failed').slice(0, 200) };
218
+ }
219
+ };
220
+ }
221
+
151
222
  // D-7: a route waiter is the same record shape in the same `_waiters` registry, under the key
152
223
  // `route:<alias>`. Timer = min(waitMs, remaining hold) when the snapshot has a horizon, else waitMs.
153
224
  // The registration/wake checks read snapshot() (never admit()) so a wait is never counted as a refusal.
@@ -634,6 +705,10 @@ function createWorkerObservability({ getDeps, readWorkerRuntime, getBaseStatus }
634
705
  try { enqueueNotice(receipt); } catch { /* expected: the lead's hook notice is best-effort; walle_worker_wait and the sentinel still report */ }
635
706
  _wakeWaiters(ctmSessionId, 'terminal');
636
707
  auditScopeAtTerminal(ctmSessionId).catch((error) => reportObservationFailure(deps, ctmSessionId, error));
708
+ } else if (kind === 'started') {
709
+ // WP-0 (D-2): the live-worker index is maintained beside the notice queue from the same
710
+ // receipts. Memory only, so a failure here can never affect the receipt's persistence.
711
+ try { markWorkerLive(receipt); } catch { /* expected: the live index is best-effort; the notice queue and walle_worker_wait still report */ }
637
712
  }
638
713
  return receipt;
639
714
  }
@@ -911,8 +986,22 @@ function withIncompleteStreams(runtime, requestBudget) {
911
986
  return { ...requestBudget, incomplete_streams: count, incomplete_stream_ms: ms };
912
987
  }
913
988
 
989
+ // D-1 (session-stops-followups-2026-10-24.html): the terminal statuses a LIVE payload can carry.
990
+ // The DB-row readers in lib/api/sessions.js keep their own copy (_WORKER_TERMINAL_STATUSES):
991
+ // a row's terminal_status is written only at a real exit, so it can never be provisional.
992
+ const WORKER_TERMINAL_STATUSES = new Set(['report_ready', 'failed', 'exited']);
993
+
994
+ /** D-1: a status payload is final when it is terminal AND not a report written while the process
995
+ * is still alive (delegate-worker.js report_provisional). An absent field is final, so DB rows
996
+ * and fixtures keep today's meaning.
997
+ * @param {{status?: string, report_provisional?: boolean}|null|undefined} status
998
+ * @returns {boolean} */
999
+ function isFinalWorkerStatus(status) {
1000
+ return !!status && WORKER_TERMINAL_STATUSES.has(status.status) && status.report_provisional !== true;
1001
+ }
1002
+
914
1003
  function pollAfterMs({ status, requestBudget } = {}) {
915
- if (['report_ready', 'failed', 'exited'].includes(status?.status)
1004
+ if (isFinalWorkerStatus(status)
916
1005
  || (status?.exitCode !== null && status?.exitCode !== undefined)) return 0;
917
1006
  const p50 = requestBudget?.requestDurationP50Ms;
918
1007
  if (!Number.isInteger(p50) || p50 <= 0) return 30000;
@@ -921,6 +1010,8 @@ function pollAfterMs({ status, requestBudget } = {}) {
921
1010
 
922
1011
  module.exports = {
923
1012
  waitForWorkerChange, waitForWorkerChangeHandle, waitForWorkers, notifyWorkerGeneration, notifyWorkerAdvisory, waiterCount, waitForRouteClear, _resetRouteSubscriptionsForTest,
1013
+ createWaitWatcherCallModel,
1014
+ isFinalWorkerStatus,
924
1015
  pollAfterMs,
925
1016
  withIncompleteStreams,
926
1017
  createWorkerObservability,
@@ -940,6 +1031,7 @@ module.exports = {
940
1031
  computeExitFailure,
941
1032
  boundedWorkerText,
942
1033
  isColdDeferredSession,
1034
+ WORKER_REPORT_TEXT_CAP_BYTES,
943
1035
  THROTTLED_WINDOW_MS,
944
1036
  EVENT_RECEIPT_LIMIT,
945
1037
  NO_WRITE_ADVISORY_TURN_THRESHOLD,
@@ -65,9 +65,17 @@ async function stopWorkerSession(ctmSessionId, { mode = 'after_turn', reason = '
65
65
  return refuse('worker_not_owned', 403, id, { message: 'Only the lead that created this worker can stop it.', next_step: nextStepFor({ code: 'worker_not_owned' }) });
66
66
  }
67
67
 
68
- // 4. idempotent on a terminal worker
69
- if (row.terminal_at || TERMINAL_STATUSES.includes(String(row.terminal_status || ''))) {
70
- return refuse('already_terminal', 200, id, { terminal_status: row.terminal_status || '', generation: Number(row.generation || 1), status: 'terminal' });
68
+ // WP-2 (worker-terminal-stamp-durable 2026-10-26 D-2/D-3): a dead process with an unstamped row is repaired first,
69
+ // so the verb below answers the truth (already_terminal), never 'stopping' on a process that no longer exists.
70
+ let repaired = null;
71
+ if (typeof helpers.repairExitedWorkerRow === 'function') {
72
+ try { repaired = helpers.repairExitedWorkerRow(id) || null; } catch { repaired = null; /* expected: a repair that throws leaves the row as read; the stop proceeds on the row's own truth */ }
73
+ if (repaired) { try { row = deps.dbModule.getWorkerSession(id) || row; } catch { /* expected: keep the row already read */ } }
74
+ }
75
+
76
+ // 4. idempotent on a terminal worker -- or on a repaired one whose stamp is still riding the WP-1 retry (D-3: never 'stopping' for a dead process)
77
+ if (repaired || row.terminal_at || TERMINAL_STATUSES.includes(String(row.terminal_status || ''))) {
78
+ return refuse('already_terminal', 200, id, { terminal_status: row.terminal_status || '', generation: Number(row.generation || 1), status: 'terminal', repaired });
71
79
  }
72
80
 
73
81
  // 5. the stop record: journaled by the proxy for a proxied worker, on the row for a proxyless one
@@ -0,0 +1,315 @@
1
+ 'use strict';
2
+
3
+ // WP-1 (worker-wait-watcher, D-3/D-4): while a lead's `walle_worker_wait` is parked in CTM,
4
+ // a watcher tick asks the WORKER'S OWN pinned model whether the lead must act now -- the
5
+ // worker is stuck or looping, repeats the same error, asks a question, or works outside its
6
+ // brief. A `WAKE_LEAD` answer wakes the parked wait early with `woke_on: 'watcher'`.
7
+ //
8
+ // The watcher never delays a plain-code wake: it only ever calls `wake` from its own tick, so
9
+ // terminal / generation / advisory / user_input / route_clear / timeout resolve exactly as
10
+ // today, and a code wake that lands during an in-flight model call wins (stop() marks the
11
+ // watcher dead; the late reply is dropped).
12
+ //
13
+ // Two consecutive failures (transport error, a reply with `ok: false`, a timeout, or a reply
14
+ // that carries no decision) end the watcher with `watcher_unavailable` and the lead is back to
15
+ // today's behaviour -- no retry loop, no second route (D-4).
16
+ //
17
+ // The module holds NO network import: the caller injects `callModel` (production wiring lives
18
+ // in worker-observability.js) and the worker's pin is always the worker row's own
19
+ // `route_alias` + ':' + `model` -- no vendor or model id appears here.
20
+
21
+ const { redactSecrets } = require('../redact-secrets');
22
+
23
+ const WATCH_INTERVAL_MS = 30000; // today's lead cadence
24
+ const WATCH_TAIL_CHARS = 60000; // pilot watcher input size
25
+ const WATCH_TIMEOUT_MS = 20000; // background-completion DEFAULT_TIMEOUT_MS (wall-e/http/background-completion-api.js:27)
26
+ const WATCH_MAX_CONSECUTIVE_FAILURES = 2; // D-4
27
+ const WATCH_MAX_CONCURRENT_CALLS = 4;
28
+ const WATCH_SILENT_EVERY = 10;
29
+ const WATCH_MODE_ON = 'on';
30
+ const WATCH_MODE_SILENT = 'silent';
31
+ const WATCH_MODE_OFF = 'off';
32
+ const WATCH_MODES = new Set([WATCH_MODE_ON, WATCH_MODE_SILENT, WATCH_MODE_OFF]);
33
+ const WATCH_FEATURE = 'lead-wait-watch';
34
+ const WATCH_MAX_TOKENS = 300;
35
+ const WATCH_SYSTEM = 'You are a watcher working for a lead coding agent. The lead is waiting on a delegated worker. '
36
+ + 'Read the worker status and the end of its transcript and decide whether the lead must act now (the worker is stuck '
37
+ + 'or looping, repeats the same error, asks a question or needs a decision, works outside its brief, or anything else '
38
+ + 'needs the lead) or whether it is progressing normally and the lead should keep waiting.\n'
39
+ + 'Answer with one JSON object only: {"decision":"KEEP_WAITING"|"WAKE_LEAD","reason":"<= 25 words"}';
40
+
41
+ // The reply contract is a REGEX, not JSON.parse: a model that wraps its object in prose still
42
+ // answers, and a reply with no decision at all is a failure (D-3/D-4).
43
+ const WATCH_DECISION_RE = /"decision"\s*:\s*"(KEEP_WAITING|WAKE_LEAD)"/;
44
+ const WATCH_REASON_RE = /"reason"\s*:\s*"([^"]{0,200})"/;
45
+
46
+ // D-3: at most WATCH_MAX_CONCURRENT_CALLS model calls in flight across ALL parked waiters. A
47
+ // tick that cannot get a slot is skipped, never queued.
48
+ let _inFlightCalls = 0;
49
+ function _tryAcquireSlot() {
50
+ if (_inFlightCalls >= WATCH_MAX_CONCURRENT_CALLS) return false;
51
+ _inFlightCalls += 1;
52
+ return true;
53
+ }
54
+ function _releaseSlot() {
55
+ if (_inFlightCalls > 0) _inFlightCalls -= 1;
56
+ }
57
+
58
+ // Test-only bookkeeping: every ARMED interval timer is tracked here (a fired timer removes
59
+ // itself at fire time), so _resetForTests() can report how many watchers were still ticking
60
+ // when everything was expected to be stopped.
61
+ let _liveTimers = new Set();
62
+
63
+ function parseWatchReply(text) {
64
+ if (typeof text !== 'string' || !text) return null;
65
+ const match = WATCH_DECISION_RE.exec(text);
66
+ if (!match) return null;
67
+ const reason = WATCH_REASON_RE.exec(text);
68
+ return { decision: match[1], reason: reason ? reason[1].trim() : '' };
69
+ }
70
+
71
+ // One compact status per tick: fixed keys, missing ones null, everything bounded to what the
72
+ // watcher prompt needs (never the whole payload).
73
+ function compactWorkerStatus(status) {
74
+ const source = status && typeof status === 'object' ? status : {};
75
+ const budget = source.request_budget && typeof source.request_budget === 'object' ? source.request_budget : null;
76
+ return {
77
+ status: typeof source.status === 'string' ? source.status : null,
78
+ turns: Number.isInteger(source.turns) ? source.turns : null,
79
+ last_activity_at: Number.isFinite(source.last_activity_at) ? source.last_activity_at : null,
80
+ request_budget: budget ? {
81
+ requestCount: Number.isInteger(budget.requestCount) ? budget.requestCount : null,
82
+ completedToolCalls: Number.isInteger(budget.completedToolCalls) ? budget.completedToolCalls : null,
83
+ incompleteStreams: Number.isInteger(budget.incomplete_streams) ? budget.incomplete_streams : (Number.isInteger(budget.incompleteStreams) ? budget.incompleteStreams : null),
84
+ } : null,
85
+ advisories: Array.isArray(source.advisories) ? source.advisories.slice(0, 8) : null,
86
+ repeated_error: source.repeated_error ?? null,
87
+ observation_repeats: Number.isInteger(source.observation_repeats) ? source.observation_repeats : null,
88
+ };
89
+ }
90
+
91
+ function errorText(value) {
92
+ if (!value) return 'watch_failed';
93
+ if (typeof value === 'string') return value.slice(0, 200);
94
+ return String(value.code || value.message || value).slice(0, 200);
95
+ }
96
+
97
+ // The worker's own pin: the row's route_alias + ':' + model, or null when the row carries no
98
+ // pin (the lead keeps today's behaviour, D-3).
99
+ function workerPin(workerRoute, workerId) {
100
+ if (typeof workerRoute !== 'function') return null;
101
+ let row = null;
102
+ try { row = workerRoute(workerId); }
103
+ catch { return null; /* expected: an unreadable row carries no pin -- the lead keeps today's behaviour */ }
104
+ if (!row || typeof row !== 'object') return null;
105
+ const route = typeof row.route === 'string' ? row.route.trim() : '';
106
+ const model = typeof row.model === 'string' ? row.model.trim() : '';
107
+ if (!route || !model) return null;
108
+ return `${route}:${model}`;
109
+ }
110
+
111
+ /**
112
+ * Start the per-waiter watcher. Returns { stop() } or null when nothing is watched.
113
+ *
114
+ * @param {object} o
115
+ * @param {string[]} o.workerIds
116
+ * @param {'on'|'silent'|'off'} [o.mode='on']
117
+ * @param {(why:'watcher'|'watcher_unavailable', detail:{worker_id:string, reason?:string, error?:string}) => void} o.wake
118
+ * @param {(d:{worker_id:string, at:number, decision:'KEEP_WAITING'|'WAKE_LEAD', silent:boolean, reason?:string}) => void} [o.onDecision]
119
+ * @param {(id:string) => Promise<object|null>|object|null} o.getStatus
120
+ * @param {(id:string, maxChars:number) => Promise<string>} o.readTail
121
+ * @param {(req:{feature:string, model:string, system:string, messages:object[], maxTokens:number, timeoutMs:number, sessionId:string}) => Promise<{ok:boolean, text?:string, error?:string}>} o.callModel
122
+ * @param {(id:string) => {route:string, model:string}|null} o.workerRoute
123
+ * @param {Function} [o.setTimer=setTimeout]
124
+ * @param {Function} [o.clearTimer=clearTimeout]
125
+ * @param {Function} [o.now=Date.now]
126
+ * @returns {{ stop(): void }|null}
127
+ */
128
+ function startWaitWatcher({
129
+ workerIds, mode = WATCH_MODE_ON, wake, onDecision, getStatus, readTail, callModel, workerRoute,
130
+ setTimer = setTimeout, clearTimer = clearTimeout, now = Date.now,
131
+ } = {}) {
132
+ if (!WATCH_MODES.has(mode) || mode === WATCH_MODE_OFF) return null;
133
+ if (typeof wake !== 'function' || typeof getStatus !== 'function' || typeof readTail !== 'function' || typeof callModel !== 'function') return null;
134
+ const silent = mode === WATCH_MODE_SILENT;
135
+ const targets = [];
136
+ for (const rawId of Array.isArray(workerIds) ? workerIds : []) {
137
+ const id = String(rawId || '');
138
+ if (!id) continue;
139
+ const pinnedModel = workerPin(workerRoute, id);
140
+ if (!pinnedModel) continue; // D-3: a worker with no route_alias/model row starts no watcher
141
+ targets.push({ id, pinnedModel });
142
+ }
143
+ if (!targets.length) return null;
144
+
145
+ const state = { dead: false, timer: null, ticks: 0, failures: new Map() };
146
+
147
+ function clearOwnTimer() {
148
+ if (state.timer == null) return;
149
+ clearTimer(state.timer);
150
+ _liveTimers.delete(state.timer);
151
+ state.timer = null;
152
+ }
153
+
154
+ function stop() {
155
+ if (state.dead) return;
156
+ state.dead = true; // a reply that lands after this is dropped
157
+ clearOwnTimer();
158
+ }
159
+
160
+ function recordFailure(target, error) {
161
+ if (state.dead) return;
162
+ const consecutive = (state.failures.get(target.id) || 0) + 1;
163
+ state.failures.set(target.id, consecutive);
164
+ if (consecutive < WATCH_MAX_CONSECUTIVE_FAILURES) return;
165
+ state.failures.set(target.id, 0);
166
+ wake('watcher_unavailable', { worker_id: target.id, error: errorText(error) });
167
+ }
168
+
169
+ // The call is bounded by WATCH_TIMEOUT_MS here as well as by the request's own timeoutMs: a
170
+ // wedged lane must resolve to watcher_unavailable, not park the tick forever.
171
+ async function callWithTimeout(target, status, tail) {
172
+ const request = {
173
+ feature: WATCH_FEATURE,
174
+ model: target.pinnedModel,
175
+ system: WATCH_SYSTEM,
176
+ messages: [{ role: 'user', content: `${JSON.stringify(compactWorkerStatus(status))}\n\nTranscript tail:\n${tail}` }],
177
+ maxTokens: WATCH_MAX_TOKENS,
178
+ timeoutMs: WATCH_TIMEOUT_MS,
179
+ // WP-4a (D-9): the ledger attribution key is the WATCHED WORKER's ctm_session_id. Each tick
180
+ // is per worker and every wait-watch outcome row already stores that worker as `worker_id`
181
+ // (WP-1), so the cell's spend is exactly the ledger rows of its own workers -- never the
182
+ // lead's (a lead parks several workers at once; the lead's id would blend their cells).
183
+ sessionId: target.id,
184
+ };
185
+ const call = Promise.resolve().then(() => callModel(request));
186
+ call.catch(() => { /* expected: the watchdog may settle first; a late rejection is never unhandled */ });
187
+ let watchdogId = null;
188
+ const watchdog = new Promise((_resolve, reject) => {
189
+ watchdogId = setTimer(() => reject(new Error('watch_timeout')), WATCH_TIMEOUT_MS);
190
+ if (watchdogId && typeof watchdogId.unref === 'function') watchdogId.unref();
191
+ });
192
+ try {
193
+ return await Promise.race([call, watchdog]);
194
+ } finally {
195
+ if (watchdogId != null) {
196
+ clearTimer(watchdogId);
197
+ watchdogId = null;
198
+ }
199
+ }
200
+ }
201
+
202
+ async function runWorkerTick(target) {
203
+ if (state.dead) return;
204
+ if (!_tryAcquireSlot()) return; // skipped, never queued
205
+ try {
206
+ let reply;
207
+ try {
208
+ const status = await getStatus(target.id);
209
+ if (state.dead) return;
210
+ const tail = redactSecrets(await readTail(target.id, WATCH_TAIL_CHARS));
211
+ if (state.dead) return;
212
+ reply = await callWithTimeout(target, status, tail);
213
+ } catch (error) {
214
+ recordFailure(target, error);
215
+ return;
216
+ }
217
+ if (state.dead) return;
218
+ if (!reply || reply.ok !== true) {
219
+ recordFailure(target, reply && reply.error);
220
+ return;
221
+ }
222
+ const parsed = parseWatchReply(reply.text);
223
+ if (!parsed) {
224
+ recordFailure(target, 'watch_reply_unparsed');
225
+ return;
226
+ }
227
+ state.failures.set(target.id, 0);
228
+ const decision = {
229
+ worker_id: target.id, at: now(), decision: parsed.decision, silent, reason: parsed.reason,
230
+ };
231
+ if (typeof onDecision === 'function') {
232
+ try { onDecision(decision); }
233
+ catch { /* expected: a decision sink failure never breaks the watcher */ }
234
+ }
235
+ // `silent` records its decisions and never wakes (D-3).
236
+ if (!silent && parsed.decision === 'WAKE_LEAD') wake('watcher', { worker_id: target.id, reason: parsed.reason });
237
+ } finally {
238
+ _releaseSlot();
239
+ }
240
+ }
241
+
242
+ function schedule() {
243
+ if (state.dead) return;
244
+ const id = setTimer(() => {
245
+ state.timer = null;
246
+ _liveTimers.delete(id);
247
+ tickCycle().catch(() => { /* expected: a tick never throws out; this is the last-resort net */ });
248
+ }, WATCH_INTERVAL_MS);
249
+ state.timer = id;
250
+ _liveTimers.add(id);
251
+ if (id && typeof id.unref === 'function') id.unref();
252
+ }
253
+
254
+ async function tickCycle() {
255
+ try {
256
+ if (state.dead) return;
257
+ state.ticks += 1;
258
+ // silent runs only every WATCH_SILENT_EVERY-th tick; every other tick is a no-op.
259
+ if (!silent || state.ticks % WATCH_SILENT_EVERY === 0) {
260
+ await Promise.all(targets.map((target) => runWorkerTick(target).catch(() => { /* expected: per-worker isolation; failures are counted inside */ })));
261
+ }
262
+ } finally {
263
+ schedule();
264
+ }
265
+ }
266
+
267
+ schedule();
268
+ return { stop };
269
+ }
270
+
271
+ // Production readTail: the worker's own provider transcript, tail-first, clipped to
272
+ // WATCH_TAIL_CHARS characters (read through lib/transcript-source.js -- the only module
273
+ // allowed to open a provider transcript).
274
+ function createWaitWatcherReadTail({ dbModule } = {}) {
275
+ return async function readTail(workerId, maxChars = WATCH_TAIL_CHARS) {
276
+ const db = dbModule || require('../../db');
277
+ const agentRow = typeof db?.getAgentSessionByCtmId === 'function' ? db.getAgentSessionByCtmId(workerId) : null;
278
+ const transcriptPath = String((agentRow && agentRow.jsonl_path) || '');
279
+ if (!transcriptPath) return '';
280
+ const transcriptSource = require('../transcript-source');
281
+ const st = await transcriptSource.statTranscript(transcriptPath);
282
+ if (!st || st.exists === false) return '';
283
+ const size = Number(st.size) || 0;
284
+ const start = Math.max(0, size - maxChars * 4);
285
+ const buf = await transcriptSource.readTranscriptRange(transcriptPath, start, size);
286
+ return buf.toString('utf8').slice(-maxChars);
287
+ };
288
+ }
289
+
290
+ // Test-only: clear every armed interval timer and the module-level semaphore, returning how
291
+ // many watchers were still ticking (0 when every watcher was stopped properly).
292
+ function _resetForTests() {
293
+ const cleared = _liveTimers.size;
294
+ for (const id of _liveTimers) clearTimeout(id);
295
+ _liveTimers = new Set();
296
+ _inFlightCalls = 0;
297
+ return cleared;
298
+ }
299
+
300
+ module.exports = {
301
+ startWaitWatcher,
302
+ createWaitWatcherReadTail,
303
+ parseWatchReply,
304
+ compactWorkerStatus,
305
+ WATCH_MODES,
306
+ WATCH_FEATURE,
307
+ WATCH_INTERVAL_MS,
308
+ WATCH_TAIL_CHARS,
309
+ WATCH_TIMEOUT_MS,
310
+ WATCH_MAX_CONSECUTIVE_FAILURES,
311
+ WATCH_MAX_CONCURRENT_CALLS,
312
+ WATCH_SILENT_EVERY,
313
+ WATCH_SYSTEM,
314
+ _resetForTests,
315
+ };
@@ -118,8 +118,11 @@ function handleSessionGeometryClaim(ws, msg, replayOpts = {}) {
118
118
  let committedGrid = { cols: 0, rows: 0 };
119
119
  let queuedDueAt = 0;
120
120
  let broadcastRolesAfterAck = false;
121
+ // WP-2: the diagnostic below runs even when sanitizeGeometryClaim threw (or the claim never
122
+ // reached arbitration), so the sanitized claim is hoisted out of the try to be readable there.
123
+ let claim = null;
121
124
  try {
122
- const claim = sanitizeGeometryClaim(msg, Number(ws.socketGeneration || 0));
125
+ claim = sanitizeGeometryClaim(msg, Number(ws.socketGeneration || 0));
123
126
  sessionId = claim.id;
124
127
  session = getSession(sessionId);
125
128
  let currentLease = _geometryLease(session, sessionId);
@@ -270,6 +273,12 @@ function handleSessionGeometryClaim(ws, msg, replayOpts = {}) {
270
273
  _recordTerminalGeometryClaimDiagnostic(session, sessionId, ws.clientId, {
271
274
  disposition, reason, intent: ['reconnect', 'activation', 'pointer', 'keyboard', 'split-focus', 'revalidate', 'selection'].includes(msg?.intent) ? msg.intent : 'invalid',
272
275
  ownerGeneration, measurementId, cols: committedGrid.cols, rows: committedGrid.rows, viewerRegistered,
276
+ // WP-2 (docs/codex-terminal-geometry-sticky-cols-2026-10-24.html §5): `cols` is the
277
+ // COMMITTED grid (pty truth); it is initialised from `session._ptyCols` and only moves on an
278
+ // accepted commit, so a queued/rejected claim for 196 against a 94-col pty logged `cols: 94`
279
+ // with no record of what the client asked for -- the exact hole in the live 94-cols incident.
280
+ claimCols: Math.max(0, Number(claim?.measurement?.cols) || 0),
281
+ claimRows: Math.max(0, Number(claim?.measurement?.rows) || 0),
273
282
  });
274
283
  _sendTerminalGeometryClaimAckV2(ws, {
275
284
  id: sessionId, socketGeneration: Number(ws.socketGeneration || 0), measurementId, expectedOwnerGeneration,