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
|
@@ -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
|
+
};
|