create-walle 0.10.1 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/lib/owner-write-safety.js +135 -0
- package/lib/public-runtime.js +13 -5
- package/lib/release-schema.json +3 -2
- package/lib/runtime-closure-policy.json +6 -4
- package/package.json +1 -1
- package/template/bin/deps-doctor.js +1 -1
- package/template/bin/node-bin.sh +11 -0
- package/template/coding-task-manager/api-prompts.js +10 -22
- package/template/coding-task-manager/bin/claude-hook.js +7 -3
- package/template/coding-task-manager/bin/claude-table-parity.js +307 -0
- package/template/coding-task-manager/bin/conversation-storage-auto-migration-lib.js +16 -1
- package/template/coding-task-manager/bin/conversation-storage-generation-evidence.js +9 -2
- package/template/coding-task-manager/bin/conversation-storage-service-entry-lib.js +16 -1
- package/template/coding-task-manager/bin/conversation-storage-walle-owner.js +247 -0
- package/template/coding-task-manager/bin/ctm-machine-pair.js +160 -0
- package/template/coding-task-manager/bin/lead-wait-census.js +1348 -0
- package/template/coding-task-manager/bin/repair-foreign-clone.js +564 -0
- package/template/coding-task-manager/bin/repair-resume-echo-titles.js +248 -0
- package/template/coding-task-manager/bin/write-lock-bench.js +111 -0
- package/template/coding-task-manager/db.js +705 -286
- package/template/coding-task-manager/git-utils.js +44 -0
- package/template/coding-task-manager/lib/adopted-host-redial.js +4 -0
- package/template/coding-task-manager/lib/agent-presets.js +15 -0
- package/template/coding-task-manager/lib/agent-title-apply.js +88 -0
- package/template/coding-task-manager/lib/agent-title-backfill-read.js +92 -0
- package/template/coding-task-manager/lib/api/host-stats.js +98 -0
- package/template/coding-task-manager/lib/api/ingress.js +35 -12
- package/template/coding-task-manager/lib/api/machine-pairing.js +285 -0
- package/template/coding-task-manager/lib/api/machine-pool.js +307 -0
- package/template/coding-task-manager/lib/api/machines.js +461 -0
- package/template/coding-task-manager/lib/api/misc-ops.js +20 -5
- package/template/coding-task-manager/lib/api/providers.js +53 -6
- package/template/coding-task-manager/lib/api/resume-from-store.js +25 -0
- package/template/coding-task-manager/lib/api/sessions.js +160 -8
- package/template/coding-task-manager/lib/api/transcript-history.js +17 -4
- package/template/coding-task-manager/lib/api/worktrees.js +38 -37
- package/template/coding-task-manager/lib/approval-drift.js +16 -1
- package/template/coding-task-manager/lib/approval-logical-frame.js +5 -1
- package/template/coding-task-manager/lib/approval-scope.js +68 -4
- package/template/coding-task-manager/lib/auth-context.js +32 -2
- package/template/coding-task-manager/lib/auth-rate-limit.js +41 -1
- package/template/coding-task-manager/lib/auth-rules.js +186 -45
- package/template/coding-task-manager/lib/broadcast-serialize.js +2 -1
- package/template/coding-task-manager/lib/browser-session-broker.js +9 -1
- package/template/coding-task-manager/lib/browser-use-adapter.js +33 -2
- package/template/coding-task-manager/lib/claude-markdown-ansi.js +27 -85
- package/template/coding-task-manager/lib/claude-session-title.js +6 -30
- package/template/coding-task-manager/lib/claude-table-layout.js +322 -0
- package/template/coding-task-manager/lib/client-diag-handler.js +5 -0
- package/template/coding-task-manager/lib/codex-config-guard.js +33 -3
- package/template/coding-task-manager/lib/codex-harness-qualification.js +12 -0
- package/template/coding-task-manager/lib/codex-launch-health.js +29 -2
- package/template/coding-task-manager/lib/codex-link-preparation.js +9 -8
- package/template/coding-task-manager/lib/codex-paths.js +2 -1
- package/template/coding-task-manager/lib/coding-agent-models.js +21 -30
- package/template/coding-task-manager/lib/coding-session-model-adapters.js +27 -2
- package/template/coding-task-manager/lib/coding-session-model-projection.js +5 -0
- package/template/coding-task-manager/lib/coding-session-model-runtime.js +1 -2
- package/template/coding-task-manager/lib/coding-session-model-state.js +10 -0
- package/template/coding-task-manager/lib/coding-session-model-sync-state.js +0 -1
- package/template/coding-task-manager/lib/conversation-import/cursor-guard-owner.js +10 -0
- package/template/coding-task-manager/lib/create/resolve-launch-params.js +8 -1
- package/template/coding-task-manager/lib/ctm-boot-lifecycle.js +17 -1
- package/template/coding-task-manager/lib/db-domain-tables.js +7 -0
- package/template/coding-task-manager/lib/db-domains.js +21 -2
- package/template/coding-task-manager/lib/db-owner-op-registry.js +7 -0
- package/template/coding-task-manager/lib/db-schema-source-files.js +1 -0
- package/template/coding-task-manager/lib/feature-manifest.js +1 -1
- package/template/coding-task-manager/lib/fs-watcher.js +3 -2
- package/template/coding-task-manager/lib/harness-history-transition.js +3 -3
- package/template/coding-task-manager/lib/headless-inflight-ops.js +203 -0
- package/template/coding-task-manager/lib/headless-missing-reply.js +42 -0
- package/template/coding-task-manager/lib/headless-term-service.js +55 -17
- package/template/coding-task-manager/lib/host-control-dial.js +167 -0
- package/template/coding-task-manager/lib/host-handoff-orchestrator.js +16 -2
- package/template/coding-task-manager/lib/host-handoff.js +51 -13
- package/template/coding-task-manager/lib/host-health-ledger.js +164 -12
- package/template/coding-task-manager/lib/host-self-truth-watchdog.js +4 -0
- package/template/coding-task-manager/lib/host-stats.js +328 -0
- package/template/coding-task-manager/lib/host-threadpool-evacuation.js +160 -0
- package/template/coding-task-manager/lib/host-threadpool-monitor.js +14 -1
- package/template/coding-task-manager/lib/hosted-input-dispatch.js +203 -0
- package/template/coding-task-manager/lib/hosted-sync-worker.js +113 -5
- package/template/coding-task-manager/lib/instance-id-ownership-manifest.js +4 -0
- package/template/coding-task-manager/lib/instance-manifest-runtime.js +3 -0
- package/template/coding-task-manager/lib/instance-manifest.js +13 -1
- package/template/coding-task-manager/lib/instance-sync-status.js +15 -0
- package/template/coding-task-manager/lib/launch-model-override.js +160 -1
- package/template/coding-task-manager/lib/launch-plan-worker.js +59 -2
- package/template/coding-task-manager/lib/lead-model-switch-runtime.js +57 -4
- package/template/coding-task-manager/lib/lead-model-switch.js +3 -2
- package/template/coding-task-manager/lib/lead-model-transition-store.js +61 -3
- package/template/coding-task-manager/lib/legacy-draft-cancellation-recovery.js +111 -0
- package/template/coding-task-manager/lib/login-tripwire.js +1 -0
- package/template/coding-task-manager/lib/machine-pairing.js +238 -0
- package/template/coding-task-manager/lib/machine-pool.js +409 -0
- package/template/coding-task-manager/lib/machine-registry.js +749 -0
- package/template/coding-task-manager/lib/machine-self.js +66 -0
- package/template/coding-task-manager/lib/main-thread-lint-exemptions.json +52 -4
- package/template/coding-task-manager/lib/media-journal-control-reader.js +473 -0
- package/template/coding-task-manager/lib/media-journal.js +84 -27
- package/template/coding-task-manager/lib/media-store-options.js +43 -15
- package/template/coding-task-manager/lib/migrations/v56-host-samples-and-machine-pool.js +76 -0
- package/template/coding-task-manager/lib/mirror-feed-sanitize.js +12 -3
- package/template/coding-task-manager/lib/model-proxy/anthropic-face.js +4 -0
- package/template/coding-task-manager/lib/model-proxy/chat-response-translators.js +3 -0
- package/template/coding-task-manager/lib/model-proxy/handoff.js +494 -0
- package/template/coding-task-manager/lib/model-proxy/lead-worker-picks.js +25 -0
- package/template/coding-task-manager/lib/model-proxy/native-lead-listener.js +61 -7
- package/template/coding-task-manager/lib/model-proxy/proxy-cli.js +200 -0
- package/template/coding-task-manager/lib/model-proxy/proxy-instance-record.js +257 -0
- package/template/coding-task-manager/lib/model-proxy/stream-accounting.js +205 -0
- package/template/coding-task-manager/lib/model-proxy/tokens.js +78 -0
- package/template/coding-task-manager/lib/model-proxy/upstream-outcome.js +9 -0
- package/template/coding-task-manager/lib/native-history-control.js +2 -2
- package/template/coding-task-manager/lib/native-lead-settings-client.js +2 -1
- package/template/coding-task-manager/lib/native-lead-tls.js +18 -4
- package/template/coding-task-manager/lib/oauth-proxy-supervisor.js +628 -64
- package/template/coding-task-manager/lib/owner-action-manifest-data.json +14 -0
- package/template/coding-task-manager/lib/painted-row-recorder.js +9 -1
- package/template/coding-task-manager/lib/prompt-submission-contract.js +21 -1
- package/template/coding-task-manager/lib/provider-login-broker.js +192 -9
- package/template/coding-task-manager/lib/provider-login-capture.js +153 -0
- package/template/coding-task-manager/lib/queue-draft-status-overlay.js +36 -0
- package/template/coding-task-manager/lib/raw-codex-history-window.js +5 -3
- package/template/coding-task-manager/lib/read-pool-client.js +5 -2
- package/template/coding-task-manager/lib/read-pool-op-registry.js +10 -1
- package/template/coding-task-manager/lib/recent-catalog-query.js +26 -0
- package/template/coding-task-manager/lib/recent-session-catalog-response.js +9 -0
- package/template/coding-task-manager/lib/remote-command-context.js +12 -0
- package/template/coding-task-manager/lib/remote-command-runtime.js +13 -1
- package/template/coding-task-manager/lib/remote-machine-catalog.js +1062 -0
- package/template/coding-task-manager/lib/remote-machine-create.js +693 -0
- package/template/coding-task-manager/lib/remote-machine-link.js +939 -0
- package/template/coding-task-manager/lib/remote-machine-proxy.js +520 -0
- package/template/coding-task-manager/lib/remote-session-forward.js +306 -0
- package/template/coding-task-manager/lib/remote-submission-status.js +6 -0
- package/template/coding-task-manager/lib/restart-outcome-truth.js +20 -3
- package/template/coding-task-manager/lib/restart-preflight.js +9 -2
- package/template/coding-task-manager/lib/restore-backoff-gate.js +10 -2
- package/template/coding-task-manager/lib/restore-prepare-retry.js +12 -5
- package/template/coding-task-manager/lib/restore-resume-batch.js +22 -1
- package/template/coding-task-manager/lib/resume-blocked-wire.js +62 -1
- package/template/coding-task-manager/lib/resume-cwd.js +13 -0
- package/template/coding-task-manager/lib/resume-from-store.js +12 -1
- package/template/coding-task-manager/lib/resume-origin-facts.js +38 -1
- package/template/coding-task-manager/lib/resume-preflight.js +21 -16
- package/template/coding-task-manager/lib/review-repo-admission.js +39 -6
- package/template/coding-task-manager/lib/review-repo-registry.js +17 -0
- package/template/coding-task-manager/lib/root-delivery-runtime.js +72 -15
- package/template/coding-task-manager/lib/root-delivery-selection.js +37 -1
- package/template/coding-task-manager/lib/root-prompt-admission.js +33 -2
- package/template/coding-task-manager/lib/root-prompt-flow.js +167 -33
- package/template/coding-task-manager/lib/scrollback-archive-store.js +7 -4
- package/template/coding-task-manager/lib/session-analysis-memory.js +2 -0
- package/template/coding-task-manager/lib/session-create/codex-native-provider-gate.js +218 -0
- package/template/coding-task-manager/lib/session-create/codex-resume-exit-classify.js +52 -0
- package/template/coding-task-manager/lib/session-create/delegate-worker.js +95 -6
- package/template/coding-task-manager/lib/session-create/identity.js +20 -1
- package/template/coding-task-manager/lib/session-create/prepared.js +69 -1
- package/template/coding-task-manager/lib/session-create/provider-launch-context.js +77 -0
- package/template/coding-task-manager/lib/session-create/provider-process.js +34 -37
- package/template/coding-task-manager/lib/session-create/response.js +30 -12
- package/template/coding-task-manager/lib/session-create/transcript.js +13 -4
- package/template/coding-task-manager/lib/session-create/worker-continuation.js +43 -1
- package/template/coding-task-manager/lib/session-create/worker-notices.js +94 -5
- package/template/coding-task-manager/lib/session-create/worker-observability.js +99 -7
- package/template/coding-task-manager/lib/session-create/worker-stop.js +11 -3
- package/template/coding-task-manager/lib/session-create/worker-wait-watcher.js +315 -0
- package/template/coding-task-manager/lib/session-geometry-authority-handler.js +10 -1
- package/template/coding-task-manager/lib/session-host-manager.js +152 -206
- package/template/coding-task-manager/lib/session-jobs.js +17 -9
- package/template/coding-task-manager/lib/session-lifecycle.js +18 -0
- package/template/coding-task-manager/lib/session-model-action-preconditions.js +9 -0
- package/template/coding-task-manager/lib/session-resource-sampler.js +19 -9
- package/template/coding-task-manager/lib/session-standup.js +67 -4
- package/template/coding-task-manager/lib/session-status-ledger.js +42 -3
- package/template/coding-task-manager/lib/session-status-runtime.js +45 -1
- package/template/coding-task-manager/lib/session-title-mirror.js +25 -57
- package/template/coding-task-manager/lib/session-transcript-trash.js +8 -1
- package/template/coding-task-manager/lib/startup-pointer-write.js +57 -0
- package/template/coding-task-manager/lib/state-sync/painted-frame-field.js +11 -0
- package/template/coding-task-manager/lib/storage/coding-session-model-store.js +21 -22
- package/template/coding-task-manager/lib/surviving-owner-bootstrap.js +7 -2
- package/template/coding-task-manager/lib/surviving-owner-recovery.js +2 -1
- package/template/coding-task-manager/lib/terminal-draft-http-status.js +25 -0
- package/template/coding-task-manager/lib/transcript-archiver.js +229 -229
- package/template/coding-task-manager/lib/transcript-frame-cache.js +12 -2
- package/template/coding-task-manager/lib/transcript-guard-job.js +55 -4
- package/template/coding-task-manager/lib/transcript-link.js +77 -0
- package/template/coding-task-manager/lib/transcript-manifest-store.js +8 -4
- package/template/coding-task-manager/lib/transcript-source.js +8 -3
- package/template/coding-task-manager/lib/transcript-store.js +40 -0
- package/template/coding-task-manager/lib/tunnel-lane-governor.js +6 -3
- package/template/coding-task-manager/lib/typing-interaction-trace.js +92 -0
- package/template/coding-task-manager/lib/typing-worker-stages.js +60 -0
- package/template/coding-task-manager/lib/visible-revision-watchdog.js +8 -12
- package/template/coding-task-manager/lib/wake-skip-reasons.js +46 -0
- package/template/coding-task-manager/lib/walle-supervisor.js +98 -14
- package/template/coding-task-manager/lib/worker-failure-classify.js +13 -1
- package/template/coding-task-manager/lib/ws/state-sync.js +19 -2
- package/template/coding-task-manager/lib/ws/terminal-attach.js +32 -1
- package/template/coding-task-manager/lib/ws-router.js +68 -1
- package/template/coding-task-manager/lib/ws-send-admission.js +22 -4
- package/template/coding-task-manager/oauth-proxy.js +99 -55
- package/template/coding-task-manager/package-lock.json +24 -0
- package/template/coding-task-manager/package.json +3 -0
- package/template/coding-task-manager/providers/codex-mcp-extract.js +121 -19
- package/template/coding-task-manager/providers/codex-mcp.js +14 -5
- package/template/coding-task-manager/public/css/app.css +279 -1
- package/template/coding-task-manager/public/css/reviews.css +4 -0
- package/template/coding-task-manager/public/index.html +294 -223
- package/template/coding-task-manager/public/js/app/machines-page.js +1252 -0
- package/template/coding-task-manager/public/js/app/models-tab.js +546 -36
- package/template/coding-task-manager/public/js/app/worktrees-panel.js +7 -5
- package/template/coding-task-manager/public/js/background-work.js +13 -7
- package/template/coding-task-manager/public/js/coding-session-model-presentation.js +44 -4
- package/template/coding-task-manager/public/js/core/session-commands.js +676 -0
- package/template/coding-task-manager/public/js/core/session-selectors.js +5 -1
- package/template/coding-task-manager/public/js/history-app-seam.js +26 -2
- package/template/coding-task-manager/public/js/history-painted-rows.js +17 -2
- package/template/coding-task-manager/public/js/history-reader-park.js +13 -1
- package/template/coding-task-manager/public/js/history-scroller.js +187 -9
- package/template/coding-task-manager/public/js/history-transcript-pages.js +5 -5
- package/template/coding-task-manager/public/js/machine-pool-shared.js +436 -0
- package/template/coding-task-manager/public/js/private-state-scope.js +31 -3
- package/template/coding-task-manager/public/js/prompt-submission-client.js +173 -8
- package/template/coding-task-manager/public/js/prompt-submission-tray.js +281 -0
- package/template/coding-task-manager/public/js/queue-panel-submission-reconcile.js +174 -1
- package/template/coding-task-manager/public/js/reviews.js +7 -3
- package/template/coding-task-manager/public/js/root-draft-lifetime.js +139 -2
- package/template/coding-task-manager/public/js/root-queue-builder-lifetime.js +16 -0
- package/template/coding-task-manager/public/js/row-join.js +4 -2
- package/template/coding-task-manager/public/js/session-model-picker.js +1 -1
- package/template/coding-task-manager/public/js/session-search-utils.js +46 -19
- package/template/coding-task-manager/public/js/state-sync-client.js +4 -0
- package/template/coding-task-manager/public/js/terminal/runtime-02.js +16 -7
- package/template/coding-task-manager/public/js/terminal/runtime-03.js +41 -0
- package/template/coding-task-manager/public/js/terminal/runtime-04.js +41 -4
- package/template/coding-task-manager/public/js/terminal/runtime-12.js +52 -2
- package/template/coding-task-manager/public/js/terminal/runtime-16.js +12 -12
- package/template/coding-task-manager/public/js/terminal/runtime-18.js +14 -0
- package/template/coding-task-manager/public/js/terminal/runtime-19.js +8 -4
- package/template/coding-task-manager/public/js/terminal/runtime-20.js +210 -73
- package/template/coding-task-manager/public/js/terminal/runtime-21.js +19 -6
- package/template/coding-task-manager/public/js/terminal/runtime-22.js +86 -0
- package/template/coding-task-manager/public/js/terminal-history-viewport.js +1 -1
- package/template/coding-task-manager/public/js/terminal-wheel.js +39 -2
- package/template/coding-task-manager/public/js/walle/chat-conversation.js +11 -3
- package/template/coding-task-manager/public/js/walle-session.js +30 -7
- package/template/coding-task-manager/public/m/app.css +226 -8
- package/template/coding-task-manager/public/m/app.js +502 -9
- package/template/coding-task-manager/public/m/index.html +38 -1
- package/template/coding-task-manager/public/m/machines.js +1313 -0
- package/template/coding-task-manager/public/m/pool-failover.js +391 -0
- package/template/coding-task-manager/public/m/private-queue-actions.js +26 -7
- package/template/coding-task-manager/public/m/sw.js +5 -0
- package/template/coding-task-manager/queue-engine.js +13 -0
- package/template/coding-task-manager/server/approvals.js +5 -6
- package/template/coding-task-manager/server/boot.js +136 -17
- package/template/coding-task-manager/server.js +902 -239
- package/template/coding-task-manager/session-integrity.js +7 -9
- package/template/coding-task-manager/workers/db-owner-worker.js +53 -7
- package/template/coding-task-manager/workers/read-pool-worker.js +17 -6
- package/template/coding-task-manager/workers/session-host-pool-process.js +18 -4
- package/template/coding-task-manager/workers/session-host-process.js +14 -3
- package/template/coding-task-manager/workers/transcript-archive-worker.js +1 -1
- package/template/coding-task-manager/workers/worktree-worker.js +34 -0
- package/template/package-lock.json +2 -2
- package/template/package.json +1 -1
- package/template/scripts/conversation-storage-v2-migration.js +78 -11
- package/template/shared/conversation-storage-v2-contract.js +42 -6
- package/template/shared/fixtures/worker-status-payloads.json +392 -0
- package/template/shared/git-remote-key.js +83 -0
- package/template/shared/host-identity.js +98 -2
- package/template/shared/owner-write-safety.js +135 -0
- package/template/shared/public-runtime.js +13 -5
- package/template/shared/release-schema.json +3 -2
- package/template/shared/report-citations.js +14 -0
- package/template/shared/sqlite-write-lock.js +361 -223
- package/template/shared/storage/contract.js +35 -3
- package/template/wall-e/agent.js +21 -0
- package/template/wall-e/api-walle.js +5 -0
- package/template/wall-e/bin/brain-purge-legacy-ctm.js +3 -2
- package/template/wall-e/bin/walle-mcp-stdio.js +12 -6
- package/template/wall-e/brain/helper-rules-schema.js +129 -0
- package/template/wall-e/brain/migrations.js +51 -22
- package/template/wall-e/brain/model-deployment-facts.js +31 -1
- package/template/wall-e/brain/owner-call-whitelist.js +16 -0
- package/template/wall-e/brain/schema-ensures.js +22 -0
- package/template/wall-e/brain.js +1 -1
- package/template/wall-e/coding/journal-dispatcher.js +51 -0
- package/template/wall-e/coding/session-journal.js +28 -40
- package/template/wall-e/coding-orchestrator.js +23 -26
- package/template/wall-e/http/auth.js +18 -1
- package/template/wall-e/http/blog-views-api.js +318 -0
- package/template/wall-e/http/helper-rules-api.js +321 -0
- package/template/wall-e/http/model-proxy-providers.js +3 -1
- package/template/wall-e/http/telemetry-api.js +12 -0
- package/template/wall-e/lib/blog-view-policy.js +88 -0
- package/template/wall-e/lib/blog-view-store.js +169 -0
- package/template/wall-e/lib/client-config-lock.js +2 -1
- package/template/wall-e/lib/coding-journal-writer-child-client.js +67 -18
- package/template/wall-e/lib/error-kinds.js +10 -1
- package/template/wall-e/lib/feature-manifest.js +1 -1
- package/template/wall-e/lib/helper-rules/cell-state.js +123 -0
- package/template/wall-e/lib/helper-rules/evaluate.js +357 -0
- package/template/wall-e/lib/helper-rules/explore-rubric.md +12 -0
- package/template/wall-e/lib/helper-rules/grade.js +277 -0
- package/template/wall-e/lib/helper-rules/label.js +247 -0
- package/template/wall-e/lib/helper-rules/lead-call-hook.js +522 -0
- package/template/wall-e/lib/helper-rules/store.js +667 -0
- package/template/wall-e/lib/mcp-integration.js +5 -6
- package/template/wall-e/lib/runtime-worker-pool.js +1 -0
- package/template/wall-e/lib/scheduler-worker-jobs.js +6 -0
- package/template/wall-e/lib/telemetry-db-schema.js +37 -0
- package/template/wall-e/lib/telemetry-process-client.js +76 -1
- package/template/wall-e/llm/default-fallback.js +4 -0
- package/template/wall-e/llm/model-catalog.js +13 -4
- package/template/wall-e/llm/model-roles.js +6 -0
- package/template/wall-e/llm/portkey-http.js +150 -0
- package/template/wall-e/llm/portkey-sync.js +15 -145
- package/template/wall-e/llm/worker-mcp-routing.js +61 -0
- package/template/wall-e/llm/worker-route.js +43 -1
- package/template/wall-e/loops/brain-custom-writes.js +45 -0
- package/template/wall-e/mcp-protocol-contract.js +41 -1
- package/template/wall-e/mcp-server.js +13 -2
- package/template/wall-e/mcp-worker-schemas.js +20 -2
- package/template/wall-e/memory/ctm-context-client.js +25 -5
- package/template/wall-e/shared/release-schema.json +3 -2
- package/template/wall-e/shared/sqlite-write-lock.js +361 -223
- package/template/wall-e/shared/storage/contract.js +35 -3
- package/template/wall-e/skills/_templates/delegate-worker/VERSION +1 -1
- package/template/wall-e/skills/_templates/delegate-worker/released-sha256.json +6 -0
- package/template/wall-e/skills/_templates/delegate-worker/templates/claude/SKILL.md +7 -2
- package/template/wall-e/skills/_templates/delegate-worker/templates/codex/SKILL.md +7 -2
- package/template/wall-e/tools/local-tools.js +6 -4
- package/template/wall-e/workers/brain-owner-worker.js +9 -0
- package/template/wall-e/workers/coding-journal-writer-child.js +8 -1
- package/template/wall-e/workers/telemetry-worker.js +8 -0
|
@@ -0,0 +1,1062 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// WP-6 (docs/remote-machine-hub-sessions-2026-10-22.html §5 D-2/D-6, §6.6, §7, §8 AC-6a..AC-6d,
|
|
4
|
+
// §9 S-3/S-4/S-5/S-8/S-27): the hub-side REMOTE CATALOG — the one place that knows which ctm
|
|
5
|
+
// session ids belong to a box, and the one place that builds the remote rows the hub's session list
|
|
6
|
+
// carries.
|
|
7
|
+
//
|
|
8
|
+
// Three sources, one catalog (D-2: the box owns the session; the hub only mirrors):
|
|
9
|
+
// LIVE the box's own session list, fetched over the WP-3 machine link (WS `list`, answered by
|
|
10
|
+
// the box's `sessions` frame — server.js handleList). Refreshed on every registry probe
|
|
11
|
+
// sweep (lib/hosted-sync-worker.js), on every link (re)connect, right after a forwarded
|
|
12
|
+
// create and right after a relayed kill. A catalog-initiated link open counts as ONE link
|
|
13
|
+
// user and is released as soon as the reply lands, so the link's existing 60 s idle close
|
|
14
|
+
// still applies — the catalog never keeps a machine link open.
|
|
15
|
+
// SYNC the at-rest mirror rows the sync layer already materializes on the hub
|
|
16
|
+
// (`startup_tasks.park_reason='remote-mirror'`, db.js's ctm_session applier). These lag
|
|
17
|
+
// (a ~15 s tick) and never carry an IN-MEMORY lifecycle park (S-27): a box session parked
|
|
18
|
+
// by a lifecycle action still looks `running` to a DB reader until the next sync push. The
|
|
19
|
+
// live list is the truth while the box is reachable; this source is the DEGRADED look.
|
|
20
|
+
// CREATE lib/remote-machine-create.js's in-memory `_mirrorSessions` — ids this hub forwarded a
|
|
21
|
+
// create for in THIS process (never persisted, OQ-4).
|
|
22
|
+
//
|
|
23
|
+
// D-3/S-5: machine labeling is the MINTED instance id plus the user-set display name. A hostname is
|
|
24
|
+
// never used to label a machine (lib/recent-session-catalog-response.js overwrites every row's
|
|
25
|
+
// hostname with the LOCAL hostname, so a hostname label would silently claim a box session is local).
|
|
26
|
+
//
|
|
27
|
+
// Everything is injectable for tests; nothing here opens a DB handle of its own.
|
|
28
|
+
//
|
|
29
|
+
// DEPENDENCY DIRECTION (lint ceiling: 0 cycles in lib/**): this module is a SINK. Every peer it
|
|
30
|
+
// drives — the machine link, the create/mirror registry, the HTTP proxy, the origin facts, the db
|
|
31
|
+
// owner client — transitively REQUIRES this module (the link's relay hook, the create post-hook and
|
|
32
|
+
// the sync worker's sweep hook all call in here), so a lexical `require` of any of them would close
|
|
33
|
+
// a dependency cycle and make load order load-bearing. They arrive instead through
|
|
34
|
+
// `configureProduction()`, called ONCE by server.js — the composition root, which is outside lib —
|
|
35
|
+
// with lazy getters, so nothing is loaded earlier than it is used. Tests inject the same names
|
|
36
|
+
// through each function's `deps` argument. lib/machine-registry.js is the only direct require: it is
|
|
37
|
+
// a leaf (nothing it requires reaches back here).
|
|
38
|
+
|
|
39
|
+
const { boolFlag, registerFlag } = require('./env-flags');
|
|
40
|
+
const { makeDedupeLogger } = require('../../shared/log-dedupe');
|
|
41
|
+
|
|
42
|
+
// db.js's own spelling of an at-rest box mirror (db.js `_applySyncCtmSessionOnHandle`).
|
|
43
|
+
const SYNC_MIRROR_PARK_REASON = 'remote-mirror';
|
|
44
|
+
|
|
45
|
+
// The three routing fields §6.6/§8 add to EVERY session row the hub sends. Local rows carry them
|
|
46
|
+
// EMPTY (the client never has to guess); remote rows carry the registry's values.
|
|
47
|
+
const MACHINE_FIELDS = Object.freeze(['machine', 'machineName', 'machineHealth']);
|
|
48
|
+
|
|
49
|
+
const DEFAULT_CATALOG_TIMEOUT_MS = 5000;
|
|
50
|
+
// The registry read is a small indexed SELECT through the db owner; a couple of seconds of staleness
|
|
51
|
+
// is harmless (the probe sweep is the only writer and it runs ~15 s apart) and it keeps the relay's
|
|
52
|
+
// hot path — one lookup per relayable frame — off the database.
|
|
53
|
+
const REGISTRY_TTL_MS = 2000;
|
|
54
|
+
// The at-rest mirror scan is only paid for when the live catalog cannot answer (a hub restart, or an
|
|
55
|
+
// unreachable box), and then at most once per TTL.
|
|
56
|
+
const SYNC_MIRROR_TTL_MS = 15000;
|
|
57
|
+
|
|
58
|
+
let _injected = null;
|
|
59
|
+
// The production peers, as lazy getters (see the dependency-direction note above). null until
|
|
60
|
+
// server.js wires them; a missing peer degrades ONE feature (see `_peer` call sites), never boot.
|
|
61
|
+
let _wired = null;
|
|
62
|
+
|
|
63
|
+
// machineInstanceId -> { rows, byId: Map<ctmSessionId, row>, fetchedAt, ok, error }
|
|
64
|
+
const _catalogs = new Map();
|
|
65
|
+
const _registryCache = { machines: [], at: 0, revision: null };
|
|
66
|
+
// The remote half of the last merged list (machine health + the appended rows). `_finishBroadcast`
|
|
67
|
+
// folds it into its no-op elision key, so a box-side change is never elided as "nothing changed"
|
|
68
|
+
// just because the LOCAL rows did not move.
|
|
69
|
+
let _lastMergeSignature = '';
|
|
70
|
+
const _syncMirrorCache = { byId: new Map(), at: 0 };
|
|
71
|
+
|
|
72
|
+
/** Test/host seam: override any production dependency (db, registry, link, clock, log). */
|
|
73
|
+
function configure(deps) {
|
|
74
|
+
_injected = deps && typeof deps === 'object' ? deps : null;
|
|
75
|
+
// Each cache below holds a SNAPSHOT of ONE read. A caller supplying its own registry/db (the host
|
|
76
|
+
// seam, every unit test) must not be served a snapshot that a DIFFERENT caller's read cached —
|
|
77
|
+
// `machineRows` deliberately ignores its `deps` argument once a snapshot is warm, because in
|
|
78
|
+
// production every caller reads the same process-global registry through the same deps.
|
|
79
|
+
_registryCache.machines = [];
|
|
80
|
+
_registryCache.at = 0;
|
|
81
|
+
_registryCache.revision = null;
|
|
82
|
+
_syncMirrorCache.byId = new Map();
|
|
83
|
+
_syncMirrorCache.at = 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Production wiring (server.js, once at boot): `{ link, create, proxy, originFacts, db }`, each a
|
|
88
|
+
* function returning the module, so the requires happen in the composition root and stay lazy.
|
|
89
|
+
*/
|
|
90
|
+
function configureProduction(peers) {
|
|
91
|
+
_wired = peers && typeof peers === 'object' ? peers : null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve one peer: an explicit `deps` value wins (tests), then the production getter. Returns null
|
|
96
|
+
* when neither exists — every caller below treats null as "this one degraded feature is unavailable",
|
|
97
|
+
* because a hub that boots without a wired catalog must still serve its LOCAL session list.
|
|
98
|
+
*/
|
|
99
|
+
function _peer(name, d) {
|
|
100
|
+
if (d && d[name]) return d[name];
|
|
101
|
+
const getter = _wired ? _wired[name] : null;
|
|
102
|
+
if (typeof getter !== 'function') return null;
|
|
103
|
+
try {
|
|
104
|
+
return getter() || null;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
_report(d, `${name} peer unavailable: ${error?.message || error}`);
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function _string(value, max = 200) {
|
|
112
|
+
return String(value == null ? '' : value).trim().slice(0, max);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Lazy: a pure unit test (merge/dedupe) must never load db.js / the owner client. `db` and
|
|
116
|
+
// `registry` are therefore NOT part of this object — every reader below falls back to a lazy
|
|
117
|
+
// `require`/peer only when a caller did not inject its own handle.
|
|
118
|
+
function _productionDeps() {
|
|
119
|
+
return {
|
|
120
|
+
env: process.env,
|
|
121
|
+
now: () => Date.now(),
|
|
122
|
+
setTimeoutFn: global.setTimeout,
|
|
123
|
+
clearTimeoutFn: global.clearTimeout,
|
|
124
|
+
catalogTimeoutMs: DEFAULT_CATALOG_TIMEOUT_MS,
|
|
125
|
+
registryTtlMs: REGISTRY_TTL_MS,
|
|
126
|
+
syncMirrorTtlMs: SYNC_MIRROR_TTL_MS,
|
|
127
|
+
log: console,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* "Is this id a session THIS instance owns locally right now?" — server.js wires it to
|
|
133
|
+
* server-state's getSession (the sessions Map covers live sessions AND deferred boot placeholders).
|
|
134
|
+
* 2026-10-23 live incident: a sync peer (DGX Spark) materialized this hub's synced startup tasks as
|
|
135
|
+
* its own sessions and listed them in its `sessions` broadcast; recordSessionList ingested the echo,
|
|
136
|
+
* the mirror lookup claimed the hub's OWN sessions as box mirrors, and the WP-3 relay intercept
|
|
137
|
+
* diverted their acks/bootstraps/attaches/input/wakes to the box — live sessions wedged read-only
|
|
138
|
+
* (ackedSeq=0 forever) and deferred restores never started. The §6.6 display merge already enforces
|
|
139
|
+
* "a local id always wins"; this lookup lets the RELAY enforce the same rule, before any source.
|
|
140
|
+
*/
|
|
141
|
+
function _localSessionLookup(d) {
|
|
142
|
+
if (typeof d.localSession === 'function') return d.localSession;
|
|
143
|
+
const lookup = _peer('localSession', d);
|
|
144
|
+
return typeof lookup === 'function' ? lookup : null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* "What instance id is THIS hub?" — server.js wires it to db.js's hosted identity. The ingest fence
|
|
149
|
+
* needs it to recognise a box row that echoes the hub's own sessions (D-1 clause b). Null when
|
|
150
|
+
* unwired: clause (b) then faults and passes the row (D-2 fail-open).
|
|
151
|
+
*/
|
|
152
|
+
function _localInstanceIdFn(d) {
|
|
153
|
+
if (typeof d.localInstanceId === 'function') return d.localInstanceId;
|
|
154
|
+
const fn = _peer('localInstanceId', d);
|
|
155
|
+
return typeof fn === 'function' ? fn : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* "Is this instance id lineage-linked to ours?" (lib/host-identity.js's graph closure through
|
|
160
|
+
* db.js's isSameOwnerLineage, which defaults its second argument to this process's own id). A
|
|
161
|
+
* previous id of the SAME host — a re-mint — is the same owner, so its rows are echoes too (D-1).
|
|
162
|
+
*/
|
|
163
|
+
function _ownerLineageFn(d) {
|
|
164
|
+
if (typeof d.ownerLineage === 'function') return d.ownerLineage;
|
|
165
|
+
const fn = _peer('ownerLineage', d);
|
|
166
|
+
return typeof fn === 'function' ? fn : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* "Does this hub's DURABLE ledger hold the id, and who owns it?" (db.js getCtmSessionOwnerRow —
|
|
171
|
+
* ctm_sessions, the one table that still knows an ENDED session). Clause (d) needs it because an
|
|
172
|
+
* ended session is in neither the sessions Map (clause c) nor startup_tasks, yet a box clone row
|
|
173
|
+
* for it can carry a restamped owner that defeats clause (b). Null when unwired: fail-open (D-2).
|
|
174
|
+
*/
|
|
175
|
+
function _ledgerOwnerFn(d) {
|
|
176
|
+
if (typeof d.ledgerOwner === 'function') return d.ledgerOwner;
|
|
177
|
+
const handle = d.db || _peer('db', d);
|
|
178
|
+
const fn = handle && typeof handle.getCtmSessionOwnerRow === 'function' ? handle.getCtmSessionOwnerRow : null;
|
|
179
|
+
return fn ? (id) => fn(id) : null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The in-memory create registrations (lib/remote-machine-create.js), through the wired peer. */
|
|
183
|
+
function _localMirrorLookup(d) {
|
|
184
|
+
if (typeof d.localMirror === 'function') return d.localMirror;
|
|
185
|
+
const create = _peer('create', d);
|
|
186
|
+
if (create && typeof create.getLocalMirrorSession === 'function') {
|
|
187
|
+
return (sessionId) => create.getLocalMirrorSession(sessionId);
|
|
188
|
+
}
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function _d(overrides) {
|
|
193
|
+
return Object.assign({}, _productionDeps(), _injected || {}, overrides || {});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function _report(d, message) {
|
|
197
|
+
// REPORT (never throw): a catalog fault is a degraded remote list, not a hub fault.
|
|
198
|
+
try {
|
|
199
|
+
if (d && d.log && typeof d.log.warn === 'function') d.log.warn(`[remote-machine-catalog] ${message}`);
|
|
200
|
+
} catch { /* expected: an injected log that throws must not break the caller */ }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------------------------
|
|
204
|
+
// Machine registry snapshot
|
|
205
|
+
// ---------------------------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
/** The registered machines (registry view rows). Cached for `registryTtlMs` — see the note above. */
|
|
208
|
+
function machineRows(deps = {}) {
|
|
209
|
+
const d = _d(deps);
|
|
210
|
+
const now = d.now();
|
|
211
|
+
const ttl = Number(d.registryTtlMs || REGISTRY_TTL_MS) || REGISTRY_TTL_MS;
|
|
212
|
+
let registry = null;
|
|
213
|
+
try {
|
|
214
|
+
registry = d.registry || require('./machine-registry');
|
|
215
|
+
} catch {
|
|
216
|
+
registry = null; // expected: a pure unit harness without the registry module -> zero machines
|
|
217
|
+
}
|
|
218
|
+
// A registry write (register/remove/seed/probe) invalidates the snapshot at once: within the TTL
|
|
219
|
+
// alone, a snapshot taken just before a registration hid the new machine from the relay lookup
|
|
220
|
+
// (live: a hub viewer's attach for a just-created box session fell through to "Session not found").
|
|
221
|
+
const revision = registry && typeof registry.registryRevision === 'function' ? registry.registryRevision() : null;
|
|
222
|
+
if (_registryCache.at && now - _registryCache.at < ttl && _registryCache.revision === revision) return _registryCache.machines;
|
|
223
|
+
let machines = [];
|
|
224
|
+
try {
|
|
225
|
+
const listed = registry ? (registry.listMachines({ db: d.db || _peer('db', d) }) || {}) : {};
|
|
226
|
+
machines = Array.isArray(listed.machines) ? listed.machines : [];
|
|
227
|
+
} catch {
|
|
228
|
+
machines = []; // expected: pre-boot / no machines table yet -> zero machines, zero remote rows
|
|
229
|
+
}
|
|
230
|
+
_registryCache.machines = machines;
|
|
231
|
+
_registryCache.at = now;
|
|
232
|
+
_registryCache.revision = revision;
|
|
233
|
+
return machines;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function _enabledMachines(d) {
|
|
237
|
+
return machineRows(d).filter((machine) => machine && machine.enabled !== false);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function _findMachine(machineId, d) {
|
|
241
|
+
const id = _string(machineId, 160);
|
|
242
|
+
if (!id) return null;
|
|
243
|
+
return machineRows(d).find((machine) => _string(machine.instanceId, 160) === id) || null;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* "Is this instance id THIS host, or an id lineage-linked to it?" — the SAME test the WP-1
|
|
248
|
+
* ownership fences use (D-1 clause b), so a recent row and a sidebar row can never disagree about
|
|
249
|
+
* which sessions are local (D-10). A previous id of the SAME host (a re-mint) counts as local.
|
|
250
|
+
*
|
|
251
|
+
* FAIL-OPEN, in the D-2 direction: a peer that THROWS returns true ("treat as local"), which hides
|
|
252
|
+
* a chip rather than claiming a local session lives on a box. An unwired peer is not evidence of
|
|
253
|
+
* locality, so it returns false.
|
|
254
|
+
*/
|
|
255
|
+
function isLocalOwnerId(instanceId, deps = {}) {
|
|
256
|
+
const id = _string(instanceId, 160);
|
|
257
|
+
if (!id) return false;
|
|
258
|
+
const d = _d(deps);
|
|
259
|
+
try {
|
|
260
|
+
const localIdFn = _localInstanceIdFn(d);
|
|
261
|
+
if (localIdFn && _string(localIdFn(), 160) === id) return true;
|
|
262
|
+
} catch { return true; /* expected: a faulty identity peer hides the chip (D-2 fail-open) */ }
|
|
263
|
+
try {
|
|
264
|
+
const lineageFn = _ownerLineageFn(d);
|
|
265
|
+
if (lineageFn && lineageFn(id) === true) return true;
|
|
266
|
+
} catch { return true; /* expected: a faulty lineage peer hides the chip (D-2 fail-open) */ }
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* WP-3 D-9/D-10: the ONE cheap input both HTTP surfaces that MISS the §6.6 merge stamp from — the
|
|
272
|
+
* ENABLED registered machines (`{instanceId, displayName}`, never a hostname: D-3/S-5) and the set
|
|
273
|
+
* of ids that are LOCAL to this host (its own minted instance id, plus any registered id that is
|
|
274
|
+
* the same host or a lineage-linked re-mint of it — `isLocalOwnerId`).
|
|
275
|
+
*
|
|
276
|
+
* Pure in-memory registry snapshot (the `machineRows` TTL cache); nothing here opens a handle. A
|
|
277
|
+
* throwing identity/lineage peer degrades to "no extra local id" and is reported, never thrown.
|
|
278
|
+
*
|
|
279
|
+
* @returns {{machines: Array<{instanceId: string, displayName: string}>, localIds: string[]}}
|
|
280
|
+
*/
|
|
281
|
+
function machineTagInputs(deps = {}) {
|
|
282
|
+
const d = _d(deps);
|
|
283
|
+
const localIds = [];
|
|
284
|
+
try {
|
|
285
|
+
const localIdFn = _localInstanceIdFn(d);
|
|
286
|
+
const localId = localIdFn ? _string(localIdFn(), 160) : '';
|
|
287
|
+
if (localId) localIds.push(localId);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
_report(d, `machineTagInputs: local identity unavailable: ${error?.message || error}`);
|
|
290
|
+
}
|
|
291
|
+
const machines = _enabledMachines(d)
|
|
292
|
+
.map((machine) => ({
|
|
293
|
+
instanceId: _string(machine.instanceId, 160),
|
|
294
|
+
displayName: _string(machine.displayName, 200),
|
|
295
|
+
}))
|
|
296
|
+
.filter((machine) => machine.instanceId);
|
|
297
|
+
// D-10: "names a registered machine AND is not this host or its lineage". The host's own id is
|
|
298
|
+
// local by definition (above); a REGISTERED machine that is lineage-linked to this host is the
|
|
299
|
+
// same owner, so its id joins `localIds` and can never label a row as a box.
|
|
300
|
+
for (const machine of machines) {
|
|
301
|
+
if (localIds.includes(machine.instanceId)) continue;
|
|
302
|
+
if (isLocalOwnerId(machine.instanceId, d)) localIds.push(machine.instanceId);
|
|
303
|
+
}
|
|
304
|
+
return { machines, localIds };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---------------------------------------------------------------------------------------------
|
|
308
|
+
// WP-1 ingest fence (docs/remote-hub-ownership-fences-and-restore-skip-diagnostics-2026-10-23.html
|
|
309
|
+
// §5 D-1/D-2/D-10, §8 AC-1a..AC-1e, §9 S-1)
|
|
310
|
+
// ---------------------------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
// The five rejection reasons, IN CLAUSE ORDER. `fenceFault` is not a clause: it counts a clause
|
|
313
|
+
// that could not be evaluated (a throwing or unwired peer) — the D-2 fail-open path.
|
|
314
|
+
const FENCE_REASONS = Object.freeze(['thirdPartyTag', 'ownEcho', 'localLive', 'localLedger', 'fenceFault']);
|
|
315
|
+
// D-1 clause (b): first NON-EMPTY wins, in this order. Origin fields (`originInstanceId`,
|
|
316
|
+
// `origin_instance_id`) are deliberately absent: origin is write-once provenance, not a live
|
|
317
|
+
// ownership pointer (db.js:1438), so a session genuinely transferred TO the box keeps hub origin
|
|
318
|
+
// and must keep working.
|
|
319
|
+
const OWNER_FIELDS = Object.freeze([
|
|
320
|
+
'currentOwnerInstanceId', 'current_owner_instance_id', 'ownerInstanceId', 'owner_instance_id',
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
// Cumulative, process-lifetime: machineInstanceId -> { thirdPartyTag, ownEcho, localLive, localLedger, fenceFault }.
|
|
324
|
+
const _fenceCounters = new Map();
|
|
325
|
+
const _fenceTotals = { thirdPartyTag: 0, ownEcho: 0, localLive: 0, localLedger: 0, fenceFault: 0 };
|
|
326
|
+
|
|
327
|
+
// One warn per (machine, clause) window; the window's repeats collapse into a single summary line
|
|
328
|
+
// (shared/log-dedupe.js, 600 s default — the state-sync-ack-stall precedent). The sink is the
|
|
329
|
+
// CURRENT caller's `d.log` through the module's own `_report`, so this stays the same
|
|
330
|
+
// "[remote-machine-catalog] …" channel as every other report here and never throws.
|
|
331
|
+
// NOTE: the composite key separator must be the ESCAPED \u0000 form, never a raw control byte.
|
|
332
|
+
let _fenceLogDeps = null;
|
|
333
|
+
const _fenceLog = makeDedupeLogger({
|
|
334
|
+
logFn: (line) => _report(_fenceLogDeps, line),
|
|
335
|
+
formatSummary: (key, count, windowMs) => {
|
|
336
|
+
const [machine, reason] = String(key).split('\u0000');
|
|
337
|
+
return `ingest-fence: ${count} rows rejected by ${reason} from ${machine || '(unknown machine)'} in ${windowMs} ms (further repeats collapsed)`;
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// D-10: the ONE kill switch, registered at its owner so the flag inventory sees it (read-pool-client
|
|
342
|
+
// precedent). '0' restores the pre-WP-1 verbatim ingest; READ through lib/env-flags.js, never a raw
|
|
343
|
+
// process.env read (the flag-read lint's rule).
|
|
344
|
+
registerFlag('CTM_HUB_INGEST_FENCE', {
|
|
345
|
+
kind: 'bool',
|
|
346
|
+
default: true,
|
|
347
|
+
desc: 'Hub ingest fence (WP-1 D-1): drop box rows this hub does not own (third-party-tagged, echoing this hub\'s instance id or a lineage-linked id, naming a live local session, or held in this hub\'s own session ledger — ended sessions included). 0 restores verbatim ingest.',
|
|
348
|
+
expires: null,
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
function _fenceEnabled() {
|
|
352
|
+
try {
|
|
353
|
+
return boolFlag('CTM_HUB_INGEST_FENCE', true);
|
|
354
|
+
} catch { /* expected: an env-flags fault must not change ingest at all */ return true; }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/** D-1 clause (b): the row's owner field, first non-empty wins. '' when the row names no owner. */
|
|
358
|
+
function _rowOwner(row) {
|
|
359
|
+
for (const field of OWNER_FIELDS) {
|
|
360
|
+
const owner = _string(row[field], 160);
|
|
361
|
+
if (owner) return owner;
|
|
362
|
+
}
|
|
363
|
+
return '';
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function _bumpFenceCounters(machineId, rejection) {
|
|
367
|
+
let row = _fenceCounters.get(machineId);
|
|
368
|
+
if (!row) {
|
|
369
|
+
row = { thirdPartyTag: 0, ownEcho: 0, localLive: 0, localLedger: 0, fenceFault: 0 };
|
|
370
|
+
_fenceCounters.set(machineId, row);
|
|
371
|
+
}
|
|
372
|
+
for (const reason of FENCE_REASONS) {
|
|
373
|
+
const count = rejection[reason] || 0;
|
|
374
|
+
if (!count) continue;
|
|
375
|
+
row[reason] += count;
|
|
376
|
+
_fenceTotals[reason] += count;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function _reportFenceRejection(d, machineId, reason, id) {
|
|
381
|
+
_fenceLogDeps = d;
|
|
382
|
+
_fenceLog.log(`${machineId}\u0000${reason}`,
|
|
383
|
+
() => `ingest-fence rejected a row: clause=${reason} machine=${machineId || '(unnamed)'} id=${id} — the sender does not own it (D-1)`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* D-1, the four-clause drop predicate, FIRST MATCH WINS. Each clause is individually guarded
|
|
388
|
+
* (D-2): a clause that cannot be evaluated — a throwing peer, or one this process never wired —
|
|
389
|
+
* PASSES the row and counts `fenceFault`, exactly like the relay guard's fail-open. A catalog
|
|
390
|
+
* fault must degrade to "the remote list is slightly wrong", never to "a genuine box session lost
|
|
391
|
+
* its relay/display".
|
|
392
|
+
*
|
|
393
|
+
* @returns {{reason: string|null, faults: string[]}} the clause that dropped the row (null = keep)
|
|
394
|
+
*/
|
|
395
|
+
function _fenceVerdict(machineId, row, id, d) {
|
|
396
|
+
const faults = [];
|
|
397
|
+
// Clause 1 (D-1a): the SENDER itself tags the row as a third machine's (the box's own merge marks
|
|
398
|
+
// provenance — lib/remote-machine-catalog.js MACHINE_FIELDS). No peer, so no fault path in
|
|
399
|
+
// practice; the guard is structural.
|
|
400
|
+
try {
|
|
401
|
+
const tag = _string(row.machine, 160);
|
|
402
|
+
if (tag && tag !== machineId) return { reason: 'thirdPartyTag', faults };
|
|
403
|
+
} catch { faults.push('thirdPartyTag'); /* expected: an unreadable row never drops and never throws */ }
|
|
404
|
+
|
|
405
|
+
// Clause 2 (D-1b): the row's OWNER names this hub, or an id lineage-linked to it. Identity-based,
|
|
406
|
+
// so it holds for a phantom id that is not (yet) in the sessions Map — the §9 S-2/S-3 case.
|
|
407
|
+
try {
|
|
408
|
+
const owner = _rowOwner(row);
|
|
409
|
+
const localIdFn = owner ? _localInstanceIdFn(d) : null;
|
|
410
|
+
const lineageFn = owner ? _ownerLineageFn(d) : null;
|
|
411
|
+
if (owner && (!localIdFn || !lineageFn)) faults.push('ownEcho');
|
|
412
|
+
if (owner) {
|
|
413
|
+
const localId = localIdFn ? _string(localIdFn(), 160) : '';
|
|
414
|
+
if (localId && owner === localId) return { reason: 'ownEcho', faults };
|
|
415
|
+
if (lineageFn && lineageFn(owner) === true) return { reason: 'ownEcho', faults };
|
|
416
|
+
}
|
|
417
|
+
} catch { faults.push('ownEcho'); /* expected: a faulty identity/lineage peer fails OPEN (D-2) */ }
|
|
418
|
+
|
|
419
|
+
// Clause 3 (D-1c): the id is a session THIS instance already holds locally (live or a deferred
|
|
420
|
+
// boot placeholder). The belt to clause 2's braces, and the only clause that can fire for an
|
|
421
|
+
// older box that sends no owner field at all.
|
|
422
|
+
try {
|
|
423
|
+
const localSession = _localSessionLookup(d);
|
|
424
|
+
if (!localSession) faults.push('localLive');
|
|
425
|
+
else if (localSession(id)) return { reason: 'localLive', faults };
|
|
426
|
+
} catch { faults.push('localLive'); /* expected: a faulty local-lookup fails OPEN (D-2) */ }
|
|
427
|
+
|
|
428
|
+
// Clause 4 (D-1d, the ENDED-session hole distributed-10 reported): this hub's durable ledger
|
|
429
|
+
// (ctm_sessions) holds the id as its OWN — owner blank (pre-multi-machine rows are local by
|
|
430
|
+
// definition, db.js:15665 precedent), this instance, or lineage-linked. An ended session is in
|
|
431
|
+
// neither the sessions Map nor startup_tasks, and a file-cloned box lists it with owner fields
|
|
432
|
+
// restamped to ITSELF, so clauses 2 and 3 both miss it. A ledger row owned by the SENDER (a
|
|
433
|
+
// session genuinely transferred to the box) passes — ownership, never origin, decides (D-1b).
|
|
434
|
+
try {
|
|
435
|
+
const ledgerFn = _ledgerOwnerFn(d);
|
|
436
|
+
if (!ledgerFn) faults.push('localLedger');
|
|
437
|
+
else {
|
|
438
|
+
const ledger = ledgerFn(id);
|
|
439
|
+
if (ledger && ledger.exists) {
|
|
440
|
+
const owner = _string(ledger.currentOwner, 160);
|
|
441
|
+
if (!owner) return { reason: 'localLedger', faults };
|
|
442
|
+
const localIdFn = _localInstanceIdFn(d);
|
|
443
|
+
const lineageFn = _ownerLineageFn(d);
|
|
444
|
+
if (!localIdFn || !lineageFn) faults.push('localLedger');
|
|
445
|
+
const localId = localIdFn ? _string(localIdFn(), 160) : '';
|
|
446
|
+
if (localId && owner === localId) return { reason: 'localLedger', faults };
|
|
447
|
+
if (lineageFn && lineageFn(owner) === true) return { reason: 'localLedger', faults };
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
} catch { faults.push('localLedger'); /* expected: a faulty ledger/identity peer fails OPEN (D-2) */ }
|
|
451
|
+
|
|
452
|
+
return { reason: null, faults };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** The cumulative ingest-fence counters (the pending-client-registry snapshot() shape). */
|
|
456
|
+
function ingestFenceCounters() {
|
|
457
|
+
const byMachine = {};
|
|
458
|
+
for (const [machineId, row] of _fenceCounters) byMachine[machineId] = { ...row };
|
|
459
|
+
return { byMachine, total: { ..._fenceTotals } };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ---------------------------------------------------------------------------------------------
|
|
463
|
+
// Live catalog
|
|
464
|
+
// ---------------------------------------------------------------------------------------------
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Store a box's session list. Exported because the link also calls it for the box's UNSOLICITED
|
|
468
|
+
* `sessions` broadcasts (the box re-broadcasts its list on every session-list change), which is the
|
|
469
|
+
* cheapest possible freshness signal for the hub's sidebar.
|
|
470
|
+
*
|
|
471
|
+
* WP-1: rows the SENDER does not own are dropped here (the four D-1 clauses, first match wins) and
|
|
472
|
+
* tallied — see `_fenceVerdict` and `ingestFenceCounters()`.
|
|
473
|
+
*
|
|
474
|
+
* @returns {{rows: Array, byId: Map, fetchedAt: number, ok: true,
|
|
475
|
+
* rejected?: {thirdPartyTag: number, ownEcho: number, localLive: number, localLedger: number, fenceFault: number}}}
|
|
476
|
+
* `rejected` is present only when at least one counter is non-zero.
|
|
477
|
+
*/
|
|
478
|
+
function recordSessionList(machineInstanceId, rows, deps = {}) {
|
|
479
|
+
const d = _d(deps);
|
|
480
|
+
const machineId = _string(machineInstanceId, 160);
|
|
481
|
+
const byId = new Map();
|
|
482
|
+
const clean = [];
|
|
483
|
+
// WP-1 (D-1): the fence runs HERE, on the one choke point above every consumer of catalog rows
|
|
484
|
+
// (§9 S-1), before the id-dedupe ever sees the row. Flag off (D-10) = the pre-WP-1 verbatim loop.
|
|
485
|
+
const fencing = _fenceEnabled();
|
|
486
|
+
const rejected = { thirdPartyTag: 0, ownEcho: 0, localLive: 0, localLedger: 0, fenceFault: 0 };
|
|
487
|
+
for (const row of Array.isArray(rows) ? rows : []) {
|
|
488
|
+
if (!row || typeof row !== 'object') continue;
|
|
489
|
+
const id = _string(row.id || row.sessionId, 200);
|
|
490
|
+
// Deduped by ctm session id: the box listing one id twice must not produce two hub rows.
|
|
491
|
+
if (!id || byId.has(id)) continue;
|
|
492
|
+
if (fencing) {
|
|
493
|
+
const { reason, faults } = _fenceVerdict(machineId, row, id, d);
|
|
494
|
+
if (faults.length) rejected.fenceFault += faults.length;
|
|
495
|
+
if (reason) {
|
|
496
|
+
rejected[reason] += 1;
|
|
497
|
+
_reportFenceRejection(d, machineId, reason, id);
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
byId.set(id, row);
|
|
502
|
+
clean.push(row);
|
|
503
|
+
}
|
|
504
|
+
const entry = { rows: clean, byId, fetchedAt: d.now(), ok: true };
|
|
505
|
+
if (rejected.thirdPartyTag || rejected.ownEcho || rejected.localLive || rejected.localLedger || rejected.fenceFault) {
|
|
506
|
+
entry.rejected = rejected;
|
|
507
|
+
_bumpFenceCounters(machineId, rejected);
|
|
508
|
+
}
|
|
509
|
+
if (machineId) _catalogs.set(machineId, entry);
|
|
510
|
+
_notifyChanged(d);
|
|
511
|
+
return entry;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Ask the hub to re-broadcast its session list (the wired `onChange` peer: the DEBOUNCED
|
|
516
|
+
* broadcastSessionList). Cheap to over-call: an unchanged merge has an unchanged signature, so the
|
|
517
|
+
* broadcast is elided.
|
|
518
|
+
*/
|
|
519
|
+
function _notifyChanged(d) {
|
|
520
|
+
const onChange = _peer('onChange', d);
|
|
521
|
+
if (typeof onChange !== 'function') return;
|
|
522
|
+
try { onChange(); } catch (error) { _report(d, `onChange failed: ${error?.message || error}`); }
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** The remote half of the last merge ('' with zero machines) — see _lastMergeSignature. */
|
|
526
|
+
function lastMergeSignature() {
|
|
527
|
+
return _lastMergeSignature;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function catalogFor(machineInstanceId) {
|
|
531
|
+
return _catalogs.get(_string(machineInstanceId, 160)) || null;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/** The ids the latest catalog holds for a registered machine (a Set; empty when never fetched). */
|
|
535
|
+
function catalogSessionIds(machineInstanceId) {
|
|
536
|
+
const entry = catalogFor(machineInstanceId);
|
|
537
|
+
return entry ? new Set(entry.byId.keys()) : new Set();
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* S-3/AC-6a: fetch one machine's LIVE session list over the WP-3 machine link.
|
|
542
|
+
*
|
|
543
|
+
* Never throws and never rejects: an unreachable box, a missing credential or a link that never
|
|
544
|
+
* answers all resolve to `{ok:false}` and leave the previous catalog (if any) in place, which is
|
|
545
|
+
* what degrades a dropped box to its sync-mirror look instead of emptying the sidebar.
|
|
546
|
+
*/
|
|
547
|
+
async function fetchCatalog(machineOrId, deps = {}) {
|
|
548
|
+
const d = _d(deps);
|
|
549
|
+
try {
|
|
550
|
+
const row = machineOrId && typeof machineOrId === 'object' ? machineOrId : _findMachine(machineOrId, d);
|
|
551
|
+
if (!row) return { ok: false, code: 'machine_unknown' };
|
|
552
|
+
const machineId = _string(row.instanceId, 160);
|
|
553
|
+
if (!machineId) return { ok: false, code: 'machine_unknown' };
|
|
554
|
+
if (row.enabled === false) return { ok: false, code: 'machine_disabled' };
|
|
555
|
+
if (_string(row.health, 40) !== 'reachable') return { ok: false, code: 'machine_unreachable' };
|
|
556
|
+
const link = _peer('link', d);
|
|
557
|
+
if (!link || typeof link.openLink !== 'function' || typeof link.requestSessionList !== 'function') {
|
|
558
|
+
// Degraded, never a throw: an unwired catalog must not break a create/sweep caller.
|
|
559
|
+
return { ok: false, code: 'machine_unreachable', error: 'catalog_link_not_wired' };
|
|
560
|
+
}
|
|
561
|
+
const opened = link.openLink(row, d);
|
|
562
|
+
if (!opened) return { ok: false, code: 'machine_unreachable' };
|
|
563
|
+
const reply = await link.requestSessionList(opened, d);
|
|
564
|
+
if (!reply || !reply.ok) {
|
|
565
|
+
return { ok: false, code: (reply && reply.code) || 'machine_unreachable', error: (reply && reply.error) || '' };
|
|
566
|
+
}
|
|
567
|
+
const entry = recordSessionList(machineId, reply.rows, d);
|
|
568
|
+
return { ok: true, machineInstanceId: machineId, rowCount: entry.rows.length, fetchedAt: entry.fetchedAt };
|
|
569
|
+
} catch (error) {
|
|
570
|
+
// REPORT via the return value; the caller (a sweep or a fire-and-forget hook) decides.
|
|
571
|
+
_report(d, `catalog fetch failed: ${error?.message || error}`);
|
|
572
|
+
return { ok: false, code: 'machine_unreachable', error: _string(error?.message || error, 300) };
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* The probe-sweep hook (lib/hosted-sync-worker.js, where the sweep result is available — never a
|
|
578
|
+
* second timer). Every ENABLED machine the sweep judged `reachable` gets a live list; a machine the
|
|
579
|
+
* sweep judged otherwise keeps whatever catalog it had, so its rows degrade to the sync-mirror look.
|
|
580
|
+
*/
|
|
581
|
+
function refreshAll(deps = {}) {
|
|
582
|
+
const d = _d(deps);
|
|
583
|
+
const enabled = _enabledMachines(d);
|
|
584
|
+
const reachable = enabled.filter((machine) => _string(machine.health, 40) === 'reachable');
|
|
585
|
+
if (!reachable.length) {
|
|
586
|
+
// Zero machines (the single-machine install) stays silent; registered-but-all-down re-broadcasts.
|
|
587
|
+
if (enabled.length) _notifyChanged(d);
|
|
588
|
+
return Promise.resolve({ ok: true, refreshed: 0 });
|
|
589
|
+
}
|
|
590
|
+
return Promise.all(reachable.map((machine) => fetchCatalog(machine, d)))
|
|
591
|
+
.then((results) => {
|
|
592
|
+
// A sweep can flip a machine's health with no new list (a dropped box): re-broadcast so the
|
|
593
|
+
// sidebar degrades to the mirror look now, not at the next unrelated local change.
|
|
594
|
+
_notifyChanged(d);
|
|
595
|
+
return { ok: true, refreshed: results.filter((result) => result && result.ok).length, results };
|
|
596
|
+
})
|
|
597
|
+
.catch((error) => {
|
|
598
|
+
_report(d, `catalog sweep failed: ${error?.message || error}`);
|
|
599
|
+
return { ok: false, refreshed: 0, error: _string(error?.message || error, 300) };
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Link (re)connect hook: the box just said hello, so ask it for its list on this very link. */
|
|
604
|
+
function onLinkReady(link, deps = {}) {
|
|
605
|
+
const d = _d(deps);
|
|
606
|
+
const machineId = _string(link && link.machineInstanceId, 160);
|
|
607
|
+
if (!machineId) return Promise.resolve({ ok: false, code: 'machine_unknown' });
|
|
608
|
+
const row = _findMachine(machineId, d);
|
|
609
|
+
if (!row || row.enabled === false || _string(row.health, 40) !== 'reachable') {
|
|
610
|
+
return Promise.resolve({ ok: false, code: 'machine_unreachable' });
|
|
611
|
+
}
|
|
612
|
+
return fetchCatalog(row, d);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ---------------------------------------------------------------------------------------------
|
|
616
|
+
// Sync mirrors (at-rest)
|
|
617
|
+
// ---------------------------------------------------------------------------------------------
|
|
618
|
+
|
|
619
|
+
function _readSyncMirrorRows(d) {
|
|
620
|
+
const byId = new Map();
|
|
621
|
+
try {
|
|
622
|
+
const handle = d.db || _peer('db', d);
|
|
623
|
+
if (!handle || typeof handle.listStartupTasks !== 'function') return byId;
|
|
624
|
+
// includeParked/includeRemoteOwned: an at-rest box mirror IS a parked row owned by the box —
|
|
625
|
+
// the restore-path defaults filter exactly these out, which is why they are named here.
|
|
626
|
+
const tasks = handle.listStartupTasks({ includeParked: true, includeRemoteOwned: true }) || [];
|
|
627
|
+
for (const task of tasks) {
|
|
628
|
+
if (_string(task.park_reason, 60) !== SYNC_MIRROR_PARK_REASON) continue;
|
|
629
|
+
const id = _string(task.ctm_session_id || task.session_id, 200);
|
|
630
|
+
const machineInstanceId = _string(task.current_owner_instance_id || task.origin_instance_id, 160);
|
|
631
|
+
if (!id || !machineInstanceId) continue;
|
|
632
|
+
byId.set(id, { machineInstanceId, task });
|
|
633
|
+
}
|
|
634
|
+
} catch {
|
|
635
|
+
// expected: pre-boot / a database without startup_tasks -> no sync mirrors, which under-claims
|
|
636
|
+
}
|
|
637
|
+
return byId;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function _syncMirrorIndex(d) {
|
|
641
|
+
const now = d.now();
|
|
642
|
+
const ttl = Number(d.syncMirrorTtlMs || SYNC_MIRROR_TTL_MS) || SYNC_MIRROR_TTL_MS;
|
|
643
|
+
if (!_syncMirrorCache.at || now - _syncMirrorCache.at >= ttl) {
|
|
644
|
+
_syncMirrorCache.byId = _readSyncMirrorRows(d);
|
|
645
|
+
_syncMirrorCache.at = now;
|
|
646
|
+
}
|
|
647
|
+
return _syncMirrorCache.byId;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** The degraded (parked-look) row a box session gets while its machine is not serving live state. */
|
|
651
|
+
function _mirrorSessionRow(sessionId, task, machine, health) {
|
|
652
|
+
const row = task || {};
|
|
653
|
+
const id = _string(sessionId || row.ctm_session_id || row.session_id, 200);
|
|
654
|
+
const type = _string(row.type, 40) || 'pty';
|
|
655
|
+
return {
|
|
656
|
+
id,
|
|
657
|
+
type,
|
|
658
|
+
session_type: type,
|
|
659
|
+
runtime_type: type,
|
|
660
|
+
label: _string(row.label, 400),
|
|
661
|
+
// S-27/D-4: `row.title` was a dead read — the mirrored row IS a startup_tasks row, and that table
|
|
662
|
+
// has no `title` column (db.js ~6749-6756), so every parked hub row carried title:'' and a client
|
|
663
|
+
// that prefers `title` over `label` showed nothing. The sync payload's task object is built from
|
|
664
|
+
// `_startupTaskRow` (startup_tasks columns only, db.js ~3150) and the applier persists only
|
|
665
|
+
// startup_tasks columns (db.js ~2728), so there is no box-side title field on the wire: the row's
|
|
666
|
+
// own label is the box's name for it, and that is what `title` must carry.
|
|
667
|
+
title: _string(row.label, 400),
|
|
668
|
+
cwd: _string(row.cwd, 4000),
|
|
669
|
+
cmd: _string(row.cmd, 4000),
|
|
670
|
+
// The at-rest mirror's status IS 'parked' (db.js's applier writes exactly that for a row whose
|
|
671
|
+
// owner is another instance), and the lifecycle fields below are the client's parked card.
|
|
672
|
+
status: 'parked',
|
|
673
|
+
lifecycleState: 'parked',
|
|
674
|
+
parkedAt: _string(row.parked_at, 60),
|
|
675
|
+
parkReason: SYNC_MIRROR_PARK_REASON,
|
|
676
|
+
executionHostInstanceId: _string(row.current_owner_instance_id || row.origin_instance_id, 160),
|
|
677
|
+
createdAt: _string(row.created_at, 60),
|
|
678
|
+
// §6.6/D-3: the machine tag. `machineName` is the registry DISPLAY NAME — never a hostname (S-5).
|
|
679
|
+
machine: _string(machine.instanceId, 160),
|
|
680
|
+
machineName: _string(machine.displayName, 200),
|
|
681
|
+
machineHealth: _string(health || machine.health, 40) || 'unknown',
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// ---------------------------------------------------------------------------------------------
|
|
686
|
+
// The read seam (WP-3's intercept calls this for every relayable frame)
|
|
687
|
+
// ---------------------------------------------------------------------------------------------
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* S-3/S-27/§7: which machine owns this ctm session id — the UNION of the three sources above, and
|
|
691
|
+
* the single read seam lib/remote-machine-link.js's pre-dispatch hook consults (through
|
|
692
|
+
* lib/remote-machine-create.js's `getMirrorSessionMachine`).
|
|
693
|
+
*
|
|
694
|
+
* Precedence: this process's forwarded creates (newest knowledge) > the live catalog > the at-rest
|
|
695
|
+
* sync mirror. A machine that is not registered can never claim an id, so an unregistered owner
|
|
696
|
+
* keeps today's observe-only behaviour.
|
|
697
|
+
*
|
|
698
|
+
* @returns {{machineInstanceId: string, machineName: string, health: string, source: string, at: number}|null}
|
|
699
|
+
*/
|
|
700
|
+
function getMirrorSessionMachine(ctmSessionId, deps = {}) {
|
|
701
|
+
const id = _string(ctmSessionId, 200);
|
|
702
|
+
if (!id) return null;
|
|
703
|
+
const d = _d(deps);
|
|
704
|
+
// A LOCAL session id always wins (see _localSessionLookup — the 2026-10-23 relay-hijack incident):
|
|
705
|
+
// whatever a box lists or a mirror row says, a session this instance holds in its own sessions Map
|
|
706
|
+
// is never a mirror, so its WS plane (acks, bootstraps, input, wakes) stays on the local handlers.
|
|
707
|
+
const localSession = _localSessionLookup(d);
|
|
708
|
+
if (localSession) {
|
|
709
|
+
try {
|
|
710
|
+
if (localSession(id)) return null;
|
|
711
|
+
} catch { /* expected: a local-lookup fault must fail OPEN — a genuine box session keeps its relay */ }
|
|
712
|
+
}
|
|
713
|
+
const localMirrorFn = _localMirrorLookup(d);
|
|
714
|
+
const local = localMirrorFn ? localMirrorFn(id) : null;
|
|
715
|
+
if (local && local.machineInstanceId) {
|
|
716
|
+
const machine = _findMachine(local.machineInstanceId, d);
|
|
717
|
+
// A registration removed while the process still holds the map entry no longer claims relaying:
|
|
718
|
+
// an unregistered owner keeps today's behaviour (S-7).
|
|
719
|
+
if (!machine || machine.enabled === false) return null;
|
|
720
|
+
return {
|
|
721
|
+
machineInstanceId: _string(machine.instanceId, 160),
|
|
722
|
+
machineName: _string(machine.displayName, 200),
|
|
723
|
+
health: _string(machine.health, 40) || 'unknown',
|
|
724
|
+
source: 'create',
|
|
725
|
+
at: Number(local.at) || 0,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
// Zero registered machines ⇒ no catalog and no mirror scan at all: the relay path is exactly
|
|
729
|
+
// today's (one map miss) for every frame of every local session.
|
|
730
|
+
const machines = _enabledMachines(d);
|
|
731
|
+
if (!machines.length) return null;
|
|
732
|
+
for (const machine of machines) {
|
|
733
|
+
const entry = catalogFor(machine.instanceId);
|
|
734
|
+
if (entry && entry.byId.has(id)) {
|
|
735
|
+
return {
|
|
736
|
+
machineInstanceId: _string(machine.instanceId, 160),
|
|
737
|
+
machineName: _string(machine.displayName, 200),
|
|
738
|
+
health: _string(machine.health, 40) || 'unknown',
|
|
739
|
+
source: 'catalog',
|
|
740
|
+
at: entry.fetchedAt,
|
|
741
|
+
};
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
const mirror = _syncMirrorIndex(d).get(id);
|
|
745
|
+
if (mirror) {
|
|
746
|
+
const machine = machines.find((row) => row.instanceId === mirror.machineInstanceId);
|
|
747
|
+
if (machine) {
|
|
748
|
+
return {
|
|
749
|
+
machineInstanceId: _string(machine.instanceId, 160),
|
|
750
|
+
machineName: _string(machine.displayName, 200),
|
|
751
|
+
health: _string(machine.health, 40) || 'unknown',
|
|
752
|
+
source: 'sync-mirror',
|
|
753
|
+
at: 0,
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// ---------------------------------------------------------------------------------------------
|
|
761
|
+
// §6.6 merge (S-3/S-4, AC-6c)
|
|
762
|
+
// ---------------------------------------------------------------------------------------------
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* The remote rows this hub adds to its session list, one per box session, deduped by ctm session id.
|
|
766
|
+
* A REACHABLE machine contributes its LIVE rows (its own state); anything else contributes the
|
|
767
|
+
* at-rest sync mirror rows for that machine — the parked look plus `machineHealth` (§6.6, AC-6a's
|
|
768
|
+
* degradation half) — and, for a session with no mirror row, its last good live row in that look.
|
|
769
|
+
*/
|
|
770
|
+
function remoteRows(deps = {}) {
|
|
771
|
+
const d = _d(deps);
|
|
772
|
+
const out = [];
|
|
773
|
+
const seen = new Set();
|
|
774
|
+
for (const machine of _enabledMachines(d)) {
|
|
775
|
+
const entry = catalogFor(machine.instanceId);
|
|
776
|
+
const live = (_string(machine.health, 40) === 'reachable' && entry && entry.ok) ? entry.rows : null;
|
|
777
|
+
if (live) {
|
|
778
|
+
for (const row of live) {
|
|
779
|
+
const id = _string(row.id || row.sessionId, 200);
|
|
780
|
+
if (!id || seen.has(id)) continue; // AC-6c: never two rows for one session
|
|
781
|
+
seen.add(id);
|
|
782
|
+
out.push({
|
|
783
|
+
...row,
|
|
784
|
+
machine: _string(machine.instanceId, 160),
|
|
785
|
+
machineName: _string(machine.displayName, 200),
|
|
786
|
+
machineHealth: 'reachable',
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
continue;
|
|
790
|
+
}
|
|
791
|
+
for (const row of mirrorRowsForMachine(machine, d)) {
|
|
792
|
+
const id = _string(row.id, 200);
|
|
793
|
+
if (!id || seen.has(id)) continue;
|
|
794
|
+
seen.add(id);
|
|
795
|
+
out.push(row);
|
|
796
|
+
}
|
|
797
|
+
// No at-rest mirror for a session (a hub with no sync peer has none at all): its last good
|
|
798
|
+
// live row stands in, in the same parked look, so a box going down never makes its sessions
|
|
799
|
+
// vanish from the hub — the unreachable chip promises "its last known state".
|
|
800
|
+
for (const row of (entry && entry.ok && Array.isArray(entry.rows)) ? entry.rows : []) {
|
|
801
|
+
const id = _string(row && (row.id || row.sessionId), 200);
|
|
802
|
+
if (!id || seen.has(id)) continue;
|
|
803
|
+
seen.add(id);
|
|
804
|
+
out.push({
|
|
805
|
+
...row,
|
|
806
|
+
status: 'parked',
|
|
807
|
+
lifecycleState: 'parked',
|
|
808
|
+
parkReason: SYNC_MIRROR_PARK_REASON,
|
|
809
|
+
machine: _string(machine.instanceId, 160),
|
|
810
|
+
machineName: _string(machine.displayName, 200),
|
|
811
|
+
machineHealth: _string(machine.health, 40) || 'unknown',
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return out;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
function mirrorRowsForMachine(machine, deps = {}) {
|
|
819
|
+
const d = _d(deps);
|
|
820
|
+
const machineId = _string(machine && machine.instanceId, 160);
|
|
821
|
+
if (!machineId) return [];
|
|
822
|
+
const out = [];
|
|
823
|
+
for (const [id, entry] of _syncMirrorIndex(d)) {
|
|
824
|
+
if (entry.machineInstanceId !== machineId) continue;
|
|
825
|
+
out.push(_mirrorSessionRow(id, entry.task, machine, machine.health));
|
|
826
|
+
}
|
|
827
|
+
return out;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* The §6.6 merge seam for the hub's session-list projection (server.js `handleList` /
|
|
832
|
+
* `_finishBroadcast`).
|
|
833
|
+
*
|
|
834
|
+
* BYTE-IDENTITY (the lead's rule): with ZERO registered machines this returns the caller's own array
|
|
835
|
+
* — same reference, no fields added, the payload is indistinguishable from today's. With machines
|
|
836
|
+
* registered, local rows gain the three routing fields EMPTY (the client never has to guess) and the
|
|
837
|
+
* remote rows are appended. A LOCAL session id always wins: it is pre-seeded into the dedupe set, so
|
|
838
|
+
* a box row can never shadow a local session.
|
|
839
|
+
*/
|
|
840
|
+
function mergeSessionRows(localRows, deps = {}) {
|
|
841
|
+
const rows = Array.isArray(localRows) ? localRows : [];
|
|
842
|
+
const d = _d(deps);
|
|
843
|
+
const machines = _enabledMachines(d);
|
|
844
|
+
if (!machines.length) { _lastMergeSignature = ''; return rows; }
|
|
845
|
+
const seen = new Set();
|
|
846
|
+
for (const row of rows) {
|
|
847
|
+
const id = _string(row && (row.id || row.sessionId), 200);
|
|
848
|
+
if (id) seen.add(id);
|
|
849
|
+
}
|
|
850
|
+
const out = rows.map((row) => ({
|
|
851
|
+
...(row && typeof row === 'object' ? row : {}),
|
|
852
|
+
machine: '',
|
|
853
|
+
machineName: '',
|
|
854
|
+
machineHealth: '',
|
|
855
|
+
}));
|
|
856
|
+
for (const remote of remoteRows(d)) {
|
|
857
|
+
const id = _string(remote.id || remote.sessionId, 200);
|
|
858
|
+
if (!id || seen.has(id)) continue;
|
|
859
|
+
seen.add(id);
|
|
860
|
+
out.push(remote);
|
|
861
|
+
}
|
|
862
|
+
_lastMergeSignature = JSON.stringify([machines.map((m) => `${m.instanceId}:${m.health}`), out.slice(rows.length)]);
|
|
863
|
+
return out;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
// ---------------------------------------------------------------------------------------------
|
|
867
|
+
// S-8 / §7 — lifecycle routing for a mirror session
|
|
868
|
+
// ---------------------------------------------------------------------------------------------
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* The registry row a mirror lookup resolved to (null when the machine is gone/disabled).
|
|
872
|
+
*
|
|
873
|
+
* Exported as `machineRowForMirror` for WP-2's forwarder (docs/remote-session-image-upload-
|
|
874
|
+
* forwarding-2026-10-24.html §6): the mirror lookup answers WHO owns an id, this answers WHERE to
|
|
875
|
+
* send it (and whether the box is known to be down). Same function, same behaviour — the internal
|
|
876
|
+
* name stays for the lifecycle forwarder below.
|
|
877
|
+
*/
|
|
878
|
+
function _machineRowForMirror(mirror, d) {
|
|
879
|
+
if (!mirror) return null;
|
|
880
|
+
const machine = _findMachine(mirror.machineInstanceId, d);
|
|
881
|
+
if (!machine || machine.enabled === false || !_string(machine.url, 1000)) return null;
|
|
882
|
+
return machine;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* §7 ownership routing (AC-6b), HTTP half: `POST /api/sessions/:id/(park|revive|pin|wake)` for a
|
|
887
|
+
* MIRROR session must be acted on the BOX, never locally — the hub's `parkSession`/`getSession`
|
|
888
|
+
* would otherwise be asked about a session it does not have (OQ-4: mirror sessions are deliberately
|
|
889
|
+
* NOT in the hub's sessions Map).
|
|
890
|
+
*
|
|
891
|
+
* @returns {Promise<boolean>} true = the request was answered here (do NOT run the local handler)
|
|
892
|
+
*/
|
|
893
|
+
async function forwardLifecycleRequest(req, res, sessionId, action, deps = {}) {
|
|
894
|
+
const d = _d(deps);
|
|
895
|
+
const verb = _string(action, 20);
|
|
896
|
+
if (!['park', 'revive', 'pin', 'wake'].includes(verb)) return false;
|
|
897
|
+
const mirror = getMirrorSessionMachine(sessionId, d);
|
|
898
|
+
if (!mirror) return false; // a LOCAL session takes the byte-identical existing path
|
|
899
|
+
const machine = _machineRowForMirror(mirror, d);
|
|
900
|
+
const machineName = _string(machine ? machine.displayName : mirror.machineName, 200);
|
|
901
|
+
if (!machine) {
|
|
902
|
+
_sendJson(res, 503, {
|
|
903
|
+
ok: false, code: 'machine_unreachable', error: `Machine ${machineName || mirror.machineInstanceId} is not available`,
|
|
904
|
+
machine: mirror.machineInstanceId, machineName,
|
|
905
|
+
});
|
|
906
|
+
return true;
|
|
907
|
+
}
|
|
908
|
+
if (_string(machine.health, 40) === 'unreachable') {
|
|
909
|
+
// Never a local act: a lifecycle action on a session the hub does not own is refused, not
|
|
910
|
+
// silently applied to a same-id session on this host (which does not exist anyway).
|
|
911
|
+
_sendJson(res, 503, {
|
|
912
|
+
ok: false, code: 'machine_unreachable', error: `Machine ${machineName} is unreachable`,
|
|
913
|
+
machine: mirror.machineInstanceId, machineName,
|
|
914
|
+
});
|
|
915
|
+
return true;
|
|
916
|
+
}
|
|
917
|
+
const proxy = _peer('proxy', d);
|
|
918
|
+
if (!proxy || typeof proxy.forwardMachineHttp !== 'function') {
|
|
919
|
+
_sendJson(res, 503, {
|
|
920
|
+
ok: false, code: 'machine_unreachable', error: `Machine ${machineName} could not be reached`,
|
|
921
|
+
machine: mirror.machineInstanceId, machineName,
|
|
922
|
+
});
|
|
923
|
+
return true;
|
|
924
|
+
}
|
|
925
|
+
const result = await proxy.forwardMachineHttp({
|
|
926
|
+
machine: machine,
|
|
927
|
+
pathname: `/api/sessions/${encodeURIComponent(mirror && sessionId)}/${verb}`,
|
|
928
|
+
method: 'POST',
|
|
929
|
+
body: '',
|
|
930
|
+
deps: d,
|
|
931
|
+
});
|
|
932
|
+
if (!result || !result.ok) {
|
|
933
|
+
_sendJson(res, 503, {
|
|
934
|
+
ok: false, code: (result && result.code) || 'machine_unreachable',
|
|
935
|
+
error: (result && result.error) || `Machine ${machineName} is unreachable`,
|
|
936
|
+
machine: mirror.machineInstanceId, machineName,
|
|
937
|
+
});
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
940
|
+
// The box's own status and body are returned VERBATIM (§6.5): the client parses the box's answer
|
|
941
|
+
// exactly as it parses the local one.
|
|
942
|
+
if (!res.headersSent) {
|
|
943
|
+
res.writeHead(Number(result.status) || 502, { 'Content-Type': result.contentType || 'application/json' });
|
|
944
|
+
res.end(result.text || '');
|
|
945
|
+
}
|
|
946
|
+
return true;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
function _sendJson(res, status, payload) {
|
|
950
|
+
const body = JSON.stringify(payload);
|
|
951
|
+
if (res.headersSent) return true;
|
|
952
|
+
res.writeHead(status, { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) });
|
|
953
|
+
res.end(body);
|
|
954
|
+
return true;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* S-8/S-25 (AC-6d) action half: the machine a stopped session should be RESUMED on, when its origin
|
|
959
|
+
* instance is a registered, reachable box. '' / not-registered / not-reachable ⇒ null (today's flow).
|
|
960
|
+
*/
|
|
961
|
+
function resumeMachineFor(ctmSessionId, deps = {}) {
|
|
962
|
+
const d = _d(deps);
|
|
963
|
+
const id = _string(ctmSessionId, 200);
|
|
964
|
+
if (!id) return null;
|
|
965
|
+
const machines = _enabledMachines(d);
|
|
966
|
+
if (!machines.length) return null;
|
|
967
|
+
let originId = '';
|
|
968
|
+
try {
|
|
969
|
+
const facts = _peer('originFacts', d);
|
|
970
|
+
if (!facts || typeof facts.resumeOriginFacts !== 'function') return null;
|
|
971
|
+
// `resumeOriginFacts` is the ONE relative-origin rule (lib/resume-origin-facts.js): it already
|
|
972
|
+
// knows how to compare the row's origin against THIS instance's lineage, and it now also carries
|
|
973
|
+
// the raw origin id, which is the key the machine registry is pinned by (D-3).
|
|
974
|
+
const looked = facts.resumeOriginFacts(d.db || null, id, {}) || {};
|
|
975
|
+
if (looked.origin !== 'other_instance') return null;
|
|
976
|
+
originId = _string(looked.originInstanceId, 160);
|
|
977
|
+
} catch {
|
|
978
|
+
return null; // expected: no DB / pre-boot -> today's flow, byte-identical
|
|
979
|
+
}
|
|
980
|
+
if (!originId) return null;
|
|
981
|
+
const machine = machines.find((row) => _string(row.instanceId, 160) === originId) || null;
|
|
982
|
+
if (!machine || _string(machine.health, 40) !== 'reachable') return null;
|
|
983
|
+
return { instanceId: _string(machine.instanceId, 160), displayName: _string(machine.displayName, 200) };
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/** S-8/S-25 action half: resume this session ON the box, through the WP-2 allowlist (no widening —
|
|
987
|
+
* a resume needs `id`, `cmd`, `args`, `cwd`, `label`/`title`, all of which are already on it). */
|
|
988
|
+
async function forwardResumeOnMachine({ launch, machine } = {}, deps = {}) {
|
|
989
|
+
const d = _d(deps);
|
|
990
|
+
const row = (machine && machine.url ? machine : null) || _findMachine(machine && machine.instanceId, d);
|
|
991
|
+
if (!row || !_string(row.url, 1000)) return { ok: false, code: 'machine_unreachable', error: 'machine_url_missing' };
|
|
992
|
+
const create = _peer('create', d);
|
|
993
|
+
if (!create || typeof create.filterRemoteCreateForward !== 'function' || typeof create.sendRemoteCreate !== 'function') {
|
|
994
|
+
return { ok: false, code: 'machine_unreachable', error: 'catalog_create_peer_not_wired' };
|
|
995
|
+
}
|
|
996
|
+
const payload = create.filterRemoteCreateForward({
|
|
997
|
+
id: launch && launch.id,
|
|
998
|
+
cmd: launch && launch.cmd,
|
|
999
|
+
args: launch && launch.args,
|
|
1000
|
+
cwd: launch && launch.cwd,
|
|
1001
|
+
label: launch && launch.title,
|
|
1002
|
+
title: launch && launch.title,
|
|
1003
|
+
});
|
|
1004
|
+
const result = await create.sendRemoteCreate({ machineUrl: row.url, payload, deps: d });
|
|
1005
|
+
if (!result.ok) return { ok: false, code: result.code, error: result.error };
|
|
1006
|
+
const frame = result.frame || {};
|
|
1007
|
+
const sessionId = _string(frame.id || payload.id, 200);
|
|
1008
|
+
if (sessionId && typeof create.recordMirrorSession === 'function') create.recordMirrorSession(sessionId, row);
|
|
1009
|
+
return {
|
|
1010
|
+
ok: true,
|
|
1011
|
+
sessionId: sessionId || _string(payload.id, 200),
|
|
1012
|
+
machineInstanceId: _string(row.instanceId, 160),
|
|
1013
|
+
machineName: _string(row.displayName, 200),
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/** Test seam: every cache and the injection slot are module state. */
|
|
1018
|
+
function _resetForTests() {
|
|
1019
|
+
_catalogs.clear();
|
|
1020
|
+
_registryCache.machines = [];
|
|
1021
|
+
_registryCache.at = 0;
|
|
1022
|
+
_registryCache.revision = null;
|
|
1023
|
+
_syncMirrorCache.byId = new Map();
|
|
1024
|
+
_syncMirrorCache.at = 0;
|
|
1025
|
+
_injected = null;
|
|
1026
|
+
_lastMergeSignature = '';
|
|
1027
|
+
// WP-1: the fence counters are process-cumulative, so a test that resets the module must reset
|
|
1028
|
+
// them too or a later assertion reads an earlier test's tallies. (The log-dedupe WINDOW state is
|
|
1029
|
+
// deliberately left alone: it only ever suppresses duplicate lines, never changes a verdict, and
|
|
1030
|
+
// flushing it here would emit summaries into a resetting test's output.)
|
|
1031
|
+
_fenceCounters.clear();
|
|
1032
|
+
for (const reason of FENCE_REASONS) _fenceTotals[reason] = 0;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
module.exports = {
|
|
1036
|
+
SYNC_MIRROR_PARK_REASON,
|
|
1037
|
+
MACHINE_FIELDS,
|
|
1038
|
+
DEFAULT_CATALOG_TIMEOUT_MS,
|
|
1039
|
+
configure,
|
|
1040
|
+
configureProduction,
|
|
1041
|
+
machineRows,
|
|
1042
|
+
machineTagInputs,
|
|
1043
|
+
isLocalOwnerId,
|
|
1044
|
+
recordSessionList,
|
|
1045
|
+
ingestFenceCounters,
|
|
1046
|
+
catalogFor,
|
|
1047
|
+
catalogSessionIds,
|
|
1048
|
+
fetchCatalog,
|
|
1049
|
+
refreshAll,
|
|
1050
|
+
onLinkReady,
|
|
1051
|
+
getMirrorSessionMachine,
|
|
1052
|
+
// WP-2 (§6 step 3): the registry row behind a mirror lookup, for lib/remote-session-forward.js.
|
|
1053
|
+
machineRowForMirror: _machineRowForMirror,
|
|
1054
|
+
remoteRows,
|
|
1055
|
+
mirrorRowsForMachine,
|
|
1056
|
+
mergeSessionRows,
|
|
1057
|
+
lastMergeSignature,
|
|
1058
|
+
forwardLifecycleRequest,
|
|
1059
|
+
resumeMachineFor,
|
|
1060
|
+
forwardResumeOnMachine,
|
|
1061
|
+
_resetForTests,
|
|
1062
|
+
};
|