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
@@ -22,6 +22,11 @@ function closedReceipt(stream) {
22
22
  return stream.closed ? Promise.resolve() : new Promise((resolve) => stream.once('close', resolve));
23
23
  }
24
24
 
25
+ // A hit refreshes the entry's mtime (LRU order), but a shared 4 GiB cache does not need 5 s LRU
26
+ // granularity: touching at most once per minute per file drops mtime-only writes by ~12x while keeping
27
+ // the eviction order of multi-GB entries unchanged (fsevents-churn D-10b).
28
+ const TOUCH_MIN_INTERVAL_MS = 60_000;
29
+
25
30
  function createDecodedFrameCache({ withDecoder, cacheRoot, cacheMax, now, frameFile, pruneCache, codeError }) {
26
31
  const entries = new Map();
27
32
 
@@ -34,7 +39,12 @@ function createDecodedFrameCache({ withDecoder, cacheRoot, cacheMax, now, frameF
34
39
  }
35
40
 
36
41
  async function touch(handle, opts) {
37
- const when = new Date(now(opts));
42
+ const st = await handle.stat();
43
+ const at = now(opts);
44
+ // The inode's mtime IS the last-touch record: entries are dropped from the map when their last
45
+ // lease closes, so anything in memory would reset on every acquisition cycle (model-sync, 5 s).
46
+ if (at - st.mtimeMs < TOUCH_MIN_INTERVAL_MS) return; // fresh enough: a touch this minute already happened
47
+ const when = new Date(at);
38
48
  await handle.utimes(when, when);
39
49
  }
40
50
 
@@ -241,4 +251,4 @@ function createDecodedFrameCache({ withDecoder, cacheRoot, cacheMax, now, frameF
241
251
  return { acquireDecodedFrame };
242
252
  }
243
253
 
244
- module.exports = { createDecodedFrameCache };
254
+ module.exports = { createDecodedFrameCache, TOUCH_MIN_INTERVAL_MS };
@@ -24,12 +24,35 @@ const ARCHIVE_INTERVAL_MS = 60 * 60 * 1000;
24
24
  const SCRUB_INTERVAL_MS = 24 * 60 * 60 * 1000;
25
25
  const ARCHIVE_TIMEOUT_MS = 60 * 60 * 1000; // a multi-GB compress + verify outlasts the 10 minute default
26
26
 
27
+ // D-24/WP-6: the counters the `restoredOriginals` recovery result carries, flattened for the job result and
28
+ // its log line. A cycle that skipped the phase (or a guard that never reported one) counts as all zeros.
29
+ function _restoredCounts(restoredOriginals) {
30
+ const r = restoredOriginals || {};
31
+ const count = (v) => (Array.isArray(v) ? v.length : 0);
32
+ return {
33
+ units: Number(r.restoredUnits) || 0,
34
+ files: Number(r.restoredFiles) || 0,
35
+ linked: Number(r.linkedFromTrash) || 0,
36
+ decodedBytes: Number(r.decodedBytes) || 0,
37
+ deferredLowDisk: Number(r.deferredLowDisk) || 0,
38
+ targetExists: count(r.targetExists),
39
+ failed: count(r.failed),
40
+ };
41
+ }
42
+
27
43
  // Scheduler job `transcript-archive` (D-16..D-19, D-23): archive idle transcripts, purge the trash after the
28
44
  // grace period, scrub archives once a day. `env` supplies what only the host knows:
29
45
  // paths() -> { projectsRoot, codexSessionsDir, walleSessionsDir, trashRoot, dataDir? }
30
46
  // liveCtmSessionIds() -> ids of CTM sessions with a live PTY (a live session owns its transcript)
31
47
  // hostShedding() -> true while the load-pressure controller is shedding work
32
- function registerArchiveJob(scheduler, guard, env, shouldRun) {
48
+ // now() -> (optional, test seam) ms clock; defaults to Date.now
49
+ // log(line) -> (optional, test seam) one-line sink; defaults to console.log
50
+ // WP-6 observability: a skipped cycle and an empty cycle used to look identical in the logs. Every primary run
51
+ // now logs exactly one `ran` line, and a run that the `shouldRun` guard held back logs a `deferred` line at most
52
+ // once per ARCHIVE_INTERVAL_MS -- with how long it has been waiting and why. A successful run resets the
53
+ // deferral state. `deferReason` (5th argument, optional) is a function returning a short reason string ('' when
54
+ // none); without it the reason is 'guard'.
55
+ function registerArchiveJob(scheduler, guard, env, shouldRun, deferReason) {
33
56
  let first = true;
34
57
  let claudeAfter = '';
35
58
  let codexAfter = '';
@@ -37,6 +60,26 @@ function registerArchiveJob(scheduler, guard, env, shouldRun) {
37
60
  let claudeDigestAfter = '';
38
61
  let digestAfter = '';
39
62
  let lastScrubAt = 0;
63
+ const nowMs = () => (typeof env.now === 'function' ? Number(env.now()) : Date.now());
64
+ const log = typeof env.log === 'function' ? env.log : console.log;
65
+ // Deferral state (WP-6): startedAt = when the guard first held a cycle back, checks/reasons = what it saw,
66
+ // loggedAt = the last `deferred` line, so a long deferral is one line per interval, not one per tick.
67
+ const defer = { startedAt: 0, checks: 0, reasons: new Map(), loggedAt: 0 };
68
+ const resetDefer = () => { defer.startedAt = 0; defer.checks = 0; defer.reasons.clear(); defer.loggedAt = 0; };
69
+ const guardedShouldRun = typeof shouldRun === 'function' ? (...args) => {
70
+ if (shouldRun(...args)) return true;
71
+ const now = nowMs();
72
+ if (!defer.startedAt) defer.startedAt = now;
73
+ defer.checks += 1;
74
+ const reason = String((typeof deferReason === 'function' ? deferReason() : '') || 'guard');
75
+ defer.reasons.set(reason, (defer.reasons.get(reason) || 0) + 1);
76
+ if (!defer.loggedAt || now - defer.loggedAt >= ARCHIVE_INTERVAL_MS) {
77
+ defer.loggedAt = now;
78
+ const reasons = Array.from(defer.reasons, ([name, count]) => `${name}:${count}`).join(',');
79
+ log(`[transcript-archive] deferred ${Math.floor((now - defer.startedAt) / 60000)}m checks=${defer.checks} reasons=${reasons}`);
80
+ }
81
+ return false;
82
+ } : shouldRun;
40
83
  scheduler.register({
41
84
  name: 'transcript-archive',
42
85
  heavy: true,
@@ -44,10 +87,10 @@ function registerArchiveJob(scheduler, guard, env, shouldRun) {
44
87
  startDelayMs: 10 * 60 * 1000,
45
88
  priority: 9,
46
89
  pool: 'io',
47
- shouldRun,
90
+ shouldRun: guardedShouldRun,
48
91
  run: async () => {
49
92
  if (typeof guard.isPrimary !== 'function' || !guard.isPrimary()) return { skipped: { not_primary: 1 } };
50
- const now = Date.now();
93
+ const now = nowMs();
51
94
  const scrub = now - lastScrubAt >= SCRUB_INTERVAL_MS;
52
95
  const result = await guard.run('archive', {
53
96
  isPrimary: true,
@@ -77,9 +120,17 @@ function registerArchiveJob(scheduler, guard, env, shouldRun) {
77
120
  digestAfter = digest.nextCodexDigestAfter || '';
78
121
  }
79
122
  const { archived, bytesIn, bytesOut, trashed, purged, verified, corrupt, skipped } = result;
123
+ const restored = _restoredCounts(result.restoredOriginals);
124
+ const errors = (result.errors || []).length + (digest ? digest.errors.length : 0);
125
+ const waitedMs = defer.startedAt ? Math.max(0, nowMs() - defer.startedAt) : 0;
126
+ log(`[transcript-archive] ran archived=${Number(archived) || 0} restoredUnits=${restored.units} `
127
+ + `restoredFiles=${restored.files} linked=${restored.linked} decodedBytes=${restored.decodedBytes} `
128
+ + `deferredLowDisk=${restored.deferredLowDisk} targetExists=${restored.targetExists} `
129
+ + `restoreFailed=${restored.failed} errors=${errors} waitedMs=${waitedMs}`);
130
+ resetDefer();
80
131
  return {
81
132
  archived, bytesIn, bytesOut, trashed, purged, verified, corrupt, skipped, digested: digest ? digest.digested : 0,
82
- errors: (result.errors || []).length + (digest ? digest.errors.length : 0),
133
+ errors, restored,
83
134
  };
84
135
  },
85
136
  });
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ // WP-2 (worker-transcript-link-derived-not-discovered-2026-10-25.html §5 D-4/D-7, §6, §8):
4
+ // the resume preflight predicts a Claude worker's transcript path from (cwd, agent_session_id)
5
+ // when agent_sessions.jsonl_path is empty, and must VERIFY the prediction before it links it --
6
+ // a predicted path is not evidence. This module is the one place that verification happens.
7
+ //
8
+ // It is the only module besides lib/transcript-source.js that names a transcript path, and it
9
+ // touches that path ONLY through transcript-source functions: `require('fs')` here would be a
10
+ // new site for bin/transcript-open-lint.js (the read has to stay on the one sanctioned reader,
11
+ // so archive/trash states, sibling `.zst` archives and the manifest all resolve identically).
12
+
13
+ const transcriptSource = require('./transcript-source');
14
+
15
+ // 64 KiB: enough to reach the session id in Claude Code's first record while never pulling a
16
+ // multi-megabyte transcript into memory. It is NOT a bound on the first LINE: a measured 34 of
17
+ // the 300 most recent real transcripts start with a single `queue-operation` record of 66 KB to
18
+ // 387 KB, so the head routinely ends mid-record. Only the `sessionId` key is read out of it
19
+ // (a bounded scan, never a JSON.parse of the whole record, which such a line would fail).
20
+ const HEAD_BYTES = 65536;
21
+
22
+ // Claude Code writes `"sessionId"` before any user content in the first record. Measured offset in
23
+ // real transcripts: byte 87. The value is a session uuid, so no escapes are possible.
24
+ const SESSION_ID_RE = /"sessionId"\s*:\s*"([^"]*)"/;
25
+
26
+ /**
27
+ * Verify that `derivedPath` really is the transcript of `agentSessionId`.
28
+ *
29
+ * States (the caller reports them verbatim in a refusal, so the lead can tell "the file is not
30
+ * where Claude writes it" from "the file is there but belongs to someone else"):
31
+ * plain -- a local file whose first JSON line names this session (ok: true)
32
+ * missing -- nothing at the path (or the resolver itself threw)
33
+ * archived -- resolved to an archived/trashed source; resume restores those by path, so a
34
+ * predicted path that is not plain is never linked
35
+ * unreadable -- the head could not be read, or its first line names no sessionId at all
36
+ * (empty file, non-JSON bytes, a record without the key)
37
+ * id-mismatch -- readable, but its sessionId is a different session
38
+ *
39
+ * @returns {Promise<{ok: boolean, state: string, headSessionId: string|null}>}
40
+ */
41
+ async function verifyClaudeTranscriptAt(derivedPath, agentSessionId, {
42
+ resolveTranscriptSource = transcriptSource.resolveTranscriptSource,
43
+ readTranscriptHead = transcriptSource.readTranscriptHead,
44
+ } = {}) {
45
+ let source = null;
46
+ try {
47
+ source = await resolveTranscriptSource(derivedPath);
48
+ } catch {
49
+ // expected: an unreadable/absent path is exactly the "no transcript there" answer
50
+ return { ok: false, state: 'missing', headSessionId: null };
51
+ }
52
+ if (!source || source.state === 'missing') return { ok: false, state: 'missing', headSessionId: null };
53
+ // Anything not plain (archived, trashed, ...) is reported as archived: the resolve path below
54
+ // restores an archived transcript at the LINKED path, and a derived path has no link yet.
55
+ if (source.state !== 'plain') return { ok: false, state: 'archived', headSessionId: null };
56
+
57
+ let head = '';
58
+ try {
59
+ head = String(await readTranscriptHead(derivedPath, HEAD_BYTES));
60
+ } catch {
61
+ // expected: an unreadable/absent file -- unverifiable, never a link
62
+ return { ok: false, state: 'unreadable', headSessionId: null };
63
+ }
64
+
65
+ // Scan the first line only (or the whole head when it is one unterminated line, i.e. the record
66
+ // is longer than HEAD_BYTES). Never parse the record: a first line over 64 KiB is normal and
67
+ // would throw. The key precedes user content, so the first match is the record's own session id.
68
+ const firstLine = head.split('\n')[0];
69
+ const match = SESSION_ID_RE.exec(firstLine);
70
+ if (!match) return { ok: false, state: 'unreadable', headSessionId: null };
71
+
72
+ const headSessionId = match[1];
73
+ if (headSessionId !== String(agentSessionId || '')) return { ok: false, state: 'id-mismatch', headSessionId };
74
+ return { ok: true, state: 'plain', headSessionId };
75
+ }
76
+
77
+ module.exports = { verifyClaudeTranscriptAt };
@@ -42,14 +42,18 @@ function _row(input) {
42
42
  const ALL_STATES = Object.freeze([...ARCHIVE_STATES, ...LEGACY_STATES]);
43
43
 
44
44
  // WP-9. Rows in any of `states`, optionally changed before `olderThanMs` (state_changed_at_ms) or with a source
45
- // mtime before `mtimeBeforeMs`. orderBy: 'mtime' (oldest source first) | 'verified' (least recently verified
46
- // first) | 'changed' (oldest state change first, the default). Bounded to 2000 rows.
45
+ // mtime before `mtimeBeforeMs`. orderBy: 'mtime' (oldest source first) | 'mtime_desc' (newest source first, the
46
+ // WP-2 recovery order) | 'verified' (least recently verified first) | 'changed' (oldest state change first, the
47
+ // default). Bounded to 2000 rows.
47
48
  function listByState(db, { states = [], olderThanMs = 0, mtimeBeforeMs = 0, orderBy = 'changed', limit = 500 } = {}) {
48
49
  const wanted = (states || []).map(String).filter((s) => ALL_STATES.includes(s));
49
50
  if (!wanted.length) return [];
50
51
  const bounded = Math.max(1, Math.min(2000, Number(limit) || 500));
51
- const order = { mtime: 'source_mtime_ms, source_path', verified: 'last_verified_at_ms, source_path' }[orderBy]
52
- || 'state_changed_at_ms, source_path';
52
+ const order = {
53
+ mtime: 'source_mtime_ms, source_path',
54
+ mtime_desc: 'source_mtime_ms DESC, source_path',
55
+ verified: 'last_verified_at_ms, source_path',
56
+ }[orderBy] || 'state_changed_at_ms, source_path';
53
57
  const marks = wanted.map(() => '?').join(',');
54
58
  const where = [`state IN (${marks})`];
55
59
  const params = [...wanted];
@@ -22,7 +22,7 @@ const { finished } = require('node:stream/promises');
22
22
  const zlib = require('node:zlib');
23
23
  const { decodeFrame, waitForDecodedFrameClose, decompressParams } = require('./transcript-archive-codec');
24
24
  const { getDataDir } = require('./data-dir');
25
- const { createDecodedFrameCache } = require('./transcript-frame-cache');
25
+ const { createDecodedFrameCache, TOUCH_MIN_INTERVAL_MS } = require('./transcript-frame-cache');
26
26
 
27
27
  const CACHE_DIR_NAME = 'transcript-archive-cache';
28
28
  const DEFAULT_CACHE_MAX_BYTES = 4 * 1024 * 1024 * 1024;
@@ -1109,8 +1109,13 @@ function _decodeFrameSync(archive, index, opts, learnLength) {
1109
1109
  // The cached bytes are this request's decoded bytes: charge them before returning, so a warm
1110
1110
  // cache hit is accounted for exactly like a cold decode would be.
1111
1111
  if (ledger && learnLength) _chargeLearnedDecoded(ledger, `${archive.cacheKey}:${index}`, hit.size);
1112
- const when = new Date(_now(opts));
1113
- fs.utimesSync(file, when, when);
1112
+ // Same rule as the async cache's touch (D-10b): the hit's own mtime is the last-touch record, so a
1113
+ // file touched in the last minute is left alone instead of paying a utimes write per read.
1114
+ const at = _now(opts);
1115
+ if (at - hit.mtimeMs >= TOUCH_MIN_INTERVAL_MS) {
1116
+ const when = new Date(at);
1117
+ fs.utimesSync(file, when, when);
1118
+ }
1114
1119
  return file;
1115
1120
  }
1116
1121
  const max = _cacheMax(opts);
@@ -59,6 +59,11 @@ function setErrorSink(sink) {
59
59
  }
60
60
  const { shouldYieldAfterChunk } = require('./conversation-import/chunk-runner');
61
61
  const { createImportMetrics } = require('./conversation-import/import-metrics');
62
+ // WP-2 (D-4): the ingest lane already parses every record, so the agent's own session name
63
+ // (`ai-title` / `custom-title`) is captured here and applied at the ingestJsonlFile choke point.
64
+ const { extractTitleSignalFromRecord } = require('./session-title-signals');
65
+ const { getAgentCapabilities } = require('./agent-capabilities');
66
+ const { applyAgentTitleSignals } = require('./agent-title-apply');
62
67
 
63
68
  const PARSER_VERSION = 3;
64
69
  const DEFAULT_INITIAL_TAIL_BYTES = 2 * 1024 * 1024;
@@ -1008,6 +1013,16 @@ function claudeMessagesFromEntry(entry, state = {}) {
1008
1013
  if (queued) entry = queued;
1009
1014
  if (shouldDropInheritedClaudeEntry(entry, state)) return [];
1010
1015
 
1016
+ // WP-2 (D-4): a `custom-title`/`ai-title` record produces no rows, but CTM is already reading
1017
+ // every record here — keep the last non-inherited one of each kind so ingestJsonlFile can apply
1018
+ // it (lib/agent-title-apply.js). Last wins; an ai-title never clears a custom-title.
1019
+ const titleSignal = extractTitleSignalFromRecord(entry);
1020
+ if (titleSignal.userTitle || titleSignal.providerTitle) {
1021
+ if (!state.agentTitleSignals) state.agentTitleSignals = {};
1022
+ if (titleSignal.userTitle) state.agentTitleSignals.customTitle = titleSignal.userTitle;
1023
+ else state.agentTitleSignals.aiTitle = titleSignal.providerTitle;
1024
+ }
1025
+
1011
1026
  if (entry.type === 'system' && entry.subtype === 'compact_boundary'
1012
1027
  && structuredCapture.structuredCaptureEnabled()) {
1013
1028
  const compactMeta = entry.compactMetadata || {};
@@ -1458,6 +1473,8 @@ function healSelfLinkedTranscriptRows(db, agentSessionId, ownerCtmSessionId) {
1458
1473
  try {
1459
1474
  const f = db.prepare('UPDATE transcript_files SET ctm_session_id=? WHERE agent_session_id=? AND ctm_session_id=?').run(owner, agentId, agentId);
1460
1475
  const e = db.prepare('UPDATE transcript_events SET ctm_session_id=? WHERE agent_session_id=? AND ctm_session_id=?').run(owner, agentId, agentId);
1476
+ // Rows whose owner changed used to change silently. Ids only, truncated; never a path, never content.
1477
+ if (f.changes || e.changes) console.log(`[transcript-owner] re-keyed self-link agent=${agentId.slice(0, 8)} -> owner=${owner.slice(0, 8)} files=${f.changes || 0} events=${e.changes || 0}`);
1461
1478
  return { files: f.changes || 0, events: e.changes || 0 };
1462
1479
  } catch { return { files: 0, events: 0 }; }
1463
1480
  }
@@ -1688,6 +1705,10 @@ function parseTranscriptTail(options = {}) {
1688
1705
  return {
1689
1706
  ...base,
1690
1707
  messages: projected.messages,
1708
+ // WP-2 (D-4): the agent-name signal rides with the parsed window so both the inline ingest
1709
+ // and the read-pool → preParsed apply path (workers/read-pool-worker.js parseTranscriptDelta)
1710
+ // carry it without a second read.
1711
+ agentTitleSignals: state.agentTitleSignals || null,
1691
1712
  metrics,
1692
1713
  read: { nextOffset: read.nextOffset, partialLine: read.partialLine || '', bytesRead: read.bytesRead },
1693
1714
  };
@@ -2034,6 +2055,8 @@ function _ingestJsonlFileV2(db, options = {}) {
2034
2055
  agentSessionId,
2035
2056
  generation: 1,
2036
2057
  fileSize: stat.size,
2058
+ // WP-2 (D-4): the parsed window's agent-name signal (title records produce no rows).
2059
+ agentTitleSignals: (preParsed || parsed).agentTitleSignals || null,
2037
2060
  oversizedNonRetainedRecordsSkipped,
2038
2061
  metrics: {
2039
2062
  ...(parsed.metrics || {}),
@@ -2466,6 +2489,8 @@ function _ingestJsonlFile(db, options = {}) {
2466
2489
 
2467
2490
  let read;
2468
2491
  let messages;
2492
+ // WP-2 (D-4): the agent-name signal (title records produce no rows) from whichever parse ran.
2493
+ let agentTitleSignals = null;
2469
2494
  if (preParsed) {
2470
2495
  // Off-thread parse already produced the delta (parseTranscriptTail) — apply is writes only.
2471
2496
  read = {
@@ -2474,6 +2499,7 @@ function _ingestJsonlFile(db, options = {}) {
2474
2499
  bytesRead: Number(preParsed.read?.bytesRead || 0),
2475
2500
  };
2476
2501
  messages = Array.isArray(preParsed.messages) ? preParsed.messages : [];
2502
+ agentTitleSignals = preParsed.agentTitleSignals || null;
2477
2503
  } else {
2478
2504
  const maxBytes = Math.min(
2479
2505
  Math.max(0, readCeiling - cursor.offset),
@@ -2501,6 +2527,7 @@ function _ingestJsonlFile(db, options = {}) {
2501
2527
  });
2502
2528
  metrics.parseMs += Math.max(0, Date.now() - parseStartedAt);
2503
2529
  messages = projected.messages;
2530
+ agentTitleSignals = state.agentTitleSignals || null;
2504
2531
  metrics.recordsParsed += projected.recordsParsed;
2505
2532
  }
2506
2533
  metrics.sourceBytesRead += Number(read.bytesRead || 0);
@@ -3266,6 +3293,7 @@ function _ingestJsonlFile(db, options = {}) {
3266
3293
  values: insertChunkStatementCounts,
3267
3294
  },
3268
3295
  overBudget: elapsedMs > IMPORT_SLICE_HARD_MAX_MS,
3296
+ agentTitleSignals,
3269
3297
  messages: partial ? messages.slice(0, processedCount) : messages,
3270
3298
  rowCursorDelta: {
3271
3299
  ctmSessionId,
@@ -3367,6 +3395,18 @@ function ingestJsonlFile(db, options = {}) {
3367
3395
  result,
3368
3396
  coreDb: options.coreDb,
3369
3397
  });
3398
+ // WP-2 (D-4/D-5/D-9, S-17): the single choke point for every ingest (event delta, targeted
3399
+ // import, v13 migration) — apply the agent's own name to its CTM row. Gated on the provider
3400
+ // declaring it reads ai-titles; a refused delta applied nothing, so it has no signal to apply.
3401
+ // D-11: the caller publishes the outcome (titleApply) on the main thread.
3402
+ if (result && !result.refused && result.agentTitleSignals && ownerCtmSessionId
3403
+ && getAgentCapabilities(normalizeProvider(options.provider, filePath)).titleSpec?.readsAiTitle) {
3404
+ result.titleApply = applyAgentTitleSignals(options.coreDb || db, {
3405
+ ctmSessionId: ownerCtmSessionId,
3406
+ provider: normalizeProvider(options.provider, filePath),
3407
+ ...result.agentTitleSignals,
3408
+ });
3409
+ }
3370
3410
  return result;
3371
3411
  }
3372
3412
  function dryRunTranscriptByteBackfill(db, options = {}) {
@@ -65,10 +65,13 @@ function _socketBytesWritten(socket) {
65
65
  // The traffic class of a message, from the channel the caller already resolved (`opts.channel`
66
66
  // || the decoded `type`) plus the class `_wsSend` may have resolved explicitly (`opts.class` —
67
67
  // the live-status heartbeat sends `session-activity` as `{ class: 'stream', channel: 'heartbeat' }`).
68
- // `classifyWsPayload` stays the one authority for the class split.
68
+ // T2 (docs/walle-turn-lifecycle-delivery-2026-10-26.html §3): the classification is delegated with
69
+ // the ACTUAL payload plus the caller's class and channel — a synthetic `{ type: channel }` object
70
+ // threw away the lifecycle fields (recordType/partType) that make a walle-transcript-append opener
71
+ // a control notification rather than a replaceable sample, which in economy let progress replace a
72
+ // queued opener. `classifyWsPayload` stays the one authority for the class split.
69
73
  function _channelClass(channel, payload, cls) {
70
- const ch = String(channel || '');
71
- return classifyWsPayload(ch ? { type: ch } : payload, cls);
74
+ return classifyWsPayload(payload, cls, channel);
72
75
  }
73
76
 
74
77
  function createTunnelLaneGovernor({ now = Date.now, setIntervalFn = setInterval, clearIntervalFn = clearInterval, send } = {}) {
@@ -17,6 +17,12 @@ const STAGES = Object.freeze([
17
17
  // echoed). Required (not in OPTIONAL_STAGES below), same as 'pty-written'.
18
18
  'host-posted',
19
19
  'pty-written',
20
+ // WP-4 (clock-free-tests-followups-r4-2026-10-24.html §5 R-9, §6.5): the host's OWN receipt
21
+ // of the PTY batch -- the instant its first byte entered the mirror-feed coalescer, carried on
22
+ // the 'feed' op as `hostReceivedAt`. `pty-output` below is the MAIN-thread arrival, which is a
23
+ // different clock hop; neither is causal for the other (every post-`pty-written` stage is
24
+ // "first X after the key"). Optional, like its neighbours.
25
+ 'host-pty-data',
20
26
  'pty-output',
21
27
  // WP-3 D-C2: the two shadow, per-key echo matchers -- VS Code typeahead's byte rule and
22
28
  // mosh's cell rule (see lib/typing-worker-stages.js, which runs them in the mirror). Both are
@@ -34,6 +40,19 @@ const STAGES = Object.freeze([
34
40
  'mirror-written',
35
41
  'emit-offered',
36
42
  'emit-refused',
43
+ // WP-4 (clock-free-tests-followups-r3-2026-10-24.html §5 R-7, §6 WP-4): stamped by the worker
44
+ // (lib/typing-worker-stages.js's noteFramePostedForEchoed, called from emitSettled) for exactly
45
+ // the pending ids the frame that just SHIPPED already covers (their own inputSeq <= that
46
+ // frame's echoedThroughSeq, carried as `offeredEchoSeq`). Optional, like its neighbours above:
47
+ // a key whose frame is refused, or that is delivered without ever being pending, never gets it.
48
+ 'frame-posted',
49
+ // WP-4 (clock-free-tests-followups-r4-2026-10-24.html §5 R-7/R-8, §6.5): the FIRST main-thread
50
+ // touch of the frame that carries this key's echo (`session-host-manager.js`'s
51
+ // `_stampMainArrival`), split out from `mirror-revision` (where main stamped the interaction)
52
+ // to price the receiver-loop leg alone. Carries `mainSpans` (R-8) -- the top recent main-thread
53
+ // spans overlapping [frame-posted.at, frame-arrived.at]. Optional, same reason as
54
+ // `frame-posted`: a key whose frame was refused, or that never had a pending row, never gets it.
55
+ 'frame-arrived',
37
56
  'mirror-revision',
38
57
  'frame-sent',
39
58
  'socket-received',
@@ -67,6 +86,14 @@ const STAGE_INDEX = new Map(STAGES.map((stage, index) => [stage, index]));
67
86
  // WP-3 D-C2: 'echo-bytes'/'echo-cell' join them -- they are shadow stamps most keys never get.
68
87
  const OPTIONAL_STAGES = new Set([
69
88
  'pty-output', 'worker-received', 'mirror-written', 'emit-offered', 'emit-refused',
89
+ // WP-4 (R-7): 'frame-posted' joins them -- its absence must never move
90
+ // lastCompletedBoundary/missingBoundary for a row that shipped its frame before anything was
91
+ // pending (the common case).
92
+ 'frame-posted',
93
+ // WP-4 r4 (R-7/R-8/R-9): 'frame-arrived' and 'host-pty-data' join for the same reason -- both
94
+ // are first-touch stamps on paths most interactions never take (a refused frame; a host whose
95
+ // coalescer delivered before any keystroke was pending), so neither may end the boundary walk.
96
+ 'frame-arrived', 'host-pty-data',
70
97
  'echo-bytes', 'echo-cell',
71
98
  ]);
72
99
  const WORKLOAD_FIELDS = Object.freeze([
@@ -85,6 +112,27 @@ function boundedText(value, max = 80) {
85
112
  return String(value || '').replace(/[^A-Za-z0-9_.:-]/g, '').slice(0, max);
86
113
  }
87
114
 
115
+ // WP-4 r4 (R-8, §6.5): R-8's floor -- mainSpans are computed only for a frame-posted ->
116
+ // frame-arrived window at or above this, so the fast path allocates nothing.
117
+ const MAIN_SPANS_MIN_WINDOW_MS = 15;
118
+
119
+ // The `frame-arrived` record's mainSpans shape: a malformed entry is DROPPED rather than repaired
120
+ // into a plausible-looking 0 -- a reader may only ever see entries the sender actually produced.
121
+ // Name is bounded to 120 chars and overlapMs floored to a non-negative integer, and the record is
122
+ // the TOP 3 by overlapMs whatever order the sender used: the ranking is part of this record's
123
+ // contract (§6.5), not of the sender's good manners.
124
+ function sanitizeMainSpans(rawSpans) {
125
+ const mainSpans = [];
126
+ for (const entry of Array.isArray(rawSpans) ? rawSpans : []) {
127
+ if (!entry || typeof entry !== 'object') continue;
128
+ const name = String(entry.name == null ? '' : entry.name).slice(0, 120);
129
+ if (!name) continue;
130
+ mainSpans.push({ name, overlapMs: Math.max(0, Math.floor(finite(entry.overlapMs))) });
131
+ }
132
+ mainSpans.sort((a, b) => b.overlapMs - a.overlapMs);
133
+ return mainSpans.slice(0, 3);
134
+ }
135
+
88
136
  function percentile(values, pct) {
89
137
  if (!values.length) return 0;
90
138
  const sorted = values.slice().sort((a, b) => a - b);
@@ -172,6 +220,8 @@ function buildSlowInteractionRow(row, ktpMs, lastStage) {
172
220
  echoBytesMs: stageMsFromKeydown(row, 'echo-bytes'),
173
221
  echoCellMs: stageMsFromKeydown(row, 'echo-cell'),
174
222
  mirrorRevisionMs: stageMsFromKeydown(row, 'mirror-revision'),
223
+ // WP-4 (R-7): the shipped-frame leg, same keydown rule as mirrorRevisionMs above.
224
+ framePostedMs: stageMsFromKeydown(row, 'frame-posted'),
175
225
  frameSentMs: stageMsFromKeydown(row, 'frame-sent'),
176
226
  socketReceivedMs: stageMsFromKeydown(row, 'socket-received'),
177
227
  applyStartedMs: stageMsFromKeydown(row, 'apply-started'),
@@ -221,6 +271,9 @@ function createTypingInteractionTraceStore(options = {}) {
221
271
  // emitting workerLoopMaxMs: 0 / host: {0,0,0} exactly as before this WP.
222
272
  const getRollupExtras = typeof options.getRollupExtras === 'function' ? options.getRollupExtras : null;
223
273
  const rollupIntervalMs = Math.max(1000, finite(options.rollupIntervalMs, 60_000));
274
+ // WP-4 r4 (R-8): `(startMs, endMs) => [{ name, overlapMs }]`, a pure read of the caller's
275
+ // main-thread span ring (server.js `_mainSpansForWindow`). Absent -> no spans are computed.
276
+ const mainSpansForWindow = typeof options.mainSpansForWindow === 'function' ? options.mainSpansForWindow : null;
224
277
  const rows = new Map();
225
278
  const publicRows = new Map();
226
279
  const pendingClosed = new Map();
@@ -320,6 +373,8 @@ function createTypingInteractionTraceStore(options = {}) {
320
373
  workerReceivedMs: stageDelta(row, 'worker-received', 'browser-keydown'),
321
374
  mirrorWrittenMs: stageDelta(row, 'mirror-written', 'browser-keydown'),
322
375
  emitOfferedMs: stageDelta(row, 'emit-offered', 'browser-keydown'),
376
+ // WP-4 (R-7): the shipped-frame leg for the closed interaction, read the same way.
377
+ framePostedMs: stageMsFromKeydown(row, 'frame-posted'),
323
378
  gate,
324
379
  gateHoldMs,
325
380
  clientId: row.clientId,
@@ -453,10 +508,43 @@ function createTypingInteractionTraceStore(options = {}) {
453
508
  rejectedSurfaceEpoch: Math.max(0, finite(event.rejectedSurfaceEpoch)),
454
509
  sweepSkipReason: boundedText(event.sweepSkipReason, 24),
455
510
  };
511
+ } else if (stage === 'emit-offered' || stage === 'frame-posted') {
512
+ // WP-4 (R-7): these two stages carry the OFFERED/SHIPPED frame's echoedThroughSeq, so a
513
+ // reader can compare it against this row's own `inputSeq` (set once by begin(), above) --
514
+ // that comparison is what separates "the offered frame did not yet contain this key's
515
+ // echo" from "worker->main delivery / main-thread queueing". A sender that carries no seq
516
+ // records `{ at }` alone: an absent seq must never read as a real 0.
517
+ // First-wins like every other stage: the `!previous || at < previous.at` guard above owns
518
+ // idempotency, so a later note never rewrites this record.
519
+ const offeredEchoSeq = event.offeredEchoSeq;
520
+ row.stages[stage] = offeredEchoSeq == null
521
+ ? { at }
522
+ : { at, offeredEchoSeq: Math.max(0, Math.floor(finite(offeredEchoSeq))) };
523
+ } else if (stage === 'frame-arrived') {
524
+ // WP-4 r4 (R-8, §6.5): the arrival stamp may carry `mainSpans` -- the top ≤3 main-thread
525
+ // spans overlapping [frame-posted.at, frame-arrived.at], each `{ name, overlapMs }`.
526
+ // Same shape rule as `offeredEchoSeq` above: no array offered means `{ at }` alone (the
527
+ // store may still attach computed spans below, once both stamps are present).
528
+ row.stages[stage] = Array.isArray(event.mainSpans)
529
+ ? { at, mainSpans: sanitizeMainSpans(event.mainSpans) }
530
+ : { at };
456
531
  } else {
457
532
  row.stages[stage] = { at };
458
533
  }
459
534
  }
535
+ // R-8, order-independent: the worker posts `frame-posted` as its own 'typing-stage' message
536
+ // AFTER the frame shipped (headless-term-service.js emitSettled -> noteFramePostedForEchoed),
537
+ // so main usually stamps `frame-arrived` BEFORE it knows `frame-posted`. Compute the spans when
538
+ // the SECOND of the two lands, once per row; the window is in the past either way, and the
539
+ // span ring (lib/perf-tracker.js, 3 s+) still holds it.
540
+ if (mainSpansForWindow && (stage === 'frame-posted' || stage === 'frame-arrived')) {
541
+ const arrival = row.stages['frame-arrived'];
542
+ const postedAt = row.stages['frame-posted']?.at;
543
+ if (arrival && !arrival.mainSpans && Number.isFinite(postedAt)
544
+ && arrival.at - postedAt >= MAIN_SPANS_MIN_WINDOW_MS) {
545
+ arrival.mainSpans = sanitizeMainSpans(mainSpansForWindow(postedAt, arrival.at));
546
+ }
547
+ }
460
548
  if (stage === 'painted' && !previous) captureClosedInteraction(row);
461
549
  row.updatedAt = Math.max(row.updatedAt, at);
462
550
  row.lastCompletedBoundary = '';
@@ -782,6 +870,9 @@ function noteWorkerStageMessage(store, msg = {}) {
782
870
  sessionId: msg.sessionId,
783
871
  stage: msg.stage,
784
872
  at: msg.at,
873
+ // WP-4 (R-7): the offered/shipped frame's echoedThroughSeq rides the same worker message as
874
+ // the stage stamp, so emit-offered/frame-posted rows can be read against their inputSeq.
875
+ offeredEchoSeq: msg.offeredEchoSeq,
785
876
  gate: msg.gate,
786
877
  inFlightSeq: msg.inFlightSeq,
787
878
  inFlightKind: msg.inFlightKind,
@@ -811,4 +902,5 @@ function pendingInteractionIdsFor(byInputSeqMap, limit = 24) {
811
902
 
812
903
  module.exports = {
813
904
  createTypingInteractionTraceStore, STAGES, GATES, noteWorkerStageMessage, pendingInteractionIdsFor,
905
+ MAIN_SPANS_MIN_WINDOW_MS,
814
906
  };
@@ -25,6 +25,18 @@ function mergePendingInteractionIds(existing, incoming, limit = PENDING_IDS_LIMI
25
25
  return merged.slice(-limit);
26
26
  }
27
27
 
28
+ // WP-4 (R-7, clock-free-tests-followups-r3-2026-10-24.html §5 R-7, §6 WP-4): keep the per-id
29
+ // inputSeq map to exactly the ids `pending` still carries, oldest dropped first, capped at
30
+ // `limit` -- the same "keep the most recent N" policy mergePendingInteractionIds applies to the
31
+ // id list itself, so the two can never drift apart.
32
+ function prunePendingInputSeqs(byId, pending, limit = PENDING_IDS_LIMIT) {
33
+ for (const id of Array.from(byId.keys())) {
34
+ if (!pending.includes(id)) byId.delete(id);
35
+ }
36
+ while (byId.size > limit) byId.delete(byId.keys().next().value);
37
+ return byId;
38
+ }
39
+
28
40
  // Post ONE 'typing-stage' message covering every id currently pending for this session -- the
29
41
  // STAGES store's note() (lib/typing-interaction-trace.js) fans it out per-interactionId and is
30
42
  // idempotent per (interactionId, stage), so calling this more than once for the same boundary is
@@ -126,6 +138,22 @@ function armEchoPrediction(entry, msg) {
126
138
  entry.state.pendingTypingInteractionIds, msg.interactionIds,
127
139
  );
128
140
  entry.state.typingIdsAwaitingFeed = true;
141
+ // WP-4 (R-7): remember each pending id's OWN inputSeq here, so emitSettled can tell "the
142
+ // offered frame did not yet contain this key's echo" apart from "worker->main delivery or
143
+ // main-thread queueing" (the (i)/(ii) split design r3 §1.4 says today's stamps cannot make).
144
+ // Earliest seq wins for an id seen twice; the Math.max(0, ...) normalization is the same one
145
+ // headless-term-service.js's 'note-input' handler applies to msg.inputSeq.
146
+ const inputSeq = Math.max(0, Number(msg.inputSeq) || 0);
147
+ const seqById = entry.state.pendingTypingInputSeqById instanceof Map
148
+ ? entry.state.pendingTypingInputSeqById
149
+ : new Map();
150
+ for (const rawId of msg.interactionIds) {
151
+ const id = String(rawId);
152
+ if (!seqById.has(id) || inputSeq < seqById.get(id)) seqById.set(id, inputSeq);
153
+ }
154
+ entry.state.pendingTypingInputSeqById = prunePendingInputSeqs(
155
+ seqById, entry.state.pendingTypingInteractionIds,
156
+ );
129
157
  }
130
158
  const echo = msg.echo;
131
159
  if (!echo || !echo.interactionId || typeof echo.char !== 'string' || !echo.char) return;
@@ -177,9 +205,41 @@ function matchEchoPredictions(postTransport, sessionId, entry, data, now = Date.
177
205
  entry.state.echoPredictions = kept;
178
206
  }
179
207
 
208
+ // WP-4 (R-7), called from headless-term-service.js's emitSettled ONLY after
209
+ // _stateSyncEmitter.onMirrorUpdated returned `sent` -- the frame that just shipped is the first
210
+ // one that could carry these keys' echoes, so stamp `frame-posted` for exactly the pending ids
211
+ // the frame's `echoedThroughSeq` already covers (recorded inputSeq >= 1 AND <= offeredEchoSeq).
212
+ // An id with NO recorded seq is treated as echoed, which preserves the pre-WP-4 blanket clear.
213
+ // Returns the ids that remain un-echoed (recorded seq > offeredEchoSeq) so the caller can keep
214
+ // only those pending -- that is what makes `emit-offered -> frame-posted` readable as "the
215
+ // offered frame DID contain this key's echo" versus "the frame lacked it" instead of collapsing
216
+ // both into one delivery gap.
217
+ function noteFramePostedForEchoed(postTransport, sessionId, entry, offeredEchoSeq) {
218
+ const pending = Array.isArray(entry?.state?.pendingTypingInteractionIds)
219
+ ? entry.state.pendingTypingInteractionIds
220
+ : [];
221
+ const seqById = entry?.state?.pendingTypingInputSeqById;
222
+ const covering = Math.max(0, Number(offeredEchoSeq) || 0);
223
+ const echoedIds = [];
224
+ const remaining = [];
225
+ for (const rawId of pending) {
226
+ const id = String(rawId);
227
+ const seq = seqById instanceof Map && seqById.has(id) ? Number(seqById.get(id)) : 0;
228
+ if (seq > covering) remaining.push(id);
229
+ else echoedIds.push(id);
230
+ }
231
+ noteWorkerTypingStage(postTransport, sessionId, echoedIds, 'frame-posted', { offeredEchoSeq: covering });
232
+ // The echoed ids are delivered; only the un-echoed ones stay pending (and keep their seq).
233
+ if (seqById instanceof Map) {
234
+ for (const id of echoedIds) seqById.delete(id);
235
+ }
236
+ return remaining;
237
+ }
238
+
180
239
  module.exports = {
181
240
  mergePendingInteractionIds,
182
241
  noteWorkerTypingStage,
242
+ noteFramePostedForEchoed,
183
243
  PENDING_IDS_LIMIT,
184
244
  isSingleGraphemeEcho,
185
245
  armEchoPrediction,