Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion config/scripts/ci-shard-timings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4604,7 +4604,6 @@
"src/main/window/terminal-tab-close-request-relay.test.ts": 55,
"src/main/window/updater-package-recovery-ipc.test.ts": 4430,
"src/main/window/window-close-decision.test.ts": 41,
"src/main/windows-descendant-exit-verification.test.ts": 100,
"src/main/windows-live-tree-kill.win32.test.ts": 46,
"src/main/windows-process-tree-kill.test.ts": 61,
"src/main/windows-pty-root-identity.test.ts": 94,
Expand Down
13 changes: 6 additions & 7 deletions docs/reference/windows-process-enumeration.md
Original file line number Diff line number Diff line change
Expand Up @@ -374,10 +374,9 @@ relay uses the scan and the WMI launch fallback.
`node_addon_api.gyp` resolves outside the repo and hourly Windows builds
die at configure. `node-pty` is patched the same way for the same reason.
4. **No PEB reads, no `PROCESS_VM_READ`.** See below.
5. **The `CreationTime` flag (4).** Upstream exposes no process start time, and
`isWindowsProcessStartTimeAvailable()` gates structured Claude and Codex
chat on it, so without this change win32 silently fell back to the legacy
transcript path. `GetProcessCreationTime` opens
5. **The `CreationTime` flag (4).** Upstream exposes no process start time.
Structured Claude and Codex chat no longer depend on it; the owner probe and
the orcad runtime preflight still read it. `GetProcessCreationTime` opens
`PROCESS_QUERY_LIMITED_INFORMATION` and converts `GetProcessTimes`' FILETIME
to Unix ms; a process that denies the handle is emitted with the field
absent, never zero, because callers must be able to tell "cannot identify"
Expand All @@ -395,9 +394,9 @@ relay uses the scan and the WMI launch fallback.
so itself.

Two readers depend on it. `isWindowsProcessStartTimeAvailable()` returns
false unless this bit is set, because claiming otherwise leaves
`captureWindowsDescendantSnapshot` returning null forever while structured
chat believes it has a reaper. And `windows-process-tree-creation-time.cjs`
false unless this bit is set, so the owner probe never scans the whole table
for times it cannot get (the Windows Claude descendant snapshot that once
relied on it is gone). And `windows-process-tree-creation-time.cjs`
asserts it during install, which is what forces a from-source rebuild —
the same role `node-pty-job-ownership.cjs` plays for node-pty's job exports.

Expand Down
2 changes: 1 addition & 1 deletion src/main/agent-launch/agent-launch-mode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,7 @@ async function readStructuredCreateSupport(

/**
* Applies the executing host's `agentSession.createSupport` answer, which is the authority on WSL,
* remoteness and the Windows process-start-time gate for the resolved workspace.
* remoteness and per-agent support for the resolved workspace.
*/
export function downgradeAgentLaunchModeForHost(
receipt: AgentLaunchModeReceipt,
Expand Down
54 changes: 9 additions & 45 deletions src/main/claude/claude-agent-sdk-exit-proof-identity.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { describe, expect, it, vi } from 'vitest'
import type { DescendantSnapshot } from '../pty-descendant-termination'
import type { WindowsDescendantSnapshot } from '../windows-descendant-exit-verification'
import { collectDescendantRows } from '../pty-descendant-termination'
import { createClaudeChildTreeReaper } from './claude-agent-sdk-exit-proof'
import { mergeClaudeCapturedTrees } from './claude-child-tree-snapshot'
import { mergeClaudeDescendantSnapshots } from './claude-child-tree-snapshot'

function posixSnapshot(capturedAtMs: number): DescendantSnapshot {
return {
Expand All @@ -14,43 +13,28 @@ function posixSnapshot(capturedAtMs: number): DescendantSnapshot {
}
}

function windowsSnapshot(): WindowsDescendantSnapshot {
return {
root: { pid: 100, creationTimeMs: 5 },
descendants: [{ pid: 200, creationTimeMs: 7 }],
unidentifiedCount: 0,
capturedAtMs: 1
}
}

describe('Claude child root identity', () => {
it('keeps a retained row boundary when a refresh observes no new descendants', () => {
const previous = posixSnapshot(1_700_000_000_900)
const next = posixSnapshot(1_700_000_002_100)

expect(
mergeClaudeCapturedTrees(
{ platform: 'posix', tree: previous },
{ platform: 'posix', tree: next }
)
).toEqual({
platform: 'posix',
tree: { ...next, capturedAtMsByPid: { '200': previous.capturedAtMs } }
expect(mergeClaudeDescendantSnapshots(previous, next)).toEqual({
...next,
capturedAtMsByPid: { '200': previous.capturedAtMs }
})
})

it('keeps the descendant verdict when a POSIX root probe is unavailable', async () => {
it('keeps the descendant verdict alongside the POSIX root kill', async () => {
const child = { pid: 100, kill: vi.fn(() => true) }
const terminateDescendants = vi.fn(async () => 'exited' as const)
const tree = createClaudeChildTreeReaper(child, {
platform: 'linux',
captureDescendants: vi.fn(async () => posixSnapshot(1)),
terminateDescendants,
verifyRootIdentity: vi.fn(async () => false)
terminateDescendants
})

// POSIX runs no bare-pid root operation, so a declined probe withholds
// nothing: the handle kill still lands and the verification still speaks.
// POSIX runs no bare-pid root operation: the handle kill lands and the
// verification still speaks.
await expect(tree.reap()).resolves.toBe('exited')
expect(terminateDescendants).toHaveBeenCalled()
expect(child.kill).toHaveBeenCalledWith('SIGKILL')
Expand All @@ -72,8 +56,7 @@ describe('Claude child root identity', () => {
1
)
),
terminateDescendants,
verifyRootIdentity: vi.fn(async () => true)
terminateDescendants
})

await expect(tree.reap()).resolves.toBe('unverifiable')
Expand All @@ -82,23 +65,4 @@ describe('Claude child root identity', () => {
expect(terminateDescendants).not.toHaveBeenCalled()
expect(child.kill).toHaveBeenCalledWith('SIGKILL')
})

it('fails closed when Windows root identity revalidation is unavailable', async () => {
const child = { pid: 100, kill: vi.fn(() => true) }
const terminateWindowsTree = vi.fn(async () => {})
const terminateWindowsDescendants = vi.fn(async () => 'exited' as const)
const tree = createClaudeChildTreeReaper(child, {
platform: 'win32',
captureWindowsDescendants: vi.fn(async () => windowsSnapshot()),
terminateWindowsTree,
terminateWindowsDescendants,
verifyRootIdentity: vi.fn(async () => false)
})

await expect(tree.reap()).resolves.toBe('unverifiable')
// taskkill /T /F addresses a bare pid and stays gated; the handle does not.
expect(terminateWindowsTree).not.toHaveBeenCalled()
expect(terminateWindowsDescendants).not.toHaveBeenCalled()
expect(child.kill).toHaveBeenCalledWith('SIGKILL')
})
})
Loading
Loading