Skip to content

test(catalog): budget the K-owner file and reap children before teardown (Windows stack 3/3) - #3550

Merged
lidge-jun merged 5 commits into
devfrom
codex/win-3-k-owner-budget
Sep 5, 2026
Merged

test(catalog): budget the K-owner file and reap children before teardown (Windows stack 3/3)#3550
lidge-jun merged 5 commits into
devfrom
codex/win-3-k-owner-budget

Conversation

@lidge-jun

@lidge-jun lidge-jun commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Summary

Windows suite stabilization, PR 3 of 3 (stacked on #3549). Fixes the codex-retained-root-serialization timeouts on the hosted Windows runner and the teardown race they exposed.

Run 33920624827 (windows 2/4): the three-child K-owner case timed out at its 15 s budget, Bun reported "killed 2 dangling processes", and the finally then wrote a release marker into a sandbox root afterEach had already removed (ENOENT). Budgeting only that case (first commit) moved the failure one case down the file — 18.7 s passed under the new budget, the next case then timed out at 20 s by 140 ms with an unhandled "sync exited before provider barrier (143)" on top. The second commit fixes the class:

  • All four explicit per-case budgets become SPAWN_BUDGET_MS, the repository's named bound for real child processes. Every case boots the same kind of Bun child on the same runner (8-19 s each, 2× run-to-run variance) and was sized the same way from a ~450 ms local run.
  • Every Bun.spawn registers with the sandbox; teardownSandbox() releases holders, kills survivors, and awaits every exit before the root is deleted. afterEach is async and runs it first.
  • raceBarrier() attaches a no-op catch to the early-exit branch up front, so a child killed after the barrier wins cannot surface as an unhandled rejection.

Ablation per test-budget.ts rule 2 was run before the fix: the one-token mutation BEGIN IMMEDIATE → BEGIN in catalog-write-serialization.ts turns the behavioural existsSync assertion red, so the wait is intrinsic and the case is not vacuous. The barrier fix was verified by a probe (5 s sleep after the barrier under a 3 s budget): original code reports the timeout plus an unhandled 143, fixed code reports the timeout alone. Two intermediate attempts that failed that probe were discarded.

No product change.

Verification

  • bun test tests/codex-integration/codex-retained-root-serialization.test.ts (macOS): 6 pass
  • GitHub Actions run 33926041666 (before rebase onto dev): windows 1/4 4462/0, 2/4 4628/0, 3/4 4305/0, 4/4 4413/0, no dangling-process line in any log; the six cases in this file ran 1.8-9.9 s under a 45 s budget
  • Run 33928082123 dispatched on the rebased head as the second consecutive confirmation
  • bun run typecheck clean

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Summary by CodeRabbit

  • Documentation

    • Updated Windows stabilization records with revised ACL findings and clarified that the previously reported ACL defect is retracted.
    • Added investigation notes for a remaining OAuth cleanup failure and CI retained-root failures.
    • Documented confirmation results showing Windows suite failures reduced from 25 to 1, with acceptance criteria still pending.
  • Tests

    • Improved Windows integration-test cleanup by tracking spawned processes and ensuring orderly teardown.
    • Added coverage for preserving native ChatGPT entries in v1 multi-agent mode.

@lidge-jun
lidge-jun requested a review from Ingwannu as a code owner September 4, 2026 23:05
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Team

Run ID: 21f03933-e14e-4fe6-abc8-20e2f0cccc80

📥 Commits

Reviewing files that changed from the base of the PR and between 9b9f8ae and ffa6881.

📒 Files selected for processing (8)
  • devlog/_plan/260905_windows_suite_stabilization/000_plan.md
  • devlog/_plan/260905_windows_suite_stabilization/002_v140_baseline.md
  • devlog/_plan/260905_windows_suite_stabilization/007_acl_defect_retracted.md
  • devlog/_plan/260905_windows_suite_stabilization/008_oauth_lease_residual.md
  • devlog/_plan/260905_windows_suite_stabilization/009_confirmation_run_1.md
  • devlog/_plan/260905_windows_suite_stabilization/050_ci_residual_retained_root.md
  • tests/codex-integration/codex-retained-root-serialization.test.ts
  • tests/codex-integration/multi-agent-keep-native-v1.test.ts

📝 Walkthrough

Walkthrough

The PR updates Windows-suite stabilization records, retracts an ACL diagnosis, documents an unresolved OAuth cleanup failure, records confirmation runs, and hardens retained-root integration tests by reaping child processes before sandbox deletion.

Changes

Windows suite stabilization

Layer / File(s) Summary
Baseline and ACL diagnosis retraction
devlog/_plan/260905_windows_suite_stabilization/000_plan.md, devlog/_plan/260905_windows_suite_stabilization/002_v140_baseline.md, devlog/_plan/260905_windows_suite_stabilization/007_acl_defect_retracted.md, devlog/_plan/260905_windows_suite_stabilization/009_confirmation_run_1.md
The records retract the ACL-seam diagnosis, identify killed-run contamination, update the defect count, and record a confirmation run with one remaining failure.
OAuth residual investigation
devlog/_plan/260905_windows_suite_stabilization/008_oauth_lease_residual.md
The document records reproduction and isolation measurements for the remaining EPERM failure. The cause remains unidentified and no fix is attempted.
Retained-root cleanup and timeout handling
devlog/_plan/260905_windows_suite_stabilization/050_ci_residual_retained_root.md, tests/codex-integration/codex-retained-root-serialization.test.ts
The sandbox tracks spawned children and release markers. Teardown releases markers, kills and awaits children, then removes the root. Test budgets use SPAWN_BUDGET_MS, and child exit races use raceBarrier.
Launcher parsing test clarification
tests/codex-integration/multi-agent-keep-native-v1.test.ts
Comments and the test name clarify semantic argv extraction, malformed-input rejection, and launcher-identity boundaries.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Suggested reviewers: ingwannu

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/win-3-k-owner-budget

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Deterministic PR hygiene checks passed.

@github-actions github-actions Bot added the chore Maintenance, CI, tests, refactors, or build changes (not a user-facing bug or feature). label Sep 4, 2026

@Ingwannu Ingwannu left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The substantive test-harness direction is sound: all asynchronous children are registered before they can escape teardown, holders are released before kill/await/delete, the late rejecting race branch is marked handled, and the named spawn budget replaces local-machine-derived timeouts without removing the behavioral assertions. The retained-root ablation evidence is also useful.

This PR currently targets codex/win-2-unlinked-cwd, which is not an accepted integration target under the repository policy. Please keep the stack relationship while the predecessors are open, but after #3548 and #3549 land, retarget/rebase this layer onto the then-current dev and run exact-head CI on that final diff. Do not merge it into the temporary stack branch. I will re-review the retargeted head, including the final multi-agent-keep-native-v1 documentation-only delta.

@lidge-jun

Copy link
Copy Markdown
Owner Author

리뷰 · 우선순위 68 / 80

설명

이 PR은 Windows 스위트 안정화 스택의 마지막 장(3/3)이다. 베이스는 codex/win-2-unlinked-cwd(#3549)이고, 제품 소스는 건드리지 않는다. 지금 dev HEAD는 7eddfb3eb(패키지 2.43.0)이며, 방금 #3541(호출자 취소가 upstream 실패 로그에 안 섞이게)과 #3542(continuation spill 쓰기 건강)가 들어왔다. 이 스택은 그 응답/취소 레일과 별도로, 호스티드 Windows CI에서만 터지던 테스트 하네스를 고친다.

실패 이야기는 측정으로 잘 정리돼 있다. CI run 33920624827의 windows 2/4에서 tests/codex-integration/codex-retained-root-serialization.test.ts의 세 자식 K-owner 케이스(락 홀더가 살아 있는 동안 startup 프로브와 CLI sync-cache가 직렬로 도는 케이스)가 15초 예산에 타임아웃했다. Bun이 dangling process 두 개를 죽이고, finallyholder.release()가 이미 afterEach가 지운 샌드박스 루트에 마커를 쓰려다 ENOENT를 냈다. 로컬 macOS는 ~450ms라서 15초가 넉넉해 보였지만, windows-latest에서 Bun 자식이 마커까지 가는 데만 8–11초라는 기존 주석과 맞지 않았다. tests/helpers/test-budget.tsSPAWN_BUDGET_MS(45초)가 바로 “실제 자식 프로세스”용으로 이름 붙여 둔 예산이다.

첫 커밋이 그 한 케이스만 올리면 실패가 파일 안 다음 케이스로 미끄러졌다. 18.7초로 통과한 뒤, convergence POST 케이스가 20초를 140ms 넘기며 또 타임아웃하고, 배리어 이후 죽은 자식의 143 거부가 unhandled로 위에 얹혔다. 두 번째 커밋이 그걸 클래스로 고친다. 명시 예산 네 곳을 모두 SPAWN_BUDGET_MS로 통일하고, 인라인 Bun.spawn까지 샌드박스 children에 등록하며, teardownSandbox()가 홀더 해제 → kill → exited await → 그다음에야 루트 삭제를 한다. raceBarrier()는 배리어가 이긴 뒤에도 남아 있던 거절 Promise에 미리 no-op catch를 붙여, 타임아웃 킬이 unhandled로 안 보이게 한다. ablation도 규칙에 맞게 했다. src/codex/catalog-write-serialization.tsBEGIN IMMEDIATEBEGIN으로 바꾸면 동작 단언(existsSync(cachePath)가 false여야 함)이 빨개진다고 적혀 있고, 지금 HEAD에도 그 IMMEDIATE 줄이 그대로 있다.

같이 실린 계획 문서(008/009/050, 007 정정)는 가치가 크다. ACL 시임 22실패는 오염이었고 철회했고, OAuth 리스 잔여 1건은 아직 원인 미확정이라 고치지 않겠다고 명시했다. 추측 패치 대신 “1 fail remaining, cause unidentified”로 남겨 두는 태도가 이 유닛의 방법론과 맞다. multi-agent-keep-native-v1.test.ts의 SCOPE 주석은 #3548 헬퍼가 실행 파일 정체성까지 보지 않는다는 경계를 분명히 한 것이고, 런처 계약은 기존 codex-v2-gate/win-exec가 계속 맡는다. types.ts/config.ts 분할과도 무관하다.

라인 - teardownSandbox · release → kill → await exited → clear 순서가 CI에서 본 ENOENT/dangling을 정면으로 막는다. 좋다.
라인 - 네 곳의 명시 예산이 모두 SPAWN_BUDGET_MS · 한 케이스만 올리면 실패가 옆으로 이동한다는 두 번째 CI 교훈을 반영했다. 맞다.
라인 - raceBarrierexitedEarly.catch(() => undefined) · 배리어 승 이후 143 unhandled를 없앤다. 프로브(배리어 뒤 5초 sleep + 3초 예산)로 검증했다고 본문에 적혀 있다.
라인 - 첫 케이스의 Bun.spawnSync CLI 단계 · 여전히 동기라 중간 kill이 안 된다. 주석·본문이 follow-up으로 남겼고, teardown이 비동기를 먼저 거두니 최악은 “느림”이지 dangling+ENOENT는 아니다. 지금은 머지를 막을 결함은 아니다.
경로 waitForPath(..., 12_000|16_000) - 바깥 예산 안의 내부 데드라인이라 test-budget.ts 안내와 맞다. 그대로 둬도 된다.
경로 devlog/.../008_oauth_lease_residual.md - 잔여 1건을 고치지 않기로 한 기록이 착륙에 포함된다. 의도된 감사 로그인지 확인만 하면 된다(본문·추천과 일치).

메인테이너의 판단이 필요한 지점

너의 추천
#3548·#3549가 먼저 또는 함께 초록이면 이 PR을 LAND_AS_IS로 머지하자. 제품 변경 없고, 예산·teardown·배리어를 클래스로 고쳤으며 ablation과 CI 확인 주장도 구체적이다. spawnSync 비동기화와 OAuth 잔여는 후속으로 두고, 머지 후 leftover 원본이 있으면 landed-via 정리 규칙을 적용하면 된다.

이 댓글은 grok-bot이 작성했습니다

jun added 5 commits September 5, 2026 11:37
Implementation review found three overclaims, none in behaviour:

- featureActionOf never sees `file` and accepts any .cmd/.bat target, so it
  cannot reject a non-Codex invocation. It extracts semantic arguments; the
  executable identity is pinned by codex-v2-gate and win-exec. Said so in the
  helper, renamed the case to 'rejects malformed argv'.
- 007 claimed the killed process kept its own handle. Windows closes a terminated
  process's handles, and no handle-owner snapshot was taken, so the honest claim
  is that the killed run left a holder that was not identified. The operational
  rule does not depend on which.
- 002 still read as a live ACL diagnosis; it now carries a superseded banner and
  000 labels it as raw shard counts only.
…failure

Confirmation run 1: 25 fail -> 1. Both fixes verified on Windows; shards 1, 3
and 4 are green.

The survivor is documented to the limit of what was measured, including three
dead ends: it is not the file alone (0/5 solo), not rmSync (isolated probe
passes all four shapes), and not the leftover directory (pre-creating it
passes). A two-file minimal repro exists but only reproduces under load - eight
consecutive idle runs pass in 5.5s while every failing run took 119s with 5.2s
teardowns, which is removeTreeWithRetry's full retry budget.

The handle-owner snapshot that would close it failed: a 1 Hz watcher polling
through powershell.exe became the load itself, broke 72 cases across ten
unrelated suites, and never sampled the millisecond window. That run is
discarded. Recorded because it is the same mistake as 007 - adding a process to
the box and reading the result as a property of the code.

No fix attempted for it: one case in 17807, no idle reproduction, and two prior
read-only diagnoses in this unit were both wrong.
…cally

Same case, same log offset, on a fully idle box. Three shard-2 runs, three
identical failures, so the load hypothesis from measurement 6 is corrected: load
explains why the two-file pair needed it, not why the shard fails. Binary
search over shard 2 with oauth-store-multi pinned last is the cheapest next
step.
…before teardown

CI run 33920624827 (windows 2/4): the case timed out at its 15 s per-test
budget, Bun killed two dangling children, and the finally then wrote a release
marker into a sandbox root afterEach had already removed (ENOENT). The case
spawns a lock holder that stays alive plus a startup probe and a real CLI
sync-cache in series - three Bun boots, two importing the server/CLI graphs,
at 8-11 s each on windows-latest. 15 s was sized from the ~450 ms local run.

Budget: SPAWN_BUDGET_MS, the repository's named bound for real child
processes. Verified per test-budget.ts rule 2 - a one-token ablation
(BEGIN IMMEDIATE -> BEGIN at catalog-write-serialization.ts:188) turns the
behavioural existsSync assertion red, so the wait is intrinsic and the case is
not vacuous.

Teardown: children are tracked on the sandbox and teardownSandbox() releases
holders, kills survivors, and AWAITS every exit before the root is deleted;
afterEach is async and runs it first, and holder.release() tolerates ENOENT.
Forced to a 1 ms budget the case now fails cleanly with no unhandled ENOENT.
The Bun.spawnSync CLI step stays synchronous; converting it is a follow-up.
…nd detach the barrier race

CI run 33923803071 (windows 2/4): the first fix moved the failure one case down
the file. The startup+CLI case ran 18.7 s and passed under SPAWN_BUDGET_MS; the
convergence case then timed out at its 20 s by 140 ms, with 'killed 1 dangling
process' and an unhandled 'sync exited before provider barrier (143)' on top.
Budgeting one case instead of the class was the mistake.

All four explicit budgets in the file become SPAWN_BUDGET_MS: every case boots
the same kind of Bun child on the same runner and was sized the same way.

The four inline Bun.spawn calls now register with the sandbox so the reap-
before-delete teardown covers them; the previous commit only reached the two
helpers.

raceBarrier() replaces the duplicated Promise.race: the early-exit branch is a
rejecting promise that stays pending after the barrier wins, and when a timeout
later kills the child that rejection has no awaiter - which is the unhandled
error CI showed. A no-op catch attached up front marks it handled without
changing the race. Verified by inserting a 5 s sleep after the barrier under a
3 s budget: the original reports the timeout PLUS an unhandled error; this
reports the timeout alone. Probe removed; 6/6, typecheck clean.
@lidge-jun
lidge-jun force-pushed the codex/win-3-k-owner-budget branch from 60425ed to ffa6881 Compare September 5, 2026 02:38
@lidge-jun
lidge-jun merged commit 8860964 into dev Sep 5, 2026
8 of 9 checks passed
@lidge-jun
lidge-jun deleted the codex/win-3-k-owner-budget branch September 5, 2026 02:39
lidge-jun pushed a commit that referenced this pull request Sep 5, 2026
… server (Windows class fix)

Five dispatches of this stack on windows-latest failed on five DIFFERENT
literal deadlines, none of them an assertion: 15 s and 20 s case budgets
(#3550), then a 10 s waitFor default in codex-write-lock and a 30 s per-request
abort in codex-composed-acceptance on run 33930757649. Each run samples one or
two new ones because a spawned Bun child boots in 8-19 s on that runner and the
suite has many waits sized below that from local timings.

Inventory in devlog 080: 58 literal deadlines under tests/, classified by what
they gate on. This commit changes the ones that gate on a spawned child or a
live server (class A/B): helper DEFAULTS (waitFor, waitForPath, waitForPort,
waitForLiveWorker, waitForIdle) and inline deadlines now go through
watchdogMs(), which keeps the local number and applies the CI/platform floor
(45 s on Windows). The held-request in composed-acceptance gets two server
budgets because it spans a startup plus a held gather, not one round-trip.
Deliberately-short bounds (500 ms /healthz probes, refused-connection checks,
in-process polls) are untouched; class D sites are left for the next pass.

12 test files; typecheck clean. Local verification: 59 pass across the three
codex-integration files run in isolation; a full local sweep was blocked by
another session holding the user test lock, so the CI dispatch is the gate.
lidge-jun added a commit that referenced this pull request Sep 5, 2026
…erver (Windows stack 5/5) (#3558)

* docs(devlog): run 5 exposes two more sub-floor internal waits; plan the class inventory (080)

* docs(devlog): inventory every sub-floor internal wait in tests/ and classify it (080)

* test: budget every sub-floor internal wait that gates on a child or a server (Windows class fix)

Five dispatches of this stack on windows-latest failed on five DIFFERENT
literal deadlines, none of them an assertion: 15 s and 20 s case budgets
(#3550), then a 10 s waitFor default in codex-write-lock and a 30 s per-request
abort in codex-composed-acceptance on run 33930757649. Each run samples one or
two new ones because a spawned Bun child boots in 8-19 s on that runner and the
suite has many waits sized below that from local timings.

Inventory in devlog 080: 58 literal deadlines under tests/, classified by what
they gate on. This commit changes the ones that gate on a spawned child or a
live server (class A/B): helper DEFAULTS (waitFor, waitForPath, waitForPort,
waitForLiveWorker, waitForIdle) and inline deadlines now go through
watchdogMs(), which keeps the local number and applies the CI/platform floor
(45 s on Windows). The held-request in composed-acceptance gets two server
budgets because it spans a startup plus a held gather, not one round-trip.
Deliberately-short bounds (500 ms /healthz probes, refused-connection checks,
in-process polls) are untouched; class D sites are left for the next pass.

12 test files; typecheck clean. Local verification: 59 pass across the three
codex-integration files run in isolation; a full local sweep was blocked by
another session holding the user test lock, so the CI dispatch is the gate.

* test: keep internal waits under their case budgets - INTERNAL_DEADLINE_MS inside, SPAWN_BUDGET_MS outside

Review of 3b431b4 (FAIL, 6 blockers) caught a composition error: watchdogMs()
is 45 s on Windows CI, and I had put it on INTERNAL waits inside cases budgeted
at 15-30 s. On the platform the fix targets, Bun's per-test timeout would fire
before the wait's own diagnostic - a bare timeout instead of the marker name,
which is where this unit started. test-budget.ts:64 states the invariant:
an internal deadline stays a few times under its enclosing budget.

Corrected shape, two knobs moved together:
- every child/server-gated internal wait uses INTERNAL_DEADLINE_MS (15 s),
  the repository's named in-test bound, as windows-tray and cli-models already
  do; helper DEFAULTS changed so callers inherit
- every case whose body spawns a child gets SPAWN_BUDGET_MS (45 s): the four
  15 s and three 30 s spawn cases in native-profile-manager, all four
  codex-write-lock cases, history-lock, history-worker, oauth-refresh-lock

Other blockers folded:
- retained-root :515 (8 s two-child barrier) and :555 (20 s attempt loop) were
  in my own inventory and unchanged; now INTERNAL_DEADLINE_MS / SPAWN_BUDGET_MS
- storage-policy-job-responsive fell through on expiry with no assertion, so a
  job that never went idle still passed; expect(settled) added - a real
  vacuous test independent of Windows
- composed-acceptance: SERVER_BUDGET_MS * 2 was arithmetic on a wrong model
  (the held request and the OFF mutation overlap; startup precedes both).
  Named HELD_REQUEST_BUDGET_MS = SERVER_BUDGET_MS + INTERNAL_DEADLINE_MS with
  the actual shape in its comment
- ten more A/B candidates from the reviewer's re-grep read one by one: five
  budgeted (codex-shim, codex-prompt-route, codex-prompt-text-probe,
  helpers/storage-policy-api, storage-mutation-race), five left with a reason
  each in devlog 080 (in-process polls, an asserted-latency bound, a preflight
  whose failure is a skip)

Ablation (test-budget.ts rule 2), both run on macOS with the lock free:
- history-lock: holder's ready-marker write disabled -> 'timed out waiting for
  .../held' from the helper at 15.0 s, the enclosing 45 s case intact
- storage-policy-job-responsive: idle transition masked -> expect(settled)
  Expected true / Received false; before this commit the same mutation passed

17 test files + 1 helper; each file run individually on macOS: 331 pass /
0 fail; typecheck clean.

---------

Co-authored-by: t <a@b.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore Maintenance, CI, tests, refactors, or build changes (not a user-facing bug or feature).

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants