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
@@ -23,7 +23,7 @@ const { armAt } = require('./clamped-timer');
23
23
  const admission = require('../worker-admission');
24
24
  const { createWorkerAdmissionExit } = require('./worker-admission-exit');
25
25
  const { createWorkerContinuation, validCallerToolUseId } = require('./worker-continuation');
26
- const { createWorkerObservability, safeToolCounts, progressForGeneration, baselineChanged, statusProgress, currentGenerationReceipts, currentReceiptState, computeExitFailure, boundedWorkerText, isColdDeferredSession, workerHideLogLine, probeCachedFileState, THROTTLED_WINDOW_MS, EVENT_RECEIPT_LIMIT } = require('./worker-observability');
26
+ const { createWorkerObservability, safeToolCounts, progressForGeneration, baselineChanged, statusProgress, currentGenerationReceipts, currentReceiptState, computeExitFailure, boundedWorkerText, isColdDeferredSession, workerHideLogLine, probeCachedFileState, THROTTLED_WINDOW_MS, EVENT_RECEIPT_LIMIT, WORKER_REPORT_TEXT_CAP_BYTES } = require('./worker-observability');
27
27
  const { killSessionProcess } = require('../session-class'); // worker-session-tree WP-2: the one kill authority
28
28
  const { loadWorkerBrief, validateWorkerCwd, validateBriefPath, validateBriefRealpathInsideCwd, renderedBriefPathForSession, renderedBriefWithBudget, ownedPaths: parseOwnedPaths, findScopeOverlap } = require('./worker-brief');
29
29
  const { checkCallerAuthority } = require('./worker-launch-sandbox');
@@ -49,10 +49,27 @@ const EFFORT_VALUES = new Set(['low', 'medium', 'high', 'xhigh']);
49
49
  const { createProxyRouteReadiness, DelegateError, setLeadRouteReadiness, DEFAULT_ROUTE_ALIAS, DEFAULT_MODEL, READINESS_BUDGET_MS } = require('./proxy-route-readiness');
50
50
  const DEFAULT_TOKEN_TTL_MS = 24 * 60 * 60 * 1000 + 10 * 60 * 1000; // session lifetime + 10 min (D-4)
51
51
 
52
+ // D-3 (lead-cost-routing-round2-2026-10-24.html §5 D-3, §6.1/§6.2): the ONE citation counter,
53
+ // shared with wall-e/lib/helper-rules/label.js so the counter and the labeller cannot drift.
54
+ const { countReportCitations } = require('../../../shared/report-citations');
55
+
52
56
  // Only generated UUIDs are accepted before report paths are constructed, preventing encoded
53
57
  // path traversal through GET /api/sessions/:id/worker-status?include_report=1.
54
58
  const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
55
59
 
60
+ // The statuses whose payload carries a citation count; every other status reports null/null
61
+ // because no text has been selected yet (D-3's §6.2 table).
62
+ const WORKER_TERMINAL_STATUSES = new Set(['report_ready', 'exited', 'failed']);
63
+
64
+ // D-3: read at most WORKER_REPORT_TEXT_CAP_BYTES of a report/last-message file, decode as UTF-8
65
+ // and drop a trailing U+FFFD left by a split multi-byte sequence -- byte-for-byte the semantics
66
+ // lib/api/sessions.js:931 applies to an ?include_report=1 read. A throw (ENOENT/EACCES/EISDIR)
67
+ // is the caller's to classify: getWorkerStatus turns it into report_read_error, never a failure.
68
+ function _readBoundedReportText(filePath) {
69
+ const bytes = fs.readFileSync(filePath);
70
+ return bytes.subarray(0, WORKER_REPORT_TEXT_CAP_BYTES).toString('utf8').replace(/\uFFFD$/, '');
71
+ }
72
+
56
73
  function _assertValidSessionId(id) {
57
74
  if (!SESSION_ID_RE.test(String(id || ''))) {
58
75
  throw new DelegateError('session_invalid', 400, 'ctm_session_id is not a valid session id');
@@ -791,6 +808,31 @@ function getWorkerStatus(ctmSessionId, { now = Date.now } = {}) {
791
808
  status = 'starting';
792
809
  }
793
810
 
811
+ // D-3 (lead-cost-routing-round2-2026-10-24.html §5 D-3, §6.2): CTM is the ONLY counter of a
812
+ // report's distinct `file:line` citations. Count whichever text the row's OWN report_source
813
+ // names -- the report file, the last-message fallback, or the final provider result -- on
814
+ // every read, independent of ?include_report=1, so the whole-set wait's nested `worker`
815
+ // payload (which never carries report_text) still answers with a row count. Non-terminal
816
+ // statuses have selected no text yet and answer null/null; a read failure degrades to 0/0
817
+ // plus report_read_error and never fails the status read (same posture as report_present).
818
+ const terminalStatus = WORKER_TERMINAL_STATUSES.has(status);
819
+ let citationText = '';
820
+ let reportReadError = null;
821
+ if (terminalStatus) {
822
+ if (reportSource === 'report' || reportSource === 'last_message') {
823
+ try {
824
+ citationText = _readBoundedReportText(reportSource === 'report' ? reportPath : lastMessagePath);
825
+ } catch (error) {
826
+ // expected: report/last-message file missing, unreadable or a directory -- the count
827
+ // degrades to 0 with the error code exposed, exactly like the include_report=1 reader.
828
+ reportReadError = (error && error.code) || 'read_failed';
829
+ citationText = '';
830
+ }
831
+ } else if (reportSource === 'final_result') {
832
+ citationText = typeof successfulFinalText === 'string' ? successfulFinalText : '';
833
+ }
834
+ }
835
+
794
836
  const identity = workerRow ? 'marked' : 'unmarked';
795
837
 
796
838
  return {
@@ -799,6 +841,12 @@ function getWorkerStatus(ctmSessionId, { now = Date.now } = {}) {
799
841
  report_present: reportPresent,
800
842
  report_source: reportSource,
801
843
  report_text: reportText,
844
+ // D-3 (§6.2): the citation count and the character count of the text report_source names,
845
+ // never gated by include_report. report_read_error rides along ONLY when the file read
846
+ // threw (its error code, or 'read_failed'), in which case both counts are 0.
847
+ report_citation_rows: terminalStatus ? countReportCitations(citationText) : null,
848
+ report_chars: terminalStatus ? citationText.length : null,
849
+ ...(reportReadError ? { report_read_error: reportReadError } : {}),
802
850
  report_provisional: !!reportPresent && alive,
803
851
  last_message_path: lastMessagePath,
804
852
  turns: observer ? Number(observer.count || 0) : (Number.isInteger(runtime?.turns) ? runtime.turns : (Number.isInteger(session?._workerTurnCount) ? session._workerTurnCount : null)),
@@ -961,6 +1009,13 @@ function cancelRetirement(ctmSessionId) {
961
1009
  // that never exits doesn't spam the log every 60s forever.
962
1010
  const HIDE_LIVE_PROCESS_RETRY_MS = 60_000;
963
1011
 
1012
+ // WP-1 (durable terminal stamp, worker-terminal-stamp-durable-2026-10-26): the ceiling for
1013
+ // markWorkerTerminal's transient-lock retry backoff. The write lock is held by the db-owner core
1014
+ // writer's transactions (observed 259-1210 ms back-to-back for 19 s on 2026-10-26), so
1015
+ // the retry has NO attempt ceiling -- it re-arms until the stamp lands or the row is already
1016
+ // terminal / hidden / gone -- and the geometric backoff stops growing here.
1017
+ const TERMINAL_STAMP_RETRY_CAP_MS = 5000;
1018
+
964
1019
  async function _hideWorker(ctmSessionId, { now = Date.now } = {}) {
965
1020
  const deps = _requireDeps();
966
1021
  _retentionTimers.delete(ctmSessionId);
@@ -1074,11 +1129,13 @@ function _invalidateWorkerFailureCache(deps, ctmSessionId) {
1074
1129
  session._workerFailure = null;
1075
1130
  }
1076
1131
 
1077
- function markWorkerTerminal(ctmSessionId, { terminalStatus, failure = null, runtime = null, now = Date.now, retry = 0 } = {}) {
1132
+ function markWorkerTerminal(ctmSessionId, { terminalStatus, failure = null, runtime = null, now = Date.now, retry = 0, firstAttemptMs = null } = {}) {
1078
1133
  const deps = _requireDeps();
1079
1134
  let row;
1080
1135
  try { row = deps.dbModule.getWorkerSession(ctmSessionId); } catch { row = null; /* expected: an unreadable row is treated as no worker row, so nothing is stamped or scheduled -- a later exit-path call or the boot reload retries */ }
1081
- if (!row) return;
1136
+ // WP-1: a row that no longer exists, or that another path already hid (hidden_at stamped =
1137
+ // the worker is retired and its row is out of every listing), ends the retry chain here.
1138
+ if (!row || row.hidden_at) return;
1082
1139
  const nowMs = typeof now === 'function' ? now() : Number(now);
1083
1140
  const nowIso = new Date(nowMs).toISOString();
1084
1141
  const patch = {};
@@ -1113,12 +1170,23 @@ function markWorkerTerminal(ctmSessionId, { terminalStatus, failure = null, runt
1113
1170
  try {
1114
1171
  deps.dbModule.updateWorkerSession(ctmSessionId, patch);
1115
1172
  } catch (err) {
1116
- // WP-7 live finding: a busy write lock fails FAST and the old early return also dropped the wait wake + terminal sentinel -- disposition is now REPORT + bounded retry of the whole stamp.
1173
+ // WP-7 live finding: a busy write lock fails FAST and the old early return also dropped the wait wake + terminal sentinel -- disposition is now REPORT + retry of the whole stamp.
1174
+ // WP-1: that retry has NO attempt ceiling -- a transient lock error is not the stamp's
1175
+ // failure, it is contention on the main-thread writer (2026-10-26: 19 s of back-to-back
1176
+ // 259-1210 ms transactions outlived the old 5-attempt / 1.5 s budget and two finished
1177
+ // workers stayed listed as live for 9 h and 6 h). The chain ends at the top of the
1178
+ // re-entered call: row gone / hidden_at set, or the patch finding terminal_at already
1179
+ // written -- and, of course, when this write lands.
1117
1180
  // The retry's terminal writes start only when it fires, so the pending retry is tracked by the terminal settle.
1118
- if (['SQLITE_WRITE_LOCK_BUSY', 'SQLITE_BUSY', 'SQLITE_LOCKED'].includes(err && err.code) && retry < 5) return void workerLedger.trackTerminalWork(new Promise((resolve) => setTimeout(() => { try { markWorkerTerminal(ctmSessionId, { terminalStatus, failure, runtime, now, retry: retry + 1 }); } finally { resolve(); } }, 100 * (retry + 1)).unref?.()));
1181
+ if (['SQLITE_WRITE_LOCK_BUSY', 'SQLITE_BUSY', 'SQLITE_LOCKED'].includes(err && err.code)) {
1182
+ const firstAtMs = firstAttemptMs ?? nowMs;
1183
+ if (retry === 5) console.warn(`[delegate-worker] terminal stamp still busy after 6 attempts (${String((err && err.code) || 'write_failed').slice(0, 48)}); retrying every ${TERMINAL_STAMP_RETRY_CAP_MS} ms`);
1184
+ return void workerLedger.trackTerminalWork(new Promise((resolve) => setTimeout(() => { try { markWorkerTerminal(ctmSessionId, { terminalStatus, failure, runtime, now, retry: retry + 1, firstAttemptMs: firstAtMs }); } finally { resolve(); } }, Math.min(100 * (retry + 1), TERMINAL_STAMP_RETRY_CAP_MS)).unref?.()));
1185
+ }
1119
1186
  console.warn(`[delegate-worker] terminal stamp failed (${String((err && err.code) || "write_failed").slice(0, 48)})`); return;
1120
1187
  }
1121
1188
  row = { ...row, ...patch };
1189
+ if (retry > 0) console.log(`[delegate-worker] terminal stamp landed for ${ctmSessionId} after ${retry + 1} attempts in ${nowMs - (firstAttemptMs ?? nowMs)} ms`);
1122
1190
  _invalidateWorkerFailureCache(deps, ctmSessionId);
1123
1191
  }
1124
1192
  const hideAtMs = _computeHideAtMs(row, _retainMsForSession(deps, ctmSessionId));
@@ -1195,6 +1263,25 @@ function retireNow(session, now) {
1195
1263
  markWorkerTerminal(session.id, { terminalStatus: result.status, failure: result.failure, runtime, now });
1196
1264
  }
1197
1265
 
1266
+ // WP-2 (worker-terminal-stamp-durable-2026-10-26 D-2/D-3): one idempotent read repair for a
1267
+ // worker that EXITED but whose row never got its exit-time stamp (the write-lock loss WP-1 now
1268
+ // retries). Every lead-facing reader calls this FIRST, so a dead-but-unstamped worker is never
1269
+ // listed live and the stop verb never answers 'stopping' on a process that no longer exists.
1270
+ // Returns 'retired' | 'lost' | null. Idempotent: a stamped or hidden row is a no-op.
1271
+ // retireWorkerOnExit is deliberately NOT awaited -- a proxied worker's stamp is deferred by one
1272
+ // async read and tracked by workerLedger.trackTerminalWork, so callers must not assume the
1273
+ // stamp is synchronous (the next read retries anyway).
1274
+ function repairExitedWorkerRow(ctmSessionId, { now = Date.now } = {}) {
1275
+ const deps = _requireDeps();
1276
+ let row = null;
1277
+ try { row = deps.dbModule.getWorkerSession(ctmSessionId); } catch { row = null; /* expected: an unreadable row cannot be repaired now; the next read retries */ }
1278
+ if (!row || row.terminal_at || row.hidden_at) return null;
1279
+ const session = typeof deps.getSession === 'function' ? deps.getSession(ctmSessionId) : null;
1280
+ if (session && session.agentRole === 'worker' && (session._exitCode != null || session._exited)) { retireWorkerOnExit(session, { now }); return 'retired'; }
1281
+ if (!session) { retireLostWorker(row, { now }); return 'lost'; }
1282
+ return null; // a live process: nothing to repair
1283
+ }
1284
+
1198
1285
  // Lifecycle WP-5 (§5 D-10, R-3, §9 S-8, AC-5c): boot re-arm -- every live (hidden_at === '')
1199
1286
  // worker_sessions row that already has a persisted hide_at gets its retirement timer
1200
1287
  // re-armed at that EXACT value (never recomputed off the current retainMs, so a config
@@ -1263,7 +1350,7 @@ const retireLostWorker = bootRetire.retireLostWorker, retireSurvivingWorker = bo
1263
1350
  module.exports = {
1264
1351
  configure, _terminalJournalSettled: workerLedger.terminalJournalSettled,
1265
1352
  resumeWorkerSession: workerContinuation.resumeWorkerSession,
1266
- stopWorkerSession: (id, opts) => require('./worker-stop').stopWorkerSession(id, opts, _requireDeps(), { resolveParent: resolveParentCtmSessionId }),
1353
+ stopWorkerSession: (id, opts) => require('./worker-stop').stopWorkerSession(id, opts, _requireDeps(), { resolveParent: resolveParentCtmSessionId, repairExitedWorkerRow }),
1267
1354
  probeWorkerRoute: workerContinuation.probeWorkerRoute,
1268
1355
  restoreQuotaHoldsAtBoot: workerContinuation.restoreQuotaHoldsAtBoot,
1269
1356
  markAgentConsumed: workerContinuation.markAgentConsumed,
@@ -1290,6 +1377,8 @@ module.exports = {
1290
1377
  markAcknowledged,
1291
1378
  markWorkerTerminal,
1292
1379
  retireWorkerOnExit,
1380
+ // WP-2 (D-2): the idempotent read repair both lead-facing readers call before answering.
1381
+ repairExitedWorkerRow,
1293
1382
  reloadRetirementsAtBoot,
1294
1383
  listPendingRetirements,
1295
1384
  _resetRetentionTimersForTest,
@@ -7,6 +7,25 @@ function configure(getDeps) {
7
7
  _getDeps = getDeps;
8
8
  }
9
9
 
10
+ // WP-4 (docs/remote-hub-followups-credential-projects-rows-idempotency-2026-10-23.html §5 D-11):
11
+ // the create fingerprint an Idempotency-Key retry is compared against -- the JSON of these nine
12
+ // launch-shaping fields, IN THIS ORDER, with a missing field read as ''. Geometry (cols/rows) and
13
+ // label/title are deliberately EXCLUDED: a retry may differ in window size or naming and is still
14
+ // the same create. Pure: reads nothing but `msg`, allocates nothing, touches no socket.
15
+ const CREATE_FINGERPRINT_FIELDS = Object.freeze([
16
+ 'cmd', 'args', 'cwd', 'createCwd', 'model_id', 'model_provider', 'mcp_profile', 'agentType', 'chatSessionId',
17
+ ]);
18
+
19
+ function createFingerprint(msg) {
20
+ const source = msg && typeof msg === 'object' ? msg : {};
21
+ return JSON.stringify(CREATE_FINGERPRINT_FIELDS.map((field) => {
22
+ const value = source[field];
23
+ // `args` is compared AS GIVEN (an argv array serializes element by element); every other
24
+ // field is a scalar. Absent and null both read as ''.
25
+ return value === undefined || value === null ? '' : value;
26
+ }));
27
+ }
28
+
10
29
  function prepareCreate(ws, msg) {
11
30
  const deps = _getDeps();
12
31
  const handleCreateStartedAt = deps.now();
@@ -67,4 +86,4 @@ function prepareCreate(ws, msg) {
67
86
  };
68
87
  }
69
88
 
70
- module.exports = { configure, prepareCreate };
89
+ module.exports = { configure, prepareCreate, createFingerprint };
@@ -1,5 +1,10 @@
1
1
  'use strict';
2
2
 
3
+ // WP-4 (docs/remote-hub-followups-credential-projects-rows-idempotency-2026-10-23.html §5 D-11):
4
+ // the ONE fingerprint definition lives in lib/session-create/identity.js; this module stamps it on a
5
+ // session it fully owned and compares it on the next same-id create.
6
+ const { createFingerprint } = require('./identity');
7
+
3
8
  // WP-2b (docs/session-model-authority-and-display-2026-10-15.html §9): the COMPLETE prepared-create
4
9
  // orchestration, moved verbatim out of server.js's `handleCreatePrepared`. It owns one whole
5
10
  // responsibility -- admitted request -> durable generation -> prepared current plan -> trusted
@@ -26,11 +31,23 @@
26
31
  // argv, token or raw send-error text is ever echoed into the report. Every other `_wsSend` in
27
32
  // the handler (mcp_profile_invalid, launch-generation-refused, cwd-create-failed,
28
33
  // launch_transcript_restore_failed, the folder toast) keeps its original throwing behavior.
34
+ // WP-4 (docs/remote-hub-followups-credential-projects-rows-idempotency-2026-10-23.html §5 D-13):
35
+ // the per-id IN-FLIGHT create ledger -- id -> { fingerprint } (D-11). One entry per create that has
36
+ // passed owner/MCP admission and has not yet left handleCreatePrepared; a second create with the
37
+ // SAME id AND the same fingerprint while the first is in flight is refused `create_in_progress`
38
+ // before it reserves a generation, plans, or spawns anything. The entry is deleted in the handler's
39
+ // `finally`, which covers every exit (success, typed refusal, early null return, throw). A DIFFERENT
40
+ // fingerprint never refuses and never overwrites the first entry (D-14: a model-switch relaunch
41
+ // stays the generation runtime's business).
42
+ const _inflightCreates = new Map();
43
+
29
44
  function createPreparedSessionCreate(host) {
30
45
  const {
31
46
  // Admission / identity
32
47
  crypto, _sessionMcpProfileSetting, _wsSend, _codingSessionGenerationRuntime,
33
48
  recordSessionDiagnostic, isSessionResumeBlocked, clearSessionResumeBlocked, getSession,
49
+ // WP-4 (§5 D-12): the one `created` frame a duplicate create is re-answered with.
50
+ sendNativeSessionCreated,
34
51
  // Input / local path
35
52
  serverConfig, path, fs, detectAgentType, extractResumeTarget,
36
53
  // Metadata / planning
@@ -61,6 +78,9 @@ function createPreparedSessionCreate(host) {
61
78
  }
62
79
  }
63
80
 
81
+ // WP-4 (§5 D-13): admission, then the in-flight claim, then the unchanged orchestration. The
82
+ // claim's `finally` wraps the WHOLE body, so every exit of the orchestration releases it -- and no
83
+ // line of the orchestration itself had to move to make that true.
64
84
  async function handleCreatePrepared(ws, msg) {
65
85
  if (host._ownerRecovery.rejectRequest(ws, msg)) return null; const id = msg?.id || crypto.randomUUID();
66
86
  const preparedMsg = { ...(msg || {}), id };
@@ -70,6 +90,44 @@ function createPreparedSessionCreate(host) {
70
90
  if (ws?.readyState === 1) _wsSend(ws, JSON.stringify({ type: 'error', code: 'mcp_profile_invalid', message: `Unknown mcp_profile: ${JSON.stringify(_rawMcpProfile)}` }));
71
91
  return null;
72
92
  }
93
+ // WP-4 (§5 D-12/D-13): a client-supplied id is the Idempotency-Key, and both duplicate answers
94
+ // are decided HERE, before any generation reservation, plan, markSpawning or markLive:
95
+ // - the id names a LIVE session spawned by an identical create => re-answer ONE `created`
96
+ // frame (same builder a fresh create uses) and return it; nothing is attached or rolled back;
97
+ // - an identical create is still in flight => one typed `create_in_progress` refusal.
98
+ // A DIFFERENT fingerprint is NOT a duplicate (D-14): it walks today's path (attach, or the
99
+ // generation runtime's model-switch relaunch) and leaves the other fingerprint's entry alone.
100
+ let _claimedInflight = false;
101
+ const fingerprint = msg?.id ? createFingerprint(preparedMsg) : '';
102
+ if (fingerprint) {
103
+ const live = getSession(id);
104
+ if (ws && live?.ptyProcess && live._createFingerprint === fingerprint) {
105
+ sendNativeSessionCreated(ws, live);
106
+ return live;
107
+ }
108
+ const inFlight = _inflightCreates.get(id);
109
+ if (inFlight && inFlight.fingerprint === fingerprint) {
110
+ sendPreparedError(ws, {
111
+ type: 'error',
112
+ code: 'create_in_progress',
113
+ id,
114
+ message: 'A create for this session is already in progress',
115
+ });
116
+ return null;
117
+ }
118
+ if (!inFlight) { _inflightCreates.set(id, { fingerprint }); _claimedInflight = true; }
119
+ }
120
+ try {
121
+ return await _prepareCreateAdmitted(ws, msg, preparedMsg, id, fingerprint);
122
+ } finally {
123
+ if (_claimedInflight) _inflightCreates.delete(id);
124
+ }
125
+ }
126
+
127
+ // The orchestration itself: byte-for-byte the body that used to be handleCreatePrepared's, from
128
+ // the generation reservation on, plus the one D-11 fingerprint stamp on the owned success path.
129
+ // Split out ONLY so the D-13 claim above can wrap every exit.
130
+ async function _prepareCreateAdmitted(ws, msg, preparedMsg, id, fingerprint) {
73
131
  const reserved = _codingSessionGenerationRuntime.revalidateReservation(await _codingSessionGenerationRuntime.reserveSessionGeneration(id));
74
132
  if (!reserved.ok) { recordSessionDiagnostic(id, 'launch-generation-refused', { code: reserved.code, message: reserved.message }); if (ws?.readyState === 1) _wsSend(ws, JSON.stringify({ type: 'error', code: reserved.code, message: `Launch refused: ${reserved.message}` })); return null; }
75
133
  const generation = reserved.generation;
@@ -274,6 +332,10 @@ function createPreparedSessionCreate(host) {
274
332
  });
275
333
  return null;
276
334
  }
335
+ // WP-4 (§5 D-11): only a create this handler fully owned, keyed by a client id, can be re-answered
336
+ // later. A retry landing between the `created` frame and this line still finds the in-flight
337
+ // entry above and gets `create_in_progress`.
338
+ if (fingerprint && created && created.type !== 'walle') created._createFingerprint = fingerprint;
277
339
  _launchPlanCoordinator.markLive(id, generation, result.plan);
278
340
  return created || null;
279
341
  }
@@ -281,4 +343,10 @@ function createPreparedSessionCreate(host) {
281
343
  return handleCreatePrepared;
282
344
  }
283
345
 
284
- module.exports = { createPreparedSessionCreate };
346
+ module.exports = {
347
+ createPreparedSessionCreate,
348
+ // WP-4 test seam (AC-4a/§10 abuse case): the in-flight ledger is process-local, so a test that
349
+ // drives the real factory in-process asserts it drains through this accessor rather than reaching
350
+ // into the module's internals.
351
+ inflightCreateCount: () => _inflightCreates.size,
352
+ };
@@ -0,0 +1,77 @@
1
+ 'use strict';
2
+
3
+ // native-same-thread-model-switch / §6.5: the PRIVATE launch capture a rollback recovery replays.
4
+ // It is recorded for EVERY native harness whose `recoverSource` can resume the same canonical
5
+ // thread -- Codex AND Claude -- because this capture is the only proof of THIS spawn's
6
+ // executable/argv/cwd/home/generation. It is taken from the prepared launch plan at the REAL
7
+ // spawn (never synthesized later from mutable session fields), and every field is a copy: the
8
+ // argv array is cloned, so a later `session.args` rewrite cannot edit the receipt.
9
+ // WP-0b: extracted verbatim from provider-process.js (same spawn point) so that file stays under
10
+ // the file-size ceiling; behavior is unchanged. The existing Codex receipt dependency is called
11
+ // once as before; this adds no reader, state, or credential copy.
12
+
13
+ const path = require('node:path');
14
+ const { strFlag } = require('../env-flags');
15
+
16
+ // Explicit launch inputs (never the whole process env): the prepared plan, the caller's argv/cwd,
17
+ // the ACTUAL spawn env, the recipe/generation the launch was prepared with, the Codex config
18
+ // receipt already read for this spawn, and the existing codex-paths dependency (no extra read).
19
+ // Returns `null` for a harness with no same-canonical-thread rollout to resume.
20
+ function buildModelTransitionLaunchContext({ agentType, preparedLaunchPlan, cmd, args, cwd, spawnEnv,
21
+ serverHome, executionRecipe, processGeneration, nativeConfigReceipt = null, codexPaths = null }) {
22
+ if (agentType !== 'codex' && agentType !== 'claude') return null;
23
+ const capturedHome = spawnEnv.HOME || serverHome || null;
24
+ const capturedTmpDir = typeof spawnEnv.TMPDIR === 'string' && path.isAbsolute(spawnEnv.TMPDIR)
25
+ ? spawnEnv.TMPDIR : null;
26
+ if (agentType === 'claude') {
27
+ // A native Claude launch claims NO Codex receipt/config: `codexHome`, `nativeStorageRootReceipt`
28
+ // and `nativeConfigReceipt` (read from the Codex `config.toml`) are explicitly null, so a
29
+ // rollback can never mistake this capture for a Codex one. The configured storage root is the
30
+ // Claude `projects/` dir under the ACTUAL `CLAUDE_CONFIG_DIR` (else `HOME/.claude`) and stays
31
+ // null when neither is known. The canonical thread is this launch's OWN recipe thread --
32
+ // computed by `launchProviderThreadId` from the resume plan, an injected/explicit
33
+ // `--session-id` and the caller's argv -- never inferred from the CTM session id, and left
34
+ // null (a fail-closed refusal in `recoverSource`) when the identity is unknown.
35
+ // A blank child CLAUDE_CONFIG_DIR falls back to that child's HOME/.claude, never the parent's
36
+ // config root. Codex keeps its CTM_CODEX_SESSIONS_DIR fallback below, unchanged.
37
+ const claudeConfigDir = spawnEnv.CLAUDE_CONFIG_DIR
38
+ || (capturedHome ? path.join(capturedHome, '.claude') : null);
39
+ return {
40
+ executablePath: preparedLaunchPlan?.cmd || cmd,
41
+ args: Array.isArray(preparedLaunchPlan?.args) ? [...preparedLaunchPlan.args] : [...args],
42
+ workingDirectory: preparedLaunchPlan?.cwd || cwd,
43
+ providerThreadId: executionRecipe?.providerThreadId || null,
44
+ processGeneration,
45
+ homeDir: capturedHome,
46
+ codexHome: null,
47
+ nativeStorageRootConfiguredPath: claudeConfigDir ? path.join(claudeConfigDir, 'projects') : null,
48
+ nativeStorageRootReceipt: null,
49
+ tmpDir: capturedTmpDir,
50
+ nativeConfigReceipt: null,
51
+ };
52
+ }
53
+ const sessionsDir = spawnEnv.CTM_CODEX_SESSIONS_DIR || strFlag('CTM_CODEX_SESSIONS_DIR', '');
54
+ return {
55
+ executablePath: preparedLaunchPlan?.cmd || cmd,
56
+ args: Array.isArray(preparedLaunchPlan?.args) ? [...preparedLaunchPlan.args] : [...args],
57
+ workingDirectory: preparedLaunchPlan?.cwd || cwd,
58
+ providerThreadId: preparedLaunchPlan?.resume?.providerResumeId || preparedLaunchPlan?.resume?.sessionId
59
+ || preparedLaunchPlan?.resume?.agentSessionId || null,
60
+ processGeneration,
61
+ homeDir: capturedHome,
62
+ codexHome: spawnEnv.CODEX_HOME || (capturedHome ? path.join(capturedHome, '.codex') : null),
63
+ nativeStorageRootConfiguredPath: spawnEnv.CTM_CODEX_SESSIONS_DIR
64
+ || sessionsDir
65
+ || ((spawnEnv.CODEX_HOME || capturedHome)
66
+ ? path.join(spawnEnv.CODEX_HOME || path.join(capturedHome, '.codex'), 'sessions') : null),
67
+ nativeStorageRootReceipt: codexPaths?.codexSessionsRootReceipt?.({
68
+ homeDir: capturedHome,
69
+ codexHomePath: spawnEnv.CODEX_HOME || (capturedHome ? path.join(capturedHome, '.codex') : null),
70
+ sessionsDir: sessionsDir || undefined,
71
+ }) || null,
72
+ tmpDir: capturedTmpDir,
73
+ nativeConfigReceipt: nativeConfigReceipt || { state: 'unknown' },
74
+ };
75
+ }
76
+
77
+ module.exports = { buildModelTransitionLaunchContext };
@@ -20,8 +20,11 @@ const { RECIPE_ENV_KEY, MAX_RECIPE_BYTES, modelExecutionRecipeMetadata } = requi
20
20
  const { validateDeferredRestoreAttempt } = require('../deferred-restore-attempt');
21
21
  const { getPrivateSourceRecoveryEnvironment, capturePrivateSourceEnvironment,
22
22
  hasCapturedEmptySourceEnvironmentRestore } = require('../lead-model-restore-reconciliation');
23
- const { modelStorageRootsFromEnv } = require('../host-handoff');
24
- const { strFlag } = require('../env-flags');
23
+ const { modelStorageRootsFromEnv, privateOverridesFromEnv } = require('../host-handoff');
24
+ // WP-0b: the rollback launch capture (moved out of this file, same spawn point) -- see
25
+ // lib/session-create/provider-launch-context.js for the field-by-field contract.
26
+ const { buildModelTransitionLaunchContext } = require('./provider-launch-context');
27
+ const codexNativeProviderGate = require('./codex-native-provider-gate');
25
28
  const MAX_CODEX_CONFIG_BYTES = 1024 * 1024;
26
29
 
27
30
  async function readCodexConfigReceipt(codexHome, onCleanupError = () => {}) {
@@ -195,6 +198,7 @@ async function prepareProviderSession(state) {
195
198
  _wsSend,
196
199
  recordSessionDiagnostic,
197
200
  harnessModelProfileLoader,
201
+ markSessionResumeBlocked, // WP-1: the deterministic refusal hook (server.js)
198
202
  } = _getDeps();
199
203
  const { args, cmd, createAgentType, cwd, env, id, msg, preparedLaunchPlan, restoreSeedSnapshot, ws, signal,
200
204
  claudeResumeId, _injectedSessionId } = state;
@@ -324,6 +328,14 @@ async function prepareProviderSession(state) {
324
328
  if (typeof recordSessionDiagnostic === 'function') recordSessionDiagnostic(id, 'lead-route-read-failed', { message: String(error && error.message || error).slice(0, 200) });
325
329
  }
326
330
  }
331
+ // WP-1 (codex-routed-fork-cold-restore-guard §5 D-2..D-4): a native `codex resume T` loads the provider
332
+ // STORED IN T; the gate refuses (D-4) or repairs the route from the journal (D-3) before any spawn.
333
+ if (!target && !readFailed && createAgentType === 'codex' && resolvedProviderThreadId) {
334
+ const gated = await codexNativeProviderGate.gateNativeCodexLaunch({ sessionId: id, threadId: resolvedProviderThreadId,
335
+ env, preparedLaunchPlan, dbModule, recordSessionDiagnostic, markSessionResumeBlocked, ws, wsSend: _wsSend });
336
+ if (gated.refused) { if (runtime) await runtime.close(); return { handled: true, providerState: null }; }
337
+ if (gated.target) target = gated.target;
338
+ }
327
339
  if (target && !readFailed) {
328
340
  let lr;
329
341
  if (target.preparedPlan) {
@@ -863,13 +875,17 @@ function createProviderSession(state) {
863
875
  const leadApplied = _applyLeadRouteToSpawn({ launchArgs, env, leadRoute });
864
876
  launchArgs = leadApplied.launchArgs;
865
877
  // The host receipt carries only whether the original per-session overrides were empty or
866
- // require a surviving private owner. Storage roots projected for this restore remain in the
867
- // launch context and do not turn an otherwise empty override set into a private environment.
878
+ // require a surviving private owner. Storage roots and TMPDIR projected into the launch context
879
+ // (`launchEnvironmentOverrides`) are launch context, not private overrides: for EVERY spawn, not
880
+ // only a host restore, they are dropped before the receipt is computed, so a projected HOME/TMPDIR
881
+ // never turns an otherwise empty override set into a private environment. The spawned child still
882
+ // receives them (`launchEnvOverrides` below).
868
883
  const trustedCapturedEmptySourceEnvironment = msg._isRestore === true
869
884
  && hasCapturedEmptySourceEnvironmentRestore(msg);
870
885
  const sourceOverrideCapture = capturePrivateSourceEnvironment(sourceRecoveryEnvironment
871
- ? sourceRecoveryEnvironment.environment : trustedCapturedEmptySourceEnvironment ? {}
872
- : (msg.env && typeof msg.env === 'object' ? msg.env : {}));
886
+ ? privateOverridesFromEnv(sourceRecoveryEnvironment.environment)
887
+ : trustedCapturedEmptySourceEnvironment ? {}
888
+ : privateOverridesFromEnv(msg.env && typeof msg.env === 'object' ? msg.env : {}));
873
889
  const sourceEnvironmentReference = trustedCapturedEmptySourceEnvironment ? 'captured-empty'
874
890
  : sourceOverrideCapture.state === 'captured'
875
891
  ? (Object.keys(sourceOverrideCapture.values || {}).length === 0 ? 'captured-empty' : 'private-owner-required')
@@ -1065,32 +1081,14 @@ function createProviderSession(state) {
1065
1081
  _modelProcessGeneration: modelProcessGeneration,
1066
1082
  _modelExecutionRecipe: modelExecutionRecipeMetadata(executionRecipe,
1067
1083
  { sessionId: id, processGeneration: modelProcessGeneration }).modelExecutionRecipe || null,
1068
- ...(createAgentType === 'codex' ? { _modelTransitionLaunchContext: (() => {
1069
- const capturedHome = leadApplied.spawnEnv.HOME || serverConfig.HOME || null;
1070
- const sessionsDir = leadApplied.spawnEnv.CTM_CODEX_SESSIONS_DIR || strFlag('CTM_CODEX_SESSIONS_DIR', '');
1071
- return {
1072
- executablePath: preparedLaunchPlan?.cmd || cmd,
1073
- args: Array.isArray(preparedLaunchPlan?.args) ? [...preparedLaunchPlan.args] : [...args],
1074
- workingDirectory: preparedLaunchPlan?.cwd || cwd,
1075
- providerThreadId: preparedLaunchPlan?.resume?.providerResumeId || preparedLaunchPlan?.resume?.sessionId
1076
- || preparedLaunchPlan?.resume?.agentSessionId || null,
1077
- processGeneration: modelProcessGeneration,
1078
- homeDir: capturedHome,
1079
- codexHome: leadApplied.spawnEnv.CODEX_HOME || (capturedHome ? path.join(capturedHome, '.codex') : null),
1080
- nativeStorageRootConfiguredPath: leadApplied.spawnEnv.CTM_CODEX_SESSIONS_DIR
1081
- || sessionsDir
1082
- || ((leadApplied.spawnEnv.CODEX_HOME || capturedHome)
1083
- ? path.join(leadApplied.spawnEnv.CODEX_HOME || path.join(capturedHome, '.codex'), 'sessions') : null),
1084
- nativeStorageRootReceipt: codexPaths?.codexSessionsRootReceipt?.({
1085
- homeDir: capturedHome,
1086
- codexHomePath: leadApplied.spawnEnv.CODEX_HOME || (capturedHome ? path.join(capturedHome, '.codex') : null),
1087
- sessionsDir: sessionsDir || undefined,
1088
- }) || null,
1089
- tmpDir: typeof leadApplied.spawnEnv.TMPDIR === 'string' && path.isAbsolute(leadApplied.spawnEnv.TMPDIR)
1090
- ? leadApplied.spawnEnv.TMPDIR : null,
1091
- nativeConfigReceipt: nativeConfigReceipt || { state: 'unknown' },
1092
- };
1093
- })() } : {}),
1084
+ // native-same-thread-model-switch / §6.5, WP-0b: the PRIVATE launch capture a rollback recovery
1085
+ // replays, built at THIS REAL spawn point by the sibling module (see
1086
+ // lib/session-create/provider-launch-context.js for the field-by-field contract).
1087
+ ...((createAgentType === 'codex' || createAgentType === 'claude') ? { _modelTransitionLaunchContext: buildModelTransitionLaunchContext({
1088
+ agentType: createAgentType, preparedLaunchPlan, cmd, args, cwd,
1089
+ spawnEnv: leadApplied.spawnEnv, serverHome: serverConfig.HOME,
1090
+ executionRecipe, processGeneration: modelProcessGeneration, nativeConfigReceipt, codexPaths,
1091
+ }) } : {}),
1094
1092
  clients: sessionClients,
1095
1093
  _primaryWs: null,
1096
1094
  // scrollback removed — headless terminal is source of truth (Phase 3D)
@@ -1455,11 +1453,10 @@ function createProviderSession(state) {
1455
1453
  provider: sessionAgentType,
1456
1454
  providerResumeId: sessionAgentType === 'codex' ? (claudeResumeId || '') : '',
1457
1455
  cwd,
1458
- title: session.title || session.label,
1459
- userRenamed: !!session.userRenamed,
1460
- titleSource: session.userRenamed ? 'user' : '',
1461
- titleStatus: session.userRenamed ? 'user_named' : '',
1462
- titleStatusReason: session.userRenamed ? 'new_session_title' : '',
1456
+ // D-2 (session-name design 2026-10-23): a resume of a titled row keeps its title + provenance (upsertSession keeps omitted keys).
1457
+ ...(isResumeLaunch && existingTitle?.title ? {} : { title: session.title || session.label, userRenamed: !!session.userRenamed,
1458
+ titleSource: session.userRenamed ? 'user' : '', titleStatus: session.userRenamed ? 'user_named' : '',
1459
+ titleStatusReason: session.userRenamed ? 'new_session_title' : '' }),
1463
1460
  projectEntry: sessionAgentType === 'claude' ? encodeClaudeProjectEntry(cwd) : encodeProjectEntry(cwd),
1464
1461
  projectPath: cwd,
1465
1462
  },
@@ -9,6 +9,12 @@ const { releaseAdmissionOnExit, retireWorkerOnExit, scheduleWorkerObservation }
9
9
  // whole frame is built by the SAME bounded pure transport leaf (it copies the fixed typed whitelist out
10
10
  // of the payload `_sessionPayload` already produced).
11
11
  const { buildCreatedSessionPayload } = require('../coding-session-model-public');
12
+ // WP-2 (docs/codex-routed-fork-cold-restore-guard-2026-10-22.html §5 D-5): the module carries the
13
+ // rationale — a Codex config-load failure is a precondition failure, never a restart-budget consumer.
14
+ const { classifyCodexResumeExit } = require('./codex-resume-exit-classify');
15
+ const { codexProviderMissingMessage } = require('./codex-native-provider-gate'); // WP-1 owns the one block sentence
16
+ // WP-1 (D-1/D-2, S-11): the pure chooser for the block a deferred exit carries (a direct require, like the two leaves above: the injected `runtime` is built in server.js, which this WP does not own).
17
+ const { deferBlockForExit } = require('../resume-blocked-wire');
12
18
 
13
19
  // D-9 (delegated-worker-lifecycle-identity-observability-2026-09-27.html §5 D-9, §9 S-15):
14
20
  // best-effort proxy-token revoke for a worker session exiting on the CREATE path (never
@@ -887,7 +893,7 @@ function installResponseRuntime(runtime) {
887
893
  // classification BEFORE this handler is installed; the handler then passes its FULL event through it.
888
894
  const leadRouteCleanup = runtime.session.agentRole === 'worker' ? null : require('./lead-route-launch').createLeadRouteLifetimeCleanup({ sessionId: runtime.session.id, ownership: runtime.session._leadRouteTokenOwnership, hosted: _useSessionHostProcessForPty() === true });
889
895
  const onPtyExit = async (exitEvent) => {
890
- const { exitCode, signal } = exitEvent || {};
896
+ const { exitCode, signal, exitSource } = exitEvent || {};
891
897
  runtime.sessionInputAuthority.observeProvider({ sessionId: runtime.id, status: 'exited' });
892
898
  runtime.session._ptyExitLifecycle = {
893
899
  stage: 'capturing-viewport',
@@ -1064,6 +1070,20 @@ function installResponseRuntime(runtime) {
1064
1070
  // retry path as an exit 1 rather than being mistaken for a completed terminal lifecycle.
1065
1071
  const maxAutoRestarts = 3;
1066
1072
  const restartCount = runtime.session._autoRestartCount || 0;
1073
+ // WP-2 (§5 D-5, §9 S-9/S-10): classify THIS attempt's buffer — never scrollback — before the
1074
+ // auto-restart decision, and block a Codex config-load failure instead of spending the budget.
1075
+ const codexProviderMissingExit = (agentType === 'codex' && exitedBeforeResumeReady) ? classifyCodexResumeExit(runtime.session._resumeAttemptOutput || '') : null;
1076
+ if (codexProviderMissingExit) {
1077
+ runtime.recordSessionDiagnostic(runtime.id, 'codex-provider-missing-exit', { provider: codexProviderMissingExit.provider, exitCode });
1078
+ // Marked ONCE. This is what makes the auto-restart decision below false
1079
+ // (`resumeBlocked: isSessionResumeBlocked(runtime.id)`), so `handleCreatePrepared` is never
1080
+ // scheduled for this exit; the flow then falls through to the ordinary exit-persistence path,
1081
+ // which sees the session already blocked and keeps the placeholder + startup recipe as is.
1082
+ markSessionResumeBlocked(runtime.id, {
1083
+ code: 'codex_provider_missing', detail: codexProviderMissingExit.line, agentType: 'codex', deterministic: true,
1084
+ message: codexProviderMissingMessage(codexProviderMissingExit.provider),
1085
+ }, 'codex-provider-exit');
1086
+ }
1067
1087
  // A worker continuation does launch `-p --resume` / `exec resume`, but its exit is terminal and
1068
1088
  // never an interactive auto-restart (that would relaunch it as a TUI under the worker's identity,
1069
1089
  // stuck at worker-status 'starting'). lib/session-class.js declares that: relaunchFromRestore=false.
@@ -1208,25 +1228,22 @@ function installResponseRuntime(runtime) {
1208
1228
  const exitPersistence = sessionExitPersistenceDecision({
1209
1229
  intentionalKill: !!runtime.session._intentionalKill,
1210
1230
  exitCode,
1211
- signal,
1231
+ signal, exitSource,
1212
1232
  isResumable,
1213
1233
  resumeContext: resumeExitContext(runtime.session), deferOnAbnormalExit: sessionPolicy(runtime.session).deferOnAbnormalExit,
1214
1234
  });
1215
1235
  runtime.session._ptyExitLifecycle.stage = `persistence-${String(exitPersistence.action || 'unknown')}`;
1216
1236
  if (exitPersistence.action === 'defer') {
1217
1237
  const latestResumeExit = resumeExitContext(runtime.session);
1218
- if (!isSessionResumeBlocked(runtime.id)) {
1219
- const label = getAgentCapabilities(agentType).label || 'The agent';
1238
+ const label = getAgentCapabilities(agentType).label || 'The agent';
1239
+ // WP-1 (D-1/D-2): the block comes from the exit EVIDENCE -- an unknown exit yields null (defer with no block: exited, and the next input resumes it).
1240
+ const block = deferBlockForExit({ exitPersistence, latestResumeExit, exitCode, signal, autoRestartCount: runtime.session._autoRestartCount || 0, label });
1241
+ if (block && !isSessionResumeBlocked(runtime.id)) {
1220
1242
  markSessionResumeBlocked(runtime.id, {
1221
- code: latestResumeExit.failedBeforeReady
1222
- ? (latestResumeExit.code || 'resume_failed')
1223
- : 'session_crash_loop',
1224
- message: latestResumeExit.failedBeforeReady
1225
- ? `${label} did not become ready while resuming.`
1226
- : `${label} exited unexpectedly and automatic recovery was exhausted.`,
1243
+ ...block,
1227
1244
  detail: resumeFailureDetail(agentType, runtime.session._resumeAttemptOutput || ''),
1228
1245
  agentType,
1229
- });
1246
+ }, 'response-defer');
1230
1247
  }
1231
1248
  // Keep the CTM identity and startup recipe as a deferred placeholder. Retiring the startup
1232
1249
  // task here would make retry impossible. Failed resume mirrors also stay provisional so they
@@ -1243,7 +1260,8 @@ function installResponseRuntime(runtime) {
1243
1260
  runtime.session._restoreStarting = false;
1244
1261
  runtime.session._lifecycleState = 'deferred';
1245
1262
  runtime._markServerSessionTurnClosed(runtime.session, Date.now(), 'session-exit'); // R-15: PROCESS_EXIT, never a dead wait
1246
- runtime.session._resumeBlocked = true;
1263
+ // WP-1 (D-1): read the flag, never assert it -- a defer with no block is "exited, resumes on input", not "resuming".
1264
+ runtime.session._resumeBlocked = isSessionResumeBlocked(runtime.id);
1247
1265
  runtime.session._exitPersistenceReason = exitPersistence.reason;
1248
1266
  runtime.session._restoreTask = {
1249
1267
  session_id: runtime.id,
@@ -120,12 +120,21 @@ async function createWalleSession(state) {
120
120
  if (createResult) deps.recordWalleTranscriptAppend(session, createResult);
121
121
  for (const record of seeded.records) deps.recordWalleTranscriptAppend(session, record);
122
122
  try {
123
+ // WP-0 (session-name-agent-title-precedence-2026-10-23.html §5 D-2): reusing an existing
124
+ // Wall-E transcript must not rewrite the row's title or its provenance — the client echoes
125
+ // the displayed name back (D-1), and upsertSession keeps the stored value for any omitted
126
+ // title key (or empty titleSource/titleStatus/titleStatusReason), so omit them on a resume
127
+ // of a row that already has a title.
128
+ const preserveTitleProvenance = !msg._isRestore
129
+ && (launch.walleResumeRequest || launch.isResumeLaunch) && !!existingTitle?.title;
123
130
  void deps.detachOwnerMutation('walle-session-created', () => deps.dbModule.upsertSession(id, {
124
131
  agentSessionId: chatSessionId, provider: 'walle', cwd, projectPath: cwd,
125
- title: session.title, userRenamed: !!session.userRenamed,
126
- titleSource: session.userRenamed ? 'user' : '',
127
- titleStatus: session.userRenamed ? 'user_named' : '',
128
- titleStatusReason: session.userRenamed ? 'new_session_title' : '',
132
+ ...(preserveTitleProvenance ? {} : {
133
+ title: session.title, userRenamed: !!session.userRenamed,
134
+ titleSource: session.userRenamed ? 'user' : '',
135
+ titleStatus: session.userRenamed ? 'user_named' : '',
136
+ titleStatusReason: session.userRenamed ? 'new_session_title' : '',
137
+ }),
129
138
  jsonlPath, fileSize: 0, modifiedAt: '', model: modelId || '', hostname: deps.hostname,
130
139
  }));
131
140
  } catch (error) {