create-walle 0.10.1 → 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 (341) hide show
  1. package/README.md +4 -0
  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 +6 -4
  6. package/package.json +1 -1
  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/claude-hook.js +7 -3
  11. package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
  12. package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
  13. package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
  14. package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
  15. package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
  16. package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
  17. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  18. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  19. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  20. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  21. package/template/coding-task-manager/db.js +705 -286
  22. package/template/coding-task-manager/git-utils.js +44 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
  24. package/template/coding-task-manager/lib/agent-presets.js +15 -0
  25. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  26. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  27. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  28. package/template/coding-task-manager/lib/api/ingress.js +35 -12
  29. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  30. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  31. package/template/coding-task-manager/lib/api/machines.js +461 -0
  32. package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
  33. package/template/coding-task-manager/lib/api/providers.js +53 -6
  34. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  35. package/template/coding-task-manager/lib/api/sessions.js +160 -8
  36. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  37. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  38. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  39. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  40. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  41. package/template/coding-task-manager/lib/auth-context.js +32 -2
  42. package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
  43. package/template/coding-task-manager/lib/auth-rules.js +186 -45
  44. package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
  45. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  46. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  47. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  48. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  49. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  50. package/template/coding-task-manager/lib/client-diag-handler.js +5 -0
  51. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  52. package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
  53. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  54. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  55. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  56. package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
  57. package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
  58. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  59. package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
  60. package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
  61. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  62. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  63. package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
  64. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  65. package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
  66. package/template/coding-task-manager/lib/db-domains.js +21 -2
  67. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  68. package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
  69. package/template/coding-task-manager/lib/feature-manifest.js +1 -1
  70. package/template/coding-task-manager/lib/fs-watcher.js +3 -2
  71. package/template/coding-task-manager/lib/harness-history-transition.js +3 -3
  72. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  73. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  74. package/template/coding-task-manager/lib/headless-term-service.js +55 -17
  75. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  76. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  77. package/template/coding-task-manager/lib/host-handoff.js +51 -13
  78. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  79. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  80. package/template/coding-task-manager/lib/host-stats.js +328 -0
  81. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  82. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  83. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  84. package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
  85. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
  86. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  87. package/template/coding-task-manager/lib/instance-manifest.js +13 -1
  88. package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
  89. package/template/coding-task-manager/lib/launch-model-override.js +160 -1
  90. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  91. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
  92. package/template/coding-task-manager/lib/lead-model-switch.js +3 -2
  93. package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
  94. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  95. package/template/coding-task-manager/lib/login-tripwire.js +1 -0
  96. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  97. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  98. package/template/coding-task-manager/lib/machine-registry.js +749 -0
  99. package/template/coding-task-manager/lib/machine-self.js +66 -0
  100. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +52 -4
  101. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  102. package/template/coding-task-manager/lib/media-journal.js +84 -27
  103. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  104. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  105. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  106. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  107. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  108. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  109. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  110. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  111. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  112. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  113. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  114. package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
  115. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  116. package/template/coding-task-manager/lib/native-history-control.js +2 -2
  117. package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
  118. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  119. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
  120. package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
  121. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  122. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  123. package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
  124. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  125. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  126. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  127. package/template/coding-task-manager/lib/read-pool-client.js +5 -2
  128. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  129. package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
  130. package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
  131. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  132. package/template/coding-task-manager/lib/remote-command-runtime.js +13 -1
  133. package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
  134. package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
  135. package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
  136. package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
  137. package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
  138. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  139. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  140. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  141. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  142. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  143. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  144. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  145. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  146. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  147. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  148. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  149. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  150. package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
  151. package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
  152. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  153. package/template/coding-task-manager/lib/root-prompt-admission.js +33 -2
  154. package/template/coding-task-manager/lib/root-prompt-flow.js +167 -33
  155. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  156. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  157. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  158. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  159. package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
  160. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  161. package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
  162. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  163. package/template/coding-task-manager/lib/session-create/provider-process.js +34 -37
  164. package/template/coding-task-manager/lib/session-create/response.js +30 -12
  165. package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
  166. package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
  167. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  168. package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
  169. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  170. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  171. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  172. package/template/coding-task-manager/lib/session-host-manager.js +152 -206
  173. package/template/coding-task-manager/lib/session-jobs.js +17 -9
  174. package/template/coding-task-manager/lib/session-lifecycle.js +18 -0
  175. package/template/coding-task-manager/lib/session-model-action-preconditions.js +9 -0
  176. package/template/coding-task-manager/lib/session-resource-sampler.js +19 -9
  177. package/template/coding-task-manager/lib/session-standup.js +67 -4
  178. package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
  179. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  180. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  181. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  182. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  183. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  184. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
  185. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  186. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  187. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  188. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  189. package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
  190. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  191. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  192. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  193. package/template/coding-task-manager/lib/transcript-source.js +8 -3
  194. package/template/coding-task-manager/lib/transcript-store.js +40 -0
  195. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  196. package/template/coding-task-manager/lib/typing-interaction-trace.js +92 -0
  197. package/template/coding-task-manager/lib/typing-worker-stages.js +60 -0
  198. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  199. package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
  200. package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
  201. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  202. package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
  203. package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
  204. package/template/coding-task-manager/lib/ws-router.js +68 -1
  205. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  206. package/template/coding-task-manager/oauth-proxy.js +99 -55
  207. package/template/coding-task-manager/package-lock.json +24 -0
  208. package/template/coding-task-manager/package.json +3 -0
  209. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  210. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  211. package/template/coding-task-manager/public/css/app.css +279 -1
  212. package/template/coding-task-manager/public/css/reviews.css +4 -0
  213. package/template/coding-task-manager/public/index.html +294 -223
  214. package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
  215. package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
  216. package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
  217. package/template/coding-task-manager/public/js/background-work.js +13 -7
  218. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
  219. package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
  220. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  221. package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
  222. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  223. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  224. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  225. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  226. package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
  227. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  228. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -8
  229. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  230. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  231. package/template/coding-task-manager/public/js/reviews.js +7 -3
  232. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  233. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  234. package/template/coding-task-manager/public/js/row-join.js +4 -2
  235. package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
  236. package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
  237. package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
  238. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  239. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  240. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  241. package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
  242. package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
  243. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  244. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  245. package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
  246. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  247. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  248. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -1
  249. package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
  250. package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
  251. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  252. package/template/coding-task-manager/public/m/app.css +226 -8
  253. package/template/coding-task-manager/public/m/app.js +502 -9
  254. package/template/coding-task-manager/public/m/index.html +38 -1
  255. package/template/coding-task-manager/public/m/machines.js +1313 -0
  256. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  257. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  258. package/template/coding-task-manager/public/m/sw.js +5 -0
  259. package/template/coding-task-manager/queue-engine.js +13 -0
  260. package/template/coding-task-manager/server/approvals.js +5 -6
  261. package/template/coding-task-manager/server/boot.js +136 -17
  262. package/template/coding-task-manager/server.js +902 -239
  263. package/template/coding-task-manager/session-integrity.js +7 -9
  264. package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
  265. package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
  266. package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
  267. package/template/coding-task-manager/workers/session-host-process.js +14 -3
  268. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  269. package/template/coding-task-manager/workers/worktree-worker.js +34 -0
  270. package/template/package-lock.json +2 -2
  271. package/template/package.json +1 -1
  272. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  273. package/template/shared/conversation-storage-v2-contract.js +42 -6
  274. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  275. package/template/shared/git-remote-key.js +83 -0
  276. package/template/shared/host-identity.js +98 -2
  277. package/template/shared/owner-write-safety.js +135 -0
  278. package/template/shared/public-runtime.js +13 -5
  279. package/template/shared/release-schema.json +3 -2
  280. package/template/shared/report-citations.js +14 -0
  281. package/template/shared/sqlite-write-lock.js +361 -223
  282. package/template/shared/storage/contract.js +35 -3
  283. package/template/wall-e/agent.js +21 -0
  284. package/template/wall-e/api-walle.js +5 -0
  285. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  286. package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
  287. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  288. package/template/wall-e/brain/migrations.js +51 -22
  289. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  290. package/template/wall-e/brain/owner-call-whitelist.js +16 -0
  291. package/template/wall-e/brain/schema-ensures.js +22 -0
  292. package/template/wall-e/brain.js +1 -1
  293. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  294. package/template/wall-e/coding/session-journal.js +28 -40
  295. package/template/wall-e/coding-orchestrator.js +23 -26
  296. package/template/wall-e/http/auth.js +18 -1
  297. package/template/wall-e/http/blog-views-api.js +318 -0
  298. package/template/wall-e/http/helper-rules-api.js +321 -0
  299. package/template/wall-e/http/model-proxy-providers.js +3 -1
  300. package/template/wall-e/http/telemetry-api.js +12 -0
  301. package/template/wall-e/lib/blog-view-policy.js +88 -0
  302. package/template/wall-e/lib/blog-view-store.js +169 -0
  303. package/template/wall-e/lib/client-config-lock.js +2 -1
  304. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  305. package/template/wall-e/lib/error-kinds.js +10 -1
  306. package/template/wall-e/lib/feature-manifest.js +1 -1
  307. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  308. package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
  309. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  310. package/template/wall-e/lib/helper-rules/grade.js +277 -0
  311. package/template/wall-e/lib/helper-rules/label.js +247 -0
  312. package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
  313. package/template/wall-e/lib/helper-rules/store.js +667 -0
  314. package/template/wall-e/lib/mcp-integration.js +5 -6
  315. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  316. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
  317. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  318. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  319. package/template/wall-e/llm/default-fallback.js +4 -0
  320. package/template/wall-e/llm/model-catalog.js +13 -4
  321. package/template/wall-e/llm/model-roles.js +6 -0
  322. package/template/wall-e/llm/portkey-http.js +150 -0
  323. package/template/wall-e/llm/portkey-sync.js +15 -145
  324. package/template/wall-e/llm/worker-mcp-routing.js +61 -0
  325. package/template/wall-e/llm/worker-route.js +43 -1
  326. package/template/wall-e/loops/brain-custom-writes.js +45 -0
  327. package/template/wall-e/mcp-protocol-contract.js +41 -1
  328. package/template/wall-e/mcp-server.js +13 -2
  329. package/template/wall-e/mcp-worker-schemas.js +20 -2
  330. package/template/wall-e/memory/ctm-context-client.js +25 -5
  331. package/template/wall-e/shared/release-schema.json +3 -2
  332. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  333. package/template/wall-e/shared/storage/contract.js +35 -3
  334. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  335. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
  336. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
  337. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
  338. package/template/wall-e/tools/local-tools.js +6 -4
  339. package/template/wall-e/workers/brain-owner-worker.js +9 -0
  340. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  341. package/template/wall-e/workers/telemetry-worker.js +8 -0
@@ -0,0 +1,200 @@
1
+ 'use strict';
2
+
3
+ // §5 D-7 / §6 (docs/model-proxy-survives-ctm-restart-2026-10-22.html): the CLI half of oauth-proxy.js.
4
+ // It lives here so oauth-proxy.js stays inside its hard 1500-line ceiling, and nothing in this file is
5
+ // reachable from an embedded start() -- only from `node oauth-proxy.js`.
6
+ //
7
+ // Two ways in, distinguished by ONE thing: whether there is an IPC channel.
8
+ // * the supervisor spawns the proxy with the admin secret on STDIN (WP-0 §5 D-4): read one line, start
9
+ // serving, and the secret is only ever used for the loopback admin calls above;
10
+ // * a PREDECESSOR performing an upgrade handoff (§5 D-7) spawns it with an `ipc` channel and no stdin
11
+ // at all (`stdio: ['ignore', logFd, logFd, 'ipc']`): the three `handoff/v1*` messages carry the two
12
+ // listening sockets, the admin secret and the memory-only token state, and this process is the
13
+ // SUCCESSOR. It publishes its own identity and answers `ready` -- the predecessor stops accepting
14
+ // only after that answer, which is why the answer is sent from start(), not from here.
15
+
16
+ const readline = require('node:readline');
17
+
18
+ const LOOPBACK_HOST = '127.0.0.1';
19
+ const LOOPBACK_REFUSAL_EXIT_CODE = 78;
20
+ const STARTUP_FAILURE_EXIT_CODE = 1;
21
+ const SIGTERM_EXIT_CODE = 143;
22
+
23
+ // Test seam (§6, AC-2b): truthy => the successor does everything a successor does EXCEPT send `ready`, so
24
+ // a test can observe the predecessor's bounded wait from outside both processes. It is read through a
25
+ // local `env` binding -- the shape lib/hermetic-agent-launch-guard.js uses for its own test-only
26
+ // switches -- and it reaches a successor because the supervisor's `childEnv = { ...process.env }` and the
27
+ // predecessor's `env: process.env` both pass it through. There is no operator flag here.
28
+ function _holdReadyFlag(env = process.env) {
29
+ return String(env.CTM_TEST_HANDOFF_SUCCESSOR_HOLD_READY || '') === '1';
30
+ }
31
+
32
+ /**
33
+ * §5 D-8 test seam: the CLI is the ONLY caller that arms the proxy's own linger timer, and this is its
34
+ * idle window. It is read through a local `env` binding, exactly like `_holdReadyFlag` above, so a child
35
+ * spawned by a test can be observed leaving its window in about a second instead of fifteen minutes; the
36
+ * production value comes from handoff.js (`PROXY_LINGER_MS`, === HOST_LINGER_MS). No operator flag, and
37
+ * no production path ever sets this.
38
+ */
39
+ function _lingerMsFlag(env = process.env, handoff) {
40
+ const raw = Number(env.CTM_TEST_PROXY_LINGER_MS);
41
+ return Number.isSafeInteger(raw) && raw > 0 ? raw : handoff.PROXY_LINGER_MS;
42
+ }
43
+
44
+ /** D-1: the CLI's bind host, resolved once from the environment exactly as it always was. */
45
+ function _resolveHost(env = process.env) {
46
+ const raw = env.OAUTH_PROXY_HOST;
47
+ return typeof raw === 'string' && raw.length > 0 ? raw : LOOPBACK_HOST;
48
+ }
49
+
50
+ // ── D-4 stdin secret (CLI mode only) ────────────────────────────────────
51
+ // Reads exactly one line from stdin at process start. Never env, never argv.
52
+ // Resolves null (no admin secret; admin endpoints stay locked) if stdin is a
53
+ // TTY or closes without producing a line.
54
+ function _readAdminSecretFromStdin() {
55
+ return new Promise((resolve) => {
56
+ if (!process.stdin || process.stdin.isTTY) {
57
+ resolve(null);
58
+ return;
59
+ }
60
+ const rl = readline.createInterface({ input: process.stdin, terminal: false });
61
+ let resolved = false;
62
+ const finish = (value) => {
63
+ if (resolved) return;
64
+ resolved = true;
65
+ rl.close();
66
+ resolve(value);
67
+ };
68
+ rl.once('line', (line) => finish((line || '').trim() || null));
69
+ rl.once('close', () => finish(null));
70
+ });
71
+ }
72
+
73
+ /**
74
+ * §9 S-5: record the exit, say it once, then leave. Semantics are unchanged -- nothing drains, and
75
+ * 128+15 keeps the supervisor's `code !== 0` classification of a SIGTERM exactly as it is today.
76
+ */
77
+ function _installSigtermHandler({ startedContext, streamAccountingMod, instanceRecord, handoff }) {
78
+ process.on('SIGTERM', () => {
79
+ const ctx = startedContext();
80
+ if (!ctx) {
81
+ process.exit(SIGTERM_EXIT_CODE);
82
+ return;
83
+ }
84
+ const openStreams = ctx.streamAccounting.count();
85
+ // §5 D-5: an explicit stop leaves no record, so the next boot spawns rather than adopts. A
86
+ // PREDECESSOR that is draining a handoff is the one exception: the record on disk already names its
87
+ // successor, and removing it would take the live proxy out of the next boot's reach.
88
+ if (!handoff.isDraining()) instanceRecord.unlinkInstanceRecord({ dataDir: ctx.dataDir });
89
+ streamAccountingMod.writeExitRecord({ dataDir: ctx.dataDir, reason: 'SIGTERM', openStreams })
90
+ .catch((err) => {
91
+ // REPORT: the exit code below still classifies this stop, so a failed write is not fatal.
92
+ console.error(`[model-proxy] exit record not written: ${(err && err.code) || 'unknown'}`);
93
+ })
94
+ .then(() => {
95
+ console.log(`[model-proxy] exit reason=SIGTERM open_streams=${openStreams}`);
96
+ process.exit(SIGTERM_EXIT_CODE);
97
+ });
98
+ });
99
+ }
100
+
101
+ /**
102
+ * The CLI entry point, called by oauth-proxy.js's `require.main === module` block with its own
103
+ * dependencies injected (so this module never requires oauth-proxy.js back).
104
+ *
105
+ * @param {object} input
106
+ * @param {Function} input.start oauth-proxy.js's start()
107
+ * @param {object} input.streamAccountingMod lib/model-proxy/stream-accounting.js
108
+ * @param {object} input.instanceRecord lib/model-proxy/proxy-instance-record.js
109
+ * @param {object} input.handoff lib/model-proxy/handoff.js
110
+ * @param {Function} [input.startedContext] getter for the context of the server this process started
111
+ * @param {string} [input.loopbackErrorCode] the named refusal code start() rejects with
112
+ */
113
+ function run({
114
+ start,
115
+ streamAccountingMod,
116
+ instanceRecord,
117
+ handoff,
118
+ startedContext = () => null,
119
+ loopbackErrorCode = 'EBIND_NOT_LOOPBACK',
120
+ } = {}) {
121
+ const host = _resolveHost();
122
+ if (host !== LOOPBACK_HOST) {
123
+ // D-1: refuse before ever touching stdin, so a bad --host/env doesn't hang waiting for an admin
124
+ // secret that will never matter.
125
+ console.error(`[oauth-proxy] ${loopbackErrorCode}: refusing to bind to "${host}" — must be 127.0.0.1 (D-1)`);
126
+ process.exit(LOOPBACK_REFUSAL_EXIT_CODE);
127
+ return;
128
+ }
129
+ const successor = typeof process.send === 'function';
130
+ const startup = successor
131
+ // §5 D-7: the successor never reads stdin -- its secret and its data directory arrive in the
132
+ // handoff message, and its listener is a socket it did not open.
133
+ ? handoff.receiveInheritance().then((received) => ({
134
+ host,
135
+ adminSecret: received.inherited.adminSecret,
136
+ dataDir: received.inherited.dataDir,
137
+ inherited: { ...received.inherited, state: received.state, holdReady: _holdReadyFlag() },
138
+ }))
139
+ // A TTY, or stdin closing before a line arrived, is not a failure: the process simply serves with no
140
+ // admin secret, and its admin surface stays locked. The read itself already resolves null in both of
141
+ // those cases, so this tail exists only for an unexpected readline error.
142
+ : _readAdminSecretFromStdin()
143
+ .catch((/* expected: no line on stdin -- start with no admin secret rather than not at all */) => null)
144
+ .then((adminSecret) => ({ host, adminSecret }));
145
+
146
+ startup
147
+ // §5 D-8: `lingerMs` is what makes THIS process (and only this one: an embedded start() passes none)
148
+ // leave on its own once CTM has been gone for a full window.
149
+ .then((args) => start({ ...args, lingerMs: _lingerMsFlag(process.env, handoff) }))
150
+ .then(() => {
151
+ if (successor) {
152
+ // §5 D-7: the successor outlives the predecessor that spawned it, so the channel it was born
153
+ // with must never be what keeps it alive -- nor may its closure end it. `ready` is already sent.
154
+ try {
155
+ process.channel.unref();
156
+ } catch (err) {
157
+ // expected: nothing to detach when this process has no channel state left to manage.
158
+ void err;
159
+ }
160
+ }
161
+ _installSigtermHandler({ startedContext, streamAccountingMod, instanceRecord, handoff });
162
+ })
163
+ .catch((err) => {
164
+ if (err && err.code === loopbackErrorCode) {
165
+ console.error(`[oauth-proxy] ${err.code}: ${err.message}`);
166
+ process.exit(LOOPBACK_REFUSAL_EXIT_CODE);
167
+ return;
168
+ }
169
+ console.error('[oauth-proxy] failed to start:', err.message);
170
+ process.exit(STARTUP_FAILURE_EXIT_CODE);
171
+ });
172
+ }
173
+
174
+ /**
175
+ * The `require.main === module` entry: oauth-proxy.js calls exactly this, so its own CLI seams
176
+ * (start, the accounting/record modules, the SIGTERM context) do not have to be spelled out at the bottom
177
+ * of a file that is at its hard line ceiling. The require is deferred to call time, so loading THIS module
178
+ * never loads oauth-proxy.js back -- and when the proxy is its own main, this module is loaded from
179
+ * inside that same evaluation, by which point oauth-proxy.js's exports are complete.
180
+ */
181
+ function runCli() {
182
+ const proxy = require('../../oauth-proxy.js');
183
+ return run({
184
+ start: proxy.start,
185
+ streamAccountingMod: require('./stream-accounting'),
186
+ instanceRecord: require('./proxy-instance-record'),
187
+ handoff: require('./handoff'),
188
+ startedContext: proxy._startedContext,
189
+ loopbackErrorCode: proxy.EBIND_NOT_LOOPBACK,
190
+ });
191
+ }
192
+
193
+ module.exports = {
194
+ run,
195
+ runCli,
196
+ _readAdminSecretFromStdin,
197
+ _holdReadyFlag,
198
+ _lingerMsFlag,
199
+ _resolveHost,
200
+ };
@@ -0,0 +1,257 @@
1
+ 'use strict';
2
+
3
+ // §5 D-1/D-2/D-3 + §6 (docs/model-proxy-survives-ctm-restart-2026-10-22.html): the durable identity of a
4
+ // LIVE model proxy, and the four paths it lives on. The supervisor binds the child's stdout/stderr to
5
+ // proxyLogPath() (the proxy never writes into a CTM-owned pipe -- incident 7: an orphaned child dies on
6
+ // its second write with EPIPE); the child publishes instance.json after listen and removes it on an
7
+ // explicit stop; CTM writes the per-spawn admin secret BEFORE the spawn, so a CTM that dies between the
8
+ // spawn and its own exit still leaves a readable secret for the next boot -- which is also the CTM that
9
+ // adopts the survivor (§5 D-4). Both processes must agree on these paths byte-for-byte, so they are
10
+ // defined once, here.
11
+ //
12
+ // Publications are atomic in the shape lib/host-registry.js uses (a nonce-scoped tmp file created with
13
+ // flag 'wx' and mode 0600, renamed over the final path), and the directory is 0700 -- the same tree the
14
+ // native mTLS authority already lives in. The same 0600 secret authenticates loopback admin calls only;
15
+ // nothing here is a transport credential.
16
+
17
+ const crypto = require('node:crypto');
18
+ const fs = require('node:fs');
19
+ const path = require('node:path');
20
+ const { execFileSync } = require('node:child_process');
21
+ const { computeClosure } = require('../host-closure-hash');
22
+
23
+ // lib/host-closure-hash.js resolves its own default repo root from ITS __dirname (coding-task-manager/lib
24
+ // -> the repo root). This module sits one level deeper, so the same root is re-stated explicitly: the
25
+ // file list the PROXY hashes and the file list CTM hashes from disk must name the same files.
26
+ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
27
+
28
+ const RECORD_VERSION = 1;
29
+ const PROXY_PROTOCOL = 'proxy-v1';
30
+ const PROXY_FLOOR_VERSION = 0;
31
+ const PROXY_SCRIPT_NAME = 'oauth-proxy.js';
32
+ // §5 D-1: one generation of log is kept. 203,900 request lines in 26 days is ~1.5 MB/day, so ~40 days
33
+ // per generation; the rotation is a rename, not a truncation, so the previous generation stays readable.
34
+ const LOG_ROTATE_BYTES = 64 * 1024 * 1024;
35
+ const LOG_ROTATE_SUFFIX = '.1';
36
+ const PS_TIMEOUT_MS = 2000;
37
+
38
+ function proxyDir(dataDir) {
39
+ return path.join(dataDir, 'oauth-proxy');
40
+ }
41
+
42
+ function instanceRecordPath(dataDir) {
43
+ return path.join(proxyDir(dataDir), 'instance.json');
44
+ }
45
+
46
+ function adminSecretPath(dataDir) {
47
+ return path.join(proxyDir(dataDir), 'admin-secret');
48
+ }
49
+
50
+ function proxyLogPath(dataDir) {
51
+ return path.join(proxyDir(dataDir), 'oauth-proxy.log');
52
+ }
53
+
54
+ // 0700, created on demand. Synchronous because the spawn-time log descriptor below needs the directory to
55
+ // exist before the child does; one call per spawn and one per publication, never a request path.
56
+ function ensureProxyDir(dataDir) {
57
+ const dir = proxyDir(dataDir);
58
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
59
+ return dir;
60
+ }
61
+
62
+ function _writeAtomic0600Sync(target, text) {
63
+ const tmp = `${target}.${process.pid}.${crypto.randomUUID()}.tmp`;
64
+ fs.writeFileSync(tmp, text, { mode: 0o600, flag: 'wx' });
65
+ fs.renameSync(tmp, target);
66
+ return target;
67
+ }
68
+
69
+ function _unlinkQuiet(target) {
70
+ try {
71
+ fs.unlinkSync(target);
72
+ return true;
73
+ } catch (err) {
74
+ if (err && err.code === 'ENOENT') return false;
75
+ // REPORT: a file that cannot be removed is stale truth on disk; the caller's decision still stands.
76
+ process.stderr.write(`[model-proxy] record not removed: ${path.basename(target)} ${(err && err.code) || 'unknown'}\n`);
77
+ return false;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * §5 D-2: the record the proxy publishes after listen. Written before the first request is answered, so
83
+ * a CTM that restarts immediately afterwards still finds the exact identity of what is listening.
84
+ * @returns {object} the record written
85
+ */
86
+ function writeInstanceRecord({ dataDir, record }) {
87
+ ensureProxyDir(dataDir);
88
+ _writeAtomic0600Sync(instanceRecordPath(dataDir), `${JSON.stringify(record)}\n`);
89
+ return record;
90
+ }
91
+
92
+ /**
93
+ * §5 D-3/D-2: compute this process's loaded-code hash ONCE, publish the record, and hand the same two
94
+ * fields to the accounting the /_health answer reports from -- so identity and the published record can
95
+ * never describe different code. §5 D-7 (WP-2): a handoff successor publishes `generation: n+1` and the
96
+ * predecessor it took over from, so the record names the LIVE socket owner and who handed it over.
97
+ */
98
+ function publishInstanceRecord({ dataDir, port, nativePort = null, accounting = null, generation = 1, predecessorPid = null }) {
99
+ const { closureHash, closureFiles } = computeClosure({ repoRoot: REPO_ROOT });
100
+ const record = writeInstanceRecord({
101
+ dataDir,
102
+ record: {
103
+ v: RECORD_VERSION,
104
+ pid: process.pid,
105
+ port,
106
+ nativePort,
107
+ protocol: PROXY_PROTOCOL,
108
+ floorVersion: PROXY_FLOOR_VERSION,
109
+ closureHash,
110
+ closureFiles,
111
+ generation,
112
+ predecessorPid,
113
+ startedAt: new Date().toISOString(),
114
+ draining: false,
115
+ },
116
+ });
117
+ if (accounting && typeof accounting.setIdentity === 'function') {
118
+ accounting.setIdentity({ closureHash, closureFiles, generation, draining: false });
119
+ }
120
+ return record;
121
+ }
122
+
123
+ /**
124
+ * §5 D-7 step 2: rewrite the published record in place with one patch (today: `draining`). Written
125
+ * BEFORE the successor is spawned, so a predecessor that dies mid-handoff leaves a truthful record --
126
+ * and rewritten with `draining:false` on any abort, because the predecessor kept serving.
127
+ * @returns {object|null} the rewritten record, or null when this process published none
128
+ */
129
+ function updateInstanceRecord({ dataDir, patch }) {
130
+ const current = readInstanceRecord({ dataDir });
131
+ if (!current) return null;
132
+ return writeInstanceRecord({ dataDir, record: { ...current, ...patch } });
133
+ }
134
+
135
+ /**
136
+ * @returns {object|null} the published record, or null when there is none. A truncated, hand-edited or
137
+ * unusable record is reported as absent AND removed: writes are atomic via rename, so it can only be
138
+ * leftover garbage, and leaving it behind would make the next boot re-read the same lie.
139
+ */
140
+ function readInstanceRecord({ dataDir }) {
141
+ let raw;
142
+ try {
143
+ raw = fs.readFileSync(instanceRecordPath(dataDir), 'utf8');
144
+ } catch (err) {
145
+ if (err && err.code === 'ENOENT') return null; // expected: no proxy has published one
146
+ throw err;
147
+ }
148
+ try {
149
+ const record = JSON.parse(raw);
150
+ if (record && typeof record === 'object' && Number.isSafeInteger(record.pid) && record.pid > 0) return record;
151
+ } catch {
152
+ // expected: an unparseable record is not evidence -- fall through and remove it
153
+ }
154
+ unlinkInstanceRecord({ dataDir });
155
+ return null;
156
+ }
157
+
158
+ function unlinkInstanceRecord({ dataDir }) {
159
+ return _unlinkQuiet(instanceRecordPath(dataDir));
160
+ }
161
+
162
+ /** §5 D-2: written before each fresh spawn, read back when CTM adopts a proxy it did not spawn. */
163
+ function writeAdminSecret({ dataDir, secret }) {
164
+ ensureProxyDir(dataDir);
165
+ _writeAtomic0600Sync(adminSecretPath(dataDir), `${String(secret)}\n`);
166
+ return secret;
167
+ }
168
+
169
+ /** @returns {string|null} the current secret, or null when there is none to authenticate with. */
170
+ function readAdminSecret({ dataDir }) {
171
+ let raw;
172
+ try {
173
+ raw = fs.readFileSync(adminSecretPath(dataDir), 'utf8');
174
+ } catch (err) {
175
+ if (err && err.code === 'ENOENT') return null; // expected: this CTM never spawned a proxy here
176
+ throw err;
177
+ }
178
+ const secret = raw.trim();
179
+ return secret || null;
180
+ }
181
+
182
+ function unlinkAdminSecret({ dataDir }) {
183
+ return _unlinkQuiet(adminSecretPath(dataDir));
184
+ }
185
+
186
+ /**
187
+ * §5 D-1: the descriptor the child's stdout and stderr are bound to. One synchronous open per spawn
188
+ * (never a request path), rotating one generation at 64 MiB first: every proxy generation passes through
189
+ * this spawn, including WP-2's handoff successor, so this is the one place rotation has to happen.
190
+ */
191
+ function openProxyLogFd(dataDir) {
192
+ ensureProxyDir(dataDir);
193
+ const target = proxyLogPath(dataDir);
194
+ const fd = fs.openSync(target, 'a');
195
+ if (fs.fstatSync(fd).size <= LOG_ROTATE_BYTES) return fd;
196
+ fs.closeSync(fd);
197
+ fs.renameSync(target, `${target}${LOG_ROTATE_SUFFIX}`);
198
+ return fs.openSync(target, 'a');
199
+ }
200
+
201
+ /** The supervisor's own copy of the descriptor; the child holds its own. */
202
+ function closeProxyLogFd(fd) {
203
+ try {
204
+ fs.closeSync(fd);
205
+ } catch (err) {
206
+ // expected: a descriptor that is already closed costs nothing -- the child has its own copy
207
+ void err;
208
+ }
209
+ }
210
+
211
+ /** §5 D-4: is this pid a live proxy? `ps` is the authority -- a recycled pid must never be signalled. */
212
+ function isProxyProcess(pid) {
213
+ if (!Number.isSafeInteger(pid) || pid < 1) return false;
214
+ try {
215
+ return execFileSync('ps', ['-o', 'command=', '-p', String(pid)], { encoding: 'utf8', timeout: PS_TIMEOUT_MS })
216
+ .includes(PROXY_SCRIPT_NAME);
217
+ } catch (err) {
218
+ // expected: ps exits non-zero for a pid that is gone, and a failed probe is never a proxy
219
+ void err;
220
+ return false;
221
+ }
222
+ }
223
+
224
+ /** D-4 step 1: `process.kill(pid, 0)` -- EPERM means alive but not ours, which is still alive. */
225
+ function isProcessAlive(pid) {
226
+ if (!Number.isSafeInteger(pid) || pid < 1) return false;
227
+ try {
228
+ process.kill(pid, 0);
229
+ return true;
230
+ } catch (err) {
231
+ return err && err.code === 'EPERM';
232
+ }
233
+ }
234
+
235
+ module.exports = {
236
+ RECORD_VERSION,
237
+ PROXY_PROTOCOL,
238
+ PROXY_FLOOR_VERSION,
239
+ LOG_ROTATE_BYTES,
240
+ proxyDir,
241
+ instanceRecordPath,
242
+ adminSecretPath,
243
+ proxyLogPath,
244
+ ensureProxyDir,
245
+ publishInstanceRecord,
246
+ updateInstanceRecord,
247
+ writeInstanceRecord,
248
+ readInstanceRecord,
249
+ unlinkInstanceRecord,
250
+ writeAdminSecret,
251
+ readAdminSecret,
252
+ unlinkAdminSecret,
253
+ openProxyLogFd,
254
+ closeProxyLogFd,
255
+ isProxyProcess,
256
+ isProcessAlive,
257
+ };
@@ -0,0 +1,205 @@
1
+ 'use strict';
2
+
3
+ // §5 D-9 / §9 S-5, S-8: how many requests this proxy is still carrying, and what it was carrying when
4
+ // it last stopped. Both truths live here on purpose -- the exit record is nothing but the counter read
5
+ // at exit, and a second copy of that number is exactly how the two would drift apart.
6
+
7
+ const fsp = require('node:fs/promises');
8
+ const path = require('node:path');
9
+
10
+ // The health probe is itself a request, so counting it would make the counter unobservable from the one
11
+ // surface that reports it (the probe would always see its own 1). Only these two status paths are
12
+ // exempt; every other path is counted, keep-alive or not.
13
+ const UNCOUNTED_PATHS = new Set(['/_health', '/_status']);
14
+
15
+ const EXIT_RECORD_VERSION = 1;
16
+
17
+ function _pathOf(url) {
18
+ const raw = String(url || '');
19
+ const cut = raw.indexOf('?');
20
+ return cut === -1 ? raw : raw.slice(0, cut);
21
+ }
22
+
23
+ /**
24
+ * The process's own in-flight stream counter.
25
+ *
26
+ * @param {{startedAt?: string}} [options] `startedAt` is overridable only so a test can pin it.
27
+ */
28
+ function createStreamAccounting({ startedAt = new Date().toISOString() } = {}) {
29
+ let openStreams = 0;
30
+ // §5 D-3: the loaded-code hash this process published, set ONCE after listen by
31
+ // proxy-instance-record.publishInstanceRecord(). Read here so the /_health answer and the on-disk
32
+ // record can never describe different code; null until then (a start that never binds publishes none).
33
+ // §5 D-7 (WP-2): `generation` and `draining` travel the same way -- the successor of a handoff is
34
+ // generation N+1 and says so on /_health while its predecessor is still draining.
35
+ let identity = null;
36
+ // §5 D-7 step 7: the drain ends when this counter reaches zero, so the DECREMENT is what notifies --
37
+ // there is no polling loop and no second copy of the count.
38
+ const idleWaiters = new Set();
39
+
40
+ function _notifyIdle() {
41
+ const waiters = [...idleWaiters];
42
+ idleWaiters.clear();
43
+ for (const waiter of waiters) {
44
+ try {
45
+ waiter();
46
+ } catch (err) {
47
+ // REPORT: a drain waiter that throws is not the counter's problem; the drain deadline owns the
48
+ // fallback, so the failure is worth one line and nothing else.
49
+ process.stderr.write(`[model-proxy] drain waiter failed: ${(err && err.message) || 'unknown'}\n`);
50
+ }
51
+ }
52
+ }
53
+
54
+ return {
55
+ /**
56
+ * Count every request this server is still answering. An idle keep-alive SOCKET carries no request
57
+ * and is never counted; a request is in flight until its response emits 'close' (whether it
58
+ * completed, errored or was cut by the client).
59
+ */
60
+ track(server) {
61
+ server.on('request', (req, res) => {
62
+ if (UNCOUNTED_PATHS.has(_pathOf(req.url))) return;
63
+ openStreams += 1;
64
+ res.once('close', () => {
65
+ openStreams -= 1;
66
+ if (openStreams === 0) _notifyIdle();
67
+ });
68
+ });
69
+ },
70
+ count() {
71
+ return openStreams;
72
+ },
73
+ snapshot() {
74
+ return { openStreams, startedAt, identity };
75
+ },
76
+ /** §5 D-3: the code this process loaded, published once; what /_health reports from. */
77
+ setIdentity(next) {
78
+ identity = next
79
+ ? {
80
+ closureHash: next.closureHash || null,
81
+ closureFiles: Array.isArray(next.closureFiles) ? next.closureFiles : [],
82
+ generation: Number.isSafeInteger(next.generation) && next.generation > 0 ? next.generation : 1,
83
+ draining: next.draining === true,
84
+ }
85
+ : null;
86
+ },
87
+ /** §5 D-7 step 8: the predecessor answers /_health with draining:true from here until it exits. */
88
+ setDraining(value) {
89
+ const current = identity || { closureHash: null, closureFiles: [], generation: 1, draining: false };
90
+ identity = { ...current, draining: value === true };
91
+ },
92
+ /**
93
+ * §5 D-7 step 7: called ONCE by the handoff drain, on the decrement that brings the count to zero.
94
+ * @returns {Function} unsubscribe, so a drain that was already forced does not hold a callback
95
+ */
96
+ onIdle(callback) {
97
+ idleWaiters.add(callback);
98
+ return () => idleWaiters.delete(callback);
99
+ },
100
+ };
101
+ }
102
+
103
+ /**
104
+ * §9 S-8: the process-identity fields the `/_health` answer reports alongside its existing ones. The
105
+ * count is read live, never cached, so the answer can never describe an earlier moment. `generation`
106
+ * and `draining` come from the published identity (a handoff successor is generation N+1 and reports
107
+ * `draining:true` while its predecessor drains); a process that published none reports today's
108
+ * 1/false.
109
+ */
110
+ function healthFields(accounting) {
111
+ const snapshot = accounting.snapshot();
112
+ const identity = snapshot.identity || {};
113
+ return {
114
+ pid: process.pid,
115
+ generation: identity.generation || 1,
116
+ protocol: 'proxy-v1',
117
+ floor_version: 0,
118
+ open_streams: snapshot.openStreams,
119
+ draining: identity.draining === true,
120
+ started_at: snapshot.startedAt,
121
+ // §5 D-3: what CTM compares against its own disk hash to decide adopt vs hand off.
122
+ closure_hash: identity.closureHash || null,
123
+ closure_files: identity.closureFiles || [],
124
+ };
125
+ }
126
+
127
+ /**
128
+ * §9 S-8: the health fields merged into whatever JSON body the responder writes. The relay keeps
129
+ * ownership of its own five fields (unchanged, names and types); this only adds what D-9/S-8 own, and a
130
+ * non-JSON end (there is none today) is passed through untouched rather than guessed at.
131
+ */
132
+ function withHealthFields(res, accounting) {
133
+ const end = res.end.bind(res);
134
+ res.end = (chunk, ...rest) => {
135
+ let body;
136
+ try {
137
+ body = JSON.parse(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk));
138
+ } catch {
139
+ return end(chunk, ...rest); // expected: only the health JSON body is augmented
140
+ }
141
+ return end(JSON.stringify({ ...body, ...healthFields(accounting) }), ...rest);
142
+ };
143
+ return res;
144
+ }
145
+
146
+ function exitRecordPath(dataDir) {
147
+ return path.join(dataDir, 'oauth-proxy', 'last-exit.json');
148
+ }
149
+
150
+ /**
151
+ * §9 S-5: the previous exit, written once. Atomic (tmp + rename) and owner-only, so a concurrent reader
152
+ * sees either the whole record or no record — never a half-written one. Async like the budget journals
153
+ * beside it (request-budget-store.js): the proxy awaits the write before it exits, and the supervisor
154
+ * reads it off the main thread.
155
+ * @returns {Promise<object>} the record written
156
+ */
157
+ async function writeExitRecord({ dataDir, reason, openStreams }) {
158
+ const record = { v: EXIT_RECORD_VERSION, at: new Date().toISOString(), pid: process.pid, reason, openStreams };
159
+ const target = exitRecordPath(dataDir);
160
+ const tmp = `${target}.${process.pid}.tmp`;
161
+ await fsp.mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
162
+ await fsp.writeFile(tmp, `${JSON.stringify(record)}\n`, { mode: 0o600 });
163
+ await fsp.rename(tmp, target);
164
+ return record;
165
+ }
166
+
167
+ /**
168
+ * @returns {Promise<object|null>} the recorded previous exit, or null when there is none to report. A
169
+ * truncated or hand-edited record is reported as absent rather than thrown: it is evidence of nothing.
170
+ */
171
+ async function readExitRecord({ dataDir }) {
172
+ let raw;
173
+ try {
174
+ raw = await fsp.readFile(exitRecordPath(dataDir), 'utf8');
175
+ } catch (err) {
176
+ if (err && err.code === 'ENOENT') return null; // expected: no previous exit was recorded
177
+ throw err;
178
+ }
179
+ try {
180
+ const record = JSON.parse(raw);
181
+ return record && typeof record === 'object' ? record : null;
182
+ } catch {
183
+ return null; // expected: an unparseable record is not evidence; report it as absent
184
+ }
185
+ }
186
+
187
+ /** One exit is reported once: the supervisor reads the record, then clears it. */
188
+ async function clearExitRecord({ dataDir }) {
189
+ try {
190
+ await fsp.rm(exitRecordPath(dataDir), { force: true });
191
+ } catch (err) {
192
+ // REPORT: a record that cannot be removed is stale truth on disk, which is worth a line.
193
+ console.error(`[model-proxy] exit record not removed: ${(err && err.code) || 'unknown'}`);
194
+ }
195
+ }
196
+
197
+ module.exports = {
198
+ createStreamAccounting,
199
+ healthFields,
200
+ withHealthFields,
201
+ writeExitRecord,
202
+ readExitRecord,
203
+ clearExitRecord,
204
+ exitRecordPath,
205
+ };