Skip to content

test(ssh): prove connect-time conversion to managed orcad on real Linux and Windows hosts - #24981

Merged
OrcaWin merged 32 commits into
OrcaWin/node-rt-phase3from
OrcaWin/node-rt-p3-convert-host-cells
Oct 3, 2026
Merged

OrcaWin merged 32 commits into
OrcaWin/node-rt-phase3from
OrcaWin/node-rt-p3-convert-host-cells

Conversation

@OrcaWin

@OrcaWin OrcaWin commented Oct 3, 2026 •

Copy link
Copy Markdown
Collaborator
Files Added Deleted Net
Test 16 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​951 $\color{#cf222e}{\Huge{\mathbf{−}}}$​6 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​945
Prod 18 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​243 $\color{#cf222e}{\Huge{\mathbf{−}}}$​81 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​162

ELI5

This adds tests that run the real app against real SSH hosts and check the new connect-time conversion (#24979). A host Orca used over the old relay must move onto a managed Orca server the next time it connects. Its projects, folders and terminal tabs must come along, and its old records must stay until source retirement is switched on. It runs on a Linux Docker host and on a Windows OpenSSH host. A third test checks that the last relay-only release (v1.4.218) can still see a converted host and its project, in case someone downgrades.

What Changed

  • tests/e2e/ssh-orcad-auto-convert.spec.ts, one spec for both host kinds:
    1. Relay era: the app's orcad template path (ORCA_ORCAD_TEMPLATE_PATH) is empty, so the connect keeps the relay (orcad_unavailable). The spec registers the repo, adds a remote project group and folder workspace, adds a session tab that never spawns a shell, and runs then exits a shell (its exit record lets the terminal gate prove nothing is running).
    2. Conversion: the template is copied in, then disconnect and connect. The status must reach managed. On the new environment, repo.list, folderWorkspace.list and session.tabs.listAll must contain the repo, the folder and the tab.
    3. Retention, then retirement: the cutover journal must be destination-committed with sourceRetainedAt. With the retirement flag switched on, the next connect must move it to source-retired, and the server must still serve the repo.
  • tests/e2e/helpers/orcad-convert-host.ts: the host is the Docker fixture (ORCA_E2E_ORCAD_CONVERT_HOST=docker) or a Windows host-cell descriptor.
  • Linux: a new orcad-auto-convert-docker job in e2e.yml builds the linux-x64-glibc template outside the app's default lookup and runs the spec. It's registered as a dedicated spec (job selection, sharding expectation, source route, release permissions).
  • Windows: a new orcad-convert cell in ssh-windows-hosts, run on the x64 inbox host only and last in the run.
    • It creates the repo as the private account over the lane's own SSH, then moves out/orcad-template aside.
    • It switches native modules to Electron's runtime, builds the e2e app, and runs the same spec against the descriptor.
    • The descriptor parser accepts the new cell id.
  • Downgrade: tests/e2e/cross-version-wire/managed-orcad-convert-downgrade.unit.test.ts loads v1.4.218's own normalizeSshTarget and SshConnectionStore from a tagged checkout.
    • The host this build persists (fence in orcadFence, no owner) must still be listed with its relay endpoint.
    • This build's visibleRepos hides the retained project, which that release's unfiltered repos:list still returns.
  • One e2e-only product hook: ORCA_E2E_ROLLOUT_FLAGS_FILE, read on every rollout flag check and honored only when ORCA_E2E_USER_DATA_DIR is set (the gate the other ORCA_E2E_* variables use). The retirement flag otherwise only arrives through the hardcoded campaign fetch, so a test couldn't switch it on between two connects.

Why

The conversion reads the app's global SSH connection manager and target registry, so only the real app can drive it end to end. Running the same spec against Docker and a Windows host shows the conversion the same way on both kinds of host. The downgrade check uses the cross-version harness so the old build's own code decides visibility, not a re-implementation of it.

Linked Issue

Part of the Node-runtime convergence (#16741), Phase 3. Stacked on #24979.

Visual Proof

N/A: tests and CI.

Testing

  • Locally: the downgrade cross-version test (2 tests against a v1.4.218 checkout), the rollout override tests, the host-cell tests, and the workflow and routing contract suites (42 files, 388 tests). Lint, the gate and the e2e typecheck are clean.

  • The Docker and Windows cells run in this PR's CI.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

The Windows host lane cannot provision the conversion cell this PR adds: $cells now holds six entries, but those are passed as -Accounts 6 to a script whose parameter is capped at 5. The x64/inbox ssh-windows-hosts job fails before any test runs.

Reviewed changes

  • New e2e spec ssh-orcad-auto-convert.spec.ts — real-host proof that a relay-era target converts to managed orcad on connect: relay era (managedServer kind:'relay' / reason:'orcad_unavailable') seeds a repo, folder workspace and dormant tab; after the template is copied in, a reconnect reaches managed and the new server's repo.list / folderWorkspace.list / session.tabs.listAll contain those rows; the cutover journal moves destination-committed → source-retired once orcad-source-retirement is on.
  • New host helper orcad-convert-host.ts — resolves the docker fixture or a Windows host-cell descriptor.
  • Linux CI — new orcad-auto-convert-docker job builds the linux-x64-glibc template outside the default lookup and runs the spec; registered as a dedicated spec across the job-selection, source-routing, sharding, and release-permission contract scripts.
  • Windows CI — new orcad-convert cell (x64/inbox only, appended last because it switches native modules to Electron) plus descriptor-parser and relay-lane guard updates.
  • Downgrade unit test — loads v1.4.218's own normalizeSshTarget / SshConnectionStore and pins that this build's orcadFence keeps the host visible over its relay while visibleRepos hides the retained repo.
  • One product hook — readE2ERolloutConfigOverride, read on every isRolloutFlagActive but gated on ORCA_E2E_USER_DATA_DIR, so production is unaffected.

The conversion mechanism itself reads sound and the assertions are tied to the real server catalog rather than a re-implementation. Two issues with the lane wiring would prevent the new coverage from running as intended.

Technical details — Windows cell count exceeds the provider's account cap
# The provider rejects the sixth account

## Affected sites
- `.github/workflows/ssh-windows-hosts.yml:156` — appends `orcad-convert`, making `$cells.Count` 6 for x64/inbox.
- `.github/workflows/ssh-windows-hosts.yml:169` — `-Accounts $cells.Count` passed to `prove-preview-openssh.ps1`.
- `config/ci/windows-ssh-provider/preview-ssh/prove-preview-openssh.ps1:5` — `[ValidateRange(1,5)][int]$Accounts=1`.
- `config/ci/windows-ssh-provider/preview-ssh/prove-preview-openssh.ps1:35` — account names derive 1:1 from `$Accounts`.

## Required outcome
- The x64/inbox job provisions six private accounts and runs the conversion cell.

## Suggested approach
- Widen `ValidateRange` to admit six (e.g. `1,6`), or avoid consuming a sixth slot. The provider already
  derives account names from `$Accounts`, so widening the range is sufficient.
Technical details — dedicated Docker job is skipped for test-only edits
# ORCAD_AUTO_CONVERT_E2E_SPEC missing from e2e_needs_build

## Affected sites
- `config/scripts/ci-e2e-job-selection.mjs:43` — new dedicated spec constant.
- `config/scripts/ci-e2e-job-selection.mjs:85-90` — `e2e_needs_build` OR-list names `dockerSpecs`, `LOCALHOST_SSH_E2E_SPEC` and `ORCAD_SERVE_MODE_SWITCH_E2E_SPEC`, but not the new spec.
- `config/scripts/ci-e2e-job-selection.test.mjs:31-37` — build-prerequisite case does not cover either orcad spec.
- `.github/workflows/e2e.yml:560` — `orcad-auto-convert-docker.if` cannot compensate: its `needs` are skipped.

## Required outcome
- A PR that changes only `tests/e2e/ssh-orcad-auto-convert.spec.ts` (or `orcad-convert-host.ts`) still runs the
  `orcad-auto-convert-docker` job.

## Suggested approach
- Add `spec === ORCAD_AUTO_CONVERT_E2E_SPEC` to the `e2e_needs_build` `specs.some(...)` list, and extend the
  `ci-e2e-job-selection.test.mjs` iteration to assert it (as `ORCAD_SERVE_MODE_SWITCH_E2E_SPEC` already is in the
  production list).

## Open questions for the human
- Is the Docker job intentionally gated on `ssh_source_changed == 'true'` (so it runs on any SSH source change),
  unlike `orcad-serve-mode-switch`? If so that is only a CI-cost decision; the test-only gap above still applies
  when no source changes.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment thread .github/workflows/ssh-windows-hosts.yml
// Needs the packaged orcad slot, which only its own job builds.
export const ORCAD_SERVE_MODE_SWITCH_E2E_SPEC = 'tests/e2e/orcad-serve-mode-switch.spec.ts'
// Needs the orcad template for its host's target, which only its own job builds.
export const ORCAD_AUTO_CONVERT_E2E_SPEC = 'tests/e2e/ssh-orcad-auto-convert.spec.ts'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This dedicated spec depends on build/prepare-native-cache like ORCAD_SERVE_MODE_SWITCH_E2E_SPEC, but it was not added to classifyE2eJobs's e2e_needs_build specs.some(...) list (lines 85-90). A PR that only edits tests/e2e/ssh-orcad-auto-convert.spec.ts (or orcad-convert-host.ts) yields e2e_needs_build=false, so build/prepare-native-cache skip and the dependent orcad-auto-convert-docker job is skipped despite contains(inputs.test_files, …) being true. Add spec === ORCAD_AUTO_CONVERT_E2E_SPEC there and cover it in ci-e2e-job-selection.test.mjs's build-prerequisite case.

Technical details
# Dedicated Docker job never runs on test-only edits

## Affected sites
- `config/scripts/ci-e2e-job-selection.mjs:85-90` — `e2e_needs_build` list omits the new spec.
- `config/scripts/ci-e2e-job-selection.test.mjs:31-37` — build-prerequisite iteration omits both orcad specs.
- `.github/workflows/e2e.yml:560` — job `if` cannot rescue a skipped `needs`.

## Required outcome
- Editing only the spec or its helper still runs `orcad-auto-convert-docker`.

## Suggested approach
- Add `spec === ORCAD_AUTO_CONVERT_E2E_SPEC` to the OR-list and extend the test iteration.

@OrcaWin
OrcaWin force-pushed the OrcaWin/node-rt-p3-convert-host-cells branch from 2348137 to 3e5ebdc Compare October 3, 2026 09:54
@OrcaWin
OrcaWin requested a review from brennanb2025 as a code owner October 3, 2026 09:54
@OrcaWin
OrcaWin changed the base branch from OrcaWin/node-rt-p3-managed-default to OrcaWin/node-rt-phase3 October 3, 2026 09:54
@OrcaWin OrcaWin closed this Oct 3, 2026
@OrcaWin OrcaWin reopened this Oct 3, 2026

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the new commits. The Windows provisioning blocker from the prior review is fixed; one earlier thread (the dedicated Docker job's e2e_needs_build gating) is still open and unaddressed, so this run cannot approve.

Reviewed changes

  • Windows conversion cell can provision — prove-preview-openssh.ps1 raises the private-account cap from five to six ([ValidateRange(1,6)]), so the x64/inbox lane that appends orcad-convert no longer fails PowerShell parameter binding before any cell runs.
  • Account-cap contract test — ssh-windows-hosts-workflow.test.mjs now asserts the workflow appends orcad-convert and that the provisioner's cap is at least WINDOWS_HOST_CELL_IDS.length + WINDOWS_ORCAD_CELL_IDS.length + 1, so the count can't silently drift past the range again.
  • Spec reads the host's session partition — the dormant-tab check in ssh-orcad-auto-convert.spec.ts now calls window.api.session.get(toSshExecutionHostId(remote.targetId)) instead of the local partition, matching how ssh-relay-session.ts writes an SSH worktree's workspace session.
  • Packaged builds refuse the E2E rollout override — readE2ERolloutConfigOverride takes an isPackaged flag defaulting to !hasAppEnvironment() || getAppEnvironment().isPackaged(), so an env-named flags file cannot move rollout flags in a shipped build; tests cover the packaged, unpackaged, and missing-file cases.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the newest commit. The prior review's one open thread — the dedicated Docker job's e2e_needs_build gating (config/scripts/ci-e2e-job-selection.mjs:85-90) — is still unaddressed, so this run cannot approve.

Reviewed changes

  • Convert poll reports the full relay state on timeout — tests/e2e/ssh-orcad-auto-convert.spec.ts replaces the toMatchObject({ kind: 'managed' }) poll with an isManaged helper that returns 'managed' or JSON.stringify(server), so a timeout failure shows why the host stayed on the relay rather than a bare matcher diff. Test-only; no product surface.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the newest commit. The one open thread from the initial review — the dedicated Docker job's e2e_needs_build gating (config/scripts/ci-e2e-job-selection.mjs:85-90) — is still unaddressed, so this run cannot approve.

Reviewed changes

  • Require the relay to list no terminals before the converting connect — tests/e2e/ssh-orcad-auto-convert.spec.ts now polls window.api.pty.listSessions({ connectionId }) to '[]' after the shell exits, so the reconnect that triggers conversion starts from a state the relay has confirmed is terminal-free. pty:listSessions routes the scoped call to the same provider.listProcesses() the conversion gate's relay lister uses (orcad-migration-relay-pty-lister.ts), so this is a faithful precondition, not a local-only shadow. If a relay PTY lingers, the poll fails with the relay's actual session JSON rather than surfacing later as an opaque conversion timeout. Test-only; no product surface.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the newest commits. The one open thread from the initial review — the dedicated Docker job's e2e_needs_build gating (config/scripts/ci-e2e-job-selection.mjs:85-90) — is still unaddressed, so this run cannot approve.

Reviewed changes

  • The terminal gate's lease input is now asserted before conversion — tests/e2e/ssh-orcad-auto-convert.spec.ts polls the target's persisted sshRemotePtyLeases until none read attached/detached, mirroring the live-lease filter in orcad-migration-terminal-gate.ts:25. Together with the earlier pty.listSessions === '[]' poll (the relay's process list, which keeps the expired-lease path from blocking as unverifiable), both gate inputs are now proven before the reconnect.
  • The convert poll's timeout failure names the leases — a targetLeases helper is extracted and reused, and the non-managed branch now returns JSON.stringify({ server, leases }), so a timeout shows why the host stayed on the relay.
  • The convert poll timeout drops from 8 minutes to 30 seconds — the removed CONVERT_TIMEOUT_MS is replaced by a fixed 30_000. This is safe: ssh-connect-flow.ts:172 awaits decideHostServer (and publishManagedServerConnect broadcasts) before window.api.ssh.connect resolves, so the poll is only waiting on the renderer broadcast, not on the conversion.

Test-only; no product surface.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the newest commit. The one open thread from the initial review — the dedicated Docker job's e2e_needs_build gating (config/scripts/ci-e2e-job-selection.mjs:85-90) — is still unaddressed, so this run cannot approve.

Reviewed changes

  • The relay era now ends with no relay shell left to respawn — tests/e2e/ssh-orcad-auto-convert.spec.ts switches to the local worktree (switchToWorktree(page, localWorktreeId)) before sending exit, then creates the dormant session tab after the exit and both gate polls, with { activate: false } only. Previously the tab was created up front with pendingActivationSpawn: true; now it is created while its worktree is hidden, so it never mounts a shell. Closing the exited tab can no longer activate and spawn a live relay shell on the remote worktree, which would otherwise keep the host on the relay. Test-only; no product surface.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the newest commit. The one open thread from the initial review — the dedicated Docker job's e2e_needs_build gating (config/scripts/ci-e2e-job-selection.mjs:85-90) — is still unaddressed, so this run cannot approve.

Reviewed changes

  • Settles before the converting connect — tests/e2e/ssh-orcad-auto-convert.spec.ts now waits 5s after copying the template and re-asserts that pty.listSessions is empty and no lease reads attached/detached, catching a relay shell that starts after the earlier gate polls have already passed.
  • Names the tabs a lingering shell belongs to — the seeded tab's id is captured via getActiveTabId, and both it and the dormant sessionTabId are included in the pre-convert snapshot and the convert-poll failure JSON, so a timeout shows which sessions a resurrected relay shell is tied to.

Test-only; no product surface.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the newest commit. The one open thread from the initial review — the dedicated Docker job's e2e_needs_build gating (config/scripts/ci-e2e-job-selection.mjs:85-90) — is still unaddressed, so this run cannot approve.

Reviewed changes

  • The session tab is now the exited relay tab — tests/e2e/ssh-orcad-auto-convert.spec.ts drops the createTab(remote.worktreeId, …, { activate: false }) dormant tab and instead captures the active terminal tab (getActiveTabId) as sessionTabId, runs the marker and exit in it, then asserts that same id appears in the host's session partition and in the managed server's session.tabs.listAll after conversion. Any tab that mounts starts a shell, so reusing the tab already exited removes the risk that the "dormant" tab mounts and blocks the conversion; the pre-convert snapshot and poll failure JSON now carry only sessionTabId.

Test-only; no product surface.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the newest commit. The one open thread from the initial review — the dedicated Docker job's e2e_needs_build gating (config/scripts/ci-e2e-job-selection.mjs:85-90) — is still unaddressed, so this run cannot approve.

Reviewed changes

  • The converted session tab is now an editor tab — tests/e2e/ssh-orcad-auto-convert.spec.ts opens <remoteRepo>/README.md via store.openFile on the remote worktree instead of reusing the exited relay terminal tab. Because editor tabs never mount a shell, this removes the last way the relay era could respawn a terminal and hold the host back, while still exercising session migration: the spec asserts the host's session partition includes the file path, and the managed server's session.tabs.listAll contains it after conversion. Editor rows are part of the orcad workspace-session migration (orcad-destination-workspace-session.ts / orcad-source-workspace-session-fragments.ts), so the assertion is on the migrated session rather than a local shadow. The terminal is still run and exited only to satisfy the conversion gate; the header comment and poll-failure JSON were updated to match.

Test-only; no product surface.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the newest commit. The one open thread from the initial review — the dedicated Docker job's e2e_needs_build gating (config/scripts/ci-e2e-job-selection.mjs:85-90) — is still unaddressed, so this run cannot approve.

Reviewed changes

  • Refused conversions now log their blockers — src/main/ssh/orcad-runtime-conversion.ts replaces the fenced/refuse ternary in fenceOrResume with an explicit early return; on the refusal path it console.warns the preflight blocker codes (and dependencies for orcad_migration_dependent_state) before returning refuse(...). The connect still surfaces only result.reason, so this is main-process support diagnostics. The success path is unchanged, and the [ssh]-prefixed console.warn matches the convention used across src/main/ssh.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the newest commit. The one open thread from the initial review — the dedicated Docker job's e2e_needs_build gating (config/scripts/ci-e2e-job-selection.mjs:85-90) — is still unaddressed, so this run cannot approve.

Reviewed changes

  • Pre-convert diagnostics now print both session partitions — tests/e2e/ssh-orcad-auto-convert.spec.ts adds a console.log that reads the host partition (window.api.session.get(hostId)) plus the local partition's fields that mention the remote worktree, beside the existing census-input log, right before the converting connect. Diagnostic-only; no assertion or app behavior changes.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog

pullfrog Bot commented Oct 3, 2026

Copy link
Copy Markdown
Contributor

This run hit its time limit while still working, so it stopped before finishing.

Give runs longer with --timeout=2h (or --notimeout) in this repo's instructions — or raise the workflow's timeout input if it sets one, since that input wins. A smaller PR or a lower effort also finishes sooner.

Repo settings → · Flags →

agent run timed out after 1h

Pullfrog  | Rerun failed job ➔ | View workflow run | via Pullfrog | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the new commits. Reviewed the three product fixes and the spec diagnostics; the one open thread from the initial review — the dedicated Docker job's e2e_needs_build gating (config/scripts/ci-e2e-job-selection.mjs:85-90) — is still unaddressed, so this run cannot approve.

Reviewed changes

  • A converted host's managed state now reaches the renderer — sshConnectionStatesEqual also compares managedServer and plainSsh (via JSON.stringify), so a connect that ends on the same status/epoch it set up under no longer gets dropped by the store's dedupe; the function is extracted into ssh-connection-state-equality.ts, both callers are updated, and store-level tests cover the setting-up → managed transition.
  • Port-0 forwards report the port the OS bound — Ssh2PortForwardProvider reads server.address().port after listen, so OrcadManagedTunnelManager.start (which requests port 0 for an OS-assigned tunnel) records a usable localPort for pairing instead of 0.
  • Out-of-scope focus no longer blocks conversion — countUnsupportedSessionState stops counting the source host partition's activeWorktreeId/activeWorkspaceKey when they point at another host's workspace; that scalar is a copy of client-wide focus and is not carried anyway (an in-scope copy is). Covered by a new census test that fails before the change.
  • Conversion poll regains its budget and richer stall diagnostics — the spec's poll timeout returns to 8 * 60_000 (CONVERT_TIMEOUT_MS) and now reports the connect's own result, main's managed state, the cutover journal phase, and both session partitions on timeout; serverCall prints the response in its failure message.
  • Migrated editor tab is host-stamped — the spec calls setActiveWorktree(worktreeId, sshHostId) before openFile, matching a sidebar click, so the tab is attributed to the remote host and carried by the workspace-session migration.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the two commits pushed during the prior review. They are test-only robustness changes; the earlier open thread (the dedicated Docker job's e2e_needs_build gating) remains unaddressed, so this run cannot approve.

Reviewed changes

  • Server calls get a longer budget — serverCall now forwards params and sets timeoutMs: 120_000, because a fresh managed server's first session inventory restores every migrated tab and can outlast the default call timeout. Both the preload bridge (runtime-environments-bridge.ts) and the main IPC handler accept params/timeoutMs.
  • Session-tab assertion is scoped to the converted worktree — the spec calls session.tabs.list with { worktree: 'id:<worktreeId>' } instead of session.tabs.listAll, matching the host RPC method's scoped form (the id: worktree key is the shape mobile clients use).

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The converted-host editor-tab assertion was replaced with a log, so the spec no longer proves the editor-tab migration the PR advertises. The surface it checked (session.tabs.list) is terminal/browser-only and can never contain an editor path, so this reads as a pending server bug but is actually an oracle mismatch.

Reviewed changes

  • Editor-tab assertion became a diagnostic log — tests/e2e/ssh-orcad-auto-convert.spec.ts no longer asserts that the managed server lists the migrated README.md; it logs session.tabs.list for the worktree plus the runtime host partition and continues, "pending the server-side fix". Added the toRuntimeExecutionHostId import for the partition read.

ℹ️ Nitpicks

  • The spec header still says "the new server lists that repository, folder and editor tab", but the editor tab is no longer asserted. Align the header (and the PR body's session.tabs.listAll claim) once the assertion is restored or the gap is tracked.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

Comment on lines +286 to +294
// Not asserted yet: the server lists no migrated editor tab (see the PR); logged for the fix.
console.log(
`[orcad-convert] server tabs ${await serverCall(page, environment!.id, 'session.tabs.list', {
worktree: `id:${remote.worktreeId}`
})} runtime partition ${await page.evaluate(
async (hostId) => JSON.stringify(await window.api.session.get(hostId)),
toRuntimeExecutionHostId(environment!.id)
)}`
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The deleted .toContain(sessionFilePath) targeted session.tabs.list, whose projection carries only terminal and browser tabs — see mobile-session-terminal-projection.ts:11 (RuntimeMobileSessionTerminalTab) and orca-runtime-hydrate-headless-mobile-session-tabs-from-workspace-session.ts:114-128 (snapshot = terminal tabs + browser tabs); the e2e helpers host-session-tabs.ts:24-25 and paired-html-preview-inventory.ts:14-15 state the same. An editor file path therefore could not appear there whether or not the tab migrated, so "the server lists no migrated editor tab" is not evidence of a server-side bug. The log already reads the runtime partition via session.get(toRuntimeExecutionHostId(environment!.id)), so assert the migrated editor row there (or on whatever surface carries openFilesByWorktree) to keep the coverage; if the real plan is to extend the server's session-tab projection to editor tabs, track that follow-up explicitly.

Technical details
# Migrated editor tab is invisible to session.tabs.list

## Affected sites
- `tests/e2e/ssh-orcad-auto-convert.spec.ts:286-294` — assertion replaced by a log.
- `src/main/runtime/mobile-session-terminal-projection.ts:11` — projects only `RuntimeMobileSessionTerminalTab`.
- `src/main/runtime/orca-runtime-hydrate-headless-mobile-session-tabs-from-workspace-session.ts:114-128` — snapshot is terminal tabs + browser tabs only.
- `tests/e2e/helpers/host-session-tabs.ts:24-25`, `tests/e2e/helpers/paired-html-preview-inventory.ts:14-15` — document that `session.tabs.list` carries only terminals.

## Required outcome
- The spec again proves the migrated editor tab reaches the converted host, on a surface that can represent editor tabs, or an explicit tracked follow-up records the missing server capability.

## Suggested approach
- Assert against the runtime host partition already logged, or the destination workspace-session record holding `openFilesByWorktree`.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the new commits. Both are test-only: a stronger retirement assertion and a diagnostics cleanup. The two earlier open threads — the Docker job's e2e_needs_build gating and the editor-tab oracle mismatch — remain unaddressed, so this run cannot approve.

Reviewed changes

  • Retirement is now proven by the dropped source rows — sourceRows(...) counts the relay-era repos/folderWorkspaces by connectionId; the spec asserts {repos:1, folderWorkspaces:1} while the journal is destination-committed, then polls to {repos:0, folderWorkspaces:0} with the journal absent or source-retired once orcad-source-retirement is on.
  • Conversion diagnostics removed — drops the logConversionInputs helper and the session-partition console.log now that the cell passes, and simplifies the retirement poll while keeping the compacted-or-source-retired equivalence.
  • Header aligned with the oracle — the phase-2 comment now states the editor tab is logged, not asserted.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the new commit. The change is test-only; the two earlier open threads remain unaddressed, so this run cannot approve.

Reviewed changes

  • The conversion reconnect fails per step — tests/e2e/ssh-orcad-auto-convert.spec.ts splits reconnect into separate disconnect and connect page.evaluate steps, each raced against a 6-minute timeout. A hung step now throws with main's ssh.getState JSON instead of consuming the whole test budget; a successful disconnect returns '' and the loop proceeds to the connect step, whose resolved managed-server string feeds the poll's diagnostics.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@OrcaWin
OrcaWin force-pushed the OrcaWin/node-rt-p3-convert-host-cells branch from 96c9819 to 8e93409 Compare October 3, 2026 14:26

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta. It extracts the conversion flow into shared e2e helpers and adds a Docker + Windows test for a relay-era profile that converts on its first connect after upgrading. The two earlier open threads — the Docker job's e2e_needs_build gating and the editor-tab oracle mismatch — remain unaddressed, so this run cannot approve.

Reviewed changes

  • Conversion flow extracted into orcad-convert-flow.ts — reconnect, the managed-server poll, the source-row/lease reads, serverCall, and the convert → retain → retire sequence move out of the spec unchanged, so the relay-era cell and the new upgraded-profile cell share one implementation.
  • Relay-era profile seeder orcad-upgrade-profile.ts — seedRelayEraProfile writes a fence-less SSH target (normalizeSshTarget, no orcadFence), its repo, project group, folder workspace, and a host-partitioned editor tab into a stopped profile, then the spec relaunches against it.
  • New upgraded-profile test — launches once to establish the profile, seeds the relay-era rows, relaunches, and runs the same convert-then-retire assertions; it runs on both Docker and Windows, while the runtime relay-era test is now skipped off Docker (the Windows lane hides host Node).
  • Windows trigger paths — ssh-windows-hosts.yml now also fires on orcad-convert-flow.ts and orcad-upgrade-profile.ts.

Verified locally: oxlint and oxfmt --check clean on the four test files; ssh-windows-hosts-workflow, ci-e2e-job-selection, and pr-e2e-gate-contract suites pass (59 tests). The Docker and Windows cells were still running at review time.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this delta. The commit only tightens the upgraded-profile fixture; the two earlier open threads remain unaddressed, so this run cannot approve.

Reviewed changes

  • Seeded relay-era target now matches SshConnectionStore.addTarget — tests/e2e/helpers/orcad-upgrade-profile.ts adds a nextGeneration helper (high-water mark over sshTargetGenerationCounter and the existing targets' generations) and writes configHost, source: 'manual' and that fresh generation through normalizeSshTarget, so the upgraded-profile fixture's target carries the same defaults a real registration would instead of relying on load-time normalization.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in this commit. It is a main-process crash fix plus a diagnostic log; the two earlier open threads — the Docker job's e2e_needs_build gating and the editor-tab oracle mismatch — remain unaddressed, so this run cannot approve.

Reviewed changes

  • A stale ssh2 forward no longer crashes main on 'Not connected' — src/main/ssh/ssh2-port-forward-provider.ts wraps client.forwardOut in a try/catch and destroys the local socket when it throws synchronously after the SSH transport has dropped, mirroring the existing async error path; the new case in ssh-port-forward.test.ts stubs that throw and asserts the server's connection handler neither throws nor leaves the socket open.
  • The upgraded-profile launch forwards its own SSH logs — tests/e2e/ssh-orcad-auto-convert.spec.ts passes an onStderr handler relaying [ssh]-prefixed main-process chunks on the second launch, which bypasses the fixture's log relay.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the new commit. It fixes the Windows active-slot readiness probe; the two earlier open threads — the dedicated Docker job's e2e_needs_build gating and the editor-tab oracle mismatch — remain unaddressed, so this run cannot approve.

Reviewed changes

  • Windows active-slot readiness probe reads through the host script — new readOrcadReadinessNowCommand in orcad-remote-readiness-wait.ts selects the Windows host-script readiness-wait op with no wait vs POSIX head; probeActiveOrcadReadiness and readRemoteOrcadManagedStopTarget now both call it. Previously probeActiveOrcadReadiness called readOrcadReadinessCommand, whose assertPosixHost throws on Windows, so the active-slot probe could never run there; POSIX output is byte-identical to before. A new orcad-remote-windows-commands.test.ts case asserts the second exec is the host-script wait op and would fail before the fix (the throw happened before any exec).

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the new commit. It grants the Windows conversion cell's SSH account the local forwarding the managed tunnel needs. The two earlier threads — the dedicated Docker job's e2e_needs_build gating and the editor-tab oracle mismatch — remain unaddressed, so this run cannot approve.

Reviewed changes

  • Only the convert cell's account may open an SSH local forward — ssh-windows-hosts.yml computes $forwarding as 1 only when the last cell is orcad-convert and passes -ForwardingAccounts $forwarding to the provisioner. prove-preview-openssh.ps1 adds a Match User <convert account> block with AllowTcpForwarding local, overriding the global AllowTcpForwarding no, so the managed server's tunnel can pair while every other cell stays forwarding-disabled.
  • Contract test pins the wiring — ssh-windows-hosts-workflow.test.mjs asserts the workflow emits $forwarding=if($cells[-1] -eq 'orcad-convert'){1}else{0} and passes -ForwardingAccounts $forwarding.

Verified locally: the generated Match User block passes sshd -t, and sshd -T -C user=<last> resolves allowtcpforwarding local for the last account while a non-last account resolves no; Select-Object -Last 0 emits nothing, so -ForwardingAccounts 0 leaves the config unchanged. The forwarding account is accounts[-1] = Cells[-1] (invoke-pinned-relay-cells.ps1:92), matching the workflow's last-cell check, and AllowTcpForwarding local is documented in the OpenSSH 8.1-era man page, so the Windows inbox baseline supports it.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues in the JSON-escaping fix. The two earlier open threads — the dedicated Docker job's e2e_needs_build gating and the editor-tab oracle mismatch — remain unaddressed, so this run cannot approve.

Reviewed changes

  • Server path assertions now match the JSON-escaped form — tests/e2e/helpers/orcad-convert-flow.ts adds jsonText(value) (JSON.stringify(value).slice(1, -1)) and checks the repo.list and folderWorkspace.list responses against jsonText(host.repoPath) / jsonText(host.folderPath). serverCall returns JSON.stringify(response) where response is a parsed RuntimeRpcResponse object, so a Windows path's backslashes arrive doubled in that text and the raw path no longer matched; the escaped form matches, and Linux paths are unchanged.

Pullfrog  | Fix it ➔ | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@OrcaWin
OrcaWin merged commit e567b6f into OrcaWin/node-rt-phase3 Oct 3, 2026
58 of 60 checks passed
OrcaWin added a commit that referenced this pull request Oct 7, 2026
… the relay (#24863)

* Revert "revert: take the 26 Phase 3 (#16741 port) PRs back out of main (#24559)"

This reverts commit 5f308bfa9c796f33e17c9e1cff180a2247450d0a.

* feat(orcad): Windows remote primitives for managed orcad hosts (W1) (#24525)

* feat(orcad): Windows remote primitives for managed orcad hosts (W1)

* refactor(orcad): run Windows host ops as node.exe with plain argv, no PowerShell hop

* fix(orcad): refuse secret-shaped names on the breakaway launcher's --env

* fix(orcad): name the secret env guard for its role

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(orcad): stage and commit a dormant migration catalog on the managed server (#16741 T6-9) (#24521)

* feat(orcad): stage and commit a dormant migration catalog on the managed server (#16741 T6-9)

The destination half of a catalog migration: an orcad stages a T6-7 manifest
(repositories, project groups, folder workspaces, dormant session, client,
automation and worktree metadata, retired names, scrollback snapshots) with
exclusive claims, then commits it with a receipt so a retried commit returns
the same receipt and never imports twice. Served as orcad.migration.* runtime
RPC behind the orcad.migration-catalog.v1 capability; the client refuses a
host without it or with method-not-found, and any other failure is left for
the caller to recheck. Dormant only: no live PTY projection. Inert on the
desktop until T6-10.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(rpc): catalog the orcad.migration params in the shared contract; name the catalog-import install target

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(ssh): journal and fence an SSH host for dormant migration, gated on proven terminal exit (#16741 T8-c1+c2) (#24522)

A migration from a relay-hosted SSH target into a managed orcad now starts with
a journal in its own sidecar directory, then the target's managed-owner fence,
then a profile flush, before any remote call. A fence with no journal is
unverifiable and never released; a journal whose fence is gone is stale and
grants nothing; an unreadable journal fails closed. The fence requires every
terminal the target ever leased to be proven exited, checked before the fence
(with the relay's process list) and again under it. Same-owner claims now need
the durable record that explains them. Inert until T8-c4/T6-10.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(orcad): run orcad itself on Windows hosts (W2) (#24529)

* feat(orcad): run orcad itself on Windows hosts (W2)

* test(orcad): load the ConPTY smoke's addon from out/orcad so the temp slot can be removed

* test(orcad): skip the foreign-uid lock case when running as root

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): a connected but unused SSH host previews as movable (#24609)

The untransferred-dependency census counted activeConnectionIdsAtShutdown naming the
target as workspace-session state. The renderer rewrites that list on every connection
change, so merely connecting to an empty host blocked the move. It is a reconnect hint;
the remote work it can stand for is counted on its own. The empty-target claim check
likewise ignores global-field copies inside the host's session partition.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(ssh): stage, commit and abort a dormant migration against its destination (#16741 T8-c3) (#24523)

* feat(ssh): stage, commit and abort a dormant migration against its destination (#16741 T8-c3)

The coordinator re-checks before every stage and commit that the fenced source
still exports the journaled manifest, carries no untransferable state and
started no terminal. A lost answer is re-read from the destination's catalog
state; only a committed read whose receipt matches the journal advances it,
and the journal is on disk before anything returns. Abort releases the fence
only on proof the destination holds nothing, or on an unsupported destination
before anything was staged, and never once the destination committed. Codes
against T6-9's catalog client through an injected interface. Inert until
T8-c4/T6-10.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* refactor(ssh): import the T6-9 client's unsupported refusal instead of mirroring it

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(orcad): deploy, activate and roll back orcad on Windows SSH hosts (W3) (#24563)

* feat(orcad): deploy, activate and roll back orcad on Windows SSH hosts (W3)

* test(orcad): exhaustive op switch in the Windows lifecycle fake

* test(ssh): narrow the Windows host-cell descriptor by lane before building a relay cell

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(serve): run orca serve on the local orcad slot behind ORCA_SERVE_RUNTIME=orcad (T6-11) (#24608)

* feat(serve): run orca serve on the local orcad slot behind ORCA_SERVE_RUNTIME=orcad (T6-11)

* fix(serve): keep orcad selection app-side and wait out Windows temp cleanup

* refactor(orcad): move the data-root privacy check out of the instance lock

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* test(serve): prove D7 and the profile lock across a real Electron/orcad serve switch (#24619)

* test(serve): prove D7 and the profile lock across a real Electron/orcad serve switch

* ci(e2e): install ripgrep for the serve mode-switch job's window-manager wait

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(ssh): convert an SSH host with Orca state into a managed server through a journaled migration (#16741 T8-c4) (#24562)

* feat(ssh): convert an SSH host with Orca state into a managed server through a journaled migration (#16741 T8-c4)

The conversion entry resumes or takes the fence, deploys and pairs the
managed server into it, marks the server as migrated, then stages and commits
the dormant catalog. Every step is keyed by the journal, so a repeat after a
crash, deferral or lost reply resumes the same migration. Status reports an
unfinished migration, and rollback is refused while one runs or when the
rollback snapshot predates the migrated catalog. Inert until T6-10.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* style: oxfmt the c4 conversion and maintenance files

* fix(ssh): name the fake migration destination's type so declarations stay portable

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(orcad): decommission, managed stop and GC on Windows SSH hosts (W4) (#24570)

* feat(orcad): decommission, managed stop and GC on Windows SSH hosts (W4)

* fix(orcad): accept a managed stop request whose lock path is spelled with Windows client separators

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(ssh): retire a migrated SSH host's source state only after a proven commit (#16741 T8-c5) (#24565)

* feat(ssh): retire a migrated SSH host's source state only after a proven commit (#16741 T8-c5)

Once the journal records destination-committed, the source profile drops the
manifest's repositories, folder workspaces and unreferenced project groups,
its dormant session, automation, client and worktree state, and the leases
the fence proved exited. The profile flushes, the retirement is verified,
the journal moves to source-retired and compacts once the server matches.
A retry after any crash repeats idempotent work. The fenced target stays: it
carries the managed server's tunnel. Conversion now ends retired. Inert until
T6-10.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(orcad): retirement drops the migrated host from the reconnect hint

The census no longer treats activeConnectionIdsAtShutdown as untransferable (#24609), so
retirement must remove the target from it; otherwise a restart dials a host that is now a
managed server.

* style: oxfmt the c5 conversion file

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(orcad): convert Windows relay-hosted SSH targets to managed orcad (W5 part 1) (#24579)

* feat(orcad): convert Windows relay-hosted SSH targets to managed orcad (W5 part 1)

* test(orcad): start the Windows lane's exec spy after the relay gate prelude restores its own

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(settings): managed servers and "Move to managed server", behind an experimental setting (#16741 T6-6 + T6-10 UI) (#24590)

* feat(settings): managed servers and "Move to managed server", behind an experimental setting (#16741 T6-6 + T6-10 UI)

Adds a Managed servers section under Remote servers (deploy an empty server,
status with deferred-update and migration states, update, rollback, recover,
stop and cancel-stop, and SSH access for paired servers), and a Move to managed
server action on connected macOS and Linux SSH hosts with a preflight summary,
a terminals-closed confirmation and a resumable progress view. Main wires the
conversion to the relay's process list, the direct session and the T6-9 catalog
client. Everything is hidden until the new experimental setting is turned on,
and Windows SSH hosts are never offered. Merges the T6-9 branch (#24521) until
it lands on the integration branch.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(settings): align managed-server form controls and name the section the setting reveals

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(orcad): ask the relay with an absolute deadline via the W5 terminal-gate lister

The conversion wiring passed a relative 10 s as listProcesses' deadlineMs, which the
provider reads as an absolute time, so every relay inventory timed out after 1 ms and
the terminal gate could never prove exit. Adopt #24579's lister verbatim so the stacks
merge cleanly.

* feat(settings): name blocking saved state in plain, localized words

The move preview listed internal dependency ids such as workspace-session; each kind
now has its own catalog entry.

* feat(settings): offer managed servers and the move on Windows SSH hosts

W1-W5 are on the integration branch, so a Windows relay-hosted host can deploy, convert
and retire like a POSIX one. The move still waits for a connected relay that reported
its platform.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* test(ci): list the serve mode-switch e2e job in the release-cut permissions matrix (#24865)

* test(ci): list the serve mode-switch e2e job in the release-cut permissions matrix

* test(ci): expect the orcad Windows host cells in the SSH Windows hosts workflow

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* test(orcad): wait for killed terminal daemons to exit before removing their temp profiles (#24871)

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(updater): read rollout kill switches from the update-campaign payload, all inactive (#24867)

The nudge request Orca already polls may now carry an optional versioned rollout block
naming the Node runtime flips. A typed reader resolves each flip with kill-switch, version
range and install-id-bucketed percent semantics, falling back to the baked value (every flip
inactive) when the block is absent, invalid or never read. No consumer reads it yet.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(telemetry): report Windows security-software refusals and unverifiable or failed runtime checks (#24866)

ssh_remote_runtime_resolved dropped the self-test's security_software refusal to 'none' and
sent nothing when a self-test was unverifiable or failed, because those attempts throw before
a rung settles. Add the refusal value, self_test 'unverifiable', and an outcome field
(resolved | unverifiable | failed) deduplicated per host and outcome per session.

Co-authored-by: m4air <m4air@Mac.localdomain>

* refactor(settings): move WorktreeVisibilityDefaults into its own module (#24884)

global-settings-types.ts sits at the 300-line max-lines ceiling; merging main's two new
agent-state-rules settings with experimentalManagedServers put it at 301. The worktree
visibility defaults type moves next to the other visibility types and is re-exported so its
38 importers are unchanged.

Co-authored-by: m4air <m4air@Mac.localdomain>

* refactor(watcher): move the supervisor's child, terminating child and canary into a child slot (#24887)

* refactor(watcher): move the supervisor's child, terminating child and canary into a child slot

* fix(watcher,runtime): take the child slot's child type from the shared wrapper, and stub main's title-display clear in the projection test

---------

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: m4air <m4air@m4airs-Air.localdomain>

* ci(adhoc): build and ship the orcad template in adhoc macOS and Windows builds (#24969)

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>

* feat(orcad): bound the desktop slot cache and prune proven-stopped orcad versions after each managed deploy (#24973)

* feat(orcad): bound the desktop slot cache and prune proven-stopped orcad versions after each managed deploy

* fix(orcad): keep the in-use slot plus the two most recent others, and prove same-version reuse survives eviction

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>

* Phase 3: managed orcad on every SSH connect, with downgrade-safe fence (#24975, #24979)

* feat(orcad): fence managed hosts outside owner and keep converted projects for downgrades

Shipped builds hide any SSH target with an owner, so a downgrade made a converted host and its
projects vanish. The managed fence moves to a new orcadFence field (older builds keep but ignore
it), phase-3 owner fences migrate on load, and managed hosts stay visible but refuse a direct
relay. Conversion now stops at destination-committed with sourceRetainedAt; source retirement
waits on the baked-off orcad-source-retirement rollout flag. This build hides retained rows, and
a start that finds an older build changed them marks the host sourceChangedAt (relay, needs a
new move) instead of merging a second manifest.

* feat(ssh): every SSH host runs managed orcad, decided on each connect (#24979)

* feat(settings): managed servers are no longer experimental

Remove experimentalManagedServers and its gates; the Managed servers section always shows.
Loading drops a stored value, which an older build reads back as its own default (off).

* feat(ssh): every SSH host runs managed orcad, decided on each connect

Before a relay is started, the connect decides the host's server: a converted host connects
through its tunnel (retiring a retained source once orcad-source-retirement is on), an empty
host deploys orcad, and a host with Orca state converts through the journaled migration. A host
with live or unproven relay terminals keeps the relay this session and converts later. A
refusal for any other reason keeps the relay and names the blocker. A host orcad can't run on
(unsupported target, no template, native preflight, runtime self-test) releases any claim or
fence it took, records why with this app version, and keeps the pinned-relay ladder.

Progress and the decision ride an optional SshConnectionState.managedServer field (dropped by
older clients' admission). SSH Hosts shows each host's server status; the manual move dialog
is removed.

* fix(ssh): always await the connect's server decision, after the provider authority rotates

The decision now runs after the old session and transport are torn down and the authority has
rotated synchronously, so concurrent connects still join one attempt; a shutdown that began
during the decision wins over the rotation. The IPC tests use an async double, no sync branch.

* test(renderer): the IPC events store double carries its SSH connection states

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>

* test(ssh): prove connect-time conversion to managed orcad on real Linux and Windows hosts (#24981)

* test(ssh): prove connect-time conversion to managed orcad on real Linux and Windows hosts, and the downgrade view

* fix(e2e): read the SSH host's session partition, provision the convert cell's account, and keep rollout file overrides out of packaged builds

* test(e2e): report the full relay state when the convert poll times out

* test(e2e): require the relay to list no terminals before the converting connect

* test(e2e): require no running-terminal lease before the converting connect

* test(e2e): report the host's terminal leases when the converting connect keeps the relay

* test(e2e): end the relay era with no relay shell left to respawn

* test(e2e): settle before the converting connect and name the tabs a blocking shell belongs to

* test(e2e): use the exited relay tab as the session tab, since any mounted tab starts a shell

* test(e2e): carry an editor tab through the conversion instead of a terminal tab

* test(e2e): log the conversion census inputs before the converting connect

* fix(orcad): log which state blocked a refused conversion

* test(e2e): log both session partitions before the converting connect

* fix(orcad): a source partition's copy of focus on another host no longer blocks conversion

* fix(ssh): an ssh2 forward on port 0 reports the port it bound, so managed tunnels pair

* test(e2e): give the conversion its full budget again

* test(e2e): report the migration journal phase when the conversion stalls

* test(e2e): report the connect's own result and main's state when the conversion stalls

* fix(ssh): a converted host's managed state reaches the renderer instead of staying on 'converting'

* test(e2e): print a failed server call's response

* test(e2e): give server calls the budget a fresh server's first inventory needs

* test(e2e): read the converted worktree's tabs with a scoped session.tabs.list

* test(e2e): log the converted worktree's tabs instead of asserting them, pending the server-side fix

* test(e2e): prove retirement by the dropped source rows; the journal compacts away after it

* test(e2e): drop the conversion diagnostics now the cell passes

* test(e2e): fail a hung disconnect or connect with main's state instead of the whole budget

* test(e2e): convert an upgraded relay-era profile's host on its first connect, on Docker and Windows

* test(e2e): seed the relay-era target the way addTarget registers it

* fix(ssh): a stale ssh2 forward drops a late connection instead of crashing main on 'Not connected'

* fix(orcad): the active-slot readiness probe reads Windows hosts through the host script

* ci(ssh-windows): let only the convert cell's account open the SSH local forward its managed server needs

* test(e2e): match server paths in their JSON-escaped form, for Windows backslashes

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(orcad): move what an older build added to a converted host, or keep the server's version (#24980)

* feat(orcad): move what an older build added to a converted host, or keep the server's version

A host an older build changed stays on the relay with two actions. 'Move the new projects' runs a
fresh journaled conversion of only what the host's earlier migrations didn't move: the source is
viewed with those migrations retired from it, so nothing that overlaps the server is merged, and
a row the server already holds fails the whole move at stage, before any commit. Its journal
supersedes the chain head; retained-source checks compare against its baseline, and retirement
retires every manifest in the chain only after the newest committed. 'Keep the server's version'
records the current source as the baseline and returns the host to its managed server.

* test(ipc): the runtime environment handler contract lists the delta-move channels

* test(native-chat): snapshot the journal directory after the attach's restart-offer lock is released

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): converted hosts keep their editor tabs, forwarding-refusing hosts stay on the relay, listAll settles (#25099)

* fix(orcad): publish the headless graph so session.tabs.listAll settles instead of hanging

* fix(ssh): a system SSH forward on port 0 picks a free port first and reports it

* fix(runtime): a headless host lists and closes the editor tabs its session holds, so migrated editors reach clients

* fix(ssh): keep a host that refuses TCP forwarding on the relay, and release a conversion it stranded

* test(e2e): assert the migrated editor tab and a settled listAll, and keep a forwarding-refusing host on the relay

* fix: restore the journal import after rebase, type the probe's failure code, and update headless-graph test seams

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): client focus and 'local'-stamped tabs no longer refuse a host's move to its managed server (#25110)

* fix(orcad): client focus and 'local'-stamped tabs no longer refuse a host's move to its managed server

* fix(orcad): a v1.4.218 profile focused on the SSH worktree converts, and a refusal names what blocks it

The debounced session writer never patched activeWorkspaceKey or activeWorkspaceExecutionHostId,
so the first focus stayed on disk. The all-dependency census also counted global focus copies in
every non-source partition.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(ssh): offer to move a host whose open terminals keep it on the relay (#25097)

A host with any open SSH terminal never converted: the connect gate keeps the relay while relay
terminals are live, and an open tab respawns them on every connect. The first such connect per
host per app version now marks the relay status with offerMove (recorded as
managedServerMoveOffered), which toasts "Move <host> ... Its N open terminals will restart." The
SSH Hosts status line keeps a "Move to managed server" action while terminals are live.

Confirming runs ssh:moveToManagedServer: stop the host's relay terminals through the extracted
ssh:terminateSessions path, re-run the connect gate's terminal census, refuse on anything but
exited, then reconnect so the connect-time decision runs the journaled conversion.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(orcad): reach managed orcad over an SSH stdio bridge where sshd refuses port forwarding (#25120)

* feat(orcad): reach managed orcad over an SSH stdio bridge where sshd refuses port forwarding

Hosts with AllowTcpForwarding no were kept on the relay. The managed tunnel now
probes forwarding each time it starts and, on refusal, serves the same local port
through a second provider: each accepted socket opens one SSH exec channel running
a small bridge on the host's pinned Node, which dials orcad's loopback port.
POSIX hosts run it with node -e; Windows hosts run it as the content-addressed
host script's stdio-bridge op with base64 line framing. Bridges are capped at 8
per connection under sshd's MaxSessions default, and a lost channel only drops its
socket. Only a host where even the bridge cannot run keeps the relay, recorded as
ssh_tunnel_unavailable; the older tcp_forwarding_refused record is retried.

* test(e2e): prove the stdio bridge by refused forwarding plus a working call

* test(e2e): connect the refused-forwarding host without the relay-only repo step

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(ssh): report each connect's server decision, and show orcad.log's last lines when setup fails (#25118)

* telemetry: one enum-only event per connect decision (outcome, reason, tunnel transport, host platform, duration), plus conversion start/commit/fail, deploy failures, and the per-host move offer and its result
* errors: deploy, launch and rollback failures carry the last 40 redacted lines of the host's orcad.log, read over SSH (Windows through the node.exe host script)
* SSH Hosts: a deferred or failed setup offers its reason, log tail included, under Details

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(startup): Windows never crashes resolving userData when roaming AppData is unavailable (#25113)

* fix(startup): pin Windows appData and userData before anything resolves them

A Windows session without a loaded profile (e.g. orca serve over SSH) can fail the
roaming AppData known-folder lookup. Electron 43 then falls through to Chromium's
userData provider and crashes natively. Resolve appData first (falling back to
APPDATA, then USERPROFILE\AppData\Roaming), and set userData explicitly so
Electron's provider never runs.

* test(startup): remove the AppData fixture through the retrying helper

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): keep a terminal the previous Orca version's relay still runs instead of replacing it (#25124)

After an app update the new relay answers "not found" for a PTY the previous build's relay still
runs, because the old relay refuses this build's handshake. The client read that as absence: it
expired the lease and the pane cold-restored into an empty shell while the user's shell kept
running, unreachable. Each deploy now takes a census of this target's older relay endpoints; while
one is live or unverifiable, a not-found reattach keeps the lease and the pane binding, and the
pane says the terminal is still running under the previous Orca version. A detached lease also
keeps blocking managed-server conversion until that terminal exits.

The cross-version harness now extracts src/relay, and a new test drives v1.4.218's relay socket and
grace lifecycle with this build's endpoint probe: the probe leaves no grace deadline and reads the
old relay as live work.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(orcad): update a managed server on connect when it runs an older build and is idle (#25122)

* feat(orcad): update a managed server on connect when it runs an older build and is idle

A connect to a managed SSH host now runs the Managed servers update when the host's
orcad differs from this app's bundled build, the template carries the host's target,
and the update planner finds no live or uncounted terminals. A rejected candidate is
restored through the activation journal; the reason is recorded per app version so
later connects don't retry it. A host a newer Orca activated is never downgraded:
the activation record now names the app version behind each build, and an explicit
rollback holds the build it left.

* test(e2e): connect without a racing disconnect after relaunch, and report each attempt

On launch the app already reaches the managed server through its tunnel; a disconnect racing that
restore cancelled the connect that runs the update.

* feat(orcad): run the update check when the launch restores a managed server's tunnel

An auto-restored host may never see an SSH connect, so its server would never update. The tunnel
restore now runs the same check, once per server per session and off the caller's path, through
the shared update-check module; a server mid-migration is left alone.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): retry the pinned Node download through transient network failures (#25154)

A Chromium network change (net::ERR_NETWORK_CHANGED) during the pinned Node
download failed the deploy and sent the host back to the relay. The archive
download now retries up to three more times, after 1s, 3s and 9s, on dropped
connections, timeouts, stalls and retryable HTTP statuses, removing the partial
file first; checksum mismatches, other HTTP errors and cancels stay final. The
transient-error classifier moves from the speech download to
src/main/network/transient-download-error.ts so both share it.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(serve): run orca serve on orcad by default, with ORCA_SERVE_RUNTIME=electron as the opt-out (#24972)

* feat(serve): run orca serve on orcad by default, with ORCA_SERVE_RUNTIME=electron as the opt-out

* test(serve): read Electron serve's pretty-printed readiness in the CLI mode-switch e2e

* feat(serve): gate D7 on Windows and serve on orcad there by default

* test(serve): take the profile lock in the CLI mode-switch e2e, and keep Windows profile logs on failure

* test(serve): tell Electron and orcad serve apart by readiness health, and trace Windows startup

* ci(e2e): dump Electron's native log and stack on the Windows serve mode-switch job

* fix(serve): keep Windows on Electron serve until it can adopt orcad's daemon

The Windows D7 job shows Electron serve exiting before its window when it relaunches
onto a terminal daemon orcad forked. Restore the win32 fallback and skip that case
there as a known gap; the follow-up PR fixes it and re-flips Windows.

* fix(serve): let ORCA_SERVE_RUNTIME=orcad opt in on Windows while Electron stays the default

* test(serve): skip the Windows D7 cases where orcad forks the daemon, and stop cleanup hiding a failed relaunch

Test 3 hits the same Windows gap as test 2: orcad forks its own daemon there, and Electron
crashes at startup beside it. A failed relaunch also made dispose close the old, already
closed app, whose throw replaced the launch error.

* test(serve): run every Windows D7 case, with the isolated home's AppData in place

Electron 43 crashes natively (0xFFFF7003) when it resolves userData and Windows cannot find
roaming AppData. The e2e home isolation points USERPROFILE at a fresh folder with no AppData,
so later launches hit that. The harness now creates it, every D7 case runs on Windows again,
and a new case proves Electron serve starts beside another profile's live orcad daemon.

* test(serve): retry removing a Windows e2e profile while a killed daemon releases it

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(ssh): resume a terminal the previous Orca version's relay still runs, through that relay's own bridge (#25170)

* feat(ssh): resume a terminal the previous Orca version's relay still runs, through that relay's own bridge

After an app update the previous relay keeps the user's shells alive but refuses this build's
handshake. Its own relay.js --connect, run from its own version directory, presents its own bundle
hash, so on POSIX hosts the client now reaches it that way: a pane whose reattach the current relay
held for an older relay opens a route through the old bridge, takes the PTY owner role without
output flow control, reattaches the PTY with its replay, and routes every later operation on that
id to the old relay. When the last pane a route serves exits, the route hangs up and the old relay's
own idle grace retires it. Windows hosts, relocated short sockets and unreachable bridges keep the
held-pane behaviour.

The cross-version harness now builds v1.4.218's relay from its tagged sources and runs it as the
real detached daemon: a shipped client leaves a shell in it, this build resumes the pane through the
old bridge, types into it, sees its output, and watches the old relay exit on its own after the
shell does.

* fix(ssh): read the legacy relay router through its instance

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): a host re-upgraded after a downgrade can move what the older build added (#25179)

The delta view left the moved projects' session state in place whenever it held anything
unmovable, so it then counted against the delta. Relay PTY bindings, shutdown markers and the
relay consumer's recovery record blocked every move although the terminal gate already proves
those terminals exited before any move commits; the move now drops them instead.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(worktree-ps): report a lost-contact host's terminals as unverifiable, not live:0 pty:no (#25167)

During a network drop to a relay-served SSH host, every PTY on it reads as an
unconfirmed exit, so worktree ps skipped them and printed live:0 pty:no for a
terminal that was still running. The listing now counts terminals whose liveness
verdict is unverifiable into a new optional unverifiableTerminalCount, and the CLI
prints live:unverifiable pty:unverifiable (JSON carries the same word) instead of
zero. A host-confirmed exit still reads as zero.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(terminal): a held pane shows no client OS or shell, and the boundary doc says POSIX hosts resume it (#25194)

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): tunnel to the port managed orcad bound and verify it is ours (#25182)

When another Orca already listens on 6768, orcad binds a different port. The
tunnel kept forwarding to 6768, reached the other runtime, was rejected with
4001, and the connect still reported a managed server.

The tunnel now reads orcad's bound port from its active slot's readiness
(falling back to the persisted port for slots without one) and, after the
forward is up, proves the server answering is the paired runtime. On a
mismatch it re-reads the port once and fails with orcad_identity_mismatch
instead of reporting managed.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): a converted host shows only its managed server's rows, and its old editor tabs load there (#25193)

Retained relay-era project groups and the per-host SSH catalog now hide like repos and folders.
On the managed transition the renderer reloads server names, groups, folders and worktrees, then
drops the host's relay-era rows a local refresh would keep. A restored tab with no host stamp in
a workspace now owned by a managed server takes that server as owner, in place.

Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Claude <noreply@anthropic.com>

* fix(orcad): ship the port-scan worker so managed servers detect workspace ports (#25197)

orcad resolves port-scan-command-worker-entry.js beside orcad.js, but the orcad
build never emitted it and ORCAD_ARTIFACTS never listed it, so every managed
server logged 'probe worker unavailable' and had no port detection. The build now
emits it with the other children, and the artifact list carries it, so it is
uploaded, hashed and covered by the template contract.

A new test bundles orcad and its children and fails when the bundle names a
worker or child entry the slot does not ship. Two existing gaps it found,
session-scanner-service-entry.js and wsl-transcript-fs-process-entry.js, are
listed as known and may only shrink.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): keep retrying when a reconnect loses the socket before the SSH banner (#25195)

After a network outage, a port forwarder or NAT can accept the TCP
connection and then close it before the server sends its banner. ssh2
reports that as 'Connection lost before handshake' with no errno, so the
reconnect ladder classified it as permanent and published 'error' with
no retry scheduled. Treat it as recoverable on the bounded ladder only;
the initial connect keeps its narrow classifier.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(serve): serve on orcad by default on Windows too (#25162)

D7 now runs every case on Windows (orcad-serve-mode-switch-windows), including Electron
serve adopting a daemon orcad forked, so Windows no longer needs the Electron default.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): the terminal gate asks the relays before treating a detached terminal as running (#25200)

* fix(orcad): the terminal gate asks the relays before treating a detached terminal as running, and retiring a host drops its relay recovery record

* fix(ssh): earlier-relay census gaps and an unanswered relay stay unverifiable; asking the terminal gate changes nothing

- the gate is read-only; the conversion and delta move retire proven detached leases themselves
- an expired lease also needs every earlier-build relay to answer before it reads as exited
- a failed, input-less or truncated census marks its older relays unverifiable, so reattach holds
- a legacy relay route closes only when no attach or listing still awaits it
- a disposed session forgets the census it started

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(migration): recover interrupted conversions and delta moves, and trim dead migration code (#25226)

* refactor(migration): drop the unused full dependency census

* refactor(migration): one table for the routed UI fields a migration carries

* refactor(migration): share the session focus field list and fix stale claim comments

* fix(session): keep a fenced SSH host's source session partition intact through renderer saves

The renderer cannot see a fenced host's repos or folders, so hydration drops their
worktree rows; a save that still routes any row to ssh:<target> (a folder workspace
key stays valid) rewrote that partition without them. That changes the migration's
source manifest and loses the session a downgraded build reads back. Main now
ignores renderer writes to a fenced, unchanged host's partition.

* fix(migration): resume or back out unfinished conversions and delta moves, serialize delta moves, clean stale journals

- On connect, a registered server whose conversion never committed resumes the commit; a failure aborts it through the destination, unregisters the server and releases the fence.
- An interrupted delta move keeps its journal and gets its changed mark back; the next move resumes it from the journaled manifest or aborts it when the source has moved on.
- Delta moves run under the target lifecycle queue and re-check the head, so two concurrent moves cannot write two journal heads.
- Delta checks re-read the live source instead of a frozen copy.
- Stale journals from a stopped or never-registered server are cleared.
- Retiring a chain goes newest first and compacts only at the end.
- The journal schema tolerates fields a newer build adds.

* fix(migration): hide source rows only after commit, and retire what an older build added to a moved project

- Source rows and the renderer session guard share one rule: hidden once the server committed, shown while a migration is still in flight.
- A delta move a crash interrupted gets its changed mark back on the next start.
- Retirement removes worktree metadata and automations by moved-project scope, so an older build's additions no longer fail the leftover check forever; that check ignores rows of other migrations in the chain.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): survive a slow first daemon start and close review gaps (#25213)

- Give the terminal daemon 30 s to start on Windows and retry the spawn once before
  falling back, so a cold first deploy is not refused as daemonless.
- Say in the activation refusal that orcad.log holds the daemon's error and that the
  next connect retries.
- Managed stop completion now waits out daemon retirement plus the shutdown deadline.
- Publish the instance lock atomically and reclaim an abandoned torn lock.
- Fix the stop listener closing before it was defined on an already-present request.
- `orca serve`'s cache prune keeps the slot a running local orcad uses.
- A timed-out daemon retirement reopens admission once it is refused; a retirement that
  may have reached the daemon stays fenced.
- A headless host keeps an editor tab's unsaved draft unless the close is forced.
- Unverifiable terminals are attributed by the host's PTY record, like live ones.
- An explicit --user-data-dir is no longer overridden on Windows.
- Read Windows daemon process identity from the process table, not PowerShell.
- Remove the unread data-event incarnationId and daemon health runtime fields, share
  one process-alive and error-code check, and fold the websocket limits file back.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(cli): stop, update, roll back and recover managed Orca servers from the CLI (#25201)

* feat(cli): stop, update, roll back and recover managed Orca servers from the CLI

orca environment status|update|rollback|recover|stop|cancel-stop call the same managed-server
actions as Settings > Managed servers, over new managedServer.* runtime RPC methods. The desktop
main process registers those actions; the runtime advertises managedServer.v1 only then, and the
CLI refuses on a runtime without it or one that answers method_not_found. stop requires --yes.

* fix(ipc): keep a missing managed-server selector a rejection, not a synchronous throw

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): Move to managed server converts the host it is offered for (#25196)

The move stops the relay terminals and passes the terminal census; with #25179 the conversion no
longer refuses on the stopped terminal's saved tab, layout leaf and pane incarnation. A new test
drives one live relay terminal through Move to a conversion whose manifest carries the tab without
its relay PTY, so it spawns a fresh shell on orcad.

ssh:terminateSessions also no longer records a not-found shutdown as terminated while an older
Orca build's relay may still run the PTY (#25124): that terminal is reported unverifiable and its
lease kept, so the move refuses instead of converting over a running shell.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): deploy the glibc 2.17 runtime on old-glibc hosts, and fall back to the pinned relay (#25180)

* fix(orcad): deploy the glibc 2.17 runtime on old-glibc hosts, and fall back to the pinned relay

Managed orcad picked its runtime from the host's libc flavour alone, so a CentOS 7 host
(glibc 2.17) got the default linux-x64-glibc Node, whose self-test fails there. The deploy
now picks the runtime by glibc the same way the relay ladder picks rung A or B, and the
host-side slot checks accept the compat target it ships.

When orcad still can't run, the host is recorded as such and the relay connect that
follows now runs the pinned-runtime ladder instead of defaulting to the host-Node relay,
so a host with no Node lands on rung B rather than failing.

The hostile-host lane deploys managed orcad on a fresh CentOS 7 host and asserts it runs on
the compat runtime with no default runtime uploaded.

* test(ssh): CentOS 7 cell asserts the compat runtime pick, then the refusal and relay rung B fallback

The compat template still ships the base @parcel/watcher binary, which needs GLIBCXX_3.4.20; CentOS 7's
libstdc++ stops at 3.4.19, so the candidate's preflight refuses. The cell now pins that end-to-end
behaviour: compat runtime picked and uploaded alone, refusal classified native_preflight, and the relay
that follows settles on rung B with no runtime setting.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* test(serve): prove D7 on an installed Windows app whose daemon runs from the relocated daemon-host (#24976)

* test(serve): prove D7 on an installed Windows app whose daemon runs from the relocated daemon-host

* test(serve): read the pre-switch scrollback best-effort and wait for it after reattach

* test(serve): opt the packaged Windows serve switch into orcad explicitly

---------

Co-authored-by: m4air <m4air@m4airs-Air.localdomain>
Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(migration): a retirement that fails after deleting rows resumes instead of reading as changed (#25234)

The chain head records sourceRetiringAt before any row is retired; the startup change check and the chain's own comparison skip a head that carries it.

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(orcad): managed orcad idles out after 15 minutes and is started again whenever it is down (#25121)

* feat(orcad): a managed orcad stops after 15 idle minutes and starts again on the next connect

A client-launched orcad now exits, like the relay, once no client, terminal,
working agent, staged migration or activation fence has been seen for 15
minutes. The exit is the normal graceful shutdown, which leaves the terminal
daemon running; the daemon retires only if it proves itself empty. A record in
the data root tells the next start (and its readiness health) that the stop
was an idle one rather than a crash.

On connect and after host resume, a fresh tunnel whose server does not answer
starts the activated slot under the activation fence, but only on a proven
exit, so a stopped server reads as not running rather than a failure.

* fix(orcad): keep orcad-entry under max-lines; idle e2e connects without a relay repo

* fix(orcad): deploy and rollback launches carry the managed idle-exit fence

The candidate launch in activation and the rollback launch built their
own launch spec without the activation root, so a freshly deployed orcad
never enabled idle exit; only the wake path did. The field is now required
on every launch spec, so the type system covers each launch site.

* feat(ssh): start a stopped managed orcad on connect, on restore and after resume

Once orcad stopped (idle, kill or host reboot), a connect still resolved
managed over a forward to a dead port and every call failed. Every connect
now checks the server behind its tunnel, as does a call through a restored
environment; a server proven stopped is started from its activated slot
under the activation fence, adopting a surviving daemon and its terminals.
The status line shows the start, and a start that fails keeps the host
managed with the reason and orcad.log's tail, never as a terminal verdict.

* fix(ssh): reuse a serving verdict only on the same SSH transport, for 5s

A reconnect right after a reboot was answered from the previous
transport's cached verdict, so the stopped server was never started.

* fix(ssh): key the serving verdict on the tunnel's remote port too

* test(ssh): a stopped server starts before the update counts its terminals

* feat(ssh): check serving at the bound port, and follow a restarted orcad to a new one

The serving check uses the port the tunnel forwards to (the one orcad bound).
A restart that binds a different port drops the forward and rebuilds it at
the new port, within the same ensure or on the explicit connect check. The
tunnel manager class moves to its own file to stay under max-lines.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a connect with no leases asks the host's relay endpoints before converting (#25223)

* fix(ssh): a connect with no leases and no relay session asks the host's relay endpoints before converting

* refactor(ssh): one isLiveSshPtyLease for every lease-liveness check

* fix(ssh): the host relay census asks a relay its PTYs before reading it as live work

An accepting relay whose holders or children the probe could not read (no lsof, an unrecognised
service child) read as live work, so a relay-era host that had exited its last shell never
converted. The relay's own bridge answers pty.listProcesses without the owner role.

* fix(ssh): the host relay census runs each relay's probe and bridge on the runtime it runs on

Pinned-ladder hosts often have no Node on PATH, so a PATH Node read every relay as unverifiable.
Each daemon's own argv names its pinned runtime, or the host Node a legacy relay started with.

* fix(ssh): an older relay's bridge runs on that relay's own runtime

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* feat(orcad): build @parcel/watcher into the glibc 2.17 compat slot, so CentOS 7 runs managed orcad (#25199)

The compat target swapped in only node-pty, so it shipped the base target's upstream
watcher.node, which needs GLIBCXX_3.4.20; CentOS 7 stops at 3.4.19. orcad's preflight
refused it, and relay rung B lost file watching without saying so.

The compat slot now compiles @parcel/watcher from the package's own sources against the
pinned headers with the C++ runtime static, like node-pty. The slot gates (glibc 2.17
symbol floor, no shared libstdc++, N-API 8) and the smoke load cover it, the template
stages it into the compat target, and both orcad and relay rung B pick it up from there.

COMPAT_SLOT_ADDONS names a compat slot's own addons: a compat slot missing one fails
--require-slots, and a compat template target that would ship any native file without a
compat build fails the template build. The CentOS 7 cell expects an activated managed
server again, and every launched relay cell loads its watcher directly.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(phase3): close e2e routing holes, localize managed-server outcomes, trim harness and CI (#25206)

* fix(phase3): close e2e routing holes, localize managed-server outcomes, trim harness and CI

- Route the auto-convert spec into needs_build and route the new orcad/serve
  e2e helpers to the specs that use them, with routing tests.
- Run the auto-convert Docker lane only when routed; fold the missing-AppData
  check into the Windows mode-switch job and drop crash-hunt diagnostic env.
- Delta-move dialog keys its preview on the target id and cannot close or
  resubmit mid-move; Resume has an in-flight guard.
- Managed-server toasts and status lines show localized messages instead of
  raw codes or main-process English; add singular and zero-count wording.
- Settings style fixes (quiet Cancel, section header, labelled fields,
  progress labels); delete dead i18n keys and the unused previewConversion.
- Harness: kill the serve child on readiness timeout, share the isolated
  profile and spawn-until-ready helpers, hooks and a condition wait in the
  auto-convert spec, shared cross-version exec helper.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(phase3): localize the refusal toast, share serve readiness and liveness helpers, correct D7 docs

- The refusal toast no longer shows main's English blocker detail; the SSH
  Hosts status line keeps it under Details. A missing terminal count is no
  longer defaulted to 0.
- startOrcadServe uses spawnUntilReady, so a readiness timeout kills orcad;
  one isPidAlive replaces the spec's copy and the lock-holder loop.
- The port-6768 auto-convert test uses the shared hooks.
- The docs say the Windows D7 job checks rather than gates.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ci): give the idle-exit spec its app build and route the convert harness to it

Co-authored-by: m4air <m4air@Mac.localdomain>

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* ci(adhoc): pin every job of an adhoc build to the commit resolved at dispatch (#25311)

Each job read the requested branch name and checked out whatever it pointed at when that
job started. A push mid-run mixed commits: in run 37234548638 the glibc217 slot lane
built 2ddea8736e, then #25199 merged, and desktop_template checked out 70948d597f, whose
merge step requires the compat watcher that lane never built.

A first resolve-ref job (no secrets) resolves the branch, tag or full SHA once, and every
job checks out that commit. The mac job still vets it for reachability before signing;
the requested name stays the concurrency key and the release label.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(terminal): report a confirmed relay PTY exit as exited and give a cold Windows PTY probe more time (#25304)

A worktree terminal close returned as soon as the relay confirmed the stop, but the
exit frame reaches the runtime record only after the SSH output intake drains, so the
verdict read straight after saw a still-connected PTY and answered unverifiable. The
stop now waits, bounded by its deadline or 10 s, for that record.

The bundled runtime's PTY probe gives the first Windows spawn 15 s and retries once
after a timeout only; spawn errors and non-zero exits still fail at once.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(relay,orcad): keep new wire messages forward compatible, drop unshipped relay.reset, fix shutdown and serve gaps (#25298)

* fix(orcad-migration): keep migration wire forward compatible with newer peers

* docs(pty): record why pty.resumeClient negotiates by method-not-found

* fix(relay): remove the unused relay.reset method and keep a deferred shutdown serving

* refactor: drop dead hold API, release gate and migration pass-throughs; fix serve and delegation gaps

* fix(lint): keep reopen hooks within file size limits

* revert type dedupe in orcad-incumbent-recovery to avoid a parallel conflict

* fix(lint): name stop-reply request fields for their role

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* refactor(ssh): one managed-tunnel ownership check and forward bookkeeping (#25316)

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a stuck managed server can recover, cancels finish their change, unknown stays unknown; delete unused reset/drain code (#25227)

* refactor(ssh): delete the unused connection reset and drain machinery

* fix(ssh): surface a stuck managed server, finish activations past their first change, and fall back from Windows launch refusals

- A rejected build that changed profile state now refuses as orcad_recovery_changed_state
  (unverifiable) with a Recover path that restores the snapshot once the operator accepts;
  an interrupted activation is no longer reported as a quiet update deferral.
- Activation and rollback drop the abort signal after their first journaled change.
- Windows launch refusals and unsafe command lines send the host back to the relay.
- Fence refusals keep unverifiable blockers unverifiable; the POSIX liveness probe reads
  kill errors in the C locale and treats permission errors as unknown.
- A host with no relay fallback surfaces its real connect error; disconnect and removal
  clear the setting-up status, and a cancelled decision's progress is dropped.
- Remove tcp_forwarding_refused leftovers, dedupe incumbent stop/restore, exec-or-empty,
  census client, errorMessage and the blocking-blocker predicate; split activation and
  snapshot files under 300 lines.

* fix(ssh): one import per module in the rollback transition

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(migration): committed reads trust the receipt, conversions survive UI churn, journals are cached, and round-2 low items (#25327)

* fix(ssh): keep a newer build's fence and note fields, and validate a fence beside a legacy owner

Normalization now validates known fields but passes unknown ones through on orcadFence and the managed-server notes. A legacy managed owner next to a malformed fence falls back to the owner's environment id instead of keeping the malformed fence.

* fix(migration): delete a retired migration's scrollback files once retirement is durable

Retirement dropped the moved terminals' scrollback refs from session state but kept the files forever. After the journal records source-retired, the files the manifest names are deleted, except refs any session partition or pending export still names. A crash before the delete repeats it on the next retirement pass.

* fix(orcad): a committed migration reads as committed from its receipt, survives receipt eviction, and abandoned stages expire

- Committed-state reads trusted only a byte-identical copy of every moved row and snapshot, so a live server that had been used could never confirm its own commit. The receipt alone now proves it; full equality stays inside the commit.
- A receipt that ages out of the 64-entry list keeps a compact record, so the commit never reads as absent.
- A stage no client returns to within a week stops holding the server awake and is dropped at the next stage.

* fix(migration): freeze a host's session from the fence on, ignore UI churn in the frozen-source check, and cache parsed journals

- Renderer writes to a fenced host's ssh: partition are skipped from fence time, not only after commit, so tab work mid-conversion cannot change the source.
- The frozen-source check leaves out workspace session and client routing state, which the UI rewrites as the user works (including through the local partition and UI state); the server gets them as journaled.
- Parsed journals are cached per directory, keyed by each file's inode, size and mtime and dropped on every write and remove, so list and session calls stop re-parsing every manifest.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): an older relay a census cannot rule out stays held, and a reconnect resumes held PTYs through it (#25331)

- A census that does not know the host platform is unverifiable, never "no older relay". On Windows
  it now probes each older version directory's pipe for the target, the way relay GC does, so a
  live older Windows relay keeps its PTYs held instead of respawning their panes; those endpoints
  are held, never bridged.
- On reconnect, a PTY the current relay disowned while an older relay holds it is reattached through
  that relay's own bridge, with its runtime restored and its replay forwarded. When no route serves
  it, it is left for recovery like an exhausted reattach.
- A superseded or disposed deploy no longer starts the census, so it cannot replace the current
  attempt's.
- A route stops holding an unserved PTY that exits.
- Drop the unused censusPreviousRelays export.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(phase3): round-2 ui-infra review fixes (#25325)

- An unverifiable move refusal with no count says so instead of "0 terminals".
- One move per host: the dialog stays mounted while open, joins a run already
  in flight on remount, and the toast shares the same guard.
- A managed server's start, wake or update no longer reloads every host's
  catalog; only a new environment for the host loads, scoped to that host
  and the local catalog.
- A failed host-partition session write is no longer acknowledged as written,
  so the writer re-queues those fields.
- The deploy picker lists only hosts with no managed server or pending move.
- Harness: orcad and the released relay daemon are stopped when startup fails.
- Windows host CI: the convert cell always runs last and fails on a failed
  native switch or build; ssh-host-server unit tests no longer trigger it.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(relay,cli): never report an unreachable terminal as exited; keep PTYs through a deferred shutdown (#25328)

* fix(relay,cli): never read an unreachable relay or terminal as exited; keep PTYs on a deferred shutdown

* fix: stop an unrecorded breakaway child, route orcad serve through the spawn chokepoint, and drop review-flagged leftovers

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): reclaim a crashed desktop lock, close the stop-cancel race, drop dead daemon and RPC code (#25338)

* fix(orcad): reclaim a crashed desktop lock, close the stop-cancel race, drop dead daemon and RPC code

- A desktop reclaims a stale desktop lock record (Electron's lock proves it), and a real
  orcad hold shows a dialog instead of exiting silently.
- A failing quit handler no longer skips closing observability.
- Completion withdraws its request when a cancel lands mid-write; the listener removes a
  cancelled leftover.
- An idle stop's clean record is retracted when the shutdown fails or overruns.
- The managed-stop request tolerates unknown fields from a newer client.
- Shared error-code checks, one win32 coverage rule, no redundant isAlive filters.
- Remove the recovery-only daemon provider, requirePinnedWsPort/strictPort and the
  superseded orcad.migration.importCatalog RPC.

* fix(startup): keep main-process-preflight under the line cap

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): fail fast on a held fence, restore untouched rollbacks, atomic Windows host script, honest tunnel ensure (#25332)

* fix(ssh): fail fast on a held activation fence, restore a rollback the target never touched, stage the Windows host script atomically

- withOrcadActivationLock no longer waits up to 15 minutes inside the target lifecycle: a held
  fence answers at once (orcad_activation_recovery_required); deploy probes it before uploading.
- A rejected rollback target that left the restored snapshot untouched puts the newer build back
  unasked; only a real change waits for the operator. Crash recovery applies the same rule.
- The Windows host script is written only when missing, through a partial file and a rename; a
  bridge exit without a sentinel is unverifiable unless the shell could not find the command.
- The managed tunnel's ensure() throws when superseded, and a caller arriving after close()
  builds a fresh run instead of joining the doomed one.
- An unparseable stop answer after a stop was sent keeps the fence (new 'unconfirmed' outcome).
- stopRemote starts over instead of reporting live when another run settled the journal.
- Releasing an unreachable setup stops the orcad it activated and clears active on proven exit.
- A failed startup is torn down quietly so its error stays published; port-forward listeners
  keep an error handler; a linked SSH access connect is cancelled when its window closes.
- Shared errorMessage, one SFTP transfer helper, getConnectGeneration only, test-cell names.

* fix(ssh): stage the Windows host script through the pinned node.exe, which cmd.exe and PowerShell both run

* fix(ssh): host-script staging runs as host-script ops; a joiner of an overtaken tunnel run builds its own

- The presence check and the install are ops in the uploaded host script (script-present, and
  script-install run from the partial upload itself), invoked as node.exe <script> <op> <args>
  like every other Windows host op: no inline code.
- ensure() callers that joined an in-flight run no longer inherit its 'superseded' end; they
  start a fresh run, which also covers a close() in between.

---------

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a connect re-checks unverifiable relay terminals once its relay session can answer (#25385)

The server decision runs before any relay session exists, so a terminal this desktop left
detached could never be asked about and read unverifiable for as long as it ran. On Windows no
endpoint census can fill that gap. Once the session is up, the relay that holds the PTY answers
and a terminal it still runs is reported live.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(settings): a managed server whose status never loaded reads unknown, not "Not running" (#25389)

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(relay,runtime-env,cli): keep a deferred relay's AI Vault and skill uploads, make managed re-pair crash-safe, show unverifiable in worktree ps text (#25393)

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(orcad): keep a surviving daemon's slot through the cache prune, and drop an idle record a signal stop took over (#25387)

- The slot prune also protects any slot a live terminal daemon's PID record points into, and
  evicts nothing while a daemon record is unreadable.
- The shutdown trigger reports whether it took ownership; an idle stop that another source
  took over discards its clean-idle record.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): edit a managed host's connection, show rows no migration owns, drop the unused quit-drain predicate (#25403)

- SSH settings may edit a managed host's connection fields; its fence and generation are never written from the renderer, and its tunnel is closed so the next use redials. Removing it is refused with a pointer to Stop under Managed servers, which stops and removes the server; the renderer no longer ends the host's terminals before that refusal.
- A fenced host with no journal (an empty host's deploy, or one whose journal compacted after retirement) no longer hides its rows: no committed move owns them, so projects an older build added stay visible. An unreadable journal still hides them.
- The quit drain's mayDetach predicate and disconnectAll's filter had no production caller and are removed.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): input to a terminal an older relay holds but no route serves is refused, not dropped (#25404)

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): refresh a changed host's status once its delta move or keep-server choice lands (#25416)

The status line kept saying the host was changed on an older Orca until a manual reconnect. Main now publishes the host as managed by its server after a successful move or keep, with a disconnected state when the move released the relay session.

Co-authored-by: m4air <m4air@Mac.localdomain>

* fix(ssh): a stopped managed server stops showing on its SSH host without a reconnect (#25409)

* fix(ssh): a stopped managed server stops showing on its SSH host without a reconnect

On unlink, forget the host's managed-server decision and republish its connection state. The
republish also drops the host's cached worktree scans, so worktree.ps stops naming the removed
server.

* fix(runtime-environments): a removed server's workspace session partition goes with it

Host-scoped listings enumerate session partitions as known hosts, so worktree ps kept naming a
stopped or remov…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant