[GH-ISSUE #5140] [Bug] Process filesystem snapshots lose handle cleanup ownership and primary outcomes #3161

Closed
opened 2026-10-31 00:37:21 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @morluto on GitHub (Oct 19, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5140

Before submitting

  • Searched open and closed issues (FilesystemSnapshot, snapshot/close, capture/descriptor). Related closed #4458/#4266 concern shared stable artifact reads; #4575 concerns disappearing entries, not snapshot handle finalization.
  • No credentials or private target evidence included.

Area

Process capture or runtime observation.

Observed behavior

Controlled close-fault probes against the compiled snapshot producer at d3627b1038:

  • A successfully hashed real file returns only the injected close error; the completed hash outcome is lost and the exact descriptor remains open.
  • A real file whose stat() and close() both fail returns only the close error. The original stat failure is not retained.
  • Fully enumerating a real directory whose close is denied returns only the close error; its real directory handle remains readable.
    None of these errors exposes a cleanup capability. The probe restored the real methods and explicitly closed every retained handle.

Reproduction

Ran an ignored local Node script against dist/process/capture/FilesystemSnapshot.js on the revision above. It creates a temporary real regular file/directory, intercepts only that file's fs.promises.open() to deny its real handle's close (and separately stat), and injects a callback close denial on Dir.prototype.close before opening the real directory. Calls hashFile() or snapshotRoots() with a parsed bounded scenario. Each of the three cases reproduced once. This is a production-boundary controlled fault, not a public CLI/MCP reproduction or naturally occurring OS failure.

Smallest retained result:

{"file_success":{"exactCloseError":true,"descriptorStillOpen":true},"file_primary":{"exactCloseError":true,"primaryRetained":false,"descriptorStillOpen":true},"directory_success":{"exactCloseError":true,"directoryStillReadable":true}}

Expected behavior

The snapshot resource owner should preserve completed observations or the original failure alongside a failed cleanup lease, retain exact resource authority, block replacement work while cleanup is unresolved, and expose confirmed recovery through the owning capture/session. See docs/tool-design.md#own-the-lifecycle and docs/testing.md#lifecycle-regressions.

Root-cause evidence

src/process/capture/FilesystemSnapshot.ts hashes through a raw FileHandle with finally { await handle.close(); } and enumerates a raw Dir with for await, delegating close to Node's iterator. Neither resource is admitted to the capture resource scope. prepareProcessCapture() takes the initial snapshot before allocating capture runtime resources; the final snapshot likewise has no descriptor owner. A catch or added error message alone cannot fix retained-resource ownership.

Impact and scope

Observed loss of completed hash and primary error, with live resources recoverable only because the probe retained them externally. Broader CLI/MCP cleanup projection and repeated-capture behavior are source-derived risks, not runtime-tested here. Natural close-failure frequency is NOT_ENOUGH_INFO. Directory iterator read-failure plus close-failure precedence has not been reproduced and is not claimed.

Version / runtime / target

REA source d3627b1038; Node24.18.0, macOS arm64. Synthetic local file/directory only; no native engine.

Interface / earliest stage

Other: compiled production snapshot boundary. Capture or observation / final cleanup.

Proposed verification

Use existing owned file/directory lifecycle primitives, preserving the directory owner's documented inability to infer safe retry after a native close rejects. Cover success+close denial, primary+close denial, exact cancellation during finalization, mixed resources, admission/shutdown and confirmed retry. Add public CLI/MCP controlled-fault coverage where the capture lifecycle supports it. These are proposed checks, not executed results.

Originally created by @morluto on GitHub (Oct 19, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5140 ## Before submitting - [x] Searched open and closed issues (FilesystemSnapshot, snapshot/close, capture/descriptor). Related closed #4458/#4266 concern shared stable artifact reads; #4575 concerns disappearing entries, not snapshot handle finalization. - [x] No credentials or private target evidence included. ## Area Process capture or runtime observation. ## Observed behavior Controlled close-fault probes against the compiled snapshot producer at d3627b10382b0f28b12cad1543634e165470b29a: - A successfully hashed real file returns only the injected close error; the completed hash outcome is lost and the exact descriptor remains open. - A real file whose `stat()` and `close()` both fail returns only the close error. The original stat failure is not retained. - Fully enumerating a real directory whose close is denied returns only the close error; its real directory handle remains readable. None of these errors exposes a cleanup capability. The probe restored the real methods and explicitly closed every retained handle. ## Reproduction Ran an ignored local Node script against `dist/process/capture/FilesystemSnapshot.js` on the revision above. It creates a temporary real regular file/directory, intercepts only that file's `fs.promises.open()` to deny its real handle's close (and separately stat), and injects a callback close denial on `Dir.prototype.close` before opening the real directory. Calls `hashFile()` or `snapshotRoots()` with a parsed bounded scenario. Each of the three cases reproduced once. This is a production-boundary controlled fault, not a public CLI/MCP reproduction or naturally occurring OS failure. Smallest retained result: ```json {"file_success":{"exactCloseError":true,"descriptorStillOpen":true},"file_primary":{"exactCloseError":true,"primaryRetained":false,"descriptorStillOpen":true},"directory_success":{"exactCloseError":true,"directoryStillReadable":true}} ``` ## Expected behavior The snapshot resource owner should preserve completed observations or the original failure alongside a failed cleanup lease, retain exact resource authority, block replacement work while cleanup is unresolved, and expose confirmed recovery through the owning capture/session. See `docs/tool-design.md#own-the-lifecycle` and `docs/testing.md#lifecycle-regressions`. ## Root-cause evidence `src/process/capture/FilesystemSnapshot.ts` hashes through a raw FileHandle with `finally { await handle.close(); }` and enumerates a raw Dir with `for await`, delegating close to Node's iterator. Neither resource is admitted to the capture resource scope. `prepareProcessCapture()` takes the initial snapshot before allocating capture runtime resources; the final snapshot likewise has no descriptor owner. A catch or added error message alone cannot fix retained-resource ownership. ## Impact and scope Observed loss of completed hash and primary error, with live resources recoverable only because the probe retained them externally. Broader CLI/MCP cleanup projection and repeated-capture behavior are source-derived risks, not runtime-tested here. Natural close-failure frequency is NOT_ENOUGH_INFO. Directory iterator read-failure plus close-failure precedence has not been reproduced and is not claimed. ## Version / runtime / target REA source d3627b10382b0f28b12cad1543634e165470b29a; Node24.18.0, macOS arm64. Synthetic local file/directory only; no native engine. ## Interface / earliest stage Other: compiled production snapshot boundary. Capture or observation / final cleanup. ## Proposed verification Use existing owned file/directory lifecycle primitives, preserving the directory owner's documented inability to infer safe retry after a native close rejects. Cover success+close denial, primary+close denial, exact cancellation during finalization, mixed resources, admission/shutdown and confirmed retry. Add public CLI/MCP controlled-fault coverage where the capture lifecycle supports it. These are proposed checks, not executed results.
bsakaguchi 2026-10-31 00:37:21 +00:00
  • closed this issue
  • added the
    triage
    bug
    labels
Author
Owner

@morluto commented on GitHub (Oct 19, 2026):

Fixed on main in 9af0b6113.

Raw finally/iterator cleanup discarded the operation outcome and exact handle owner. Snapshot hashing, explicit directory traversal and the adjacent executable reader now preserve tagged read outcomes and exact cleanup leases in the existing capture scope. Initial/final partial observations keep examined facts, while coverage reflects actual examined roots. Primary read failures survive close-time cancellation; undefined/string close rejections still retain cleanup ownership. Finalization retains the typed primary and its completed read facts, and CLI/default shutdown preserves a distinct retry failure.

Validation: 188 focused tests across 17 files passed, with four existing Windows skips. Full static/type/architecture checks, documentation checks and test discovery passed. Controlled real-file/Dir regressions cover primary-plus-close failure, cancellation during final close, mixed file outcomes, admission blocking and shutdown. The compiled CLI now retains its completed final hash and child exit and retries the exact descriptor (confirmed EBADF). Public in-memory MCP verifies blocked repeat admission, close_binary recovery and a subsequent successful Node capture. The unchanged-source three-case and compiled-CLI RED controls were recorded before implementation.

Directory close rejection remains an explicit native limit: Node does not expose enough validity evidence to authorize another native close. REA retains that unresolved owner and blocks replacement captures; it does not reinterpret ERR_DIR_CLOSED as release proof. File recovery is verified through the existing exact-handle owner. These are controlled faults on Node 24.18.0/macOS arm64, not naturally occurring OS failure or native Windows/provider verification.

Six final completed SDK controls with 128 hashed files retained the same 207-tool catalog and semantic oracle. Warm medians: elapsed 3.529 to 3.485 s, CPU 4.158 to 4.209 s, sampled family memory 889.750 to 894.968 MB; request estimate 108 tokens unchanged and complete SDK reply estimate 969917 to 969927 cl100k tokens. Small samples, unflushed OS caches and ambient wire variation establish no performance improvement; actual model context and fault-path costs are unmeasured.

<!-- gh-comment-id:6259136366 --> @morluto commented on GitHub (Oct 19, 2026): Fixed on main in [`9af0b6113`](https://github.com/morluto/rea/commit/9af0b6113c81803f88ec7d242e8fabaaa5d7f1f3). Raw `finally`/iterator cleanup discarded the operation outcome and exact handle owner. Snapshot hashing, explicit directory traversal and the adjacent executable reader now preserve tagged read outcomes and exact cleanup leases in the existing capture scope. Initial/final partial observations keep examined facts, while coverage reflects actual examined roots. Primary read failures survive close-time cancellation; undefined/string close rejections still retain cleanup ownership. Finalization retains the typed primary and its completed read facts, and CLI/default shutdown preserves a distinct retry failure. Validation: 188 focused tests across 17 files passed, with four existing Windows skips. Full static/type/architecture checks, documentation checks and test discovery passed. Controlled real-file/Dir regressions cover primary-plus-close failure, cancellation during final close, mixed file outcomes, admission blocking and shutdown. The compiled CLI now retains its completed final hash and child exit and retries the exact descriptor (confirmed EBADF). Public in-memory MCP verifies blocked repeat admission, `close_binary` recovery and a subsequent successful Node capture. The unchanged-source three-case and compiled-CLI RED controls were recorded before implementation. Directory close rejection remains an explicit native limit: Node does not expose enough validity evidence to authorize another native close. REA retains that unresolved owner and blocks replacement captures; it does not reinterpret ERR_DIR_CLOSED as release proof. File recovery is verified through the existing exact-handle owner. These are controlled faults on Node 24.18.0/macOS arm64, not naturally occurring OS failure or native Windows/provider verification. Six final completed SDK controls with 128 hashed files retained the same 207-tool catalog and semantic oracle. Warm medians: elapsed 3.529 to 3.485 s, CPU 4.158 to 4.209 s, sampled family memory 889.750 to 894.968 MB; request estimate 108 tokens unchanged and complete SDK reply estimate 969917 to 969927 cl100k tokens. Small samples, unflushed OS caches and ambient wire variation establish no performance improvement; actual model context and fault-path costs are unmeasured.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#3161
No description provided.