Skip to content

fix(server): ACP teardown survives processes exiting during the /proc scan - #36719

Merged
juliusmarminge merged 2 commits into
mainfrom
factory/issue-36707
Nov 14, 2026
Merged

juliusmarminge merged 2 commits into
mainfrom
factory/issue-36707

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Nov 14, 2026 •

Copy link
Copy Markdown
Member

This round needed no code change. The brief said nothing had to change yet; this step was only to watch PR #36719. The fix (unchanged): makeLinuxProcessTreeController().snapshot now lists /proc by name with readdirSync("/proc") and keeps numeric entries, so Node no longer lstats each dirent; that lstat threw ENOENT when a process exited mid-scan and poisoned the ACP session. readLinuxProcessIdentity already skips vanished processes. All 37 checks passed on 53935ea. Macroscope approved with no line comments. CodeRabbit reported no actionable comments and minimal merge risk. No high-or-above findings were open, so I stopped watching the PR.

Fixes #36707

Reproduction on main

Commit origin/main 96735d7, Node v26.13.1, Linux. (1) Real run: a script imported packages/provider-acp/src/server/AcpSessionRuntime.ts, built the real controller with makePosixProcessTreeController("linux") and called snapshot() in a tight loop for 12s while a separate bash loop started batches of 20 short-lived /bin/true processes. Result: 61 of 7697 snapshot() calls threw Error: ENOENT: no such file or directory, lstat '/proc/83724', the reporter's exact error from readdirSync("/proc", { withFileTypes: true }). (2) Deterministic test: added a Linux-only case to apps/server/src/provider/acp/AcpSessionRuntime.processTree.test.ts. It patches fs.readdirSync (via createRequire + syncBuiltinESMExports) to throw ENOENT/lstat for the withFileTypes read of /proc and expects snapshot() to still return the current process. vp test run src/provider/acp/AcpSessionRuntime.processTree.test.ts fails on main (1 failed | 17 passed). With the reporter's proposed fix temporarily applied (readdirSync("/proc") by name, numeric entries only), all 18 tests pass; the fix was reverted and not committed.

Evidence

Stop an ACP thread during real Linux process churn, then send another message to the same session. Fresh synthetic databases; Chromium, Linux x64, Node v26.12.1; identical viewport and theme. A free ACP surrogate supplies model responses; production teardown and /proc reads run unchanged. Cgroups are read-only in a private mount namespace to exercise the process-tree fallback.

Before After
96735d7311 53935eae57
Before: continuation fails After: continuation responds
Before recording After recording

Across 30 live server cycles, base poisoned 9 sessions; head poisoned none and resumed all 30. The recordings show the same additional Stop-and-continue interaction with eight churn workers.

Reproduction scripts, instructions and runtime transcripts · Server driver

Not shown:

  • Paid Grok model behavior and authentication were not exercised; a free ACP surrogate supplied protocol responses through the real adapter.
  • The scenario runner was not used because its replay provider does not select the Linux process-ownership path involved here.

Tests

No new runs this round. CI on 53935ea: all 37 checks passed (Build and Typecheck, Test, Test Server 1-8, Test Web 1-2, Macroscope checks, CodeRabbit). Earlier: vp test run src/provider/acp/AcpSessionRuntime.processTree.test.ts 18/18 passed and vp run check:changed passed everything.

Not in this change

  • sweepStaleLinuxCgroupSiblings and the cgroup remove() helper also use readdirSync withFileTypes, but on cgroupfs (which reports entry types and is not affected by process exit), so they were left unchanged.
  • sweepStaleLinuxCgroupSiblings and the cgroup remove() helper also use readdirSync withFileTypes, but on cgroupfs, which reports entry types, so they were left unchanged.

Who did the work

  • Written by Claude Opus 5.5, in Claude Code
  • Reviewed by GPT-6.1 Sol, in Codex, and by Claude Opus 5.5, each on its own
  • Proof captured by GPT-6.1 Sol and checked by Claude Opus 5.5

Run by the t3code factory in T3 Code.

juliusmarminge and others added 2 commits November 14, 2026 12:05
…id-scan

Fails on main: the Linux process-tree snapshot reads /proc with
withFileTypes, and the lstat Node does for an entry of unknown type
throws ENOENT when that process exits during the scan (#36707).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… scan

The Linux process-tree snapshot listed /proc with withFileTypes, and the
lstat Node does for entries of unknown type threw ENOENT when a process
exited mid-scan, poisoning the session. List /proc by name instead;
readLinuxProcessIdentity already skips vanished processes.

Fixes #36707

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added vouch:trusted Author is trusted by repo permissions or the VOUCHED list. size:XS 0-9 changed lines (additions + deletions). labels Nov 14, 2026
@macroscopeapp

macroscopeapp Bot commented Nov 14, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Approved at 53935ea

Macroscope's review found this PR approvable — Linux ACP teardown now lists /proc by name so processes exiting during enumeration do not abort the snapshot. Existing identity and ownership safeguards remain unchanged, keeping the change narrowly scoped.

You can add or adjust custom eligibility rules. Learn more.

@github-actions

Copy link
Copy Markdown
Contributor

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Codex Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Codex Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Codex Live turn WebSocket decoded 21.5 KiB 21.5 KiB 0 B (0.0%) 24.4 KiB ✅
Codex Live turn messages 1 1 0 (0.0%) 8 ✅
Claude Total thread wire 5.0 KiB 5.0 KiB 0 B (0.0%) 6.8 KiB ✅
Claude Thread snapshot wire 3.8 KiB 3.8 KiB 0 B (0.0%) 4.9 KiB ✅
Claude Live turn WebSocket wire 1.2 KiB 1.2 KiB 0 B (0.0%) 2.0 KiB ✅
Claude Live turn WebSocket decoded 21.9 KiB 21.9 KiB 0 B (0.0%) 24.4 KiB ✅
Claude Live turn messages 1 1 0 (0.0%) 8 ✅

Other scenarios

All 64 scenario measurements match main.

All 64 scenario measurements
Provider Scenario Metric Main baseline This PR Impact PR ceiling
Codex short turn resume WebSocket decoded 10.5 KiB 10.5 KiB 0 B (0.0%) 12.7 KiB ✅
Codex short turn resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex long turn running, cold open HTTP decoded 281.7 KiB 281.7 KiB 0 B (0.0%) 302.7 KiB ✅
Codex long turn running, cold open rows 75 75 0 (0.0%) 75 ✅
Codex long turn running, history page HTTP decoded 92.4 KiB 92.4 KiB 0 B (0.0%) 107.4 KiB ✅
Codex long turn running, history page rows 75 75 0 (0.0%) 75 ✅
Codex long turn finished, cold open HTTP decoded 381.5 KiB 381.5 KiB 0 B (0.0%) 410.2 KiB ✅
Codex long turn finished, cold open rows 151 151 0 (0.0%) 151 ✅
Codex long turn finished, socket cold open WebSocket decoded 381.6 KiB 381.6 KiB 0 B (0.0%) 410.2 KiB ✅
Codex long turn finished, socket cold open WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex long turn finished, socket cold open rows 151 151 0 (0.0%) 151 ✅
Codex long turn finished, work page HTTP decoded 186.2 KiB 186.2 KiB 0 B (0.0%) 200.2 KiB ✅
Codex long turn finished, work page rows 150 150 0 (0.0%) 150 ✅
Codex idle thread resume WebSocket decoded 65 B 65 B 0 B (0.0%) 200 B ✅
Codex idle thread resume WebSocket messages 1 1 0 (0.0%) 1 ✅
Codex fleet shell snapshot HTTP decoded 42.4 KiB 42.4 KiB 0 B (0.0%) 53.7 KiB ✅
Codex fleet shell snapshot rows 27 27 0 (0.0%) 27 ✅
Codex fleet shell subscribe WebSocket decoded 65 B 65 B 0 B (0.0%) 200 B ✅
Codex fleet shell subscribe WebSocket messages 1 1 0 (0.0%) 1 ✅
Codex fleet shell resume check HTTP decoded 18 B 18 B 0 B (0.0%) 100 B ✅
Codex fleet shell resume check rows 0 0 0 0 ✅
Codex stale shell reload HTTP decoded 42.4 KiB 42.4 KiB 0 B (0.0%) 53.7 KiB ✅
Codex stale shell reload rows 27 27 0 (0.0%) 27 ✅
Codex stale shell resume WebSocket decoded 140 B 140 B 0 B (0.0%) 400 B ✅
Codex stale shell resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Codex stale todo list cold open HTTP decoded 72.4 KiB 72.4 KiB 0 B (0.0%) 83.0 KiB ✅
Codex stale todo list cold open rows 75 75 0 (0.0%) 75 ✅
Codex stale todo list cold open nodes 2 2 0 (0.0%) 4 ✅
Codex stale todo list cold open subagents 2 2 0 (0.0%) 4 ✅
Codex stale todo list cold open plans 1 1 0 (0.0%) 2 ✅
Codex stale todo list cold open providerTurns 1 1 0 (0.0%) 2 ✅
Codex stale todo list cold open checkpoints 1 1 0 (0.0%) 2 ✅
Claude short turn resume WebSocket decoded 10.6 KiB 10.6 KiB 0 B (0.0%) 12.7 KiB ✅
Claude short turn resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude long turn running, cold open HTTP decoded 283.3 KiB 283.3 KiB 0 B (0.0%) 302.7 KiB ✅
Claude long turn running, cold open rows 75 75 0 (0.0%) 75 ✅
Claude long turn running, history page HTTP decoded 93.4 KiB 93.4 KiB 0 B (0.0%) 107.4 KiB ✅
Claude long turn running, history page rows 75 75 0 (0.0%) 75 ✅
Claude long turn finished, cold open HTTP decoded 384.2 KiB 384.2 KiB 0 B (0.0%) 410.2 KiB ✅
Claude long turn finished, cold open rows 151 151 0 (0.0%) 151 ✅
Claude long turn finished, socket cold open WebSocket decoded 384.3 KiB 384.3 KiB 0 B (0.0%) 410.2 KiB ✅
Claude long turn finished, socket cold open WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude long turn finished, socket cold open rows 151 151 0 (0.0%) 151 ✅
Claude long turn finished, work page HTTP decoded 188.1 KiB 188.1 KiB 0 B (0.0%) 200.2 KiB ✅
Claude long turn finished, work page rows 150 150 0 (0.0%) 150 ✅
Claude idle thread resume WebSocket decoded 66 B 66 B 0 B (0.0%) 200 B ✅
Claude idle thread resume WebSocket messages 1 1 0 (0.0%) 1 ✅
Claude fleet shell snapshot HTTP decoded 43.0 KiB 43.0 KiB 0 B (0.0%) 53.7 KiB ✅
Claude fleet shell snapshot rows 27 27 0 (0.0%) 27 ✅
Claude fleet shell subscribe WebSocket decoded 66 B 66 B 0 B (0.0%) 200 B ✅
Claude fleet shell subscribe WebSocket messages 1 1 0 (0.0%) 1 ✅
Claude fleet shell resume check HTTP decoded 18 B 18 B 0 B (0.0%) 100 B ✅
Claude fleet shell resume check rows 0 0 0 0 ✅
Claude stale shell reload HTTP decoded 43.0 KiB 43.0 KiB 0 B (0.0%) 53.7 KiB ✅
Claude stale shell reload rows 27 27 0 (0.0%) 27 ✅
Claude stale shell resume WebSocket decoded 140 B 140 B 0 B (0.0%) 400 B ✅
Claude stale shell resume WebSocket messages 2 2 0 (0.0%) 2 ✅
Claude stale todo list cold open HTTP decoded 73.4 KiB 73.4 KiB 0 B (0.0%) 83.0 KiB ✅
Claude stale todo list cold open rows 75 75 0 (0.0%) 75 ✅
Claude stale todo list cold open nodes 2 2 0 (0.0%) 4 ✅
Claude stale todo list cold open subagents 2 2 0 (0.0%) 4 ✅
Claude stale todo list cold open plans 1 1 0 (0.0%) 2 ✅
Claude stale todo list cold open providerTurns 1 1 0 (0.0%) 2 ✅
Claude stale todo list cold open checkpoints 1 1 0 (0.0%) 2 ✅

Baseline: 13a6782 · PR result: 53935ea · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

@coderabbitai

coderabbitai Bot commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

The Linux process-tree snapshot now reads /proc entry names without requesting directory-type metadata. A Linux-only test simulates an ENOENT error during entry resolution and checks that the snapshot includes the current process.

Changes: Linux ACP process snapshot

Layer / File(s) Summary
Read /proc entries and test vanished-entry handling
packages/provider-acp/src/server/AcpSessionRuntime.ts, apps/server/src/provider/acp/AcpSessionRuntime.processTree.test.ts
The snapshot reads entry names and resolves numeric names through readLinuxProcessIdentity. A Linux-only test simulates ENOENT, checks that the snapshot includes the current process, and restores the patched filesystem function and builtin exports.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix · Severity of issue fixed: Low

Merge Risk: Minimal · up to 53935

The change makes Linux process-tree teardown tolerate processes that exit during the /proc scan, so Stop should complete instead of failing the session. No actionable merge-blocking risk remains.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title names the fix and its scope: ACP teardown no longer fails when processes exit during the /proc scan. This matches the Linux process-tree snapshot change in the diff and the linked issue #367…
Linked Issues check Passed [#36707] Requirement: on Linux process-tree teardown, a process that exits during the /proc scan must be ignored, so the snapshot returns the running processes and Stop completes. The diff changes mak…
Out of Scope Changes check Passed The only production change is the /proc snapshot listing in makeLinuxProcessTreeController, which is the fix named by #36707. The only test change adds a Linux-only regression case for that snapshot a…
Description check Passed The description is detailed and covers the problem (ENOENT from lstat during /proc scan poisoning ACP sessions, linked to issue #36707), the change (listing /proc by name with readdirSync and keeping …
Full details: Title check

Explanation

The title names the fix and its scope: ACP teardown no longer fails when processes exit during the /proc scan. This matches the Linux process-tree snapshot change in the diff and the linked issue #36707. The title follows the conventional commit form the template asks for.

Full details: Linked Issues check

Explanation

[#36707] Requirement: on Linux process-tree teardown, a process that exits during the /proc scan must be ignored, so the snapshot returns the running processes and Stop completes. The diff changes makeLinuxProcessTreeController().snapshot in packages/provider-acp/src/server/AcpSessionRuntime.ts from readdirSync("/proc", { withFileTypes: true }) to readdirSync("/proc"). Names are returned as strings, so Node does not lstat each entry and the ENOENT from a vanished process cannot be raised there. Numeric names still pass the anchored regex ^[1-9][0-9]*$ and go to readLinuxProcessIdentity, which the issue states already skips vanished processes. I did not re-read readLinuxProcessIdentity; that step relies on the issue and PR text. The added test in apps/server/src/provider/acp/AcpSessionRuntime.processTree.test.ts forces the withFileTypes-style ENOENT for /proc and asserts that snapshot() still includes process.pid. This covers the deterministic reproduction the issue asks for. The cgroup and macOS/Windows paths are unchanged, as the issue scope requires. The PR body's real-app before/after evidence is still pending. It is not a coding requirement, so it does not affect this result.

Full details: Out of Scope Changes check

Explanation

The only production change is the /proc snapshot listing in makeLinuxProcessTreeController, which is the fix named by #36707. The only test change adds a Linux-only regression case for that snapshot and restores the patched fs.readdirSync and ESM builtin exports in a finally block. The PR leaves sweepStaleLinuxCgroupSiblings and the cgroup remove() helper unchanged, which matches the issue's exclusion of cgroup teardown. No unrelated files appear in the reviewed diffs. The PR description and PR summary are documentation of the same change, not code scope.

Full details: Description check

Explanation

The description is detailed and covers the problem (ENOENT from lstat during /proc scan poisoning ACP sessions, linked to issue #36707), the change (listing /proc by name with readdirSync and keeping numeric entries), scope (fix for a reported bug, linked issue, cgroup paths deliberately excluded), and verification (reproduction on main with a deterministic test that fails before and passes with the fix, CI results, and before/after evidence). It also names the model and harness used. It has some gaps: the Problem section does not separate a clear expected-behavior statement from the reproduction, the Change section is brief, the Scope section does not explicitly justify the absence of a maintainer request, and the Tests section contains a duplicated bullet about cgroup paths in the 'Not in this change' section. The description also includes run-process narrative ("the brief said nothing had to change yet", "I stopped watching the PR") that does not match the template's requirements. Overall the template's required information is present, so the check passes.

Finishing Touches

Generate unit tests (beta)

  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts
Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 13a6782 and 53935ea.

Files selected for processing: 2

  • apps/server/src/provider/acp/AcpSessionRuntime.processTree.test.ts
  • packages/provider-acp/src/server/AcpSessionRuntime.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 3 remain after this review.

@juliusmarminge
juliusmarminge merged commit 77d3358 into main Nov 14, 2026
38 checks passed
@juliusmarminge
juliusmarminge deleted the factory/issue-36707 branch November 14, 2026 12:56
AIdoesmyjob pushed a commit to AIdoesmyjob/t3code that referenced this pull request Nov 14, 2026
Pipyakas pushed a commit to Pipyakas/t3code that referenced this pull request Nov 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XS 0-9 changed lines (additions + deletions). vouch:trusted Author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Linux: ACP process-tree teardown fails, and poisons the session, when a process exits during the /proc scan

1 participant