diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
index 43fb630f58d..df25d6405e6 100644
--- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
+++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist-summary.md
@@ -42,8 +42,12 @@ splits lock, immutable publication/quarantine, and in-use eviction into separate
are locally green under `E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001`, including a real second process,
heartbeat, cancellation, stale-dead reclaim, and live/ambiguous/displaced-owner safety. Artifact,
relay, release, typecheck, full lint, max-lines, and workflow gates pass. Both native job families
-require the suite under `E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001`; next is exact-head all-six
-Node 24 execution. Desktop consumers, cache publication/quarantine/eviction, SSH transfer/install,
+require the suite under `E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001`. Exact-head Windows x64 then
+found the open-heartbeat-handle directory-rename incompatibility under
+`E-M5-ARTIFACT-CACHE-LOCK-CI-WINDOWS-RED-001`; Windows arm64 also found equivalent zlib truncation
+wording outside the extraction test oracle under `E-M5-ARTIFACT-EXTRACTION-CI-WINDOWS-ARM64-RED-001`.
+The ownership-preserving release order and equivalent-error assertion are locally green under
+`E-M5-WINDOWS-NATIVE-GATE-CORRECTIONS-LOCAL-001`; a fresh all-six Node 24 run is now required. Desktop consumers, cache publication/quarantine/eviction, SSH transfer/install,
mode wiring, tuple enablement, publication, production keys/environment/seed, and merge to `main`
remain disconnected.
diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
index d6eaeb57138..2d74dec03ae 100644
--- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
+++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-implementation-checklist.md
@@ -8,8 +8,8 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps
Date created: 2026-07-14
Last updated: 2026-07-15
-Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: disconnected cross-process content-lock native gate**. E-M5-ARTIFACT-CACHE-LOCK-LOCAL-001 closes ten local contracts plus broad/static gates; E-M5-ARTIFACT-CACHE-LOCK-CI-WIRING-LOCAL-001 requires the suite on both native job families. Checkpoint and push, then require exact-head Node 24 execution on all six clients before cache-entry publication work. Do not add cache publication/quarantine/eviction, a desktop caller, SSH transfer/install, tuple enablement, mode wiring, release publication, or default behavior.
-Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — the disconnected content lock uses a fully populated pending directory followed by atomic rename, a nonce-bound heartbeat through the original owner file handle, conservative same-host PID liveness, stale-dead-owner tombstoning, bounded/cancellable wait, directory-inode plus token ownership assertion, and owner-only release. Ten focused tests include a real second Node process; artifact, relay, release, typecheck, full lint, max-lines, workflow, and diff gates pass. The final direct-child harness stays inside the unchanged 30s test bound even while unrelated worktrees heavily saturate the host; broad elapsed times are recorded as contention-affected, not performance baselines. The suite is wired into both native Node 24 job families but exact-head all-six execution remains required. The user authorizes commits, pushes, draft-PR updates, CI runs/reruns, PR-contained rehearsals, and PR-contained release writes where checklist gates require them, but not merging to `main`. There is no release publication, cache entry, cache consumer, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.
+Current phase: Milestone 5 / Work Package 4 desktop cache boundary — **In progress — 2026-07-15, Codex implementation owner: checkpoint and fresh all-six Windows-native correction proof**. E-M5-WINDOWS-NATIVE-GATE-CORRECTIONS-LOCAL-001 closes the narrow local release-order and equivalent truncation-oracle corrections exposed by run `29458719333`. Checkpoint and push the exact tree, then require all six primary Node 24 jobs plus companion PR Checks and Golden E2E to pass before beginning immutable cache-entry publication/quarantine. Do not add a desktop caller, SSH transfer/install, tuple enablement, mode wiring, release publication, or default behavior.
+Session checkpoint: **In progress — 2026-07-15, Codex implementation owner** — resumed with user authority for end-to-end PR-contained implementation, commits, pushes, CI, rehearsals, and release writes, with merging to `main` explicitly prohibited. The worktree was clean and exactly matched pushed head `869603a3e86933cc9985f9e4d240e615146d86d8` at session start. Both Windows jobs then reproduced the open-handle `EPERM`; Windows arm64 also exposed equivalent zlib truncation wording outside the assertion oracle. The corrected local tree passes 31 focused lock/extraction tests, 95 artifact tests with one environment skip, all 279 release-runtime tests, typecheck, full lint, formatting, max-lines, and diff gates. Cache publication remains gated; there is no release publication, cache entry, cache consumer, SSH transfer/install, tuple enablement, or production/default behavior change. Legacy remains the production default.
Primary design: [SSH relay GitHub Release plan](./2026-07-14-ssh-relay-github-release-plan.html)
Motivating issues: [#8450](https://github.com/stablyai/orca/issues/8450), [#1693](https://github.com/stablyai/orca/issues/1693)
@@ -13082,6 +13082,98 @@ sort`; targeted `rg -n` over `cache|extract|atomic|quarantine`; and complete rea
- Follow-up: checkpoint and push, then require exact-head all-six native jobs plus PR Checks and
Golden E2E before beginning immutable cache-entry publication/quarantine.
+### E-M5-ARTIFACT-CACHE-LOCK-CI-WINDOWS-RED-001 — Open heartbeat handle blocks Windows release rename
+
+- Date: 2026-07-15
+- Owner: Codex implementation owner
+- Source: exact pushed head `869603a3e86933cc9985f9e4d240e615146d86d8`; SSH Relay Runtime
+ Artifacts run [29458719333](https://github.com/stablyai/orca/actions/runs/29458719333), Windows x64
+ job [87497521527](https://github.com/stablyai/orca/actions/runs/29458719333/job/87497521527), GitHub
+ Actions runner `GitHub Actions 1000056419`, label `windows-2022`; and Windows arm64 job
+ [87497521511](https://github.com/stablyai/orca/actions/runs/29458719333/job/87497521511), runner
+ `GitHub Actions 1000056418`, label `windows-11-arm`.
+- Exact log command:
+ `gh api repos/stablyai/orca/actions/jobs/87497521527/logs 2>&1 | tail -240`.
+- Result: required CI RED in `Run runtime artifact contract tests`; one failed lock file / six failed
+ lock tests while 64 other files passed, with 414 passed / 13 skipped tests overall in 16.66s.
+ Five owner-release cases fail with `EPERM` renaming the lock directory to its release tombstone;
+ the displacement fixture fails with the same `EPERM`. The open `owner.json` heartbeat handle keeps
+ the containing directory non-renamable on Windows. The later `ENOENT` is waiter/test cleanup fallout
+ after release throws, not a separate acquisition defect.
+- Safety decision: retain heartbeat writes through the original owner handle while the lease is
+ active. On release, stop and settle heartbeat work, assert directory identity plus nonce, close the
+ owned handle, then atomically rename to a release tombstone. A close-before-final-assert change is
+ forbidden; a rename retry that guesses through ownership changes is forbidden. Replace the fixture's
+ live-directory rename with portable nonce displacement so Windows proves successor preservation
+ without depending on a filesystem operation it explicitly rejects while the handle is live.
+- Does not prove: corrected Windows behavior, Windows arm64, the other five native jobs, cache
+ publication/quarantine/eviction, packaged Electron, SSH, or an enabled tuple.
+- Follow-up: implement the narrow release-order/fixture correction, run focused and broad local gates,
+ push, and require a fresh exact-head all-six native run before cache-entry publication begins.
+
+### E-M5-ARTIFACT-EXTRACTION-CI-WINDOWS-ARM64-RED-001 — Equivalent zlib truncation wording rejected by oracle
+
+- Date: 2026-07-15
+- Owner: Codex implementation owner
+- Source: exact pushed head `869603a3e86933cc9985f9e4d240e615146d86d8`; SSH Relay Runtime
+ Artifacts run [29458719333](https://github.com/stablyai/orca/actions/runs/29458719333), Windows arm64
+ job [87497521511](https://github.com/stablyai/orca/actions/runs/29458719333/job/87497521511), runner
+ `GitHub Actions 1000056418`, label `windows-11-arm`.
+- Exact log command:
+ `gh api repos/stablyai/orca/actions/jobs/87497521511/logs 2>&1 | sed -n '1320,1360p'`.
+- Result: archive mutation is rejected, but the test fails because Windows arm64 zlib returns
+ `unexpected end of file`; the assertion allowed semantically identical `unexpected EOF` but not
+ that spelling. This is an assertion portability defect, not accepted extraction or an integrity
+ fallback. The same contract step reports two failed files / 63 passed files before lock-test
+ fallout is separated.
+- Safety decision: accept only the equivalent explicit truncation phrase in the hostile-mutation
+ assertion. Do not alter extraction, cleanup, archive/tree verification, fallback classification,
+ or fail-closed behavior.
+- Follow-up: run the focused extraction and artifact globs locally, then require fresh Windows arm64
+ and all-six native execution on the corrected head.
+
+### E-M5-WINDOWS-NATIVE-GATE-CORRECTIONS-LOCAL-001 — Narrow lock release and truncation-oracle corrections pass locally
+
+- Date: 2026-07-15
+- Owner: Codex implementation owner
+- Source: uncommitted correction atop exact pushed head
+ `869603a3e86933cc9985f9e4d240e615146d86d8`, following
+ E-M5-ARTIFACT-CACHE-LOCK-CI-WINDOWS-RED-001 and
+ E-M5-ARTIFACT-EXTRACTION-CI-WINDOWS-ARM64-RED-001.
+- Lock correction: release stops and settles heartbeat work, final-checks directory identity and
+ nonce through the still-open owner handle, closes that handle, atomically renames the lock to its
+ unselectable tombstone, then removes the tombstone. Active heartbeats still write only through the
+ originally opened handle. The displacement contract now replaces only the nonce-bearing owner
+ record, which is portable while Windows keeps the directory non-renamable, and still proves
+ `assertOwned()` failure plus successor preservation on release.
+- Extraction-oracle correction: the hostile archive mutation assertion adds only zlib's explicit
+ equivalent `unexpected end of file` spelling alongside `unexpected EOF`; production extraction,
+ cleanup, verification, and fallback classification are unchanged.
+- Exact commands/results:
+ - `pnpm exec oxfmt --check src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts
+ src/main/ssh/ssh-relay-artifact-cache-lock.test.ts
+ src/main/ssh/ssh-relay-artifact-extraction.test.ts`: pass;
+ - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1
+ src/main/ssh/ssh-relay-artifact-cache-lock.test.ts
+ src/main/ssh/ssh-relay-artifact-extraction.test.ts`: two passed files / 31 passed tests in 2.88s
+ Vitest / 8.25s wall, 156,925,952-byte maximum RSS, zero swaps;
+ - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1
+ src/main/ssh/ssh-relay-artifact-*.test.ts`: five passed files / one environment-skipped file,
+ 95 passed / one skipped tests in 23.52s Vitest / 26.47s wall, 143,228,928-byte maximum RSS,
+ zero swaps;
+ - `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1
+ config/scripts/ssh-relay-runtime-*.test.mjs`: 50 passed files / 279 passed tests in 105.46s
+ Vitest / 109.78s wall, 193,576,960-byte maximum RSS, zero swaps;
+ - `pnpm typecheck`; `pnpm lint`; `pnpm run check:max-lines-ratchet`; `git diff --check`: pass. Full
+ lint retains only existing unrelated warnings.
+- Consumer/diff oracle: the preserved Milestone 0 resolver pair and release-required-assets pair have
+ zero diff. No cache publication/quarantine/eviction, desktop caller, SSH path, setting, mode, tuple,
+ release write, or default behavior is added.
+- Does not prove: Windows semantics, any native client, cache publication, packaged Electron, SSH, or
+ an enabled tuple. Fresh exact-head all-six native execution remains mandatory.
+- Follow-up: checkpoint/push, require all six primary native jobs plus PR Checks and Golden E2E, and
+ record exact runner/job evidence before opening the immutable cache-entry publication package.
+
## Accepted Gaps
No product gap is accepted merely because it appears in this list. Each entry requires explicit
diff --git a/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts b/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts
index 647daadf54e..854e6de3c86 100644
--- a/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts
+++ b/src/main/ssh/ssh-relay-artifact-cache-lock-lease.ts
@@ -116,17 +116,11 @@ class SshRelayArtifactCacheLockLease {
return
}
+ // Windows cannot rename a directory containing an open file; ownership is final-checked first.
+ await this.ownerHandle.close()
const tombstone = `${this.lockPath}.released-${this.token}`
- let renamed = false
- try {
- await rename(this.lockPath, tombstone)
- renamed = true
- } finally {
- await this.ownerHandle.close()
- }
- if (renamed) {
- await rm(tombstone, { recursive: true, force: true })
- }
+ await rename(this.lockPath, tombstone)
+ await rm(tombstone, { recursive: true, force: true })
}
}
diff --git a/src/main/ssh/ssh-relay-artifact-cache-lock.test.ts b/src/main/ssh/ssh-relay-artifact-cache-lock.test.ts
index bae6bd23a26..47aba5ed9ca 100644
--- a/src/main/ssh/ssh-relay-artifact-cache-lock.test.ts
+++ b/src/main/ssh/ssh-relay-artifact-cache-lock.test.ts
@@ -1,5 +1,5 @@
import { spawn } from 'node:child_process'
-import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promises'
+import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { hostname, tmpdir } from 'node:os'
import { join } from 'node:path'
import { pathToFileURL } from 'node:url'
@@ -273,14 +273,20 @@ describe('SSH relay artifact cache content lock', () => {
it('does not publish or delete through a lock whose nonce ownership was displaced', async () => {
const root = await cacheRoot()
const owner = await acquireSshRelayArtifactCacheLock({ cacheRoot: root, contentId })
- const displacedPath = `${owner.lockPath}.displaced`
- await rename(owner.lockPath, displacedPath)
- await writeManualLock({
- root,
- pid: process.pid,
- heartbeatAtMs: Date.now(),
- token: successorOwnerToken
- })
+ const now = Date.now()
+ // Windows forbids renaming a directory with the live heartbeat handle; nonce replacement is portable.
+ await writeFile(
+ join(owner.lockPath, 'owner.json'),
+ `${JSON.stringify({
+ schemaVersion: 1,
+ contentId,
+ token: successorOwnerToken,
+ hostname: hostname(),
+ pid: process.pid,
+ acquiredAtMs: now,
+ heartbeatAtMs: now
+ })}\n`
+ )
await expect(owner.assertOwned()).rejects.toThrow(/ownership/i)
await owner.release()
diff --git a/src/main/ssh/ssh-relay-artifact-extraction.test.ts b/src/main/ssh/ssh-relay-artifact-extraction.test.ts
index b0b8a8e3dd3..e57a49a7944 100644
--- a/src/main/ssh/ssh-relay-artifact-extraction.test.ts
+++ b/src/main/ssh/ssh-relay-artifact-extraction.test.ts
@@ -495,7 +495,9 @@ describe('SSH relay artifact extraction', () => {
archivePath: value.archivePath,
outputDirectory
})
- ).rejects.toThrow(/archive|central directory|changed|invalid|unexpected EOF|zip/i)
+ ).rejects.toThrow(
+ /archive|central directory|changed|invalid|unexpected (?:EOF|end of file)|zip/i
+ )
await waitForPath(outputDirectory)
await writeFile(value.archivePath, Buffer.alloc(value.artifact.archive.size, 0x62))