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
@@ -5,11 +5,13 @@ const crypto = require('node:crypto');
5
5
  const os = require('node:os');
6
6
  const path = require('node:path');
7
7
  const runtimeHealth = require('../lib/runtime-health');
8
+ const errorSink = require('../lib/error-sink');
8
9
  const { redactSecrets } = require('../llm/provider-error');
9
10
  const { redactPositionalSecretArray, redactSecretText } = require('../lib/secret-redaction');
10
11
  const { defaultSessionsDir } = require('./workspace-replay');
11
12
  const { canonicalRepoKey } = require('./repo-identity');
12
13
  const transcriptSource = require('../../coding-task-manager/lib/transcript-source');
14
+ const { selectJournalDispatcher } = require('./journal-dispatcher');
13
15
 
14
16
  const MAX_BODY_CHARS = 1500;
15
17
  const MAX_SEGMENT_CHARS = 4000;
@@ -311,38 +313,9 @@ async function handleDurableFailure(kind, entry, error, options = {}, startedAt
311
313
  }
312
314
 
313
315
  function recoveryJournalDispatcher(options = {}) {
314
- if (options.dispatcher) return options.dispatcher;
315
- const { getSharedBrainOwnerClient } = require('../lib/brain-owner-worker-client');
316
- const shared = getSharedBrainOwnerClient();
317
- if (shared) return createJournalWriteDispatcher({ client: shared });
318
- // Stop hooks and standalone lifecycle processes cannot see the daemon's
319
- // process-local singleton. The bounded one-shot child owns SQLite and keeps
320
- // this process free from better-sqlite3 teardown hazards.
321
- const { writeJournalEntryInChild } = require('../lib/coding-journal-writer-child-client');
322
- return createJournalWriteDispatcher({
323
- client: {
324
- persistCodingJournal(entry) {
325
- const dbPath = options.dbPath || defaultBrainDbPath(options.env);
326
- // J-1: no daemon is actually serving a brain (no explicit dbPath, no
327
- // WALL_E_DATA_DIR, no live-data instance-id.json) — never spawn the
328
- // one-shot child, which holds an owner grant and would create/migrate
329
- // the phantom ~/.walle/data brain. Write nothing; skip loudly.
330
- if (!dbPath) {
331
- console.error('[wall-e] coding journal write skipped: reason=no_served_brain (no daemon-served brain to write to)');
332
- return Promise.resolve({ ok: true, skipped: true, reason: 'no_served_brain' });
333
- }
334
- return writeJournalEntryInChild(entry, {
335
- dbPath,
336
- // S-8: state THIS process's schema generation so the fresh-code child
337
- // refuses rather than migrating the brain out from under us.
338
- parentSchemaVersion: require('../brain').SCHEMA_VERSION,
339
- timeoutMs: options.timeoutMs,
340
- forkImpl: options.forkImpl,
341
- env: options.env,
342
- });
343
- },
344
- },
345
- });
316
+ return selectJournalDispatcher(options, {
317
+ createJournalWriteDispatcher, defaultBrainDbPath,
318
+ }).dispatcher;
346
319
  }
347
320
 
348
321
  async function readJournalRetryMarker(markerPath) {
@@ -1001,7 +974,11 @@ function dispatchOffThread(kind, dispatcher, prepare, options = {}) {
1001
974
  narrativeOp.end({ ok: true, meta: { applied } });
1002
975
  } catch (error) {
1003
976
  narrativeOp.end({ ok: false, error });
1004
- options.logger?.warn?.(`[coding-journal] narrative polisher failed open: ${redactJournalString(error?.message || error, 240)}`);
977
+ try {
978
+ options.logger?.warn?.(`[coding-journal] narrative polisher failed open: ${redactJournalString(error?.message || error, 240)}`);
979
+ } catch (warningError) {
980
+ errorSink.report('coding-journal', 'narrative-warning', warningError);
981
+ }
1005
982
  }
1006
983
  }
1007
984
 
@@ -1251,14 +1228,24 @@ function journalInputForRun({
1251
1228
  });
1252
1229
  }
1253
1230
 
1254
- function scheduleRunJournal(input = {}, options = {}) {
1231
+ function startRunJournal(input = {}, options = {}) {
1255
1232
  const timestamp = String(input.timestamp || new Date().toISOString());
1256
- return dispatchOffThread(
1257
- 'entry',
1258
- options.dispatcher || defaultJournalDispatcher(options),
1259
- () => buildJournalEntry({ ...journalInputForRun(input), timestamp }),
1260
- options,
1261
- );
1233
+ const selection = selectJournalDispatcher(options, {
1234
+ createJournalWriteDispatcher, defaultBrainDbPath,
1235
+ });
1236
+ return Object.freeze({
1237
+ completion: dispatchOffThread(
1238
+ 'entry',
1239
+ selection.dispatcher,
1240
+ () => buildJournalEntry({ ...journalInputForRun(input), timestamp }),
1241
+ options,
1242
+ ),
1243
+ defaultAwaitTerminalJournal: selection.defaultAwaitTerminalJournal,
1244
+ });
1245
+ }
1246
+
1247
+ function scheduleRunJournal(input = {}, options = {}) {
1248
+ return startRunJournal(input, options).completion;
1262
1249
  }
1263
1250
 
1264
1251
  function transcriptText(record = {}) {
@@ -1577,6 +1564,7 @@ module.exports = {
1577
1564
  persistJournalEntry,
1578
1565
  persistJournalSegment,
1579
1566
  scheduleJournalEntry,
1567
+ startRunJournal,
1580
1568
  scheduleRunJournal,
1581
1569
  scheduleJournalSegment,
1582
1570
  extractDecisionContent,
@@ -3393,18 +3393,13 @@ async function runAgentLoopUnaccounted(prompt, opts = {}) {
3393
3393
  // that stable session when the caller omitted runId.
3394
3394
  const runId = String(opts.runId || crypto.randomUUID());
3395
3395
  const sessionId = String(opts.chatSessionId || opts.session_id || runId);
3396
- let awaitTerminalJournal = opts.awaitTerminalJournal;
3397
- if (awaitTerminalJournal == null) {
3398
- try {
3399
- const { getSharedBrainOwnerClient } = require('./lib/brain-owner-worker-client');
3400
- // Daemon CTM turns enqueue to the already-running owner and return without
3401
- // gating user-visible completion on SQLite. Standalone/CLI runs own their
3402
- // process lifetime, so they drain the one-shot writer before exiting.
3403
- awaitTerminalJournal = Boolean(opts.journalDispatcher) || !getSharedBrainOwnerClient();
3404
- } catch {
3405
- awaitTerminalJournal = true;
3406
- }
3407
- }
3396
+ // The wait policy belongs to the dispatcher actually selected at the terminal
3397
+ // boundary, which is why this is only the caller's explicit override, read
3398
+ // once at run start (see flushTerminalJournal). A daemon turn enqueues to the
3399
+ // already-running owner and returns without gating user-visible completion on
3400
+ // SQLite; a standalone/CLI run owns its process lifetime and drains the
3401
+ // one-shot writer before exiting.
3402
+ const explicitJournalAwait = opts.awaitTerminalJournal;
3408
3403
  const cwd = await realpathBestEffort(opts.cwd || process.cwd());
3409
3404
  let repoKey = String(opts.repoKey || '');
3410
3405
  if (!repoKey) {
@@ -3428,10 +3423,10 @@ async function runAgentLoopUnaccounted(prompt, opts = {}) {
3428
3423
  opts.journalNarrativeLogger?.warn?.(`[coding-journal] narrative setup skipped: ${error?.message || String(error)}`);
3429
3424
  }
3430
3425
  }
3431
- let terminalJournalPromise = null;
3426
+ let terminalJournalOperation = null;
3432
3427
  const scheduleTerminalJournal = ({ result = null, error = null } = {}) => {
3433
- const { scheduleRunJournal } = require('./coding/session-journal');
3434
- terminalJournalPromise = scheduleRunJournal({
3428
+ const { startRunJournal } = require('./coding/session-journal');
3429
+ terminalJournalOperation = startRunJournal({
3435
3430
  runId,
3436
3431
  sessionId,
3437
3432
  repoKey,
@@ -3452,20 +3447,22 @@ async function runAgentLoopUnaccounted(prompt, opts = {}) {
3452
3447
  });
3453
3448
  };
3454
3449
  const flushTerminalJournal = async () => {
3455
- if (!terminalJournalPromise || !awaitTerminalJournal) return;
3456
- const timeoutMs = Math.min(Math.max(Number(opts.journalFlushTimeoutMs) || 12000, 1000), 60000);
3457
- let timer = null;
3450
+ if (!terminalJournalOperation) return;
3451
+ const shouldAwait = explicitJournalAwait == null
3452
+ ? terminalJournalOperation.defaultAwaitTerminalJournal !== false
3453
+ : explicitJournalAwait;
3454
+ if (!shouldAwait) return;
3455
+ // The awaited caller joins the journal scheduler's own promise, so the
3456
+ // dispatch enqueue and the scheduler's durable-failure handling finish before
3457
+ // the run boundary returns; when the selected dispatcher is the native
3458
+ // writer, that same promise also covers its real child close. A caller-side
3459
+ // race would abandon that ownership while the write is still in flight. A
3460
+ // custom dispatcher proves only what its own promise proves: its resolved
3461
+ // Promise is not physical-effect evidence, and no timer can supply one.
3458
3462
  try {
3459
- await Promise.race([
3460
- terminalJournalPromise,
3461
- new Promise((resolve) => {
3462
- timer = setTimeout(() => resolve({ ok: false, timedOut: true }), timeoutMs);
3463
- }),
3464
- ]);
3463
+ await terminalJournalOperation.completion;
3465
3464
  } catch (error) {
3466
3465
  console.warn?.(`[coding-journal] terminal flush failed: ${error.message}`);
3467
- } finally {
3468
- if (timer) clearTimeout(timer);
3469
3466
  }
3470
3467
  };
3471
3468
  let actionMemoryWriter = opts.actionMemoryWriter || null;
@@ -8,6 +8,11 @@ const PUBLIC_ROUTES = new Set([
8
8
  '/api/wall-e/health',
9
9
  '/api/wall-e/webhook',
10
10
  '/api/wall-e/feedback/submit',
11
+ // WP-1 (blog-view-counts): the public aggregate counter for the three static
12
+ // articles. Public at this outer gate ONLY — the handler itself accepts a POST
13
+ // solely from the two published site origins (getBlogViewOrigin), and its
14
+ // writes are capped by the store's receipt bound.
15
+ '/api/wall-e/telemetry/blog-views',
11
16
  '/api/wall-e/telemetry/ingest',
12
17
  '/api/wall-e/telemetry/summary',
13
18
  '/api/wall-e/telemetry/storage',
@@ -17,6 +22,7 @@ const PUBLIC_ROUTES = new Set([
17
22
  ]);
18
23
 
19
24
  const { ctmPort: resolveCtmPort } = require('../lib/ctm-endpoint');
25
+ const { getBlogViewOrigin, isBlogViewsPath } = require('../lib/blog-view-policy');
20
26
  // WP-7: request provenance comes from the VENDORED standalone copy of the
21
27
  // canonical shared module (byte-identical sibling) — the standalone product
22
28
  // never imports the CTM checkout.
@@ -24,8 +30,19 @@ const { inspectRequestOrigin, isLoopbackRequest } = require('../shared/request-p
24
30
 
25
31
  function getAllowedOrigin(req) {
26
32
  const ctmPort = process.env.CTM_PORT || resolveCtmPort();
27
- if (process.env.CTM_ORIGIN) return process.env.CTM_ORIGIN;
28
33
  const origin = req?.headers?.origin;
34
+ // WP-1 (blog-view-counts, D-4): ROUTE-SCOPED exception for the public blog
35
+ // counter only. The static site is served from walle.sh, so its two exact
36
+ // origins are reflected for this one path — walle.sh is NEVER added to the
37
+ // private routes' set (which stays the local CTM origin), and an unrecognized
38
+ // origin still falls through to the existing local answer.
39
+ //
40
+ // Deliberately FIRST, ahead of the CTM_ORIGIN override below: a private
41
+ // deployment's custom origin setting must never be able to break this exact
42
+ // public route's two published site origins. Every other route still answers
43
+ // with CTM_ORIGIN / the local set exactly as before.
44
+ if (isBlogViewsPath(String(req?.url || '').split('?')[0]) && getBlogViewOrigin(origin)) return origin;
45
+ if (process.env.CTM_ORIGIN) return process.env.CTM_ORIGIN;
29
46
  const allowed = new Set([
30
47
  `http://127.0.0.1:${ctmPort}`,
31
48
  `http://localhost:${ctmPort}`,
@@ -0,0 +1,318 @@
1
+ 'use strict';
2
+
3
+ // WP-1 (docs/blog-view-counts-2026-10-22.html §5 D-4/D-5, §6): the ONE public,
4
+ // unauthenticated blog-view counter endpoint (GET totals for the index, POST one
5
+ // visible article opening) at the exact path the policy module names —
6
+ // /api/wall-e/telemetry/blog-views, which api-walle.js already routes into the
7
+ // isolated telemetry process in the daemon role. Dispatched from
8
+ // http/telemetry-api.js with
9
+ // {getTelemetryDb, errorSink} — the same deps the telemetry routes already get —
10
+ // and deliberately narrow: only the exact path, no query parameters, no cookies,
11
+ // no Set-Cookie, no CORS header of its own (server.js reflects the route-scoped
12
+ // origin from http/auth.js), and never an exception detail in a response body.
13
+ //
14
+ // Bounded by construction: the 1024-byte body cap, a separate in-memory per-IP
15
+ // POST limiter (60/min, 10000 buckets, lazily pruned) and the store's receipt
16
+ // cap. The client's raw address is only ever a limiter key in this process's
17
+ // memory — never persisted, never logged, never forwarded to the error sink.
18
+ //
19
+ // Test seams (no environment flag): callers may inject `now` and a `rateLimiter`
20
+ // built by createBlogViewRateLimiter() for deterministic windows.
21
+
22
+ const net = require('node:net');
23
+ const { readBody } = require('./api-utils');
24
+ const {
25
+ BLOG_VIEW_MAX_BODY_BYTES,
26
+ getBlogViewOrigin,
27
+ isBlogPostSlug,
28
+ isBlogViewsPath,
29
+ isBlogViewToken,
30
+ isKnownBotUserAgent,
31
+ } = require('../lib/blog-view-policy');
32
+ const {
33
+ BLOG_VIEW_CAPACITY,
34
+ BLOG_VIEW_OVERFLOW,
35
+ getBlogViewCounts,
36
+ recordBlogView,
37
+ } = require('../lib/blog-view-store');
38
+
39
+ const BLOG_VIEW_RATE_MAX_PER_WINDOW = 60;
40
+ const BLOG_VIEW_RATE_WINDOW_MS = 60000;
41
+ const BLOG_VIEW_RATE_MAX_BUCKETS = 10000;
42
+ const UNKNOWN_CLIENT_KEY = 'unknown';
43
+
44
+ // Bounded fixed-window limiter. check(key, nowMs) never allocates for a key it
45
+ // refuses, and a full bucket map refuses the NEW key instead of evicting live
46
+ // counters (a flood must not be able to reset everyone's window). Expired buckets
47
+ // are pruned lazily, at most once per window, so no timer is needed.
48
+ function createBlogViewRateLimiter(options = {}) {
49
+ const maxPerWindow = Number.isInteger(options.maxPerWindow) ? options.maxPerWindow : BLOG_VIEW_RATE_MAX_PER_WINDOW;
50
+ const windowMs = Number.isInteger(options.windowMs) ? options.windowMs : BLOG_VIEW_RATE_WINDOW_MS;
51
+ const maxBuckets = Number.isInteger(options.maxBuckets) ? options.maxBuckets : BLOG_VIEW_RATE_MAX_BUCKETS;
52
+ const buckets = new Map();
53
+ let lastPruneMs = 0;
54
+
55
+ function prune(nowMs) {
56
+ if (nowMs - lastPruneMs < windowMs) return;
57
+ lastPruneMs = nowMs;
58
+ for (const [key, bucket] of buckets) {
59
+ if (bucket.startMs + windowMs <= nowMs) buckets.delete(key);
60
+ }
61
+ }
62
+
63
+ function check(key, nowMs) {
64
+ const now = Number.isFinite(nowMs) ? nowMs : Date.now();
65
+ prune(now);
66
+ const existing = buckets.get(key);
67
+ if (!existing) {
68
+ if (buckets.size >= maxBuckets) {
69
+ return { ok: false, retryAfterSeconds: Math.max(1, Math.ceil(windowMs / 1000)) };
70
+ }
71
+ buckets.set(key, { startMs: now, count: 1 });
72
+ return { ok: true, retryAfterSeconds: 0 };
73
+ }
74
+ if (now - existing.startMs >= windowMs) {
75
+ existing.startMs = now;
76
+ existing.count = 1;
77
+ return { ok: true, retryAfterSeconds: 0 };
78
+ }
79
+ existing.count += 1;
80
+ if (existing.count > maxPerWindow) {
81
+ const remainingMs = existing.startMs + windowMs - now;
82
+ return { ok: false, retryAfterSeconds: Math.max(1, Math.ceil(remainingMs / 1000)) };
83
+ }
84
+ return { ok: true, retryAfterSeconds: 0 };
85
+ }
86
+
87
+ return {
88
+ check,
89
+ size: () => buckets.size,
90
+ reset: () => { buckets.clear(); lastPruneMs = 0; },
91
+ };
92
+ }
93
+
94
+ // The process-wide limiter the API dispatch path uses. Exported so a test can
95
+ // reset or exercise it without reaching into module state.
96
+ const blogViewRateLimiter = createBlogViewRateLimiter();
97
+
98
+ function readHeaderString(headers, name) {
99
+ const raw = headers ? headers[name] : undefined;
100
+ return typeof raw === 'string' ? raw : '';
101
+ }
102
+
103
+ // Every counter response is uncacheable at the edge and varies by Origin; none of
104
+ // them carries a cookie or a secret.
105
+ function sendJson(res, status, body, extraHeaders) {
106
+ const payload = JSON.stringify(body);
107
+ res.writeHead(status, {
108
+ 'Content-Type': 'application/json',
109
+ 'Content-Length': Buffer.byteLength(payload),
110
+ 'Cache-Control': 'no-store',
111
+ 'Vary': 'Origin',
112
+ ...(extraHeaders || {}),
113
+ });
114
+ res.end(payload);
115
+ }
116
+
117
+ function isJsonContentType(value) {
118
+ if (typeof value !== 'string') return false;
119
+ const [type, ...parameters] = value.split(';');
120
+ if (type.trim().toLowerCase() !== 'application/json') return false;
121
+ return parameters.every((parameter) => {
122
+ const trimmed = parameter.trim().toLowerCase();
123
+ return trimmed === '' || trimmed.startsWith('charset=');
124
+ });
125
+ }
126
+
127
+ // Fly terminates TLS and forwards the peer it saw in Fly-Client-IP; that header is
128
+ // trusted ONLY when this process really is a Fly app. Otherwise the socket address
129
+ // is the truth. X-Forwarded-For is client-controlled here and is never read.
130
+ function resolveClientIp(req) {
131
+ const headers = (req && req.headers) || {};
132
+ if (process.env.FLY_APP_NAME) {
133
+ const flyClientIp = readHeaderString(headers, 'fly-client-ip').trim().slice(0, 64);
134
+ if (net.isIP(flyClientIp)) return flyClientIp;
135
+ }
136
+ const socketAddress = req && req.socket ? req.socket.remoteAddress : undefined;
137
+ if (typeof socketAddress === 'string') {
138
+ const trimmed = socketAddress.trim().slice(0, 64);
139
+ if (net.isIP(trimmed)) return trimmed;
140
+ }
141
+ // Missing/invalid address: one shared bucket, never a per-garbage-value one.
142
+ return UNKNOWN_CLIENT_KEY;
143
+ }
144
+
145
+ // Strict envelope: exactly {slug, token}, both strings. Anything else (an array,
146
+ // null, extra keys, a nested object) is a malformed payload.
147
+ function parseBlogViewPayload(body) {
148
+ if (!body || typeof body !== 'object' || Array.isArray(body)) return null;
149
+ if (Object.keys(body).length !== 2) return null;
150
+ if (!Object.prototype.hasOwnProperty.call(body, 'slug')) return null;
151
+ if (!Object.prototype.hasOwnProperty.call(body, 'token')) return null;
152
+ if (typeof body.slug !== 'string' || typeof body.token !== 'string') return null;
153
+ return { slug: body.slug, token: body.token };
154
+ }
155
+
156
+ function handleBlogViewsApi(req, res, url, deps = {}) {
157
+ if (!url || !isBlogViewsPath(url.pathname)) return false;
158
+
159
+ const getTelemetryDb = typeof deps.getTelemetryDb === 'function' ? deps.getTelemetryDb : () => null;
160
+ const now = typeof deps.now === 'function' ? deps.now : Date.now;
161
+ const rateLimiter = deps.rateLimiter || blogViewRateLimiter;
162
+ const errorSink = deps.errorSink;
163
+ const headers = (req && req.headers) || {};
164
+
165
+ // The sink receives a static, sanitized error only: no request headers, no
166
+ // payload, no visitor address and no driver message ever reach it.
167
+ function reportRecordFailure() {
168
+ if (!errorSink || typeof errorSink.report !== 'function') return;
169
+ try {
170
+ errorSink.report('wall-e', 'blog_view_record_failed', new Error('blog view record failed'));
171
+ } catch { /* expected: a reporting sink is contractually fail-open and must never break the response it reports on */ }
172
+ }
173
+
174
+ // The ONE 503 shape. `expectedRefusal` marks a policy outcome — a full receipt
175
+ // table or a counter at the safe-integer ceiling — which is answered the same
176
+ // way but is not reported as a failure. Every other error is reported (with a
177
+ // static error object, never the driver's), and the response is written at most
178
+ // once.
179
+ function failClosed(expectedRefusal) {
180
+ if (!expectedRefusal) reportRecordFailure();
181
+ if (!res.headersSent) sendJson(res, 503, { error: 'View count unavailable' });
182
+ }
183
+
184
+ // getTelemetryDb() is a real open: it can DECLINE (telemetry server mode off →
185
+ // a null handle, answered 404 "not enabled") or THROW (an unusable database,
186
+ // answered 503 like every other internal failure). Both are caught here so no
187
+ // path can leak an exception out of the handler — and for BOTH methods.
188
+ function resolveDb() {
189
+ try {
190
+ return { db: getTelemetryDb() || null, thrown: false };
191
+ } catch {
192
+ return { db: null, thrown: true };
193
+ }
194
+ }
195
+
196
+ // Writes the generic refusal for a failed resolveDb(): 503 when the open threw,
197
+ // 404 when the counter is simply not enabled in this deployment mode.
198
+ function refuseUnavailable(resolved) {
199
+ if (resolved.thrown) failClosed(false);
200
+ else if (!res.headersSent) sendJson(res, 404, { error: 'View count unavailable' });
201
+ }
202
+
203
+ // The counter takes no query string at all: url.search is empty or the request
204
+ // is malformed (a cache-buster or an extra parameter is not a documented input,
205
+ // and silently ignoring one would make the surface wider than the design's
206
+ // "no queries"). Refused for both methods, before anything is read or written.
207
+ if (url.search) {
208
+ sendJson(res, 400, { error: 'query_not_allowed' });
209
+ return true;
210
+ }
211
+
212
+ const method = String(req.method || '').toUpperCase();
213
+ const hasOrigin = Object.prototype.hasOwnProperty.call(headers, 'origin');
214
+ const allowedOrigin = getBlogViewOrigin(readHeaderString(headers, 'origin'));
215
+
216
+ if (method === 'GET') {
217
+ // The public listing may be read without an Origin; a present Origin must be
218
+ // exactly one of the two published site origins.
219
+ if (hasOrigin && !allowedOrigin) {
220
+ sendJson(res, 403, { error: 'origin_not_allowed' });
221
+ return true;
222
+ }
223
+ const resolved = resolveDb();
224
+ if (!resolved.db) {
225
+ refuseUnavailable(resolved);
226
+ return true;
227
+ }
228
+ try {
229
+ sendJson(res, 200, { views: getBlogViewCounts(resolved.db) });
230
+ } catch {
231
+ failClosed(false);
232
+ }
233
+ return true;
234
+ }
235
+
236
+ if (method !== 'POST') {
237
+ sendJson(res, 405, { error: 'method_not_allowed' }, { Allow: 'GET, POST' });
238
+ return true;
239
+ }
240
+
241
+ // A write must come from the published site itself and be JSON.
242
+ if (!allowedOrigin) {
243
+ sendJson(res, 403, { error: 'origin_not_allowed' });
244
+ return true;
245
+ }
246
+ if (!isJsonContentType(readHeaderString(headers, 'content-type'))) {
247
+ sendJson(res, 415, { error: 'unsupported_media_type' });
248
+ return true;
249
+ }
250
+ const gate = rateLimiter.check(resolveClientIp(req), now());
251
+ if (!gate.ok) {
252
+ sendJson(res, 429, { error: 'rate_limited' }, { 'Retry-After': String(gate.retryAfterSeconds) });
253
+ return true;
254
+ }
255
+
256
+ // Body handling is asynchronous, exactly like the existing ingest route: the
257
+ // request is already claimed, so the boolean is true now and the response is
258
+ // written when the bounded read finishes.
259
+ readBody(req, { maxBytes: BLOG_VIEW_MAX_BODY_BYTES }).then((body) => {
260
+ try {
261
+ const payload = parseBlogViewPayload(body);
262
+ if (!payload) {
263
+ sendJson(res, 400, { error: 'invalid_payload' });
264
+ return;
265
+ }
266
+ if (!isBlogPostSlug(payload.slug)) {
267
+ sendJson(res, 404, { error: 'unknown_post' });
268
+ return;
269
+ }
270
+ if (!isBlogViewToken(payload.token)) {
271
+ sendJson(res, 400, { error: 'invalid_payload' });
272
+ return;
273
+ }
274
+ const resolved = resolveDb();
275
+ if (!resolved.db) {
276
+ refuseUnavailable(resolved);
277
+ return;
278
+ }
279
+ if (isKnownBotUserAgent(readHeaderString(headers, 'user-agent'))) {
280
+ // A recognized crawler is answered honestly with the current total and no
281
+ // write at all — the counter never claims a view it did not record.
282
+ sendJson(res, 200, { slug: payload.slug, views: getBlogViewCounts(resolved.db)[payload.slug], counted: false });
283
+ return;
284
+ }
285
+ const result = recordBlogView(resolved.db, payload.slug, payload.token, now());
286
+ sendJson(res, 200, { slug: result.slug, views: result.views, counted: result.counted });
287
+ } catch (error) {
288
+ // Every INTERNAL exception lands here — a store/driver failure, a broken
289
+ // getTelemetryDb(), a failed query — and answers 503. Capacity/overflow are
290
+ // policy refusals: identical body, no failure report. Neither leaks a driver
291
+ // message, and the `headersSent` guard in failClosed() keeps this to one
292
+ // response even when a body write already started.
293
+ failClosed(!!error && (error.code === BLOG_VIEW_CAPACITY || error.code === BLOG_VIEW_OVERFLOW));
294
+ }
295
+ }).catch((error) => {
296
+ // readBody rejects ONLY on a client-side body problem: an oversized body is a
297
+ // 413, an aborted or malformed one a 400. The fulfillment handler above never
298
+ // rethrows (it answers 503 itself), so nothing internal can be misclassified
299
+ // as a client error here.
300
+ if (res.headersSent) return;
301
+ if (error && error.code === 'BODY_TOO_LARGE') {
302
+ sendJson(res, 413, { error: 'payload_too_large' });
303
+ return;
304
+ }
305
+ // A malformed/aborted body is a client problem, never a 500 with details.
306
+ sendJson(res, 400, { error: 'invalid_payload' });
307
+ });
308
+ return true;
309
+ }
310
+
311
+ module.exports = {
312
+ BLOG_VIEW_RATE_MAX_BUCKETS,
313
+ BLOG_VIEW_RATE_MAX_PER_WINDOW,
314
+ BLOG_VIEW_RATE_WINDOW_MS,
315
+ blogViewRateLimiter,
316
+ createBlogViewRateLimiter,
317
+ handleBlogViewsApi,
318
+ };