From caa404d166ed15e43e4f2ef0a94e68f933e4de09 Mon Sep 17 00:00:00 2001 From: garrytan Date: Wed, 28 Oct 2026 14:04:17 +0000 Subject: [PATCH] fix(browse): a win32 start-time mismatch with an unreadable command line is unknown, not gone Fix-check (adversarial reviewer): when the Bun reader and a Node-written record disagree and the PowerShell command-line query also fails, the agent was read as gone and a second agent could start beside it. Base treated a failed query as unknown; this restores that. CHANGELOG [Unreleased] entry. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- CHANGELOG.md | 15 +++++++++++++++ browse/src/terminal-agent-control.ts | 1 + browse/test/process-liveness-windows.test.ts | 5 +++++ 3 files changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 30ebc2a80..187e8b796 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [Unreleased] + +**The terminal agent on Windows no longer waits on PowerShell to learn its own start time, so it boots in about a second and a slow PowerShell can no longer leave it without an identity.** Every agent boot ran `powershell.exe Get-CimInstance` with a 2 s timeout, twice when started standalone. On a loaded `windows-latest` runner one launch took 0.94 s at the median and up to 10.8 s, so agent boot took 2.4 to 2.8 s at the median and 5.1 s on a runner's first boot, past the owner-watchdog test's 5 s window. Measured again after the change under the same load: 0.86 to 0.91 s at the median, 1.18 s at most, 120 of 120 boots inside the window. + +### Fixed +- **`browse/src/terminal-agent-control.ts`**: under Bun the Windows start time comes from `GetProcessTimes` in-process and is written as the same `/Date()/` value PowerShell's `CreationDate` gives (147 of 147 processes matched), so records from earlier releases and from the Node daemon, which keeps the PowerShell reader, still compare. The kill-time check still reads the command line through PowerShell, and a process is stopped only when it carries the record's `--agent-gen`. On Windows a start time that disagrees between the two readers no longer reads as gone when the live process carries that generation or its command line cannot be read, so no second agent starts beside it. + +### Tests +- `browse/test/process-liveness-windows.test.ts`: the in-process reader, the Node fallback, the kill-time command-line check, a reader disagreement and the unreadable path (never signalled, never gone, blocks a second agent), plus two Windows-only cases: the in-process value equals PowerShell's `CreationDate`, and a live agent whose identity cannot be read blocks a second one. +- `browse/test/path-security-realpath.test.ts` builds its 8.3 alias with `GetShortPathNameW` instead of a PowerShell launch that stalled for 30 s in Windows Free Tests 40624015029 and was reported as "8.3 short names are disabled". A failed lookup now fails the test instead of skipping it. +- `browse/test/cookie-import-native-job.test.ts` prints the Restart Manager receipt when the owner assertion fails, so the next red names its cause. + +### Gate integrity +- Test 5 in `process-liveness-windows.test.ts` expected the PowerShell mock's `fixture-owner-start` as the owner start time on every platform. On Windows that expectation followed the old implementation, so it moved with the product. It now requires the in-process value, equality with a real read, and no PowerShell query for the owner. The Linux and macOS assertion is unchanged. + ## [1.91.294.0] - 2026-10-28 **`gstack-doctor` and setup's hook check start far fewer processes, so the doctor tests fit the Windows CI lane again.** On Windows Git Bash each process start costs 12 to 77 ms, and the doctor spent most of its time starting them: about 3.3 s per `--check` and 6 s per dashboard run. `windows-free-shard (1)` was red on main because `test/gstack-doctor.test.ts` took 553 to 647 s there. Every doctor row, fix line and JSON field prints the same as before. diff --git a/browse/src/terminal-agent-control.ts b/browse/src/terminal-agent-control.ts index ac2eb78a4..d53dcf624 100644 --- a/browse/src/terminal-agent-control.ts +++ b/browse/src/terminal-agent-control.ts @@ -369,6 +369,7 @@ function agentStatus(record: AgentRecord, ownerPid?: number): 'owned' | 'gone' | const actual = agentProcessInfo(record.pid); if (!actual.startTime) return isAgentRecordLive(record) ? 'unknown' : 'gone'; const ownsGeneration = actual.commandLine.split(/\s+/).some(arg => arg.replace(/^['"]|['"]$/g, '') === `--agent-gen=${record.gen}`); + if (actual.startTime !== record.startTime && process.platform === 'win32' && !actual.commandLine) return 'unknown'; if (actual.startTime !== record.startTime && !(process.platform === 'win32' && ownsGeneration)) return 'gone'; try { let state: string | undefined; diff --git a/browse/test/process-liveness-windows.test.ts b/browse/test/process-liveness-windows.test.ts index 252f5fd1f..9e00a50e3 100644 --- a/browse/test/process-liveness-windows.test.ts +++ b/browse/test/process-liveness-windows.test.ts @@ -267,6 +267,11 @@ describe('win32 terminal-agent identity (GSTA-139)', () => { expect(stopAgentByRecord(record(daemonRecorded), 0)).toBe(true); expect(calls.signals).toEqual(['SIGTERM']); }); + onWin32({ ffi: birth, powershell: { status: null } }, calls => { + expect(isAgentRecordGone(record(daemonRecorded))).toBe(false); + expect(stopAgentByRecord(record(daemonRecorded), 0)).toBe(false); + expect(calls.signals).toEqual([]); + }); }); test('an unreadable start time is unknown: never signalled and never treated as gone', () => {