create-walle 0.10.2 → 0.10.3

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 (488) hide show
  1. package/README.md +3 -1
  2. package/lib/release-schema.json +2 -2
  3. package/lib/runtime-closure-policy.json +8 -7
  4. package/package.json +1 -1
  5. package/template/coding-task-manager/api-prompts.js +45 -7
  6. package/template/coding-task-manager/bin/claude-hook.js +55 -1
  7. package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
  8. package/template/coding-task-manager/bin/land.js +2069 -0
  9. package/template/coding-task-manager/bin/memory-census.js +4 -2
  10. package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
  11. package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
  12. package/template/coding-task-manager/db.js +156 -32
  13. package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
  14. package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
  15. package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
  16. package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
  17. package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
  18. package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
  19. package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
  20. package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
  21. package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
  22. package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
  23. package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
  24. package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
  25. package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
  26. package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
  27. package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
  28. package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
  29. package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
  30. package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
  31. package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
  32. package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
  33. package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
  34. package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
  35. package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
  36. package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
  37. package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
  38. package/template/coding-task-manager/lib/agent-definitions.js +19 -0
  39. package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
  40. package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
  41. package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
  42. package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
  43. package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
  44. package/template/coding-task-manager/lib/agent-presets.js +84 -1
  45. package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
  46. package/template/coding-task-manager/lib/agent-version-service.js +74 -11
  47. package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
  48. package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
  49. package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
  50. package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
  51. package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
  52. package/template/coding-task-manager/lib/api/fleet.js +118 -0
  53. package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
  54. package/template/coding-task-manager/lib/api/ingress.js +9 -1
  55. package/template/coding-task-manager/lib/api/landing.js +403 -0
  56. package/template/coding-task-manager/lib/api/machines.js +691 -2
  57. package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
  58. package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
  59. package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
  60. package/template/coding-task-manager/lib/api/session-attach.js +623 -0
  61. package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
  62. package/template/coding-task-manager/lib/api/sessions.js +77 -51
  63. package/template/coding-task-manager/lib/api/setup-network.js +544 -16
  64. package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
  65. package/template/coding-task-manager/lib/api/setup.js +829 -10
  66. package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
  67. package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
  68. package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
  69. package/template/coding-task-manager/lib/auth-rules.js +292 -21
  70. package/template/coding-task-manager/lib/background-stop.js +369 -0
  71. package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
  72. package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
  73. package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
  74. package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
  75. package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
  76. package/template/coding-task-manager/lib/build-info.js +4 -2
  77. package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
  78. package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
  79. package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
  80. package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
  81. package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
  82. package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
  83. package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
  84. package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
  85. package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
  86. package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
  87. package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
  88. package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
  89. package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
  90. package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
  91. package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
  92. package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
  93. package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
  94. package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
  95. package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
  96. package/template/coding-task-manager/lib/conversation-sync.js +355 -0
  97. package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
  98. package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
  99. package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
  100. package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
  101. package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
  102. package/template/coding-task-manager/lib/db-domains.js +1 -0
  103. package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
  104. package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
  105. package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
  106. package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
  107. package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
  108. package/template/coding-task-manager/lib/feature-manifest.js +4 -0
  109. package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
  110. package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
  111. package/template/coding-task-manager/lib/fleet-hub.js +762 -0
  112. package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
  113. package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
  114. package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
  115. package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
  116. package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
  117. package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
  118. package/template/coding-task-manager/lib/fleet-update.js +798 -0
  119. package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
  120. package/template/coding-task-manager/lib/fs-watcher.js +58 -42
  121. package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
  122. package/template/coding-task-manager/lib/headless-term-service.js +84 -8
  123. package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
  124. package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
  125. package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
  126. package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
  127. package/template/coding-task-manager/lib/host-handoff.js +16 -19
  128. package/template/coding-task-manager/lib/host-stats.js +150 -3
  129. package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
  130. package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
  131. package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
  132. package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
  133. package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
  134. package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
  135. package/template/coding-task-manager/lib/hub-bridge.js +398 -0
  136. package/template/coding-task-manager/lib/idle-notify.js +491 -0
  137. package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
  138. package/template/coding-task-manager/lib/instance-manifest.js +5 -1
  139. package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
  140. package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
  141. package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
  142. package/template/coding-task-manager/lib/landing/config.js +536 -0
  143. package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
  144. package/template/coding-task-manager/lib/landing/gate.js +360 -0
  145. package/template/coding-task-manager/lib/landing/git.js +344 -0
  146. package/template/coding-task-manager/lib/landing/lease.js +247 -0
  147. package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
  148. package/template/coding-task-manager/lib/landing/proof.js +223 -0
  149. package/template/coding-task-manager/lib/landing/queue.js +998 -0
  150. package/template/coding-task-manager/lib/landing/radar.js +489 -0
  151. package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
  152. package/template/coding-task-manager/lib/landing/state.js +263 -0
  153. package/template/coding-task-manager/lib/landing/stop.js +49 -0
  154. package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
  155. package/template/coding-task-manager/lib/landing/sync.js +456 -0
  156. package/template/coding-task-manager/lib/landing/train.js +885 -0
  157. package/template/coding-task-manager/lib/landing/window.js +57 -0
  158. package/template/coding-task-manager/lib/launch-environment.js +121 -0
  159. package/template/coding-task-manager/lib/launch-model-override.js +36 -10
  160. package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
  161. package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
  162. package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
  163. package/template/coding-task-manager/lib/lead-model-options.js +160 -3
  164. package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
  165. package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
  166. package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
  167. package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
  168. package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
  169. package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
  170. package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
  171. package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
  172. package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
  173. package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
  174. package/template/coding-task-manager/lib/machine-registry.js +750 -5
  175. package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
  176. package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
  177. package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
  178. package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
  179. package/template/coding-task-manager/lib/memory-join.js +337 -0
  180. package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
  181. package/template/coding-task-manager/lib/memory-sync.js +565 -0
  182. package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
  183. package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
  184. package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
  185. package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
  186. package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
  187. package/template/coding-task-manager/lib/model-availability.js +727 -0
  188. package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
  189. package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
  190. package/template/coding-task-manager/lib/model-id-rule.js +19 -0
  191. package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
  192. package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
  193. package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
  194. package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
  195. package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
  196. package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
  197. package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
  198. package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
  199. package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
  200. package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
  201. package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
  202. package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
  203. package/template/coding-task-manager/lib/native-history-control.js +6 -4
  204. package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
  205. package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
  206. package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
  207. package/template/coding-task-manager/lib/opencode-cli.js +480 -0
  208. package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
  209. package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
  210. package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
  211. package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
  212. package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
  213. package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
  214. package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
  215. package/template/coding-task-manager/lib/perf-summary.js +7 -0
  216. package/template/coding-task-manager/lib/perf-tracker.js +63 -2
  217. package/template/coding-task-manager/lib/project-config.js +7 -0
  218. package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
  219. package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
  220. package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
  221. package/template/coding-task-manager/lib/read-pool-client.js +89 -2
  222. package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
  223. package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
  224. package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
  225. package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
  226. package/template/coding-task-manager/lib/relay-client.js +57 -3
  227. package/template/coding-task-manager/lib/relink-audit.js +34 -5
  228. package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
  229. package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
  230. package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
  231. package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
  232. package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
  233. package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
  234. package/template/coding-task-manager/lib/restart-guard.js +18 -6
  235. package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
  236. package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
  237. package/template/coding-task-manager/lib/resume-admission.js +17 -3
  238. package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
  239. package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
  240. package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
  241. package/template/coding-task-manager/lib/runtime-contract.js +3 -1
  242. package/template/coding-task-manager/lib/schema-indexes.js +1 -0
  243. package/template/coding-task-manager/lib/session-context-locator.js +742 -0
  244. package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
  245. package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
  246. package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
  247. package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
  248. package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
  249. package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
  250. package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
  251. package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
  252. package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
  253. package/template/coding-task-manager/lib/session-create/response.js +7 -7
  254. package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
  255. package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
  256. package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
  257. package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
  258. package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
  259. package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
  260. package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
  261. package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
  262. package/template/coding-task-manager/lib/session-empty.js +13 -10
  263. package/template/coding-task-manager/lib/session-host-manager.js +60 -0
  264. package/template/coding-task-manager/lib/session-jobs.js +3 -1
  265. package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
  266. package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
  267. package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
  268. package/template/coding-task-manager/lib/session-park.js +738 -0
  269. package/template/coding-task-manager/lib/session-path-grant.js +13 -4
  270. package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
  271. package/template/coding-task-manager/lib/session-transport.js +17 -0
  272. package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
  273. package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
  274. package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
  275. package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
  276. package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
  277. package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
  278. package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
  279. package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
  280. package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
  281. package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
  282. package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
  283. package/template/coding-task-manager/lib/submission-ack.js +90 -2
  284. package/template/coding-task-manager/lib/systemd-scope.js +115 -0
  285. package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
  286. package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
  287. package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
  288. package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
  289. package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
  290. package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
  291. package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
  292. package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
  293. package/template/coding-task-manager/lib/transcript-source.js +36 -4
  294. package/template/coding-task-manager/lib/transcript-store.js +9 -0
  295. package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
  296. package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
  297. package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
  298. package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
  299. package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
  300. package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
  301. package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
  302. package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
  303. package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
  304. package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
  305. package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
  306. package/template/coding-task-manager/lib/walle/turn.js +1 -1
  307. package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
  308. package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
  309. package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
  310. package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
  311. package/template/coding-task-manager/lib/walle-row-history.js +495 -0
  312. package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
  313. package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
  314. package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
  315. package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
  316. package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
  317. package/template/coding-task-manager/lib/ws/misc.js +1 -1
  318. package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
  319. package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
  320. package/template/coding-task-manager/oauth-proxy.js +112 -115
  321. package/template/coding-task-manager/package-lock.json +10 -94
  322. package/template/coding-task-manager/package.json +4 -3
  323. package/template/coding-task-manager/protocol/message-table.json +10 -0
  324. package/template/coding-task-manager/protocol/messages.js +10 -0
  325. package/template/coding-task-manager/protocol/ws-schema.json +20 -0
  326. package/template/coding-task-manager/protocol/ws-schema.md +2 -0
  327. package/template/coding-task-manager/public/css/app.css +170 -0
  328. package/template/coding-task-manager/public/css/walle-session.css +4 -0
  329. package/template/coding-task-manager/public/index.html +473 -243
  330. package/template/coding-task-manager/public/js/agent-command.js +86 -0
  331. package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
  332. package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
  333. package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
  334. package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
  335. package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
  336. package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
  337. package/template/coding-task-manager/public/js/background-work.js +196 -27
  338. package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
  339. package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
  340. package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
  341. package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
  342. package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
  343. package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
  344. package/template/coding-task-manager/public/js/document-review-links.js +9 -1
  345. package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
  346. package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
  347. package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
  348. package/template/coding-task-manager/public/js/message-renderer.js +4 -3
  349. package/template/coding-task-manager/public/js/model-picker.js +3 -1
  350. package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
  351. package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
  352. package/template/coding-task-manager/public/js/pasted-content.js +236 -0
  353. package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
  354. package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
  355. package/template/coding-task-manager/public/js/prompts.js +11 -10
  356. package/template/coding-task-manager/public/js/protocol.js +10 -0
  357. package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
  358. package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
  359. package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
  360. package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
  361. package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
  362. package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
  363. package/template/coding-task-manager/public/js/stream-view.js +6 -5
  364. package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
  365. package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
  366. package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
  367. package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
  368. package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
  369. package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
  370. package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
  371. package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
  372. package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
  373. package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
  374. package/template/coding-task-manager/public/js/walle.js +6 -0
  375. package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
  376. package/template/coding-task-manager/public/m/app.css +140 -0
  377. package/template/coding-task-manager/public/m/app.js +507 -68
  378. package/template/coding-task-manager/public/m/index.html +17 -0
  379. package/template/coding-task-manager/public/m/machines.js +838 -4
  380. package/template/coding-task-manager/public/m/private-api.js +31 -1
  381. package/template/coding-task-manager/public/usage.html +1 -0
  382. package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
  383. package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
  384. package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
  385. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
  386. package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
  387. package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
  388. package/template/coding-task-manager/server.js +1950 -11679
  389. package/template/coding-task-manager/session-integrity.js +64 -4
  390. package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
  391. package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
  392. package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
  393. package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
  394. package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
  395. package/template/coding-task-manager/workers/session-host-process.js +5 -5
  396. package/template/coding-task-manager/workers/watch-worker.js +105 -0
  397. package/template/coding-task-manager/workers/worktree-worker.js +168 -33
  398. package/template/ctm-relay/lib/protocol.js +14 -1
  399. package/template/package-lock.json +2 -2
  400. package/template/package.json +1 -1
  401. package/template/shared/backup-retention.js +82 -0
  402. package/template/shared/conversation-admission.js +2 -0
  403. package/template/shared/conversation-provenance.js +3 -7
  404. package/template/shared/conversation-storage-v2-contract.js +5 -0
  405. package/template/shared/linked-worktree.js +51 -0
  406. package/template/shared/macos-fda-target.js +57 -0
  407. package/template/shared/orphan-parents.js +55 -0
  408. package/template/shared/release-schema.json +2 -2
  409. package/template/shared/storage/contract.js +3 -0
  410. package/template/wall-e/adapters/imessage.js +12 -0
  411. package/template/wall-e/agent.js +71 -4
  412. package/template/wall-e/api-walle.js +21 -1
  413. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
  414. package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
  415. package/template/wall-e/brain/brain-join.js +859 -0
  416. package/template/wall-e/brain/brain-sync-membership.js +20 -0
  417. package/template/wall-e/brain/brain-sync-store.js +131 -77
  418. package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
  419. package/template/wall-e/brain/brain-sync-v2.js +578 -0
  420. package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
  421. package/template/wall-e/brain/deterministic-id.js +51 -0
  422. package/template/wall-e/brain/memories.js +2 -2
  423. package/template/wall-e/brain/migrations.js +40 -6
  424. package/template/wall-e/brain/model-identity-migration.js +1 -1
  425. package/template/wall-e/brain/owner-call-whitelist.js +23 -7
  426. package/template/wall-e/brain/schema.js +45 -85
  427. package/template/wall-e/brain.js +171 -35
  428. package/template/wall-e/chat.js +3 -3
  429. package/template/wall-e/coding/agent-shell-env.js +7 -2
  430. package/template/wall-e/coding/prompt-bundle.js +66 -21
  431. package/template/wall-e/config/main-loop-exemptions.json +2 -2
  432. package/template/wall-e/http/admin-api.js +37 -0
  433. package/template/wall-e/http/gws-api.js +4 -0
  434. package/template/wall-e/http/helper-rules-api.js +7 -1
  435. package/template/wall-e/http/model-admin.js +8 -1
  436. package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
  437. package/template/wall-e/http/model-proxy-providers.js +174 -1
  438. package/template/wall-e/http/model-route-health.js +157 -0
  439. package/template/wall-e/http/sync-api.js +278 -464
  440. package/template/wall-e/jobs/job-runner.js +28 -1
  441. package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
  442. package/template/wall-e/lib/ctm-data-dir.js +26 -0
  443. package/template/wall-e/lib/derivation-home.js +147 -0
  444. package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
  445. package/template/wall-e/lib/helper-rules/grade.js +51 -8
  446. package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
  447. package/template/wall-e/lib/helper-rules/store.js +4 -2
  448. package/template/wall-e/lib/landing-mcp-tools.js +170 -0
  449. package/template/wall-e/lib/mcp-client-config.js +133 -14
  450. package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
  451. package/template/wall-e/lib/mcp-integration.js +58 -13
  452. package/template/wall-e/lib/parent-death.js +7 -1
  453. package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
  454. package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
  455. package/template/wall-e/llm/delegate-vendor-default.js +12 -5
  456. package/template/wall-e/llm/model-roles.js +1 -1
  457. package/template/wall-e/llm/native-delegate-selection.js +56 -8
  458. package/template/wall-e/llm/worker-delegate-args.js +72 -7
  459. package/template/wall-e/llm/worker-mcp-routing.js +66 -17
  460. package/template/wall-e/loops/brain-custom-writes.js +19 -0
  461. package/template/wall-e/loops/ingest.js +17 -1
  462. package/template/wall-e/loops/think.js +19 -4
  463. package/template/wall-e/mcp-contract.js +222 -6
  464. package/template/wall-e/mcp-protocol-contract.js +40 -5
  465. package/template/wall-e/mcp-server.js +128 -29
  466. package/template/wall-e/mcp-worker-schemas.js +5 -5
  467. package/template/wall-e/memory/ctm-context-client.js +85 -0
  468. package/template/wall-e/memory/ctm-session-context.js +9 -3
  469. package/template/wall-e/memory/hub-bridge-client.js +388 -0
  470. package/template/wall-e/memory/replica-origin.js +56 -0
  471. package/template/wall-e/memory/session-pool.js +135 -0
  472. package/template/wall-e/runtime/live-capabilities.js +8 -3
  473. package/template/wall-e/server.js +4 -2
  474. package/template/wall-e/shared/release-schema.json +2 -2
  475. package/template/wall-e/shared/sqlite-file-set.js +39 -0
  476. package/template/wall-e/shared/storage/contract.js +3 -0
  477. package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
  478. package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
  479. package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
  480. package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
  481. package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
  482. package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
  483. package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
  484. package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
  485. package/template/wall-e/tools/chrome-automation.js +6 -0
  486. package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
  487. package/template/wall-e/workers/brain-owner-worker.js +18 -4
  488. package/template/wall-e/workers/mcp-gateway-process.js +5 -5
@@ -1,66 +1,91 @@
1
1
  'use strict';
2
2
 
3
- // WP-5 (multi-machine-login-resume-sync-2026-10-18 §6.6, R-10/R-11): the Wall-E
4
- // half of the brain sync pair.
5
- //
6
- // GET /api/walle/sync/outbox?after_seq=N&limit=M -- this host's brain outbox
7
- // POST /api/walle/sync/apply { sourceInstanceId, entries:[...] }
8
- // POST /api/walle/sync/prune { throughSeq } -- outbox retention (follow-up #17)
9
- //
10
- // The `/api/wall-e/...` spellings are accepted as aliases; the CTM worker calls
11
- // the doc's `/api/walle/...` form.
12
- //
13
- // AUTH: a bearer that matches the CTM peer token is required, and loopback alone
14
- // is NOT enough. The laptop pushes to the Spark through the ssh LocalForward
15
- // (`bin/spark-home-forward.sh`), so the request arrives on the Spark as a
16
- // loopback request — the same hole WP-4 F7 closed on CTM
17
- // (coding-task-manager/lib/auth-context.js `_syncCredentialPresented`). Both
18
- // spellings of the token env are read: CTM_SYNC_PEER_TOKEN (one peer) and
19
- // CTM_SYNC_PEER_TOKENS (the `peerId=token` / JSON map form the CTM side parses in
20
- // lib/hosted-sync-worker.js).
3
+ // Wall-E's half of the brain sync pair — now the v2 version-vector feed only (WP-1). The
4
+ // v1 outbox routes (`/api/walle/sync/{outbox,apply,prune}` + the `/api/wall-e/` aliases),
5
+ // their env-token peer auth and the outbox storage were retired in WP-6 (migration 105).
21
6
  //
22
7
  // WRITES: brain row writes belong to the brain owner process. The route resolves
23
8
  // the owner worker client first (lib/brain-owner-worker-client) and only falls
24
9
  // back to the in-process brain module when no owner client exists (dev, tests, a
25
10
  // single-process host) or when the owner refuses the call as not whitelisted —
26
- // see _callLocalBrain. Everything a batch writes happens inside ONE
27
- // `db.transaction(...).immediate()` in applyBrainSyncEntries, with the
11
+ // see callLocalBrain. Everything a v2 apply writes happens inside ONE
12
+ // `db.transaction(...).immediate()` in brain-sync-v2.js, with the
28
13
  // connection-scoped TEMP TABLE `brain_sync_applying` present for its duration so
29
- // the capture triggers cannot re-enqueue what we just applied.
14
+ // the capture triggers cannot re-stamp what we just applied.
30
15
  //
31
- // The DDL for the four sync tables and the eight triggers lives in the
32
- // runner-owned ladder: migration 89 in wall-e/brain/migrations.js, whose text is
33
- // brain/schema.js's BRAIN_SYNC_DDL (the sanctioned DDL module). This file owns NO
34
- // DDL — the apply-batch sentinel's CREATE/DROP TEMP TABLE text is
16
+ // This file owns NO DDL — the apply-batch sentinel's CREATE/DROP TEMP TABLE text is
35
17
  // schema.createBrainSyncSentinel / dropBrainSyncSentinel.
36
18
 
37
- const { strFlag } = require('../lib/env-flags');
38
- const { createBrainSyncSentinel, dropBrainSyncSentinel } = require('../brain/schema');
39
- const brainSyncStore = require('../brain/brain-sync-store');
40
-
41
- const SYNC_PATHS = new Set([
42
- '/api/walle/sync/outbox',
43
- '/api/walle/sync/apply',
44
- '/api/wall-e/sync/outbox',
45
- '/api/wall-e/sync/apply',
46
- '/api/walle/sync/prune',
47
- '/api/wall-e/sync/prune',
48
- ]);
49
-
50
- const REPLICATED_TABLES = brainSyncStore.REPLICATED_TABLES;
51
- const APPLY_SENTINEL_TABLE = 'brain_sync_applying';
52
- const DEFAULT_LIMIT = 100;
53
- const MAX_LIMIT = 1000;
54
- // Follow-up #17: one prune call deletes at most this many rows per table, so a backlog of a
55
- // few hundred thousand outbox rows drains over several ticks instead of one long write lock.
56
- const PRUNE_CHUNK = 5000;
57
- const APPLIED_RETENTION_DAYS = 7;
19
+ const { strFlag, boolFlag } = require('../lib/env-flags');
20
+ const { resolveCtmDataDir } = require('../lib/ctm-data-dir');
21
+ const { requireBearer } = require('./auth');
22
+ const { isLoopbackRequest } = require('../shared/request-provenance');
23
+
24
+ // WP-1 (shared-memory-every-machine-2026-10-31.html §6.4, D-12): the v2 change feed. Loopback +
25
+ // the LOCAL Wall-E token only — a peer never calls these directly (its CTM does, with the
26
+ // pool credential), so a tunnel-forwarded request is refused even with a valid token. The
27
+ // storage half lives in brain/brain-sync-v2.js and runs on the owner's connection.
28
+ const V2_ROUTE_SPECS = Object.freeze({
29
+ vector: { method: 'GET', op: 'brainSyncV2Vector' },
30
+ changes: { method: 'POST', op: 'brainSyncV2Changes' },
31
+ apply: { method: 'POST', op: 'brainSyncV2Apply' },
32
+ 'member-vector': { method: 'POST', op: 'brainSyncV2MemberVector' },
33
+ 'prune-tombstones': { method: 'POST', op: 'brainSyncV2PruneTombstones' },
34
+ digest: { method: 'GET', op: 'brainSyncV2Digest' },
35
+ // WP-4 §6.7: the join trio. The home serves the snapshot; the member records the import and
36
+ // lets the brain-join-import job drive the pages. Same gate as every other v2 route.
37
+ 'join/snapshot': { method: 'POST', op: 'brainSyncV2JoinSnapshot' },
38
+ 'join/import': { method: 'POST', op: 'brainSyncV2JoinImport' },
39
+ 'join/status': { method: 'GET', op: 'brainSyncV2JoinStatus' },
40
+ });
41
+ const V2_PATH_PREFIXES = Object.freeze(['/api/walle/sync/v2/', '/api/wall-e/sync/v2/']);
42
+ // §6.4: JSON bodies are capped at 8 MiB and one apply carries at most 2000 entries.
43
+ const V2_MAX_BODY_BYTES = 8 * 1024 * 1024;
44
+ const V2_MAX_ENTRIES = 2000;
45
+ // lib/env-flags.js's own falsy set — an injected env (tests) must parse exactly the same way
46
+ // as boolFlag() does for a real run.
47
+ const FALSY_TOKENS = new Set(['0', 'false', 'no', 'off']);
48
+
49
+ function _v2Route(pathname) {
50
+ const prefix = V2_PATH_PREFIXES.find((candidate) => pathname.startsWith(candidate));
51
+ if (!prefix) return null;
52
+ const spec = V2_ROUTE_SPECS[pathname.slice(prefix.length)];
53
+ return spec ? { ...spec, pathname } : null;
54
+ }
55
+
56
+ // Kill switch (§6.4): the hub's memory tick and these routes are off together, through one
57
+ // flag read. A real run goes through lib/env-flags.js; an injected env is parsed with the same
58
+ // token set so the two can never disagree about what "off" means.
59
+ function brainSyncEnabled(env) {
60
+ if (!env || env === process.env) return boolFlag('CTM_SYNC_BRAIN_ENABLED', true);
61
+ const raw = _flag('CTM_SYNC_BRAIN_ENABLED', '', env).trim();
62
+ return !raw || !FALSY_TOKENS.has(raw.toLowerCase());
63
+ }
64
+
65
+ // 403 when the request is not provably on-box (D-12: the CTM front is what crosses the wire),
66
+ // 401/403 for the token, 503 when the host has no local token configured at all — an
67
+ // unauthenticated route is never "open just because the token is empty".
68
+ function authorizeV2Request(req, env) {
69
+ if (!isLoopbackRequest(req)) return { ok: false, status: 403, error: 'loopback_required' };
70
+ const token = _string(_flag('WALL_E_API_TOKEN', '', env), 1000).trim();
71
+ if (!token) return { ok: false, status: 503, error: 'walle_token_unconfigured' };
72
+ if (requireBearer(req, token)) return { ok: true };
73
+ return { ok: false, status: bearerToken(req) ? 403 : 401, error: bearerToken(req) ? 'wall_e_token_invalid' : 'wall_e_token_required' };
74
+ }
58
75
 
59
76
  function _string(value, max = 4000) {
60
77
  if (typeof value === 'string') return value.slice(0, max);
61
78
  return value == null ? '' : String(value).slice(0, max);
62
79
  }
63
80
 
81
+ // The presented bearer, or '' — the v2 gate distinguishes "forgot it" (401) from "wrong
82
+ // token" (403) with this.
83
+ function bearerToken(req) {
84
+ const header = _string(req?.headers?.authorization, 1200);
85
+ const match = /^Bearer\s+(.+)$/i.exec(header.trim());
86
+ return match ? match[1].trim() : '';
87
+ }
88
+
64
89
  // Real runs read through the canonical flag parser (lib/env-flags.js), which is
65
90
  // the only sanctioned place a CTM_*/WALLE_* name is read — an injected env (the
66
91
  // integration test constructs several) keeps its own object verbatim.
@@ -71,69 +96,6 @@ function _flag(name, fallback, env) {
71
96
  return String(raw);
72
97
  }
73
98
 
74
- // The `peerId=token,peer2=token2` (or JSON object) form CTM parses in
75
- // lib/hosted-sync-worker.js `_parsePeerTokenMap`; kept byte-compatible in spirit:
76
- // both spellings resolve to the same tokens on both sides.
77
- function parsePeerTokenMap(raw) {
78
- const value = _string(raw, 20000).trim();
79
- const map = new Map();
80
- if (!value) return map;
81
- if (value.startsWith('{')) {
82
- try {
83
- const parsed = JSON.parse(value);
84
- if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
85
- for (const [peerId, token] of Object.entries(parsed)) {
86
- const id = _string(peerId, 160).trim();
87
- const clean = _string(token, 1000).trim();
88
- if (id && clean) map.set(id, clean);
89
- }
90
- return map;
91
- }
92
- } catch {
93
- return map; // REPORT: a malformed JSON token map yields no peers — the route then refuses every request rather than authorizing one
94
- }
95
- }
96
- for (const part of value.split(',')) {
97
- const [peerId, ...rest] = part.split('=');
98
- const id = _string(peerId, 160).trim();
99
- const clean = _string(rest.join('='), 1000).trim();
100
- if (id && clean) map.set(id, clean);
101
- }
102
- return map;
103
- }
104
-
105
- function peerTokens(env) {
106
- const tokens = new Set();
107
- const single = _string(_flag('CTM_SYNC_PEER_TOKEN', '', env), 1000).trim();
108
- if (single) tokens.add(single);
109
- for (const token of parsePeerTokenMap(_flag('CTM_SYNC_PEER_TOKENS', '', env)).values()) tokens.add(token);
110
- return tokens;
111
- }
112
-
113
- // R-12 parity with CTM: sync is on iff peers are configured. Wall-E reads this to
114
- // report/behave consistently; the routes themselves are gated by the bearer, so a
115
- // host with no peers configured still ACCEPTS a push (it is a receiver).
116
- function syncEnabledByPeers(env) {
117
- return _string(_flag('CTM_SYNC_PEERS', '', env), 20000).trim().length > 0;
118
- }
119
-
120
- function bearerToken(req) {
121
- const header = _string(req?.headers?.authorization, 1200);
122
- const match = /^Bearer\s+(.+)$/i.exec(header.trim());
123
- return match ? match[1].trim() : '';
124
- }
125
-
126
- // 401 when nothing was presented, 403 when a bearer was presented but is not a
127
- // peer token: the caller must be able to tell "you forgot the token" from "that
128
- // token is not mine".
129
- function authorizePeerRequest(req, env) {
130
- const presented = bearerToken(req);
131
- if (!presented) return { ok: false, status: 401, error: 'peer_token_required' };
132
- const tokens = peerTokens(env);
133
- if (!tokens.size || !tokens.has(presented)) return { ok: false, status: 403, error: 'peer_token_invalid' };
134
- return { ok: true, token: presented, tokens };
135
- }
136
-
137
99
  function _jsonResponse(res, body, status = 200) {
138
100
  const payload = JSON.stringify(body);
139
101
  res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payload) });
@@ -168,293 +130,6 @@ function _readBody(req, limitBytes = 32 * 1024 * 1024) {
168
130
  });
169
131
  }
170
132
 
171
- // ---------------------------------------------------------------------------
172
- // The sync core. Exported so the integration test can drive it against a real
173
- // temp brain without an HTTP listener.
174
- // ---------------------------------------------------------------------------
175
-
176
- function _tableColumns(db, table) {
177
- try {
178
- return db.prepare(`PRAGMA table_info("${table}")`).all().map((row) => row.name);
179
- } catch {
180
- return [];
181
- }
182
- }
183
-
184
- // Payloads arrive from a peer: identifiers that reach SQL come only from the
185
- // LOCAL table's column list, values are always bound parameters.
186
- function _payloadFields(payload, allowedColumns) {
187
- if (!payload || typeof payload !== 'object' || Array.isArray(payload)) return [];
188
- const allowed = new Set(allowedColumns);
189
- return Object.keys(payload).filter((key) => allowed.has(key));
190
- }
191
-
192
- function _parsePayload(raw) {
193
- if (raw && typeof raw === 'object') return raw;
194
- try {
195
- const parsed = JSON.parse(String(raw || '{}'));
196
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
197
- } catch {
198
- return null; // REPORT: an unparseable payload from a peer is recorded as a conflict; the cursor stays behind it
199
- }
200
- }
201
-
202
- function _parseSeq(value) {
203
- const n = Number(value);
204
- return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
205
- }
206
-
207
- function listBrainOutbox(db, { afterSeq = 0, limit = DEFAULT_LIMIT } = {}) {
208
- const after = Math.max(0, _parseSeq(afterSeq));
209
- const bounded = Math.max(1, Math.min(MAX_LIMIT, Math.floor(Number(limit) || 0) || DEFAULT_LIMIT));
210
- const localId = brainSyncStore.localBrainInstanceId(db);
211
- const page = db.prepare(`
212
- SELECT seq, operation_id, table_name, row_id, op, updated_at, origin_instance_id
213
- FROM brain_outbox
214
- WHERE seq > ?
215
- ORDER BY seq ASC
216
- LIMIT ?
217
- `);
218
- let rows = page.all(after, bounded);
219
- // Follow-up #16: a short page means the live log is drained past `after` — top it up with the
220
- // next slice of pre-capture history (never more than the room left in this page, AC-16d), then
221
- // re-read so the new rows ship in this same response.
222
- let backfill;
223
- if (rows.length < bounded) {
224
- const filled = brainSyncStore.enqueueBackfillPage(db, bounded - rows.length);
225
- backfill = filled.backfill;
226
- if (filled.enqueued) rows = page.all(after, bounded);
227
- } else {
228
- backfill = brainSyncStore.readBackfillState(db);
229
- }
230
- // The payload is the LIVE row at read time (the triggers record only the id and the clock):
231
- // a row that is gone by now ships as a delete, which is what the peer should mirror.
232
- const entries = rows.map((row) => {
233
- const live = brainSyncStore.readReplicatedRow(db, row.table_name, row.row_id);
234
- return {
235
- seq: Number(row.seq) || 0,
236
- operation_id: row.operation_id,
237
- table_name: row.table_name,
238
- row_id: row.row_id,
239
- op: live ? 'upsert' : 'delete',
240
- payload: JSON.stringify(live || { id: row.row_id }),
241
- updated_at: row.updated_at,
242
- origin_instance_id: row.origin_instance_id || localId,
243
- };
244
- });
245
- return {
246
- ok: true,
247
- entries,
248
- lastSeq: entries.reduce((max, entry) => Math.max(max, entry.seq), 0),
249
- pending: Number(db.prepare('SELECT count(*) AS c FROM brain_outbox WHERE seq > ?').get(after)?.c || 0) || 0,
250
- backfill,
251
- };
252
- }
253
-
254
- // Follow-up #17: outbox retention. CTM calls this on the LOCAL Wall-E once every configured
255
- // peer's push cursor is past `throughSeq` (lib/hosted-sync-worker.js). Bounded: at most
256
- // PRUNE_CHUNK rows per table per call; `more` says whether outbox rows <= throughSeq remain.
257
- // brain_sync_applied rows older than APPLIED_RETENTION_DAYS go too: re-delivering a pruned
258
- // operation is already a no-op (memories INSERT OR IGNORE; the LWW compare skips an equal clock
259
- // from the same origin), so the applied set only has to cover the in-flight window.
260
- function pruneBrainOutbox(db, { throughSeq = 0, limit = PRUNE_CHUNK } = {}) {
261
- const through = _parseSeq(throughSeq);
262
- const chunk = Math.max(1, Math.min(PRUNE_CHUNK, Math.floor(Number(limit) || 0) || PRUNE_CHUNK));
263
- return db.transaction(() => {
264
- const pruned = through
265
- ? db.prepare(`
266
- DELETE FROM brain_outbox
267
- WHERE seq IN (SELECT seq FROM brain_outbox WHERE seq <= ? ORDER BY seq ASC LIMIT ?)
268
- `).run(through, chunk).changes
269
- : 0;
270
- const more = through
271
- ? !!db.prepare('SELECT 1 AS hit FROM brain_outbox WHERE seq <= ? LIMIT 1').get(through)
272
- : false;
273
- const appliedPruned = db.prepare(`
274
- DELETE FROM brain_sync_applied
275
- WHERE rowid IN (SELECT rowid FROM brain_sync_applied WHERE applied_at < datetime('now', ?) LIMIT ?)
276
- `).run(`-${APPLIED_RETENTION_DAYS} days`, chunk).changes;
277
- return { ok: true, throughSeq: through, pruned, more, appliedPruned };
278
- }).immediate();
279
- }
280
-
281
- function _lwwWins(db, table, rowId, updatedAt, originInstanceId, localId) {
282
- const local = db.prepare(
283
- 'SELECT updated_at, origin_instance_id FROM brain_row_clock WHERE table_name = ? AND row_id = ?'
284
- ).get(table, rowId);
285
- if (!local) return true;
286
- const localAt = _string(local.updated_at, 64);
287
- const incomingAt = _string(updatedAt, 64);
288
- if (incomingAt > localAt) return true;
289
- if (incomingAt < localAt) return false;
290
- // Deterministic tiebreak (R-10, P-6): equal clock → the lexicographically smaller
291
- // origin_instance_id wins on BOTH hosts, so the two converge. '' in the local clock
292
- // means "this host" (the triggers cannot stamp the id), so it compares as localId.
293
- const localOrigin = _string(local.origin_instance_id, 160) || _string(localId, 160);
294
- return _string(originInstanceId, 160) < localOrigin;
295
- }
296
-
297
- function _writeRow(db, table, fields, payload) {
298
- const columns = fields.map((column) => `"${column}"`).join(', ');
299
- const placeholders = fields.map(() => '?').join(', ');
300
- const values = fields.map((column) => payload[column]);
301
- if (table === 'memories') {
302
- // R-10: memories replicate append-only by id — a re-sent (or locally edited)
303
- // memory never overwrites the row that is already here.
304
- return db.prepare(`INSERT OR IGNORE INTO "memories" (${columns}) VALUES (${placeholders})`).run(...values);
305
- }
306
- const updates = fields.filter((column) => column !== 'id').map((column) => `"${column}" = excluded."${column}"`).join(', ');
307
- const conflict = updates ? `ON CONFLICT("id") DO UPDATE SET ${updates}` : 'ON CONFLICT("id") DO NOTHING';
308
- return db.prepare(`INSERT INTO "${table}" (${columns}) VALUES (${placeholders}) ${conflict}`).run(...values);
309
- }
310
-
311
- function _stampClock(db, table, rowId, updatedAt, originInstanceId) {
312
- db.prepare(`
313
- INSERT INTO brain_row_clock (table_name, row_id, updated_at, origin_instance_id)
314
- VALUES (?, ?, ?, ?)
315
- ON CONFLICT(table_name, row_id) DO UPDATE SET
316
- updated_at = excluded.updated_at, origin_instance_id = excluded.origin_instance_id
317
- `).run(table, rowId, _string(updatedAt, 64), _string(originInstanceId, 160));
318
- }
319
-
320
- function _sortBySeq(entries) {
321
- return entries
322
- .map((entry, index) => ({ entry, index, seq: _parseSeq(entry?.seq) }))
323
- .sort((a, b) => (a.seq - b.seq) || (a.index - b.index))
324
- .map((wrapped) => wrapped.entry);
325
- }
326
-
327
- // Apply one batch from a peer, one transaction per batch.
328
- //
329
- // R-10 rules: memories → INSERT OR IGNORE by id; knowledge/entities/people →
330
- // upsert only when the incoming clock is newer, or equal with a smaller
331
- // origin_instance_id. knowledge.superseded_by whose target is not here gets the
332
- // same repair migration 88 applies to a dangling pointer (NULL) and is reported
333
- // as `brain_fk_dropped`: an outbox row is immutable, so "park and retry" could
334
- // only ever re-offer the identical row — a permanent stall of everything behind
335
- // it — and ordered paging means a target that DID travel always arrives first
336
- // (a lower seq). The same holds for a malformed row: it is recorded as a
337
- // conflict, marked applied so it is never re-litigated, and the cursor moves on.
338
- // Nothing in a batch can stop the cursor.
339
- function applyBrainSyncEntries(db, payload = {}) {
340
- const sourceInstanceId = _string(payload.sourceInstanceId || payload.source_instance_id, 160).trim();
341
- if (!sourceInstanceId) {
342
- const error = new Error('brain_sync_source_instance_required');
343
- error.code = 'BRAIN_SYNC_SOURCE_REQUIRED';
344
- throw error;
345
- }
346
- const entries = _sortBySeq(Array.isArray(payload.entries) ? payload.entries : []);
347
- const localId = brainSyncStore.localBrainInstanceId(db);
348
-
349
- // The sentinel's CREATE TEMP TABLE text lives in brain/schema.js (the sanctioned
350
- // DDL module) — this HTTP module owns no DDL (bin/ddl-outside-runner-lint.js).
351
- createBrainSyncSentinel(db);
352
- try {
353
- return db.transaction(() => {
354
- let applied = 0;
355
- let skipped = 0;
356
- let lastSeq = 0;
357
- const conflicts = [];
358
- // Idempotence is keyed by operation_id alone (a 128-bit random id, UNIQUE in
359
- // the producer's outbox): the same row reaching this host twice — once pulled,
360
- // once pushed, filed under two different source ids — is still one write.
361
- const seen = db.prepare('SELECT 1 AS hit FROM brain_sync_applied WHERE operation_id = ?');
362
- const record = db.prepare(
363
- "INSERT OR IGNORE INTO brain_sync_applied (source_instance_id, operation_id, applied_at) VALUES (?, ?, datetime('now'))"
364
- );
365
- const settle = (seq, operationId) => {
366
- if (operationId) record.run(sourceInstanceId, operationId);
367
- lastSeq = Math.max(lastSeq, seq);
368
- };
369
- for (const entry of entries) {
370
- const seq = _parseSeq(entry?.seq);
371
- const operationId = _string(entry?.operation_id || entry?.operationId, 160).trim();
372
- const table = _string(entry?.table_name || entry?.tableName, 40).trim();
373
- const rowId = _string(entry?.row_id || entry?.rowId, 160).trim();
374
- if (!operationId || !rowId || !REPLICATED_TABLES.includes(table)) {
375
- conflicts.push({ seq, operation_id: operationId, reason: 'brain_entry_invalid' });
376
- settle(seq, operationId);
377
- continue;
378
- }
379
- if (seen.get(operationId)) {
380
- // Idempotent consumer: the same operation_id twice is a skip, never a
381
- // second write.
382
- skipped += 1;
383
- settle(seq, '');
384
- continue;
385
- }
386
- const parsed = _parsePayload(entry.payload);
387
- if (!parsed) {
388
- conflicts.push({ seq, operation_id: operationId, reason: 'brain_payload_invalid' });
389
- settle(seq, operationId);
390
- continue;
391
- }
392
- const updatedAt = _string(entry.updated_at || entry.updatedAt, 64);
393
- const originInstanceId = _string(entry.origin_instance_id || entry.originInstanceId, 160);
394
- const op = _string(entry.op, 16).trim() || 'upsert';
395
- const columns = _tableColumns(db, table);
396
- if (!columns.length) {
397
- conflicts.push({ seq, operation_id: operationId, reason: 'brain_table_unknown' });
398
- settle(seq, operationId);
399
- continue;
400
- }
401
- if (op === 'delete') {
402
- if (table !== 'memories' && !_lwwWins(db, table, rowId, updatedAt, originInstanceId, localId)) {
403
- skipped += 1;
404
- settle(seq, operationId);
405
- continue;
406
- }
407
- db.prepare(`DELETE FROM "${table}" WHERE id = ?`).run(rowId);
408
- _stampClock(db, table, rowId, updatedAt, originInstanceId);
409
- settle(seq, operationId);
410
- applied += 1;
411
- continue;
412
- }
413
- if (table !== 'memories' && !_lwwWins(db, table, rowId, updatedAt, originInstanceId, localId)) {
414
- // Older (or tied-but-losing) write: silently dropped per R-10, but
415
- // recorded as applied so the same batch is never re-litigated.
416
- skipped += 1;
417
- settle(seq, operationId);
418
- continue;
419
- }
420
- const row = { ...parsed, id: parsed.id || rowId };
421
- // Follow-up #19: a memory this host did not ingest is never extracted here — the origin
422
- // host's extraction produced its knowledge rows, and those replicate on their own. The
423
- // insert is INSERT OR IGNORE, so this only ever labels a row that apply itself creates.
424
- if (table === 'memories') row.extraction_status = 'replicated';
425
- if (table === 'knowledge') {
426
- const target = _string(row.superseded_by, 160).trim();
427
- if (target && !brainSyncStore.knowledgeRowExists(db, target)) {
428
- conflicts.push({ seq, operation_id: operationId, reason: 'brain_fk_dropped', row_id: rowId, referenced_id: target });
429
- row.superseded_by = null;
430
- }
431
- }
432
- const fields = _payloadFields(row, columns);
433
- if (!fields.includes('id')) fields.unshift('id');
434
- _writeRow(db, table, fields, row);
435
- if (table !== 'memories') _stampClock(db, table, rowId, updatedAt, originInstanceId);
436
- settle(seq, operationId);
437
- applied += 1;
438
- }
439
- db.prepare(`
440
- INSERT INTO brain_sync_hwm (source_instance_id, last_seq, updated_at)
441
- VALUES (?, ?, datetime('now'))
442
- ON CONFLICT(source_instance_id) DO UPDATE SET
443
- last_seq = MAX(brain_sync_hwm.last_seq, excluded.last_seq),
444
- updated_at = excluded.updated_at
445
- `).run(sourceInstanceId, lastSeq);
446
- return { ok: true, applied, skipped, conflicts, lastSeq };
447
- }).immediate();
448
- } finally {
449
- dropBrainSyncSentinel(db);
450
- }
451
- }
452
-
453
- function getBrainSyncHwm(db, sourceInstanceId) {
454
- const row = db.prepare('SELECT last_seq FROM brain_sync_hwm WHERE source_instance_id = ?').get(_string(sourceInstanceId, 160));
455
- return Number(row?.last_seq || 0) || 0;
456
- }
457
-
458
133
  // ---------------------------------------------------------------------------
459
134
  // Local brain resolution (owner worker first, in-process module second).
460
135
  // ---------------------------------------------------------------------------
@@ -511,9 +186,21 @@ async function callLocalBrain(method, args = []) {
511
186
  // header: the two owner-call registrations are called out in the WP-5 report).
512
187
  }
513
188
  }
514
- if (method === 'brainSyncOutbox') return _withDb(method, args, listBrainOutbox);
515
- if (method === 'brainSyncApplyEntries') return _withDb(method, args, applyBrainSyncEntries);
516
- if (method === 'brainSyncPrune') return _withDb(method, args, pruneBrainOutbox);
189
+ // WP-1: the v2 feed. Same shape as the v1 trio — the storage module is loaded lazily so the
190
+ // (brain → http) edge this file would otherwise create never exists at require time.
191
+ const v2 = () => require('../brain/brain-sync-v2');
192
+ if (method === 'brainSyncV2Vector') return _withDb(method, args, (db) => v2().readVector(db));
193
+ if (method === 'brainSyncV2Changes') return _withDb(method, args, (db, options) => v2().changesSince(db, options || {}));
194
+ if (method === 'brainSyncV2Apply') return _withDb(method, args, (db, payload) => v2().applyChanges(db, payload || {}));
195
+ if (method === 'brainSyncV2MemberVector') return _withDb(method, args, (db, payload) => v2().recordMemberVector(db, payload || {}));
196
+ if (method === 'brainSyncV2PruneTombstones') return _withDb(method, args, (db, options) => v2().pruneTombstones(db, options || {}));
197
+ if (method === 'brainSyncV2Digest') return _withDb(method, args, (db, options) => v2().digest(db, options || {}));
198
+ // WP-4 §6.7: the join trio. Same lazy-require shape as the feed above — brain/brain-join.js
199
+ // owns the snapshot/import/status storage half, and this file stays the thin route layer.
200
+ const join = () => require('../brain/brain-join');
201
+ if (method === 'brainSyncV2JoinSnapshot') return _withDb(method, args, (db, options) => join().createJoinSnapshot({ db, ...(options || {}) }));
202
+ if (method === 'brainSyncV2JoinImport') return _withDb(method, args, (db, options) => join().startJoinImport(db, options || {}));
203
+ if (method === 'brainSyncV2JoinStatus') return _withDb(method, args, (db) => join().joinStatus(db));
517
204
  throw new Error(`unknown brain sync method: ${method}`);
518
205
  }
519
206
 
@@ -522,89 +209,223 @@ async function callLocalBrain(method, args = []) {
522
209
  // ---------------------------------------------------------------------------
523
210
 
524
211
  function handlesSyncPath(pathname) {
525
- return SYNC_PATHS.has(_string(pathname, 200));
212
+ return !!_v2Route(_string(pathname, 200));
526
213
  }
527
214
 
528
- async function _handleOutbox(req, res, url, env) {
529
- const auth = authorizePeerRequest(req, env);
530
- if (!auth.ok) return _jsonResponse(res, { ok: false, error: auth.error }, auth.status);
531
- let result;
532
- try {
533
- result = await callLocalBrain('brainSyncOutbox', [{
534
- afterSeq: url.searchParams.get('after_seq') || url.searchParams.get('afterSeq') || 0,
535
- limit: url.searchParams.get('limit') || DEFAULT_LIMIT,
536
- }]);
537
- } catch (err) {
538
- return _jsonResponse(res, { ok: false, error: err?.message || String(err) }, 500);
539
- }
540
- if (result == null) return _jsonResponse(res, { ok: false, error: 'brain_unavailable' }, 503);
541
- return _jsonResponse(res, result);
542
- }
215
+ // ---------------------------------------------------------------------------
216
+ // v2 routes (WP-1 §6.4). Thin by design: kill switch → auth → bounded body → owner call.
217
+ // ---------------------------------------------------------------------------
543
218
 
544
- async function _handleApply(req, res, env) {
545
- const auth = authorizePeerRequest(req, env);
546
- if (!auth.ok) return _jsonResponse(res, { ok: false, error: auth.error }, auth.status);
547
- let body;
548
- try {
549
- body = await _readBody(req);
550
- } catch (err) {
551
- const status = err?.code === 'PAYLOAD_TOO_LARGE' ? 413 : 400;
552
- return _jsonResponse(res, { ok: false, error: err?.code === 'PAYLOAD_TOO_LARGE' ? 'payload_too_large' : 'invalid_json' }, status);
219
+ // 400/413 for the two business-rule refusals, 500 otherwise; the code, not the message text,
220
+ // is what the route keys on.
221
+ function _v2ErrorStatus(err) {
222
+ if (err?.code === 'BRAIN_SYNC_SOURCE_REQUIRED') return 400;
223
+ if (err?.code === 'BRAIN_SYNC_ENTRIES_TOO_LARGE') return 413;
224
+ if (err?.code === 'BRAIN_SYNC_TABLE_UNKNOWN') return 400;
225
+ if (err?.code === 'BRAIN_SYNC_MEMBER_REQUIRED') return 400;
226
+ // WP-4 §6.7: the join trio's business-rule refusals — a seed walk still running, a different
227
+ // join already in flight, a join file outside the inbox, a missing joinId.
228
+ if (err?.code === 'JOIN_CLOCK_SEED_PENDING') return 409;
229
+ if (err?.code === 'JOIN_IN_PROGRESS') return 409;
230
+ if (err?.code === 'JOIN_PATH_INVALID') return 400;
231
+ if (err?.code === 'JOIN_ID_REQUIRED') return 400;
232
+ return 500;
233
+ }
234
+
235
+ // Answers and returns null when the request must not proceed; true when it may.
236
+ function _v2Gate(req, res, env) {
237
+ if (!brainSyncEnabled(env)) {
238
+ _jsonResponse(res, { ok: false, error: 'brain_sync_disabled' }, 503);
239
+ return null;
553
240
  }
554
- // The source instance id is trusted only from a bearer-authenticated request.
555
- if (!_string(body.sourceInstanceId || body.source_instance_id, 160).trim()) {
556
- return _jsonResponse(res, { ok: false, error: 'sourceInstanceId_required' }, 400);
241
+ const auth = authorizeV2Request(req, env);
242
+ if (!auth.ok) {
243
+ _jsonResponse(res, { ok: false, error: auth.error }, auth.status);
244
+ return null;
557
245
  }
558
- let result;
246
+ return true;
247
+ }
248
+
249
+ async function _v2Body(req, res, limitBytes) {
559
250
  try {
560
- result = await callLocalBrain('brainSyncApplyEntries', [{ ...body, sourceInstanceId: _string(body.sourceInstanceId || body.source_instance_id, 160).trim() }]);
251
+ return await _readBody(req, limitBytes);
561
252
  } catch (err) {
562
- const status = err?.code === 'BRAIN_SYNC_SOURCE_REQUIRED' ? 400 : 500;
563
- return _jsonResponse(res, { ok: false, error: err?.message || String(err) }, status);
253
+ const tooLarge = err?.code === 'PAYLOAD_TOO_LARGE';
254
+ _jsonResponse(res, { ok: false, error: tooLarge ? 'payload_too_large' : 'invalid_json' }, tooLarge ? 413 : 400);
255
+ return null;
564
256
  }
565
- if (result == null) return _jsonResponse(res, { ok: false, error: 'brain_unavailable' }, 503);
566
- return _jsonResponse(res, result);
567
257
  }
568
258
 
569
- async function _handlePrune(req, res, env) {
570
- const auth = authorizePeerRequest(req, env);
571
- if (!auth.ok) return _jsonResponse(res, { ok: false, error: auth.error }, auth.status);
572
- let body;
259
+ // One call to the owner; a null result means no local writer exists (503), a throw maps through
260
+ // _v2ErrorStatus. `res` is answered either way.
261
+ async function _v2OwnerCall(res, op, args) {
573
262
  try {
574
- body = await _readBody(req, 64 * 1024);
263
+ const result = await callLocalBrain(op, args);
264
+ if (result == null) {
265
+ _jsonResponse(res, { ok: false, error: 'brain_unavailable' }, 503);
266
+ return null;
267
+ }
268
+ return result;
575
269
  } catch (err) {
576
- const status = err?.code === 'PAYLOAD_TOO_LARGE' ? 413 : 400;
577
- return _jsonResponse(res, { ok: false, error: err?.code === 'PAYLOAD_TOO_LARGE' ? 'payload_too_large' : 'invalid_json' }, status);
270
+ _jsonResponse(res, { ok: false, error: err?.message || String(err) }, _v2ErrorStatus(err));
271
+ return null;
578
272
  }
579
- const throughSeq = _parseSeq(body.throughSeq ?? body.through_seq);
273
+ }
274
+
275
+ async function _handleV2Vector(req, res, url, env) {
276
+ if (!_v2Gate(req, res, env)) return true;
277
+ const result = await _v2OwnerCall(res, 'brainSyncV2Vector', []);
278
+ if (result == null) return true;
279
+ return _jsonResponse(res, { ok: true, ...result });
280
+ }
281
+
282
+ async function _handleV2Changes(req, res, url, env) {
283
+ if (!_v2Gate(req, res, env)) return true;
284
+ const body = await _v2Body(req, res, V2_MAX_BODY_BYTES);
285
+ if (body == null) return true;
286
+ const since = body.since && typeof body.since === 'object' && !Array.isArray(body.since) ? body.since : {};
287
+ const result = await _v2OwnerCall(res, 'brainSyncV2Changes', [{ since, limit: body.limit }]);
288
+ if (result == null) return true;
289
+ // §6.4: a node whose WP-0 clock walk has not finished cannot answer a change feed — its
290
+ // clocks are incomplete, so anything it shipped would be a lie. The peer retries.
291
+ if (result.clockSeedDone === false) return _jsonResponse(res, { ok: false, error: 'clock_seed_pending' }, 409);
292
+ return _jsonResponse(res, { ok: true, entries: result.entries, more: result.more });
293
+ }
294
+
295
+ async function _handleV2Apply(req, res, url, env) {
296
+ if (!_v2Gate(req, res, env)) return true;
297
+ const body = await _v2Body(req, res, V2_MAX_BODY_BYTES);
298
+ if (body == null) return true;
299
+ const sourceInstanceId = _string(body.sourceInstanceId || body.source_instance_id, 160).trim();
300
+ if (!sourceInstanceId) return _jsonResponse(res, { ok: false, error: 'sourceInstanceId_required' }, 400);
301
+ const entries = Array.isArray(body.entries) ? body.entries : [];
302
+ if (entries.length > V2_MAX_ENTRIES) return _jsonResponse(res, { ok: false, error: 'too_many_entries' }, 413);
303
+ // D-5 membership marker: the hub's round (via the member's CTM front) names itself in this header;
304
+ // a Wall-E that has been synced BY a home is a member and stops running derived producers.
305
+ const memberOf = _string(req?.headers?.['x-ctm-memory-sync-home'], 160).trim();
306
+ const result = await _v2OwnerCall(res, 'brainSyncV2Apply', [{ sourceInstanceId, entries, ...(memberOf ? { memberOf } : {}) }]);
307
+ if (result == null) return true;
308
+ return _jsonResponse(res, { ok: true, ...result });
309
+ }
310
+
311
+ async function _handleV2MemberVector(req, res, url, env) {
312
+ if (!_v2Gate(req, res, env)) return true;
313
+ const body = await _v2Body(req, res, V2_MAX_BODY_BYTES);
314
+ if (body == null) return true;
315
+ const result = await _v2OwnerCall(res, 'brainSyncV2MemberVector', [{
316
+ memberInstanceId: _string(body.memberInstanceId || body.member_instance_id, 160).trim(),
317
+ vector: body.vector && typeof body.vector === 'object' && !Array.isArray(body.vector) ? body.vector : {},
318
+ }]);
319
+ if (result == null) return true;
320
+ return _jsonResponse(res, { ok: true, ...result });
321
+ }
322
+
323
+ async function _handleV2PruneTombstones(req, res, url, env) {
324
+ if (!_v2Gate(req, res, env)) return true;
325
+ const body = await _v2Body(req, res, V2_MAX_BODY_BYTES);
326
+ if (body == null) return true;
327
+ const result = await _v2OwnerCall(res, 'brainSyncV2PruneTombstones', [{ rejoinAfterDays: body.rejoinAfterDays }]);
328
+ if (result == null) return true;
329
+ return _jsonResponse(res, { ok: true, ...result });
330
+ }
331
+
332
+ async function _handleV2Digest(req, res, url, env) {
333
+ if (!_v2Gate(req, res, env)) return true;
334
+ const table = _string(url?.searchParams?.get('table') || 'memories', 40).trim();
335
+ const result = await _v2OwnerCall(res, 'brainSyncV2Digest', [{ table }]);
336
+ if (result == null) return true;
337
+ return _jsonResponse(res, { ok: true, ...result });
338
+ }
339
+
340
+ // WP-4 §6.7: the join trio. The home's snapshot runs where the backup API runs (the owner), and
341
+ // the member's import is only RECORDED here — the brain-join-import job drives the pages.
342
+ async function _handleV2JoinSnapshot(req, res, url, env) {
343
+ if (!_v2Gate(req, res, env)) return true;
344
+ // R-4/R-5 (§6.4): `{ fresh: true }` asks the home for a NEW snapshot instead of the one inside
345
+ // the 6 h reuse window — the member could not resume it. An empty body (a v1 caller, or a home
346
+ // that has nothing to retry) keeps today's reuse behavior.
347
+ const body = await _v2Body(req, res, V2_MAX_BODY_BYTES);
348
+ if (body == null) return true;
349
+ const result = await _v2OwnerCall(res, 'brainSyncV2JoinSnapshot', [{ fresh: body.fresh === true }]);
350
+ if (result == null) return true;
351
+ return _jsonResponse(res, { ok: true, ...result });
352
+ }
353
+
354
+ // R-6 (§5): the member's join file must sit in CTM's inbox, and ONLY this process knows where
355
+ // that is — the owner worker's env is a whitelist that drops CTM_DATA_DIR (I-5). So the DAEMON
356
+ // resolves CTM's data dir here and hands the root down as `dataDir`; the owner process env is
357
+ // never widened, and the path check itself is unchanged.
358
+ function _joinImportOptions(body, env) {
359
+ return {
360
+ joinId: _string(body.joinId || body.join_id, 200).trim(),
361
+ path: _string(body.path, 4096).trim(),
362
+ sha256: _string(body.sha256, 128).trim(),
363
+ homeInstanceId: _string(body.homeInstanceId || body.home_instance_id, 160).trim(),
364
+ dataDir: resolveCtmDataDir(env || process.env),
365
+ };
366
+ }
367
+
368
+ async function _handleV2JoinImport(req, res, url, env) {
369
+ if (!_v2Gate(req, res, env)) return true;
370
+ const body = await _v2Body(req, res, V2_MAX_BODY_BYTES);
371
+ if (body == null) return true;
580
372
  let result;
581
373
  try {
582
- result = await callLocalBrain('brainSyncPrune', [{ throughSeq }]);
374
+ result = await callLocalBrain('brainSyncV2JoinImport', [_joinImportOptions(body, env)]);
583
375
  } catch (err) {
584
- return _jsonResponse(res, { ok: false, error: err?.message || String(err) }, 500);
376
+ // §6.7: a different join already in flight answers 409 with the id that holds it.
377
+ if (err?.code === 'JOIN_IN_PROGRESS') {
378
+ return _jsonResponse(res, { ok: false, error: 'join_in_progress', joinId: _string(err.joinId, 200) || null }, 409);
379
+ }
380
+ return _jsonResponse(res, { ok: false, error: err?.message || String(err) }, _v2ErrorStatus(err));
585
381
  }
586
382
  if (result == null) return _jsonResponse(res, { ok: false, error: 'brain_unavailable' }, 503);
587
- return _jsonResponse(res, result);
383
+ // 202: accepted and recorded, not yet imported (the job does the pages). R-4 (§6.4): a record
384
+ // this call RESUMED at its stored cursor reports `resumed: true`, so the hub knows it is
385
+ // continuing an existing import rather than starting a new one.
386
+ return _jsonResponse(res, { ok: true, state: result.state, ...(result.resumed ? { resumed: true } : {}) }, 202);
387
+ }
388
+
389
+ async function _handleV2JoinStatus(req, res, url, env) {
390
+ if (!_v2Gate(req, res, env)) return true;
391
+ const result = await _v2OwnerCall(res, 'brainSyncV2JoinStatus', []);
392
+ if (result == null) return true;
393
+ return _jsonResponse(res, { ok: true, ...result });
394
+ }
395
+
396
+ function _v2Handler(route) {
397
+ switch (route.op) {
398
+ case 'brainSyncV2Vector': return _handleV2Vector;
399
+ case 'brainSyncV2Changes': return _handleV2Changes;
400
+ case 'brainSyncV2Apply': return _handleV2Apply;
401
+ case 'brainSyncV2MemberVector': return _handleV2MemberVector;
402
+ case 'brainSyncV2PruneTombstones': return _handleV2PruneTombstones;
403
+ case 'brainSyncV2Digest': return _handleV2Digest;
404
+ case 'brainSyncV2JoinSnapshot': return _handleV2JoinSnapshot;
405
+ case 'brainSyncV2JoinImport': return _handleV2JoinImport;
406
+ case 'brainSyncV2JoinStatus': return _handleV2JoinStatus;
407
+ default: return null;
408
+ }
588
409
  }
589
410
 
590
411
  // Returns false when the path is not ours (api-walle's routeWalleApi uses that to
591
412
  // keep dispatching); returns true once it has written the response.
592
413
  function handleSyncApi(req, res, url, deps = {}) {
593
414
  const pathname = _string(url?.pathname, 200);
594
- if (!handlesSyncPath(pathname)) return false;
415
+ const route = _v2Route(pathname);
416
+ // WP-6 (migration 105): only the v2 feed is routed here any more; anything else is not ours.
417
+ if (!route) return false;
595
418
  const env = deps.env || process.env;
596
- const isOutbox = pathname.endsWith('/sync/outbox');
597
419
  const method = _string(req?.method, 10).toUpperCase();
598
- if (isOutbox && method !== 'GET') return _jsonResponse(res, { ok: false, error: 'method_not_allowed' }, 405);
599
- if (!isOutbox && method !== 'POST') return _jsonResponse(res, { ok: false, error: 'method_not_allowed' }, 405);
600
- const isPrune = pathname.endsWith('/sync/prune');
601
- const promise = isOutbox ? _handleOutbox(req, res, url, env) : (isPrune ? _handlePrune(req, res, env) : _handleApply(req, res, env));
420
+ if (method !== route.method) return _jsonResponse(res, { ok: false, error: 'method_not_allowed' }, 405);
421
+ const handler = _v2Handler(route);
422
+ const promise = handler(req, res, url, env);
602
423
  promise.catch((err) => {
603
- // REPORT: the handler already tried to answer; a late rejection is logged, not
604
- // swallowed, and must not take the daemon down.
424
+ // REPORT: the handler already answered; a late rejection is logged, not swallowed, and
425
+ // must not take the daemon down.
605
426
  try {
606
427
  if (!res.headersSent) _jsonResponse(res, { ok: false, error: err?.message || String(err) }, 500);
607
- else console.warn(`[sync-api] late handler failure: ${err?.message || err}`);
428
+ else console.warn(`[sync-api] late v2 handler failure: ${err?.message || err}`);
608
429
  } catch {
609
430
  /* expected: the socket is already gone — there is nothing left to answer */
610
431
  }
@@ -613,20 +434,13 @@ function handleSyncApi(req, res, url, deps = {}) {
613
434
  }
614
435
 
615
436
  module.exports = {
616
- SYNC_PATHS,
617
- REPLICATED_TABLES,
618
- APPLY_SENTINEL_TABLE,
437
+ V2_ROUTE_SPECS,
619
438
  handleSyncApi,
620
439
  handlesSyncPath,
621
- authorizePeerRequest,
622
- parsePeerTokenMap,
623
- peerTokens,
624
- syncEnabledByPeers,
625
- listBrainOutbox,
626
- applyBrainSyncEntries,
627
- pruneBrainOutbox,
628
- getBrainSyncHwm,
440
+ authorizeV2Request,
441
+ brainSyncEnabled,
629
442
  callLocalBrain,
630
443
  _setSyncBrainForTests,
631
444
  _setOwnerClientForTests,
445
+ _joinImportOptions,
632
446
  };