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
@@ -0,0 +1,859 @@
1
+ 'use strict';
2
+
3
+ // WP-4 (§5 D-4/D-6/D-7, §6.7) — the JOIN half of shared memory: the home produces ONE
4
+ // consistent snapshot of its brain; the member imports only the four replicated tables (plus
5
+ // their clock rows), never host-local state (D-7).
6
+ //
7
+ // createJoinSnapshot({ db, brainDir, now, fresh }) -> { ok, joinId, path, bytes, sha256, vector, createdAt }
8
+ // startJoinImport(db, { joinId, path, sha256, homeInstanceId }) -> { ok, state, resumed? }
9
+ // (a same-joinId call whose record is a resumable failure re-enters at the stored cursor and
10
+ // answers `resumed: true` — R-1..R-5)
11
+ // joinImportStep(db, { limit, now, _beforePage }) -> { state, table, done, retry? } (one bounded page)
12
+ // joinStatus(db) -> the status route's shape
13
+ //
14
+ // WP-3 (§5 R-11/R-12): a step is bounded by TIME, not only by rows. `_importPage` walks its step in
15
+ // JOIN_CHUNK_ROWS-row chunks and stops after the chunk during which the elapsed time inside the
16
+ // transaction reached JOIN_STEP_TIME_BUDGET_MS, so one hold stays under the brain write lock's
17
+ // WRITE_LOCK_HOLD_BUDGET_MS. A page the lock aborts (hold-budget/busy) is transient: the cursor is
18
+ // unchanged, the record stays 'importing', and the same page runs next tick (bounded retries).
19
+ //
20
+ // The snapshot's own brain_row_clock IS the consistency point (P-1): the vector is read FROM
21
+ // THE SNAPSHOT FILE (readonly), never from the live DB, so a change the home writes during
22
+ // transfer arrives later through the normal v2 feed (P-2). The import runs on the member's
23
+ // OWNER connection with the connection-scoped TEMP sentinel on, so nothing it writes is
24
+ // captured as a local edit and no host-local table (kv_store, loop_checkpoints, owner, …) is
25
+ // ever read from the snapshot.
26
+ //
27
+ // The cursor and the imported/rekeyed counters live in kv_store under 'brain_sync.join' — one
28
+ // host-local record, self-describing, resumable after a crash. This module owns NO DDL: the
29
+ // sentinel's CREATE/DROP TEMP TABLE text is brain/schema.js createBrainSyncSentinel /
30
+ // dropBrainSyncSentinel.
31
+
32
+ const crypto = require('node:crypto');
33
+ const fs = require('node:fs');
34
+ const os = require('node:os');
35
+ const path = require('node:path');
36
+ const Database = require('better-sqlite3');
37
+
38
+ const { createBrainSyncSentinel, dropBrainSyncSentinel } = require('./schema');
39
+ const { clockSeedState, localBrainInstanceId } = require('./brain-sync-store');
40
+ const { markMembership } = require('./brain-sync-v2');
41
+ const { WRITE_LOCK_HOLD_BUDGET_MS } = require('../shared/sqlite-write-lock');
42
+ const { strFlag } = require('../lib/env-flags');
43
+ const errorSink = require('../lib/error-sink');
44
+
45
+ const JOIN_KEY = 'brain_sync.join';
46
+ const JOIN_DIR_NAME = 'sync-join';
47
+ const INBOX_DIR_NAME = 'sync-inbox';
48
+ // The snapshot's four tables, in import order (matches the clock-seed / backfill walk).
49
+ const JOIN_TABLE_ORDER = Object.freeze(['entities', 'people', 'knowledge', 'memories']);
50
+ // The import CURSOR order (WP-0 §5 R-2): the four tables plus a `knowledge_links` pass between
51
+ // knowledge and memories. `knowledge_links` is a cursor POSITION, not a table — it restores the
52
+ // superseded_by pointers the knowledge pages deliberately insert as NULL (R-1).
53
+ const JOIN_STEP_ORDER = Object.freeze(['entities', 'people', 'knowledge', 'knowledge_links', 'memories']);
54
+ const JOIN_STEP_DEFAULT = 5000;
55
+ // R-11 (§5): rows fetched, re-keyed and inserted per chunk. Small enough that one chunk's work is a
56
+ // fraction of the lock budget under any load, large enough to keep the per-chunk statement count low.
57
+ const JOIN_CHUNK_ROWS = 500;
58
+ // R-11: the time a single import transaction may hold the brain write lock. Derived from the lock's
59
+ // own budget, never re-typed: a hold stays ≤ budget/3 plus one chunk, so the lock's over-budget
60
+ // abort (which rolls the page back and fails the join) is unreachable in normal operation.
61
+ const JOIN_STEP_TIME_BUDGET_MS = Math.floor(WRITE_LOCK_HOLD_BUDGET_MS / 3);
62
+ // R-12: consecutive transient (hold-budget / busy) aborts of the SAME cursor before the join fails.
63
+ const JOIN_MAX_TRANSIENT_RETRIES = 5;
64
+ // R-3 (§6.4): how many times a FAILED join may be resumed under the SAME joinId before the caller
65
+ // has to give up on the snapshot (WP-2 then asks the home for a fresh one). A transient failure
66
+ // costs one backoff instead of a multi-GB re-transfer, and the cap keeps a permanently broken
67
+ // cursor from looping forever.
68
+ const JOIN_MAX_RESUMES = 3;
69
+ // R-2 (§6.4): the two failures a resume can never fix — the snapshot's bytes are wrong, or the
70
+ // file is gone. Both are the exact strings joinImportStep's verifying step stores via _markFailed.
71
+ const NON_RESUMABLE_ERRORS = Object.freeze(new Set(['sha256_mismatch', 'snapshot_missing']));
72
+ // R-12 (§5): the two codes a rolled-back write hold raises. Both leave the committed cursor intact,
73
+ // so the page is safe to retry; anything else is a real failure.
74
+ const TRANSIENT_LOCK_CODES = Object.freeze(new Set(['SQLITE_WRITE_LOCK_HOLD_BUDGET', 'SQLITE_BUSY']));
75
+ const REUSE_WINDOW_MS = 6 * 60 * 60 * 1000; // a second call within 6 h reuses the newest snapshot
76
+ const RETENTION_MS = 48 * 60 * 60 * 1000; // sync-join/*.db older than this is swept on every call
77
+ const MAX_ERROR_LEN = 500;
78
+ const TERMINAL_STATES = Object.freeze(new Set(['done', 'failed']));
79
+ const EMPTY_IMPORTED = Object.freeze({ memories: 0, knowledge: 0, entities: 0, people: 0 });
80
+ // D-7 / §6.7: an imported memory is visible but not re-extracted here — the home's extraction
81
+ // produced its knowledge rows, and this host's read stamps/retry counters describe ITS work.
82
+ // The inserted rows get these values (the SELECT below), never the snapshot's.
83
+ const MEMORY_INSERT_OVERRIDES = Object.freeze({
84
+ extraction_status: "'replicated'",
85
+ extraction_attempts: '0',
86
+ last_accessed: 'NULL',
87
+ access_count: '0',
88
+ embed_failed_count: '0',
89
+ });
90
+ // R-1 (§5): a snapshot knowledge row's superseded_by points FORWARD — at a row with a LARGER
91
+ // rowid — because a supersede chain is written oldest-first (34,244 of the live hub's 312,638
92
+ // knowledge rows, every one of them forward, none dangling). The knowledge page therefore inserts
93
+ // the pointer as NULL; the later `knowledge_links` step restores it once every row is present.
94
+ // FKs stay ON — the two passes are each individually valid (§3, P-1).
95
+ const KNOWLEDGE_INSERT_OVERRIDES = Object.freeze({ superseded_by: 'NULL' });
96
+ // Per-table column overrides applied by _insertSnapshotPage.
97
+ const INSERT_OVERRIDES = Object.freeze({
98
+ memories: MEMORY_INSERT_OVERRIDES,
99
+ knowledge: KNOWLEDGE_INSERT_OVERRIDES,
100
+ });
101
+
102
+ function _string(value, max = 4000) {
103
+ if (typeof value === 'string') return value.slice(0, max);
104
+ return value == null ? '' : String(value).slice(0, max);
105
+ }
106
+
107
+ function _coded(message, code) {
108
+ const error = new Error(message);
109
+ error.code = code;
110
+ return error;
111
+ }
112
+
113
+ // Wall-E's own data dir — lib/env-flags.js is the one sanctioned env reader (flag-read lint).
114
+ // Mirrors brain.js's DATA_DIR (os.homedir(), not process.env.HOME, which is unset under launchd).
115
+ function _dataDir() {
116
+ const configured = _string(strFlag('WALL_E_DATA_DIR', ''), 4096).trim();
117
+ if (configured) return path.resolve(configured);
118
+ return path.join(os.homedir(), '.walle', 'data');
119
+ }
120
+
121
+ function _resolveBrainDir(brainDir) {
122
+ const given = _string(brainDir, 4096).trim();
123
+ if (given) return path.resolve(given);
124
+ return path.dirname(require('../brain').getDbPath());
125
+ }
126
+
127
+ function _resolveDataDir(dataDir) {
128
+ const given = _string(dataDir, 4096).trim();
129
+ return given ? path.resolve(given) : _dataDir();
130
+ }
131
+
132
+ // §6.7/the wire contract: the member's join file must live in the CTM data dir's sync-inbox/ — the
133
+ // daemon passes that root down as `dataDir` (R-6/I-5: the owner worker's env whitelist cannot see
134
+ // CTM_DATA_DIR) — or beside the brain file's own sync-inbox/. Resolve both sides before comparing
135
+ // (a symlinked temp path must not pass by spelling alone).
136
+ function isInsideJoinInbox(filePath, { brainDir, dataDir } = {}) {
137
+ const resolved = path.resolve(_string(filePath, 4096));
138
+ const dirs = [
139
+ path.join(_resolveBrainDir(brainDir), INBOX_DIR_NAME),
140
+ path.join(_resolveDataDir(dataDir), INBOX_DIR_NAME),
141
+ ];
142
+ return dirs.some((dir) => resolved !== dir && resolved.startsWith(`${dir}${path.sep}`));
143
+ }
144
+
145
+ function _sha256File(filePath) {
146
+ return new Promise((resolve, reject) => {
147
+ const hash = crypto.createHash('sha256');
148
+ const stream = fs.createReadStream(filePath);
149
+ stream.on('error', reject);
150
+ stream.on('data', (chunk) => hash.update(chunk));
151
+ stream.on('end', () => resolve(hash.digest('hex')));
152
+ });
153
+ }
154
+
155
+ // The snapshot's vector, read FROM THE SNAPSHOT FILE (P-1) — never from the live DB. origin ''
156
+ // ("this host" before v2 identity, or a v1 row) is not addressable and is excluded (§6.4).
157
+ function vectorFromSnapshotFile(filePath) {
158
+ const d = new Database(filePath, { readonly: true, fileMustExist: true });
159
+ try {
160
+ const vector = {};
161
+ const rows = d.prepare(`
162
+ SELECT origin_instance_id AS origin, MAX(origin_seq) AS max_seq
163
+ FROM brain_row_clock
164
+ WHERE origin_instance_id <> ''
165
+ GROUP BY origin_instance_id
166
+ ORDER BY origin_instance_id ASC
167
+ `).all();
168
+ for (const row of rows) vector[String(row.origin)] = Number(row.max_seq) || 0;
169
+ return vector;
170
+ } finally {
171
+ try { d.close(); } catch { /* expected: the snapshot handle is only read from; a close failure must not mask the vector */ }
172
+ }
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Read/write of the one host-local join record (kv_store; no capture trigger exists on kv_store)
177
+ // ---------------------------------------------------------------------------
178
+
179
+ function readJoinRecord(db) {
180
+ const row = db.prepare('SELECT value FROM kv_store WHERE key = ?').get(JOIN_KEY);
181
+ if (!row || !row.value) return null;
182
+ try {
183
+ const parsed = JSON.parse(row.value);
184
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
185
+ } catch {
186
+ return null; // HANDLE: a corrupt join record reads as "no join" — the next start overwrites it
187
+ }
188
+ }
189
+
190
+ function _writeJoinRecord(db, record) {
191
+ db.prepare(`
192
+ INSERT INTO kv_store (key, value, updated_at) VALUES (?, ?, datetime('now'))
193
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at
194
+ `).run(JOIN_KEY, JSON.stringify(record));
195
+ }
196
+
197
+ function _normalizeRecord(record) {
198
+ const imported = {};
199
+ for (const table of Object.keys(EMPTY_IMPORTED)) imported[table] = Math.max(0, Number(record.imported?.[table]) || 0);
200
+ // `knowledge_links` is a valid cursor position alongside the four tables (R-2); anything else
201
+ // normalizes to null, which _importPage reads as "start at the first step".
202
+ const table = _string(record.table, 40);
203
+ return {
204
+ joinId: _string(record.joinId, 200) || null,
205
+ path: _string(record.path, 4096) || null,
206
+ sha256: _string(record.sha256, 128) || null,
207
+ homeInstanceId: _string(record.homeInstanceId, 160) || null,
208
+ state: _string(record.state, 40) || 'none',
209
+ table: JOIN_STEP_ORDER.includes(table) ? table : null,
210
+ after: Math.max(0, Math.floor(Number(record.after) || 0)),
211
+ imported,
212
+ rekeyed: Math.max(0, Number(record.rekeyed) || 0),
213
+ linksDropped: Math.max(0, Number(record.linksDropped) || 0),
214
+ error: record.error ? _string(record.error, MAX_ERROR_LEN) : null,
215
+ startedAt: _string(record.startedAt, 64) || null,
216
+ finishedAt: _string(record.finishedAt, 64) || null,
217
+ // R-12: consecutive transient lock aborts of the SAME (table, after) cursor, the cursor they
218
+ // happened at, and the last message — so a load spike is visible in the record, not just a tick.
219
+ transientRetries: Math.max(0, Math.floor(Number(record.transientRetries) || 0)),
220
+ lastTransientAt: record.lastTransientAt ? _string(record.lastTransientAt, 120) : null,
221
+ lastTransientError: record.lastTransientError ? _string(record.lastTransientError, MAX_ERROR_LEN) : null,
222
+ // R-3 (§6.4): how many times this joinId has been resumed after a failure, and when the last
223
+ // resume happened — both survive in the record so the cap and the audit trail are durable.
224
+ resumes: Math.max(0, Math.floor(Number(record.resumes) || 0)),
225
+ resumedAt: record.resumedAt ? _string(record.resumedAt, 64) : null,
226
+ };
227
+ }
228
+
229
+ // ---------------------------------------------------------------------------
230
+ // Home: create (or reuse) the join snapshot
231
+ // ---------------------------------------------------------------------------
232
+
233
+ function _joinFileCreatedAtMs(joinId) {
234
+ const match = /-(\d+)$/.exec(String(joinId || ''));
235
+ return match ? Number(match[1]) : 0;
236
+ }
237
+
238
+ function _pruneJoinDir(dir, now) {
239
+ let names;
240
+ try { names = fs.readdirSync(dir); } catch { return 0; } // HANDLE: the dir is created by the caller; an unreadable dir prunes nothing
241
+ let removed = 0;
242
+ for (const name of names) {
243
+ if (!name.endsWith('.db')) continue;
244
+ const full = path.join(dir, name);
245
+ try {
246
+ if (now - fs.statSync(full).mtimeMs > RETENTION_MS) { fs.unlinkSync(full); removed += 1; }
247
+ } catch { /* expected: a file another process is sweeping at the same moment is already gone */ }
248
+ }
249
+ return removed;
250
+ }
251
+
252
+ // Newest snapshot THIS instance produced (its name carries the local instance id) whose mtime is
253
+ // inside the reuse window. Reuse is what makes a repeated call within 6 h cheap and idempotent.
254
+ function _reusableJoinFile(dir, localId, now) {
255
+ if (!localId) return null;
256
+ let names;
257
+ try { names = fs.readdirSync(dir); } catch { return null; } // HANDLE: no dir yet — nothing to reuse
258
+ const prefix = `brain-${localId}-`;
259
+ let best = null;
260
+ for (const name of names) {
261
+ if (!name.startsWith(prefix) || !name.endsWith('.db')) continue;
262
+ const joinId = name.slice('brain-'.length, name.length - '.db'.length);
263
+ const full = path.join(dir, name);
264
+ let mtimeMs;
265
+ try { mtimeMs = fs.statSync(full).mtimeMs; } catch { continue; } // HANDLE: a file that vanished between readdir and stat is not reusable
266
+ if (now - mtimeMs > REUSE_WINDOW_MS) continue;
267
+ if (!best || mtimeMs > best.mtimeMs) best = { path: full, joinId, mtimeMs };
268
+ }
269
+ if (!best) return null;
270
+ return { path: best.path, joinId: best.joinId, createdAt: new Date(_joinFileCreatedAtMs(best.joinId) || best.mtimeMs).toISOString() };
271
+ }
272
+
273
+ // Concurrent calls share ONE in-flight build (§6.7): the second caller awaits the same promise.
274
+ let _inFlightSnapshot = null;
275
+
276
+ async function createJoinSnapshot({ db, brainDir, now, fresh = false } = {}) {
277
+ const handle = db || require('../brain').getDb();
278
+ const seed = clockSeedState(handle);
279
+ if (!seed.done) throw _coded('clock_seed_pending', 'JOIN_CLOCK_SEED_PENDING');
280
+ if (_inFlightSnapshot) return _inFlightSnapshot;
281
+ const build = _buildJoinSnapshot({
282
+ db: handle,
283
+ brainDir: _resolveBrainDir(brainDir),
284
+ now: Number(now) || Date.now(),
285
+ fresh: fresh === true,
286
+ });
287
+ _inFlightSnapshot = build.finally(() => { _inFlightSnapshot = null; });
288
+ return _inFlightSnapshot;
289
+ }
290
+
291
+ async function _buildJoinSnapshot({ db, brainDir, now, fresh = false }) {
292
+ const dir = path.join(brainDir, JOIN_DIR_NAME);
293
+ fs.mkdirSync(dir, { recursive: true });
294
+ _pruneJoinDir(dir, now);
295
+ const localId = localBrainInstanceId(db);
296
+
297
+ // R-4/R-5 (§6.4): the caller whose member could not resume asks for a NEW snapshot, because the
298
+ // one inside the reuse window is the one the member just rejected. An in-flight build is still
299
+ // shared (`_inFlightSnapshot`) — the build it awaits is new by construction.
300
+ const reused = fresh ? null : _reusableJoinFile(dir, localId, now);
301
+ let joinId;
302
+ let filePath;
303
+ let createdAt;
304
+ if (reused) {
305
+ ({ joinId, path: filePath, createdAt } = reused);
306
+ } else {
307
+ createdAt = new Date(now).toISOString();
308
+ joinId = `${localId}-${now}`;
309
+ filePath = path.join(dir, `brain-${joinId}.db`);
310
+ await require('../brain').createConsistentSnapshot(filePath);
311
+ }
312
+
313
+ const bytes = fs.statSync(filePath).size;
314
+ const sha256 = await _sha256File(filePath);
315
+ const vector = vectorFromSnapshotFile(filePath);
316
+ return { ok: true, joinId, path: filePath, bytes, sha256, vector, createdAt };
317
+ }
318
+
319
+ // ---------------------------------------------------------------------------
320
+ // Member: record, then one bounded import page per call
321
+ // ---------------------------------------------------------------------------
322
+
323
+ // The inbox file names the member may reclaim: CTM's snapshot name and its two in-progress
324
+ // spellings (`join-<joinId>.db`, `join-<joinId>.part`, `join-<joinId>.part.json`).
325
+ const JOIN_INBOX_NAME_RE = /^join-.*\.(?:db|part|part\.json)$/;
326
+
327
+ // R-8 (§5/D-5): reclaim the superseded inbox files. When startJoinImport ACCEPTS a new join file
328
+ // (`keepPath`), every other regular `join-*.{db,part,part.json}` in `keepPath`'s OWN directory
329
+ // whose mtime is strictly older is deleted. `dir` is the accepted file's directory, already
330
+ // validated by isInsideJoinInbox, so nothing outside an inbox is ever considered; the accepted
331
+ // file itself is never touched. A readdir/stat fault is HANDLE — reclamation must never be a
332
+ // reason to refuse a join. Returns the number of files deleted.
333
+ function _sweepJoinInbox(dir, keepPath) {
334
+ let names;
335
+ try {
336
+ names = fs.readdirSync(dir);
337
+ } catch {
338
+ errorSink.expected('brain-join', 'join-inbox-sweep');
339
+ return 0;
340
+ }
341
+ let keepMtimeMs;
342
+ try {
343
+ keepMtimeMs = fs.statSync(keepPath).mtimeMs;
344
+ } catch {
345
+ // Without the accepted file's mtime there is no honest "older than" — the sweep reclaims
346
+ // nothing; the verifying step reports the file's absence in its own terms.
347
+ errorSink.expected('brain-join', 'join-inbox-sweep');
348
+ return 0;
349
+ }
350
+ const keep = path.resolve(keepPath);
351
+ let deleted = 0;
352
+ for (const name of names) {
353
+ if (!JOIN_INBOX_NAME_RE.test(name)) continue;
354
+ const full = path.join(dir, name);
355
+ if (path.resolve(full) === keep) continue;
356
+ let stat;
357
+ try {
358
+ stat = fs.statSync(full);
359
+ if (!stat.isFile() || stat.mtimeMs >= keepMtimeMs) continue;
360
+ } catch {
361
+ errorSink.expected('brain-join', 'join-inbox-sweep');
362
+ continue;
363
+ }
364
+ _deleteJoinFile(full);
365
+ deleted += 1;
366
+ }
367
+ return deleted;
368
+ }
369
+
370
+ // R-1..R-5 (§6.4): a same-joinId call may only RESUME a failed record. `normalized` is the stored
371
+ // record after normalization and `{ filePath, sha256 }` the incoming call (sha256 already trimmed
372
+ // and lowercased; '' means the caller did not assert one). The snapshot file must still be there —
373
+ // a record whose file vanished has nothing to resume from, and the caller gets the stored `failed`.
374
+ function _isResumableRecord(normalized, { filePath, sha256 } = {}) {
375
+ if (normalized.state !== 'failed') return false;
376
+ if (!normalized.path || path.resolve(normalized.path) !== path.resolve(filePath)) return false;
377
+ if (sha256 && normalized.sha256 !== sha256) return false;
378
+ if (NON_RESUMABLE_ERRORS.has(String(normalized.error || ''))) return false;
379
+ if (normalized.resumes >= JOIN_MAX_RESUMES) return false;
380
+ return fs.existsSync(normalized.path);
381
+ }
382
+
383
+ function startJoinImport(db, options = {}) {
384
+ const joinId = _string(options.joinId, 200).trim();
385
+ if (!joinId) throw _coded('join_id_required', 'JOIN_ID_REQUIRED');
386
+ const filePath = _string(options.path, 4096).trim();
387
+ if (!filePath) throw _coded('join_path_required', 'JOIN_PATH_INVALID');
388
+ const sha256 = _string(options.sha256, 128).trim().toLowerCase();
389
+ const homeInstanceId = _string(options.homeInstanceId, 160).trim();
390
+
391
+ const current = readJoinRecord(db);
392
+ // Idempotent: the same joinId already recorded is answered with its current state — EXCEPT when
393
+ // the record is a resumable failure (R-1..R-5, §6.4). Every import page is idempotent and commits
394
+ // its cursor inside the same IMMEDIATE transaction (`_importPage`), so re-entering at the stored
395
+ // (table, after) is safe and costs one hub backoff instead of a fresh multi-GB snapshot.
396
+ if (current && current.joinId === joinId) {
397
+ const normalized = _normalizeRecord(current);
398
+ if (_isResumableRecord(normalized, { filePath, sha256 })) {
399
+ const resumed = _normalizeRecord({
400
+ ...normalized,
401
+ state: normalized.table ? 'importing' : 'verifying',
402
+ error: null,
403
+ finishedAt: null,
404
+ transientRetries: 0,
405
+ lastTransientAt: null,
406
+ lastTransientError: null,
407
+ resumes: normalized.resumes + 1,
408
+ resumedAt: new Date().toISOString(),
409
+ });
410
+ _writeJoinRecord(db, resumed);
411
+ // A resume is NOT an acceptance: the inbox sweep (R-8) runs only when a NEW file is taken.
412
+ return { ok: true, state: resumed.state, joinId, resumed: true };
413
+ }
414
+ return { ok: true, state: normalized.state, joinId };
415
+ }
416
+ // A DIFFERENT joinId while one is still in flight is refused; a terminal record is replaced.
417
+ if (current && current.joinId && !TERMINAL_STATES.has(current.state)) {
418
+ const error = _coded('join_in_progress', 'JOIN_IN_PROGRESS');
419
+ error.joinId = current.joinId;
420
+ throw error;
421
+ }
422
+ if (!isInsideJoinInbox(filePath, { brainDir: options.brainDir, dataDir: options.dataDir })) {
423
+ throw _coded('join_path_invalid', 'JOIN_PATH_INVALID');
424
+ }
425
+
426
+ // R-7 (§5): replacing a TERMINAL record (done/failed) under a DIFFERENT joinId is the hub
427
+ // retrying after a failure, so the replaced record's stranded snapshot is removed — but only
428
+ // when it sits inside a join inbox and is not the file being started now. A path outside the
429
+ // inbox is never deleted (the member's only sweep is this one). A non-terminal record never
430
+ // reaches here: it already threw join_in_progress above.
431
+ if (current && current.joinId && TERMINAL_STATES.has(current.state)) {
432
+ const replaced = _string(current.path, 4096).trim();
433
+ if (replaced) {
434
+ const resolvedReplaced = path.resolve(replaced);
435
+ if (resolvedReplaced !== path.resolve(filePath)
436
+ && isInsideJoinInbox(resolvedReplaced, { brainDir: options.brainDir, dataDir: options.dataDir })) {
437
+ _deleteJoinFile(resolvedReplaced);
438
+ }
439
+ }
440
+ }
441
+
442
+ // R-8 (§5/D-5, I-4): this is the ONE moment the member knows which inbox file is current —
443
+ // it is about to write a new record — so every OLDER join file beside it is reclaimed here. A
444
+ // refused or superseded transfer used to strand its multi-GB snapshot in the inbox forever;
445
+ // the same-joinId early return above and every refusal path deliberately do not sweep.
446
+ _sweepJoinInbox(path.dirname(path.resolve(filePath)), filePath);
447
+
448
+ const record = _normalizeRecord({
449
+ joinId,
450
+ path: path.resolve(filePath),
451
+ sha256,
452
+ homeInstanceId,
453
+ state: 'verifying',
454
+ table: null,
455
+ after: 0,
456
+ imported: EMPTY_IMPORTED,
457
+ rekeyed: 0,
458
+ error: null,
459
+ startedAt: new Date().toISOString(),
460
+ finishedAt: null,
461
+ });
462
+ _writeJoinRecord(db, record);
463
+ return { ok: true, state: record.state, joinId };
464
+ }
465
+
466
+ // The columns of `qualifiedTable` ("main"."x" / "snap"."x") as SQLite sees them on this connection.
467
+ function _columns(db, qualifiedTable) {
468
+ try {
469
+ return db.prepare(`SELECT * FROM ${qualifiedTable} LIMIT 0`).columns().map((column) => column.name);
470
+ } catch {
471
+ return []; // HANDLE: a missing table (or a schema a migration has not yet reached) has no columns to copy
472
+ }
473
+ }
474
+
475
+ // The snapshot's brain_row_clock rows for a page's ids — INSERT OR REPLACE: the snapshot clock is
476
+ // the truth for those rows (§6.7). Runs with the sentinel on, so it stamps no local origin.
477
+ function _copySnapshotClock(db, table, ids) {
478
+ if (!ids.length) return 0;
479
+ const placeholders = ids.map(() => '?').join(', ');
480
+ const rows = db.prepare(`
481
+ SELECT table_name, row_id, updated_at, origin_instance_id, origin_seq, deleted
482
+ FROM snap.brain_row_clock
483
+ WHERE table_name = ? AND row_id IN (${placeholders})
484
+ `).all(table, ...ids);
485
+ const upsert = db.prepare(`
486
+ INSERT OR REPLACE INTO brain_row_clock (table_name, row_id, updated_at, origin_instance_id, origin_seq, deleted)
487
+ VALUES (?, ?, ?, ?, ?, ?)
488
+ `);
489
+ let copied = 0;
490
+ for (const row of rows) {
491
+ upsert.run(
492
+ String(row.table_name), String(row.row_id), _string(row.updated_at, 64),
493
+ _string(row.origin_instance_id, 160), Number(row.origin_seq) || 0, Number(row.deleted) ? 1 : 0,
494
+ );
495
+ copied += 1;
496
+ }
497
+ return copied;
498
+ }
499
+
500
+ // D-4/§6.7 re-keying: a snapshot memory whose (source, source_id) is already here under a
501
+ // DIFFERENT id replaces it — every local knowledge.source_memory_ids array that names the local
502
+ // id is rewritten to the snapshot id, then the local row and its clock row are deleted (with the
503
+ // sentinel on, so the delete leaves no tombstone). Returns how many rows were re-keyed.
504
+ // WP-0 (§5 R-1): the rewrite is ONE batched knowledge scan for the whole page, not one per
505
+ // collision — the live hub page held 2,792 collisions and 105.5 s of the write lock, while one
506
+ // batched scan of the same rows measures 77 ms. Deletes keep their exact per-collision order.
507
+ function _rekeyCollidingMemories(db, ids) {
508
+ if (!ids.length) return 0;
509
+ const placeholders = ids.map(() => '?').join(', ');
510
+ const snaps = db.prepare('SELECT id, source, source_id FROM snap.memories WHERE id IN (' + placeholders + ')').all(...ids);
511
+ const findLocal = db.prepare('SELECT id FROM memories WHERE source = ? AND source_id = ? AND id <> ? LIMIT 1');
512
+ const deleteMemory = db.prepare('DELETE FROM memories WHERE id = ?');
513
+ const deleteClock = db.prepare('DELETE FROM brain_row_clock WHERE table_name = ? AND row_id = ?');
514
+ const mapping = new Map(); // local id -> snapshot id, both String; insertion order == `snaps` order
515
+ let rekeyed = 0;
516
+ for (const snap of snaps) {
517
+ if (snap.source_id == null || snap.source_id === '') continue; // the identity pair needs a non-null source_id
518
+ const local = findLocal.get(_string(snap.source, 200), _string(snap.source_id, 400), String(snap.id));
519
+ if (!local) continue;
520
+ mapping.set(String(local.id), String(snap.id));
521
+ deleteMemory.run(local.id);
522
+ deleteClock.run('memories', local.id);
523
+ rekeyed += 1;
524
+ }
525
+ if (mapping.size > 0) {
526
+ let chained = false;
527
+ for (const target of mapping.values()) if (mapping.has(target)) { chained = true; break; }
528
+ // K-1 (§11): the batch maps each element ONCE, but the old code composed a chain — a local id
529
+ // that is also a later collision's snapshot id (L1->S1, then L2=S1->S2 left S2). A chain needs
530
+ // one id to name different content on two machines, which per-insert id generation prevents
531
+ // (never observed on the live snapshot); when one appears, replay the pairs one at a time in
532
+ // `snaps` order to reproduce the composed result instead of the single-hop one.
533
+ if (chained) {
534
+ for (const [fromId, toId] of mapping) _replaceKnowledgeMemoryIds(db, new Map([[fromId, toId]]));
535
+ } else {
536
+ _replaceKnowledgeMemoryIds(db, mapping);
537
+ }
538
+ }
539
+ return rekeyed;
540
+ }
541
+
542
+ // R-3/R-4 (§5): one statement finds every main.knowledge row whose source_memory_ids carries one
543
+ // of `mapping`'s local ids, then the JS rewrite applies today's rules. The CASE keeps json_each
544
+ // away from malformed text (P-2) whatever order SQLite evaluates the AND terms in; CAST … AS TEXT
545
+ // keeps today's String(value) equality for numeric elements. Returns how many rows changed.
546
+ function _replaceKnowledgeMemoryIds(db, mapping) {
547
+ if (!mapping.size) return 0;
548
+ const candidates = db.prepare(`
549
+ SELECT k.id, k.source_memory_ids FROM main.knowledge k
550
+ WHERE k.source_memory_ids IS NOT NULL
551
+ AND EXISTS (SELECT 1 FROM json_each(CASE WHEN json_valid(k.source_memory_ids) THEN k.source_memory_ids ELSE '[]' END) j
552
+ WHERE CAST(j.value AS TEXT) IN (SELECT value FROM json_each(?)))
553
+ `).all(JSON.stringify([...mapping.keys()]));
554
+ const update = db.prepare('UPDATE main.knowledge SET source_memory_ids = ? WHERE id = ?');
555
+ let updated = 0;
556
+ for (const row of candidates) {
557
+ let ids;
558
+ try {
559
+ ids = JSON.parse(row.source_memory_ids);
560
+ } catch {
561
+ continue; // HANDLE: an unparseable source_memory_ids is reported by utils/repair.js — this sweep leaves it alone
562
+ }
563
+ if (!Array.isArray(ids)) continue;
564
+ let changed = false;
565
+ const next = ids.map((value) => {
566
+ const key = String(value);
567
+ if (!mapping.has(key)) return value;
568
+ changed = true;
569
+ return mapping.get(key);
570
+ });
571
+ if (changed) {
572
+ update.run(JSON.stringify(next), row.id);
573
+ updated += 1;
574
+ }
575
+ }
576
+ return updated;
577
+ }
578
+
579
+ // INSERT OR IGNORE ... SELECT over the INTERSECTION of the local and snapshot column lists (the two
580
+ // schemas may differ by a migration), restricted to the page's snapshot rowids.
581
+ function _insertSnapshotPage(db, table, page) {
582
+ if (!page.length) return 0;
583
+ const localCols = _columns(db, `main."${table}"`);
584
+ const snapCols = _columns(db, `snap."${table}"`);
585
+ const cols = localCols.filter((column) => snapCols.includes(column));
586
+ if (!cols.length) return 0;
587
+ const overrides = INSERT_OVERRIDES[table];
588
+ const selectExpr = cols.map((column) => {
589
+ if (overrides && overrides[column]) return `${overrides[column]} AS "${column}"`;
590
+ return `"${column}"`;
591
+ });
592
+ const placeholders = page.map(() => '?').join(', ');
593
+ const sql = `INSERT OR IGNORE INTO main."${table}" (${cols.map((c) => `"${c}"`).join(', ')})
594
+ SELECT ${selectExpr.join(', ')} FROM snap."${table}" WHERE rowid IN (${placeholders})`;
595
+ return db.prepare(sql).run(...page.map((row) => row.rid)).changes || 0;
596
+ }
597
+
598
+ // brain_vv := MAX(local, snapshot vector) per origin (P-2). The snapshot vector is read from the
599
+ // file, so a home write made during transfer keeps a higher seq and still travels the normal feed.
600
+ function _mergeVector(db, snapshotPath) {
601
+ const vector = vectorFromSnapshotFile(snapshotPath);
602
+ const upsert = db.prepare(`
603
+ INSERT INTO brain_vv (origin_instance_id, max_seq) VALUES (?, ?)
604
+ ON CONFLICT(origin_instance_id) DO UPDATE SET max_seq = MAX(brain_vv.max_seq, excluded.max_seq)
605
+ `);
606
+ for (const [origin, seq] of Object.entries(vector)) {
607
+ if (!origin) continue;
608
+ upsert.run(_string(origin, 160), Math.max(0, Number(seq) || 0));
609
+ }
610
+ return vector;
611
+ }
612
+
613
+ // R-3/R-4 (§5): one bounded CHUNK of the `knowledge_links` step — the next `budget` snapshot
614
+ // knowledge rows (rowid > after) that CARRY a pointer. For each, main.knowledge.superseded_by is
615
+ // set to the snapshot value only when the main row exists, its pointer IS NULL and the target id
616
+ // exists in main.knowledge. A pointer whose target is missing stays NULL and is counted in
617
+ // record.linksDropped (R-4, the same repair the feed makes). A row whose main id is absent or
618
+ // whose pointer is already non-NULL is neither linked nor dropped (R-5: local truth wins).
619
+ // Runs inside the step's IMMEDIATE transaction with the sentinel on, so no clock/seq/vv is stamped.
620
+ function _linkKnowledgePage(db, record, after, budget) {
621
+ const page = db.prepare(`
622
+ SELECT rowid AS rid, id, superseded_by
623
+ FROM snap."knowledge"
624
+ WHERE rowid > ? AND superseded_by IS NOT NULL
625
+ ORDER BY rowid ASC LIMIT ?
626
+ `).all(after, budget);
627
+ const current = db.prepare('SELECT superseded_by FROM main."knowledge" WHERE id = ?');
628
+ const targetExists = db.prepare('SELECT 1 AS hit FROM main."knowledge" WHERE id = ? LIMIT 1');
629
+ const setPointer = db.prepare('UPDATE main."knowledge" SET superseded_by = ? WHERE id = ?');
630
+ let linked = 0;
631
+ let dropped = 0;
632
+ for (const row of page) {
633
+ const id = row.id == null ? '' : String(row.id);
634
+ const target = row.superseded_by == null ? '' : String(row.superseded_by);
635
+ if (!id || !target) continue;
636
+ const local = current.get(id);
637
+ if (!local) continue; // the main row is absent: nothing to point at (neither linked nor dropped)
638
+ if (local.superseded_by !== null && local.superseded_by !== undefined) continue; // R-5
639
+ if (!targetExists.get(target)) { dropped += 1; continue; } // R-4: dangling pointer stays NULL
640
+ setPointer.run(target, id);
641
+ linked += 1;
642
+ }
643
+ record.linksDropped = (Number(record.linksDropped) || 0) + dropped;
644
+ return { page, linked, dropped };
645
+ }
646
+
647
+ // One bounded page of ONE step, inside the caller's IMMEDIATE transaction. R-11 (§5): the page is
648
+ // walked in JOIN_CHUNK_ROWS-row chunks and stops after the chunk during which the elapsed time
649
+ // reached JOIN_STEP_TIME_BUDGET_MS, so the transaction never outlives the write lock's own budget.
650
+ // The record is mutated in place and written before returning so a crash resumes exactly here.
651
+ function _importPage(db, record, budget, now) {
652
+ const startedAt = now();
653
+ const step = JOIN_STEP_ORDER.includes(record.table) ? record.table : JOIN_STEP_ORDER[0];
654
+ let after = Math.max(0, Math.floor(Number(record.after) || 0));
655
+
656
+ let processed = 0;
657
+ let lastRid = after;
658
+ let exhausted = false; // a chunk came back shorter than asked — the step has no more rows
659
+ while (processed < budget) {
660
+ const chunk = Math.min(JOIN_CHUNK_ROWS, budget - processed);
661
+ let pageLength;
662
+ let chunkLastRid = after;
663
+ if (step === 'knowledge_links') {
664
+ const linked = _linkKnowledgePage(db, record, after, chunk);
665
+ pageLength = linked.page.length;
666
+ if (pageLength) chunkLastRid = Number(linked.page[pageLength - 1].rid) || after;
667
+ } else {
668
+ const page = db.prepare(`SELECT rowid AS rid, id FROM snap."${step}" WHERE rowid > ? ORDER BY rowid ASC LIMIT ?`).all(after, chunk);
669
+ const ids = page.map((row) => row.id).filter((id) => id != null && id !== '').map(String);
670
+ const rekeyed = step === 'memories' ? _rekeyCollidingMemories(db, ids) : 0;
671
+ const inserted = _insertSnapshotPage(db, step, page);
672
+ _copySnapshotClock(db, step, ids);
673
+ record.imported[step] = (Number(record.imported[step]) || 0) + inserted;
674
+ record.rekeyed = (Number(record.rekeyed) || 0) + rekeyed;
675
+ pageLength = page.length;
676
+ if (pageLength) chunkLastRid = Number(page[pageLength - 1].rid) || after;
677
+ }
678
+ processed += pageLength;
679
+ lastRid = chunkLastRid;
680
+ after = chunkLastRid;
681
+ // A short chunk means the step is exhausted; the cursor moves on. A chunk that filled the ask
682
+ // either used the row allowance or ran out of time — both keep the cursor INSIDE this step.
683
+ if (pageLength < chunk) { exhausted = true; break; }
684
+ if (processed >= budget) break;
685
+ if (now() - startedAt >= JOIN_STEP_TIME_BUDGET_MS) break;
686
+ }
687
+
688
+ // R-12: the page committed, so any earlier transient abort is behind us.
689
+ record.transientRetries = 0;
690
+ delete record.lastTransientAt;
691
+ delete record.lastTransientError;
692
+
693
+ const index = JOIN_STEP_ORDER.indexOf(step) + (exhausted ? 1 : 0);
694
+
695
+ if (index >= JOIN_STEP_ORDER.length) {
696
+ _mergeVector(db, record.path);
697
+ markMembership(db, { homeInstanceId: record.homeInstanceId });
698
+ record.state = 'done';
699
+ record.table = null;
700
+ record.after = 0;
701
+ record.finishedAt = new Date().toISOString();
702
+ _writeJoinRecord(db, record);
703
+ return { state: 'done', table: null, done: true, deleteSnapshot: true };
704
+ }
705
+ record.state = 'importing';
706
+ record.table = JOIN_STEP_ORDER[index];
707
+ record.after = exhausted ? 0 : lastRid;
708
+ _writeJoinRecord(db, record);
709
+ return { state: 'importing', table: record.table, done: false, deleteSnapshot: false };
710
+ }
711
+
712
+ function _markFailed(db, record, err) {
713
+ const message = _string(typeof err === 'string' ? err : (err?.message || String(err)), MAX_ERROR_LEN);
714
+ record.state = 'failed';
715
+ record.error = message;
716
+ record.finishedAt = new Date().toISOString();
717
+ try {
718
+ _writeJoinRecord(db, record);
719
+ } catch (writeError) {
720
+ // A locked/broken kv_store must not turn a join failure into a route-level throw: the
721
+ // in-memory record still answers this tick, and the next tick re-reads the same 'failed' row.
722
+ errorSink.report('brain-join', 'join-failure-record-write', writeError);
723
+ }
724
+ return { state: 'failed', table: record.table || null, done: true };
725
+ }
726
+
727
+ // R-12 (§5): a page whose transaction threw a write-lock hold-budget / busy error has ROLLED BACK,
728
+ // so the committed cursor is exactly where it was and the same page runs again next tick. The
729
+ // in-memory record was mutated inside the rolled-back transaction, so re-read the committed record
730
+ // from kv, count the retry against the (table, after) it happened at, and stay 'importing'. A
731
+ // deterministic failure at one cursor still ends the join after JOIN_MAX_TRANSIENT_RETRIES tries.
732
+ function _recordTransientRetry(db, failure) {
733
+ const fresh = _normalizeRecord(readJoinRecord(db) || {});
734
+ const cursor = `${fresh.table == null ? '' : fresh.table}:${fresh.after}`;
735
+ fresh.transientRetries = fresh.lastTransientAt === cursor ? fresh.transientRetries + 1 : 1;
736
+ fresh.lastTransientAt = cursor;
737
+ fresh.lastTransientError = _string(failure?.message || String(failure), MAX_ERROR_LEN);
738
+ fresh.state = 'importing';
739
+ if (fresh.transientRetries >= JOIN_MAX_TRANSIENT_RETRIES) return _markFailed(db, fresh, failure);
740
+ _writeJoinRecord(db, fresh);
741
+ return { state: 'importing', table: fresh.table, done: false, retry: true };
742
+ }
743
+
744
+ // §6.7: attach the snapshot READONLY, so the import can never write to it. The documented shape
745
+ // is the URI form `file:<path>?mode=ro`; a SQLite build with URI filenames disabled for ATTACH
746
+ // (this one — verified: `file:` is only honoured when SQLITE_OPEN_URI is set) rejects it, so fall
747
+ // back to a plain attach. The import only ever SELECTs from `snap`, and the file is deleted once
748
+ // the import reaches 'done'.
749
+ function _attachSnapshot(db, filePath) {
750
+ try {
751
+ db.prepare('ATTACH DATABASE ? AS snap').run(`file:${filePath}?mode=ro`);
752
+ } catch {
753
+ db.prepare('ATTACH DATABASE ? AS snap').run(filePath);
754
+ }
755
+ return true;
756
+ }
757
+
758
+ function _deleteJoinFile(filePath) {
759
+ try {
760
+ fs.rmSync(filePath, { force: true });
761
+ } catch {
762
+ // `force` already covers ENOENT; anything left (EPERM/EBUSY) is benign — a MEMBER inbox file
763
+ // is reclaimed by the R-7 replacement delete or the R-8 inbox sweep on the next accepted join,
764
+ // never by the hub's 48 h sweep of <brainDir>/sync-join (that directory is the home's).
765
+ errorSink.expected('brain-join', 'join-file-unlink');
766
+ }
767
+ }
768
+
769
+ // `now` is the injectable clock used for the R-11 time budget (defaults to the real one); `_beforePage`
770
+ // is a TEST-ONLY seam: called inside the import transaction before the page work, so a test can make
771
+ // one page throw a coded error and exercise the R-12 transient path. Production passes neither.
772
+ async function joinImportStep(db, { limit = JOIN_STEP_DEFAULT, now = Date.now, _beforePage } = {}) {
773
+ const record = readJoinRecord(db);
774
+ if (!record) return { state: 'none', table: null, done: true };
775
+ if (TERMINAL_STATES.has(record.state)) return { state: record.state, table: record.table || null, done: true };
776
+
777
+ const budget = Math.max(1, Math.floor(Number(limit) || 0) || JOIN_STEP_DEFAULT);
778
+
779
+ // verifying: the hash is a READ of the file — never inside the import transaction.
780
+ if (record.state === 'verifying') {
781
+ if (!fs.existsSync(record.path)) return _markFailed(db, record, 'snapshot_missing');
782
+ const actual = await _sha256File(record.path);
783
+ if (record.sha256 && actual !== record.sha256) return _markFailed(db, record, 'sha256_mismatch');
784
+ record.state = 'importing';
785
+ record.table = JOIN_STEP_ORDER[0];
786
+ record.after = 0;
787
+ _writeJoinRecord(db, record);
788
+ return { state: 'importing', table: record.table, done: false };
789
+ }
790
+
791
+ // importing: ATTACH the snapshot readonly, sentinel on, ONE IMMEDIATE transaction for the page.
792
+ let attached = false;
793
+ let outcome = null;
794
+ let failure = null;
795
+ createBrainSyncSentinel(db);
796
+ try {
797
+ attached = _attachSnapshot(db, record.path);
798
+ outcome = db.transaction(() => {
799
+ if (typeof _beforePage === 'function') _beforePage(record);
800
+ return _importPage(db, record, budget, now);
801
+ }).immediate();
802
+ } catch (err) {
803
+ failure = err;
804
+ } finally {
805
+ if (attached) {
806
+ try { db.exec('DETACH DATABASE snap'); } catch { /* expected: a rolled-back page may already have released the attachment; the failure is reported below */ }
807
+ }
808
+ dropBrainSyncSentinel(db);
809
+ }
810
+ if (failure) {
811
+ if (TRANSIENT_LOCK_CODES.has(failure.code)) return _recordTransientRetry(db, failure);
812
+ return _markFailed(db, record, failure);
813
+ }
814
+ if (outcome.deleteSnapshot) _deleteJoinFile(record.path);
815
+ return { state: outcome.state, table: outcome.table, done: outcome.done };
816
+ }
817
+
818
+ function joinStatus(db) {
819
+ const record = readJoinRecord(db);
820
+ if (!record) {
821
+ return { ok: true, joinId: null, state: 'none', table: null, imported: { ...EMPTY_IMPORTED }, rekeyed: 0, linksDropped: 0, embeddings: 'skipped', error: null, transientRetries: 0, resumes: 0, resumedAt: null };
822
+ }
823
+ const normalized = _normalizeRecord(record);
824
+ return {
825
+ ok: true,
826
+ joinId: normalized.joinId,
827
+ state: normalized.state,
828
+ table: normalized.table,
829
+ imported: normalized.imported,
830
+ rekeyed: normalized.rekeyed,
831
+ linksDropped: normalized.linksDropped,
832
+ embeddings: 'skipped',
833
+ error: normalized.error,
834
+ // R-12: visible so a retry loop under load is diagnosable from the status route.
835
+ transientRetries: normalized.transientRetries,
836
+ // R-3 (§6.4): the resume budget's state, so a member stuck at the cap is visible from outside.
837
+ resumes: normalized.resumes,
838
+ resumedAt: normalized.resumedAt,
839
+ };
840
+ }
841
+
842
+ module.exports = {
843
+ JOIN_KEY,
844
+ JOIN_TABLE_ORDER,
845
+ JOIN_STEP_ORDER,
846
+ JOIN_STEP_DEFAULT,
847
+ JOIN_CHUNK_ROWS,
848
+ JOIN_STEP_TIME_BUDGET_MS,
849
+ JOIN_MAX_TRANSIENT_RETRIES,
850
+ JOIN_MAX_RESUMES,
851
+ EMPTY_IMPORTED,
852
+ readJoinRecord,
853
+ isInsideJoinInbox,
854
+ vectorFromSnapshotFile,
855
+ createJoinSnapshot,
856
+ startJoinImport,
857
+ joinImportStep,
858
+ joinStatus,
859
+ };