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.
- package/README.md +3 -1
- package/lib/release-schema.json +2 -2
- package/lib/runtime-closure-policy.json +8 -7
- package/package.json +1 -1
- package/template/coding-task-manager/api-prompts.js +45 -7
- package/template/coding-task-manager/bin/claude-hook.js +55 -1
- package/template/coding-task-manager/bin/ctm-relay-client.js +35 -12
- package/template/coding-task-manager/bin/land.js +2069 -0
- package/template/coding-task-manager/bin/memory-census.js +4 -2
- package/template/coding-task-manager/bin/memory-sync-verify.js +151 -0
- package/template/coding-task-manager/bin/probe-refusal-scope.js +530 -0
- package/template/coding-task-manager/db.js +156 -32
- package/template/coding-task-manager/lib/acp/acp-client.js +383 -0
- package/template/coding-task-manager/lib/acp/dsh-context-truth.js +176 -0
- package/template/coding-task-manager/lib/acp/dsh-launch-model.js +302 -0
- package/template/coding-task-manager/lib/acp/dsh-mcp-servers.js +194 -0
- package/template/coding-task-manager/lib/acp/dsh-prompt-lane.js +616 -0
- package/template/coding-task-manager/lib/acp/dsh-route-patch.js +308 -0
- package/template/coding-task-manager/lib/acp/dsh-session-runtime.js +522 -0
- package/template/coding-task-manager/lib/acp/dsh-transcript.js +395 -0
- package/template/coding-task-manager/lib/acp/dsh-usage-log.js +162 -0
- package/template/coding-task-manager/lib/acp/ndjson-stdio-transport.js +225 -0
- package/template/coding-task-manager/lib/adopted-host-redial.js +47 -11
- package/template/coding-task-manager/lib/adopted-launch-environment.js +123 -0
- package/template/coding-task-manager/lib/agent-auto-update-runtime.js +0 -3
- package/template/coding-task-manager/lib/agent-auto-update.js +26 -10
- package/template/coding-task-manager/lib/agent-bundle/build.js +657 -0
- package/template/coding-task-manager/lib/agent-bundle/codex-install.js +558 -0
- package/template/coding-task-manager/lib/agent-bundle/drift.js +120 -0
- package/template/coding-task-manager/lib/agent-bundle/gc.js +522 -0
- package/template/coding-task-manager/lib/agent-bundle/inventory.js +346 -0
- package/template/coding-task-manager/lib/agent-bundle/push.js +717 -0
- package/template/coding-task-manager/lib/agent-bundle/skill-transfer.js +724 -0
- package/template/coding-task-manager/lib/agent-bundle/spawn.js +119 -0
- package/template/coding-task-manager/lib/agent-bundle/store.js +568 -0
- package/template/coding-task-manager/lib/agent-capabilities.js +142 -12
- package/template/coding-task-manager/lib/agent-cli-cache.js +30 -5
- package/template/coding-task-manager/lib/agent-definitions.js +19 -0
- package/template/coding-task-manager/lib/agent-hooks-codex-toml.js +28 -16
- package/template/coding-task-manager/lib/agent-hooks-codex-workspace.js +9 -1
- package/template/coding-task-manager/lib/agent-hooks-installer.js +23 -8
- package/template/coding-task-manager/lib/agent-hooks-prune.js +37 -11
- package/template/coding-task-manager/lib/agent-hooks-shim.js +105 -0
- package/template/coding-task-manager/lib/agent-presets.js +84 -1
- package/template/coding-task-manager/lib/agent-upgrade-jobs.js +12 -1
- package/template/coding-task-manager/lib/agent-version-service.js +74 -11
- package/template/coding-task-manager/lib/all-sessions-query.js +6 -2
- package/template/coding-task-manager/lib/api/agent-bundles.js +436 -0
- package/template/coding-task-manager/lib/api/backup-retention.js +83 -0
- package/template/coding-task-manager/lib/api/conversation-sync-routes.js +124 -0
- package/template/coding-task-manager/lib/api/diagnostics.js +549 -69
- package/template/coding-task-manager/lib/api/fleet.js +118 -0
- package/template/coding-task-manager/lib/api/hub-bridge.js +243 -0
- package/template/coding-task-manager/lib/api/ingress.js +9 -1
- package/template/coding-task-manager/lib/api/landing.js +403 -0
- package/template/coding-task-manager/lib/api/machines.js +691 -2
- package/template/coding-task-manager/lib/api/memory-sync-routes.js +335 -0
- package/template/coding-task-manager/lib/api/misc-ops.js +8 -3
- package/template/coding-task-manager/lib/api/remote-sessions.js +1326 -0
- package/template/coding-task-manager/lib/api/session-attach.js +623 -0
- package/template/coding-task-manager/lib/api/session-messages.js +1384 -0
- package/template/coding-task-manager/lib/api/sessions.js +77 -51
- package/template/coding-task-manager/lib/api/setup-network.js +544 -16
- package/template/coding-task-manager/lib/api/setup-wizard-handlers.js +884 -0
- package/template/coding-task-manager/lib/api/setup.js +829 -10
- package/template/coding-task-manager/lib/api/walle-bridge.js +9 -1
- package/template/coding-task-manager/lib/attachment-reconciler-wiring.js +5 -2
- package/template/coding-task-manager/lib/auth-rate-limit.js +23 -0
- package/template/coding-task-manager/lib/auth-rules.js +292 -21
- package/template/coding-task-manager/lib/background-stop.js +369 -0
- package/template/coding-task-manager/lib/best-of-attempts.js +4 -1
- package/template/coding-task-manager/lib/best-of-launch-plan.js +52 -0
- package/template/coding-task-manager/lib/box-keep-awake.js +89 -0
- package/template/coding-task-manager/lib/broadcast-reason-stats.js +152 -0
- package/template/coding-task-manager/lib/broadcast-serialize.js +5 -0
- package/template/coding-task-manager/lib/build-info.js +4 -2
- package/template/coding-task-manager/lib/claude-model-truth.js +13 -2
- package/template/coding-task-manager/lib/claude-transcript-terminal-renderer.js +17 -13
- package/template/coding-task-manager/lib/client-diag-handler.js +64 -2
- package/template/coding-task-manager/lib/codex-app-server-client.js +14 -16
- package/template/coding-task-manager/lib/codex-configuration-history.js +55 -0
- package/template/coding-task-manager/lib/codex-harness-qualification.js +43 -20
- package/template/coding-task-manager/lib/codex-rollout-history.js +743 -0
- package/template/coding-task-manager/lib/coding-agent-models.js +27 -12
- package/template/coding-task-manager/lib/coding-cli-skill-installer.js +44 -14
- package/template/coding-task-manager/lib/coding-session-model-adapters.js +43 -2
- package/template/coding-task-manager/lib/coding-session-model-owner.js +1 -1
- package/template/coding-task-manager/lib/coding-session-model-public.js +7 -1
- package/template/coding-task-manager/lib/coding-session-model-runtime.js +23 -4
- package/template/coding-task-manager/lib/coding-session-model-scan.js +31 -0
- package/template/coding-task-manager/lib/coding-session-model-state.js +7 -0
- package/template/coding-task-manager/lib/conversation-freshness-diagnostics.js +280 -0
- package/template/coding-task-manager/lib/conversation-item-clock.js +261 -0
- package/template/coding-task-manager/lib/conversation-source-registry.js +5 -0
- package/template/coding-task-manager/lib/conversation-store-v2.js +44 -0
- package/template/coding-task-manager/lib/conversation-sync.js +355 -0
- package/template/coding-task-manager/lib/create/resolve-launch-params.js +96 -12
- package/template/coding-task-manager/lib/create/worker-role-profile.js +4 -4
- package/template/coding-task-manager/lib/create-intent-lease.js +51 -0
- package/template/coding-task-manager/lib/ctm-session-context-api.js +97 -6
- package/template/coding-task-manager/lib/db-domain-tables.js +1 -0
- package/template/coding-task-manager/lib/db-domains.js +1 -0
- package/template/coding-task-manager/lib/db-owner-dispatch-probe.js +32 -0
- package/template/coding-task-manager/lib/db-owner-request-counts.js +178 -0
- package/template/coding-task-manager/lib/db-owner-worker-client.js +68 -14
- package/template/coding-task-manager/lib/db-schema-source-files.js +5 -0
- package/template/coding-task-manager/lib/deferred-restore-attempt.js +6 -2
- package/template/coding-task-manager/lib/feature-manifest.js +4 -0
- package/template/coding-task-manager/lib/fleet-conversation-route.js +97 -0
- package/template/coding-task-manager/lib/fleet-fanout.js +78 -0
- package/template/coding-task-manager/lib/fleet-hub.js +762 -0
- package/template/coding-task-manager/lib/fleet-origin-follower.js +252 -0
- package/template/coding-task-manager/lib/fleet-resolver-effects.js +147 -0
- package/template/coding-task-manager/lib/fleet-resolver-tick.js +148 -0
- package/template/coding-task-manager/lib/fleet-resolver.js +454 -0
- package/template/coding-task-manager/lib/fleet-session-list-routes.js +114 -0
- package/template/coding-task-manager/lib/fleet-session-lists.js +672 -0
- package/template/coding-task-manager/lib/fleet-update.js +798 -0
- package/template/coding-task-manager/lib/fs-watcher-root-watch.js +141 -0
- package/template/coding-task-manager/lib/fs-watcher.js +58 -42
- package/template/coding-task-manager/lib/harness-history-transition.js +50 -9
- package/template/coding-task-manager/lib/headless-term-service.js +84 -8
- package/template/coding-task-manager/lib/hermetic-agent-launch-guard.js +10 -0
- package/template/coding-task-manager/lib/history-transition-read-op.js +175 -8
- package/template/coding-task-manager/lib/host-cpu-categories.js +310 -0
- package/template/coding-task-manager/lib/host-fseventsd-sampler.js +399 -0
- package/template/coding-task-manager/lib/host-handoff.js +16 -19
- package/template/coding-task-manager/lib/host-stats.js +150 -3
- package/template/coding-task-manager/lib/hosted-sync-worker.js +43 -320
- package/template/coding-task-manager/lib/hub-bridge-access.js +798 -0
- package/template/coding-task-manager/lib/hub-bridge-box.js +409 -0
- package/template/coding-task-manager/lib/hub-bridge-mcp.js +380 -0
- package/template/coding-task-manager/lib/hub-bridge-mesh.js +313 -0
- package/template/coding-task-manager/lib/hub-bridge-ops.js +721 -0
- package/template/coding-task-manager/lib/hub-bridge.js +398 -0
- package/template/coding-task-manager/lib/idle-notify.js +491 -0
- package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +8 -0
- package/template/coding-task-manager/lib/instance-manifest.js +5 -1
- package/template/coding-task-manager/lib/instance-sync-status.js +18 -8
- package/template/coding-task-manager/lib/landing/agent-rule.txt +1 -0
- package/template/coding-task-manager/lib/landing/batch-runner.js +366 -0
- package/template/coding-task-manager/lib/landing/config.js +536 -0
- package/template/coding-task-manager/lib/landing/ctm-view.js +256 -0
- package/template/coding-task-manager/lib/landing/gate.js +360 -0
- package/template/coding-task-manager/lib/landing/git.js +344 -0
- package/template/coding-task-manager/lib/landing/lease.js +247 -0
- package/template/coding-task-manager/lib/landing/merge-delta.js +380 -0
- package/template/coding-task-manager/lib/landing/proof.js +223 -0
- package/template/coding-task-manager/lib/landing/queue.js +998 -0
- package/template/coding-task-manager/lib/landing/radar.js +489 -0
- package/template/coding-task-manager/lib/landing/resolve-cli.js +424 -0
- package/template/coding-task-manager/lib/landing/state.js +263 -0
- package/template/coding-task-manager/lib/landing/stop.js +49 -0
- package/template/coding-task-manager/lib/landing/sync-cli.js +475 -0
- package/template/coding-task-manager/lib/landing/sync.js +456 -0
- package/template/coding-task-manager/lib/landing/train.js +885 -0
- package/template/coding-task-manager/lib/landing/window.js +57 -0
- package/template/coding-task-manager/lib/launch-environment.js +121 -0
- package/template/coding-task-manager/lib/launch-model-override.js +36 -10
- package/template/coding-task-manager/lib/launch-plan-pool.js +2 -1
- package/template/coding-task-manager/lib/launch-plan-warm-cache.js +6 -0
- package/template/coding-task-manager/lib/lead-model-observer.js +57 -1
- package/template/coding-task-manager/lib/lead-model-options.js +160 -3
- package/template/coding-task-manager/lib/lead-model-restore-reconciliation.js +106 -24
- package/template/coding-task-manager/lib/lead-model-switch-runtime.js +352 -68
- package/template/coding-task-manager/lib/lead-model-switch.js +114 -10
- package/template/coding-task-manager/lib/lead-model-transition-runtime.js +14 -2
- package/template/coding-task-manager/lib/lead-model-transition-store.js +24 -6
- package/template/coding-task-manager/lib/lead-route-preflight.js +179 -0
- package/template/coding-task-manager/lib/lead-switch-history-mode.js +110 -0
- package/template/coding-task-manager/lib/lead-wire-agent.js +7 -0
- package/template/coding-task-manager/lib/lifecycle-wake-experiment.js +9 -2
- package/template/coding-task-manager/lib/live-defer-announce.js +45 -0
- package/template/coding-task-manager/lib/machine-registry.js +750 -5
- package/template/coding-task-manager/lib/macos-capabilities.js +32 -15
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +433 -53
- package/template/coding-task-manager/lib/mainloop-observability.js +14 -9
- package/template/coding-task-manager/lib/media-draft-owner.js +16 -3
- package/template/coding-task-manager/lib/memory-join.js +337 -0
- package/template/coding-task-manager/lib/memory-sync-verify.js +393 -0
- package/template/coding-task-manager/lib/memory-sync.js +565 -0
- package/template/coding-task-manager/lib/migrations/v57-delegate-pref-mirror-opencode.js +68 -0
- package/template/coding-task-manager/lib/migrations/v58-subagent-launch-native-subagents.js +42 -0
- package/template/coding-task-manager/lib/migrations/v59-coding-session-model-state-dsh.js +73 -0
- package/template/coding-task-manager/lib/migrations/v60-delegate-pref-mirror-dsh.js +78 -0
- package/template/coding-task-manager/lib/migrations/v61-conversation-item-clock.js +71 -0
- package/template/coding-task-manager/lib/model-availability.js +727 -0
- package/template/coding-task-manager/lib/model-execution-recipe-metadata.js +4 -6
- package/template/coding-task-manager/lib/model-host-receipt.js +6 -3
- package/template/coding-task-manager/lib/model-id-rule.js +19 -0
- package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
- package/template/coding-task-manager/lib/model-proxy/auto-mode-review.js +110 -23
- package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
- package/template/coding-task-manager/lib/model-proxy/generation-probe.js +60 -17
- package/template/coding-task-manager/lib/model-proxy/historical-tool-declarations.js +37 -0
- package/template/coding-task-manager/lib/model-proxy/lead-request-validation.js +1 -0
- package/template/coding-task-manager/lib/model-proxy/lead-token-admin.js +130 -0
- package/template/coding-task-manager/lib/model-proxy/refusal-scope.js +151 -0
- package/template/coding-task-manager/lib/model-proxy/request-budget-store.js +11 -2
- package/template/coding-task-manager/lib/model-proxy/tokens.js +69 -5
- package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
- package/template/coding-task-manager/lib/native-agent-model-args.js +25 -22
- package/template/coding-task-manager/lib/native-history-control.js +6 -4
- package/template/coding-task-manager/lib/native-lead-settings-client.js +44 -4
- package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +173 -36
- package/template/coding-task-manager/lib/online-backup-worker-client.js +2 -0
- package/template/coding-task-manager/lib/opencode-cli.js +480 -0
- package/template/coding-task-manager/lib/opencode-launch-model.js +471 -0
- package/template/coding-task-manager/lib/opencode-v1-plugin/model-carrier.mjs +35 -0
- package/template/coding-task-manager/lib/opencode-v2-plugin/server.js +118 -0
- package/template/coding-task-manager/lib/owner-action-manifest-data.json +1 -1
- package/template/coding-task-manager/lib/park-composer-draft.js +54 -0
- package/template/coding-task-manager/lib/park-fresh-relaunch.js +41 -10
- package/template/coding-task-manager/lib/perf-phase-trace.js +72 -0
- package/template/coding-task-manager/lib/perf-summary.js +7 -0
- package/template/coding-task-manager/lib/perf-tracker.js +63 -2
- package/template/coding-task-manager/lib/project-config.js +7 -0
- package/template/coding-task-manager/lib/prompt-purge-v2.js +52 -1
- package/template/coding-task-manager/lib/provider-login-broker.js +72 -15
- package/template/coding-task-manager/lib/provider-login-tab.js +10 -2
- package/template/coding-task-manager/lib/read-pool-client.js +89 -2
- package/template/coding-task-manager/lib/read-pool-worker-handle.js +29 -4
- package/template/coding-task-manager/lib/recent-catalog-query.js +15 -0
- package/template/coding-task-manager/lib/recent-session-catalog-response.js +67 -10
- package/template/coding-task-manager/lib/recovery-evidence.js +26 -11
- package/template/coding-task-manager/lib/relay-client.js +57 -3
- package/template/coding-task-manager/lib/relink-audit.js +34 -5
- package/template/coding-task-manager/lib/remote-machine-catalog.js +321 -26
- package/template/coding-task-manager/lib/remote-machine-create.js +576 -36
- package/template/coding-task-manager/lib/remote-machine-link.js +546 -59
- package/template/coding-task-manager/lib/remote-machine-proxy.js +143 -9
- package/template/coding-task-manager/lib/remote-session-forward.js +65 -7
- package/template/coding-task-manager/lib/remote-terminal-target.js +9 -2
- package/template/coding-task-manager/lib/restart-guard.js +18 -6
- package/template/coding-task-manager/lib/restore-seed-modes.js +87 -0
- package/template/coding-task-manager/lib/restored-scrollback-hydration-runtime.js +22 -2
- package/template/coding-task-manager/lib/resume-admission.js +17 -3
- package/template/coding-task-manager/lib/review-repo-registry.js +58 -19
- package/template/coding-task-manager/lib/root-delivery-runtime.js +2 -2
- package/template/coding-task-manager/lib/root-prompt-flow.js +1 -1
- package/template/coding-task-manager/lib/runtime-contract.js +3 -1
- package/template/coding-task-manager/lib/schema-indexes.js +1 -0
- package/template/coding-task-manager/lib/session-context-locator.js +742 -0
- package/template/coding-task-manager/lib/session-create/attachment.js +2 -2
- package/template/coding-task-manager/lib/session-create/create-phases.js +118 -0
- package/template/coding-task-manager/lib/session-create/delegate-worker.js +86 -16
- package/template/coding-task-manager/lib/session-create/dsh-session.js +930 -0
- package/template/coding-task-manager/lib/session-create/lead-route-launch.js +186 -13
- package/template/coding-task-manager/lib/session-create/native-lead-lifetime.js +55 -3
- package/template/coding-task-manager/lib/session-create/opencode-lead-provider.js +50 -0
- package/template/coding-task-manager/lib/session-create/prepared.js +162 -38
- package/template/coding-task-manager/lib/session-create/provider-process.js +160 -28
- package/template/coding-task-manager/lib/session-create/response.js +7 -7
- package/template/coding-task-manager/lib/session-create/structured-session.js +27 -0
- package/template/coding-task-manager/lib/session-create/subagent-pick-table.js +3 -1
- package/template/coding-task-manager/lib/session-create/subagent-pick.js +458 -9
- package/template/coding-task-manager/lib/session-create/transcript.js +1 -1
- package/template/coding-task-manager/lib/session-create/worker-budget-setup.js +21 -2
- package/template/coding-task-manager/lib/session-create/worker-continuation.js +7 -1
- package/template/coding-task-manager/lib/session-create/worker-launch-sandbox.js +39 -10
- package/template/coding-task-manager/lib/session-create/worker-observability.js +2 -2
- package/template/coding-task-manager/lib/session-empty.js +13 -10
- package/template/coding-task-manager/lib/session-host-manager.js +60 -0
- package/template/coding-task-manager/lib/session-jobs.js +3 -1
- package/template/coding-task-manager/lib/session-lifecycle.js +10 -4
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +6 -2
- package/template/coding-task-manager/lib/session-model-intent-contract.js +11 -8
- package/template/coding-task-manager/lib/session-park.js +738 -0
- package/template/coding-task-manager/lib/session-path-grant.js +13 -4
- package/template/coding-task-manager/lib/session-status-ledger.js +106 -4
- package/template/coding-task-manager/lib/session-transport.js +17 -0
- package/template/coding-task-manager/lib/session-workspace-projection.js +54 -5
- package/template/coding-task-manager/lib/session-workspace-tracking-runtime.js +213 -1
- package/template/coding-task-manager/lib/skill-autocomplete-query.js +120 -3
- package/template/coding-task-manager/lib/skill-autocomplete.js +15 -1
- package/template/coding-task-manager/lib/sqlite-file-set.js +39 -0
- package/template/coding-task-manager/lib/standup-search-adapter.js +16 -2
- package/template/coding-task-manager/lib/state-sync/client-ledger.js +12 -0
- package/template/coding-task-manager/lib/state-sync/frame-emitter.js +319 -100
- package/template/coding-task-manager/lib/storage/coding-session-model-store.js +4 -1
- package/template/coding-task-manager/lib/storage/delegate-pref-mirror-store.js +32 -9
- package/template/coding-task-manager/lib/structured-session-boot-sweep.js +235 -0
- package/template/coding-task-manager/lib/submission-ack.js +90 -2
- package/template/coding-task-manager/lib/systemd-scope.js +115 -0
- package/template/coding-task-manager/lib/telemetry-receiver.js +39 -0
- package/template/coding-task-manager/lib/terminal-attach-geometry.js +985 -0
- package/template/coding-task-manager/lib/terminal-bootstrap.js +30 -0
- package/template/coding-task-manager/lib/terminal-draft-operation.js +357 -0
- package/template/coding-task-manager/lib/terminal-input-admission.js +17 -1
- package/template/coding-task-manager/lib/terminal-input-prepared.js +563 -0
- package/template/coding-task-manager/lib/transcript-cache-temps.js +171 -0
- package/template/coding-task-manager/lib/transcript-frame-cache.js +8 -3
- package/template/coding-task-manager/lib/transcript-source.js +36 -4
- package/template/coding-task-manager/lib/transcript-store.js +9 -0
- package/template/coding-task-manager/lib/typing-interaction-trace.js +106 -3
- package/template/coding-task-manager/lib/usage-ledger/backfill.js +40 -5
- package/template/coding-task-manager/lib/usage-ledger/client.js +4 -1
- package/template/coding-task-manager/lib/usage-ledger/dsh-parse.js +152 -0
- package/template/coding-task-manager/lib/usage-ledger/dsh-read.js +73 -0
- package/template/coding-task-manager/lib/usage-ledger/ingest.js +34 -6
- package/template/coding-task-manager/lib/usage-ledger/live.js +35 -8
- package/template/coding-task-manager/lib/usage-ledger/read.js +29 -20
- package/template/coding-task-manager/lib/usage-ledger/schema.js +61 -1
- package/template/coding-task-manager/lib/usage-ledger/task-coordinator.js +4 -2
- package/template/coding-task-manager/lib/wake-skip-reasons.js +1 -0
- package/template/coding-task-manager/lib/walle/turn.js +1 -1
- package/template/coding-task-manager/lib/walle-history-serve.js +838 -0
- package/template/coding-task-manager/lib/walle-kill-authority.js +2 -2
- package/template/coding-task-manager/lib/walle-log-rotation.js +164 -1
- package/template/coding-task-manager/lib/walle-mcp-auto-config.js +56 -2
- package/template/coding-task-manager/lib/walle-row-history.js +495 -0
- package/template/coding-task-manager/lib/walle-supervisor.js +33 -14
- package/template/coding-task-manager/lib/watch-worker-client.js +203 -0
- package/template/coding-task-manager/lib/worker-terminal-output.js +5 -2
- package/template/coding-task-manager/lib/ws/hub-bridge.js +35 -0
- package/template/coding-task-manager/lib/ws/lifecycle.js +6 -11
- package/template/coding-task-manager/lib/ws/misc.js +1 -1
- package/template/coding-task-manager/lib/ws/state-sync.js +106 -4
- package/template/coding-task-manager/lib/ws/terminal-attach.js +91 -0
- package/template/coding-task-manager/oauth-proxy.js +112 -115
- package/template/coding-task-manager/package-lock.json +10 -94
- package/template/coding-task-manager/package.json +4 -3
- package/template/coding-task-manager/protocol/message-table.json +10 -0
- package/template/coding-task-manager/protocol/messages.js +10 -0
- package/template/coding-task-manager/protocol/ws-schema.json +20 -0
- package/template/coding-task-manager/protocol/ws-schema.md +2 -0
- package/template/coding-task-manager/public/css/app.css +170 -0
- package/template/coding-task-manager/public/css/walle-session.css +4 -0
- package/template/coding-task-manager/public/index.html +473 -243
- package/template/coding-task-manager/public/js/agent-command.js +86 -0
- package/template/coding-task-manager/public/js/app/backups-panel.js +111 -5
- package/template/coding-task-manager/public/js/app/coding-agent-upgrade.js +103 -4
- package/template/coding-task-manager/public/js/app/machines-page.js +1328 -8
- package/template/coding-task-manager/public/js/app/models-tab.js +113 -38
- package/template/coding-task-manager/public/js/app/restart-banner.js +53 -0
- package/template/coding-task-manager/public/js/app/worktrees-panel.js +184 -2
- package/template/coding-task-manager/public/js/background-work.js +196 -27
- package/template/coding-task-manager/public/js/coding-session-model-presentation.js +138 -10
- package/template/coding-task-manager/public/js/core/active-session-groups.js +179 -0
- package/template/coding-task-manager/public/js/core/pending-create.js +238 -0
- package/template/coding-task-manager/public/js/core/recent-machine-source.js +228 -0
- package/template/coding-task-manager/public/js/core/session-commands.js +180 -14
- package/template/coding-task-manager/public/js/dashboard-client-function-order.json +8 -0
- package/template/coding-task-manager/public/js/document-review-links.js +9 -1
- package/template/coding-task-manager/public/js/file-resolution-client.js +52 -1
- package/template/coding-task-manager/public/js/history-app-seam.js +22 -2
- package/template/coding-task-manager/public/js/machine-pool-shared.js +558 -3
- package/template/coding-task-manager/public/js/message-renderer.js +4 -3
- package/template/coding-task-manager/public/js/model-picker.js +3 -1
- package/template/coding-task-manager/public/js/native-subagent-review.js +15 -2
- package/template/coding-task-manager/public/js/new-session-agent-grid.js +346 -0
- package/template/coding-task-manager/public/js/pasted-content.js +236 -0
- package/template/coding-task-manager/public/js/pe-private-content-lifetime.js +39 -7
- package/template/coding-task-manager/public/js/private-model-lifetime.js +6 -2
- package/template/coding-task-manager/public/js/prompts.js +11 -10
- package/template/coding-task-manager/public/js/protocol.js +10 -0
- package/template/coding-task-manager/public/js/restart-banner-policy.js +29 -1
- package/template/coding-task-manager/public/js/session-model-picker.js +447 -57
- package/template/coding-task-manager/public/js/session-search-utils.js +18 -2
- package/template/coding-task-manager/public/js/session-usage-view.js +2 -1
- package/template/coding-task-manager/public/js/session-workspace-tracking.js +51 -1
- package/template/coding-task-manager/public/js/state-sync-client.js +10 -1
- package/template/coding-task-manager/public/js/stream-view.js +6 -5
- package/template/coding-task-manager/public/js/terminal/conv-only-session.js +287 -0
- package/template/coding-task-manager/public/js/terminal/runtime-09.js +2 -0
- package/template/coding-task-manager/public/js/terminal/runtime-10.js +5 -5
- package/template/coding-task-manager/public/js/terminal/runtime-12.js +8 -1
- package/template/coding-task-manager/public/js/terminal/runtime-16.js +4 -0
- package/template/coding-task-manager/public/js/terminal/runtime-20.js +1 -1
- package/template/coding-task-manager/public/js/terminal-attachment-draft.js +67 -13
- package/template/coding-task-manager/public/js/terminal-image-orchestration.js +52 -6
- package/template/coding-task-manager/public/js/terminal-wheel.js +23 -3
- package/template/coding-task-manager/public/js/walle/chat-conversation.js +4 -4
- package/template/coding-task-manager/public/js/walle.js +6 -0
- package/template/coding-task-manager/public/js/ws/live-dispatch.js +10 -0
- package/template/coding-task-manager/public/m/app.css +140 -0
- package/template/coding-task-manager/public/m/app.js +507 -68
- package/template/coding-task-manager/public/m/index.html +17 -0
- package/template/coding-task-manager/public/m/machines.js +838 -4
- package/template/coding-task-manager/public/m/private-api.js +31 -1
- package/template/coding-task-manager/public/usage.html +1 -0
- package/template/coding-task-manager/scripts/dev-route-fixture/synthetic-route-upstream.js +104 -0
- package/template/coding-task-manager/scripts/dev-route-fixture.js +339 -0
- package/template/coding-task-manager/scripts/opencode-lead-route-probe.js +195 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/fake-anthropic.js +168 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe/probe-plugin/server.js +95 -0
- package/template/coding-task-manager/scripts/opencode-v2-contract-probe.js +673 -0
- package/template/coding-task-manager/server.js +1950 -11679
- package/template/coding-task-manager/session-integrity.js +64 -4
- package/template/coding-task-manager/workers/db-owner-worker.js +19 -2
- package/template/coding-task-manager/workers/online-backup-worker.js +1 -0
- package/template/coding-task-manager/workers/read-pool-model-collector.js +51 -3
- package/template/coding-task-manager/workers/read-pool-worker.js +76 -6
- package/template/coding-task-manager/workers/session-host-pool-process.js +6 -6
- package/template/coding-task-manager/workers/session-host-process.js +5 -5
- package/template/coding-task-manager/workers/watch-worker.js +105 -0
- package/template/coding-task-manager/workers/worktree-worker.js +168 -33
- package/template/ctm-relay/lib/protocol.js +14 -1
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/shared/backup-retention.js +82 -0
- package/template/shared/conversation-admission.js +2 -0
- package/template/shared/conversation-provenance.js +3 -7
- package/template/shared/conversation-storage-v2-contract.js +5 -0
- package/template/shared/linked-worktree.js +51 -0
- package/template/shared/macos-fda-target.js +57 -0
- package/template/shared/orphan-parents.js +55 -0
- package/template/shared/release-schema.json +2 -2
- package/template/shared/storage/contract.js +3 -0
- package/template/wall-e/adapters/imessage.js +12 -0
- package/template/wall-e/agent.js +71 -4
- package/template/wall-e/api-walle.js +21 -1
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +2 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +5 -5
- package/template/wall-e/brain/brain-join.js +859 -0
- package/template/wall-e/brain/brain-sync-membership.js +20 -0
- package/template/wall-e/brain/brain-sync-store.js +131 -77
- package/template/wall-e/brain/brain-sync-v2-schema.js +203 -0
- package/template/wall-e/brain/brain-sync-v2.js +578 -0
- package/template/wall-e/brain/delegate-model-prefs-store.js +13 -4
- package/template/wall-e/brain/deterministic-id.js +51 -0
- package/template/wall-e/brain/memories.js +2 -2
- package/template/wall-e/brain/migrations.js +40 -6
- package/template/wall-e/brain/model-identity-migration.js +1 -1
- package/template/wall-e/brain/owner-call-whitelist.js +23 -7
- package/template/wall-e/brain/schema.js +45 -85
- package/template/wall-e/brain.js +171 -35
- package/template/wall-e/chat.js +3 -3
- package/template/wall-e/coding/agent-shell-env.js +7 -2
- package/template/wall-e/coding/prompt-bundle.js +66 -21
- package/template/wall-e/config/main-loop-exemptions.json +2 -2
- package/template/wall-e/http/admin-api.js +37 -0
- package/template/wall-e/http/gws-api.js +4 -0
- package/template/wall-e/http/helper-rules-api.js +7 -1
- package/template/wall-e/http/model-admin.js +8 -1
- package/template/wall-e/http/model-delegate-pref-admin.js +3 -3
- package/template/wall-e/http/model-proxy-providers.js +174 -1
- package/template/wall-e/http/model-route-health.js +157 -0
- package/template/wall-e/http/sync-api.js +278 -464
- package/template/wall-e/jobs/job-runner.js +28 -1
- package/template/wall-e/lib/brain-owner-op-priority.js +9 -1
- package/template/wall-e/lib/ctm-data-dir.js +26 -0
- package/template/wall-e/lib/derivation-home.js +147 -0
- package/template/wall-e/lib/helper-rules/evaluate.js +50 -9
- package/template/wall-e/lib/helper-rules/grade.js +51 -8
- package/template/wall-e/lib/helper-rules/lead-call-hook.js +38 -5
- package/template/wall-e/lib/helper-rules/store.js +4 -2
- package/template/wall-e/lib/landing-mcp-tools.js +170 -0
- package/template/wall-e/lib/mcp-client-config.js +133 -14
- package/template/wall-e/lib/mcp-gateway-migrate.js +28 -64
- package/template/wall-e/lib/mcp-integration.js +58 -13
- package/template/wall-e/lib/parent-death.js +7 -1
- package/template/wall-e/lib/scheduler-worker-jobs.js +6 -4
- package/template/wall-e/llm/delegate-lead-prefs.js +16 -1
- package/template/wall-e/llm/delegate-vendor-default.js +12 -5
- package/template/wall-e/llm/model-roles.js +1 -1
- package/template/wall-e/llm/native-delegate-selection.js +56 -8
- package/template/wall-e/llm/worker-delegate-args.js +72 -7
- package/template/wall-e/llm/worker-mcp-routing.js +66 -17
- package/template/wall-e/loops/brain-custom-writes.js +19 -0
- package/template/wall-e/loops/ingest.js +17 -1
- package/template/wall-e/loops/think.js +19 -4
- package/template/wall-e/mcp-contract.js +222 -6
- package/template/wall-e/mcp-protocol-contract.js +40 -5
- package/template/wall-e/mcp-server.js +128 -29
- package/template/wall-e/mcp-worker-schemas.js +5 -5
- package/template/wall-e/memory/ctm-context-client.js +85 -0
- package/template/wall-e/memory/ctm-session-context.js +9 -3
- package/template/wall-e/memory/hub-bridge-client.js +388 -0
- package/template/wall-e/memory/replica-origin.js +56 -0
- package/template/wall-e/memory/session-pool.js +135 -0
- package/template/wall-e/runtime/live-capabilities.js +8 -3
- package/template/wall-e/server.js +4 -2
- package/template/wall-e/shared/release-schema.json +2 -2
- package/template/wall-e/shared/sqlite-file-set.js +39 -0
- package/template/wall-e/shared/storage/contract.js +3 -0
- package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
- package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +7 -1
- package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +4 -4
- package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +6 -6
- package/template/wall-e/skills/_templates/fleet-sync/VERSION +1 -0
- package/template/wall-e/skills/_templates/fleet-sync/released-sha256.json +16 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/claude/SKILL.md +64 -0
- package/template/wall-e/skills/_templates/fleet-sync/templates/codex/SKILL.md +64 -0
- package/template/wall-e/tools/chrome-automation.js +6 -0
- package/template/wall-e/training/setup-cuda-train-env.sh +2 -2
- package/template/wall-e/workers/brain-owner-worker.js +18 -4
- package/template/wall-e/workers/mcp-gateway-process.js +5 -5
|
@@ -1,66 +1,91 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
|
27
|
-
// `db.transaction(...).immediate()` in
|
|
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-
|
|
14
|
+
// the capture triggers cannot re-stamp what we just applied.
|
|
30
15
|
//
|
|
31
|
-
//
|
|
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 {
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
'
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
|
212
|
+
return !!_v2Route(_string(pathname, 200));
|
|
526
213
|
}
|
|
527
214
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
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
|
-
|
|
555
|
-
if (!
|
|
556
|
-
|
|
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
|
-
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function _v2Body(req, res, limitBytes) {
|
|
559
250
|
try {
|
|
560
|
-
|
|
251
|
+
return await _readBody(req, limitBytes);
|
|
561
252
|
} catch (err) {
|
|
562
|
-
const
|
|
563
|
-
|
|
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
|
-
|
|
570
|
-
|
|
571
|
-
|
|
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
|
-
|
|
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
|
-
|
|
577
|
-
return
|
|
270
|
+
_jsonResponse(res, { ok: false, error: err?.message || String(err) }, _v2ErrorStatus(err));
|
|
271
|
+
return null;
|
|
578
272
|
}
|
|
579
|
-
|
|
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('
|
|
374
|
+
result = await callLocalBrain('brainSyncV2JoinImport', [_joinImportOptions(body, env)]);
|
|
583
375
|
} catch (err) {
|
|
584
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
599
|
-
|
|
600
|
-
const
|
|
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
|
|
604
|
-
//
|
|
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
|
-
|
|
617
|
-
REPLICATED_TABLES,
|
|
618
|
-
APPLY_SENTINEL_TABLE,
|
|
437
|
+
V2_ROUTE_SPECS,
|
|
619
438
|
handleSyncApi,
|
|
620
439
|
handlesSyncPath,
|
|
621
|
-
|
|
622
|
-
|
|
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
|
};
|