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
|
@@ -9,9 +9,16 @@
|
|
|
9
9
|
// about it — a keystroke, a prompt, an approval verdict, a resize — must be carried to the box and
|
|
10
10
|
// every frame the box produces must be carried back. This module owns that carry:
|
|
11
11
|
//
|
|
12
|
-
// LINK
|
|
13
|
-
//
|
|
14
|
-
//
|
|
12
|
+
// LINK two kinds, both lazy, both in ONE Map (WP-1 D-1):
|
|
13
|
+
// * VIEWER link — one upstream WS per (machine, hub socket), keyed by
|
|
14
|
+
// `machineId\0clientId:socketGeneration`. Opened on the FIRST viewed session of that
|
|
15
|
+
// hub socket and closed immediately when the socket goes away (or after
|
|
16
|
+
// IDLE_CLOSE_MS once it stops viewing every session on that machine). It carries
|
|
17
|
+
// that one socket's session traffic and nobody else's, so the box runs one delivery
|
|
18
|
+
// (one in-flight frame, acked by that one viewer) per hub viewer — exactly like a
|
|
19
|
+
// local client.
|
|
20
|
+
// * CONTROL link — one per machine, keyed by machineId, created ONLY by openLink()
|
|
21
|
+
// (the catalog `list` lane + onLinkReady). It never carries a session view.
|
|
15
22
|
// INTERCEPT the single dispatch seam (lib/ws-router.js's pre-dispatch hook, consulted at
|
|
16
23
|
// `dispatchWs` before ANY local handler). A message naming a MIRROR session is
|
|
17
24
|
// diverted here; a message naming a local session returns false immediately and takes
|
|
@@ -19,29 +26,52 @@
|
|
|
19
26
|
// the byte-identical existing path").
|
|
20
27
|
// FORWARD hub browser -> box, VERBATIM. `clientMessageId` (S-24) and every other id is the
|
|
21
28
|
// ORIGINAL one the browser minted — this module never re-mints an idempotency key.
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
29
|
+
// ONE exception, and only for the geometry family (WP-3 D-4/D-5): `socketGeneration`
|
|
30
|
+
// is a PER-CONNECTION identity minted by whichever server accepted the socket, so the
|
|
31
|
+
// relay rewrites it per hop to the identity of the socket on the receiving side (RFC
|
|
32
|
+
// 9110 §7.6.1's per-hop field model) — outbound `_toBoxHop` sets link.boxSocketGeneration
|
|
33
|
+
// on terminal-geometry-claim-v2 (and its `measurement.socketGeneration`) and on a v2
|
|
34
|
+
// `resize`; inbound `_toHubHop` sets the receiving hub socket's generation on
|
|
35
|
+
// terminal-geometry-claim-ack-v2 / -fenced / -owner-revoked. `clientMessageId` and every
|
|
36
|
+
// other id stay verbatim; the browser's object is never mutated.
|
|
37
|
+
// FAN-OUT box -> the hub browser sockets VIEWING that session (S-22). WP-1 D-2: a viewer link
|
|
38
|
+
// delivers to its OWN ctx only, so co-viewers each receive their own copy on their own
|
|
39
|
+
// link. WP-1 D-3: a content payload (`type:'frame'`, or `type:'frame-v2'` since WP-2) is
|
|
40
|
+
// written with `sendFrame` (ctx.ws) — the LOCAL path — bypassing hub send admission, and
|
|
41
|
+
// WP-2 R-3 projects a `frame-v2` per RECEIVING socket (v2 verbatim, v1 through
|
|
42
|
+
// projectFrameForProtocol; a projection throw is counted and skips just that frame).
|
|
43
|
+
// Every other type keeps `ctx.send` (admission + per-hop geometry identity). Reply
|
|
44
|
+
// frames (submit-prompt-status,
|
|
45
|
+
// approval-resolution-result, model-change-result, session-lifecycle-result, …) go to
|
|
46
|
+
// the ORIGINATING hub socket ONLY, matched by the original `clientMessageId`/`requestId`
|
|
47
|
+
// when the frame carries one.
|
|
26
48
|
// RECONNECT full-jitter backoff 500 ms -> 30 s cap, reset on success; on every (re)connect the
|
|
27
49
|
// link re-attaches every viewed session and issues `keyframe-req`, so the box's own
|
|
28
50
|
// seq/keyframe authority is what re-syncs content (no duplicate/no gap by design).
|
|
29
51
|
// PINGS two consecutive missed WS pings force a reconnect (the link owns its own liveness —
|
|
30
52
|
// the peer-control allowlist deliberately has no CTM `ping` message).
|
|
31
53
|
//
|
|
32
|
-
// BACKPRESSURE (§10 risk 2), the choice this module makes:
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
// re-minted, no local credit is invented.
|
|
54
|
+
// BACKPRESSURE (§10 risk 2), the choice this module makes: with WP-1 D-1 each hub socket has its OWN
|
|
55
|
+
// upstream link, so the box runs one delivery per hub viewer (one in-flight frame, acked by that one
|
|
56
|
+
// socket) — the local client contract. A slow/stalled hub viewer can never stall the frames the OTHER
|
|
57
|
+
// viewers of the same session receive; each hub socket's acks (`ack`/`nack`/`frame-receipt`) go up
|
|
58
|
+
// its own link, which is the credit signal the box already understands (lib/ws/state-sync.js keys
|
|
59
|
+
// acks by session). Nothing is re-minted, no local credit is invented.
|
|
39
60
|
//
|
|
40
61
|
// No durable state (lead ruling 4): links and viewer refcounts are in-memory and rebuilt lazily
|
|
41
62
|
// from viewed sessions after a hub restart. Everything is injectable for tests.
|
|
42
63
|
|
|
43
64
|
const { registerWsPreDispatchHook } = require('./ws-router');
|
|
44
65
|
const remoteMachineCreate = require('./remote-machine-create');
|
|
66
|
+
const { sendFrame } = require('./ws-send-admission');
|
|
67
|
+
// WP-2 (docs/relay-frame-v2-negotiation-box-session-mouse-2026-10-31.html §5 R-3/R-6): the box now
|
|
68
|
+
// answers the hub's peer client-info with `frame-v2` (the frame that carries surface.mouse), so the
|
|
69
|
+
// hub projects it down per RECEIVING hub socket — exactly the box's own fan-out contract.
|
|
70
|
+
const { FRAME_PROTOCOL_V1, FRAME_PROTOCOL_V2, projectFrameForProtocol } = require('./state-sync/frame-protocol');
|
|
71
|
+
|
|
72
|
+
// Content frame types that take the direct path (sendFrame, no hub admission) on a viewer link.
|
|
73
|
+
// Both speak the same delivery semantics; only the projection differs per receiving socket (R-3).
|
|
74
|
+
const RELAYED_CONTENT_FRAME_TYPES = new Set(['frame', 'frame-v2']);
|
|
45
75
|
|
|
46
76
|
const BACKOFF_BASE_MS = 500;
|
|
47
77
|
const BACKOFF_CAP_MS = 30000;
|
|
@@ -58,7 +88,12 @@ const RELAY_TYPES = Object.freeze(new Set([
|
|
|
58
88
|
// receipt of the cold bootstrap) — both cross the hop, so both are relayed.
|
|
59
89
|
'attach', 'view', 'keyframe-req', 'snapshot', 'terminal-bootstrap', 'terminal-bootstrap-ack', 'wake-resync',
|
|
60
90
|
// typing + geometry
|
|
91
|
+
// WP-3 (docs/remote-terminal-cursor-and-geometry-claim-relay-2026-10-27.html §5 D-3/D-4, §9 S-4):
|
|
92
|
+
// `terminal-geometry-claim-v2` is a viewer's size claim for a MIRROR session — the box owns the
|
|
93
|
+
// PTY, so without this the claim never crosses the hop and the box PTY keeps its spawn size. Its
|
|
94
|
+
// `socketGeneration` is rewritten per hop (see the FORWARD note above).
|
|
61
95
|
'input', 'resize', 'reflow', 'reconcile', 'flow', 'detach', 'interrupt-session',
|
|
96
|
+
'terminal-geometry-claim-v2',
|
|
62
97
|
// prompts + approvals + model (S-23/S-24). `image-deliver-now` is WP-3 of
|
|
63
98
|
// docs/remote-session-image-upload-forwarding-2026-10-24.html §6: the viewer asks the BOX that
|
|
64
99
|
// owns the session to deliver the already-uploaded draft, so the frame must cross the hop.
|
|
@@ -102,6 +137,10 @@ const REPLY_TYPES = Object.freeze(new Set([
|
|
|
102
137
|
'session-lifecycle-result',
|
|
103
138
|
'session-interrupt-result',
|
|
104
139
|
'resume-prompt-result',
|
|
140
|
+
// WP-3 (docs/remote-terminal-cursor-and-geometry-claim-relay-2026-10-27.html §5 D-6, §9 S-5): the
|
|
141
|
+
// box's verdict on a relayed geometry claim. Routed to the CLAIMING socket only — every other hub
|
|
142
|
+
// viewer of the same mirror session must not see a lease outcome it never asked for.
|
|
143
|
+
'terminal-geometry-claim-ack-v2',
|
|
105
144
|
'request-error',
|
|
106
145
|
'error',
|
|
107
146
|
]));
|
|
@@ -115,12 +154,22 @@ const REPLY_TYPES = Object.freeze(new Set([
|
|
|
115
154
|
const REMEMBER_TYPES = Object.freeze(new Set([
|
|
116
155
|
'submit-prompt', 'approval-resolve', 'model-change', 'park', 'wake', 'kill', 'interrupt-session',
|
|
117
156
|
'resume-prompt-select',
|
|
157
|
+
// WP-3 (§5 D-6): a claim carries no `clientMessageId`, so its pending entry is matched by
|
|
158
|
+
// `replyType` (`terminal-geometry-claim-ack-v2`) alone — `_rememberPending` falls back to a
|
|
159
|
+
// per-session seq key when `clientMessageId` is absent, which is exactly what `_takePending`'s
|
|
160
|
+
// replyType predicate consumes.
|
|
161
|
+
'terminal-geometry-claim-v2',
|
|
118
162
|
]));
|
|
119
163
|
const PENDING_TTL_MS = 60000;
|
|
120
164
|
const PENDING_MAX = 512;
|
|
121
165
|
// Bounded queue of frames sent to a link that is still dialing (see forward()):
|
|
122
166
|
const PENDING_SEND_MAX = 64;
|
|
123
167
|
|
|
168
|
+
// WP-0 (docs/relay-per-viewer-delivery-contract-2026-10-27.html §5 D-5, §6, §8 WP-0): at most one
|
|
169
|
+
// `relay-frame-undelivered` diagnostic per (session, reason) window. The counter itself is
|
|
170
|
+
// unthrottled; only the report is.
|
|
171
|
+
const UNDELIVERED_REPORT_INTERVAL_MS = 10000;
|
|
172
|
+
|
|
124
173
|
// WP-6: the request -> reply-type pairs the box's handlers really implement (see REPLY_TYPES).
|
|
125
174
|
const REPLY_FOR_REQUEST = Object.freeze({
|
|
126
175
|
'submit-prompt': 'submit-prompt-status',
|
|
@@ -130,6 +179,8 @@ const REPLY_FOR_REQUEST = Object.freeze({
|
|
|
130
179
|
'wake': 'session-lifecycle-result',
|
|
131
180
|
'interrupt-session': 'session-interrupt-result',
|
|
132
181
|
'resume-prompt-select': 'resume-prompt-result',
|
|
182
|
+
// WP-3 (§5 D-6, §9 S-6): the box's geometry-claim verdict.
|
|
183
|
+
'terminal-geometry-claim-v2': 'terminal-geometry-claim-ack-v2',
|
|
133
184
|
});
|
|
134
185
|
|
|
135
186
|
// WP-6: how long a catalog `list` request waits for the box's `sessions` reply before giving up.
|
|
@@ -138,12 +189,25 @@ const CATALOG_TIMEOUT_MS = 5000;
|
|
|
138
189
|
let _injected = null;
|
|
139
190
|
let _uninstallIntercept = null;
|
|
140
191
|
let _productionCache = null;
|
|
192
|
+
// WP-0 (D-5): the hub's session-diagnostics reporter, wired by lib/ws/terminal-attach.js's
|
|
193
|
+
// configure() to the SAME `remoteCreateDeps.recordSessionDiagnostic` the rest of the hub uses (so
|
|
194
|
+
// this needs no server.js edit). Module state, not a deps field, because the production dep object
|
|
195
|
+
// below is memoized once and must read the sink at CALL time (the sink is set at boot).
|
|
196
|
+
let _diagnosticsSink = null;
|
|
141
197
|
|
|
142
198
|
/** Test/host seam: override any production dependency (registry, WebSocket, timers, clock, log). */
|
|
143
199
|
function configure(deps) {
|
|
144
200
|
_injected = deps && typeof deps === 'object' ? deps : null;
|
|
145
201
|
}
|
|
146
202
|
|
|
203
|
+
/**
|
|
204
|
+
* WP-0 (D-5): set the diagnostics sink the production `recordDiagnostic` dep forwards to. A
|
|
205
|
+
* non-function clears it, so a `configure()` without a reporter can never leave a stale one behind.
|
|
206
|
+
*/
|
|
207
|
+
function setDiagnosticsSink(fn) {
|
|
208
|
+
_diagnosticsSink = typeof fn === 'function' ? fn : null;
|
|
209
|
+
}
|
|
210
|
+
|
|
147
211
|
function _string(value, max = 200) {
|
|
148
212
|
return String(value == null ? '' : value).trim().slice(0, max);
|
|
149
213
|
}
|
|
@@ -181,6 +245,13 @@ function _productionDeps() {
|
|
|
181
245
|
onSessionList: (link, rows, frame) => { try { require('./remote-machine-catalog').recordSessionList(link.machineInstanceId, rows); } catch { /* expected: ditto — the box's list is a freshness bonus, not a link obligation */ } },
|
|
182
246
|
onRelayedKill: (link) => { try { require('./remote-machine-catalog').fetchCatalog(link.machineInstanceId); } catch { /* expected: ditto — the kill already crossed; the refresh is best effort */ } },
|
|
183
247
|
catalogTimeoutMs: CATALOG_TIMEOUT_MS,
|
|
248
|
+
// WP-0 (D-5, §9 S-2): the report path for a relayed frame a hub viewer did not get. Forwards to
|
|
249
|
+
// whatever lib/ws/terminal-attach.js's configure() installed via setDiagnosticsSink(); a hub with
|
|
250
|
+
// no sink records nothing but still counts (undeliveredCounters()). Tests override this dep
|
|
251
|
+
// directly with `configure({ recordDiagnostic })`.
|
|
252
|
+
recordDiagnostic: (sessionId, event, details) => {
|
|
253
|
+
if (_diagnosticsSink) _diagnosticsSink(sessionId, event, details);
|
|
254
|
+
},
|
|
184
255
|
log: console,
|
|
185
256
|
idleCloseMs: IDLE_CLOSE_MS,
|
|
186
257
|
heartbeatMs: HEARTBEAT_MS,
|
|
@@ -225,35 +296,259 @@ function sessionIdsOf(msg) {
|
|
|
225
296
|
}
|
|
226
297
|
|
|
227
298
|
function _frameSessionId(frame) {
|
|
299
|
+
// WP-0 (docs/hub-viewed-box-conv-freshness-and-paste-render-2026-10-31.html §5 D-1, §6, §9 S-1):
|
|
300
|
+
// `ctmSessionId` is the spelling the box's per-conversation broadcasts use (`conversation-updated`,
|
|
301
|
+
// `session-image-refs-updated` — server.js:9165/9192), so reading only `id`/`sessionId` dropped
|
|
302
|
+
// every one of them on the floor at :811. All three spellings name the same session.
|
|
228
303
|
if (!frame || typeof frame !== 'object') return '';
|
|
229
|
-
return _string(frame.id || frame.sessionId, 200);
|
|
304
|
+
return _string(frame.id || frame.sessionId || frame.ctmSessionId, 200);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---------------------------------------------------------------------------------------------
|
|
308
|
+
// WP-0 (docs/relay-per-viewer-delivery-contract-2026-10-27.html §5 D-5, §6, §8 WP-0, §9 S-1..S-3):
|
|
309
|
+
// the per-session relay UNDELIVERED counter.
|
|
310
|
+
//
|
|
311
|
+
// The local delivery contract (docs/terminal-delivery-contract-simplification-2026-09-21.html D-4)
|
|
312
|
+
// bypasses send admission, so a frame that reached nobody is counted locally. The relay did NOT:
|
|
313
|
+
// every hub-browser copy goes through `ctx.send` = `_wsSend`, whose ADMISSION_RESULTS value was
|
|
314
|
+
// discarded, so a throttled / stalled / coalesced viewer silently missed frames the box believed it
|
|
315
|
+
// had delivered (and acked). This counts every non-'sent' downstream send and reports it, throttled
|
|
316
|
+
// so one stalled viewer cannot flood the diagnostics table. WP-1 then makes the frame path report
|
|
317
|
+
// only send-error / socket-not-open, which is the live proof of D-3.
|
|
318
|
+
// ---------------------------------------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
// `${sessionId}\u0000${reason}` -> { count, total, sinceMs, lastReportMs, type, clientId }
|
|
321
|
+
// `count` is the accumulation since the last report; `total` is the lifetime count.
|
|
322
|
+
const _undelivered = new Map();
|
|
323
|
+
|
|
324
|
+
/** Plain snapshot of the undelivered counters, for tests and a hub diagnostics read. */
|
|
325
|
+
function undeliveredCounters() {
|
|
326
|
+
const rows = [];
|
|
327
|
+
for (const [key, entry] of _undelivered) {
|
|
328
|
+
const sep = key.indexOf('\u0000');
|
|
329
|
+
rows.push({
|
|
330
|
+
sessionId: key.slice(0, sep),
|
|
331
|
+
reason: key.slice(sep + 1),
|
|
332
|
+
count: entry.count,
|
|
333
|
+
total: entry.total,
|
|
334
|
+
type: entry.type,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return rows;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Record one downstream relayed send this viewer did not get as sent. Reason is an ADMISSION_RESULTS
|
|
342
|
+
* value ('dropped-backpressure' | 'closed-backpressure' | 'dropped-diet' | 'coalesced') or our own
|
|
343
|
+
* 'send-error' / 'socket-not-open'. At most one `relay-frame-undelivered` diagnostic per
|
|
344
|
+
* (sessionId, reason) per UNDELIVERED_REPORT_INTERVAL_MS, carrying the count accumulated in the
|
|
345
|
+
* window. A sink fault is swallowed: the COUNTER is the contract, and a diagnostics failure must
|
|
346
|
+
* never break relayed frame delivery.
|
|
347
|
+
*/
|
|
348
|
+
function _noteUndelivered(ctx, frame, reason, d) {
|
|
349
|
+
const sessionId = _frameSessionId(frame);
|
|
350
|
+
const key = `${sessionId}\u0000${reason}`;
|
|
351
|
+
const now = d.now();
|
|
352
|
+
let entry = _undelivered.get(key);
|
|
353
|
+
if (!entry) {
|
|
354
|
+
entry = {
|
|
355
|
+
count: 0,
|
|
356
|
+
total: 0,
|
|
357
|
+
sinceMs: now,
|
|
358
|
+
lastReportMs: null,
|
|
359
|
+
type: frame && frame.type,
|
|
360
|
+
clientId: String((ctx && ctx.ws && ctx.ws.clientId) || ''),
|
|
361
|
+
};
|
|
362
|
+
_undelivered.set(key, entry);
|
|
363
|
+
}
|
|
364
|
+
entry.count += 1;
|
|
365
|
+
entry.total += 1;
|
|
366
|
+
entry.type = frame && frame.type;
|
|
367
|
+
if (entry.lastReportMs != null && now - entry.lastReportMs < UNDELIVERED_REPORT_INTERVAL_MS) return;
|
|
368
|
+
try {
|
|
369
|
+
if (typeof d.recordDiagnostic === 'function') {
|
|
370
|
+
d.recordDiagnostic(sessionId, 'relay-frame-undelivered', {
|
|
371
|
+
reason, type: entry.type, count: entry.count, sinceMs: entry.sinceMs, clientId: entry.clientId,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
} catch {
|
|
375
|
+
// expected: a diagnostics-sink fault must never break relayed frame delivery
|
|
376
|
+
}
|
|
377
|
+
entry.count = 0;
|
|
378
|
+
entry.lastReportMs = now;
|
|
379
|
+
entry.sinceMs = now;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// ---------------------------------------------------------------------------------------------
|
|
383
|
+
// WP-0 (docs/remote-relay-ack-rate-limit-typing-delay-2026-10-28.html §5 D-4b, §6, §8 WP-0,
|
|
384
|
+
// §9 S-7): the upstream twin of the counter above.
|
|
385
|
+
//
|
|
386
|
+
// A box refusing a RELAYED frame answers `auth-error {messageType, code}`. `_routeAuthError` can
|
|
387
|
+
// only hand that verdict to the socket that asked, so a refusal with NO pending origin — exactly the
|
|
388
|
+
// shape of a rate-limited `ack`/`frame-receipt`, which is never remembered as pending — was dropped
|
|
389
|
+
// on the floor: the hub browser waited for a verdict that no one ever saw (design doc §5 D-4b).
|
|
390
|
+
// Count every such refusal and report it, throttled the same way and for the same reason.
|
|
391
|
+
// ---------------------------------------------------------------------------------------------
|
|
392
|
+
|
|
393
|
+
// `${machineInstanceId}\u0000${sessionId}\u0000${messageType}` -> { count, total, code, sinceMs,
|
|
394
|
+
// lastReportMs }. `count` is the accumulation since the last report; `total` is the lifetime count.
|
|
395
|
+
const _upstreamRefused = new Map();
|
|
396
|
+
|
|
397
|
+
/** Plain snapshot of the upstream-refusal counters, for tests and a hub diagnostics read. */
|
|
398
|
+
function upstreamRefusedCounters() {
|
|
399
|
+
const rows = [];
|
|
400
|
+
for (const [key, entry] of _upstreamRefused) {
|
|
401
|
+
const [machineInstanceId, sessionId, messageType] = key.split('\u0000');
|
|
402
|
+
rows.push({
|
|
403
|
+
machineInstanceId, sessionId, messageType, code: entry.code, count: entry.count, total: entry.total,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
return rows;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Record one box `auth-error` refusing a RELAYED frame that no pending origin could claim. At most
|
|
411
|
+
* one `relay-upstream-refused` diagnostic per (machine, session, messageType) per
|
|
412
|
+
* UNDELIVERED_REPORT_INTERVAL_MS, carrying the count accumulated in the window. A sink fault is
|
|
413
|
+
* swallowed: the COUNTER is the contract, and a diagnostics failure must never break relayed frames.
|
|
414
|
+
*/
|
|
415
|
+
function _noteUpstreamRefused(link, frame, messageType, d) {
|
|
416
|
+
const machineInstanceId = _string(link && link.machineInstanceId, 200);
|
|
417
|
+
const sessionId = _frameSessionId(frame);
|
|
418
|
+
const key = `${machineInstanceId}\u0000${sessionId}\u0000${messageType}`;
|
|
419
|
+
const now = d.now();
|
|
420
|
+
let entry = _upstreamRefused.get(key);
|
|
421
|
+
if (!entry) {
|
|
422
|
+
entry = {
|
|
423
|
+
count: 0,
|
|
424
|
+
total: 0,
|
|
425
|
+
code: _string(frame && frame.code, 40),
|
|
426
|
+
sinceMs: now,
|
|
427
|
+
lastReportMs: null,
|
|
428
|
+
};
|
|
429
|
+
_upstreamRefused.set(key, entry);
|
|
430
|
+
}
|
|
431
|
+
entry.count += 1;
|
|
432
|
+
entry.total += 1;
|
|
433
|
+
entry.code = _string(frame && frame.code, 40);
|
|
434
|
+
if (entry.lastReportMs != null && now - entry.lastReportMs < UNDELIVERED_REPORT_INTERVAL_MS) return;
|
|
435
|
+
try {
|
|
436
|
+
if (typeof d.recordDiagnostic === 'function') {
|
|
437
|
+
d.recordDiagnostic(sessionId, 'relay-upstream-refused', {
|
|
438
|
+
messageType, code: entry.code, count: entry.count, machineInstanceId,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
} catch {
|
|
442
|
+
// expected: a diagnostics-sink fault must never break relayed frame delivery
|
|
443
|
+
}
|
|
444
|
+
entry.count = 0;
|
|
445
|
+
entry.lastReportMs = now;
|
|
446
|
+
entry.sinceMs = now;
|
|
230
447
|
}
|
|
231
448
|
|
|
232
449
|
/** Deliver an already-serialized frame to one hub browser socket (fan-out or reply routing). */
|
|
233
|
-
function _sendToCtx(ctx, payload) {
|
|
450
|
+
function _sendToCtx(ctx, payload, frame, d) {
|
|
234
451
|
try {
|
|
235
|
-
if (ctx
|
|
452
|
+
if (!ctx || typeof ctx.send !== 'function') return;
|
|
453
|
+
if (!ctx.ws || ctx.ws.readyState !== 1) {
|
|
454
|
+
// WP-0 (D-5): the hub viewer's socket is gone — this frame reached nobody. Count it instead of
|
|
455
|
+
// dropping it silently; the box owns the session and the viewer re-subscribes on reconnect.
|
|
456
|
+
_noteUndelivered(ctx, frame, 'socket-not-open', d);
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const result = ctx.send(payload);
|
|
460
|
+
// WP-0 (D-5): `ctx.send` is `_wsSend(ws, payload)` in production, which returns an
|
|
461
|
+
// ADMISSION_RESULTS value. Anything but 'sent' means THIS viewer did not get the frame as sent.
|
|
462
|
+
// `undefined` (a test ctx whose send returns nothing) is not a verdict and records nothing.
|
|
463
|
+
if (typeof result === 'string' && result !== 'sent') _noteUndelivered(ctx, frame, result, d);
|
|
236
464
|
} catch {
|
|
237
465
|
// expected: the hub browser socket died while a relayed frame was in flight — the box owns the
|
|
238
466
|
// session and WP-5's UI re-subscribes on the next connect.
|
|
467
|
+
_noteUndelivered(ctx, frame, 'send-error', d);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function _sendFrame(ctx, frame, d) {
|
|
472
|
+
_sendToCtx(ctx, JSON.stringify(_toHubHop(frame, ctx)), frame, d);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ---------------------------------------------------------------------------------------------
|
|
476
|
+
// Per-hop identity (WP-3, docs/remote-terminal-cursor-and-geometry-claim-relay-2026-10-27.html
|
|
477
|
+
// §5 D-4/D-5, RFC 9110 §7.6.1). `socketGeneration` is minted by whichever server accepted the WS
|
|
478
|
+
// (server.js's per-connection `ws.socketGeneration`) and the box checks it against the accepting
|
|
479
|
+
// connection (lib/terminal-geometry-claim.js:59-61/106-108). The hub browser's generation is
|
|
480
|
+
// meaningless to the box, and several hub sockets share ONE box generation (they share one link),
|
|
481
|
+
// so the relay owns hop identity: it rewrites ONLY `socketGeneration` (and, for the claim, the
|
|
482
|
+
// nested `measurement.socketGeneration`) on the geometry family, per direction. `clientMessageId`
|
|
483
|
+
// and every other id stay verbatim, and the caller's object is never mutated.
|
|
484
|
+
// ---------------------------------------------------------------------------------------------
|
|
485
|
+
|
|
486
|
+
// Box -> hub: the geometry family, whose `socketGeneration` must name the RECEIVING hub socket.
|
|
487
|
+
const GEOMETRY_INBOUND_TYPES = Object.freeze(new Set([
|
|
488
|
+
'terminal-geometry-claim-ack-v2', 'terminal-geometry-fenced', 'terminal-geometry-owner-revoked',
|
|
489
|
+
]));
|
|
490
|
+
|
|
491
|
+
/** True for a hub-browser frame whose outbound `socketGeneration` must name the LINK's box socket. */
|
|
492
|
+
function _isOutboundGeometry(msg) {
|
|
493
|
+
if (!msg || typeof msg !== 'object') return false;
|
|
494
|
+
if (msg.type === 'terminal-geometry-claim-v2') return true;
|
|
495
|
+
// A v2 resize is the other per-connection identity carrier (a legacy v1 resize has no generation).
|
|
496
|
+
return msg.type === 'resize' && msg.geometryProtocolVersion === 2;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Outbound hop rewrite (D-4). Returns `msg` ITSELF unless it is a geometry message; otherwise a
|
|
501
|
+
* SHALLOW COPY with `socketGeneration = link.boxSocketGeneration` and, for the claim, a copied
|
|
502
|
+
* `measurement` with `measurement.socketGeneration` rewritten too. No other field changes.
|
|
503
|
+
*/
|
|
504
|
+
function _toBoxHop(link, msg) {
|
|
505
|
+
if (!_isOutboundGeometry(msg)) return msg;
|
|
506
|
+
const generation = Number(link && link.boxSocketGeneration) || 0;
|
|
507
|
+
const hop = { ...msg, socketGeneration: generation };
|
|
508
|
+
if (msg.type === 'terminal-geometry-claim-v2' && msg.measurement && typeof msg.measurement === 'object') {
|
|
509
|
+
hop.measurement = { ...msg.measurement, socketGeneration: generation };
|
|
239
510
|
}
|
|
511
|
+
return hop;
|
|
240
512
|
}
|
|
241
513
|
|
|
242
|
-
|
|
243
|
-
|
|
514
|
+
/**
|
|
515
|
+
* Inbound hop rewrite (D-5). Returns `frame` ITSELF unless it is one of GEOMETRY_INBOUND_TYPES;
|
|
516
|
+
* otherwise a SHALLOW COPY whose `socketGeneration` names the RECEIVING hub socket. Applied at
|
|
517
|
+
* `_sendFrame`, so both the reply-to-origin path and the fan-out path are covered per receiving
|
|
518
|
+
* socket.
|
|
519
|
+
*/
|
|
520
|
+
function _toHubHop(frame, ctx) {
|
|
521
|
+
if (!frame || typeof frame !== 'object' || !GEOMETRY_INBOUND_TYPES.has(frame.type)) return frame;
|
|
522
|
+
return { ...frame, socketGeneration: Number(ctx && ctx.ws && ctx.ws.socketGeneration) || 0 };
|
|
244
523
|
}
|
|
245
524
|
|
|
246
525
|
// ---------------------------------------------------------------------------------------------
|
|
247
526
|
// Links
|
|
248
527
|
// ---------------------------------------------------------------------------------------------
|
|
249
528
|
|
|
250
|
-
const _links = new Map(); //
|
|
529
|
+
const _links = new Map(); // key -> link (control key = machineInstanceId; viewer key = _viewerKey(...))
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* WP-1 (D-1): the VIEWER-link key — one upstream link per (machine, hub socket). `clientId` and
|
|
533
|
+
* `socketGeneration` are the hub-side per-connection identity (server.js's own `ws.clientId` /
|
|
534
|
+
* `ws.socketGeneration`), so two tabs on the same machine get two links and therefore two box
|
|
535
|
+
* deliveries. A missing ctx/ws degrades to the `':0'` suffix (the caller guards it anyway).
|
|
536
|
+
*/
|
|
537
|
+
function _viewerKey(machineId, ctx) {
|
|
538
|
+
const ws = ctx && ctx.ws;
|
|
539
|
+
return `${machineId}\u0000${String((ws && ws.clientId) || '')}:${Number(ws && ws.socketGeneration) || 0}`;
|
|
540
|
+
}
|
|
251
541
|
|
|
252
|
-
function _newLink(machineId, machineName, url) {
|
|
542
|
+
function _newLink(machineId, machineName, url, kind = 'control', viewerCtx = null) {
|
|
253
543
|
return {
|
|
544
|
+
key: kind === 'viewer' ? _viewerKey(machineId, viewerCtx) : machineId,
|
|
254
545
|
machineInstanceId: machineId,
|
|
255
546
|
machineName,
|
|
256
547
|
url,
|
|
548
|
+
// WP-1 (D-1): 'control' (key = machineId, the catalog lane) | 'viewer' (key = _viewerKey, one per
|
|
549
|
+
// hub socket). `viewerCtx` is the hub ctx a viewer link delivers to; null on a control link.
|
|
550
|
+
kind,
|
|
551
|
+
viewerCtx,
|
|
257
552
|
socket: null,
|
|
258
553
|
ready: false,
|
|
259
554
|
closed: false,
|
|
@@ -264,6 +559,10 @@ function _newLink(machineId, machineName, url) {
|
|
|
264
559
|
state: 'idle',
|
|
265
560
|
attempts: 0,
|
|
266
561
|
generation: 0,
|
|
562
|
+
// WP-3 (D-4): the BOX-side socket identity the box's `hello` reports, recorded on every hello.
|
|
563
|
+
// Outbound geometry frames carry THIS value (the box checks it against its own accepting socket);
|
|
564
|
+
// 0 until the first hello, which is why a pre-hello geometry frame is queued (flushed with it).
|
|
565
|
+
boxSocketGeneration: 0,
|
|
267
566
|
pingMisses: 0,
|
|
268
567
|
heartbeatTimer: null,
|
|
269
568
|
reconnectTimer: null,
|
|
@@ -283,8 +582,14 @@ function _newLink(machineId, machineName, url) {
|
|
|
283
582
|
};
|
|
284
583
|
}
|
|
285
584
|
|
|
286
|
-
|
|
287
|
-
|
|
585
|
+
/**
|
|
586
|
+
* WP-1 (D-1): resolve the link for `machineId` — with a `ctx`, that ctx's VIEWER link; without, the
|
|
587
|
+
* machine's CONTROL link. Returns null when no such link exists.
|
|
588
|
+
*/
|
|
589
|
+
function linkFor(machineId, ctx) {
|
|
590
|
+
const id = _string(machineId, 160);
|
|
591
|
+
if (ctx) return _links.get(_viewerKey(id, ctx)) || null;
|
|
592
|
+
return _links.get(id) || null;
|
|
288
593
|
}
|
|
289
594
|
|
|
290
595
|
function listLinks() {
|
|
@@ -292,9 +597,10 @@ function listLinks() {
|
|
|
292
597
|
}
|
|
293
598
|
|
|
294
599
|
/**
|
|
295
|
-
* Open (or reuse) the
|
|
296
|
-
*
|
|
297
|
-
* degrades to the existing "no
|
|
600
|
+
* Open (or reuse) the CONTROL link to a machine — the catalog `list`/onLinkReady lane, keyed by the
|
|
601
|
+
* machine id. `machine` is a registry row or a machine instance id. Never throws: a machine that is
|
|
602
|
+
* not registered / disabled / url-less yields null and the caller degrades to the existing "no
|
|
603
|
+
* frames" UX. A viewing hub socket gets its OWN link via `_ensureViewerLink` (WP-1 D-1).
|
|
298
604
|
*/
|
|
299
605
|
function openLink(machine, deps) {
|
|
300
606
|
const d = _d(deps);
|
|
@@ -302,7 +608,18 @@ function openLink(machine, deps) {
|
|
|
302
608
|
if (!row) return null;
|
|
303
609
|
const machineId = _string(row.instanceId, 160);
|
|
304
610
|
if (!machineId) return null;
|
|
305
|
-
|
|
611
|
+
return _openKeyed(machineId, row, d, 'control', null);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* WP-1 (D-1): the shared open/reuse body for BOTH link kinds. `key` is the Map key (machineId for a
|
|
616
|
+
* control link, `_viewerKey` for a viewer link); the enabled/url/health checks and `_dial` are
|
|
617
|
+
* identical for both, so only the key/kind/viewerCtx differ.
|
|
618
|
+
*/
|
|
619
|
+
function _openKeyed(key, row, d, kind, viewerCtx) {
|
|
620
|
+
const machineId = _string(row.instanceId, 160);
|
|
621
|
+
if (!machineId) return null;
|
|
622
|
+
const existing = _links.get(key);
|
|
306
623
|
if (existing && !existing.closed) {
|
|
307
624
|
// D-3: a link parked on a missing credential is retried by the NEXT acquire — that is the
|
|
308
625
|
// "hub restarted with CTM_SYNC_PEER_TOKEN set" path, and it is the only thing that can clear
|
|
@@ -316,8 +633,8 @@ function openLink(machine, deps) {
|
|
|
316
633
|
const url = _string(row.url, 1000);
|
|
317
634
|
if (!url) return null;
|
|
318
635
|
if (_string(row.health, 40) === 'unreachable') return null;
|
|
319
|
-
const link = _newLink(machineId, _string(row.displayName, 200), url);
|
|
320
|
-
_links.set(
|
|
636
|
+
const link = _newLink(machineId, _string(row.displayName, 200), url, kind, viewerCtx);
|
|
637
|
+
_links.set(key, link);
|
|
321
638
|
_dial(link, d);
|
|
322
639
|
return link;
|
|
323
640
|
}
|
|
@@ -473,12 +790,35 @@ function _onUpstreamFrame(link, raw, d) {
|
|
|
473
790
|
// tolerated exactly as WP-2's dial tolerates it. A verdict that NAMES a relayed message type
|
|
474
791
|
// is the box refusing THAT frame, so it is reported to the socket that sent it instead of
|
|
475
792
|
// being swallowed (a silent refusal is indistinguishable from a broken relay).
|
|
476
|
-
_routeAuthError(link, frame);
|
|
793
|
+
_routeAuthError(link, frame, d);
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
if (frame.type === 'hub-bridge-request') {
|
|
797
|
+
// WP-0 (docs/remote-session-hub-bridge-2026-10-30.html §6.1, §8 WP-0): the box -> hub request
|
|
798
|
+
// lane. The BOX asks the hub that dialled it to run an op; the reply rides the SAME CONTROL
|
|
799
|
+
// socket. Route BEFORE the session-frame branches: the frame names no session and is not a
|
|
800
|
+
// relay frame. `handleRequest` never throws and never rejects, and it writes EXACTLY ONE
|
|
801
|
+
// response through the upstream send (`_sendUpstream`) — never a local run (R-7).
|
|
802
|
+
try {
|
|
803
|
+
const hubBridge = require('./hub-bridge');
|
|
804
|
+
const pending = hubBridge.handleRequest(link, frame, (reply) => _sendUpstream(link, reply, d));
|
|
805
|
+
if (pending && typeof pending.catch === 'function') {
|
|
806
|
+
pending.catch((error) => _report(d, `link ${link.machineInstanceId}: hub-bridge failed: ${error?.message || error}`));
|
|
807
|
+
}
|
|
808
|
+
} catch (error) {
|
|
809
|
+
_report(d, `link ${link.machineInstanceId}: hub-bridge failed: ${error?.message || error}`);
|
|
810
|
+
}
|
|
477
811
|
return;
|
|
478
812
|
}
|
|
479
813
|
|
|
480
814
|
const sessionId = _frameSessionId(frame);
|
|
481
|
-
if (!sessionId)
|
|
815
|
+
if (!sessionId) {
|
|
816
|
+
// WP-0 (D-4, §9 S-2): an upstream frame that still names no session is reported once per
|
|
817
|
+
// (link, type) rather than vanishing — this exact silent `return` hid the conversation-updated
|
|
818
|
+
// drop for four minutes of live traffic.
|
|
819
|
+
_reportUnroutedOnce(link, frame.type, d);
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
482
822
|
if (_routeReplyToOrigin(link, frame, sessionId, d)) return;
|
|
483
823
|
_fanout(link, frame, sessionId, d);
|
|
484
824
|
}
|
|
@@ -493,7 +833,24 @@ function _onHello(link, frame, d) {
|
|
|
493
833
|
link.ready = true;
|
|
494
834
|
link.dialing = false;
|
|
495
835
|
link.attempts = 0; // backoff resets ON SUCCESS (full-jitter contract)
|
|
836
|
+
// WP-3 (D-4, §9 S-7): the box's `hello` carries the identity of THIS (box-side) connection — the
|
|
837
|
+
// value every outbound geometry frame must present. Recorded before the pending flush below, so a
|
|
838
|
+
// queued pre-hello claim goes out with this generation.
|
|
839
|
+
link.boxSocketGeneration = Number(frame.socketGeneration) || 0;
|
|
496
840
|
_sendUpstream(link, remoteMachineCreate.peerClientInfoFrame(frame), d);
|
|
841
|
+
// WP-0 (docs/remote-session-hub-bridge-2026-10-30.html §5 R-2, §6.1, §8 WP-0): a CONTROL link
|
|
842
|
+
// identifies ITSELF to the box once per (re)connect, right after the client-info, so the box knows
|
|
843
|
+
// which hub dialled it and can route a `hub-bridge-request` back down this very socket. Sent ONLY
|
|
844
|
+
// on the control link (a viewer link carries one socket's session traffic, never the lane), and
|
|
845
|
+
// only when this process has an identity to name — `controlHelloFrame()` returns null pre-boot.
|
|
846
|
+
if (link.kind === 'control') {
|
|
847
|
+
try {
|
|
848
|
+
const helloFrame = require('./hub-bridge').controlHelloFrame();
|
|
849
|
+
if (helloFrame) _sendUpstream(link, helloFrame, d);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
_report(d, `link ${link.machineInstanceId}: hub-bridge hello failed: ${error?.message || error}`);
|
|
852
|
+
}
|
|
853
|
+
}
|
|
497
854
|
// Re-attach AFTER the client-info above: the box's protocol gate drops a terminal-protocol frame
|
|
498
855
|
// that arrives before a compatible client-info (the dial-order rule the create lane learned).
|
|
499
856
|
link.pingMisses = 0;
|
|
@@ -501,7 +858,9 @@ function _onHello(link, frame, d) {
|
|
|
501
858
|
for (const sessionId of link.viewers.keys()) _attachUpstream(link, sessionId, d, alreadyReady ? 'link-reconnect' : 'link-ready');
|
|
502
859
|
// WP-6: a catalog `list` requested while the link was still dialing is flushed HERE, after the
|
|
503
860
|
// client-info above — the box's protocol gate drops a terminal-protocol frame that arrives first.
|
|
504
|
-
|
|
861
|
+
// WP-3 (D-4): the hop rewrite is applied at FLUSH time (not enqueue time), so a geometry frame
|
|
862
|
+
// queued before this hello carries the generation the hello just reported.
|
|
863
|
+
for (const queued of link.pendingSends.splice(0)) _sendUpstream(link, _toBoxHop(link, queued), d);
|
|
505
864
|
// A catalog request that was queued while this link was dialing sends its `list` HERE, so this
|
|
506
865
|
// hello has already asked the box for its list — skip the refresh hook below rather than paying a
|
|
507
866
|
// second identical round-trip on the same connection.
|
|
@@ -509,7 +868,9 @@ function _onHello(link, frame, d) {
|
|
|
509
868
|
for (const finish of [...link.catalogWaiters.values()]) _sendCatalogList(link, finish, d);
|
|
510
869
|
// WP-6 §8: "on link (re)connect" is one of the catalog's refresh triggers. Injected (not
|
|
511
870
|
// required) so this module never has to depend on the catalog module it feeds.
|
|
512
|
-
|
|
871
|
+
// WP-1 (D-1/S-9): the CONTROL link owns the catalog lane — a viewer link never fires this, so a
|
|
872
|
+
// per-socket viewing link never pays a catalog round-trip.
|
|
873
|
+
if (!catalogAsked && link.kind === 'control' && typeof d.onLinkReady === 'function') {
|
|
513
874
|
try { d.onLinkReady(link); } catch { /* expected: a catalog hook fault must not break the link */ }
|
|
514
875
|
}
|
|
515
876
|
}
|
|
@@ -634,11 +995,17 @@ function _takePending(link, match) {
|
|
|
634
995
|
* Deliver it to the originating socket; the routine verdict on our own client-info carries no
|
|
635
996
|
* `messageType` and stays noise.
|
|
636
997
|
*/
|
|
637
|
-
function _routeAuthError(link, frame) {
|
|
998
|
+
function _routeAuthError(link, frame, d) {
|
|
638
999
|
const messageType = _string(frame.messageType, 40);
|
|
639
1000
|
if (!messageType) return;
|
|
640
1001
|
const entry = _takePending(link, (e) => e.requestType === messageType);
|
|
641
|
-
if (entry)
|
|
1002
|
+
if (entry) {
|
|
1003
|
+
_sendFrame(entry.ctx, frame, d);
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
// WP-0 (D-4b): a refusal of a RELAYED type with no pending origin is the silent case — count it
|
|
1007
|
+
// (the reply routing above is unchanged, so this is diagnostics only).
|
|
1008
|
+
if (RELAY_TYPES.has(messageType)) _noteUpstreamRefused(link, frame, messageType, d);
|
|
642
1009
|
}
|
|
643
1010
|
|
|
644
1011
|
/**
|
|
@@ -659,7 +1026,7 @@ function _routeReplyToOrigin(link, frame, sessionId, d) {
|
|
|
659
1026
|
if (requestId) {
|
|
660
1027
|
const entry = _takePending(link, (e) => e.sessionId === sessionId
|
|
661
1028
|
&& e.replyType === frame.type && e.requestId === requestId);
|
|
662
|
-
if (entry) _sendFrame(entry.ctx, frame);
|
|
1029
|
+
if (entry) _sendFrame(entry.ctx, frame, d);
|
|
663
1030
|
return true;
|
|
664
1031
|
}
|
|
665
1032
|
// A result carrying no requestId keeps the legacy clientMessageId/type fallback below.
|
|
@@ -670,21 +1037,89 @@ function _routeReplyToOrigin(link, frame, sessionId, d) {
|
|
|
670
1037
|
: null)
|
|
671
1038
|
|| _takePending(link, (e) => e.sessionId === sessionId && e.replyType === frame.type);
|
|
672
1039
|
if (!entry) return false;
|
|
673
|
-
_sendFrame(entry.ctx, frame);
|
|
1040
|
+
_sendFrame(entry.ctx, frame, d);
|
|
674
1041
|
return true;
|
|
675
1042
|
}
|
|
676
1043
|
|
|
1044
|
+
/**
|
|
1045
|
+
* WP-1 (D-3/D-4): deliver one box content frame straight to the viewer's hub socket through
|
|
1046
|
+
* `sendFrame` — the SAME no-admission primitive server.js's local frame channel uses. A viewer
|
|
1047
|
+
* whose socket is gone (or whose write throws) did not get this frame, so it is counted (WP-0/D-5)
|
|
1048
|
+
* and THAT viewer's link is closed (D-4): the box detaches that delivery and the socket's own
|
|
1049
|
+
* reconnect re-attaches + re-bootstraps. No other viewer is affected.
|
|
1050
|
+
*
|
|
1051
|
+
* WP-2 (R-3/R-6): both content frame types (`frame`, `frame-v2`) take this direct path. A
|
|
1052
|
+
* `frame-v2` is projected to v1 for a hub socket that did not negotiate v2 — per-HOP versioning,
|
|
1053
|
+
* the hub speaks v2 to the box on its own behalf and v1 to a V1 viewer. The projection runs
|
|
1054
|
+
* WITHOUT validation for a V2 socket (verbatim; the browser validates), and a projection THROW
|
|
1055
|
+
* counts as `projection-error` and skips just that frame for that viewer — the link stays OPEN,
|
|
1056
|
+
* matching the box's own fan-out.
|
|
1057
|
+
*/
|
|
1058
|
+
function _deliverFrameDirect(ctx, frame, link, d) {
|
|
1059
|
+
if (!ctx || !ctx.ws || ctx.ws.readyState !== 1) {
|
|
1060
|
+
_noteUndelivered(ctx, frame, 'socket-not-open', d);
|
|
1061
|
+
closeLink(link, d);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
let payload = frame;
|
|
1065
|
+
if (frame.type === 'frame-v2' && ctx.ws.frameProtocol !== FRAME_PROTOCOL_V2) {
|
|
1066
|
+
try {
|
|
1067
|
+
// R-3: NARROW the v2 frame to what this v1 socket understands. The v2 projection is the
|
|
1068
|
+
// step that VALIDATES the authority (projectFrameForProtocol's v1 branch is a pure
|
|
1069
|
+
// field-strip and refuses nothing on its own), so a malformed frame-v2 is skipped here for
|
|
1070
|
+
// THIS viewer instead of being handed down half-understood.
|
|
1071
|
+
payload = projectFrameForProtocol(projectFrameForProtocol(frame, FRAME_PROTOCOL_V2), FRAME_PROTOCOL_V1);
|
|
1072
|
+
} catch {
|
|
1073
|
+
// expected: a v2 frame this hub cannot project (invalid authority) is skipped for THIS viewer
|
|
1074
|
+
// only; the counter is the contract and the link must survive (R-6).
|
|
1075
|
+
_noteUndelivered(ctx, frame, 'projection-error', d);
|
|
1076
|
+
return;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
if (!sendFrame(ctx.ws, JSON.stringify(payload))) {
|
|
1080
|
+
_noteUndelivered(ctx, frame, 'send-error', d);
|
|
1081
|
+
closeLink(link, d);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
677
1085
|
function _fanout(link, frame, sessionId, d) {
|
|
678
1086
|
const viewers = link.viewers.get(sessionId);
|
|
679
1087
|
if (!viewers || viewers.size === 0) return;
|
|
1088
|
+
// WP-1 (D-2/D-3): a viewer link holds just its own ctx, and a content payload (`frame` or, WP-2,
|
|
1089
|
+
// `frame-v2`) goes straight to that socket with `sendFrame` (the local path — no hub admission),
|
|
1090
|
+
// projected per receiving socket inside _deliverFrameDirect. Every other frame type keeps
|
|
1091
|
+
// the existing paths: a geometry frame carries the RECEIVING hub socket's own generation, so it is
|
|
1092
|
+
// still serialized per viewer via _sendFrame.
|
|
1093
|
+
if (RELAYED_CONTENT_FRAME_TYPES.has(frame.type) && link.kind === 'viewer') {
|
|
1094
|
+
for (const ctx of viewers) _deliverFrameDirect(ctx, frame, link, d);
|
|
1095
|
+
return;
|
|
1096
|
+
}
|
|
1097
|
+
if (GEOMETRY_INBOUND_TYPES.has(frame.type)) {
|
|
1098
|
+
for (const ctx of viewers) _sendFrame(ctx, frame, d);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
680
1101
|
const payload = JSON.stringify(frame);
|
|
681
|
-
for (const ctx of viewers) _sendToCtx(ctx, payload);
|
|
1102
|
+
for (const ctx of viewers) _sendToCtx(ctx, payload, frame, d);
|
|
682
1103
|
}
|
|
683
1104
|
|
|
684
1105
|
// ---------------------------------------------------------------------------------------------
|
|
685
1106
|
// Downstream (hub browser -> box)
|
|
686
1107
|
// ---------------------------------------------------------------------------------------------
|
|
687
1108
|
|
|
1109
|
+
/**
|
|
1110
|
+
* WP-1 (D-4): install the ONCE-per-hub-socket close hook that releases every bit of this socket's
|
|
1111
|
+
* relay state — its own viewer links close outright, and its viewer entries on any other link drop
|
|
1112
|
+
* (arming the idle close). Installed from BOTH the viewing path (`viewSession`) and the forward-only
|
|
1113
|
+
* path (`_ensureViewerLink`): a socket that only ever relays a lifecycle/prompt frame for a mirror it
|
|
1114
|
+
* never viewed still owns a viewer link, and without this hook that link would outlive the socket.
|
|
1115
|
+
* Idempotent per socket, because the relay must not need an edit to server.js's own `ws.on('close')`.
|
|
1116
|
+
*/
|
|
1117
|
+
function _hookSocketClose(ctx, d) {
|
|
1118
|
+
if (!ctx || !ctx.ws || typeof ctx.ws.once !== 'function' || ctx.ws._remoteRelayCloseHooked) return;
|
|
1119
|
+
ctx.ws._remoteRelayCloseHooked = true;
|
|
1120
|
+
ctx.ws.once('close', () => releaseSocket(ctx, d));
|
|
1121
|
+
}
|
|
1122
|
+
|
|
688
1123
|
/**
|
|
689
1124
|
* Register `ctx` as a viewer of `sessionId` on this link. The FIRST viewer of a session makes the
|
|
690
1125
|
* link attach upstream and ask for a keyframe, so the box pushes that session's frames to the link.
|
|
@@ -706,12 +1141,8 @@ function viewSession(link, sessionId, ctx, deps) {
|
|
|
706
1141
|
if (ctx) {
|
|
707
1142
|
viewers.add(ctx);
|
|
708
1143
|
// AC-3d: a hub browser tab closing RELEASES its viewers (and only then arms the machine's
|
|
709
|
-
// 60 s idle close) — it never closes the upstream link directly.
|
|
710
|
-
|
|
711
|
-
if (ctx.ws && typeof ctx.ws.once === 'function' && !ctx.ws._remoteRelayCloseHooked) {
|
|
712
|
-
ctx.ws._remoteRelayCloseHooked = true;
|
|
713
|
-
ctx.ws.once('close', () => releaseSocket(ctx, d));
|
|
714
|
-
}
|
|
1144
|
+
// 60 s idle close) — it never closes the upstream link directly.
|
|
1145
|
+
_hookSocketClose(ctx, d);
|
|
715
1146
|
}
|
|
716
1147
|
if (first && link.ready) _attachUpstream(link, id, d, 'hub-view');
|
|
717
1148
|
return first;
|
|
@@ -727,10 +1158,18 @@ function releaseViewer(link, sessionId, ctx, deps) {
|
|
|
727
1158
|
_maybeScheduleIdleClose(link, d);
|
|
728
1159
|
}
|
|
729
1160
|
|
|
730
|
-
/** A hub socket went away: drop it from
|
|
1161
|
+
/** A hub socket went away: close ITS viewer links outright, and drop it from any other link's viewers. */
|
|
731
1162
|
function releaseSocket(ctx, deps) {
|
|
732
1163
|
const d = _d(deps);
|
|
733
|
-
|
|
1164
|
+
// Snapshot: closeLink mutates _links while we iterate (WP-1 D-4).
|
|
1165
|
+
for (const link of [..._links.values()]) {
|
|
1166
|
+
// WP-1 (D-4): a viewer link belongs to exactly ONE hub socket, so that socket going away closes
|
|
1167
|
+
// the link IMMEDIATELY — no 60 s idle. The box then detaches that delivery, and the socket's own
|
|
1168
|
+
// reconnect re-attaches + re-bootstraps. (A LIVE socket that merely stops viewing keeps the idle.)
|
|
1169
|
+
if (link.kind === 'viewer' && link.viewerCtx === ctx) {
|
|
1170
|
+
closeLink(link, d);
|
|
1171
|
+
continue;
|
|
1172
|
+
}
|
|
734
1173
|
for (const [sessionId, viewers] of [...link.viewers]) {
|
|
735
1174
|
if (viewers.delete(ctx) && viewers.size === 0) link.viewers.delete(sessionId);
|
|
736
1175
|
}
|
|
@@ -777,7 +1216,7 @@ function _clearIdleClose(link, d) {
|
|
|
777
1216
|
* re-syncs via keyframe.
|
|
778
1217
|
*/
|
|
779
1218
|
function forward(link, msg, ctx, deps) {
|
|
780
|
-
if (!link || link.closed) return false;
|
|
1219
|
+
if (!link || link.closed) { if (msg?.type === 'kill') console.warn(`[kill-drop] site=relay-link-closed id=${sessionIdsOf(msg)[0] || ''}`); return false; }
|
|
781
1220
|
const d = _d(deps);
|
|
782
1221
|
const sessionId = sessionIdsOf(msg)[0] || '';
|
|
783
1222
|
_rememberPending(link, sessionId, msg, ctx, d);
|
|
@@ -789,7 +1228,7 @@ function forward(link, msg, ctx, deps) {
|
|
|
789
1228
|
if (link.pendingSends.length < PENDING_SEND_MAX) link.pendingSends.push(msg);
|
|
790
1229
|
return true;
|
|
791
1230
|
}
|
|
792
|
-
const sent = _sendUpstream(link, msg, d);
|
|
1231
|
+
const sent = _sendUpstream(link, _toBoxHop(link, msg), d);
|
|
793
1232
|
// WP-6: a relayed kill changes the box's session list, so the catalog re-reads it right away
|
|
794
1233
|
// rather than waiting out the ~15 s probe cadence (the row becomes exited on the box), and the
|
|
795
1234
|
// in-memory mirror registration for the killed id is dropped (the catalog keeps routing it).
|
|
@@ -828,15 +1267,21 @@ function interceptRelay(ctx, msg, deps) {
|
|
|
828
1267
|
const mirror = d.mirrorFor(id);
|
|
829
1268
|
if (mirror) mirrors.push({ id, mirror });
|
|
830
1269
|
}
|
|
831
|
-
if (mirrors.length === 0) return false;
|
|
1270
|
+
if (mirrors.length === 0) { if (type === 'kill') console.warn(`[kill-drop] site=relay-no-mirror id=${ids[0]}`); return false; }
|
|
832
1271
|
// A `view` frame declaring BOTH local and remote sessions is left to the local handler (it owns
|
|
833
1272
|
// the local viewer set); the mirror ids are still registered below.
|
|
834
1273
|
const mixedView = type === 'view' && mirrors.length !== ids.length;
|
|
835
1274
|
|
|
836
1275
|
let handled = true;
|
|
1276
|
+
// The viewer links this frame touched, so their idle close can be re-evaluated once per frame
|
|
1277
|
+
// (see the forward-only note below) instead of at every early `continue`.
|
|
1278
|
+
const touched = [];
|
|
837
1279
|
for (const { id, mirror } of mirrors) {
|
|
838
|
-
|
|
839
|
-
|
|
1280
|
+
// WP-1 (D-1): the link is resolved from the mirror's machine AND the calling hub socket, so each
|
|
1281
|
+
// hub socket relays on its OWN upstream link (one box delivery per viewer).
|
|
1282
|
+
const link = _ensureViewerLink(mirror, ctx, d);
|
|
1283
|
+
if (!link) { if (type === 'kill') console.warn(`[kill-drop] site=relay-no-link id=${id}`); handled = false; continue; }
|
|
1284
|
+
touched.push(link);
|
|
840
1285
|
if (VIEW_TYPES.has(type)) {
|
|
841
1286
|
const first = viewSession(link, id, ctx, d);
|
|
842
1287
|
// `attach`/`view` are synthesized upstream by viewSession; `keyframe-req` and the rest are
|
|
@@ -850,17 +1295,35 @@ function interceptRelay(ctx, msg, deps) {
|
|
|
850
1295
|
}
|
|
851
1296
|
forward(link, msg, ctx, d);
|
|
852
1297
|
}
|
|
1298
|
+
// WP-1 (D-4): a FORWARD-ONLY viewer link — a lifecycle/prompt frame for a mirror this socket never
|
|
1299
|
+
// viewed — registers no viewer, so nothing above would ever arm its idle close and it would hold an
|
|
1300
|
+
// upstream socket open forever. Arm it here (a no-op for a link that does hold a viewer, and
|
|
1301
|
+
// idempotent when releaseViewer already armed it). A later attach cancels it via _clearIdleClose.
|
|
1302
|
+
for (const link of touched) _maybeScheduleIdleClose(link, d);
|
|
853
1303
|
return mixedView ? false : handled;
|
|
854
1304
|
}
|
|
855
1305
|
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1306
|
+
/**
|
|
1307
|
+
* WP-1 (D-1/D-2): the VIEWER link for (mirror.machineInstanceId, ctx) — replaces the old machine-only
|
|
1308
|
+
* `_ensureLink`, so each hub socket gets its own upstream link and therefore its own box delivery.
|
|
1309
|
+
* Reuse clears the idle close. A missing ctx (or a ctx with no ws) has no per-socket identity and
|
|
1310
|
+
* falls back to the control link, silently.
|
|
1311
|
+
*/
|
|
1312
|
+
function _ensureViewerLink(mirror, ctx, d) {
|
|
1313
|
+
const machineId = _string(mirror && mirror.machineInstanceId, 160);
|
|
1314
|
+
if (!machineId) return null;
|
|
1315
|
+
if (!ctx || !ctx.ws) return openLink(machineId, d);
|
|
1316
|
+
const row = d.machineRow(machineId, d);
|
|
1317
|
+
if (!row) return null;
|
|
1318
|
+
const link = _openKeyed(_viewerKey(machineId, ctx), row, d, 'viewer', ctx);
|
|
1319
|
+
if (link) {
|
|
1320
|
+
// WP-1 (D-4): this socket now owns a viewer link — hook its close here too, not only in
|
|
1321
|
+
// viewSession, so a FORWARD-ONLY relay (a lifecycle/prompt frame for a mirror it never viewed)
|
|
1322
|
+
// is still released the moment the socket goes away. Idempotent per socket.
|
|
1323
|
+
_hookSocketClose(ctx, d);
|
|
1324
|
+
_clearIdleClose(link, d);
|
|
862
1325
|
}
|
|
863
|
-
return
|
|
1326
|
+
return link;
|
|
864
1327
|
}
|
|
865
1328
|
|
|
866
1329
|
function installIntercept() {
|
|
@@ -884,7 +1347,10 @@ function closeLink(link, deps) {
|
|
|
884
1347
|
link.ready = false;
|
|
885
1348
|
link.generation += 1;
|
|
886
1349
|
try { socket?.close?.(); } catch { /* expected: the upstream socket is already gone */ }
|
|
887
|
-
|
|
1350
|
+
// WP-1 (D-1): the Map is keyed by `link.key` (machineId for a control link, _viewerKey for a
|
|
1351
|
+
// viewer link) — never by machineId alone, or closing one viewer's link would evict another's.
|
|
1352
|
+
const key = link.key || link.machineInstanceId;
|
|
1353
|
+
if (_links.get(key) === link) _links.delete(key);
|
|
888
1354
|
}
|
|
889
1355
|
|
|
890
1356
|
function _report(d, message) {
|
|
@@ -892,6 +1358,19 @@ function _report(d, message) {
|
|
|
892
1358
|
if (d && d.log && typeof d.log.warn === 'function') d.log.warn(`[remote-machine-link] ${message}`);
|
|
893
1359
|
}
|
|
894
1360
|
|
|
1361
|
+
/**
|
|
1362
|
+
* WP-0 (docs/hub-viewed-box-conv-freshness-and-paste-render-2026-10-31.html §5 D-4): report an
|
|
1363
|
+
* upstream frame the relay cannot route — once per (link, type), so a repeating unnamed frame
|
|
1364
|
+
* cannot flood the log while the first occurrence still shows up in `ctm.log`.
|
|
1365
|
+
*/
|
|
1366
|
+
function _reportUnroutedOnce(link, type, d) {
|
|
1367
|
+
const key = _string(type, 60) || '(none)';
|
|
1368
|
+
if (!link._unroutedTypes) link._unroutedTypes = new Set();
|
|
1369
|
+
if (link._unroutedTypes.has(key)) return;
|
|
1370
|
+
link._unroutedTypes.add(key);
|
|
1371
|
+
_report(d, `link ${link.machineInstanceId}: dropped upstream frame type=${key} (names no session)`);
|
|
1372
|
+
}
|
|
1373
|
+
|
|
895
1374
|
/** Test seam: links and their timers are module state and must never leak between tests. */
|
|
896
1375
|
function _resetForTests() {
|
|
897
1376
|
for (const link of [..._links.values()]) {
|
|
@@ -906,6 +1385,10 @@ function _resetForTests() {
|
|
|
906
1385
|
try { link.heartbeatTimer && clearInterval(link.heartbeatTimer); } catch { /* expected: test teardown */ }
|
|
907
1386
|
}
|
|
908
1387
|
_links.clear();
|
|
1388
|
+
// WP-0 (D-5): the undelivered counters are module state too — a test that drove drops must not
|
|
1389
|
+
// leak its (session, reason) rows into the next one. Same for the WP-0 upstream-refusal counters.
|
|
1390
|
+
_undelivered.clear();
|
|
1391
|
+
_upstreamRefused.clear();
|
|
909
1392
|
_injected = null;
|
|
910
1393
|
}
|
|
911
1394
|
|
|
@@ -915,12 +1398,16 @@ module.exports = {
|
|
|
915
1398
|
IDLE_CLOSE_MS,
|
|
916
1399
|
PING_MISS_LIMIT,
|
|
917
1400
|
CATALOG_TIMEOUT_MS,
|
|
1401
|
+
UNDELIVERED_REPORT_INTERVAL_MS,
|
|
918
1402
|
RELAY_TYPES,
|
|
919
1403
|
VIEW_TYPES,
|
|
920
1404
|
REPLY_TYPES,
|
|
921
1405
|
REPLY_FOR_REQUEST,
|
|
922
1406
|
REMEMBER_TYPES,
|
|
923
1407
|
configure,
|
|
1408
|
+
setDiagnosticsSink,
|
|
1409
|
+
undeliveredCounters,
|
|
1410
|
+
upstreamRefusedCounters,
|
|
924
1411
|
installIntercept,
|
|
925
1412
|
openLink,
|
|
926
1413
|
linkFor,
|