create-walle 0.10.0 → 0.10.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (381) hide show
  1. package/README.md +88 -41
  2. package/lib/owner-write-safety.js +135 -0
  3. package/lib/public-runtime.js +13 -5
  4. package/lib/release-schema.json +3 -2
  5. package/lib/runtime-closure-policy.json +7 -5
  6. package/package.json +10 -4
  7. package/template/bin/deps-doctor.js +1 -1
  8. package/template/bin/node-bin.sh +11 -0
  9. package/template/coding-task-manager/api-prompts.js +10 -22
  10. package/template/coding-task-manager/bin/capture-codex-rollout-records.js +417 -0
  11. package/template/coding-task-manager/bin/census-codex-lead-history.js +325 -0
  12. package/template/coding-task-manager/bin/claude-hook.js +7 -3
  13. package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
  14. package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
  15. package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
  16. package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
  17. package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
  18. package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
  19. package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
  20. package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
  21. package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
  22. package/template/coding-task-manager/bin/seam-replay-codex.js +301 -0
  23. package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
  24. package/template/coding-task-manager/db.js +705 -286
  25. package/template/coding-task-manager/git-utils.js +44 -0
  26. package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
  27. package/template/coding-task-manager/lib/agent-presets.js +15 -0
  28. package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
  29. package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
  30. package/template/coding-task-manager/lib/api/host-stats.js +98 -0
  31. package/template/coding-task-manager/lib/api/ingress.js +35 -12
  32. package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
  33. package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
  34. package/template/coding-task-manager/lib/api/machines.js +461 -0
  35. package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
  36. package/template/coding-task-manager/lib/api/providers.js +53 -6
  37. package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
  38. package/template/coding-task-manager/lib/api/sessions.js +160 -8
  39. package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
  40. package/template/coding-task-manager/lib/api/worktrees.js +38 -37
  41. package/template/coding-task-manager/lib/approval-drift.js +16 -1
  42. package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
  43. package/template/coding-task-manager/lib/approval-scope.js +68 -4
  44. package/template/coding-task-manager/lib/auth-context.js +32 -2
  45. package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
  46. package/template/coding-task-manager/lib/auth-rules.js +186 -45
  47. package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
  48. package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
  49. package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
  50. package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
  51. package/template/coding-task-manager/lib/claude-session-title.js +6 -30
  52. package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
  53. package/template/coding-task-manager/lib/client-diag-handler.js +25 -0
  54. package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
  55. package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
  56. package/template/coding-task-manager/lib/codex-history-terminal-renderer.js +33 -33
  57. package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
  58. package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
  59. package/template/coding-task-manager/lib/codex-local-links.js +267 -0
  60. package/template/coding-task-manager/lib/codex-markdown-lists.js +160 -0
  61. package/template/coding-task-manager/lib/codex-native-history-prefix.js +4 -0
  62. package/template/coding-task-manager/lib/codex-paths.js +2 -1
  63. package/template/coding-task-manager/lib/codex-source-handoff.js +186 -0
  64. package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
  65. package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
  66. package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
  67. package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
  68. package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
  69. package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
  70. package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
  71. package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
  72. package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
  73. package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
  74. package/template/coding-task-manager/lib/db-domains.js +21 -2
  75. package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
  76. package/template/coding-task-manager/lib/db-owner-worker-client.js +22 -0
  77. package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
  78. package/template/coding-task-manager/lib/feature-manifest.js +1 -3
  79. package/template/coding-task-manager/lib/fs-watcher.js +3 -2
  80. package/template/coding-task-manager/lib/harness-history-transition.js +41 -14
  81. package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
  82. package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
  83. package/template/coding-task-manager/lib/headless-term-service.js +60 -23
  84. package/template/coding-task-manager/lib/history-transition-read-op.js +179 -23
  85. package/template/coding-task-manager/lib/host-control-dial.js +167 -0
  86. package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
  87. package/template/coding-task-manager/lib/host-handoff.js +51 -13
  88. package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
  89. package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
  90. package/template/coding-task-manager/lib/host-stats.js +328 -0
  91. package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
  92. package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
  93. package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
  94. package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
  95. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
  96. package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
  97. package/template/coding-task-manager/lib/instance-manifest.js +13 -1
  98. package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
  99. package/template/coding-task-manager/lib/kernel/diag-log.js +12 -0
  100. package/template/coding-task-manager/lib/launch-model-override.js +160 -1
  101. package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
  102. package/template/coding-task-manager/lib/lead-model-observer.js +17 -2
  103. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
  104. package/template/coding-task-manager/lib/lead-model-switch.js +11 -2
  105. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +42 -4
  106. package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
  107. package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
  108. package/template/coding-task-manager/lib/login-tripwire.js +18 -0
  109. package/template/coding-task-manager/lib/machine-pairing.js +238 -0
  110. package/template/coding-task-manager/lib/machine-pool.js +409 -0
  111. package/template/coding-task-manager/lib/machine-registry.js +749 -0
  112. package/template/coding-task-manager/lib/machine-self.js +66 -0
  113. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +56 -4
  114. package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
  115. package/template/coding-task-manager/lib/media-journal.js +84 -27
  116. package/template/coding-task-manager/lib/media-store-options.js +43 -15
  117. package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
  118. package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
  119. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +16 -3
  120. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  121. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  122. package/template/coding-task-manager/lib/model-proxy/collaboration-codec.js +94 -0
  123. package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
  124. package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
  125. package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
  126. package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
  127. package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
  128. package/template/coding-task-manager/lib/model-proxy/responses-face.js +24 -2
  129. package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
  130. package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
  131. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  132. package/template/coding-task-manager/lib/native-history-control.js +62 -14
  133. package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
  134. package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
  135. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
  136. package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
  137. package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
  138. package/template/coding-task-manager/lib/prompt-admission-trace.js +69 -0
  139. package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
  140. package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
  141. package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
  142. package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
  143. package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
  144. package/template/coding-task-manager/lib/raw-history-block.js +3 -1
  145. package/template/coding-task-manager/lib/read-pool-client.js +36 -24
  146. package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
  147. package/template/coding-task-manager/lib/read-pool-telemetry.js +14 -5
  148. package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
  149. package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
  150. package/template/coding-task-manager/lib/remote-command-context.js +12 -0
  151. package/template/coding-task-manager/lib/remote-command-runtime.js +18 -5
  152. package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
  153. package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
  154. package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
  155. package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
  156. package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
  157. package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
  158. package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
  159. package/template/coding-task-manager/lib/restart-preflight.js +9 -2
  160. package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
  161. package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
  162. package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
  163. package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
  164. package/template/coding-task-manager/lib/resume-cwd.js +13 -0
  165. package/template/coding-task-manager/lib/resume-from-store.js +12 -1
  166. package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
  167. package/template/coding-task-manager/lib/resume-preflight.js +21 -16
  168. package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
  169. package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
  170. package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
  171. package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
  172. package/template/coding-task-manager/lib/root-prompt-admission.js +43 -11
  173. package/template/coding-task-manager/lib/root-prompt-flow.js +175 -38
  174. package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
  175. package/template/coding-task-manager/lib/server-memory-sampler-runtime.js +10 -0
  176. package/template/coding-task-manager/lib/server-memory-sampler.js +41 -2
  177. package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
  178. package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
  179. package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
  180. package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
  181. package/template/coding-task-manager/lib/session-create/identity.js +20 -1
  182. package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
  183. package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
  184. package/template/coding-task-manager/lib/session-create/provider-process.js +38 -39
  185. package/template/coding-task-manager/lib/session-create/response.js +30 -12
  186. package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
  187. package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
  188. package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
  189. package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
  190. package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
  191. package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
  192. package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
  193. package/template/coding-task-manager/lib/session-host-manager.js +152 -206
  194. package/template/coding-task-manager/lib/session-jobs.js +17 -9
  195. package/template/coding-task-manager/lib/session-lifecycle.js +20 -50
  196. package/template/coding-task-manager/lib/session-model-action-preconditions.js +68 -3
  197. package/template/coding-task-manager/lib/session-resource-sampler.js +67 -9
  198. package/template/coding-task-manager/lib/session-standup.js +67 -4
  199. package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
  200. package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
  201. package/template/coding-task-manager/lib/session-status.js +2 -2
  202. package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
  203. package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
  204. package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
  205. package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
  206. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
  207. package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
  208. package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
  209. package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
  210. package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
  211. package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
  212. package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
  213. package/template/coding-task-manager/lib/transcript-link.js +77 -0
  214. package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
  215. package/template/coding-task-manager/lib/transcript-source.js +8 -3
  216. package/template/coding-task-manager/lib/transcript-store.js +40 -0
  217. package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
  218. package/template/coding-task-manager/lib/typing-interaction-trace.js +136 -2
  219. package/template/coding-task-manager/lib/typing-worker-stages.js +208 -1
  220. package/template/coding-task-manager/lib/usage-ledger/claude-parse.js +53 -2
  221. package/template/coding-task-manager/lib/usage-ledger/ingest.js +13 -4
  222. package/template/coding-task-manager/lib/usage-ledger/read.js +14 -0
  223. package/template/coding-task-manager/lib/usage-ledger/schema.js +49 -1
  224. package/template/coding-task-manager/lib/usage-ledger/store.js +337 -6
  225. package/template/coding-task-manager/lib/usage-ledger/task-read.js +123 -5
  226. package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
  227. package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
  228. package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
  229. package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
  230. package/template/coding-task-manager/lib/ws/input.js +1 -0
  231. package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
  232. package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
  233. package/template/coding-task-manager/lib/ws-router.js +68 -1
  234. package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
  235. package/template/coding-task-manager/oauth-proxy.js +99 -55
  236. package/template/coding-task-manager/package-lock.json +24 -0
  237. package/template/coding-task-manager/package.json +3 -0
  238. package/template/coding-task-manager/protocol/message-table.json +0 -10
  239. package/template/coding-task-manager/protocol/messages.js +0 -10
  240. package/template/coding-task-manager/protocol/ws-schema.json +0 -20
  241. package/template/coding-task-manager/protocol/ws-schema.md +0 -2
  242. package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
  243. package/template/coding-task-manager/providers/codex-mcp.js +14 -5
  244. package/template/coding-task-manager/public/css/app.css +279 -2
  245. package/template/coding-task-manager/public/css/reviews.css +4 -0
  246. package/template/coding-task-manager/public/index.html +297 -240
  247. package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
  248. package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
  249. package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
  250. package/template/coding-task-manager/public/js/background-work.js +13 -7
  251. package/template/coding-task-manager/public/js/client-health-core.js +81 -0
  252. package/template/coding-task-manager/public/js/client-health-main.js +61 -0
  253. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
  254. package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
  255. package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
  256. package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
  257. package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
  258. package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
  259. package/template/coding-task-manager/public/js/history-scroller.js +187 -9
  260. package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
  261. package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
  262. package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
  263. package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -4
  264. package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
  265. package/template/coding-task-manager/public/js/protocol.js +0 -10
  266. package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
  267. package/template/coding-task-manager/public/js/reviews.js +7 -3
  268. package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
  269. package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
  270. package/template/coding-task-manager/public/js/row-join.js +18 -3
  271. package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
  272. package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
  273. package/template/coding-task-manager/public/js/session-usage-view.js +1 -0
  274. package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
  275. package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
  276. package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
  277. package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
  278. package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
  279. package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
  280. package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
  281. package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
  282. package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
  283. package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
  284. package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
  285. package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -3
  286. package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
  287. package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
  288. package/template/coding-task-manager/public/js/walle-session.js +30 -7
  289. package/template/coding-task-manager/public/m/app.css +226 -8
  290. package/template/coding-task-manager/public/m/app.js +502 -9
  291. package/template/coding-task-manager/public/m/index.html +38 -1
  292. package/template/coding-task-manager/public/m/machines.js +1313 -0
  293. package/template/coding-task-manager/public/m/pool-failover.js +391 -0
  294. package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
  295. package/template/coding-task-manager/public/m/sw.js +5 -0
  296. package/template/coding-task-manager/queue-engine.js +13 -0
  297. package/template/coding-task-manager/server/approvals.js +5 -6
  298. package/template/coding-task-manager/server/boot.js +136 -17
  299. package/template/coding-task-manager/server.js +964 -425
  300. package/template/coding-task-manager/session-integrity.js +7 -9
  301. package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
  302. package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
  303. package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
  304. package/template/coding-task-manager/workers/session-host-process.js +14 -3
  305. package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
  306. package/template/coding-task-manager/workers/worktree-worker.js +34 -0
  307. package/template/package-lock.json +2 -2
  308. package/template/package.json +1 -1
  309. package/template/scripts/conversation-storage-v2-migration.js +78 -11
  310. package/template/shared/conversation-storage-v2-contract.js +42 -6
  311. package/template/shared/fixtures/worker-status-payloads.json +392 -0
  312. package/template/shared/git-remote-key.js +83 -0
  313. package/template/shared/host-identity.js +98 -2
  314. package/template/shared/owner-write-safety.js +135 -0
  315. package/template/shared/public-runtime.js +13 -5
  316. package/template/shared/release-schema.json +3 -2
  317. package/template/shared/report-citations.js +14 -0
  318. package/template/shared/sqlite-write-lock.js +361 -223
  319. package/template/shared/storage/contract.js +35 -3
  320. package/template/wall-e/agent.js +21 -0
  321. package/template/wall-e/api-walle.js +5 -0
  322. package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
  323. package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
  324. package/template/wall-e/brain/helper-rules-schema.js +129 -0
  325. package/template/wall-e/brain/migrations.js +51 -22
  326. package/template/wall-e/brain/model-deployment-facts.js +31 -1
  327. package/template/wall-e/brain/owner-call-whitelist.js +16 -0
  328. package/template/wall-e/brain/schema-ensures.js +22 -0
  329. package/template/wall-e/brain.js +1 -1
  330. package/template/wall-e/coding/host-capabilities.js +2 -2
  331. package/template/wall-e/coding/journal-dispatcher.js +51 -0
  332. package/template/wall-e/coding/session-journal.js +28 -40
  333. package/template/wall-e/coding-orchestrator.js +23 -26
  334. package/template/wall-e/fly.toml +1 -1
  335. package/template/wall-e/http/auth.js +18 -1
  336. package/template/wall-e/http/blog-views-api.js +318 -0
  337. package/template/wall-e/http/helper-rules-api.js +321 -0
  338. package/template/wall-e/http/model-proxy-providers.js +3 -1
  339. package/template/wall-e/http/telemetry-api.js +12 -0
  340. package/template/wall-e/lib/blog-view-policy.js +88 -0
  341. package/template/wall-e/lib/blog-view-store.js +169 -0
  342. package/template/wall-e/lib/client-config-lock.js +2 -1
  343. package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
  344. package/template/wall-e/lib/error-kinds.js +10 -1
  345. package/template/wall-e/lib/feature-manifest.js +1 -1
  346. package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
  347. package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
  348. package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
  349. package/template/wall-e/lib/helper-rules/grade.js +277 -0
  350. package/template/wall-e/lib/helper-rules/label.js +247 -0
  351. package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
  352. package/template/wall-e/lib/helper-rules/store.js +667 -0
  353. package/template/wall-e/lib/mcp-integration.js +5 -6
  354. package/template/wall-e/lib/runtime-worker-pool.js +1 -0
  355. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
  356. package/template/wall-e/lib/telemetry-db-schema.js +37 -0
  357. package/template/wall-e/lib/telemetry-process-client.js +76 -1
  358. package/template/wall-e/llm/default-fallback.js +4 -0
  359. package/template/wall-e/llm/model-catalog.js +13 -4
  360. package/template/wall-e/llm/model-roles.js +6 -0
  361. package/template/wall-e/llm/portkey-http.js +150 -0
  362. package/template/wall-e/llm/portkey-sync.js +15 -145
  363. package/template/wall-e/llm/worker-mcp-routing.js +61 -0
  364. package/template/wall-e/llm/worker-route.js +43 -1
  365. package/template/wall-e/loops/brain-custom-writes.js +45 -0
  366. package/template/wall-e/mcp-protocol-contract.js +41 -1
  367. package/template/wall-e/mcp-server.js +13 -2
  368. package/template/wall-e/mcp-worker-schemas.js +20 -2
  369. package/template/wall-e/memory/ctm-context-client.js +25 -5
  370. package/template/wall-e/shared/release-schema.json +3 -2
  371. package/template/wall-e/shared/sqlite-write-lock.js +361 -223
  372. package/template/wall-e/shared/storage/contract.js +35 -3
  373. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  374. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
  375. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
  376. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
  377. package/template/wall-e/tools/local-tools.js +12 -5
  378. package/template/wall-e/workers/brain-owner-worker.js +9 -0
  379. package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
  380. package/template/wall-e/workers/telemetry-worker.js +8 -0
  381. package/template/coding-task-manager/lib/giant-freeze-park.js +0 -31
@@ -8,6 +8,9 @@ function listPurgedDraftAttachmentIds(dbHandle) {
8
8
  }
9
9
 
10
10
  const path = require('path');
11
+ // WP-6 (session-name-agent-title-precedence §6): the single tier-2 agent-title apply, reused by the
12
+ // one-shot backfill (applyAgentTitleBackfillRows below) exactly as the ingest path uses it.
13
+ const { applyAgentTitleSignals } = require('./lib/agent-title-apply');
11
14
  const { createLegacyTerminalDraftCreate } = require('./lib/terminal-draft-create');
12
15
  // WP-1 (§6, §6.1): the shared canonical draft lifecycle contract — lifecycle-version reader,
13
16
  // complete canonical field projection + comparator, and the single version allocator. Shared with
@@ -43,6 +46,8 @@ const conversationStoreV2 = require('./lib/conversation-store-v2');
43
46
  const walleHistoryCutoverV2 = require('./lib/walle-history-cutover-v2');
44
47
  const { createReviewReposStore } = require('./lib/storage/review-repos-store');
45
48
  const { insertResumeLineage } = require('./lib/storage/resume-lineage-store');
49
+ // WP-4 (§9 S-21): workspace_bindings.git_remote stores the NORMALIZED key, never a raw remote URL.
50
+ const { normalizeGitRemote } = require('../shared/git-remote-key');
46
51
 
47
52
  const { execFileSync } = require('child_process');
48
53
  const { normalizeAgentType } = require('./lib/agent-capabilities');
@@ -122,7 +127,7 @@ const {
122
127
  ensureSessionModelIntentSchema,
123
128
  } = require('./lib/migrations/v53-session-model-intent');
124
129
  const { createCodingSessionGenerationStore } = require('./lib/storage/coding-session-generation-store');
125
- const { createLeadModelTransitionStore } = require('./lib/lead-model-transition-store');
130
+ const { createLeadModelTransitionStore, resumesCandidateFork } = require('./lib/lead-model-transition-store');
126
131
  const { createSessionModelIntentStore } = require('./lib/storage/session-model-intent-store');
127
132
  const { createSessionLeadEffortStore } = require('./lib/storage/session-lead-effort-store');
128
133
  const { createSessionLeadModelStore } = require('./lib/storage/session-lead-model-store');
@@ -207,7 +212,9 @@ const {
207
212
  ensureHostIdentity,
208
213
  identityFilePath,
209
214
  isSameOwnerLineage: _isSameOwnerLineagePure,
215
+ lineageCrossHost: _lineageCrossHost,
210
216
  ownerLineageComponent: _ownerLineageComponentPure,
217
+ readOsMachineIdHash,
211
218
  } = require('./lib/host-identity');
212
219
  const {
213
220
  ensureResourceLeaseSchema,
@@ -350,8 +357,9 @@ let hostedIdentityState = null;
350
357
  // import, clone/re-key handling) ever needs to change the instance identity WHILE the process is
351
358
  // already serving traffic, it MUST also null out hostedIdentitySnapshot right there, or every
352
359
  // caller downstream of getOrCreateInstanceIdentity silently keeps serving the stale identity for
353
- // the rest of the process's life. tests/instance-identity-snapshot-invariant.test.js grep-asserts
354
- // this invariant holds (no new instance_identity writer, no new snapshot reset site).
360
+ // the rest of the process's life. NOTHING test-enforces this today: the grep-based
361
+ // tests/instance-identity-snapshot-invariant.test.js was deleted on 2026-09-15
362
+ // (tests/.deflake/wp5-deletions-2026-09-15.txt) -- a new writer/reset site must be reviewed by hand.
355
363
  let hostedIdentitySnapshot = null;
356
364
 
357
365
  // WP-0 (docs/instance-identity-boot-order design, C3): lib/instance-identity-access.js keeps its
@@ -1219,6 +1227,10 @@ function _ensureHostedPrimarySchema(d = getDb()) {
1219
1227
 
1220
1228
  _invalidateTableExistsMemo(); // R4-10: the exec above may have created the sync/instance tables
1221
1229
  _ensureSyncedEntitiesOriginKey(target);
1230
+ // WP-0 (remote-machine-hub-sessions-2026-10-22.html §6.1): the hub-side machine registry.
1231
+ // The DDL body lives in _ensureMachineRegistrySchema (defined further down this file, next to
1232
+ // the other schema-ensure helpers) for the reason stated there.
1233
+ _ensureMachineRegistrySchema(target);
1222
1234
  for (const tableName of ['ctm_sessions', 'prompts', 'folders']) {
1223
1235
  _safeAlterAddColumn(tableName, "origin_instance_id TEXT DEFAULT ''");
1224
1236
  _safeAlterAddColumn(tableName, "current_owner_instance_id TEXT DEFAULT ''");
@@ -1234,6 +1246,25 @@ function _ensureHostedPrimarySchema(d = getDb()) {
1234
1246
  _safeAlterAddColumn('startup_tasks', "current_owner_instance_id TEXT DEFAULT ''");
1235
1247
  _safeAlterAddColumn('startup_tasks', "parked_at TEXT DEFAULT ''");
1236
1248
  _safeAlterAddColumn('startup_tasks', "park_reason TEXT DEFAULT ''");
1249
+ // WP-2 (docs/remote-hub-ownership-fences-and-restore-skip-diagnostics-2026-10-23.html §5 D-3,
1250
+ // §6, §9 S-5): was this re-mint edge minted ACROSS a host boundary?
1251
+ // 1 — the prior instance_identity row's stored host_fingerprint was non-empty and was NOT
1252
+ // in this host's live ∪ compatible fingerprint set at detection (_hostIdentityFingerprints,
1253
+ // below): the DATABASE FILE itself arrived from another machine (the 2026-10-14 Spark
1254
+ // file-clone whose re-mint rewrote 3779 Mac-owned rows to Spark-owned).
1255
+ // 0 — the stored fingerprint matches this host: a legitimate same-host re-mint (the Mac's
1256
+ // own 2026-09-30 ctm-LLXYK6XTNF-3456 → …-1a2c9c), or no stored fingerprint at all
1257
+ // (empty ⇒ cannot prove foreign ⇒ fail toward today's behavior, AC-2c).
1258
+ // NULL — legacy edge minted before this column existed. Treated exactly like 0 everywhere
1259
+ // (restampOwnerLineage only refuses to traverse a literal 1), so pre-existing installs
1260
+ // keep today's claiming behavior.
1261
+ // The ALTER lives HERE, in the hosted-schema block, and deliberately NOT in a new
1262
+ // lib/migrations/vNN file: instance_lineage is created by the DDL above (not by the versioned
1263
+ // ladder), and a post-v18 ALTER against an enrolled table breaks the rollback tests
1264
+ // (memory: ctm-domain-split-post-v18-alter-on-enrolled-table-breaks-rollback-tests).
1265
+ _safeAlterAddColumn('instance_lineage', 'cross_host INTEGER');
1266
+ // WP-B §5 D-B1: the hashed OS machine id of the boot that wrote this row — the cross-host verdict's primary fact.
1267
+ _safeAlterAddColumn('instance_identity', "machine_id TEXT NOT NULL DEFAULT ''");
1237
1268
  _safeAlterAddColumn('ctm_sessions', "continued_from_session_id TEXT DEFAULT ''");
1238
1269
  _safeAlterAddColumn('ctm_sessions', "handoff_target_session_id TEXT DEFAULT ''");
1239
1270
  // WP-1 (design doc §5/§8/§9 S-19): last known physical-footprint sample, persisted at
@@ -1284,6 +1315,9 @@ function _hostIdentityDataDir() {
1284
1315
  return path.dirname(currentDbPath || DEFAULT_DB_PATH);
1285
1316
  }
1286
1317
 
1318
+ let _osMachineIdHashMemo = null; // WP-B §9 B-4: OS machine id read ONCE per process ('' memoized too); tests inject `machineId`
1319
+ function _osMachineIdHashOnce() { if (_osMachineIdHashMemo === null) _osMachineIdHashMemo = readOsMachineIdHash(); return _osMachineIdHashMemo; }
1320
+
1287
1321
  function _hostIdentityFingerprints(options = {}) {
1288
1322
  const hostname = options.hostname || _identityHostname();
1289
1323
  const port = options.port || process.env.CTM_PORT || '';
@@ -1320,6 +1354,7 @@ function _hostIdentityFingerprints(options = {}) {
1320
1354
 
1321
1355
  function _recordInstanceLineage(d, lineage) {
1322
1356
  if (!lineage || !lineage.priorInstanceId || !lineage.newInstanceId) return null;
1357
+ const crossHost = Number(lineage.crossHost) === 1 ? 1 : 0;
1323
1358
  const existing = d.prepare(`
1324
1359
  SELECT * FROM instance_lineage
1325
1360
  WHERE prior_instance_id = ? AND new_instance_id = ? AND reason = ?
@@ -1327,17 +1362,28 @@ function _recordInstanceLineage(d, lineage) {
1327
1362
  `).get(lineage.priorInstanceId, lineage.newInstanceId, lineage.reason || 'clone_detected');
1328
1363
  if (existing) return existing;
1329
1364
  d.prepare(`
1330
- INSERT INTO instance_lineage (prior_instance_id, new_instance_id, reason, detected_at)
1331
- VALUES (?, ?, ?, ?)
1365
+ INSERT INTO instance_lineage (prior_instance_id, new_instance_id, reason, detected_at, cross_host)
1366
+ VALUES (?, ?, ?, ?, ?)
1332
1367
  `).run(
1333
1368
  lineage.priorInstanceId,
1334
1369
  lineage.newInstanceId,
1335
1370
  lineage.reason || 'clone_detected',
1336
- lineage.detectedAt || new Date().toISOString()
1371
+ lineage.detectedAt || new Date().toISOString(),
1372
+ crossHost
1337
1373
  );
1338
1374
  return d.prepare('SELECT * FROM instance_lineage WHERE rowid = last_insert_rowid()').get();
1339
1375
  }
1340
1376
 
1377
+ // WP-C §9 C-2: both read wrappers below must see each edge's cross_host verdict, or they would
1378
+ // keep traversing a cloned-DB edge and treat the source machine's ids as this host's own (design
1379
+ // §1.3 C-1..C-9). The column is added by the hosted-schema ALTER block, so it exists on any DB
1380
+ // this build has booted; a legacy schema without it reads no key, which the pure closure treats
1381
+ // as NULL — traversable — i.e. exactly today's behavior.
1382
+ function _ownerLineageReadRows() {
1383
+ const crossHost = _tableHasColumn('instance_lineage', 'cross_host') ? ', cross_host' : '';
1384
+ return getDomainDb('core').prepare(`SELECT prior_instance_id, new_instance_id${crossHost} FROM instance_lineage`).all();
1385
+ }
1386
+
1341
1387
  // WP-3 (docs/ctm-session-restore-loss-and-render-wedge-2026-07-26.html) — DB-backed wrapper
1342
1388
  // around lib/host-identity.js's pure isSameOwnerLineage: reads the full instance_lineage table
1343
1389
  // (small, control-plane, per BASE_RESIDENT-adjacent core classification — never large) and
@@ -1347,8 +1393,7 @@ function _recordInstanceLineage(d, lineage) {
1347
1393
  function isSameOwnerLineage(candidateId, currentId) {
1348
1394
  const current = String(currentId || _currentHostedInstanceId() || '').trim();
1349
1395
  if (!_tableExists('instance_lineage')) return String(candidateId || '').trim() === current;
1350
- const rows = getDomainDb('core').prepare('SELECT prior_instance_id, new_instance_id FROM instance_lineage').all();
1351
- return _isSameOwnerLineagePure(candidateId, current, rows);
1396
+ return _isSameOwnerLineagePure(candidateId, current, _ownerLineageReadRows());
1352
1397
  }
1353
1398
 
1354
1399
  // WP-3: returns the full lineage CONNECTED COMPONENT for `currentId` (every instance id
@@ -1361,8 +1406,7 @@ function getOwnerLineageComponent(currentId) {
1361
1406
  const current = String(currentId || _currentHostedInstanceId() || '').trim();
1362
1407
  if (!current) return [];
1363
1408
  if (!_tableExists('instance_lineage')) return [current];
1364
- const rows = getDomainDb('core').prepare('SELECT prior_instance_id, new_instance_id FROM instance_lineage').all();
1365
- return [..._ownerLineageComponentPure(current, rows)];
1409
+ return [..._ownerLineageComponentPure(current, _ownerLineageReadRows())];
1366
1410
  }
1367
1411
 
1368
1412
  // WP-3 re-stamp migration (docs/ctm-session-restore-loss-and-render-wedge-2026-07-26.html):
@@ -1383,9 +1427,9 @@ const _RESTAMP_EMPTY_RESULT = Object.freeze({
1383
1427
  ctm_sessions: 0,
1384
1428
  startup_tasks: 0,
1385
1429
  ctm_sessions_execution_host: 0,
1386
- // WP-5 (docs/instance-identity-boot-order-2026-08-08.html S-8/S-9/S-10): three more live
1387
- // ownership pointers the census found unrestamped.
1388
- terminal_draft_images: 0,
1430
+ // WP-5 (docs/instance-identity-boot-order-2026-08-08.html S-8/S-9/S-10): more live ownership
1431
+ // pointers the census found unrestamped. Drafts are not one of them: the media-store allocator
1432
+ // normalizes their owner at boot, never this transaction (R2 OQ-4 removed the dead counter).
1389
1433
  workspace_bindings: 0,
1390
1434
  workspace_bindings_conflicts_resolved: 0,
1391
1435
  startup_tasks_ctm_instance: 0,
@@ -1395,28 +1439,39 @@ const _RESTAMP_EMPTY_RESULT = Object.freeze({
1395
1439
  resource_leases: 0,
1396
1440
  });
1397
1441
 
1442
+ // WP-C §5 D-C2: the one closure (shared/host-identity.js ownerLineageComponent) is called twice
1443
+ // here — with the default options for the set this host may claim, and with
1444
+ // `{ traverseCrossHost: true }` to discover the ids that hang off the current host's component
1445
+ // ONLY across a cloned-DB edge. Those are foreign mirrors: rows they own belong to another
1446
+ // machine and are never claimed (P-2). The private cross-host-only twin this replaces is gone —
1447
+ // its one extra predicate now lives in the shared closure above (REPLACE IN PLACE).
1398
1448
  function restampOwnerLineage(options = {}) {
1399
1449
  if (process.env.CTM_OWNER_LINEAGE_RESTAMP === '0') return { ok: false, skipped: true, reason: 'disabled' };
1400
1450
  const currentId = String(options.currentId || _currentHostedInstanceId() || '').trim();
1401
1451
  if (!currentId) return { ok: false, skipped: true, reason: 'no-current-identity' };
1402
- if (!_tableExists('instance_lineage')) return { ok: true, restamped: { ..._RESTAMP_EMPTY_RESULT } };
1403
- const d = getDomainDb('core');
1404
- const lineageRows = d.prepare('SELECT prior_instance_id, new_instance_id FROM instance_lineage').all();
1405
- // Every DISTINCT owner id currently on record that is lineage-linked to currentId (excluding
1406
- // currentId itself — nothing to restamp there). Bounded by the lineage graph's own size, never
1407
- // a full table scan of distinct owner ids beyond what instance_lineage actually references.
1408
- const candidateIds = new Set();
1409
- for (const row of lineageRows) {
1410
- for (const id of [row.prior_instance_id, row.new_instance_id]) {
1411
- const clean = String(id || '').trim();
1412
- if (clean && clean !== currentId) candidateIds.add(clean);
1413
- }
1452
+ if (!_tableExists('instance_lineage')) {
1453
+ return { ok: true, restamped: { ..._RESTAMP_EMPTY_RESULT }, foreignParked: { startup_tasks: 0, ctm_sessions: 0 } };
1414
1454
  }
1415
- const lineageLinked = [...candidateIds].filter((id) => _isSameOwnerLineagePure(id, currentId, lineageRows));
1455
+ const d = getDomainDb('core');
1456
+ const lineageRows = _ownerLineageReadRows();
1457
+ // Every owner id transitively lineage-linked to currentId (excluding currentId itself — nothing
1458
+ // to restamp there). Bounded by the lineage graph's own size, never a full table scan of distinct
1459
+ // owner ids beyond what instance_lineage actually references.
1460
+ const lineageLinked = [..._ownerLineageComponentPure(currentId, lineageRows, { traverseCrossHost: true })]
1461
+ .filter((id) => id !== currentId);
1416
1462
  const result = { ..._RESTAMP_EMPTY_RESULT };
1417
- if (!lineageLinked.length) return { ok: true, restamped: result };
1463
+ // WP-2 §5 D-3 / §9 S-6/S-8: split the lineage-linked owners by WHICH EDGES connect them to
1464
+ // currentId. `claimable` reached currentId without crossing a cross_host = 1 edge (this host's
1465
+ // own history, including a live same-host re-mint) — the legacy claim set. `foreign` reached it
1466
+ // only across a cloned-DB edge: rows they own belong to another machine and are MIRRORED, never
1467
+ // claimed (P-2: foreign state is a mirror, never a peer write).
1468
+ const sameHostComponent = _ownerLineageComponentPure(currentId, lineageRows);
1469
+ const claimable = lineageLinked.filter((id) => sameHostComponent.has(id));
1470
+ const foreign = lineageLinked.filter((id) => !sameHostComponent.has(id));
1471
+ const foreignParked = { startup_tasks: 0, ctm_sessions: 0 };
1472
+ if (!lineageLinked.length) return { ok: true, restamped: result, foreignParked };
1418
1473
  const txn = d.transaction(() => {
1419
- for (const staleId of lineageLinked) {
1474
+ for (const staleId of claimable) {
1420
1475
  if (_tableHasColumn('ctm_sessions', 'current_owner_instance_id')) {
1421
1476
  result.ctm_sessions += d.prepare(
1422
1477
  'UPDATE ctm_sessions SET current_owner_instance_id = ? WHERE current_owner_instance_id = ?'
@@ -1483,15 +1538,46 @@ function restampOwnerLineage(options = {}) {
1483
1538
  ).run(currentId, staleId).changes || 0;
1484
1539
  }
1485
1540
  }
1541
+ // WP-2 §5 D-3 / §9 S-6: the rows owned by the FOREIGN ids are mirrored, not claimed — in the
1542
+ // SAME transaction, so a boot can never observe half a decision. The owner column stays
1543
+ // untouched on every table (P-2: foreign state is a mirror; ownership leaves a host only via
1544
+ // the explicit transfer path, design R-14), and startup_tasks gets the established parked
1545
+ // representation (status='parked' + park_reason='foreign-clone'), the same shape
1546
+ // lib/remote-machine-catalog.js reads as "exists, owned elsewhere" and _startupTaskRestorable
1547
+ // already refuses to restore. Idempotent: the != 'foreign-clone' predicate makes a second boot
1548
+ // over the same cloned DB park nothing new, and an already-'exited' row is left alone (its
1549
+ // bookkeeping is terminal).
1550
+ //
1551
+ // ctm_sessions has NO status/park_reason columns (verified with PRAGMA table_info on a temp-db
1552
+ // clone built by this file's own schema: it carries sync_status and parked_at but no park
1553
+ // vocabulary — the hosted ALTER block above never adds one), so there is no park
1554
+ // representation to write there; leaving its owner untouched IS the fence (no claim, no
1555
+ // park), and foreignParked.ctm_sessions stays 0 rather than inventing a second representation.
1556
+ if (foreign.length && _tableHasColumn('startup_tasks', 'status')
1557
+ && _tableHasColumn('startup_tasks', 'park_reason') && _tableHasColumn('startup_tasks', 'parked_at')) {
1558
+ const parkedAt = new Date().toISOString();
1559
+ for (const foreignId of foreign) {
1560
+ foreignParked.startup_tasks += d.prepare(`
1561
+ UPDATE startup_tasks
1562
+ SET status = 'parked', park_reason = 'foreign-clone', parked_at = ?
1563
+ WHERE current_owner_instance_id = ?
1564
+ AND COALESCE(status, 'running') != 'exited'
1565
+ AND COALESCE(park_reason, '') != 'foreign-clone'
1566
+ `).run(parkedAt, foreignId).changes || 0;
1567
+ }
1568
+ }
1486
1569
  });
1487
1570
  txn();
1488
1571
  const total = result.ctm_sessions + result.startup_tasks + result.ctm_sessions_execution_host
1489
- + result.terminal_draft_images + result.workspace_bindings + result.workspace_bindings_conflicts_resolved
1572
+ + result.workspace_bindings + result.workspace_bindings_conflicts_resolved
1490
1573
  + result.startup_tasks_ctm_instance + result.resource_leases;
1491
1574
  if (total > 0) {
1492
- console.log(`[owner-lineage-restamp] restamped ${total} row(s) to ${currentId} (ctm_sessions=${result.ctm_sessions}, startup_tasks=${result.startup_tasks}, ctm_sessions_execution_host=${result.ctm_sessions_execution_host}, terminal_draft_images=${result.terminal_draft_images}, workspace_bindings=${result.workspace_bindings}, workspace_bindings_conflicts_resolved=${result.workspace_bindings_conflicts_resolved}, startup_tasks_ctm_instance=${result.startup_tasks_ctm_instance}, resource_leases=${result.resource_leases})`);
1575
+ console.log(`[owner-lineage-restamp] restamped ${total} row(s) to ${currentId} (ctm_sessions=${result.ctm_sessions}, startup_tasks=${result.startup_tasks}, ctm_sessions_execution_host=${result.ctm_sessions_execution_host}, workspace_bindings=${result.workspace_bindings}, workspace_bindings_conflicts_resolved=${result.workspace_bindings_conflicts_resolved}, startup_tasks_ctm_instance=${result.startup_tasks_ctm_instance}, resource_leases=${result.resource_leases})`);
1493
1576
  }
1494
- return { ok: true, restamped: result };
1577
+ if (foreignParked.startup_tasks > 0) {
1578
+ console.log(`[owner-lineage-restamp] parked ${foreignParked.startup_tasks} foreign row(s) as park_reason='foreign-clone' (owner preserved) — ${foreign.length} id(s) reached ${currentId} only across a cross-host lineage edge`);
1579
+ }
1580
+ return { ok: true, restamped: result, foreignParked };
1495
1581
  }
1496
1582
 
1497
1583
  // Explicit, idempotent retirement utility for an auto-learned approval rule. Callers must
@@ -1558,21 +1644,28 @@ function _latestInstanceLineage(d, instanceId) {
1558
1644
  reason: row.reason || '',
1559
1645
  detectedAt: row.detected_at || '',
1560
1646
  detected_at: row.detected_at || '',
1647
+ // WP-2 §5 D-3: read the STORED verdict — never recomputed at read time (the prior identity
1648
+ // row this was derived from no longer exists). NULL = legacy edge (pre-WP-2), reported as 0
1649
+ // so consumers see the same "same host" default restampOwnerLineage applies.
1650
+ crossHost: Number(row.cross_host) === 1 ? 1 : 0,
1651
+ cross_host: Number(row.cross_host) === 1 ? 1 : 0,
1561
1652
  };
1562
1653
  }
1563
1654
 
1564
- function _replaceInstanceIdentityRow(d, identity, source) {
1655
+ function _replaceInstanceIdentityRow(d, identity, source, machineId = '') {
1656
+ const hasMachineId = _tableHasColumn('instance_identity', 'machine_id');
1565
1657
  d.prepare('DELETE FROM instance_identity').run();
1566
1658
  d.prepare(`
1567
- INSERT INTO instance_identity (instance_id, created_at, host_fingerprint, source)
1568
- VALUES (?, ?, ?, ?)
1659
+ INSERT INTO instance_identity (instance_id, created_at, host_fingerprint, source${hasMachineId ? ', machine_id' : ''})
1660
+ VALUES (?, ?, ?, ?${hasMachineId ? ', ?' : ''})
1569
1661
  `).run(
1570
1662
  identity.instanceId,
1571
1663
  identity.createdAt || new Date().toISOString(),
1572
1664
  identity.hostFingerprint || '',
1573
- source || identity.source || 'generated'
1574
- );
1575
- return d.prepare('SELECT * FROM instance_identity WHERE instance_id = ?').get(identity.instanceId);
1665
+ source || identity.source || 'generated',
1666
+ ...(hasMachineId ? [String(machineId || '')] : []));
1667
+ // WP-B D-B3: machine_id is not selected — this row spreads into the boot identity state & manifest.
1668
+ return d.prepare('SELECT instance_id, created_at, host_fingerprint, source FROM instance_identity WHERE instance_id = ?').get(identity.instanceId);
1576
1669
  }
1577
1670
 
1578
1671
  function _backfillHostedIdentityProvenance(d, state) {
@@ -1653,6 +1746,7 @@ function getOrCreateInstanceIdentity(options = {}) {
1653
1746
  const hostname = options.hostname || _identityHostname();
1654
1747
  const port = options.port || process.env.CTM_PORT || '';
1655
1748
  const fingerprints = _hostIdentityFingerprints({ hostname, port });
1749
+ const machineId = options.machineId ?? _osMachineIdHashOnce(); // WP-B D-B2: the verdict's primary fact
1656
1750
  const existing = d.prepare('SELECT * FROM instance_identity ORDER BY created_at LIMIT 1').get();
1657
1751
  const ensured = ensureHostIdentity({
1658
1752
  dataDir: _hostIdentityDataDir(),
@@ -1684,8 +1778,15 @@ function getOrCreateInstanceIdentity(options = {}) {
1684
1778
  console.log(mintLog);
1685
1779
  }
1686
1780
  }
1687
- const row = _replaceInstanceIdentityRow(d, identity, source);
1688
- const lineageRow = _recordInstanceLineage(d, ensured.lineage);
1781
+ const row = _replaceInstanceIdentityRow(d, identity, source, machineId);
1782
+ // WP-2 §5 D-3 / §9 S-5 / WP-B D-B2: stamp whether THIS re-mint edge crosses a host boundary,
1783
+ // computed from the prior instance_identity row (`existing`, read above and churned by the line
1784
+ // just before) plus the live OS machine id (WP-B: it decides alone when both sides know theirs),
1785
+ // falling back to this host's fingerprint set (`fingerprints`, :1308-1325) when either is unknown.
1786
+ // After _replaceInstanceIdentityRow the prior row is gone, so the verdict is stored (AC-2c).
1787
+ const lineageRow = _recordInstanceLineage(d, ensured.lineage
1788
+ ? { ...ensured.lineage, crossHost: _lineageCrossHost(existing, fingerprints, (priorPort) => _hostIdentityFingerprints({ hostname, port: priorPort }), machineId) }
1789
+ : ensured.lineage);
1689
1790
  const backfill = _backfillHostedIdentityProvenance(d, ensured);
1690
1791
  const lineage = ensured.lineage
1691
1792
  ? _latestInstanceLineage(d, identity.instanceId)
@@ -1763,7 +1864,7 @@ function upsertHostedWorkspaceBinding(binding = {}) {
1763
1864
  const logicalId = _hostedToken(binding.logicalId || binding.logical_id || binding.id || '', '');
1764
1865
  const hostInstanceId = _hostedToken(binding.hostInstanceId || binding.host_instance_id || binding.currentOwnerInstanceId || '', '');
1765
1866
  const hostPath = String(binding.hostPath || binding.host_path || binding.path || '').trim();
1766
- const gitRemote = String(binding.gitRemote || binding.git_remote || binding.repoRemote || '').trim();
1867
+ const gitRemote = normalizeGitRemote(binding.gitRemote || binding.git_remote || binding.repoRemote || '');
1767
1868
  if (!logicalId) throw new Error('logical_id required');
1768
1869
  if (!hostInstanceId) throw new Error('host_instance_id required');
1769
1870
  if (!hostPath) throw new Error('host_path required');
@@ -1799,7 +1900,7 @@ function seedHostedWorkspaceBindings(bindings = [], instanceId = '') {
1799
1900
  const hostPath = String(binding.hostPath || binding.host_path || '').trim();
1800
1901
  const logicalId = _hostedToken(binding.logicalId || binding.logical_id || binding.id || '', '');
1801
1902
  if (!hostPath || !logicalId) continue;
1802
- stmt.run(logicalId, id, hostPath, String(binding.gitRemote || binding.git_remote || binding.repoRemote || '').trim());
1903
+ stmt.run(logicalId, id, hostPath, normalizeGitRemote(binding.gitRemote || binding.git_remote || binding.repoRemote || ''));
1803
1904
  count += 1;
1804
1905
  }
1805
1906
  });
@@ -3326,22 +3427,15 @@ function applySyncSnapshot(options = {}) {
3326
3427
  return value;
3327
3428
  }
3328
3429
 
3430
+ // S-19 (remote-machine-hub-sessions-2026-10-22.html §9): this used to carry its own copy of
3431
+ // the CTM_SYNC_PEERS grammar (JSON array vs `id=url` CSV) — for every caller that read it and
3432
+ // for lib/hosted-sync-worker.js#parseSyncPeers, which parses the same env. One parser now:
3433
+ // the sync worker's, which the machines registry also seeds from. (The old copy additionally
3434
+ // ignored CTM_SYNC_PEER_ALLOWLIST; nothing depends on that difference — this helper has no
3435
+ // callers in the tree today, and the allowlist-filtered set is the correct peer list.)
3329
3436
  function _syncConfiguredPeerIds(env = process.env) {
3330
- const raw = String(env.CTM_SYNC_PEERS || env.CTM_SYNC_PEER_URLS || '').trim();
3331
- if (!raw) return [];
3332
- if (raw.startsWith('[')) {
3333
- try {
3334
- const parsed = JSON.parse(raw);
3335
- if (Array.isArray(parsed)) {
3336
- return parsed.map((entry) => _hostedToken(entry.instanceId || entry.instance_id || entry.id || entry.name || '', '')).filter(Boolean);
3337
- }
3338
- } catch {}
3339
- }
3340
- return raw.split(',')
3341
- .map((part) => {
3342
- const [id, ...rest] = part.split('=');
3343
- return rest.length ? _hostedToken(id, '') : '';
3344
- })
3437
+ return require('./lib/hosted-sync-worker').parseSyncPeers(env)
3438
+ .map((peer) => _hostedToken(peer.instanceId, ''))
3345
3439
  .filter(Boolean);
3346
3440
  }
3347
3441
 
@@ -4174,7 +4268,7 @@ function importHostedMigrationPack(pack = {}, options = {}) {
4174
4268
  host_path = excluded.host_path,
4175
4269
  git_remote = excluded.git_remote,
4176
4270
  updated_at = datetime('now')
4177
- `).run(logicalId, targetInstanceId, hostPath, String(binding.gitRemote || binding.git_remote || '').trim());
4271
+ `).run(logicalId, targetInstanceId, hostPath, normalizeGitRemote(binding.gitRemote || binding.git_remote || ''));
4178
4272
  bindingsImported += 1;
4179
4273
  }
4180
4274
  for (const task of tasks) {
@@ -4689,7 +4783,6 @@ function _openDbConnection(dbPath) {
4689
4783
  label: 'ctm-task-manager',
4690
4784
  mainWriteInventory: isMainThread,
4691
4785
  timeoutMs: _sqliteTimeoutMs('CTM_SQLITE_WRITE_LOCK_TIMEOUT_MS', 0),
4692
- staleMs: Number(process.env.CTM_SQLITE_WRITE_LOCK_STALE_MS || 10 * 60 * 1000),
4693
4786
  // Phase 3 fairness: the db-owner worker (a background thread that waits up to
4694
4787
  // 5s) polls tightly so it reliably claims the lock the instant the main thread
4695
4788
  // releases, instead of starving behind a burst of fast main-thread writes. The
@@ -7474,8 +7567,8 @@ function _defaultMigrationSteps() {
7474
7567
  // v53 (WP-2A): the approved session-model-intent table. Schema-only + a one-time row
7475
7568
  // integrity pass; it creates no other table, seeds nothing and stamps only after both hold.
7476
7569
  53: () => _migrateToV53Impl({ getDb: () => dbFor('session_model_intent'), setSchemaVersion }),
7477
- // v54 (WP-0) the per-session lead model pin — an explicit model choice replayed on resume; v55 (WP-2, multi-machine) stamps blank agent_sessions.origin_instance_id rows with this database's identity (one idempotent UPDATE, sharing this line because db.js is at its size ceiling).
7478
- 54: () => _migrateToV54Impl({ getDb: () => dbFor('session_lead_model'), setSchemaVersion }), 55: () => require('./lib/migrations/v55-agent-origin-backfill').migrateToV55({ getDb: () => dbFor('agent_sessions'), getIdentityDb: () => dbFor('instance_identity'), setSchemaVersion }),
7570
+ // v54 (WP-0) the per-session lead model pin — an explicit model choice replayed on resume; v55 (WP-2, multi-machine) stamps blank agent_sessions.origin_instance_id rows with this database's identity (one idempotent UPDATE, sharing this line because db.js is at its size ceiling); v56 (WP-1, machine pool) is host_samples + machine_self + machine_pairing_offers + machines.urls_json, all domain 'core'.
7571
+ 54: () => _migrateToV54Impl({ getDb: () => dbFor('session_lead_model'), setSchemaVersion }), 55: () => require('./lib/migrations/v55-agent-origin-backfill').migrateToV55({ getDb: () => dbFor('agent_sessions'), getIdentityDb: () => dbFor('instance_identity'), setSchemaVersion }), 56: () => require('./lib/migrations/v56-host-samples-and-machine-pool').migrateToV56({ getDb: () => dbFor('machines'), setSchemaVersion }),
7479
7572
  }, { reservedVersions: [18] });
7480
7573
  }
7481
7574
  let _migrationSteps = _defaultMigrationSteps();
@@ -7991,6 +8084,366 @@ function _ensureSessionModelPreferenceRoutingColumns(d = getDb()) {
7991
8084
  }
7992
8085
  }
7993
8086
 
8087
+ // ---------------------------------------------------------------------------
8088
+ // WP-0 (remote-machine-hub-sessions-2026-10-22.html §6.1) — the hub-side machine registry:
8089
+ // the durable list of boxes this CTM can drive sessions on (lib/machine-registry.js owns the
8090
+ // policy; the SQL lives here, in the owner zone, like every other CTM table).
8091
+ //
8092
+ // WHY THE DDL SITS HERE RATHER THAN IN _ensureHostedPrimarySchema'S EXEC BLOB: it is called
8093
+ // from there (one line), but the CREATE TABLE literal itself must stay late in the file.
8094
+ // persistence-format surfaces are ORDINAL-keyed in release/ownership-registry.json
8095
+ // (`coding-task-manager/db.js:ddl#N`, N = the statement's position in file order), so putting
8096
+ // the statement in the schema blob would renumber every later DDL surface in db.js and force
8097
+ // ~230 unrelated registry re-registrations for one new table. The contract is identical — one
8098
+ // idempotent CREATE TABLE IF NOT EXISTS, reached from the same ensure pass as sync_*.
8099
+ //
8100
+ // DOMAIN: `machines` has no entry in lib/db-domain-tables.js yet (that file is outside this
8101
+ // work package), so the handles below resolve via getDomainDb('core') — the same flip-aware
8102
+ // CORE handle dbFor('machines') returns once the classification lands.
8103
+ function _machinesDb() {
8104
+ return getDomainDb('core');
8105
+ }
8106
+
8107
+ /** Idempotent: creates the hub machine registry (called from _ensureHostedPrimarySchema). */
8108
+ function _ensureMachineRegistrySchema(target = _machinesDb()) {
8109
+ target.exec(`
8110
+ CREATE TABLE IF NOT EXISTS machines (
8111
+ instance_id TEXT PRIMARY KEY, -- pinned minted id of the box (D-3)
8112
+ display_name TEXT NOT NULL DEFAULT '', -- user label, e.g. "spark"
8113
+ url TEXT NOT NULL, -- e.g. http://127.0.0.1:3556
8114
+ walle_url TEXT NOT NULL DEFAULT '', -- '' = derive (CTM port + 1), same rule as parseSyncPeers
8115
+ enabled INTEGER NOT NULL DEFAULT 1,
8116
+ last_seen_at INTEGER NOT NULL DEFAULT 0, -- hub clock, ms
8117
+ health TEXT NOT NULL DEFAULT 'unknown', -- reachable|unknown|unreachable|unhealthy
8118
+ health_detail TEXT NOT NULL DEFAULT '',
8119
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
8120
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
8121
+ urls_json TEXT NOT NULL DEFAULT '[]' -- rev 2 P-1/P-4: the member's dialable URLs, in dial order
8122
+ );
8123
+ `);
8124
+ _invalidateTableExistsMemo();
8125
+ return target;
8126
+ }
8127
+
8128
+ const MACHINE_ROW_COLUMNS = 'instance_id, display_name, url, walle_url, enabled, last_seen_at, health, health_detail, created_at, updated_at, urls_json';
8129
+
8130
+ function _machineRow(row = {}) {
8131
+ return {
8132
+ instanceId: String(row.instance_id || ''),
8133
+ displayName: String(row.display_name || ''),
8134
+ url: String(row.url || ''),
8135
+ walleUrl: String(row.walle_url || ''),
8136
+ enabled: Number(row.enabled || 0) || 0,
8137
+ lastSeenAt: Number(row.last_seen_at || 0) || 0,
8138
+ health: String(row.health || 'unknown'),
8139
+ healthDetail: String(row.health_detail || ''),
8140
+ createdAt: String(row.created_at || ''),
8141
+ updatedAt: String(row.updated_at || ''),
8142
+ urls: _machineUrls(row.urls_json),
8143
+ };
8144
+ }
8145
+
8146
+ /** P-1/P-4: `urls_json` is a JSON array of dialable URLs; bad JSON reads as [] (never throws). */
8147
+ function _machineUrls(value) {
8148
+ try { const parsed = JSON.parse(String(value || '[]')); return Array.isArray(parsed) ? parsed.map((u) => String(u || '')).filter(Boolean) : []; }
8149
+ catch { return []; } // expected: a hand-edited/garbled column must not break every machine list
8150
+ }
8151
+
8152
+ /** Read the hub machine registry. Pre-boot/pre-migration reads return [] rather than throwing. */
8153
+ function listMachineRows() {
8154
+ try {
8155
+ const d = _machinesDb();
8156
+ return d.prepare(`SELECT ${MACHINE_ROW_COLUMNS} FROM machines ORDER BY instance_id ASC`)
8157
+ .all()
8158
+ .map(_machineRow);
8159
+ } catch {
8160
+ return []; // expected: the machines table does not exist yet (pre-boot handle)
8161
+ }
8162
+ }
8163
+
8164
+ /**
8165
+ * Insert-or-update one machine. `enabled` is honoured on conflict: registration is an explicit
8166
+ * "use this box" act, seeding (lib/machine-registry.js) never calls this for a row it did not
8167
+ * just find missing.
8168
+ *
8169
+ * WP-3 P-4: an optional `urls` (dial list) is written to `urls_json`. It is written ONLY when the
8170
+ * caller supplies a non-empty list — `registerMachine` and the probe sweep never pass one, and they
8171
+ * must not blank the dial list a roster push (replaceRosterMachines) or a pairing (POST
8172
+ * /api/machines/pair) put there. The `CASE` below is that guard: an empty `excluded` list leaves the
8173
+ * stored column alone instead of resetting it to '[]' (the column DEFAULT only applies to INSERT).
8174
+ */
8175
+ function upsertMachineRow(row = {}) {
8176
+ const instanceId = _hostedToken(row.instanceId || row.instance_id || '', '');
8177
+ const url = String(row.url || '').trim().replace(/\/+$/, '');
8178
+ if (!instanceId || !url) return 0;
8179
+ const urls = (Array.isArray(row.urls) ? row.urls : [])
8180
+ .map((entry) => String(entry || '').trim().replace(/\/+$/, ''))
8181
+ .filter(Boolean);
8182
+ const d = _machinesDb();
8183
+ _ensureMachineRegistrySchema();
8184
+ const changes = d.prepare(`
8185
+ INSERT INTO machines (instance_id, display_name, url, walle_url, enabled, updated_at, urls_json)
8186
+ VALUES (?, ?, ?, ?, ?, datetime('now'), ?)
8187
+ ON CONFLICT(instance_id) DO UPDATE SET
8188
+ display_name = excluded.display_name,
8189
+ url = excluded.url,
8190
+ walle_url = excluded.walle_url,
8191
+ enabled = excluded.enabled,
8192
+ urls_json = CASE WHEN excluded.urls_json = '[]' THEN machines.urls_json ELSE excluded.urls_json END,
8193
+ updated_at = datetime('now')
8194
+ `).run(
8195
+ instanceId,
8196
+ String(row.displayName || row.display_name || '').slice(0, 200),
8197
+ url,
8198
+ String(row.walleUrl || row.walle_url || '').replace(/\/+$/, ''),
8199
+ Number(row.enabled ?? 1) ? 1 : 0,
8200
+ JSON.stringify(urls),
8201
+ ).changes || 0;
8202
+ if (changes) flushWal();
8203
+ return changes;
8204
+ }
8205
+
8206
+ /** Probe outcome for one machine: health + detail, and last_seen_at when the box answered. */
8207
+ function recordMachineProbe(instanceId, patch = {}) {
8208
+ const id = _hostedToken(instanceId || '', '');
8209
+ if (!id) return 0;
8210
+ const health = String(patch.health || '').trim();
8211
+ if (!health) return 0;
8212
+ const d = _machinesDb();
8213
+ const lastSeenAt = Number(patch.lastSeenAt ?? patch.last_seen_at ?? 0) || 0;
8214
+ const changes = d.prepare(`
8215
+ UPDATE machines
8216
+ SET health = ?, health_detail = ?, last_seen_at = ?, updated_at = datetime('now')
8217
+ WHERE instance_id = ?
8218
+ `).run(health, String(patch.healthDetail || patch.health_detail || '').slice(0, 500), lastSeenAt, id).changes || 0;
8219
+ if (changes) flushWal();
8220
+ return changes;
8221
+ }
8222
+
8223
+ /** Unregister a machine (route: POST /api/machines/:id/remove). Nothing else keys on the row. */
8224
+ function deleteMachineRow(instanceId) {
8225
+ const id = _hostedToken(instanceId || '', '');
8226
+ if (!id) return 0;
8227
+ const d = _machinesDb();
8228
+ const changes = d.prepare('DELETE FROM machines WHERE instance_id = ?').run(id).changes || 0;
8229
+ if (changes) flushWal();
8230
+ return changes;
8231
+ }
8232
+
8233
+ /**
8234
+ * WP-2a (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.1, D-4 rev 2, §9 S-25): this
8235
+ * machine's OWN settings. Exactly one row (id = 1); a missing row reads as the defaults, so no
8236
+ * seeding step is needed. lib/machine-self.js caches this so the manifest path never hits the DB.
8237
+ */
8238
+ function getMachineSelf() {
8239
+ try {
8240
+ const d = _machinesDb();
8241
+ const row = d.prepare('SELECT display_name, favorite, hidden, updated_at FROM machine_self WHERE id = 1').get();
8242
+ return _machineSelfRow(row);
8243
+ } catch {
8244
+ return _machineSelfRow(null); // expected: machine_self does not exist yet (pre-boot/pre-migration)
8245
+ }
8246
+ }
8247
+
8248
+ function _machineSelfRow(row) {
8249
+ return {
8250
+ displayName: String((row && row.display_name) || ''),
8251
+ favorite: Number((row && row.favorite) || 0) || 0,
8252
+ hidden: Number((row && row.hidden) || 0) || 0,
8253
+ updatedAt: Number((row && row.updated_at) || 0) || 0,
8254
+ };
8255
+ }
8256
+
8257
+ /**
8258
+ * D-4 write: ONLY the given fields change (a partial patch merges over the stored row, so a
8259
+ * favorite toggle never clears the display name). `at` is the caller's clock.
8260
+ */
8261
+ function setMachineSelf({ displayName, favorite, hidden, at = 0 } = {}) {
8262
+ const d = _machinesDb();
8263
+ const current = getMachineSelf();
8264
+ const next = {
8265
+ displayName: displayName === undefined ? current.displayName : String(displayName || '').slice(0, 80),
8266
+ favorite: favorite === undefined ? current.favorite : (favorite ? 1 : 0),
8267
+ hidden: hidden === undefined ? current.hidden : (hidden ? 1 : 0),
8268
+ updatedAt: Math.trunc(Number(at) || 0) || current.updatedAt,
8269
+ };
8270
+ d.prepare(`
8271
+ INSERT INTO machine_self (id, display_name, favorite, hidden, updated_at)
8272
+ VALUES (1, ?, ?, ?, ?)
8273
+ ON CONFLICT(id) DO UPDATE SET
8274
+ display_name = excluded.display_name,
8275
+ favorite = excluded.favorite,
8276
+ hidden = excluded.hidden,
8277
+ updated_at = excluded.updated_at
8278
+ `).run(next.displayName, next.favorite, next.hidden, next.updatedAt);
8279
+ flushWal();
8280
+ return getMachineSelf();
8281
+ }
8282
+
8283
+ /**
8284
+ * P-5 receive side (§6.1): replace this member's machinery membership with the pushed set in ONE
8285
+ * transaction — upsert every listed member (url = urls[0], urls_json = urls), delete every row not
8286
+ * listed, and NEVER write this instance's own row (a member is not in its own roster). `enabled` is
8287
+ * left alone on conflict (it stays a per-primary local switch, D-4).
8288
+ */
8289
+ // `removable` = the PREVIOUS pool members: only those may be deleted when absent from `members`. A legacy
8290
+ // env-token row (P-8) is never a pool member, so a roster push/pull can never drop it.
8291
+ function replaceRosterMachines({ members = [], selfInstanceId = '', removable = [] } = {}) {
8292
+ const self = _hostedToken(selfInstanceId || '', '');
8293
+ const listed = [];
8294
+ const seen = new Set();
8295
+ for (const member of Array.isArray(members) ? members : []) {
8296
+ const instanceId = _hostedToken((member && (member.instanceId || member.instance_id)) || '', '');
8297
+ if (!instanceId || instanceId === self || seen.has(instanceId)) continue;
8298
+ const urls = (Array.isArray(member.urls) ? member.urls : []).map((u) => String(u || '').trim().replace(/\/+$/, '')).filter(Boolean);
8299
+ const url = String((member && (member.url || urls[0])) || '').trim().replace(/\/+$/, '');
8300
+ if (!url) continue;
8301
+ seen.add(instanceId);
8302
+ listed.push({ instanceId, url, urls: urls.length ? urls : [url], displayName: String((member && (member.displayName || member.display_name)) || '').slice(0, 200) });
8303
+ }
8304
+ const d = _machinesDb();
8305
+ _ensureMachineRegistrySchema();
8306
+ const txn = d.transaction(() => {
8307
+ let upserted = 0;
8308
+ for (const row of listed) {
8309
+ upserted += d.prepare(`
8310
+ INSERT INTO machines (instance_id, display_name, url, walle_url, enabled, updated_at, urls_json)
8311
+ VALUES (?, ?, ?, '', 1, datetime('now'), ?)
8312
+ ON CONFLICT(instance_id) DO UPDATE SET
8313
+ url = excluded.url,
8314
+ urls_json = excluded.urls_json,
8315
+ updated_at = datetime('now')
8316
+ `).run(row.instanceId, row.displayName, row.url, JSON.stringify(row.urls)).changes || 0;
8317
+ }
8318
+ let deleted = 0;
8319
+ const keep = new Set(listed.map((row) => row.instanceId));
8320
+ // A blank `self` means identity is not ready: delete nothing in that state.
8321
+ if (self) {
8322
+ const del = d.prepare('DELETE FROM machines WHERE instance_id = ?');
8323
+ for (const raw of Array.isArray(removable) ? removable : []) {
8324
+ const id = _hostedToken(raw || '', '');
8325
+ if (id && id !== self && !keep.has(id)) deleted += del.run(id).changes || 0;
8326
+ }
8327
+ }
8328
+ return { upserted, deleted };
8329
+ });
8330
+ const result = txn();
8331
+ flushWal();
8332
+ return result;
8333
+ }
8334
+
8335
+ /** WP-2a P-6 leave(): drop every roster row (the member is no longer in any pool). */
8336
+ function deleteAllMachineRows() {
8337
+ const d = _machinesDb();
8338
+ const changes = d.prepare('DELETE FROM machines').run().changes || 0;
8339
+ if (changes) flushWal();
8340
+ return changes;
8341
+ }
8342
+
8343
+ /**
8344
+ * WP-3 (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.3, D-6/D-9, §9 S-15/S-16): the
8345
+ * pairing-offer rows. Table `machine_pairing_offers` (id, code_hash UNIQUE, created_at, expires_at,
8346
+ * redeemed_at, redeemed_by) comes from migration v56 and is classified 'core' in
8347
+ * lib/db-domain-tables.js.
8348
+ *
8349
+ * INVARIANT: the plaintext CODE never reaches this file. The caller (lib/machine-pairing.js) hashes
8350
+ * it and passes only `codeHash`; nothing here formats, logs or returns a code.
8351
+ */
8352
+
8353
+ /** Mint one offer. Returns the number of rows written (0 for a blank id/hash). */
8354
+ function insertPairingOffer({ id, codeHash, createdAt = 0, expiresAt = 0 } = {}) {
8355
+ const offerId = String(id || '').trim();
8356
+ const hash = String(codeHash || '').trim();
8357
+ if (!offerId || !hash) return 0;
8358
+ const d = _machinesDb();
8359
+ const changes = d.prepare(`
8360
+ INSERT INTO machine_pairing_offers (id, code_hash, created_at, expires_at)
8361
+ VALUES (?, ?, ?, ?)
8362
+ `).run(offerId, hash, Math.trunc(Number(createdAt) || 0), Math.trunc(Number(expiresAt) || 0)).changes || 0;
8363
+ if (changes) flushWal();
8364
+ return changes;
8365
+ }
8366
+
8367
+ /**
8368
+ * Single-use: the `redeemed_at IS NULL` predicate makes two concurrent redemptions of the same offer
8369
+ * a 1-and-0 instead of a double-spend, so the loser is answered `pairing_code_invalid`.
8370
+ */
8371
+ function markPairingOfferRedeemed({ id, at = 0, redeemedBy = '' } = {}) {
8372
+ const offerId = String(id || '').trim();
8373
+ if (!offerId) return 0;
8374
+ const d = _machinesDb();
8375
+ const changes = d.prepare(`
8376
+ UPDATE machine_pairing_offers
8377
+ SET redeemed_at = ?, redeemed_by = ?
8378
+ WHERE id = ? AND redeemed_at IS NULL
8379
+ `).run(Math.trunc(Number(at) || 0), String(redeemedBy || '').slice(0, 200), offerId).changes || 0;
8380
+ if (changes) flushWal();
8381
+ return changes;
8382
+ }
8383
+
8384
+ /**
8385
+ * AC-3c: the 5th consecutive wrong redeem burns every LIVE offer (unspent and unexpired) in one
8386
+ * statement. `redeemed_by = 'burned'` is what makes the burn visible in the table without ever
8387
+ * naming a code — and it is a reserved sentinel, not an instance id.
8388
+ */
8389
+ function burnLivePairingOffers({ at = 0 } = {}) {
8390
+ const ts = Math.trunc(Number(at) || 0);
8391
+ const d = _machinesDb();
8392
+ const changes = d.prepare(`
8393
+ UPDATE machine_pairing_offers
8394
+ SET redeemed_at = ?, redeemed_by = 'burned'
8395
+ WHERE redeemed_at IS NULL AND expires_at > ?
8396
+ `).run(ts, ts).changes || 0;
8397
+ if (changes) flushWal();
8398
+ return changes;
8399
+ }
8400
+
8401
+ /**
8402
+ * The live set — unspent and unexpired as of `at`. Ordered oldest-first so the 3-offer ceiling is
8403
+ * deterministic. Pre-boot/pre-migration reads return [] rather than throwing (house read rule).
8404
+ */
8405
+ function listLivePairingOffers({ at = 0 } = {}) {
8406
+ try {
8407
+ const d = _machinesDb();
8408
+ return d.prepare(`
8409
+ SELECT id, code_hash, created_at, expires_at
8410
+ FROM machine_pairing_offers
8411
+ WHERE redeemed_at IS NULL AND expires_at > ?
8412
+ ORDER BY created_at ASC
8413
+ `).all(Math.trunc(Number(at) || 0)).map((row) => ({
8414
+ id: String(row.id || ''),
8415
+ codeHash: String(row.code_hash || ''),
8416
+ createdAt: Number(row.created_at) || 0,
8417
+ expiresAt: Number(row.expires_at) || 0,
8418
+ }));
8419
+ } catch {
8420
+ return []; // expected: machine_pairing_offers does not exist yet (pre-boot / pre-migration)
8421
+ }
8422
+ }
8423
+
8424
+ /** WP-1 D-11 (§6.1): one sample per resource tick; the 24 h purge rides the SAME transaction. */
8425
+ function recordHostSample({ at = 0, sampleJson = '', purgeBeforeMs = 0 } = {}) {
8426
+ const d = _machinesDb();
8427
+ const txn = d.transaction(() => {
8428
+ d.prepare('INSERT OR REPLACE INTO host_samples (at, sample_json) VALUES (?, ?)').run(Math.trunc(Number(at) || 0), String(sampleJson || ''));
8429
+ return d.prepare('DELETE FROM host_samples WHERE at < ?').run(Math.trunc(Number(purgeBeforeMs) || 0)).changes || 0;
8430
+ });
8431
+ const purged = txn() || 0;
8432
+ flushWal();
8433
+ return purged;
8434
+ }
8435
+
8436
+ /** Oldest first — the wire order in §6.1. Pre-migration reads return [] rather than throwing. */
8437
+ function listHostSamples({ sinceMs = 0, limit = 1440 } = {}) {
8438
+ try {
8439
+ const d = _machinesDb();
8440
+ return d.prepare('SELECT at, sample_json FROM host_samples WHERE at >= ? ORDER BY at ASC LIMIT ?')
8441
+ .all(Math.trunc(Number(sinceMs) || 0), Math.max(1, Math.trunc(Number(limit) || 1440)));
8442
+ } catch {
8443
+ return []; // expected: host_samples does not exist yet (pre-boot handle)
8444
+ }
8445
+ }
8446
+
7994
8447
  /**
7995
8448
  * Schema v12: durable smart-routing manager state.
7996
8449
  *
@@ -8413,6 +8866,11 @@ const SESSION_DIAGNOSTIC_CLIENT_MEMORY_SQL = `(event = 'client-memory')`;
8413
8866
  const SESSION_DIAGNOSTIC_SEAM_RESERVE = 64;
8414
8867
  const SESSION_DIAGNOSTIC_SEAM_SQL = `(event = 'client-history-seam-crossing')`;
8415
8868
 
8869
+ // reader-evidence-retention D-1/D-2: the reader incident beacons (scroll-write, chain-advance, page-fetch, parked) are the
8870
+ // evidence of a reader jump; a fifth lane, newest 10 rows PER EVENT, never a glob.
8871
+ const SESSION_DIAGNOSTIC_READER_PER_EVENT_RESERVE = 10;
8872
+ const SESSION_DIAGNOSTIC_READER_SQL = `(event IN ('client-history-reader-scroll-write', 'client-history-reader-chain-advance', 'client-history-page-fetch', 'client-history-reader-parked'))`;
8873
+
8416
8874
  function _sessionDiagnosticRetentionStatements(d) {
8417
8875
  return {
8418
8876
  // 1) Protected rows keep their OWN reserve — trimmed to the newest
@@ -8457,19 +8915,35 @@ function _sessionDiagnosticRetentionStatements(d) {
8457
8915
  LIMIT ${SESSION_DIAGNOSTIC_SEAM_RESERVE}
8458
8916
  )
8459
8917
  `),
8460
- // 4) Unprotected rows (neither protected-lifecycle, client-memory, nor seam-beacon) are
8461
- // trimmed to whatever budget remains under the TOTAL cap once the (already-trimmed)
8462
- // protected, client-memory and seam rows are accounted for — run AFTER all three trims
8463
- // above so this subquery sees their post-trim counts, not the pre-trim ones.
8918
+ // 4) Reader-beacon rows (the four incident events in SESSION_DIAGNOSTIC_READER_SQL) get their
8919
+ // OWN reserve too — a FIFTH lane, independent of the three above and of the unprotected budget
8920
+ // below. Unlike the other lanes it is partitioned PER EVENT (ROW_NUMBER() OVER (PARTITION BY
8921
+ // event)), because one incident chain fetched 11 pages: a shared lane would let a page-fetch
8922
+ // burst evict the single scroll-write row that names the cause (reader-evidence-retention D-2).
8923
+ trimReader: d.prepare(`
8924
+ DELETE FROM session_diagnostics
8925
+ WHERE id IN (
8926
+ SELECT id FROM (
8927
+ SELECT id, ROW_NUMBER() OVER (PARTITION BY event ORDER BY id DESC) AS rn
8928
+ FROM session_diagnostics
8929
+ WHERE session_id = ? AND ${SESSION_DIAGNOSTIC_READER_SQL}
8930
+ ) WHERE rn > ${SESSION_DIAGNOSTIC_READER_PER_EVENT_RESERVE}
8931
+ )
8932
+ `),
8933
+ // 5) Unprotected rows (neither protected-lifecycle, client-memory, seam-beacon, nor
8934
+ // reader-beacon) are trimmed to whatever budget remains under the TOTAL cap once the
8935
+ // (already-trimmed) protected, client-memory, seam and reader rows are accounted for — run
8936
+ // AFTER all four trims above so this subquery sees their post-trim counts, not the pre-trim ones.
8464
8937
  trimUnprotected: d.prepare(`
8465
8938
  DELETE FROM session_diagnostics
8466
8939
  WHERE session_id = ?
8467
8940
  AND NOT ${SESSION_DIAGNOSTIC_PROTECTED_SQL}
8468
8941
  AND NOT ${SESSION_DIAGNOSTIC_CLIENT_MEMORY_SQL}
8469
8942
  AND NOT ${SESSION_DIAGNOSTIC_SEAM_SQL}
8943
+ AND NOT ${SESSION_DIAGNOSTIC_READER_SQL}
8470
8944
  AND id NOT IN (
8471
8945
  SELECT id FROM session_diagnostics
8472
- WHERE session_id = ? AND NOT ${SESSION_DIAGNOSTIC_PROTECTED_SQL} AND NOT ${SESSION_DIAGNOSTIC_CLIENT_MEMORY_SQL} AND NOT ${SESSION_DIAGNOSTIC_SEAM_SQL}
8946
+ WHERE session_id = ? AND NOT ${SESSION_DIAGNOSTIC_PROTECTED_SQL} AND NOT ${SESSION_DIAGNOSTIC_CLIENT_MEMORY_SQL} AND NOT ${SESSION_DIAGNOSTIC_SEAM_SQL} AND NOT ${SESSION_DIAGNOSTIC_READER_SQL}
8473
8947
  ORDER BY id DESC
8474
8948
  LIMIT (${SESSION_DIAGNOSTIC_TOTAL_CAP} - (
8475
8949
  SELECT COUNT(*) FROM session_diagnostics WHERE session_id = ? AND ${SESSION_DIAGNOSTIC_PROTECTED_SQL}
@@ -8477,6 +8951,8 @@ function _sessionDiagnosticRetentionStatements(d) {
8477
8951
  SELECT COUNT(*) FROM session_diagnostics WHERE session_id = ? AND ${SESSION_DIAGNOSTIC_CLIENT_MEMORY_SQL}
8478
8952
  ) - (
8479
8953
  SELECT COUNT(*) FROM session_diagnostics WHERE session_id = ? AND ${SESSION_DIAGNOSTIC_SEAM_SQL}
8954
+ ) - (
8955
+ SELECT COUNT(*) FROM session_diagnostics WHERE session_id = ? AND ${SESSION_DIAGNOSTIC_READER_SQL}
8480
8956
  ))
8481
8957
  )
8482
8958
  `),
@@ -8487,7 +8963,8 @@ function _sweepSessionDiagnosticsRetention(d, statements, sessionId) {
8487
8963
  statements.trimProtected.run(sessionId, sessionId);
8488
8964
  statements.trimClientMemory.run(sessionId, sessionId);
8489
8965
  statements.trimSeam.run(sessionId, sessionId);
8490
- statements.trimUnprotected.run(sessionId, sessionId, sessionId, sessionId, sessionId);
8966
+ statements.trimReader.run(sessionId);
8967
+ statements.trimUnprotected.run(sessionId, sessionId, sessionId, sessionId, sessionId, sessionId);
8491
8968
  }
8492
8969
 
8493
8970
  /*
@@ -8557,6 +9034,25 @@ function flushSessionMetadataUpdates(entries) {
8557
9034
  return { updated: prepared.length };
8558
9035
  }
8559
9036
 
9037
+ // WP-2 (worker-transcript-link-derived-not-discovered-2026-10-25.html §5 D-4/D-7, §8 WP-2): the
9038
+ // ONE writer for the "link a provider transcript to an agent_session" repair. A resume preflight
9039
+ // that DERIVED the transcript path (lib/transcript-link.js verified the file really is that
9040
+ // session's) calls this so the next resume finds the link instead of deriving it again.
9041
+ //
9042
+ // The WHERE clause is the safety property: only a row whose jsonl_path is empty (or NULL) is
9043
+ // touched, so a derived guess can never REPLACE a link someone else recorded -- `updated: 0`
9044
+ // means "nothing was written", which is the correct answer for a row that is already linked.
9045
+ function updateAgentSessionTranscriptLink(agentSessionId, jsonlPath) {
9046
+ const id = String(agentSessionId || '').trim();
9047
+ const p = String(jsonlPath || '').trim();
9048
+ if (!id || !p) return { updated: 0 };
9049
+ // agent_sessions is CORE -- flip-aware handle, matching every other writer of jsonl_path.
9050
+ const result = dbFor('agent_sessions').prepare(
9051
+ "UPDATE agent_sessions SET jsonl_path = COALESCE(NULLIF(?, ''), jsonl_path), updated_at = datetime('now') WHERE agent_session_id = ? AND COALESCE(jsonl_path, '') = ''",
9052
+ ).run(p, id);
9053
+ return { updated: result.changes };
9054
+ }
9055
+
8560
9056
  function listSessionDiagnostics(sessionId, limit = 120) {
8561
9057
  const cleanSessionId = String(sessionId || '').trim();
8562
9058
  if (!cleanSessionId) return [];
@@ -15141,6 +15637,24 @@ function getSmartRoutingSummaryRefreshInput(ctmSessionId, { limit = 24 } = {}) {
15141
15637
  }
15142
15638
 
15143
15639
  // --- Startup Tasks (crash-safe session restore) ---
15640
+ //
15641
+ // S-28 (remote-machine-hub-sessions-2026-10-22.html §9): THREE different things in this repo
15642
+ // are called an "instance id". They are not interchangeable, and only ONE of them is identity:
15643
+ //
15644
+ // 1. THE MINTED IDENTITY ROW — instance_identity.instance_id (db.js getOrCreateInstanceIdentity,
15645
+ // lib/instance-identity-*.js). A random, durable, per-DATABASE id minted once at boot,
15646
+ // transferable on an ownership handoff. THE HUB KEYS ON THIS ONE (D-3: `machines.instance_id`
15647
+ // is exactly this value, pinned from the box's GET /api/sync/manifest) and it is what
15648
+ // workspace/ownership lineage and the sync layer compare against. Reminting it is a visible
15649
+ // event with a restamp path (restampOwnerLineage) — never a side effect.
15650
+ // 2. THE LIVENESS STAMP BELOW — `${hostname}:${pid}:${Date.now()}`, deliberately NOT durable and
15651
+ // NOT unique per database: it is regenerated on every process start and exists to answer
15652
+ // "is the process that wrote this startup_tasks row still the one running?" inside one boot.
15653
+ // 3. CTM_EXECUTION_HOST — the env var a remote/forwarded launch may name for the PTY child
15654
+ // (server.js's create path), i.e. "where the work ran", a display/attribution field.
15655
+ //
15656
+ // The hub (lib/machine-registry.js) keys ONLY on (1). (2) must never be registered as a machine
15657
+ // — it changes on every restart; (3) is not an identity at all.
15144
15658
  const CTM_INSTANCE_ID = `${require('os').hostname()}:${process.pid}:${Date.now()}`;
15145
15659
 
15146
15660
  function _insertStartupTaskRow(d, {
@@ -15157,7 +15671,18 @@ function _insertStartupTaskRow(d, {
15157
15671
  if (cols.has('parked_at')) { names.push('parked_at'); values.push(''); }
15158
15672
  if (cols.has('park_reason')) { names.push('park_reason'); values.push(''); }
15159
15673
  const hostedInstanceId = (cols.has('origin_instance_id') || cols.has('current_owner_instance_id')) ? _currentHostedInstanceId() : '';
15160
- if (cols.has('origin_instance_id')) { names.push('origin_instance_id'); values.push(hostedInstanceId); }
15674
+ if (cols.has('origin_instance_id')) {
15675
+ // S-6 (WP-2, docs/remote-machine-hub-sessions-2026-10-22.html §9): origin_instance_id is
15676
+ // WRITE-ONCE. INSERT OR REPLACE deletes and reinserts the row, so a RE-CREATE of an existing
15677
+ // ctm_session_id (crash recovery, restore of an exited session) used to re-stamp origin with
15678
+ // whatever instance is running now — destroying the only record of where the session was
15679
+ // first created (the hub's D-3/D-7 machine routing keys on it). Preserve the recorded origin;
15680
+ // fill from this instance only when the column is blank/missing (the same fill-if-blank shape
15681
+ // the sync/ownership writers use, e.g. the COALESCE(NULLIF(origin_instance_id, ''), ?) seed).
15682
+ const priorOrigin = _startupTaskRawOnHandle(d, sessionId);
15683
+ names.push('origin_instance_id');
15684
+ values.push(_hostedToken(priorOrigin && priorOrigin.origin_instance_id, '') || hostedInstanceId);
15685
+ }
15161
15686
  if (cols.has('current_owner_instance_id')) { names.push('current_owner_instance_id'); values.push(hostedInstanceId); }
15162
15687
  const placeholders = names.map(() => '?').join(', ');
15163
15688
  d.prepare(`INSERT OR REPLACE INTO startup_tasks (${names.join(', ')}) VALUES (${placeholders})`).run(...values);
@@ -15245,6 +15770,7 @@ function updateStartupTaskAgentSession(sessionId, agentSessionId, agentProjectDi
15245
15770
  vals.push(sessionId);
15246
15771
  let changes = 0;
15247
15772
  d.transaction(() => {
15773
+ if (cols.has('args') && resumesCandidateFork(d, sessionId, resumeArgs)) return; // only the transition commit moves it
15248
15774
  changes = d.prepare(`UPDATE startup_tasks SET ${sets.join(', ')} WHERE ctm_session_id = ?`).run(...vals).changes || 0;
15249
15775
  if (changes) _maybeEnqueueStartupTaskOutbox(d, sessionId, 'upsert');
15250
15776
  })();
@@ -15521,6 +16047,19 @@ function listStartupTasks(opts = {}) {
15521
16047
  return rows.filter((row) => _startupTaskRestorable(row, opts)).map(_startupTaskRow);
15522
16048
  }
15523
16049
 
16050
+ // Ingest-fence D-1 clause (d) (docs/remote-hub-ownership-fences-and-restore-skip-diagnostics-2026-10-23.html):
16051
+ // the durable-ledger read. ctm_sessions is the one ledger that still holds an ENDED session —
16052
+ // listStartupTasks() above drops exited rows — so "does this hub's ledger know the id, and who owns
16053
+ // it" is answered here. Deliberately throw-through: the catalog's per-clause guard counts the fault
16054
+ // and fails OPEN, which a silent empty answer here would hide.
16055
+ function getCtmSessionOwnerRow(ctmSessionId) {
16056
+ const id = String(ctmSessionId || '').trim();
16057
+ if (!id) return { exists: false, currentOwner: '' };
16058
+ const row = getDomainDb('core').prepare('SELECT current_owner_instance_id FROM ctm_sessions WHERE id = ?').get(id);
16059
+ if (!row) return { exists: false, currentOwner: '' };
16060
+ return { exists: true, currentOwner: String(row.current_owner_instance_id || '').trim() };
16061
+ }
16062
+
15524
16063
  // WP-2 restore-side backstop (docs/ctm-session-restore-loss-and-render-wedge-2026-07-26.html):
15525
16064
  // listStartupTasks() above is restore's SOLE input — a ctm_sessions row that has no
15526
16065
  // startup_tasks row (whatever the cause: RC-1's domain-routing bug, a write that exhausted its
@@ -18771,196 +19310,10 @@ function _normalizeSessionTitle(value, maxLen = 120) {
18771
19310
  return String(value || '').replace(/\s+/g, ' ').trim().slice(0, maxLen);
18772
19311
  }
18773
19312
 
18774
- function _branchTitleCandidates(branch) {
18775
- const full = String(branch || '').trim().replace(/^refs\/heads\//, '');
18776
- if (!full) return [];
18777
- const parts = full.split('/').filter(Boolean);
18778
- const leaf = parts.length ? parts[parts.length - 1] : full;
18779
- const namespace = parts.length > 1 ? parts.slice(0, -1).join('/') : '';
18780
- const displayNamespace = namespace.replace(/^ctm\//, '');
18781
- return Array.from(new Set([
18782
- full,
18783
- leaf,
18784
- namespace && leaf ? `${namespace}/${leaf}` : '',
18785
- displayNamespace && leaf ? `${displayNamespace}/${leaf}` : '',
18786
- ].filter(Boolean).map(_normalizeSessionTitle)));
18787
- }
18788
-
18789
- function _looksLikeWorktreeCwdTitle(title, cwd) {
18790
- const t = _normalizeSessionTitle(title);
18791
- const dir = String(cwd || '').trim();
18792
- if (!t || !dir) return false;
18793
- const base = _normalizeSessionTitle(path.basename(dir));
18794
- if (!base || t.toLowerCase() !== base.toLowerCase()) return false;
18795
- return /(?:^|[/\\])(?:\.claude[/\\])?worktrees[/\\][^/\\]+$/i.test(dir);
18796
- }
18797
-
18798
- function isUserSessionTitleCandidate(title, context = {}) {
18799
- const t = _normalizeSessionTitle(title);
18800
- if (!t) return false;
18801
- if (/^Resume:\s*/i.test(t)) return false;
18802
- if (/^Wall-?E session$/i.test(t)) return false;
18803
- if (/^(Claude Code|Codex|Gemini|Aider):\s*(~\/|\/|[A-Za-z]:[\\/])/i.test(t)) return false;
18804
- if (/^Shell:\s*/i.test(t)) return false;
18805
- if (/^(~\/|\/|[A-Za-z]:[\\/])/.test(t)) return false;
18806
- const cwd = _normalizeSessionTitle(context.cwd || '');
18807
- if (cwd && (t === cwd || t === cwd.replace(process.env.HOME || '', '~'))) return false;
18808
- if (_looksLikeWorktreeCwdTitle(t, context.cwd)) return false;
18809
- if (_branchTitleCandidates(context.branch).some(candidate => candidate.toLowerCase() === t.toLowerCase())) return false;
18810
- const cmd = _normalizeSessionTitle(context.cmd || '');
18811
- if (cmd && t === cmd) return false;
18812
- return true;
18813
- }
18814
-
18815
- function promoteStartupTaskTitleIfUserNamed(sessionId, { dryRun = false, reason = 'startup_task_title' } = {}) {
18816
- const cleanId = String(sessionId || '').trim();
18817
- if (!cleanId || !_tableExists('startup_tasks') || !_tableExists('ctm_sessions')) {
18818
- return { promoted: false, reason: 'missing_table_or_id' };
18819
- }
18820
- const ctmId = _resolveCtmSessionIdForWrite(cleanId);
18821
- const row = getDomainDb('core').prepare(`
18822
- SELECT
18823
- c.id,
18824
- c.title,
18825
- COALESCE(c.user_renamed, 0) AS user_renamed,
18826
- s.label,
18827
- s.cmd,
18828
- s.cwd,
18829
- s.branch,
18830
- s.type
18831
- FROM ctm_sessions c
18832
- JOIN startup_tasks s ON s.ctm_session_id = c.id
18833
- WHERE c.id = ?
18834
- LIMIT 1
18835
- `).get(ctmId);
18836
- if (!row) return { promoted: false, reason: 'no_startup_task' };
18837
- if (row.user_renamed) return { promoted: false, protected: true, title: row.title || '', reason: 'already_user_title' };
18838
-
18839
- const title = _normalizeSessionTitle(row.label);
18840
- if (!isUserSessionTitleCandidate(title, row)) {
18841
- return { promoted: false, reason: 'runtime_generated_title', title };
18842
- }
18843
-
18844
- if (dryRun) {
18845
- return {
18846
- promoted: row.title !== title || !row.user_renamed,
18847
- title,
18848
- previousTitle: row.title || '',
18849
- reason,
18850
- };
18851
- }
18852
-
18853
- const hasTitleStatus = _tableHasColumn('ctm_sessions', 'title_status');
18854
- const now = new Date().toISOString();
18855
- const result = hasTitleStatus
18856
- ? getDomainDb('core').prepare(`
18857
- UPDATE ctm_sessions SET
18858
- title = ?,
18859
- title_source = 'user',
18860
- title_status = 'user_named',
18861
- title_status_reason = ?,
18862
- title_updated_at = ?,
18863
- user_renamed = 1,
18864
- updated_at = datetime('now')
18865
- WHERE id = ? AND COALESCE(user_renamed, 0) = 0
18866
- `).run(title, reason, now, ctmId)
18867
- : getDomainDb('core').prepare(`
18868
- UPDATE ctm_sessions SET
18869
- title = ?,
18870
- user_renamed = 1,
18871
- updated_at = datetime('now')
18872
- WHERE id = ? AND COALESCE(user_renamed, 0) = 0
18873
- `).run(title, ctmId);
18874
- if (result.changes) flushWal();
18875
- return {
18876
- promoted: result.changes > 0,
18877
- protected: true,
18878
- title,
18879
- previousTitle: row.title || '',
18880
- reason,
18881
- };
18882
- }
18883
-
18884
- function repairSessionTitlesFromStartupTasks({ dryRun = false, limit = 1000 } = {}) {
18885
- if (!_tableExists('startup_tasks') || !_tableExists('ctm_sessions')) {
18886
- return { scanned: 0, promoted: 0, skipped: 0, items: [] };
18887
- }
18888
- const rows = getDomainDb('core').prepare(`
18889
- SELECT
18890
- c.id,
18891
- c.title,
18892
- COALESCE(c.user_renamed, 0) AS user_renamed,
18893
- s.label,
18894
- s.cmd,
18895
- s.cwd,
18896
- s.branch,
18897
- s.type
18898
- FROM startup_tasks s
18899
- JOIN ctm_sessions c ON c.id = s.ctm_session_id
18900
- WHERE COALESCE(c.user_renamed, 0) = 0
18901
- AND COALESCE(s.label, '') != ''
18902
- ORDER BY s.created_at DESC
18903
- LIMIT ?
18904
- `).all(Math.max(1, Number(limit) || 1000));
18905
- const items = [];
18906
- for (const row of rows) {
18907
- const title = _normalizeSessionTitle(row.label);
18908
- if (!isUserSessionTitleCandidate(title, row)) continue;
18909
- items.push({
18910
- sessionId: row.id,
18911
- title,
18912
- previousTitle: row.title || '',
18913
- });
18914
- }
18915
- if (!dryRun && items.length) {
18916
- const hasTitleStatus = _tableHasColumn('ctm_sessions', 'title_status');
18917
- const now = new Date().toISOString();
18918
- const update = hasTitleStatus
18919
- ? getDomainDb('core').prepare(`
18920
- UPDATE ctm_sessions SET
18921
- title = ?,
18922
- title_source = 'user',
18923
- title_status = 'user_named',
18924
- title_status_reason = 'startup_task_title_repair',
18925
- title_updated_at = ?,
18926
- user_renamed = 1,
18927
- updated_at = datetime('now')
18928
- WHERE id = ? AND COALESCE(user_renamed, 0) = 0
18929
- `)
18930
- : getDomainDb('core').prepare(`
18931
- UPDATE ctm_sessions SET
18932
- title = ?,
18933
- user_renamed = 1,
18934
- updated_at = datetime('now')
18935
- WHERE id = ? AND COALESCE(user_renamed, 0) = 0
18936
- `);
18937
- const txn = getDomainDb('core').transaction(() => {
18938
- for (const item of items) {
18939
- if (hasTitleStatus) update.run(item.title, now, item.sessionId);
18940
- else update.run(item.title, item.sessionId);
18941
- }
18942
- });
18943
- txn();
18944
- flushWal();
18945
- }
18946
- return {
18947
- scanned: rows.length,
18948
- promoted: items.length,
18949
- skipped: rows.length - items.length,
18950
- items,
18951
- };
18952
- }
18953
-
18954
19313
  function setSessionTitleNew(id, title, userRenamed = false, meta = {}) {
18955
19314
  const ctmId = _resolveCtmSessionIdForWrite(id);
18956
19315
  const cleanTitle = _normalizeSessionTitle(title);
18957
19316
  if (!cleanTitle) return;
18958
- if (!userRenamed) {
18959
- const protectedTitle = promoteStartupTaskTitleIfUserNamed(ctmId, {
18960
- reason: meta.source === 'ai_title' ? 'protected_from_ai_title' : 'protected_from_auto_title',
18961
- });
18962
- if (protectedTitle && protectedTitle.protected) return;
18963
- }
18964
19317
  const hasTitleStatus = _tableHasColumn('ctm_sessions', 'title_status');
18965
19318
  const source = userRenamed ? 'user' : (meta.source || '');
18966
19319
  const status = userRenamed ? 'user_renamed' : (meta.status || '');
@@ -19051,12 +19404,16 @@ function setSessionTitleStatusNew(id, status, reason = '', source = 'ai_title')
19051
19404
 
19052
19405
  // WP-1 (session-rename-mirror §6b/§6c): outcome of mirroring a CTM title to its provider.
19053
19406
  // UPDATE only, no stub rows (a mirror attempt is meaningless for a row that doesn't exist).
19407
+ // WP-4/D-12 (S-24): the REASON is persisted with the outcome — title_mirror_error is
19408
+ // `reason + (error ? ': ' + error : '')`, so an `unsupported` outcome can no longer be stored with
19409
+ // an empty error and no recorded cause.
19054
19410
  const TITLE_MIRROR_STATES = new Set(['mirrored', 'unsupported', 'failed']);
19055
- function setSessionTitleMirror(ctmSessionId, { state, error = '', value = '', at = new Date().toISOString() } = {}) {
19411
+ function setSessionTitleMirror(ctmSessionId, { state, reason = '', error = '', value = '', at = new Date().toISOString() } = {}) {
19056
19412
  if (!TITLE_MIRROR_STATES.has(state)) throw new TypeError(`setSessionTitleMirror: invalid state ${JSON.stringify(state)}`);
19057
19413
  const ctmId = _resolveCtmSessionIdForWrite(ctmSessionId);
19414
+ const mirrorError = reason ? (error ? `${reason}: ${error}` : reason) : (error || '');
19058
19415
  const result = getDomainDb('core').prepare('UPDATE ctm_sessions SET title_mirror_state=?, title_mirror_error=?, title_mirror_value=?, title_mirror_at=? WHERE id=?')
19059
- .run(state, error || '', value || '', at, ctmId);
19416
+ .run(state, mirrorError, value || '', at, ctmId);
19060
19417
  if (result.changes > 0) flushWal();
19061
19418
  }
19062
19419
  function getSessionTitleMirror(ctmSessionId) {
@@ -19160,12 +19517,7 @@ function getSessionDisplayTitleInfo(id) {
19160
19517
  COALESCE(a.rename_name, '') as rename_name,
19161
19518
  COALESCE(a.first_message, '') as first_message,
19162
19519
  COALESCE(a.last_user_content, '') as last_user_content,
19163
- COALESCE(a.first_assistant_text, '') as first_assistant_text,
19164
- st.label AS startup_label,
19165
- st.cmd AS startup_cmd,
19166
- st.cwd AS startup_cwd,
19167
- st.branch AS startup_branch,
19168
- st.type AS startup_type
19520
+ COALESCE(a.first_assistant_text, '') as first_assistant_text
19169
19521
  FROM ctm_sessions c
19170
19522
  LEFT JOIN (
19171
19523
  SELECT *, ROW_NUMBER() OVER (
@@ -19177,8 +19529,6 @@ function getSessionDisplayTitleInfo(id) {
19177
19529
  FROM agent_sessions
19178
19530
  WHERE ctm_session_id = ?
19179
19531
  ) a ON a.ctm_session_id = c.id AND a.rn = 1
19180
- LEFT JOIN startup_tasks st
19181
- ON st.ctm_session_id = c.id
19182
19532
  WHERE c.id = ?
19183
19533
  LIMIT 1
19184
19534
  `).get(ctmId, ctmId);
@@ -19189,27 +19539,19 @@ function getSessionDisplayTitleInfo(id) {
19189
19539
  if (agent && agent.ctm_session_id) row = rowForCtmId(agent.ctm_session_id);
19190
19540
  }
19191
19541
  if (!row) return null;
19192
- const startupTitle = !row.user_renamed && isUserSessionTitleCandidate(row.startup_label, {
19193
- cmd: row.startup_cmd,
19194
- cwd: row.startup_cwd,
19195
- branch: row.startup_branch,
19196
- type: row.startup_type,
19197
- }) ? _normalizeSessionTitle(row.startup_label) : '';
19198
- const effectiveRow = startupTitle ? {
19199
- ...row,
19200
- title: startupTitle,
19201
- user_renamed: 1,
19202
- } : row;
19203
- const displayTitle = pickDisplayTitle(effectiveRow);
19542
+ // The row as stored. Title provenance (userRenamed/titleSource/…) is read from
19543
+ // ctm_sessions only — a user-looking startup_tasks.label is never inferred here
19544
+ // (design 2026-10-23 D-3: store provenance with the value, never infer it).
19545
+ const displayTitle = pickDisplayTitle(row);
19204
19546
  return {
19205
19547
  ctmSessionId: row.id,
19206
- title: effectiveRow.title || '',
19548
+ title: row.title || '',
19207
19549
  aiTitle: row.user_renamed ? '' : (row.title || ''),
19208
19550
  displayTitle: displayTitle || '',
19209
- userRenamed: !!effectiveRow.user_renamed,
19210
- titleSource: startupTitle ? 'user' : (row.title_source || ''),
19211
- titleStatus: startupTitle ? 'user_named' : (row.title_status || ''),
19212
- titleStatusReason: startupTitle ? 'startup_task_title' : (row.title_status_reason || ''),
19551
+ userRenamed: !!row.user_renamed,
19552
+ titleSource: row.title_source || '',
19553
+ titleStatus: row.title_status || '',
19554
+ titleStatusReason: row.title_status_reason || '',
19213
19555
  titleUpdatedAt: row.title_updated_at || '',
19214
19556
  renameName: row.rename_name || '',
19215
19557
  };
@@ -19344,6 +19686,7 @@ function repairCodexSessionMetadataFromConversations({ dryRun = false, limit = 5
19344
19686
  title_updated_at = ?,
19345
19687
  updated_at = datetime('now')
19346
19688
  WHERE id = ? AND COALESCE(user_renamed, 0) = 0
19689
+ AND COALESCE(title_source, '') <> 'provider_title'
19347
19690
  `)
19348
19691
  : d.prepare(`
19349
19692
  UPDATE ctm_sessions SET
@@ -19867,6 +20210,39 @@ function getDirectShadowStats() {
19867
20210
  return null;
19868
20211
  }
19869
20212
 
20213
+ // WP-6 (design doc session-name-agent-title-precedence §6, §8 WP-6): the ONE-SHOT agent-title
20214
+ // backfill's write half. The read pool did the transcript I/O (workers/read-pool-worker.js's
20215
+ // 'backfillAgentTitles'); this is the owner-side apply, so it runs on the DB-owner worker's core
20216
+ // handle and is reached from the main thread through DB_OWNER_MUTATION_ACTIONS like every other
20217
+ // mutation. Every row goes through lib/agent-title-apply.js — the SINGLE tier-2 apply — so the
20218
+ // backfill obeys exactly the ingest rules: tier 1 is never rewritten, a custom-title equal to
20219
+ // title_mirror_value is CTM's own echo (the ai-title wins), and a uuid-like name is not a name.
20220
+ // A row whose apply lands also moves startup_tasks.label (D-13: the restore label and the durable
20221
+ // title must never disagree).
20222
+ /**
20223
+ * @param {Array<{ id: string, customTitle?: string, aiTitle?: string }>} rows one read-pool page
20224
+ * @returns {{ scanned: number, applied: number, reasons: Record<string, number> }}
20225
+ */
20226
+ function applyAgentTitleBackfillRows(rows = []) {
20227
+ const coreDb = getDomainDb('core');
20228
+ const result = { scanned: 0, applied: 0, reasons: {} };
20229
+ for (const row of (Array.isArray(rows) ? rows : [])) {
20230
+ const id = String(row?.id || '').trim();
20231
+ if (!id) continue;
20232
+ result.scanned += 1;
20233
+ const outcome = applyAgentTitleSignals(coreDb, {
20234
+ ctmSessionId: id, customTitle: row?.customTitle, aiTitle: row?.aiTitle,
20235
+ });
20236
+ const reason = String(outcome.reason || 'unknown');
20237
+ result.reasons[reason] = (result.reasons[reason] || 0) + 1;
20238
+ if (outcome.applied === 'agent' && outcome.title) {
20239
+ result.applied += 1;
20240
+ try { updateStartupTaskLabel(id, outcome.title); } catch (error) { console.error('[agent-title-backfill] label sync failed:', error.message); }
20241
+ }
20242
+ }
20243
+ return result;
20244
+ }
20245
+
19870
20246
  // Explicit mutation surface allowed to cross from the main process to the DB-owner
19871
20247
  // worker. Keep this as a closed map of local function references: the worker never
19872
20248
  // accepts an arbitrary exported method name, SQL string, or callback from the caller.
@@ -19967,6 +20343,7 @@ const DB_OWNER_MUTATION_ACTIONS = Object.freeze({
19967
20343
  setSessionTitle,
19968
20344
  setSessionTitleNew,
19969
20345
  setSessionTitleStatusNew,
20346
+ applyAgentTitleBackfillRows,
19970
20347
  createTemplate,
19971
20348
  deleteTemplate,
19972
20349
  trackPromptUsage,
@@ -20024,6 +20401,37 @@ const DB_OWNER_MUTATION_ACTIONS = Object.freeze({
20024
20401
  transferLease,
20025
20402
  transferSessionOwnership,
20026
20403
  upsertHostedWorkspaceBinding,
20404
+ // WP-2 (docs/remote-machine-hub-sessions-2026-10-22.html §6.1/§8, deferred from WP-0): the
20405
+ // machine-registry writes are reachable as named db.js exports so they route through the DB-owner
20406
+ // mutation Proxy — the same shape as the Review Hub registry writes above. Without these three
20407
+ // entries lib/machine-registry.js's dispatchOwnerMutation() calls landed on the MAIN thread
20408
+ // (db-owner-client's Proxy only crosses for a member of this map), which is exactly the
20409
+ // serial-writer contention the owner worker exists to prevent. Their OP_META rows
20410
+ // (shared/storage/contract.js) tag them domain:'core' on `machines`.
20411
+ upsertMachineRow,
20412
+ recordMachineProbe,
20413
+ deleteMachineRow,
20414
+ // WP-1 (docs/machine-pool-picker-pairing-dashboard-2026-10-24.html §6.1, D-11): the 60 s host
20415
+ // sample write. It joins this map for the same reason the three rows above did — the tick must
20416
+ // not put an insert+purge on the main thread — and its OP_META row tags domain:'core' on
20417
+ // `host_samples` (lib/db-domains.js CTM_WRITE_OPS carries the same name).
20418
+ recordHostSample,
20419
+ // WP-2a (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.1, D-4/P-5/P-6): the
20420
+ // machine-owned settings write, the P-5 roster replacement, and the leave() sweep. All three are
20421
+ // single-domain 'core' mutations over `machines`/`machine_self`, so they join this map for the
20422
+ // same main-thread reason as the rows above; lib/db-domains.js CTM_WRITE_OPS and
20423
+ // shared/storage/contract.js OP_META carry the matching names.
20424
+ setMachineSelf,
20425
+ replaceRosterMachines,
20426
+ deleteAllMachineRows,
20427
+ // WP-3 (machine-pool-picker-pairing-dashboard-2026-10-24.html §6.3, D-6/D-9): the three
20428
+ // pairing-offer writes. They join this map for the same main-thread reason as the rows above —
20429
+ // `machine_pairing_offers` is domain 'core' (lib/db-domain-tables.js), and each op's OP_META row in
20430
+ // shared/storage/contract.js carries the matching kind:'write' + tables:['machine_pairing_offers'].
20431
+ // `listLivePairingOffers` is the bounded read beside them and is deliberately NOT here.
20432
+ insertPairingOffer,
20433
+ markPairingOfferRedeemed,
20434
+ burnLivePairingOffers,
20027
20435
  seedHostedWorkspaceBindings,
20028
20436
  updateStartupTaskWorkspaceMapping,
20029
20437
  repointStartupTaskWorkspace,
@@ -20088,8 +20496,6 @@ const DB_OWNER_MUTATION_ACTIONS = Object.freeze({
20088
20496
  upsertAgentSessionIdentity,
20089
20497
  recordResumeLineage,
20090
20498
  setSessionStar,
20091
- promoteStartupTaskTitleIfUserNamed,
20092
- repairSessionTitlesFromStartupTasks,
20093
20499
  repairCodexSessionMetadataFromConversations,
20094
20500
  repairCodexRolloutAgentIdentities,
20095
20501
  detachPromotedProviderChildSessions,
@@ -20128,6 +20534,7 @@ module.exports = {
20128
20534
  SESSION_DIAGNOSTIC_PROTECTED_RESERVE,
20129
20535
  SESSION_DIAGNOSTIC_CLIENT_MEMORY_RESERVE,
20130
20536
  SESSION_DIAGNOSTIC_SEAM_RESERVE,
20537
+ SESSION_DIAGNOSTIC_READER_PER_EVENT_RESERVE,
20131
20538
  MigrationFailedError,
20132
20539
  migrateSchemaIfNeeded,
20133
20540
  // WP-6 (docs/approval-delivery-truth-host-skew-2026-09-01.html §5 D-9, §8
@@ -20243,7 +20650,7 @@ module.exports = {
20243
20650
  deleteSessionLeadModel: sessionLeadModelStore.remove,
20244
20651
  getSessionLeadRoute: sessionLeadRouteStore.get, setSessionLeadRoute: sessionLeadRouteStore.set, deleteSessionLeadRoute: sessionLeadRouteStore.remove,
20245
20652
  getCodingModelTransition: leadModelTransitionStore.get,
20246
- getActiveCodingModelTransitionForSession: leadModelTransitionStore.getActiveForSession,
20653
+ getActiveCodingModelTransitionForSession: leadModelTransitionStore.getActiveForSession, getCodingModelTransitionsForThread: leadModelTransitionStore.listForThread,
20247
20654
  recordLeadRouteLaunchOwnership,
20248
20655
  getCodingModelTransitionStagedToken: leadModelTransitionStore.getStagedToken,
20249
20656
  // WP-2A: the bounded owner-side approved-intent read. Synchronous BY DESIGN — it is the DB-owner
@@ -20273,10 +20680,22 @@ module.exports = {
20273
20680
  updateStartupTaskWorkspaceMapping, repointStartupTaskWorkspace,
20274
20681
  _syncFeatureEnabled,
20275
20682
  enqueueSyncOutbox, listSyncOutbox, markSyncOutboxDeliveredThrough, recordSyncOutboxServed, getSyncOutboxDeliveredSeq, reconcileSyncOutbox, pruneSyncOutbox,
20683
+ // WP-0 hub machine registry (lib/machine-registry.js): read + the three writes it takes so
20684
+ // far (register/enable, probe outcome, unregister). Handles resolve via getDomainDb('core').
20685
+ listMachineRows, upsertMachineRow, recordMachineProbe, deleteMachineRow, _ensureMachineRegistrySchema,
20686
+ // WP-1 D-11: the host-sample pair. `recordHostSample` is a DB_OWNER_MUTATION_ACTIONS entry
20687
+ // (above); `listHostSamples` is the bounded read beside it, both on the domain-'core' handle.
20688
+ recordHostSample, listHostSamples,
20689
+ // WP-2a D-4/P-5/P-6: the machine-owned settings read/write, the P-5 roster replacement and the
20690
+ // leave() sweep — all on the domain-'core' handle beside the registry pair above.
20691
+ getMachineSelf, setMachineSelf, replaceRosterMachines, deleteAllMachineRows,
20692
+ // WP-3 §6.3: the pairing-offer trio (DB_OWNER_MUTATION_ACTIONS entries above) plus its read —
20693
+ // all on the domain-'core' handle beside the machine rows.
20694
+ insertPairingOffer, markPairingOfferRedeemed, burnLivePairingOffers, listLivePairingOffers,
20276
20695
  createSyncResyncSnapshot, createSyncLocalSnapshot, applySyncSnapshot, applySyncEntries, validateSyncEntries, applySyncPurgeCommand,
20277
20696
  getSessionContentGeneration, getSyncHighWaterMark, getOpenSyncGapConflict, recordSyncPeerConflict, offerStoppedSessionHandoff,
20278
20697
  createHostedMigrationPack, importHostedMigrationPack,
20279
- addStartupTask, updateStartupTaskLabel, updateStartupTaskTitle, updateStartupTaskMcpProfile, updateStartupTaskClaudeSession, updateStartupTaskAgentSession, updateStartupTaskBranch, updateStartupTaskCwd, updateStartupTaskAttentionDismissal, updateStartupTaskLifecycle, backfillWalleStartupTaskActivity, markStartupTaskUserPromptInput, removeStartupTask, markStartupTaskExited, markStartupTaskParked, reviveStartupTask, heartbeatStartupTasks, getStartupTask, listStartupTasks, findSessionsMissingStartupTask, clearStartupTasks, listSessionLinkedRefs,
20698
+ addStartupTask, updateStartupTaskLabel, updateStartupTaskTitle, updateStartupTaskMcpProfile, updateStartupTaskClaudeSession, updateStartupTaskAgentSession, updateStartupTaskBranch, updateStartupTaskCwd, updateStartupTaskAttentionDismissal, updateStartupTaskLifecycle, backfillWalleStartupTaskActivity, markStartupTaskUserPromptInput, removeStartupTask, markStartupTaskExited, markStartupTaskParked, reviveStartupTask, heartbeatStartupTasks, getStartupTask, listStartupTasks, getCtmSessionOwnerRow, findSessionsMissingStartupTask, clearStartupTasks, listSessionLinkedRefs,
20280
20699
  createSessionWithStartupTask,
20281
20700
  appendScrollbackBatch, cleanupScrollbackChunkSeqIntegrity, getNextScrollbackSeq, loadScrollback, loadScrollbackTail, clearScrollback,
20282
20701
  getScrollbackIdentity, setScrollbackIdentity, inspectScrollbackForSession, prepareScrollbackForSession,
@@ -20302,11 +20721,11 @@ module.exports = {
20302
20721
  upsertSessionIndex, resolveSessionId,
20303
20722
  // CTM Sessions + Agent Sessions (v1 schema)
20304
20723
  getSessionIdentity, getLatestAgentSessionForCtm, getSessionConversationSourceIds, getSessionMessageIndexFreshness, getSessionUserPromptFreshness, resolveSession, batchRestoreIdentities, upsertSession, upsertAgentSessionIdentity, recordResumeLineage, setSessionStar, getStarredSessionIds,
20305
- setSessionTitleNew, setSessionTitleStatusNew, getSessionTitleNew, getSessionTitlesByIds, getSessionTitleSourceById, getSessionDisplayTitleInfo, isUserSessionTitleCandidate, promoteStartupTaskTitleIfUserNamed, repairSessionTitlesFromStartupTasks, getAllSessionsData, setSessionTitleMirror, getSessionTitleMirror,
20724
+ setSessionTitleNew, setSessionTitleStatusNew, getSessionTitleNew, getSessionTitlesByIds, getSessionTitleSourceById, getSessionDisplayTitleInfo, getAllSessionsData, setSessionTitleMirror, getSessionTitleMirror, applyAgentTitleBackfillRows,
20306
20725
  repairCodexSessionMetadataFromConversations, repairCodexRolloutAgentIdentities, detachPromotedProviderChildSessions,
20307
20726
  listProviderChildAgentOwnerMappings,
20308
20727
  getAgentSessions, getAgentSession, getAgentSessionByCtmId, getForkForDesktopUuid, listDesktopForks, deleteCtmSession,
20309
- deleteAgentSessionIdentity, clearScrollbackIdentities,
20728
+ deleteAgentSessionIdentity, clearScrollbackIdentities, updateAgentSessionTranscriptLink,
20310
20729
  // worker_sessions (WP-0 §6.2) — one writer: lib/session-create/delegate-worker.js
20311
20730
  insertWorkerSession, getWorkerSession, listWorkerTranscriptRows, updateWorkerSession, listLiveWorkerSessions, listWorkerSessionsByParent, listHiddenWorkerStartupTaskIds, listWorkerStartupTaskIds, listLatestTerminalWorkerSessionsByRoute, listTerminalWorkerSessionsSince,
20312
20731
  // Schema version