diff --git a/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs b/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs
index 7eabf2500f4..521d7bb7696 100644
--- a/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs
+++ b/config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs
@@ -91,6 +91,7 @@ afterAll(() => {
describe('Windows SSH no-input launcher', () => {
it('keeps the artifact source scoped to the reviewed Win32 boundary', () => {
const processSource = readFileSync(join(sourceRoot, 'WindowsSshChildProcess.cs'), 'utf8')
+ const programSource = readFileSync(join(sourceRoot, 'OrcaSshNoInputLauncher.cs'), 'utf8')
const consoleSource = readFileSync(join(sourceRoot, 'WindowsPrivateConsoleInput.cs'), 'utf8')
const outputSource = readFileSync(join(sourceRoot, 'WindowsBoundedOutputFiles.cs'), 'utf8')
@@ -103,6 +104,9 @@ describe('Windows SSH no-input launcher', () => {
expect(processSource).toContain('OutputLimitPollMilliseconds = 50')
expect(processSource).toContain('JobSettlementTimeoutMilliseconds = 5000')
expect(processSource).toContain('TerminateJobObject')
+ expect(processSource).toContain('diagnosticTimeoutMilliseconds')
+ expect(programSource).toContain('--diagnostic-timeout-ms')
+ expect(programSource).toContain('diagnosticTimeoutMilliseconds > 60000')
expect(processSource).toContain('outputs.EnsureWithinLimits()')
expect(processSource).toContain('outputs.ReplayOutputs()')
expect(processSource).not.toContain('WaitForMultipleObjects')
@@ -195,6 +199,26 @@ describe('Windows SSH no-input launcher', () => {
expect(listCaptureFiles()).toEqual(capturesBefore)
})
+ itWindows(
+ 'terminates the child job and replays bounded output at the diagnostic deadline',
+ { timeout: 10_000 },
+ async () => {
+ const capturesBefore = listCaptureFiles()
+ const pidPath = join(fixtureRoot, 'timed-out-child.pid')
+ const result = runLauncher('hold', [pidPath], {}, 250)
+ const childPid = Number.parseInt(readFileSync(pidPath, 'utf8'), 10)
+
+ expect(result.status).toBe(1)
+ expect(result.stdout.toString('utf8')).toBe('ORCA-HOLD-STDOUT')
+ expect(result.stderr.toString('utf8')).toContain('ORCA-HOLD-STDERR')
+ expect(result.stderr.toString('utf8')).toContain(
+ 'SSH child reached the 250 ms diagnostic timeout.'
+ )
+ await waitForProcessExit(childPid, 5_000)
+ expect(listCaptureFiles()).toEqual(capturesBefore)
+ }
+ )
+
itWindows('does not leak an unrelated inheritable parent handle into the child', () => {
const result = spawnSync(handleProbePath, [launcherPath], {
cwd: fixtureRoot,
@@ -231,15 +255,21 @@ describe('Windows SSH no-input launcher', () => {
)
})
-function runLauncher(mode, args = [], options = {}) {
- return spawnSync(launcherPath, [process.execPath, childFixturePath, mode, ...args], {
- cwd: fixtureRoot,
- encoding: null,
- timeout: 10_000,
- windowsHide: true,
- maxBuffer: 32 * 1024 * 1024,
- ...options
- })
+function runLauncher(mode, args = [], options = {}, diagnosticTimeoutMs = null) {
+ const diagnosticArgs =
+ diagnosticTimeoutMs == null ? [] : ['--diagnostic-timeout-ms', String(diagnosticTimeoutMs)]
+ return spawnSync(
+ launcherPath,
+ [...diagnosticArgs, process.execPath, childFixturePath, mode, ...args],
+ {
+ cwd: fixtureRoot,
+ encoding: null,
+ timeout: 10_000,
+ windowsHide: true,
+ maxBuffer: 32 * 1024 * 1024,
+ ...options
+ }
+ )
}
function listCaptureFiles() {
@@ -328,6 +358,8 @@ if (mode === 'arguments-and-stdin') {
}
} else if (mode === 'hold') {
require('node:fs').writeFileSync(process.argv[3], String(process.pid))
+ process.stdout.write('ORCA-HOLD-STDOUT')
+ process.stderr.write('ORCA-HOLD-STDERR')
setInterval(() => {}, 1000)
} else {
process.exit(99)
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 d50e5d6d771..18d7efcc0f4 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
@@ -9,9 +9,9 @@ keeps commands, hashes, runner identities, timings, and failure details.
A checked box means the work has evidence in the detailed ledger. Design approval alone does not
complete a box.
-Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — disconnected private-
-console plus bounded regular-output Windows diagnostic locally green; exact-head native proof
-required, 2026-07-16, Codex implementation owner.**
+Active checkpoint: **Milestone 6 / Work Package 5 bounded runtime transfer — bounded verbose
+Win32-OpenSSH lifecycle capture locally green; exact-head native proof required, 2026-07-16, Codex
+implementation owner.**
`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-PORTABLE-FIXTURE-LOCAL-001` records the portable-fixture
correction locally green: the no-Orca-publication workflow oracle permits the one exact pinned
Microsoft fixture release URL while Orca release URLs, `gh release`, and `contents: write` remain
@@ -210,6 +210,21 @@ isolation pass. Seven native
launcher cases plus live/full-size behavior remain honest local skips. Commit/push only this
runner-temp diagnostic and require fresh exact-head Windows x64/ARM64 compilation, overflow,
cleanup, cancellation, live lifecycle, and fixture-teardown proof. SignPath remains deferred.
+Exact head `149a122b735659b47d7718f409d14e1ed2f947c2`, run `29553325782`, and x64/ARM64 jobs
+`87800290122`/`87800290144` prove 101 files/862 cases with 19 skips, including both new overflow and
+capture-cleanup cases, plus unchanged reproducibility and full-size cache gates. The live probes
+still time out after 8017.5611/8042.1902 ms with ended stdout, closed channel, no sentinel/process
+exit, and zero stderr; both exact teardowns catch locked `UsrClass.dat`. All four non-Windows jobs
+and both Linux supplements pass. `E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-CI-RED-001` therefore
+falsifies output pipes as the remaining cause. Next add only an internal diagnostic deadline and
+bounded replay of verbose OpenSSH output before choosing ConPTY or another client. Production,
+Beta, fallback, tuple, publication, default, and signing paths remain untouched.
+`E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001` records that diagnostic locally green: an
+optional 100-60,000 ms runner-only deadline terminates and settles the owned job before bounded
+output replay; the live suite uses 6 seconds, `-vvv`, outer 8/10-second safety ceilings, and a 16 KiB
+trace tail. Focused 119/119, all 285 release contracts, typecheck, full lint/reliability/max-lines,
+formatting, diff, and protected-resolver isolation pass with honest native skips. Commit/push only
+this artifact and require x64/ARM64 phase classification before selecting ConPTY or another client.
`E-M6-WINDOWS-SYSTEM-SSH-TREE-LIVE-AUDIT-001` fixes the loopback-only official Microsoft
server, fixture-owned non-admin account/ACL, exact host-key trust, Windows PowerShell 5.1,
serial/default and four-channel metrics, cancellation/collision/cleanup, and deterministic teardown
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 051eb324023..3c2a337c901 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,7 +8,7 @@ work; keep exact commands, runner identities, hashes, metrics, and residual gaps
Date created: 2026-07-14
Last updated: 2026-07-16
-Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **Locally green / exact-head native proof required — 2026-07-16, Codex implementation owner: disconnected private-console plus bounded regular-output diagnostic**. `E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-LOCAL-001` records the one-variable runner-temp candidate locally green: proven private-console EOF, distinct bounded regular-file output handles, delete-on-close cleanup, bounded job settlement, and post-exit replay. Commit/push only this package and require x64/ARM64 legacy compilation, native contracts, live lifecycle, and exact teardown evidence. Every production/default SSH path remains disconnected. Real Apple/SignPath rehearsals remain deferred.
+Current phase: Milestone 6 / Work Package 5 bounded runtime transfer — **Locally green / bounded verbose client-lifecycle native proof required — 2026-07-16, Codex implementation owner**. `E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001` records the runner-temp internal deadline, job settlement, bounded replay, and verbose trace-tail contracts locally green. Commit/push only this diagnostic and require both native architectures to identify the last completed client phase before selecting ConPTY or another client. Every production/default SSH path remains disconnected. Real Apple/SignPath rehearsals remain deferred.
Session checkpoint: **PRIVATE CONSOLE EOF PROVEN / PRIVATE OUTPUT PIPES FALSIFIED / BOUNDED REGULAR OUTPUT ACTIVE — 2026-07-16, Codex implementation owner** — exact head `5841b12bc94ecd1027e507e4b741e09fcb0a093d`, run `29552101635`, and x64/ARM64 jobs `87796673326`/`87796673353` both pass 101 files/860 cases with 19 skips, legacy compilation, real-console qualification/EOF, argv/binary/exit/handle-exclusion/cancellation, unchanged full-runtime reproducibility, and full-size cache gates. Live diagnostics still time out after 8025.1553/8034.6943 milliseconds with ended stdout, closed channel, no sentinel/process exit, and zero stderr; both exact teardowns catch locked `UsrClass.dat`. All four non-Windows primary jobs and both Linux supplements pass. No product caller, full-size Windows transfer claim, upload, default/fallback, Beta, tuple publication, or signing behavior changed.
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)
@@ -19891,6 +19891,75 @@ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases wi
Beta option, fallback, full-size Windows transfer claim, tuple, publication, default, signing, or
SignPath behavior is authorized.
+### E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-CI-RED-001 — regular output files do not settle Win32-OpenSSH
+
+- Date/owner/head/run: 2026-07-16, Codex implementation owner; exact pushed head
+ `149a122b735659b47d7718f409d14e1ed2f947c2`, pull-request artifact run `29553325782`, Windows x64
+ job `87800290122` on `windows-2022`, and Windows ARM64 job `87800290144` on `windows-11-arm`.
+- Native contract proof: both architectures pass legacy .NET Framework compilation, the dedicated
+ console EOF probe, and 101 artifact-contract files with 862 runnable cases and 19 declared skips.
+ The two new cases raise the prior count from 860 to 862 and prove >16 MiB fail-closed output plus
+ successful/overflow delete-on-close cleanup; exact binary replay, argv, exit propagation,
+ unrelated-handle exclusion, job cancellation, and bounded settlement also pass. Both unchanged
+ runtimes pass build-twice identity, closure, smoke, and both full-size cache gates.
+- X64 live result: `mode=private-console-regular-output-launcher-no-n`,
+ `elapsedMs=8017.5611`, `success=false`, `timedOut=true`, `sentinel=false`, `stdoutEnded=true`,
+ `processExit=null`, `channelClosed=true`, `closeCode=null`, and `stderrBytes=0`.
+- ARM64 live result: the same lifecycle fields with `elapsedMs=8042.190199999999`. Both official
+ server logs prove accepted public-key authentication, remote command-session start, and remote
+ session close before the client requires bounded external termination. The full-size Windows
+ transfer remains correctly unexecuted after the failed diagnostic.
+- Teardown result: x64 cannot unload fixture class hive
+ `S-1-5-21-4137868374-391603265-4009536220-1003_Classes`; ARM64 independently cannot unload
+ `S-1-5-21-1882319117-3219095328-2125279949-1001_Classes`. Both catch the locked
+ `C:\Users\orca_ssh_fixture\AppData\Local\Microsoft\Windows\UsrClass.dat` and retained profile
+ directory instead of silently leaking fixture state.
+- Preserved evidence: Linux x64/ARM64 jobs `87800290119`/`87800290139`, Darwin x64/ARM64 jobs
+ `87800290123`/`87800290135`, and Linux oldest-userland supplements `87800855219`/`87800855211`
+ all pass at the same exact head.
+- Interpretation/next bound: private pipes, overlapped pipes, zero-length regular stdin, proven
+ console EOF, and now distinct bounded regular-file stdout/stderr all reproduce the live hang.
+ Do not select a production mechanism from speculation. Add only a runner-temp internal deadline
+ that terminates and settles the owned job before replaying bounded output, invoke the exact client
+ with verbose diagnostics, and record the last completed OpenSSH phase on both architectures.
+ ConPTY and alternate-client qualification remain subsequent evidence decisions. No product,
+ Beta, fallback, tuple, publication, default, signing, or SignPath behavior is authorized.
+
+### E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001 — internal timeout and bounded trace capture are locally green
+
+- Date/owner/base/runner: 2026-07-16, Codex implementation owner; uncommitted artifact-only package
+ atop exact regular-output RED head `149a122b735659b47d7718f409d14e1ed2f947c2` on Apple arm64,
+ macOS 26.2 build 25C56, Node 26.5.0, and pnpm 10.24.0.
+- Diagnostic boundary: an optional runner-only `--diagnostic-timeout-ms` accepts 100 through 60,000
+ ms and leaves ordinary launcher calls unbounded as before. At deadline the launcher terminates
+ and waits for its owned job, replays the same bounded regular-file stdout/stderr, then returns a
+ diagnostic failure. The live purpose suite uses 6,000 ms, retains the existing 8,000/10,000 ms
+ external safety ceilings, inserts `-vvv` before the exact OpenSSH destination separator, and logs
+ only a 16 KiB stderr tail plus lifecycle fields. Production argument construction is unchanged.
+- Native contract design: the new Windows case writes stdout/stderr plus its PID, holds forever,
+ and requires internal timeout failure, exact replay, child-job termination, and unchanged capture-
+ file set. Existing console EOF, argv, binary output, overflow, cleanup, handle exclusion, exit,
+ and external-cancellation cases remain. The new native case is honestly skipped locally.
+- Syntax/focused evidence: both launcher scripts pass `node --check`.
+ `/usr/bin/time -l pnpm exec vitest run --config config/vitest.config.ts --maxWorkers=1
+ src/main/ssh/ssh-connection.test.ts src/main/ssh/ssh-system-fallback.test.ts
+ src/main/ssh/ssh-system-no-input-windows-openssh.test.ts
+ src/main/ssh/ssh-relay-runtime-windows-system-ssh-openssh-full-size.test.ts
+ config/scripts/ssh-relay-runtime-windows-no-input-launcher.test.mjs
+ config/scripts/ssh-relay-runtime-workflow.test.mjs` passes 119 runnable cases with ten declared
+ native/live skips in 2.69 seconds real; maximum RSS is 171,212,800 bytes and peak footprint is
+ 97,179,456 bytes.
+- Release/static evidence: the unquoted release-contract command passes 51 files and 285 runnable
+ cases with eight declared Windows skips in 12.19 seconds. `/usr/bin/time -l pnpm typecheck`
+ passes in 2.57 seconds real with 1,258,979,328-byte maximum RSS and 97,163,024-byte peak
+ footprint. `/usr/bin/time -l pnpm lint` passes in 21.90 seconds real with the 26 existing
+ warnings, all 41 reliability gates, and the 355-entry max-lines ratchet; maximum RSS is
+ 2,039,169,024 bytes and peak footprint is 97,195,840 bytes.
+- Residual boundary: local source contracts do not prove legacy compilation, internal timeout/job
+ settlement, output replay, or useful OpenSSH verbose content on Windows. Commit/push this isolated
+ package and require exact-head x64/ARM64 evidence. Do not select ConPTY, another client, a product
+ caller, Beta, fallback, tuple, publication, default, signing, or SignPath behavior from local proof.
+
## Accepted Gaps
No product gap is accepted merely because it appears in this list. Each entry requires explicit
@@ -19949,11 +20018,10 @@ The project is not complete until every applicable item below is checked with ev
## Next Required Action
-Commit and push only the private-console plus bounded regular-output diagnostic recorded by
-`E-M6-WINDOWS-PRIVATE-CONSOLE-REGULAR-OUTPUT-LOCAL-001`, then require exact-head Windows x64 and
-ARM64 legacy-compiler, console qualification/EOF, bounded binary output/overflow/cleanup,
-argv/exit/handle-exclusion/cancellation, live diagnostic, and exact profile/directory teardown
-evidence. Do not
+Commit and push only the bounded internal-timeout and verbose-output capture recorded by
+`E-M6-WINDOWS-VERBOSE-LIFECYCLE-CAPTURE-LOCAL-001`, then require exact-head Windows x64 and
+ARM64 legacy-compiler, launcher-timeout settlement, verbose-client phase classification, and exact
+profile/directory teardown evidence. Do not
change the production Windows handle or check the broad Milestone 6 Windows system-SSH item until
both architectures select the same console-independent strategy. Do not add an
Electron/startup/product importer, per-target mode wiring, fallback, tuple
diff --git a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html
index 6f1c2e39203..44b59a7bcec 100644
--- a/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html
+++ b/docs/reference/plans/2026-07-14-ssh-relay-github-release-plan.html
@@ -1670,12 +1670,15 @@
child while retaining private output pipes, exact handle inheritance, job
ownership, and bounded cancellation. Native x64/arm64 prove the console handle
and zero-byte cooked EOF but the live client still hangs, falsifying stdin alone.
- The next one-variable diagnostic retains that console stdin and replaces only
+ The next one-variable diagnostic retained that console stdin and replaced only
child stdout/stderr pipes with unique delete-on-close regular files under an
explicit per-stream byte ceiling, bounded polling/cancellation, incremental
- post-exit replay, and exact cleanup. Build and live-test it on native x64/arm64
- before considering ConPTY, another SSH client, packaging, signing, or a product
- caller. POSIX retains ignored
+ post-exit replay, and exact cleanup. Native x64/arm64 prove those contracts, but
+ the live client still hangs with the same lifecycle tuple, falsifying redirected
+ output pipes as the remaining cause. Before selecting ConPTY or another SSH
+ client, the runner-only launcher must boundedly terminate its own diagnostic
+ child and replay an exact verbose OpenSSH trace to classify the stuck client
+ phase. Do not package, sign, or add a product caller. POSIX retains ignored
stdin plus -n; fixture-owned Windows session processes settle
before profile hives are boundedly unloaded and native profile deletion begins;
MaxSessions=1; interrupted upload; stale lock
diff --git a/native/windows-ssh-no-input-launcher/OrcaSshNoInputLauncher.cs b/native/windows-ssh-no-input-launcher/OrcaSshNoInputLauncher.cs
index 29a4cab4117..ee250c91fb3 100644
--- a/native/windows-ssh-no-input-launcher/OrcaSshNoInputLauncher.cs
+++ b/native/windows-ssh-no-input-launcher/OrcaSshNoInputLauncher.cs
@@ -7,21 +7,40 @@ internal static class OrcaSshNoInputLauncher
{
try
{
- if (args.Length < 1)
+ int executableIndex = 0;
+ int diagnosticTimeoutMilliseconds = 0;
+ if (args.Length >= 2 && args[0] == "--diagnostic-timeout-ms")
{
- Console.Error.WriteLine("Usage: orca-ssh-no-input.exe [arguments...]");
+ if (!Int32.TryParse(args[1], out diagnosticTimeoutMilliseconds) ||
+ diagnosticTimeoutMilliseconds < 100 ||
+ diagnosticTimeoutMilliseconds > 60000)
+ {
+ Console.Error.WriteLine("--diagnostic-timeout-ms requires 100 through 60000.");
+ return 2;
+ }
+ executableIndex = 2;
+ }
+ if (args.Length <= executableIndex)
+ {
+ Console.Error.WriteLine(
+ "Usage: orca-ssh-no-input.exe [--diagnostic-timeout-ms ] [arguments...]"
+ );
return 2;
}
- string executablePath = Path.GetFullPath(args[0]);
+ string executablePath = Path.GetFullPath(args[executableIndex]);
if (!File.Exists(executablePath))
{
Console.Error.WriteLine("Unable to locate the SSH executable at \"{0}\".", executablePath);
return 2;
}
- string[] childArgs = new string[args.Length - 1];
- Array.Copy(args, 1, childArgs, 0, childArgs.Length);
- return WindowsSshChildProcess.Run(executablePath, childArgs);
+ string[] childArgs = new string[args.Length - executableIndex - 1];
+ Array.Copy(args, executableIndex + 1, childArgs, 0, childArgs.Length);
+ return WindowsSshChildProcess.Run(
+ executablePath,
+ childArgs,
+ diagnosticTimeoutMilliseconds
+ );
}
catch (Exception error)
{
diff --git a/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs b/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs
index 493a1c4f94a..4b1b1acd27c 100644
--- a/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs
+++ b/native/windows-ssh-no-input-launcher/WindowsSshChildProcess.cs
@@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
+using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
@@ -18,7 +19,11 @@ internal static class WindowsSshChildProcess
private const uint OutputLimitPollMilliseconds = 50;
private const uint JobSettlementTimeoutMilliseconds = 5000;
- internal static int Run(string executablePath, string[] args)
+ internal static int Run(
+ string executablePath,
+ string[] args,
+ int diagnosticTimeoutMilliseconds
+ )
{
Stream launcherStdout = Console.OpenStandardOutput();
Stream launcherStderr = Console.OpenStandardError();
@@ -70,7 +75,22 @@ internal static class WindowsSshChildProcess
throw LastWin32("ResumeThread failed.");
}
- WaitForChildWithinOutputLimits(process.Process, outputs);
+ bool exited = WaitForChildWithinOutputLimits(
+ process.Process,
+ outputs,
+ diagnosticTimeoutMilliseconds
+ );
+ if (!exited)
+ {
+ // Why: the runner must recover verbose bytes without relying on an external
+ // kill that also destroys the delete-on-close diagnostic captures.
+ TerminateAndSettleJob(ref job);
+ outputs.ReplayOutputs();
+ throw new TimeoutException(
+ "SSH child reached the " + diagnosticTimeoutMilliseconds +
+ " ms diagnostic timeout."
+ );
+ }
uint exitCode;
if (!GetExitCodeProcess(process.Process, out exitCode))
{
@@ -177,18 +197,33 @@ internal static class WindowsSshChildProcess
}
}
- private static void WaitForChildWithinOutputLimits(
+ private static bool WaitForChildWithinOutputLimits(
IntPtr process,
- WindowsBoundedOutputFiles outputs
+ WindowsBoundedOutputFiles outputs,
+ int diagnosticTimeoutMilliseconds
)
{
+ Stopwatch elapsed = Stopwatch.StartNew();
while (true)
{
- uint waitResult = WaitForSingleObject(process, OutputLimitPollMilliseconds);
+ uint waitMilliseconds = OutputLimitPollMilliseconds;
+ if (diagnosticTimeoutMilliseconds > 0)
+ {
+ long remaining = diagnosticTimeoutMilliseconds - elapsed.ElapsedMilliseconds;
+ if (remaining <= 0)
+ {
+ return false;
+ }
+ if (remaining < waitMilliseconds)
+ {
+ waitMilliseconds = (uint)remaining;
+ }
+ }
+ uint waitResult = WaitForSingleObject(process, waitMilliseconds);
if (waitResult == WaitObject0)
{
outputs.EnsureWithinLimits();
- return;
+ return true;
}
if (waitResult == WaitFailed)
{
diff --git a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts
index 8fe45e3a699..f90acfe38f5 100644
--- a/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts
+++ b/src/main/ssh/ssh-system-no-input-windows-openssh.test.ts
@@ -7,7 +7,7 @@ import type { SshTarget } from '../../shared/ssh-types'
import { buildSshArgs } from './system-ssh-args'
import { findSystemSsh } from './system-ssh-binary'
-type DiagnosticMode = 'private-console-regular-output-launcher-no-n'
+type DiagnosticMode = 'private-console-regular-output-verbose-timeout'
type DiagnosticResult = {
mode: DiagnosticMode
@@ -19,6 +19,7 @@ type DiagnosticResult = {
channelClosed: boolean
closeCode: number | null | 'not-observed'
stderrBytes: number
+ stderrTail: string
elapsedMs: number
}
@@ -48,19 +49,26 @@ async function runDiagnostic(): Promise {
if (!sshPath) {
throw new Error('Native Windows OpenSSH client is unavailable')
}
- const mode: DiagnosticMode = 'private-console-regular-output-launcher-no-n'
+ const mode: DiagnosticMode = 'private-console-regular-output-verbose-timeout'
const startedAt = performance.now()
// Why: Win32-OpenSSH still hangs with proven console EOF plus piped output, so this diagnostic
// isolates stdout/stderr as bounded regular files without changing production transport.
+ const sshArgs = buildSshArgs(createTarget())
+ const destinationSeparator = sshArgs.indexOf('--')
+ if (destinationSeparator < 0) {
+ throw new Error('Windows OpenSSH diagnostic arguments lost the destination separator')
+ }
+ sshArgs.splice(destinationSeparator, 0, '-vvv')
const child = spawn(
launcherPath as string,
- [sshPath, ...buildSshArgs(createTarget()), 'echo ORCA-SYSTEM-SSH-OK'],
+ ['--diagnostic-timeout-ms', '6000', sshPath, ...sshArgs, 'echo ORCA-SYSTEM-SSH-OK'],
{ stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }
)
return new Promise((resolve, reject) => {
let stdout = ''
let stderrBytes = 0
+ let stderrTail = ''
let stdoutEnded = false
let processExit: number | null | 'not-observed' = 'not-observed'
let channelClosed = false
@@ -91,6 +99,7 @@ async function runDiagnostic(): Promise {
channelClosed,
closeCode,
stderrBytes,
+ stderrTail,
elapsedMs: performance.now() - startedAt
})
}
@@ -115,6 +124,8 @@ async function runDiagnostic(): Promise {
})
child.stderr?.on('data', (chunk: Buffer) => {
stderrBytes += chunk.length
+ // Why: retain only the diagnostic tail needed to classify the stuck client phase.
+ stderrTail = (stderrTail + chunk.toString('utf8')).slice(-16 * 1024)
})
child.on('exit', (code) => {
processExit = code
@@ -138,16 +149,16 @@ async function runDiagnostic(): Promise {
describe.skipIf(!hasLiveInput)('Windows OpenSSH no-input child-handle diagnostic', () => {
it(
- 'qualifies private-console EOF plus regular-file output without the Windows null-input option',
+ 'captures the verbose client phase after a bounded private-console diagnostic timeout',
{ timeout: 15_000 },
async () => {
expect(process.platform).toBe('win32')
- const privateConsoleRegularOutputLauncherNoN = await runDiagnostic()
+ const privateConsoleRegularOutputVerboseTimeout = await runDiagnostic()
console.log(
- `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privateConsoleRegularOutputLauncherNoN })}`
+ `ssh_windows_no_input_handle_diagnostic=${JSON.stringify({ privateConsoleRegularOutputVerboseTimeout })}`
)
- expect(privateConsoleRegularOutputLauncherNoN.success).toBe(true)
+ expect(privateConsoleRegularOutputVerboseTimeout.success).toBe(true)
}
)
})