From bdeb9759d8827abf055eb0c06a0fc1a6afa37cf7 Mon Sep 17 00:00:00 2001 From: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Date: Wed, 29 Jul 2026 15:23:35 -0700 Subject: [PATCH] feat(ssh): make PTY source credit the default --- config/reliability-gates.jsonc | 71 +----- .../2026-07-27-ssh-relay-pty-backpressure.md | 232 ++++++++---------- src/main/ipc/ssh.ts | 4 +- src/main/persistence.test.ts | 32 +-- src/main/persistence.ts | 8 +- .../ssh/ssh-pty-source-credit-rollout.test.ts | 79 ------ src/main/ssh/ssh-pty-source-credit-rollout.ts | 38 --- src/main/ssh/ssh-relay-deploy.test.ts | 17 +- src/main/ssh/ssh-relay-deploy.ts | 100 +------- .../ssh-relay-session-data-delivery.test.ts | 124 +++------- .../ssh/ssh-relay-session-incarnation.test.ts | 4 +- .../ssh-relay-session-model-migration.test.ts | 10 +- ...elay-session-reconnect-incarnation.test.ts | 20 +- .../ssh-relay-session-recovery-races.test.ts | 5 +- src/main/ssh/ssh-relay-session.test.ts | 17 +- src/main/ssh/ssh-relay-session.ts | 25 +- src/relay/dispatcher.test.ts | 12 +- .../relay-pty-source-publication.test.ts | 32 --- src/relay/relay.ts | 15 +- src/relay/ssh-pty-consumer-session-adapter.ts | 7 +- .../settings/SshHostAdvancedFields.test.tsx | 23 -- .../settings/SshHostAdvancedFields.tsx | 30 --- .../settings/ssh-target-draft.test.ts | 15 -- .../components/settings/ssh-target-draft.ts | 6 +- .../settings/ssh-target-save-payload.test.ts | 22 +- .../settings/ssh-target-save-payload.ts | 2 - src/renderer/src/i18n/locales/en.json | 5 +- src/renderer/src/i18n/locales/es.json | 5 +- src/renderer/src/i18n/locales/ja.json | 5 +- src/renderer/src/i18n/locales/ko.json | 5 +- src/renderer/src/i18n/locales/zh.json | 5 +- src/shared/ssh-types.ts | 2 - tests/e2e/ssh-docker-relay-perf.spec.ts | 41 ++-- 33 files changed, 240 insertions(+), 778 deletions(-) delete mode 100644 src/main/ssh/ssh-pty-source-credit-rollout.test.ts delete mode 100644 src/main/ssh/ssh-pty-source-credit-rollout.ts delete mode 100644 src/renderer/src/components/settings/SshHostAdvancedFields.test.tsx diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 4faebd1462c..dc0bda5cc61 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -3082,15 +3082,15 @@ "providers": ["local", "daemon", "ssh", "remote-runtime"], "coveredPlatforms": ["macos"], "coveredProviders": ["ssh"], - "coverageNotes": "Local macOS evidence covers main-process pending-output and projection-admission caps, runtime path-provenance history reuse, saturated-tail wait detection, direct-SSH source retention/accounting, per-target rollout persistence and session capture, exact provider-generation pause ownership, renderer exit/data ordering, decoder input bounds, bounded reconnect scheduling, and WSL stdio transport settlement through deterministic unit contracts. A joined main/runtime oracle exercises renderer-sourced headed semantics and headless-model snapshot semantics without launching a live paired server. Separately, a macOS-hosted Docker OpenSSH run exercises only the deployed Linux relay and direct SSH provider through the all-target environment override, with an exact 256 KiB source-credit plateau, concurrent PTY typing, fixed-size filesystem frames, Git churn, and owner-lease reconnect; it does not exercise the settings UI or persisted target selection. Neither deterministic topology labels nor Docker SSH constitute live headed desktop, headless orca serve, or physical WSL evidence; local/daemon, prior-version daemons, Windows named pipes/ConPTY, folder workspaces, and mixed-version clients also remain uncovered.", + "coverageNotes": "Local macOS evidence covers main-process pending-output and projection-admission caps, runtime path-provenance history reuse, saturated-tail wait detection, direct-SSH source retention/accounting, always-on V1 negotiation with legacy peer fallback, exact provider-generation pause ownership, renderer exit/data ordering, decoder input bounds, bounded reconnect scheduling, and WSL stdio transport settlement through deterministic unit contracts. A joined main/runtime oracle exercises renderer-sourced headed semantics and headless-model snapshot semantics without launching a live paired server. Separately, a macOS-hosted Docker OpenSSH run exercises only the deployed Linux relay and direct SSH provider with an exact 256 KiB source-credit plateau, concurrent PTY typing, fixed-size filesystem frames, Git churn, and owner-lease reconnect. Neither deterministic topology labels nor Docker SSH constitute live headed desktop, headless orca serve, or physical WSL evidence; local/daemon, prior-version daemons, Windows named pipes/ConPTY, folder workspaces, and mixed-version clients also remain uncovered.", "motivatingLinks": [ "https://github.com/stablyai/orca/pull/6836", "https://github.com/stablyai/orca/pull/6858", "https://github.com/stablyai/orca/pull/7002", "https://github.com/stablyai/orca/pull/7054" ], - "invariant": "High-volume terminal output must stay bounded across daemon socket writes, SSH relay writer/decoder/source retention/reconnect, main model and projection admission, runtime metadata, detectors, and tail checks, main-to-renderer in-flight bytes, renderer scheduler queues, and hidden-output restore without starving focused input. Each SSH target defaults to bounded legacy delivery unless its persisted experimental option or a process-startup-latched environment override selects V1; one session captures that selection immutably for its initial connection and automatic reconnects. Negotiated SSH spans remain exact and contiguous through activation, exit, cancellation, recovery, desktop projection, and required remote replacement; provisional activation data cannot project, reconnect may release it only into the exact attempt's private recovery quarantine after contract validation, and exit seals that quarantine against later same-token frames until ordered admission or exact cancellation proof. Reconnect checkpoint capture freezes exact provider-generation/PTy admission, cancels queued old work, and waits for the running raw completion before exposing its accepted checkpoint; timeout or failure detaches only that PTy's old model, makes the checkpoint unavailable, and releases the bounded fence without closing the shared provider, while the same failure outside an active exact migration remains generation-fatal and overlapping reconnects preserve any earlier outstanding fence. Pending renderer projection IDs are capped and compacted across split remainders, exit preparation owns its renderer fence through finalization, exit-time cancellation transfers published projections before proof commit, generation close fences late proof while draining exact projection waiters, closed-generation identity compacts without weakening stale rejection, canceled source-delivery retirement retains at most one ordered token per PTY, and pause/resume targets only the exact provider generation. Additional subscribers cannot stall the owner, while the required legacy primary retains backpressure.", - "oracle": "The current executable slice injects main-process renderer backlog pressure, then asserts unchanged path-provenance history reuse for pathless output, ordinary terminal-wait detection without a joined-tail allocation and with full prompt-family continuity, per-PTY and total pending-output caps, a 1,024-ID projection cap with split-remainder compaction, preserved sequenced-tail metadata, active-pending protection ahead of background trimming, exact provider-generation pause ownership, renderer exit/data ordering, and ACK-gated in-flight bounds. The rollout seam proves independent target selection, explicit environment precedence, normalized durable opt-in through SSH-config refresh, Advanced-settings rendering/save/clear behavior, and immutable initial/reconnect session selection. The joined source-intake/multiplex oracle blocks renderer or headless serialization, admits one snapshot-covered span and one trailing span, requires replacement reservation only after the authoritative sequence is known, advances upstream ACK eligibility exactly to the covered boundary after SnapshotEnd, and delivers then ACKs the trailing span through the ordinary live path without cancellation. Source-range ledger and multiplex seams additionally require cumulative byte ACK 40 then 100 for one 100-byte frame to release exact byte credit without early source settlement, admit a contiguous higher-generation recovered token while the prior token remains unsettled, and reject stale stream or source generations without releasing in-flight byte credit. Replacement reservation rolls back every earlier span on a later failure, commit requires the exact frozen transfer states, and an authoritatively reclaimed covered span rejects commit without local trim authority while rollback removes the reservation idempotently. The exit-deadline oracle publishes a source-backed desktop projection, advances a fake clock to cancellation, and requires one projection transfer, proof commit, preparation, and final exit with no provider close, retained obligation, duplicate finalization, or process-lifetime cancellation tombstone; a second controlled proof remains pending across generation close and must never publish final exit, while both paths release the renderer preparation lease exactly once. The model-migration oracle accepts span A, blocks span B's raw emulator completion, begins disconnect migration, and proves no checkpoint or reconnect attach can remain at A; releasing B advances the exact checkpoint to B and projects B once. Its fake-clock arm reaches the 10-second deadline, requires checkpoint-unavailable, one exact model reset, zero retained admission charge and timers, and no effect from late raw settlement. Its two-PTY failure arm rejects one running raw callback under an active exact migration and requires one checkpoint-unavailable result, one model reset, zero provider closes, retained sibling checkpoint and transport liveness, zero charges and timers, and an unaffected other generation; a non-migrating sibling oracle still requires generation close. The intake additionally closes 2,048 sequential provider generations into one exact range, rejects stale events across that range, and preserves an out-of-order live gap until its own close. Direct-SSH contracts additionally prove activation-response settlement before the first source frame, claim-gated projection under same-decoder-turn response/data delivery, exact early-ACK reservation through send settlement or same-token retry, recovery completion visible before lease-held frames transfers those frames only into the private quarantine and admits no output until exact checkpoint-to-recoveryEnd coverage, invalid-checkpoint restore retention through response settlement followed by fresh-token retry and one live source frame, isolated saturated-subscriber eviction with healthy owner liveness, preserved gate-off primary backpressure, one-write ownership across write(false)/drain, control selection ahead of queued ordinary PTY input with FIFO lanes and bounded fairness, liveness rebasing, failed-exit retention through late ACK or exact recovery, response-settlement cancellation authority, token-scoped exit cancellation, exact checkpoint-to-recoveryEnd continuity, stale-owner one-shot retry, no physical PTY teardown on recovery failure, exact provider-generation closure and once-only cleanup when recovery cancellation publication or proof rejects, 16 MiB-plus-1 MiB decoder caps, charged per-PTY/session retention budgets, and eight-wide isolated reattach. The live Electron perf oracle adds hidden-output floods, renderer scheduler queue depth, dropped-output-zero normal scenarios, and active key latency budgets before promotion.", + "invariant": "High-volume terminal output must stay bounded across daemon socket writes, SSH relay writer/decoder/source retention/reconnect, main model and projection admission, runtime metadata, detectors, and tail checks, main-to-renderer in-flight bytes, renderer scheduler queues, and hidden-output restore without starving focused input. Every new SSH session offers V1 and every same-build relay supports it; legacy delivery is reachable only when capability negotiation proves the peer cannot use V1. Negotiated SSH spans remain exact and contiguous through activation, exit, cancellation, recovery, desktop projection, and required remote replacement; provisional activation data cannot project, reconnect may release it only into the exact attempt's private recovery quarantine after contract validation, and exit seals that quarantine against later same-token frames until ordered admission or exact cancellation proof. Reconnect checkpoint capture freezes exact provider-generation/PTy admission, cancels queued old work, and waits for the running raw completion before exposing its accepted checkpoint; timeout or failure detaches only that PTy's old model, makes the checkpoint unavailable, and releases the bounded fence without closing the shared provider, while the same failure outside an active exact migration remains generation-fatal and overlapping reconnects preserve any earlier outstanding fence. Pending renderer projection IDs are capped and compacted across split remainders, exit preparation owns its renderer fence through finalization, exit-time cancellation transfers published projections before proof commit, generation close fences late proof while draining exact projection waiters, closed-generation identity compacts without weakening stale rejection, canceled source-delivery retirement retains at most one ordered token per PTY, and pause/resume targets only the exact provider generation. Additional subscribers cannot stall the owner, while the required legacy primary retains backpressure.", + "oracle": "The current executable slice injects main-process renderer backlog pressure, then asserts unchanged path-provenance history reuse for pathless output, ordinary terminal-wait detection without a joined-tail allocation and with full prompt-family continuity, per-PTY and total pending-output caps, a 1,024-ID projection cap with split-remainder compaction, preserved sequenced-tail metadata, active-pending protection ahead of background trimming, exact provider-generation pause ownership, renderer exit/data ordering, and ACK-gated in-flight bounds. The negotiation seam proves every initial connection and reconnect offers V1, the same-build relay advertises V1 without launch flags, old clients remain token-free, and method-not-found peers fall back without installing source ACK publication. The joined source-intake/multiplex oracle blocks renderer or headless serialization, admits one snapshot-covered span and one trailing span, requires replacement reservation only after the authoritative sequence is known, advances upstream ACK eligibility exactly to the covered boundary after SnapshotEnd, and delivers then ACKs the trailing span through the ordinary live path without cancellation. Source-range ledger and multiplex seams additionally require cumulative byte ACK 40 then 100 for one 100-byte frame to release exact byte credit without early source settlement, admit a contiguous higher-generation recovered token while the prior token remains unsettled, and reject stale stream or source generations without releasing in-flight byte credit. Replacement reservation rolls back every earlier span on a later failure, commit requires the exact frozen transfer states, and an authoritatively reclaimed covered span rejects commit without local trim authority while rollback removes the reservation idempotently. The exit-deadline oracle publishes a source-backed desktop projection, advances a fake clock to cancellation, and requires one projection transfer, proof commit, preparation, and final exit with no provider close, retained obligation, duplicate finalization, or process-lifetime cancellation tombstone; a second controlled proof remains pending across generation close and must never publish final exit, while both paths release the renderer preparation lease exactly once. The model-migration oracle accepts span A, blocks span B's raw emulator completion, begins disconnect migration, and proves no checkpoint or reconnect attach can remain at A; releasing B advances the exact checkpoint to B and projects B once. Its fake-clock arm reaches the 10-second deadline, requires checkpoint-unavailable, one exact model reset, zero retained admission charge and timers, and no effect from late raw settlement. Its two-PTY failure arm rejects one running raw callback under an active exact migration and requires one checkpoint-unavailable result, one model reset, zero provider closes, retained sibling checkpoint and transport liveness, zero charges and timers, and an unaffected other generation; a non-migrating sibling oracle still requires generation close. The intake additionally closes 2,048 sequential provider generations into one exact range, rejects stale events across that range, and preserves an out-of-order live gap until its own close. Direct-SSH contracts additionally prove activation-response settlement before the first source frame, claim-gated projection under same-decoder-turn response/data delivery, exact early-ACK reservation through send settlement or same-token retry, recovery completion visible before lease-held frames transfers those frames only into the private quarantine and admits no output until exact checkpoint-to-recoveryEnd coverage, invalid-checkpoint restore retention through response settlement followed by fresh-token retry and one live source frame, isolated saturated-subscriber eviction with healthy owner liveness, preserved legacy-primary backpressure, one-write ownership across write(false)/drain, control selection ahead of queued ordinary PTY input with FIFO lanes and bounded fairness, liveness rebasing, failed-exit retention through late ACK or exact recovery, response-settlement cancellation authority, token-scoped exit cancellation, exact checkpoint-to-recoveryEnd continuity, stale-owner one-shot retry, no physical PTY teardown on recovery failure, exact provider-generation closure and once-only cleanup when recovery cancellation publication or proof rejects, 16 MiB-plus-1 MiB decoder caps, charged per-PTY/session retention budgets, and eight-wide isolated reattach. The live Electron perf oracle adds hidden-output floods, renderer scheduler queue depth, dropped-output-zero normal scenarios, and active key latency budgets before promotion.", "commands": [ "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/pty.test.ts", "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/pty-pending-projection-admissions.test.ts src/main/ipc/ssh-pty-legacy-projection.test.ts src/main/ipc/ssh-pty-model-admission.test.ts --reporter=dot", @@ -3105,13 +3105,16 @@ "pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/ssh-relay-session-recovery-races.test.ts --reporter=dot", "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/ssh-pty-legacy-projection.test.ts --reporter=dot", "pnpm exec vitest run --config config/vitest.config.ts src/main/ipc/ssh-pty-remote-source-range-consumers.test.ts src/main/runtime/rpc/terminal-source-range-ledger.test.ts src/main/runtime/rpc/terminal-multiplex.test.ts --reporter=dot", - "pnpm exec vitest run --config config/vitest.config.ts src/shared/pty-consumer-session.test.ts src/main/ipc/ssh-pty-output-intake.test.ts src/main/ipc/ssh-pty-output-exit-deadline.test.ts src/main/ipc/ssh-pty-remote-source-range-consumers.test.ts src/main/runtime/rpc/terminal-multiplex.test.ts src/main/ssh/ssh-multiplexer-transport-writer.test.ts src/main/ssh/ssh-channel-multiplexer.test.ts src/main/ssh/ssh-pty-source-credit-rollout.test.ts src/main/ssh/ssh-relay-deploy.test.ts src/relay/dispatcher-client-writer.test.ts --reporter=dot", - "pnpm exec vitest run --config config/vitest.config.ts src/shared/pty-consumer-session.test.ts src/main/ipc/ssh-pty-output-intake.test.ts src/main/ipc/ssh-pty-output-generation-guard.test.ts src/main/ipc/ssh-pty-output-exit-deadline.test.ts src/main/ipc/ssh-pty-remote-source-range-consumers.test.ts src/main/runtime/rpc/terminal-multiplex.test.ts src/main/ssh/ssh-multiplexer-transport-writer.test.ts src/main/ssh/ssh-channel-multiplexer.test.ts src/main/ssh/ssh-channel-multiplexer-backpressure.test.ts src/main/ssh/ssh-pty-source-credit-rollout.test.ts src/main/ssh/ssh-relay-deploy.test.ts src/relay/dispatcher-client-writer.test.ts --reporter=dot", + "pnpm exec vitest run --config config/vitest.config.ts src/shared/pty-consumer-session.test.ts src/main/ipc/ssh-pty-output-intake.test.ts src/main/ipc/ssh-pty-output-exit-deadline.test.ts src/main/ipc/ssh-pty-remote-source-range-consumers.test.ts src/main/runtime/rpc/terminal-multiplex.test.ts src/main/ssh/ssh-multiplexer-transport-writer.test.ts src/main/ssh/ssh-channel-multiplexer.test.ts src/main/ssh/ssh-relay-deploy.test.ts src/relay/dispatcher-client-writer.test.ts --reporter=dot", + "pnpm exec vitest run --config config/vitest.config.ts src/shared/pty-consumer-session.test.ts src/main/ipc/ssh-pty-output-intake.test.ts src/main/ipc/ssh-pty-output-generation-guard.test.ts src/main/ipc/ssh-pty-output-exit-deadline.test.ts src/main/ipc/ssh-pty-remote-source-range-consumers.test.ts src/main/runtime/rpc/terminal-multiplex.test.ts src/main/ssh/ssh-multiplexer-transport-writer.test.ts src/main/ssh/ssh-channel-multiplexer.test.ts src/main/ssh/ssh-channel-multiplexer-backpressure.test.ts src/main/ssh/ssh-relay-deploy.test.ts src/relay/dispatcher-client-writer.test.ts --reporter=dot", "pnpm exec vitest run --config config/vitest.config.ts src/relay/relay-pty-source-restore-retry.test.ts src/main/ssh/ssh-relay-session-recovery-races.test.ts src/main/runtime/rpc/terminal-source-range-ledger.test.ts src/main/runtime/rpc/terminal-multiplex.test.ts src/main/ssh/ssh-multiplexer-transport-writer.test.ts src/main/ssh/ssh-channel-multiplexer-backpressure.test.ts src/main/providers/ssh-pty-notification-routing.test.ts src/main/providers/ssh-pty-source-delivery-ledger.test.ts src/main/ssh/ssh-pty-retired-source-deliveries.test.ts src/main/providers/ssh-pty-provider-agent-session-create-operation.test.ts src/main/providers/ssh-pty-provider-exit-race.test.ts src/main/ipc/ssh-pty-output-exit-deadline.test.ts src/main/ipc/ssh-pty-remote-source-range-consumers.test.ts --reporter=dot", "pnpm exec vitest run src/main/providers/ssh-pty-notification-routing.test.ts src/main/providers/ssh-pty-provider-exit-race.test.ts src/main/ssh/ssh-relay-session-data-delivery.test.ts src/main/ssh/ssh-relay-session-recovery-races.test.ts src/main/ssh/ssh-relay-session-reconnect-incarnation.test.ts src/main/ssh/ssh-relay-session-incarnation.test.ts src/main/ssh/ssh-relay-session.test.ts --reporter=dot", - "pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/ssh-pty-source-credit-rollout.test.ts src/main/ssh/ssh-relay-session.test.ts src/main/ssh/ssh-relay-session-reconnect-incarnation.test.ts src/main/persistence.test.ts src/renderer/src/components/settings/ssh-target-draft.test.ts src/renderer/src/components/settings/ssh-target-save-payload.test.ts src/renderer/src/components/settings/SshHostAdvancedFields.test.tsx --reporter=dot", + "pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/ssh-relay-session.test.ts src/main/ssh/ssh-relay-session-data-delivery.test.ts src/main/ssh/ssh-relay-session-reconnect-incarnation.test.ts src/main/ssh/ssh-relay-deploy.test.ts src/relay/ssh-pty-consumer-session-adapter.test.ts src/main/persistence.test.ts --reporter=dot", "pnpm exec vitest run --config config/vitest.config.ts src/main/providers/ssh-pty-notification-routing.test.ts src/main/providers/ssh-pty-provider-reattach-incarnation.test.ts src/relay/relay-pty-source-recovery-interleavings.test.ts src/main/ssh/ssh-relay-deploy.test.ts src/main/ssh/ssh-relay-session-data-delivery.test.ts --reporter=dot", "pnpm exec vitest run --config config/vitest.config.ts src/main/agent-hooks/wsl-hook-relay-sentinel.test.ts --reporter=dot", + "ORCA_E2E_SSH_DOCKER=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/ssh-docker-relay-perf.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", + "ORCA_E2E_SSH_DOCKER=1 pnpm exec playwright test tests/e2e/ssh-docker-relay-perf.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", + // Historical evidence commands; the override is ignored after the always-on transition. "ORCA_E2E_SSH_DOCKER=1 ORCA_SSH_PTY_SOURCE_CREDIT_V1=1 SKIP_BUILD=1 pnpm exec playwright test tests/e2e/ssh-docker-relay-perf.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1", "ORCA_E2E_SSH_DOCKER=1 ORCA_SSH_PTY_SOURCE_CREDIT_V1=1 pnpm exec playwright test tests/e2e/ssh-docker-relay-perf.spec.ts --config tests/playwright.config.ts --project electron-headless --workers=1" ], @@ -3162,12 +3165,8 @@ "src/main/ssh/ssh-multiplexer-transport-writer.test.ts", "src/main/ssh/ssh-channel-multiplexer.test.ts", "src/main/ssh/ssh-channel-multiplexer-backpressure.test.ts", - "src/main/ssh/ssh-pty-source-credit-rollout.test.ts", "src/main/ssh/ssh-relay-deploy.test.ts", "src/main/persistence.test.ts", - "src/renderer/src/components/settings/ssh-target-draft.test.ts", - "src/renderer/src/components/settings/ssh-target-save-payload.test.ts", - "src/renderer/src/components/settings/SshHostAdvancedFields.test.tsx", "src/relay/dispatcher-client-writer.test.ts", "src/main/agent-hooks/wsl-hook-relay-sentinel.test.ts", "tests/e2e/ssh-docker-relay-perf.spec.ts" @@ -3260,7 +3259,7 @@ "file": "src/main/ssh/ssh-relay-session-reconnect-incarnation.test.ts", "assertions": [ "fifty reattaches use at most eight workers and healthy siblings finish before one slow and one failed PTY", - "one target selection is captured before establish and remains unchanged through automatic reconnect" + "initial connection and automatic reconnect both offer source credit" ] }, { @@ -3269,36 +3268,10 @@ "each transient PTY failure is retried once without tearing down successful provider registration" ] }, - { - "file": "src/main/ssh/ssh-pty-source-credit-rollout.test.ts", - "assertions": [ - "targets default off and opt in independently", - "an explicit all-target environment value takes precedence over persisted target state", - "the default environment override is latched when the rollout module initializes" - ] - }, { "file": "src/main/persistence.test.ts", "assertions": [ - "only exact true persists, disabling clears the option, and SSH-config refresh preserves the Orca-owned target selection" - ] - }, - { - "file": "src/renderer/src/components/settings/ssh-target-draft.test.ts", - "assertions": [ - "editing preserves an enabled target option and reveals its Advanced fields" - ] - }, - { - "file": "src/renderer/src/components/settings/ssh-target-save-payload.test.ts", - "assertions": [ - "save persists enabled target state and explicitly clears disabled state on update" - ] - }, - { - "file": "src/renderer/src/components/settings/SshHostAdvancedFields.test.tsx", - "assertions": [ - "Advanced settings render the experimental per-target switch enabled with concise recovery copy" + "retired per-target source-credit selections are removed during normalization" ] }, { @@ -3344,7 +3317,7 @@ { "file": "src/relay/dispatcher.test.ts", "assertions": [ - "a saturated gate-off primary remains required backpressure and is never detached as an additional subscriber" + "a saturated legacy primary remains required backpressure and is never detached as an additional subscriber" ] }, { @@ -3593,15 +3566,6 @@ "durationSeconds": 7.17, "summary": "Three deterministic main/runtime files passed 78 tests, including joined renderer/headless snapshot admission, exact partial cumulative credit, and recovered-token continuity; no live paired server or Docker topology was exercised." }, - { - "date": "2026-07-29", - "runner": "local", - "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/shared/pty-consumer-session.test.ts src/main/ipc/ssh-pty-output-intake.test.ts src/main/ipc/ssh-pty-output-generation-guard.test.ts src/main/ipc/ssh-pty-output-exit-deadline.test.ts src/main/ipc/ssh-pty-remote-source-range-consumers.test.ts src/main/runtime/rpc/terminal-multiplex.test.ts src/main/ssh/ssh-multiplexer-transport-writer.test.ts src/main/ssh/ssh-channel-multiplexer.test.ts src/main/ssh/ssh-channel-multiplexer-backpressure.test.ts src/main/ssh/ssh-pty-source-credit-rollout.test.ts src/main/ssh/ssh-relay-deploy.test.ts src/relay/dispatcher-client-writer.test.ts --reporter=dot", - "result": "passed", - "durationSeconds": 6.59, - "summary": "Twelve deterministic source-intake, generation, exit-deadline, remote-consumer, multiplex, writer, rollout, and deploy files passed 205 tests, including exact active-generation gaps, exit-time ordering, control-lane drain priority, and typed mismatched-principal recovery with retained authority isolation." - }, { "date": "2026-07-29", "runner": "local", @@ -3620,15 +3584,6 @@ "durationSeconds": 1.86, "summary": "Seven current-worktree provider/session files passed 94 tests after a red deterministic seam reproduced completion visibility before lease-held recovery data; the candidate transfers held and later frames only into private recovery quarantine, final-commits after fence/admission, retires exited activation state, and preserves fail-closed malformed, gapped, overlapping, missing-body, cancellation-proof, and replacement behavior." }, - { - "date": "2026-07-28", - "runner": "local", - "platform": "macos", - "command": "pnpm exec vitest run --config config/vitest.config.ts src/main/ssh/ssh-pty-source-credit-rollout.test.ts src/main/ssh/ssh-relay-session.test.ts src/main/ssh/ssh-relay-session-reconnect-incarnation.test.ts src/main/persistence.test.ts src/renderer/src/components/settings/ssh-target-draft.test.ts src/renderer/src/components/settings/ssh-target-save-payload.test.ts src/renderer/src/components/settings/SshHostAdvancedFields.test.tsx --reporter=dot", - "result": "passed", - "durationSeconds": 15.78, - "summary": "Seven deterministic rollout, session, persistence, and renderer files passed 495 tests. The focused assertions prove per-target default-off opt-in, process-startup-latched environment precedence, normalized persistence through SSH-config refresh, Advanced-setting rendering/save/clear, and immutable selection through automatic reconnect; no live SSH or paired-runtime topology was exercised." - }, { "date": "2026-07-28", "runner": "local", @@ -3671,7 +3626,7 @@ }, "performanceBudget": { "required": true, - "evidence": "Suggested ceilings: renderer in-flight <=8MB total, <=512KB per PTY plus active reserve, renderer queued chars <=2MB, dropped backlogs 0, hidden restore <=1000ms, active key median/worst <=75ms/300ms in perf scenarios. Per-target selection adds one persisted boolean read and one constructor-time resolution per explicit session, with no polling, scans, subprocesses, provider fanout, or reconnect-path reevaluation. Restore-record retirement adds one exact map-identity check at response settlement with no polling, timers, scans, subprocesses, or provider calls. The Docker SSH gate enforces median/worst typing below 500/2,000 ms; the current private-transfer artifact observed 5.5/16.3 ms during an exact 256 KiB stalled-credit plateau, 141.2/151.5 ms during 95 completed filesystem/Git bulk reads, and a 16.0-second reconnect. With 1,024 retained provenance candidates, 4,096 pathless chunks dropped from 78.84ms and 4,096 array replacements to 1.74ms and zero replacements. Brace-free 1 KiB output dropped from 20.32/79.12/318.23ms to 0.31/0.64/2.35ms across 4,096/16,384/65,536 chunks. Repeated ordinary 252,000-character tail checks dropped from 96.34/464.20/941.79/3,796.70ms to 3.21/15.68/31.34/129.72ms across 100/500/1,000/4,096 updates." + "evidence": "Suggested ceilings: renderer in-flight <=8MB total, <=512KB per PTY plus active reserve, renderer queued chars <=2MB, dropped backlogs 0, hidden restore <=1000ms, active key median/worst <=75ms/300ms in perf scenarios. Always-on capability offers add no persisted setting reads, polling, scans, subprocesses, provider fanout, or reconnect-path reevaluation. Restore-record retirement adds one exact map-identity check at response settlement with no polling, timers, scans, subprocesses, or provider calls. The Docker SSH gate enforces median/worst typing below 500/2,000 ms; the current private-transfer artifact observed 5.5/16.3 ms during an exact 256 KiB stalled-credit plateau, 141.2/151.5 ms during 95 completed filesystem/Git bulk reads, and a 16.0-second reconnect. With 1,024 retained provenance candidates, 4,096 pathless chunks dropped from 78.84ms and 4,096 array replacements to 1.74ms and zero replacements. Brace-free 1 KiB output dropped from 20.32/79.12/318.23ms to 0.31/0.64/2.35ms across 4,096/16,384/65,536 chunks. Repeated ordinary 252,000-character tail checks dropped from 96.34/464.20/941.79/3,796.70ms to 3.21/15.68/31.34/129.72ms across 100/500/1,000/4,096 updates." }, "promotionCriteria": [ "Split daemon stream backpressure into a deterministic provider/IPC contract if full E2E is flaky.", diff --git a/docs/plans/2026-07-27-ssh-relay-pty-backpressure.md b/docs/plans/2026-07-27-ssh-relay-pty-backpressure.md index 981928c8698..04bc8f9a9d0 100644 --- a/docs/plans/2026-07-27-ssh-relay-pty-backpressure.md +++ b/docs/plans/2026-07-27-ssh-relay-pty-backpressure.md @@ -2,9 +2,9 @@ Date: 2026-07-27 -Status: architecture gate closed and exact SSH V1 wire/API implemented behind -an experimental per-target SSH setting; integrated lifecycle fixes reconciled -at current HEAD; live topology gaps remain explicit below +Status: architecture gate closed and exact SSH V1 wire/API implemented +always-on for new SSH sessions; integrated lifecycle fixes reconciled at +the current working tree; live topology gaps remain explicit below Historical unbounded baseline: `badf91101babf96fa09cb79a8294f7e23b9f081c` (the implementation branch parent). The implementation was rebased onto @@ -21,7 +21,7 @@ parser. It covers: - relay stdout and socket drain handling; - per-client isolation, fairness, ordering, and cleanup; - bounded frame decoding on both sides of the relay protocol; -- memory budgets, diagnostics, compatibility, tests, and rollout. +- memory budgets, diagnostics, compatibility, tests, and deployment. The sidebar reconnect fix in `9d3ae3adc7` is out of scope. The non-ancestor commit `1500a92904` is design input only; it must not be cherry-picked. This @@ -72,17 +72,17 @@ implemented. In this PR: The implementation is intentionally scoped: -| Surface | State in this PR | Evidence | -| ------------------------------------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | -| Same-build direct SSH/deployed relay, V1 on | implemented | current-head deterministic contracts plus provenance-bound macOS-hosted Docker OpenSSH | -| Same-build SSH, V1 off | implemented bounded legacy mode | deterministic rollout and writer contracts | -| Reconnect/owner recovery | implemented | exact contiguous recovery, fail-closed cancellation, eight-wide reattach tests, Docker reconnect | -| Headed/headless remote consumers | source-range rotation/replacement implemented | deterministic runtime/main seam only; no live paired-runtime claim | -| WSL stdio and Windows named pipe/ConPTY | shared transport paths preserved | deterministic/common-contract evidence only; no physical run | -| Local provider and local daemon | unchanged | outside SSH V1 negotiation; existing behavior only | -| Folder workspace | no `.git` dependency added | code-path review only; no dedicated live fixture | -| Prior-version/mixed-version peers | fail-closed or version-scoped legacy behavior preserved | deterministic rollout/deploy contracts; no live old binary | -| Ubuntu 20.04/glibc 2.31 | no native dependency added | cross-target relay build only; no physical packaging run | +| Surface | State in this PR | Evidence | +| ------------------------------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| Same-build direct SSH/deployed relay, V1 | implemented; every session offers and relay advertises V1 | current-working-tree deterministic contracts plus provenance-bound macOS-hosted Docker OpenSSH | +| Capability/method-not-found legacy fallback | implemented bounded legacy compatibility | deterministic negotiation and writer contracts | +| Reconnect/owner recovery | implemented | exact contiguous recovery, fail-closed cancellation, eight-wide reattach tests, Docker reconnect | +| Headed/headless remote consumers | source-range rotation/replacement implemented | deterministic runtime/main seam only; no live paired-runtime claim | +| WSL stdio and Windows named pipe/ConPTY | shared transport paths preserved | deterministic/common-contract evidence only; no physical run | +| Local provider and local daemon | unchanged | outside SSH V1 negotiation; existing behavior only | +| Folder workspace | no `.git` dependency added | code-path review only; no dedicated live fixture | +| Prior-version/mixed-version peers | fail-closed or version-scoped legacy behavior preserved | deterministic negotiation/deploy contracts; no live old binary | +| Ubuntu 20.04/glibc 2.31 | no native dependency added | cross-target relay build only; no physical packaging run | Docker SSH proves the Linux SSH provider and deployed-relay topology. It does not prove headed or headless paired-runtime behavior, WSL, Windows ConPTY or @@ -332,7 +332,7 @@ Decision: `PtyConsumerSession` is a shared semantic state machine, not a universal transport protocol. The architecture gate is closed and is no longer waiting for exact wire/API names: the SSH adapter implements `sourceActivation`, source-bearing `pty.data`, `pty.recoveryComplete`, -cumulative `pty.ackData`, and token-scoped cancellation. Current-head +cumulative `pty.ackData`, and token-scoped cancellation. Current-working-tree deterministic validation is an engineering gate; unexecuted live topologies remain separate promotion gates. A relay-only `pty.getCapabilities` followed by `pty.negotiateClient` remains rejected @@ -349,9 +349,9 @@ findings as follows: | Desktop projection admission lacked immutable range identity and rollback | Model, source span, projection range, and scanner snapshot reserve atomically; pre-commit failure rolls back, while post-commit replacement transfers with proof. | Later adversarial review also required exact recovery continuity, stale-attempt -isolation, session-latched rollout semantics, and idempotent remote detach +isolation, session-grant fallback semantics, and idempotent remote detach after token reclamation. Those are incorporated in the activation, lifecycle, -cleanup, tests, and rollout sections below. +cleanup, tests, and compatibility sections below. The final 2026-07-28 architecture/terminal re-review found and closed three additional interleavings: @@ -362,7 +362,7 @@ additional interleavings: | Recovery-response cancellation authority ended at enqueue rather than writer settlement | The request remains cancellation-authoritative until its response settles; retry reuses the same provisional replacement identity instead of creating an unfenced owner. | | Snapshot replacement transferred every pending remote span at one sequence fence | Each immutable span retains its model-sequence end; replacement transfers only spans covered by the authoritative `SnapshotEnd`, while trailing spans remain live and replay in order. | -Current-head adversarial reconciliation closed these additional integrated +Current-working-tree adversarial reconciliation closed these additional integrated failures without widening the approved topology: | Finding | Implemented resolution | @@ -482,13 +482,21 @@ prior-version isolation remain authoritative. Reusing the semantic state machine there is a later design decision and is not required to ship the SSH bound. -The optional V1 offer is present only when the SSH session's captured rollout -selection is on. The server intersects the offer with its supported versions -and clamps `windowSu`; omission selects bounded legacy delivery while still -establishing the role and generation. When V1 was offered, main accepts only a -returned version it offered and a finite safe integer satisfying -`0 < windowSu <= requestedWindowSu`. An absent or invalid V1 grant closes that -connection attempt rather than silently downgrading it. +Every new SSH session offers V1, and every relay from the same build advertises +V1. The server intersects the offer with its supported versions and clamps +`windowSu`; an older client that omits the capability receives bounded legacy +delivery while still establishing the role and generation. Main accepts only +a returned version it offered and a finite safe integer satisfying +`0 < windowSu <= requestedWindowSu`. An absent or invalid V1 grant after +`pty.openClient` succeeds closes that connection attempt rather than silently +downgrading it. + +For compatibility with a legacy relay that does not implement +`pty.openClient`, main permits token-free bounded legacy delivery only for the +narrow same-build-validated JSON-RPC method-not-found result. Other errors, +missing grants after a successful method call, and unproved version skew fail +closed. This fallback does not change the normal same-build contract: the +session offers V1 and the relay advertises and grants it. “Bounded legacy” is transport backpressure, not a hidden source-credit mode. It creates no delivery token, source window, or cumulative ACK obligation, and @@ -507,7 +515,7 @@ and projection work. Transport close cancels the connection's retained publications and reconnect uses the existing replay/restore behavior. It may pause all legacy subscribers behind one slow connection because legacy mode has no authenticated source-owner role, but it neither drops output nor grows -without bound. These mechanics remain enabled when the target's V1 option is off. +without bound. These mechanics remain available to negotiated legacy peers. The session hello creates no PTY token. A later V1 spawn/attach returns a fresh `deliveryToken` and creates a subscription only for the authenticated @@ -515,10 +523,9 @@ transport client and installed generation. `pty.attach` must receive request context just as `pty.spawn` does. Spawn failure, identity mismatch, stale context, and response cancellation create no active V1 subscription. A legacy spawn/attach returns the current identity/replay shape and creates only the -bounded transport subscription above—no token or source coordinate. The -session-latched mode never rotates a live V1 token into legacy service; normal -disconnect closes the old token before a new gate-off session opens a bounded -legacy connection. +bounded transport subscription above—no token or source coordinate. A live V1 +token never rotates into legacy service; disconnect closes it before any +separately negotiated legacy fallback connection can open. Session ownership is granted by the authenticated session hello, not constructor-assigned: @@ -560,31 +567,26 @@ launches detach on POSIX and Windows, invalidate stdout, and connect the desktop bridge through `attachClient` over the versioned Unix socket or named pipe. Unproved direct stdio remains subscriber-only. -Capability support and rollout enablement are separate. Each `SshRelaySession` -captures its target's `experimentalPtySourceCreditV1` selection at construction; -`ORCA_SSH_PTY_SOURCE_CREDIT_V1=0|1` remains an explicit process-startup-latched -CI/development override. Fresh POSIX and Windows gate-on launches receive the -safely quoted `--pty-source-credit-v1` flag; gate-off omits it. The endpoint's -persisted `.pty-source-credit-policy` records `v1` or `off` and fences -incompatible reuse. A new main may still decline V1 from an already-running -capable relay. Editing a target never changes a live session or its automatic -reconnects; a later explicit connection captures the new selection. Sink drain, -writer ordering, decoder bounds, and header-ACK hardening are correctness fixes -and are not disabled by this gate. +Capability support is negotiated on every SSH session. `SshRelaySession` +always offers V1 during initial connection and automatic reconnect, and POSIX +and Windows relays always advertise it as an unconditional capability. Sink +drain, writer ordering, decoder bounds, and header-ACK hardening remain +unconditional correctness fixes. Production deployment does not form arbitrary mixed-build main/relay pairs. `computeRemoteRelayDir` content-hash-scopes the install directory and its socket/named-pipe endpoint, and the `.version` handshake is a second fence, so -the desktop bridge and daemon reached at that endpoint share a build. Reachable -same-build modes are session-granted V1 and target-gated legacy. A missing -mandatory session grant fails readiness. Protocol tolerance for unknown fields -remains for direct/manual relay launches, but an absent session contract is -diagnostic `unsupported-version-skew`, not a rollout cohort. +the desktop bridge and daemon reached at that endpoint share a build. The +normal same-build mode is session-granted V1. A missing mandatory session grant +fails readiness except for the narrow method-not-found compatibility fallback +above. Protocol tolerance for unknown fields remains for direct/manual relay +launches, but an absent session contract outside that fallback is diagnostic +`unsupported-version-skew`, not a deployment cohort. The reachable upgrade skew is an orphaned prior-version daemon in its old version directory with live PTYs while the new main connects to a new endpoint. It retains its old behavior until its own grace/cleanup completes and is not -reachable by a new session's rollout selection. Upgrade diagnostics enumerate +reachable by a new same-build session. Upgrade diagnostics enumerate these versioned orphan processes; V1 neither adopts their PTYs nor claims to bound their memory. @@ -730,7 +732,7 @@ notification before the token record is forgotten: ``` It covers supersession, activation/exit timeout, reconnect-grace expiry, and -policy cancellation. `remainingStartSu == creditedEndSu` and +explicit delivery cancellation. `remainingStartSu == creditedEndSu` and `remainingEndSu == sentEndSu` state the exact source interval still unsettled at the relay. If the sink cannot drain the proof, its generation close is the proof. Without a replacement, main cancels the matching remaining obligations @@ -1071,7 +1073,7 @@ the `source-owner` and reserve each remaining client separately. If one additional subscriber cannot reserve bounded writer/publication capacity, close only that client and continue the healthy subscriber and source-owner sends; never pause or tear down the native PTY solely for that projection. A -gate-off primary is different: it remains a required bounded-legacy +legacy-fallback primary is different: it remains a required bounded-legacy backpressure participant and is not evicted for saturation. Constructor position still grants no session-owner role. @@ -1371,9 +1373,9 @@ ackPublishedEndSu <= ackQueuedEndSu <= obligationsTerminalEndSu Only `SshPtyOutputDelivery` may call the V1 cumulative ACK coalescer. The generic `IPtyProvider.acknowledgeDataEvent(id, delta)` API remains for local -and daemon behavior. Legacy SSH may still emit its current delta during -migration, but the relay ignores it and no legacy bound depends on it; it is a -hard no-op for negotiated V1 PTYs. At every shared `pty:ackData`, resync, heal, +and daemon behavior. Legacy-fallback SSH may still emit its current delta, but +the relay ignores it and no legacy bound depends on it; it is a hard no-op for +negotiated V1 PTYs. At every shared `pty:ackData`, resync, heal, write-off, drop, salvage, and reload call site, V1 routes renderer display progress through `SshPtyLegacyProjectionLedger`: parsing settles exact mapped source ranges and @@ -2267,9 +2269,10 @@ logging terminal contents or raw PTY IDs: - closed provider-generation range count and active gaps; - reconnect attach wave latency, per-PTY retry/failure, token supersession, relay-initiated cancellation proof, and time-to-last-success; -- same-build negotiated and disabled-by-main sessions, unsupported manual - version skew, and discovered orphan prior-version daemons, keyed by relay - build/version rather than legacy ACK traffic. +- same-build V1 grants, capability omissions from legacy clients, + method-not-found fallbacks, unsupported manual version skew, and discovered + orphan prior-version daemons, keyed by relay build/version rather than + legacy ACK traffic. Rate-limit warnings by connection and reason. Log thresholds and state transitions, never data. Add an E2E-only snapshot request so tests can assert @@ -2279,9 +2282,9 @@ without parsing logs. ## Incremental implementation map This PR implemented the design as the independently testable layers below, -without replacing local/daemon protocols or requiring every deployment -topology to enable V1. The slices remain useful review and rollback -boundaries even though they ship in one PR. +without replacing local/daemon protocols or claiming live evidence for every +deployment topology. The slices remain useful review boundaries even though +they ship in one PR. ### Slice 1: hostile-header and accounting hardening — implemented @@ -2377,7 +2380,8 @@ boundaries even though they ship in one PR. - Reuse Slice 4's projection identities and transactional admission. Separate obligations terminal, ACK queued, and ACK published state from the first implementation. -- Gate the feature and retain exact same-build legacy behavior. +- Offer V1 on every new session while retaining bounded legacy compatibility + for capability-omitting clients and method-not-found relays. ### Slice 5c: exit, reconnect, and replay — implemented @@ -2396,8 +2400,8 @@ boundaries even though they ship in one PR. - `SshPtyModelAdmission` retains generation-fatal callback handling except when the exact key is actively migrating; `SshPtyOutputIntake` uses the same key to keep provider close from escaping that migration owner. -- Reconnect V1 remains under the session-latched target gate; its late-ACK, timeout, - supersession, and generation-close deterministic oracles pass. +- Reconnect always re-offers V1; its late-ACK, timeout, supersession, and + generation-close deterministic oracles pass. ### Slice 5d: required remote consumers — deterministic seam implemented @@ -2476,8 +2480,9 @@ recorded evidence include: capacity-aware Git/PTY slicing, decoder caps, and token rotation; - main model admission, desktop identity/rollback, exit barriers, remote reserve/commit/rollback, token cancellation, exact recovery continuity, - eight-wide reconnect, retention budgets, rollout latching, and deploy policy; -- current-head restore-response retirement, fail-closed recovery cancellation, + eight-wide reconnect, retention budgets, unconditional negotiation, and + deployment; +- current-working-tree restore-response retirement, fail-closed recovery cancellation, partial remote ACK, higher-generation token rotation, reclaimed-span idempotence, three-lane mux fairness, provisional claim gating, and generation-fenced exit proof ordering plus bounded exact generation closure; @@ -2593,10 +2598,12 @@ and are then cleaned up idempotently. unimplemented, direct-stdio/WSL has deterministic common-transport evidence only, and remote-runtime source mapping is implemented at a deterministic seam but unexecuted in a live paired topology; -- verify SSH session-owner election with V1 requested and with session-granted - same-build legacy, invalid/stale credential or lease rejection, 30-second - lease expiry, and atomic reconnect generation transfer through POSIX sockets - and Windows named pipes; prove constructor stdout, WSL child stdio, and an +- verify every new SSH session and reconnect offers V1 and every same-build + relay advertises and grants it; separately verify a capability-omitting old + client remains token-free and the narrow method-not-found relay fallback is + bounded; cover invalid/stale credential or lease rejection, 30-second lease + expiry, and atomic reconnect generation transfer through POSIX sockets and + Windows named pipes; prove constructor stdout, WSL child stdio, and an unproved plain socket are never elected, and cover POSIX `0600` plus Windows current-user ACLs; - verify split shell/SFTP home discovery, per-launch marker creation under the @@ -2751,9 +2758,10 @@ and are then cleaned up idempotently. and input fairness; - fuzz frame boundaries, JSON sizes, reconnect timing, transport data during a decoder continuation, and scalar-safe Unicode splits; -- exercise same-build negotiated and main-disabled modes, reject a manually - mismatched `.version`, and identify—but do not adopt or kill—an orphaned - prior-version daemon; +- exercise unconditional same-build V1 negotiation, old-client capability + omission, and method-not-found fallback; reject a manually mismatched + `.version`, and identify—but do not adopt or kill—an orphaned prior-version + daemon; - verify the session grant rejects unrequested versions and zero, negative, excessive, non-finite, or unsafe windows. @@ -2764,8 +2772,8 @@ one last chunk to prove the transient overshoot bound. ### Integration and E2E `tests/e2e/ssh-docker-relay-perf.spec.ts` now contains V1 evidence for the -implemented Linux SSH/deployed-relay path. The test harness uses the -`ORCA_SSH_PTY_SOURCE_CREDIT_V1=1` all-target override and: +implemented Linux SSH/deployed-relay path. The test harness exercises the +normal deployment and connection path with no override and: 1. stalls desktop ACK and observes an exact 256 Ki-source-unit negotiated plateau while a second SSH PTY remains responsive; @@ -2816,70 +2824,47 @@ WSL, Windows, folder-workspace, prior-version, mixed-version, or Ubuntu 20.04 packaging topologies as executed. The WSL stdin write/callback/drain contract is deterministic adapter evidence only, not a physical WSL run. -## Rollout +## Compatibility and release validation -V1 is behind `Bounded terminal output (experimental)` in each SSH target's -Advanced settings and a persisted per-endpoint relay launch policy. Missing or -false target state selects legacy mode. `ORCA_SSH_PTY_SOURCE_CREDIT_V1=0|1` -is latched when the process loads the rollout module and explicitly overrides -every target for CI, development, and emergency rollback. Main never negotiates -merely because a long-lived relay advertises; the immutable selection captured -by a new `SshRelaySession` is final for that session and all of its automatic -reconnects. Fresh POSIX and Windows gate-on launch commands carry -`--pty-source-credit-v1`; gate-off omits it, and the persisted endpoint policy -records the selected mode. +Every new SSH session and automatic reconnect offers V1. Every relay from the +same build advertises and grants V1, so the normal deployment path always uses +source credit. Sink drain, the reserved writer lane, stdout serialization, +bounded decoder work, bulk admission, and hostile header-ACK hardening are +also unconditional. -Sink drain, the reserved writer lane, stdout serialization, bounded decoder -work, bulk admission, and hostile header-ACK hardening remain enabled -independently. Same-build gate-off sessions use the bounded legacy writer but -do not wait for V1 credit. A prior-version orphan remains governed by its own -binary and cleanup. +Compatibility behavior is explicit: -Implementation and rollout stages: +1. An old client that omits the V1 capability remains token-free and uses the + bounded legacy writer. +2. A new main may use bounded legacy delivery only when the same-build + compatibility check accepts JSON-RPC method-not-found for + `pty.openClient`. +3. A successful `pty.openClient` response without the offered V1 grant, an + invalid grant, or another request error fails closed rather than silently + downgrading. +4. Disconnect performs normal token-scoped cancellation and exit cleanup; it + never rotates a live V1 token into legacy mode in place. +5. A prior-version orphan remains governed by its own version-scoped binary and + cleanup. The new main reports but does not adopt, kill, or claim to bound it. + +Implementation and validation status: 1. Slices 1-4 are implemented under legacy-compatible framing with narrow reliability oracles. -2. Slices 5a-5c are implemented behind the per-target gate and relay launch - policy with unit/service contracts and prior direct Docker SSH evidence. +2. Slices 5a-5c implement unconditional V1 offer/advertisement, source credit, + exit, and reconnect with unit/service contracts and direct Docker SSH + evidence. 3. Slices 5d-5e implement remote source-range transactions and the final lifecycle reconciliation at deterministic main/runtime/provider/relay seams. Live headed paired, headless, WSL, Windows, local-daemon, local-provider, and folder-workspace evidence remains future work before those topologies are marked covered. -4. Current-head deterministic reconciliation and focused integrated-fix checks +4. Current-working-tree deterministic reconciliation and focused integrated-fix checks are complete. The isolated reconnect and final four-case direct-SSH Docker runs, full repository typecheck, and full lint/reliability suite are green; final exact-implementation-head architecture and transport reviews found no code blocker, and the transport review's stale-provenance finding is closed by the exact artifact record above. -5. Run an internal same-build per-target canary comparing gate-off and V1 sink, - model-admission, latency, sealed-exit, recovery, and reconnect metrics while - separately counting orphan prior-version daemons; -6. Default on only after zero ordering/data-loss failures and stable memory - plateaus for one release cycle; remove the per-target opt-in only after - same-build legacy fallback and versioned-orphan cleanup stay healthy through - the supported upgrade window. - -Rollback behavior is explicit: - -1. Change the target's Advanced setting, then explicitly disconnect and - reconnect that target. Existing sessions and automatic reconnects keep - their captured mode. -2. Disconnect performs the normal token-scoped cancellation/exit cleanup; it - does not rotate live V1 tokens into legacy mode in place. -3. On the next connection, gate-off omits the V1 offer and accepts same-build - bounded legacy service. Gate-on requires a V1-launched endpoint. -4. If a persisted detached endpoint was launched gate-off, an off-to-on - connection fails with reset/reconnect guidance rather than silently - downgrading. Resetting that endpoint and reconnecting launches it with the - new policy. -5. A gate-off client remains compatible with a same-build V1-capable relay - because omission of the offer selects bounded legacy behavior. - -The remote process may remain alive only when its persisted launch policy is -compatible with the new session selection. An orphaned prior-version daemon is -outside this sequence and is only observed until its own version-scoped -cleanup. Release criteria: @@ -2892,8 +2877,9 @@ Release criteria: decoder tasks, or paused PTYs; - no false dead-link reconnect across a 30-minute saturated/self-paused run; - bounded time-to-last-reattach with one injected per-PTY failure; -- same-build gate modes, manual mismatch rejection, orphan reporting, and - Linux/macOS/Windows/WSL smoke tests pass. +- unconditional same-build V1 negotiation, bounded legacy compatibility, + manual mismatch rejection, orphan reporting, and Linux/macOS/Windows/WSL + smoke tests pass. ## Calibrated findings and non-goals diff --git a/src/main/ipc/ssh.ts b/src/main/ipc/ssh.ts index f0c36aa5314..7e1a54026df 100644 --- a/src/main/ipc/ssh.ts +++ b/src/main/ipc/ssh.ts @@ -7,7 +7,6 @@ import type { SshConnectionCallbacks } from '../ssh/ssh-connection' import { SshConnectionManager } from '../ssh/ssh-connection-manager' import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer' import { SshRelaySession, type SshRelayAiVaultHostInfo } from '../ssh/ssh-relay-session' -import { resolveSshPtySourceCreditV1Selection } from '../ssh/ssh-pty-source-credit-rollout' import { SshPortForwardManager } from '../ssh/ssh-port-forward' import type { DetectedPort, @@ -1022,8 +1021,7 @@ export function registerSshHandlers( persistedStore!, portForwardManager!, currentRuntime, - broadcastDetectedPortsFromCurrentWindow, - () => resolveSshPtySourceCreditV1Selection(target) + broadcastDetectedPortsFromCurrentWindow ) configureRelaySessionCallbacks(session) activeSessions.set(targetId, session) diff --git a/src/main/persistence.test.ts b/src/main/persistence.test.ts index 242edace0f2..d4904900610 100644 --- a/src/main/persistence.test.ts +++ b/src/main/persistence.test.ts @@ -1548,7 +1548,7 @@ describe('Store', () => { expect(updatedTarget).not.toHaveProperty('systemSshConnectionReuse') }) - it('persists only explicit SSH terminal source-credit opt-ins', async () => { + it('drops retired per-target SSH terminal source-credit selections', async () => { const store = await createStore() store.addSshTarget({ id: 'ssh-source-credit-on', @@ -1557,32 +1557,15 @@ describe('Store', () => { port: 22, username: 'dev', experimentalPtySourceCreditV1: true - }) - store.addSshTarget({ - id: 'ssh-source-credit-off', - label: 'Legacy host', - host: 'legacy.example.com', - port: 22, - username: 'dev', - experimentalPtySourceCreditV1: false - }) + } as never) - expect(store.getSshTarget('ssh-source-credit-on')?.experimentalPtySourceCreditV1).toBe(true) - expect(store.getSshTarget('ssh-source-credit-off')).not.toHaveProperty( + expect(store.getSshTarget('ssh-source-credit-on')).not.toHaveProperty( 'experimentalPtySourceCreditV1' ) - store.flush() const persisted = readDataFile() as { sshTargets?: Record[] } - const enabled = persisted.sshTargets?.find((target) => target.id === 'ssh-source-credit-on') - const disabled = persisted.sshTargets?.find((target) => target.id === 'ssh-source-credit-off') - expect(enabled?.experimentalPtySourceCreditV1).toBe(true) - expect(disabled).not.toHaveProperty('experimentalPtySourceCreditV1') - - const updated = store.updateSshTarget('ssh-source-credit-on', { - experimentalPtySourceCreditV1: undefined - }) - expect(updated).not.toHaveProperty('experimentalPtySourceCreditV1') + const target = persisted.sshTargets?.find((entry) => entry.id === 'ssh-source-credit-on') + expect(target).not.toHaveProperty('experimentalPtySourceCreditV1') }) it('upserts ~/.ssh/config through the real store: rotated port updates in place and persists', async () => { @@ -1600,15 +1583,12 @@ describe('Store', () => { expect(inserted).toHaveLength(1) expect(inserted[0]?.source).toBe('ssh-config') expect(inserted[0]?.port).toBe(2200) - store.updateSshTarget('ssh-cfg-1', { experimentalPtySourceCreditV1: true }) - // Rotated port: upsert updates the same target in place and normalizeSshTarget must keep `source` (no false re-derive into a permanently-dirty state). sshConfigHostsToTargetsMock.mockReturnValue(candidate(2222, 'ssh-cfg-2')) const changed = sshStore.importFromSshConfig() expect(changed).toHaveLength(1) expect(changed[0]?.port).toBe(2222) expect(changed[0]?.source).toBe('ssh-config') - expect(changed[0]?.experimentalPtySourceCreditV1).toBe(true) // A third identical sync is a no-op — repeated auto-sync on every pane open writes nothing. expect(sshStore.importFromSshConfig()).toHaveLength(0) @@ -1620,14 +1600,12 @@ describe('Store', () => { expect(clusterTargets).toHaveLength(1) expect(clusterTargets[0]?.port).toBe(2222) expect(clusterTargets[0]?.source).toBe('ssh-config') - expect(clusterTargets[0]?.experimentalPtySourceCreditV1).toBe(true) // Survives a fresh load from the same data file. const reloaded = await createStore() const reloadedCluster = reloaded.getSshTargets().find((t) => t.configHost === 'cluster') expect(reloadedCluster?.port).toBe(2222) expect(reloadedCluster?.source).toBe('ssh-config') - expect(reloadedCluster?.experimentalPtySourceCreditV1).toBe(true) }) it('drops malformed migration-unsupported PTY entries on load', async () => { diff --git a/src/main/persistence.ts b/src/main/persistence.ts index 8256337e2ec..63430a1a9c8 100644 --- a/src/main/persistence.ts +++ b/src/main/persistence.ts @@ -1113,6 +1113,7 @@ function backfillLegacyAutomationContexts( type LegacySshTarget = SshTarget & { remoteWorkspaceSyncEnabled?: unknown remoteWorkspaceSyncGracePeriodSeconds?: unknown + experimentalPtySourceCreditV1?: unknown } // Why: old targets predate configHost; default to label-based lookup so imported SSH aliases still resolve via ssh -G. @@ -1122,7 +1123,6 @@ function normalizeSshTarget(t: SshTarget): SshTarget { const currentGracePeriodSeconds = target.relayGracePeriodSeconds const legacyGracePeriodSeconds = target.remoteWorkspaceSyncGracePeriodSeconds const systemSshConnectionReuse = target.systemSshConnectionReuse - const experimentalPtySourceCreditV1 = target.experimentalPtySourceCreditV1 // Why: remote sync now follows the SSH relay lifecycle, so retired per-target sync/grace fields are dropped at disk load. delete target.remoteWorkspaceSyncEnabled delete target.remoteWorkspaceSyncGracePeriodSeconds @@ -1148,9 +1148,6 @@ function normalizeSshTarget(t: SshTarget): SshTarget { if (systemSshConnectionReuse === false) { normalized.systemSshConnectionReuse = false } - if (experimentalPtySourceCreditV1 === true) { - normalized.experimentalPtySourceCreditV1 = true - } return normalized } @@ -6261,9 +6258,6 @@ export class Store { if (!Object.hasOwn(normalized, 'systemSshConnectionReuse')) { delete target.systemSshConnectionReuse } - if (!Object.hasOwn(normalized, 'experimentalPtySourceCreditV1')) { - delete target.experimentalPtySourceCreditV1 - } this.scheduleSave() return { ...target } } diff --git a/src/main/ssh/ssh-pty-source-credit-rollout.test.ts b/src/main/ssh/ssh-pty-source-credit-rollout.test.ts deleted file mode 100644 index a265d7f5cc9..00000000000 --- a/src/main/ssh/ssh-pty-source-credit-rollout.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { - assertSshPtySourceCreditRelayLaunchPolicy, - resolveSshPtySourceCreditV1Selection, - SshPtySourceCreditRestartRequiredError, - SSH_PTY_SOURCE_CREDIT_V1_ENV -} from './ssh-pty-source-credit-rollout' - -describe('SSH PTY source-credit rollout', () => { - it('defaults off and permits a per-target opt-in', () => { - expect(resolveSshPtySourceCreditV1Selection({}, {})).toBe(false) - expect(resolveSshPtySourceCreditV1Selection({ experimentalPtySourceCreditV1: false }, {})).toBe( - false - ) - expect(resolveSshPtySourceCreditV1Selection({ experimentalPtySourceCreditV1: true }, {})).toBe( - true - ) - }) - - it('lets an explicit environment value override the target selection', () => { - expect( - resolveSshPtySourceCreditV1Selection( - { experimentalPtySourceCreditV1: true }, - { [SSH_PTY_SOURCE_CREDIT_V1_ENV]: '0' } - ) - ).toBe(false) - expect( - resolveSshPtySourceCreditV1Selection( - { experimentalPtySourceCreditV1: true }, - { [SSH_PTY_SOURCE_CREDIT_V1_ENV]: 'true' } - ) - ).toBe(false) - expect( - resolveSshPtySourceCreditV1Selection( - { experimentalPtySourceCreditV1: false }, - { [SSH_PTY_SOURCE_CREDIT_V1_ENV]: '1' } - ) - ).toBe(true) - }) - - it('latches the default environment override when the module initializes', async () => { - const previous = process.env[SSH_PTY_SOURCE_CREDIT_V1_ENV] - try { - process.env[SSH_PTY_SOURCE_CREDIT_V1_ENV] = '1' - vi.resetModules() - const rollout = await import('./ssh-pty-source-credit-rollout') - process.env[SSH_PTY_SOURCE_CREDIT_V1_ENV] = '0' - - expect(rollout.resolveSshPtySourceCreditV1Selection({})).toBe(true) - } finally { - if (previous === undefined) { - delete process.env[SSH_PTY_SOURCE_CREDIT_V1_ENV] - } else { - process.env[SSH_PTY_SOURCE_CREDIT_V1_ENV] = previous - } - vi.resetModules() - } - }) - - it('surfaces a deliberate restart action for a live off-to-on transition', () => { - expect(new SshPtySourceCreditRestartRequiredError()).toMatchObject({ - name: 'SshPtySourceCreditRestartRequiredError', - code: 'ssh_pty_source_credit_restart_required', - message: expect.stringContaining('Reset the relay') - }) - }) - - it('permits stable gate-off legacy and stable gate-on V1 startup selections', () => { - expect(() => assertSshPtySourceCreditRelayLaunchPolicy(false, 'off')).not.toThrow() - expect(() => assertSshPtySourceCreditRelayLaunchPolicy(false, 'v1')).not.toThrow() - expect(() => assertSshPtySourceCreditRelayLaunchPolicy(true, 'v1')).not.toThrow() - }) - - it.each(['off', '', 'unknown'])('fails closed when gate-on reaches policy %j', (policy) => { - expect(() => assertSshPtySourceCreditRelayLaunchPolicy(true, policy)).toThrow( - SshPtySourceCreditRestartRequiredError - ) - }) -}) diff --git a/src/main/ssh/ssh-pty-source-credit-rollout.ts b/src/main/ssh/ssh-pty-source-credit-rollout.ts deleted file mode 100644 index e99feef87bf..00000000000 --- a/src/main/ssh/ssh-pty-source-credit-rollout.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { SshTarget } from '../../shared/ssh-types' - -export const SSH_PTY_SOURCE_CREDIT_V1_ENV = 'ORCA_SSH_PTY_SOURCE_CREDIT_V1' - -function readSshPtySourceCreditV1Override(env: NodeJS.ProcessEnv): boolean | undefined { - const value = env[SSH_PTY_SOURCE_CREDIT_V1_ENV] - return value === undefined ? undefined : value === '1' -} - -const startupSshPtySourceCreditV1Override = readSshPtySourceCreditV1Override(process.env) - -export function resolveSshPtySourceCreditV1Selection( - target: Pick, - env?: NodeJS.ProcessEnv -): boolean { - const override = env ? readSshPtySourceCreditV1Override(env) : startupSshPtySourceCreditV1Override - return override ?? target.experimentalPtySourceCreditV1 === true -} - -export class SshPtySourceCreditRestartRequiredError extends Error { - readonly code = 'ssh_pty_source_credit_restart_required' - - constructor() { - super( - 'SSH PTY source credit was enabled after this detached relay started. Reset the relay, then reconnect to launch it with source-credit support.' - ) - this.name = 'SshPtySourceCreditRestartRequiredError' - } -} - -export function assertSshPtySourceCreditRelayLaunchPolicy( - enabled: boolean, - relayPolicy: string -): void { - if (enabled && relayPolicy !== 'v1') { - throw new SshPtySourceCreditRestartRequiredError() - } -} diff --git a/src/main/ssh/ssh-relay-deploy.test.ts b/src/main/ssh/ssh-relay-deploy.test.ts index 811b92863bd..76826a4c670 100644 --- a/src/main/ssh/ssh-relay-deploy.test.ts +++ b/src/main/ssh/ssh-relay-deploy.test.ts @@ -473,7 +473,7 @@ describe('deployAndLaunchRelay', () => { expect(resolveRemoteNodePath).toHaveBeenCalledTimes(2) }) - it('defaults fresh relays to keep-alive-until-reset', async () => { + it('defaults fresh relays to keep-alive-until-reset without rollout artifacts', async () => { const conn = makeMockConnection() const mockExecCommand = vi.mocked(execCommand) mockExecCommand.mockResolvedValueOnce('__ORCA_REMOTE_PLATFORM__ Linux x86_64') @@ -491,7 +491,7 @@ describe('deployAndLaunchRelay', () => { expect(launchCommand).toContain(`--grace-time ${DEFAULT_SSH_RELAY_GRACE_PERIOD_SECONDS}`) expect(launchCommand).not.toContain('--pty-source-credit-v1') - expect(launchCommand).toContain("printf '%s\\n' 'off'") + expect(launchCommand).not.toContain('.pty-source-credit-policy') }) it('allows an unlimited SSH disconnect grace window', async () => { @@ -503,7 +503,7 @@ describe('deployAndLaunchRelay', () => { queueLaunchNamespaceAndDeadSocketProbe() mockExecCommand.mockResolvedValueOnce('READY') - await deployAndLaunchRelay(conn, undefined, 0, 'target-a', true) + await deployAndLaunchRelay(conn, undefined, 0, 'target-a') const launchCommand = vi .mocked(conn.exec) @@ -511,8 +511,8 @@ describe('deployAndLaunchRelay', () => { .find((cmd) => cmd.includes('--detached')) expect(launchCommand).toContain('--grace-time 0') - expect(launchCommand).toContain('--pty-source-credit-v1') - expect(launchCommand).toContain("printf '%s\\n' 'v1'") + expect(launchCommand).not.toContain('--pty-source-credit-v1') + expect(launchCommand).not.toContain('.pty-source-credit-policy') }) it('clamps configured SSH disconnect grace to the seven-day maximum', async () => { @@ -762,7 +762,7 @@ describe('deployAndLaunchRelay', () => { .mockResolvedValueOnce('READY') // named pipe poll .mockResolvedValueOnce('') // persist active pipe marker - const result = await deployAndLaunchRelay(conn, undefined, 300, 'target-a', true) + const result = await deployAndLaunchRelay(conn, undefined, 300, 'target-a') expect(result.platform).toBe('win32-x64') expect(result.remoteHome).toBe('C:/Users/me user') @@ -781,9 +781,8 @@ describe('deployAndLaunchRelay', () => { '"C:/Users/me user/.orca-remote/relay-0.1.0+abcdef012345/agent-hooks/orca-relay-' ) expect(launchScript).toContain('--endpoint-dir') - expect(launchScript).toContain('--pty-source-credit-v1') - expect(launchScript).toContain('.pty-source-credit-policy') - expect(launchScript).toContain("-Value 'v1'") + expect(launchScript).not.toContain('--pty-source-credit-v1') + expect(launchScript).not.toContain('.pty-source-credit-policy') expect(launchScript).not.toContain('\\\\.\\pipe\\agent-hooks') const waitScript = decodedScripts.find((script) => script.includes('deadline=Date.now()')) ?? '' expect(waitScript).toContain('setTimeout(attempt,intervalMs)') diff --git a/src/main/ssh/ssh-relay-deploy.ts b/src/main/ssh/ssh-relay-deploy.ts index 853e97b8163..d57e4ad55a4 100644 --- a/src/main/ssh/ssh-relay-deploy.ts +++ b/src/main/ssh/ssh-relay-deploy.ts @@ -54,7 +54,6 @@ import { isWindowsRemoteHost, joinRemotePath, normalizeRemoteHome, - remoteBasename, validateRemoteHome, type RemoteHostPlatform } from './ssh-remote-platform' @@ -74,10 +73,6 @@ import { MAX_SSH_RELAY_GRACE_PERIOD_SECONDS, MIN_SSH_RELAY_GRACE_PERIOD_SECONDS } from '../../shared/ssh-types' -import { - assertSshPtySourceCreditRelayLaunchPolicy, - SshPtySourceCreditRestartRequiredError -} from './ssh-pty-source-credit-rollout' export type RelayDeployResult = { transport: MultiplexerTransport @@ -113,37 +108,6 @@ function execHostCommand( }) } -function sourceCreditPolicyPath( - hostPlatform: RemoteHostPlatform, - remoteDir: string, - endpoint: string -): string { - return joinRemotePath( - hostPlatform, - remoteDir, - `${remoteBasename(endpoint, hostPlatform)}.pty-source-credit-policy` - ) -} - -async function assertExistingRelaySourceCreditPolicy( - conn: SshConnection, - hostPlatform: RemoteHostPlatform, - policyPath: string, - enabled: boolean, - signal?: AbortSignal -): Promise { - if (!enabled) { - return - } - const command = isWindowsRemoteHost(hostPlatform) - ? powerShellCommand( - `if (Test-Path -LiteralPath ${powerShellLiteral(policyPath)} -PathType Leaf) { Get-Content -LiteralPath ${powerShellLiteral(policyPath)} -Raw -ErrorAction SilentlyContinue }` - ) - : `test -f ${shellEscape(policyPath)} && cat ${shellEscape(policyPath)} || true` - const policy = (await execHostCommand(conn, hostPlatform, command, { signal })).trim() - assertSshPtySourceCreditRelayLaunchPolicy(enabled, policy) -} - /** * Deploy the relay to the remote host and launch it, returning the transport (relay's stdin/stdout) for multiplexer use. */ @@ -151,8 +115,7 @@ export async function deployAndLaunchRelay( conn: SshConnection, onProgress?: (status: string) => void, graceTimeSeconds?: number, - relayInstanceId?: string, - enablePtySourceCreditV1 = false + relayInstanceId?: string ): Promise { let timeoutHandle: ReturnType // Why: Promise.race doesn't cancel its loser; abort so a contended install-lock waiter can't mutate the relay after this call times out. @@ -171,7 +134,6 @@ export async function deployAndLaunchRelay( onProgress, graceTimeSeconds, relayInstanceId, - enablePtySourceCreditV1, deployAbortController.signal ), timeoutPromise @@ -303,7 +265,6 @@ async function deployAndLaunchRelayInner( onProgress?: (status: string) => void, graceTimeSeconds?: number, relayInstanceId?: string, - enablePtySourceCreditV1 = false, deploySignal?: AbortSignal ): Promise { while (true) { @@ -314,7 +275,6 @@ async function deployAndLaunchRelayInner( onProgress, graceTimeSeconds, relayInstanceId, - enablePtySourceCreditV1, deploySignal ) } catch (err) { @@ -332,7 +292,6 @@ async function deployAndLaunchRelayAttempt( onProgress?: (status: string) => void, graceTimeSeconds?: number, relayInstanceId?: string, - enablePtySourceCreditV1 = false, deploySignal?: AbortSignal ): Promise { onProgress?.('Detecting remote platform...') @@ -457,7 +416,6 @@ async function deployAndLaunchRelayAttempt( nodePath, graceTimeSeconds, relayInstanceId, - enablePtySourceCreditV1, deploySignal ) launchLivenessObserved = true @@ -1230,7 +1188,6 @@ async function launchRelay( nodePath: string, graceTimeSeconds?: number, relayInstanceId?: string, - enablePtySourceCreditV1 = false, signal?: AbortSignal ): Promise<{ transport: MultiplexerTransport @@ -1254,7 +1211,6 @@ async function launchRelay( const sockFile = relayEndpointForHost(hostPlatform, remoteDir, sockName) const endpointDir = relayHookEndpointDirForHost(hostPlatform, remoteDir, sockFile) const credentialFile = joinRemotePath(hostPlatform, remoteDir, `${sockName}.credential`) - const sourceCreditPolicyFile = sourceCreditPolicyPath(hostPlatform, remoteDir, sockFile) if (isWindowsRemoteHost(hostPlatform)) { const activePipeMarkerPath = windowsActivePipeMarkerPath(hostPlatform, remoteDir, sockName) @@ -1267,8 +1223,7 @@ async function launchRelay( ) const activeEndpoint = discoveredActiveEndpoint ?? { sockPath: sockFile, - endpointDir, - sourceCreditPolicyFile + endpointDir } const fallbackEndpoint = buildWindowsRelayFallbackEndpoint(hostPlatform, remoteDir, sockName) const launched = await launchWindowsRelay( @@ -1279,12 +1234,10 @@ async function launchRelay( nodePath, sockPath: activeEndpoint.sockPath, endpointDir: activeEndpoint.endpointDir, - sourceCreditPolicyFile: activeEndpoint.sourceCreditPolicyFile, graceTime, activePipeMarkerPath, reconnectFallback: fallbackEndpoint, - credentialFile, - enablePtySourceCreditV1 + credentialFile }, signal ) @@ -1300,13 +1253,6 @@ async function launchRelay( ) console.warn(`[ssh-relay] Socket probe result: "${probeOutput.trim()}"`) if (probeOutput.trim() === 'ALIVE') { - await assertExistingRelaySourceCreditPolicy( - conn, - hostPlatform, - sourceCreditPolicyFile, - enablePtySourceCreditV1, - signal - ) console.log('[ssh-relay] Existing relay socket found, attempting reconnect...') try { const channel = await conn.exec( @@ -1334,9 +1280,6 @@ async function launchRelay( } } } catch (err) { - if (err instanceof SshPtySourceCreditRestartRequiredError) { - throw err - } if (isUnconfirmedSshCommandTermination(err)) { throw err } @@ -1351,9 +1294,7 @@ async function launchRelay( signal }) // Why: --log-file lets the relay rotate relay.log in-process; the shell redirect stays to capture pre-JS boot/crash output. - const sourceCreditFlag = enablePtySourceCreditV1 ? ' --pty-source-credit-v1' : '' - const sourceCreditPolicy = enablePtySourceCreditV1 ? 'v1' : 'off' - const launchCmd = `cd ${escapedDir} && printf '%s\\n' ${shellEscape(sourceCreditPolicy)} > ${shellEscape(sourceCreditPolicyFile)} && chmod 600 ${shellEscape(credentialFile)} ${shellEscape(sourceCreditPolicyFile)} && nohup ${escapedNode} relay.js --detached${sourceCreditFlag} --grace-time ${graceTime} --sock-path ${shellEscape(sockFile)} --credential-file ${shellEscape(credentialFile)} --log-file ${shellEscape(logFile)} > ${shellEscape(logFile)} 2>&1 ${shellEscape(logFile)} 2>&1 {}) launchChannel.on('error', () => {}) @@ -1441,8 +1382,7 @@ function buildWindowsRelayFallbackEndpoint( const sockPath = relayEndpointForHost(hostPlatform, remoteDir, fallbackSockName) return { sockPath, - endpointDir: relayHookEndpointDirForHost(hostPlatform, remoteDir, sockPath), - sourceCreditPolicyFile: sourceCreditPolicyPath(hostPlatform, remoteDir, sockPath) + endpointDir: relayHookEndpointDirForHost(hostPlatform, remoteDir, sockPath) } } @@ -1470,8 +1410,7 @@ async function readWindowsActiveRelayEndpoint( } return { sockPath, - endpointDir: relayHookEndpointDirForHost(hostPlatform, remoteDir, sockPath), - sourceCreditPolicyFile: sourceCreditPolicyPath(hostPlatform, remoteDir, sockPath) + endpointDir: relayHookEndpointDirForHost(hostPlatform, remoteDir, sockPath) } } @@ -1501,7 +1440,6 @@ async function rememberWindowsActiveRelayEndpoint( type WindowsRelayEndpoint = { sockPath: string endpointDir: string - sourceCreditPolicyFile: string } type WindowsRelayLaunchOptions = { @@ -1510,7 +1448,6 @@ type WindowsRelayLaunchOptions = { graceTime: number activePipeMarkerPath: string credentialFile: string - enablePtySourceCreditV1: boolean } & WindowsRelayEndpoint & { reconnectFallback?: WindowsRelayEndpoint } @@ -1523,13 +1460,6 @@ async function launchWindowsRelay( ): Promise<{ transport: MultiplexerTransport; nodePath: string; sockPath: string }> { let launchOpts = opts if ((await probeWindowsRelayPipe(conn, hostPlatform, opts, signal)) === 'READY') { - await assertExistingRelaySourceCreditPolicy( - conn, - hostPlatform, - opts.sourceCreditPolicyFile, - opts.enablePtySourceCreditV1, - signal - ) try { const transport = await connectWindowsRelay(conn, hostPlatform, opts, signal) await rememberWindowsActiveRelayEndpoint( @@ -1562,13 +1492,6 @@ async function launchWindowsRelay( launchOpts !== opts && (await probeWindowsRelayPipe(conn, hostPlatform, launchOpts, signal)) === 'READY' ) { - await assertExistingRelaySourceCreditPolicy( - conn, - hostPlatform, - launchOpts.sourceCreditPolicyFile, - launchOpts.enablePtySourceCreditV1, - signal - ) try { const transport = await connectWindowsRelay(conn, hostPlatform, launchOpts, signal) await rememberWindowsActiveRelayEndpoint( @@ -1613,9 +1536,7 @@ async function launchWindowsRelay( launchOpts.graceTime, logFile, errFile, - launchOpts.credentialFile, - launchOpts.sourceCreditPolicyFile, - launchOpts.enablePtySourceCreditV1 + launchOpts.credentialFile ), { signal } ) @@ -1707,9 +1628,7 @@ function windowsRelayLaunchCommand( graceTime: number, logFile: string, errFile: string, - credentialFile: string, - sourceCreditPolicyFile: string, - enablePtySourceCreditV1 = false + credentialFile: string ): string { const relayScript = joinRemotePath(hostPlatform, remoteDir, 'relay.js') // Why: Windows sshd kills the exec channel's process tree on close; WMI re-parents the detached relay to survive. @@ -1718,7 +1637,6 @@ function windowsRelayLaunchCommand( quoted(nodePath), quoted(relayScript), '--detached', - ...(enablePtySourceCreditV1 ? ['--pty-source-credit-v1'] : []), '--grace-time', String(graceTime), '--sock-path', @@ -1739,9 +1657,7 @@ function windowsRelayLaunchCommand( nodePath, remoteDir, [ - `Set-Content -LiteralPath ${powerShellLiteral(sourceCreditPolicyFile)} -Value ${powerShellLiteral(enablePtySourceCreditV1 ? 'v1' : 'off')} -NoNewline`, `& icacls.exe ${powerShellLiteral(credentialFile)} /inheritance:r /grant:r "$($env:USERNAME):(R,W)" | Out-Null`, - `& icacls.exe ${powerShellLiteral(sourceCreditPolicyFile)} /inheritance:r /grant:r "$($env:USERNAME):(R,W)" | Out-Null`, `$result = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = ${powerShellLiteral(wmiCommandLine)}; CurrentDirectory = ${powerShellLiteral(remoteDir)} }`, `if ($result.ReturnValue -ne 0) { throw "Win32_Process.Create failed with $($result.ReturnValue)" }` ].join('; ') diff --git a/src/main/ssh/ssh-relay-session-data-delivery.test.ts b/src/main/ssh/ssh-relay-session-data-delivery.test.ts index da15627e739..61c7fe86198 100644 --- a/src/main/ssh/ssh-relay-session-data-delivery.test.ts +++ b/src/main/ssh/ssh-relay-session-data-delivery.test.ts @@ -196,10 +196,7 @@ describe('SshRelaySession data delivery', () => { args.targetId, deps.getMainWindow, deps.mockStore, - deps.mockPortForward, - undefined, - undefined, - () => true + deps.mockPortForward ) await session.establish(deps.mockConn) vi.mocked(getPtyIdsForConnection).mockReturnValue([`ssh:${args.targetId}@@pty-1`]) @@ -325,15 +322,7 @@ describe('SshRelaySession data delivery', () => { acceptedSourceEndSu: 4 } ]) - const first = new SshRelaySession( - targetId, - getMainWindow, - mockStore, - mockPortForward, - undefined, - undefined, - () => true - ) + const first = new SshRelaySession(targetId, getMainWindow, mockStore, mockPortForward) await first.establish(mockConn) first.detach() @@ -355,15 +344,7 @@ describe('SshRelaySession data delivery', () => { incarnationId: 'incarnation-1', sourceRecovery: { status: 'restoreRequired', reason: 'checkpointUnavailable' } }) - const second = new SshRelaySession( - targetId, - getMainWindow, - mockStore, - mockPortForward, - undefined, - undefined, - () => true - ) + const second = new SshRelaySession(targetId, getMainWindow, mockStore, mockPortForward) const openCallCountBeforeRetry = openConsumerSessionMock.mock.calls.length await second.establish(mockConn) @@ -403,7 +384,25 @@ describe('SshRelaySession data delivery', () => { transformed?: boolean providerGeneration: number ptyIncarnation: string + source: { + relayPtyId: string + spanId: string + clientGeneration: number + ownerGeneration: number + deliveryToken: string + sourceStartSu: number + sourceEndSu: number + } }) => void + const source = { + relayPtyId: 'pty-1', + spanId: 'token-1:0:9', + clientGeneration: 1, + ownerGeneration: 1, + deliveryToken: 'token-1', + sourceStartSu: 0, + sourceEndSu: 9 + } onData({ id: 'ssh-pty-1', @@ -411,7 +410,8 @@ describe('SshRelaySession data delivery', () => { sequenceChars: 9, transformed: true, providerGeneration: 23, - ptyIncarnation: 'incarnation-1' + ptyIncarnation: 'incarnation-1', + source }) expect(acceptOutputDataMock).toHaveBeenCalledWith({ @@ -420,7 +420,8 @@ describe('SshRelaySession data delivery', () => { providerGeneration: 23, ptyIncarnation: 'incarnation-1', rawLength: 9, - transformed: true + transformed: true, + source }) expect(runtime.onPtyData).not.toHaveBeenCalled() expect(mockWindow.webContents.send).not.toHaveBeenCalledWith('pty:data', expect.anything()) @@ -428,15 +429,7 @@ describe('SshRelaySession data delivery', () => { it('forwards negotiated source identity to the bounded intake exactly once', async () => { const { mockConn, mockStore, mockPortForward, getMainWindow } = createMockDeps() - const session = new SshRelaySession( - 'target-1', - getMainWindow, - mockStore, - mockPortForward, - undefined, - undefined, - () => true - ) + const session = new SshRelaySession('target-1', getMainWindow, mockStore, mockPortForward) await session.establish(mockConn) const ptyProvider = vi.mocked(registerSshPtyProvider).mock.calls[0]?.[1] as unknown as { onData: ReturnType @@ -482,15 +475,7 @@ describe('SshRelaySession data delivery', () => { it('rejects missing negotiated source identity before main admission', async () => { const { mockConn, mockStore, mockPortForward, getMainWindow } = createMockDeps() - const session = new SshRelaySession( - 'target-1', - getMainWindow, - mockStore, - mockPortForward, - undefined, - undefined, - () => true - ) + const session = new SshRelaySession('target-1', getMainWindow, mockStore, mockPortForward) await session.establish(mockConn) const provider = vi.mocked(registerSshPtyProvider).mock.calls[0]?.[1] as unknown as { onData: ReturnType @@ -512,6 +497,11 @@ describe('SshRelaySession data delivery', () => { }) it('keeps unoffered source metadata out of legacy intake', async () => { + openConsumerSessionMock.mockImplementationOnce(async (_mux, options) => ({ + mode: 'legacy-fallback', + clientInstanceId: options.clientInstanceId, + serverBuildId: 'test-relay-build' + })) const { mockConn, mockStore, mockPortForward, getMainWindow } = createMockDeps() const session = new SshRelaySession('target-1', getMainWindow, mockStore, mockPortForward) await session.establish(mockConn) @@ -554,21 +544,13 @@ describe('SshRelaySession data delivery', () => { expect(session.getState()).toBe('ready') expect(pauseAdapterMock).not.toHaveBeenCalled() - expect(openConsumerSessionMock.mock.calls[0][1]).not.toHaveProperty('outputFlowControl') + expect(openConsumerSessionMock.mock.calls[0][1]).toHaveProperty('outputFlowControl') expect(installSshPtySourceAckPublisher).not.toHaveBeenCalled() }) it('publishes negotiated ACK batches through mux settlement', async () => { const { mockConn, mockStore, mockPortForward, getMainWindow } = createMockDeps() - const session = new SshRelaySession( - 'target-1', - getMainWindow, - mockStore, - mockPortForward, - undefined, - undefined, - () => true - ) + const session = new SshRelaySession('target-1', getMainWindow, mockStore, mockPortForward) await session.establish(mockConn) const publish = vi.mocked(installSshPtySourceAckPublisher).mock.calls[0]?.[1] const settled = vi.fn() @@ -589,30 +571,14 @@ describe('SshRelaySession data delivery', () => { expect(openConsumerSessionMock.mock.calls[0][1]).toMatchObject({ outputFlowControl: { requestedWindowSu: 256 * 1024 } }) - expect(deployAndLaunchRelay).toHaveBeenCalledWith( - mockConn, - undefined, - undefined, - 'target-1', - true - ) + expect(deployAndLaunchRelay).toHaveBeenCalledWith(mockConn, undefined, undefined, 'target-1') expect(notifyWithSettlementMock).toHaveBeenCalledWith('pty.ackData', batch, settled) }) - it('keeps the captured V1 selection through reconnect negotiation', async () => { - let enabled = true + it('offers V1 through reconnect negotiation', async () => { const { mockConn, mockStore, mockPortForward, getMainWindow } = createMockDeps() - const session = new SshRelaySession( - 'target-1', - getMainWindow, - mockStore, - mockPortForward, - undefined, - undefined, - () => enabled - ) + const session = new SshRelaySession('target-1', getMainWindow, mockStore, mockPortForward) await session.establish(mockConn) - enabled = false await session.reconnect(mockConn) @@ -623,16 +589,14 @@ describe('SshRelaySession data delivery', () => { mockConn, undefined, undefined, - 'target-1', - true + 'target-1' ) expect(deployAndLaunchRelay).toHaveBeenNthCalledWith( 2, mockConn, undefined, undefined, - 'target-1', - true + 'target-1' ) }) @@ -661,15 +625,7 @@ describe('SshRelaySession data delivery', () => { } ]) const { mockConn, mockStore, mockPortForward, getMainWindow, mockWindow } = createMockDeps() - const session = new SshRelaySession( - 'target-1', - getMainWindow, - mockStore, - mockPortForward, - undefined, - undefined, - () => true - ) + const session = new SshRelaySession('target-1', getMainWindow, mockStore, mockPortForward) await session.establish(mockConn) vi.mocked(getPtyIdsForConnection).mockReturnValue(['ssh:target-1@@pty-1']) vi.mocked(getSshPtyProvider).mockImplementation( diff --git a/src/main/ssh/ssh-relay-session-incarnation.test.ts b/src/main/ssh/ssh-relay-session-incarnation.test.ts index 482fa0df820..129a98b3ebc 100644 --- a/src/main/ssh/ssh-relay-session-incarnation.test.ts +++ b/src/main/ssh/ssh-relay-session-incarnation.test.ts @@ -26,8 +26,10 @@ vi.mock('../ipc/ssh-pty-output-intake-registry', () => ({ })), closeSshPtyOutputGeneration: vi.fn(), getSshPtyAcceptedSourceCheckpoints: vi.fn(() => []), + applySshPtySourceCancellationProof: vi.fn(() => true), applySshPtySourceRecoveryCancellationProof: vi.fn(() => true), - installSshPtySourceAckPublisher: vi.fn(() => () => {}) + installSshPtySourceAckPublisher: vi.fn(() => () => {}), + installSshPtySourceCancellationPublisher: vi.fn(() => () => {}) })) vi.mock('./ssh-relay-deploy-helpers', () => ({ execCommand: vi.fn().mockResolvedValue('') })) vi.mock('./ssh-channel-multiplexer', () => ({ diff --git a/src/main/ssh/ssh-relay-session-model-migration.test.ts b/src/main/ssh/ssh-relay-session-model-migration.test.ts index f652bc48fcf..9bd2216357d 100644 --- a/src/main/ssh/ssh-relay-session-model-migration.test.ts +++ b/src/main/ssh/ssh-relay-session-model-migration.test.ts @@ -174,10 +174,7 @@ describe('SshRelaySession model migration', () => { targetId, deps.getMainWindow, deps.mockStore, - deps.mockPortForward, - undefined, - undefined, - () => true + deps.mockPortForward ) await session.establish(deps.mockConn) vi.mocked(getPtyIdsForConnection).mockReturnValue([appPtyId]) @@ -228,10 +225,7 @@ describe('SshRelaySession model migration', () => { targetId, deps.getMainWindow, deps.mockStore, - deps.mockPortForward, - undefined, - undefined, - () => true + deps.mockPortForward ) await session.establish(deps.mockConn) vi.mocked(getPtyIdsForConnection).mockReturnValue([appPtyId]) diff --git a/src/main/ssh/ssh-relay-session-reconnect-incarnation.test.ts b/src/main/ssh/ssh-relay-session-reconnect-incarnation.test.ts index d37476f7e3c..7ad8ac74ac3 100644 --- a/src/main/ssh/ssh-relay-session-reconnect-incarnation.test.ts +++ b/src/main/ssh/ssh-relay-session-reconnect-incarnation.test.ts @@ -214,19 +214,9 @@ describe('SshRelaySession reconnect incarnation ordering', () => { } }) - it('keeps source-credit selection immutable through reconnect', async () => { + it('offers source credit through reconnect', async () => { const { mockConn, mockStore, mockPortForward, getMainWindow } = createMockDeps() - let sourceCreditEnabled = true - const session = new SshRelaySession( - 'target-1', - getMainWindow, - mockStore, - mockPortForward, - undefined, - undefined, - () => sourceCreditEnabled - ) - sourceCreditEnabled = false + const session = new SshRelaySession('target-1', getMainWindow, mockStore, mockPortForward) await session.establish(mockConn) await session.reconnect(mockConn) @@ -236,16 +226,14 @@ describe('SshRelaySession reconnect incarnation ordering', () => { mockConn, undefined, undefined, - 'target-1', - true + 'target-1' ) expect(deployAndLaunchRelay).toHaveBeenNthCalledWith( 2, mockConn, undefined, undefined, - 'target-1', - true + 'target-1' ) expect(openConsumerSessionMock).toHaveBeenCalledTimes(2) for (const [, options] of openConsumerSessionMock.mock.calls) { diff --git a/src/main/ssh/ssh-relay-session-recovery-races.test.ts b/src/main/ssh/ssh-relay-session-recovery-races.test.ts index 54cbe57aa19..711f926e056 100644 --- a/src/main/ssh/ssh-relay-session-recovery-races.test.ts +++ b/src/main/ssh/ssh-relay-session-recovery-races.test.ts @@ -195,10 +195,7 @@ describe('SshRelaySession recovery race fencing', () => { targetId, deps.getMainWindow, deps.mockStore, - deps.mockPortForward, - undefined, - undefined, - () => true + deps.mockPortForward ) await session.establish(deps.mockConn) vi.mocked(getPtyIdsForConnection).mockReturnValue([`ssh:${targetId}@@pty-1`]) diff --git a/src/main/ssh/ssh-relay-session.test.ts b/src/main/ssh/ssh-relay-session.test.ts index 7495575958e..40b38a75b52 100644 --- a/src/main/ssh/ssh-relay-session.test.ts +++ b/src/main/ssh/ssh-relay-session.test.ts @@ -35,8 +35,10 @@ vi.mock('../ipc/ssh-pty-output-intake-registry', () => ({ })), closeSshPtyOutputGeneration: vi.fn(), getSshPtyAcceptedSourceCheckpoints: vi.fn(() => []), + applySshPtySourceCancellationProof: vi.fn(() => true), applySshPtySourceRecoveryCancellationProof: vi.fn(() => true), - installSshPtySourceAckPublisher: vi.fn(() => () => {}) + installSshPtySourceAckPublisher: vi.fn(() => () => {}), + installSshPtySourceCancellationPublisher: vi.fn(() => () => {}) })) vi.mock('./ssh-relay-deploy-helpers', () => ({ @@ -133,16 +135,9 @@ describe('SshRelaySession', () => { beforeEach(() => { vi.clearAllMocks() openConsumerSessionMock.mockImplementation(async (_mux, options) => ({ - mode: 'negotiated', + mode: 'legacy-fallback', clientInstanceId: options.clientInstanceId, - clientGeneration: 1, - ownerGeneration: 1, - ownerLease: 'test-owner-lease', - ...(options.outputFlowControl - ? { - outputFlowControl: { version: 1, windowSu: options.outputFlowControl.requestedWindowSu } - } - : {}) + serverBuildId: 'test-relay-build' })) delete process.env.ORCA_FEATURE_REMOTE_AGENT_HOOKS muxRequestMock.mockReset() @@ -833,7 +828,7 @@ describe('SshRelaySession', () => { await session.establish(mockConn, 600) - expect(deployAndLaunchRelay).toHaveBeenCalledWith(mockConn, undefined, 600, 'target-1', false) + expect(deployAndLaunchRelay).toHaveBeenCalledWith(mockConn, undefined, 600, 'target-1') }) it('restores the configured relay grace after establish', async () => { diff --git a/src/main/ssh/ssh-relay-session.ts b/src/main/ssh/ssh-relay-session.ts index b7b543e1975..0a1152e1c73 100644 --- a/src/main/ssh/ssh-relay-session.ts +++ b/src/main/ssh/ssh-relay-session.ts @@ -282,7 +282,6 @@ export class SshRelaySession { >() private readonly retiredSourceDeliveries = new SshPtyRetiredSourceDeliveries() private readonly ptyConsumerClientInstanceId: string - private readonly ptySourceCreditEnabled: boolean private ptyConsumerSessionState: SshPtyConsumerSessionState | null = null constructor( @@ -295,11 +294,9 @@ export class SshRelaySession { targetId: string, ports: DetectedPort[], platform: string - ) => void, - isPtySourceCreditEnabled: () => boolean = () => false + ) => void ) { this.ptyConsumerClientInstanceId = ptyConsumerRecoveryForTarget(targetId).clientInstanceId - this.ptySourceCreditEnabled = isPtySourceCreditEnabled() } refreshEnvironment( @@ -395,13 +392,7 @@ export class SshRelaySession { sockPath, credentialFile, hostPlatform - } = await deployAndLaunchRelay( - conn, - undefined, - graceTimeSeconds, - this.targetId, - this.ptySourceCreditEnabled - ) + } = await deployAndLaunchRelay(conn, undefined, graceTimeSeconds, this.targetId) this.hostPlatform = hostPlatform ?? null this.remoteCliBridgeEnv = remoteHome && remoteRelayDir && nodePath && sockPath && hostPlatform @@ -516,13 +507,7 @@ export class SshRelaySession { sockPath, credentialFile, hostPlatform - } = await deployAndLaunchRelay( - conn, - undefined, - graceTimeSeconds, - this.targetId, - this.ptySourceCreditEnabled - ) + } = await deployAndLaunchRelay(conn, undefined, graceTimeSeconds, this.targetId) this.hostPlatform = hostPlatform ?? null this.remoteCliBridgeEnv = remoteHome && remoteRelayDir && nodePath && sockPath && hostPlatform @@ -857,9 +842,7 @@ export class SshRelaySession { clientInstanceId: this.ptyConsumerClientInstanceId, expectedServerBuildId: serverBuildId, allowSameBuildLegacyFallback: true, - ...(this.ptySourceCreditEnabled - ? { outputFlowControl: { requestedWindowSu: DEFAULT_PTY_SOURCE_WINDOW_SU } } - : {}) + outputFlowControl: { requestedWindowSu: DEFAULT_PTY_SOURCE_WINDOW_SU } } try { return await openSshPtyConsumerSession(mux, { diff --git a/src/relay/dispatcher.test.ts b/src/relay/dispatcher.test.ts index c110fbb3634..aaa083b7743 100644 --- a/src/relay/dispatcher.test.ts +++ b/src/relay/dispatcher.test.ts @@ -542,9 +542,9 @@ describe('RelayDispatcher', () => { } }) - it('keeps a saturated gate-off primary as required backpressure', () => { + it('keeps a saturated legacy primary as required backpressure', () => { const callbacks: ((result: SinkWriteSettlement) => void)[] = [] - const gateOffDispatcher = new RelayDispatcher( + const legacyDispatcher = new RelayDispatcher( (_data, settle) => { callbacks.push(settle) return false @@ -556,13 +556,13 @@ describe('RelayDispatcher', () => { } ) const detached = vi.fn() - gateOffDispatcher.onClientDetached(detached) + legacyDispatcher.onClientDetached(detached) const payload = 'x'.repeat(128 * 1024) let admitted = 0 try { while ( - gateOffDispatcher.tryNotifyPtyDataToMatchingClients(() => true, { + legacyDispatcher.tryNotifyPtyDataToMatchingClients(() => true, { id: 'pty-1', data: payload }) @@ -575,13 +575,13 @@ describe('RelayDispatcher', () => { expect(callbacks).toHaveLength(1) expect(detached).not.toHaveBeenCalled() expect( - gateOffDispatcher.tryNotifyPtyDataToMatchingClients(() => true, { + legacyDispatcher.tryNotifyPtyDataToMatchingClients(() => true, { id: 'pty-1', data: payload }) ).toBe(false) } finally { - gateOffDispatcher.dispose() + legacyDispatcher.dispose() } }) diff --git a/src/relay/relay-pty-source-publication.test.ts b/src/relay/relay-pty-source-publication.test.ts index e979345744f..3ce4526efa6 100644 --- a/src/relay/relay-pty-source-publication.test.ts +++ b/src/relay/relay-pty-source-publication.test.ts @@ -599,38 +599,6 @@ describe('RelayPtySourcePublication', () => { ).toEqual(['pty.data', 'pty.exit']) }) - it('keeps gate-off same-build sessions on legacy capability omission', async () => { - const writes: Buffer[] = [] - dispatcher = new RelayDispatcher( - (data, onSettled) => { - writes.push(Buffer.from(data)) - onSettled({ ok: true }) - return true - }, - { supportsWriteCallback: true }, - endpointIdentity - ) - new SshPtyConsumerSessionAdapter(dispatcher, 'build-a', undefined, undefined, false) - dispatcher.feed( - requestFrame(1, 'pty.openClient', { - protocolVersion: 1, - clientInstanceId: 'client-1', - requestedRole: 'session-owner', - capabilities: { outputFlowControl: { versions: [1], requestedWindowSu: 4 } } - }) - ) - await flushRequests() - - expect( - writes - .map((buffer) => { - const length = buffer.readUInt32BE(9) - return JSON.parse(buffer.subarray(13, 13 + length).toString('utf8')).result - }) - .find(Boolean) - ).not.toHaveProperty('capabilities') - }) - it('settles the recovery fence before sending buffered live output', async () => { const harness = await createHarness(4) harness.publication.publish('pty-1', { data: 'abcdefgh' }, false) diff --git a/src/relay/relay.ts b/src/relay/relay.ts index 7cbb48763a8..433cef705c5 100644 --- a/src/relay/relay.ts +++ b/src/relay/relay.ts @@ -110,7 +110,6 @@ function parseArgs(argv: string[]): { endpointDir?: string logFile?: string credentialFile?: string - ptySourceCreditV1: boolean } { let graceTimeMs = DEFAULT_GRACE_MS let connectMode = false @@ -120,7 +119,6 @@ function parseArgs(argv: string[]): { let endpointDir: string | undefined let logFile: string | undefined let credentialFile: string | undefined - let ptySourceCreditV1 = false for (let i = 2; i < argv.length; i++) { if (argv[i] === '--grace-time' && argv[i + 1]) { const parsed = Number.parseInt(argv[i + 1], 10) @@ -147,8 +145,6 @@ function parseArgs(argv: string[]): { } else if (argv[i] === '--credential-file' && argv[i + 1]) { credentialFile = argv[i + 1] i++ - } else if (argv[i] === '--pty-source-credit-v1') { - ptySourceCreditV1 = true } } if (!sockPath) { @@ -162,8 +158,7 @@ function parseArgs(argv: string[]): { sockPath, endpointDir, logFile, - credentialFile, - ptySourceCreditV1 + credentialFile } } @@ -426,8 +421,7 @@ async function main(): Promise { sockPath, endpointDir, logFile, - credentialFile, - ptySourceCreditV1 + credentialFile } = parseArgs(process.argv) const endpointCredential = readEndpointCredential(credentialFile) @@ -564,8 +558,7 @@ async function main(): Promise { dispatcher, launchVersion, (id, paused) => ptyHandler.setConsumerDeliveryPaused(id, paused), - (id) => ptyHandler.handleSourceCreditAvailable(id), - ptySourceCreditV1 + (id) => ptyHandler.handleSourceCreditAvailable(id) ) const ptySourcePublication = new RelayPtySourcePublication( dispatcher, @@ -757,7 +750,7 @@ async function main(): Promise { active: ptyHandler.activePtyCount }, ptySourceCredit: { - enabled: ptySourceCreditV1, + enabled: true, session: ptyConsumerSessionAdapter.getDebugSnapshot(), publication: ptySourcePublication.getDebugSnapshot() }, diff --git a/src/relay/ssh-pty-consumer-session-adapter.ts b/src/relay/ssh-pty-consumer-session-adapter.ts index ecdcfc5c109..c5bedb37053 100644 --- a/src/relay/ssh-pty-consumer-session-adapter.ts +++ b/src/relay/ssh-pty-consumer-session-adapter.ts @@ -77,8 +77,7 @@ export class SshPtyConsumerSessionAdapter { dispatcher: RelayDispatcher, serverBuildId: string, private readonly setDeliveryPaused?: (id: string, paused: boolean) => void, - onSourceCreditAvailable?: (id: string) => void, - sourceCreditEnabled = true + onSourceCreditAvailable?: (id: string) => void ) { this.sourceCredit = new SshPtySourceCreditAdapter( (proof) => @@ -90,9 +89,7 @@ export class SshPtyConsumerSessionAdapter { ) this.session = new PtyConsumerSession({ serverBuildId, - ...(sourceCreditEnabled - ? { outputFlowControl: { versions: [1], maxWindowSu: DEFAULT_PTY_SOURCE_WINDOW_SU } } - : {}) + outputFlowControl: { versions: [1], maxWindowSu: DEFAULT_PTY_SOURCE_WINDOW_SU } }) dispatcher.onRequest(SSH_PTY_OPEN_CLIENT_METHOD, (params, context) => this.openClient(params, context) diff --git a/src/renderer/src/components/settings/SshHostAdvancedFields.test.tsx b/src/renderer/src/components/settings/SshHostAdvancedFields.test.tsx deleted file mode 100644 index c7722b262b5..00000000000 --- a/src/renderer/src/components/settings/SshHostAdvancedFields.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { renderToStaticMarkup } from 'react-dom/server' -import { describe, expect, it, vi } from 'vitest' -import { SshHostAdvancedFields } from './SshHostAdvancedFields' -import { EMPTY_FORM } from './ssh-target-draft' - -describe('SshHostAdvancedFields', () => { - it('renders bounded terminal output as a per-target experimental option', () => { - const markup = renderToStaticMarkup( - - ) - - expect(markup).toContain('Bounded terminal output (experimental)') - expect(markup).toContain('Limits buffered terminal output') - expect(markup).toContain('aria-checked="true"') - expect(markup).toContain('Toggle bounded terminal output') - }) -}) diff --git a/src/renderer/src/components/settings/SshHostAdvancedFields.tsx b/src/renderer/src/components/settings/SshHostAdvancedFields.tsx index 16c5608f629..3be7dca8fce 100644 --- a/src/renderer/src/components/settings/SshHostAdvancedFields.tsx +++ b/src/renderer/src/components/settings/SshHostAdvancedFields.tsx @@ -113,36 +113,6 @@ export function SshHostAdvancedFields({ )} /> -
-
- -

- {translate( - 'auto.components.settings.SshTargetForm.ptySourceCreditV1Description', - 'Limits buffered terminal output and improves recovery when this SSH target reconnects.' - )} -

-
- - onFormChange((f) => ({ - ...f, - experimentalPtySourceCreditV1: !f.experimentalPtySourceCreditV1 - })) - } - ariaLabel={translate( - 'auto.components.settings.SshTargetForm.ptySourceCreditV1Toggle', - 'Toggle bounded terminal output' - )} - /> -