[GH-ISSUE #8860] [Design] Who should own file-system work that a stuck mount holds? #7776

Open
opened 2026-11-04 04:56:20 +00:00 by bsakaguchi · 0 comments
Owner

Originally created by @Entrpi on GitHub (Nov 4, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/8860

Before submitting

  • I searched the open and closed issues and pull requests for a related discussion.
  • I understand the contribution scope must be reviewed before integration; open questions are welcome here.
  • This discussion does not include credentials, private samples, or sensitive target data.

Decision needed

Should REA bound file-system work that a stuck mount holds, across all tools, and how far should that go?

  • The FileWork design in #7487 was chosen and built without a maintainer decision. That covers the process-wide owner, its reserved budget of file steps, the saturated result, and reverting close() so it no longer fails while work is held. Please treat it as a proposal, not a settled base.
  • The work below needs your feedback on scope before substantial implementation. Nothing here is built outside #7487, and stopping early is a fine answer.

Questions:

  1. Is this worth handling beyond #7487, and to which stop below? No field report exists, so its likelihood is unknown.
  2. Is FileWork the right base? Should its budget cap all healthy target file I/O? As built it is half of libuv's pool: 2 steps by default.
  3. Is process.execve (experimental, POSIX only) acceptable as the exit route? What is required on Windows?
  4. What may MCP shutdown abandon, and how long may it wait? Is restarting the server acceptable recovery?
  5. Is 120 s per step a reasonable default? Should a stall block all work, one provider, or one path?

Context and evidence

A stuck network or FUSE mount holds a stat or read, and a libuv pool thread with it, until the mount answers. Nothing can cancel the call.

Observed. A FIFO stood in for the mount: open(2) with no writer holds a real thread, but as an interruptible sleep. No real stuck mount was tested. Node 24.22.1 on Linux and Node 26.13.0 on macOS gave the same results.

Stuck thread Way out Result
Pool, or unref()ed worker natural end, or process.exit() hangs
Pool uncaught exception, unhandled rejection, top-level throw hangs
Either SIGKILL or SIGTERM to self exits, code lost
Either process.execve(...) exits with the exact code

Node's exit path joins these threads (uv__threadpool_cleanup, Worker::JoinThread). So REA has no exit past a stuck thread, as #7487's docs now say, and worker threads would not provide one.

  • A simulated stall hides this: a never-resolving promise holds no thread, so a test built on one passes while the real case hangs.
  • With the default pool, 4 stuck steps starve a healthy stat. With UV_THREADPOOL_SIZE=16, 15 do not, and it took effect when set at scripts/rea.mjs's environment step.
  • Narrowing the read-side handle type gave 120 non-test compile errors in 57 files, as a floor. 96 are helper signatures.

Source-derived, not run.

  • src/filesystem/ has no deadline, so other tools would hang on a dead mount.
  • There is no single open boundary; ownership is the common point.
  • MCP shutdown has several unbounded waits before providers close. Exiting past them would orphan providers such as Ghidra, so this needs more than a timeout.
  • With a live but hung FUSE daemon, the kernel wait is uninterruptible (fs/fuse/dev.c), so SIGKILL and exec can both wait.

NOT_ENOUGH_INFO

  • How often this happens. The concern comes from reviewing #7487.
  • Behaviour on Windows, and on a real FUSE or NFS stall.
  • What a budget of 2 or 8 costs on a latency-bound mount.

Expected interaction with targets

Read existing files or captured data

Boundary details (optional)

File access stays read-only and caller-selected. No new target execution, mutation or network effect. A digest still means bytes that were read. Output paths are out of scope.

Options and tradeoffs

Proposed, not built. Each stop includes the ones before it.

  • S0: exit and pool. No file-layer change, no dependency on #7487. One exit owner uses process.execve once output is delivered, covering fatal errors and signals. MCP shutdown gets a deadline and closes providers before it exits. Set UV_THREADPOOL_SIZE ??= 16, and add a test harness that holds a real thread. Windows stays as it is until tested. Tools still wait on a dead mount with no answer.
  • S1: main openers. Move FileWork to src/filesystem/. Add a stalled close state that joins the existing retained-close path. Reuse AnalysisTimeoutError and AnalysisResourceConstraintError, so no public schema changes. Covers a mount dead at open, in the main openers only.
  • S2: all owned reads. A multi-reader migration of 57+ files. Each operation gets a guarded view with a per-step limit, plus the caller's signal for the whole call, the same split as #7150. Needs question 2 settled first.

Raw fs and sync calls stay unguarded at every stop.

Not proposed: I/O worker threads (no exit benefit), a watchdog at tool dispatch (no owner), a file-broker or supervisor process (the only routes past an uninterruptible wait, but costly), io_uring (Linux only, off by default in libuv), and a metadata-keyed digest cache (REA's before-and-after digests are content-based change detection).

Verification available so far

I have a prototype or observed results

Success criteria and open questions

If this goes ahead:

  • A tool on a stalled path returns a typed failure within its limit, and cancellation keeps the caller's exact reason.
  • Completed digests and partial Evidence survive.
  • The built CLI and rea mcp exit with the exact code and complete output while a real thread is stuck.
  • Tests hold a real, releasable thread with a FIFO.
  • Before S2, the budget is measured through rea mcp on local and latency-bound mounts, cold and warm. No gain is claimed until that runs.

References

Originally created by @Entrpi on GitHub (Nov 4, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/8860 ### Before submitting - [x] I searched the open and closed issues and pull requests for a related discussion. - [x] I understand the [contribution scope](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#contribution-scope) must be reviewed before integration; open questions are welcome here. - [x] This discussion does not include credentials, private samples, or sensitive target data. ### Decision needed Should REA bound file-system work that a stuck mount holds, across all tools, and how far should that go? - **The `FileWork` design in #7487 was chosen and built without a maintainer decision.** That covers the process-wide owner, its reserved budget of file steps, the `saturated` result, and reverting `close()` so it no longer fails while work is held. Please treat it as a proposal, not a settled base. - **The work below needs your feedback on scope before substantial implementation.** Nothing here is built outside #7487, and stopping early is a fine answer. Questions: 1. Is this worth handling beyond #7487, and to which stop below? No field report exists, so its likelihood is unknown. 2. Is `FileWork` the right base? Should its budget cap all healthy target file I/O? As built it is half of libuv's pool: 2 steps by default. 3. Is `process.execve` (experimental, POSIX only) acceptable as the exit route? What is required on Windows? 4. What may MCP shutdown abandon, and how long may it wait? Is restarting the server acceptable recovery? 5. Is 120 s per step a reasonable default? Should a stall block all work, one provider, or one path? ### Context and evidence A stuck network or FUSE mount holds a `stat` or read, and a libuv pool thread with it, until the mount answers. Nothing can cancel the call. **Observed.** A FIFO stood in for the mount: `open(2)` with no writer holds a real thread, but as an interruptible sleep. No real stuck mount was tested. Node 24.22.1 on Linux and Node 26.13.0 on macOS gave the same results. | Stuck thread | Way out | Result | | --- | --- | --- | | Pool, or `unref()`ed worker | natural end, or `process.exit()` | hangs | | Pool | uncaught exception, unhandled rejection, top-level throw | hangs | | Either | `SIGKILL` or `SIGTERM` to self | exits, code lost | | Either | `process.execve(...)` | exits with the exact code | Node's exit path joins these threads (`uv__threadpool_cleanup`, `Worker::JoinThread`). So REA has no exit past a stuck thread, as #7487's docs now say, and worker threads would not provide one. - A simulated stall hides this: a never-resolving promise holds no thread, so a test built on one passes while the real case hangs. - With the default pool, 4 stuck steps starve a healthy `stat`. With `UV_THREADPOOL_SIZE=16`, 15 do not, and it took effect when set at `scripts/rea.mjs`'s environment step. - Narrowing the read-side handle type gave 120 non-test compile errors in 57 files, as a floor. 96 are helper signatures. **Source-derived, not run.** - `src/filesystem/` has no deadline, so other tools would hang on a dead mount. - There is no single open boundary; ownership is the common point. - MCP shutdown has several unbounded waits before providers close. Exiting past them would orphan providers such as Ghidra, so this needs more than a timeout. - With a live but hung FUSE daemon, the kernel wait is uninterruptible (`fs/fuse/dev.c`), so `SIGKILL` and exec can both wait. **NOT_ENOUGH_INFO** - How often this happens. The concern comes from reviewing #7487. - Behaviour on Windows, and on a real FUSE or NFS stall. - What a budget of 2 or 8 costs on a latency-bound mount. ### Expected interaction with targets Read existing files or captured data ### Boundary details (optional) File access stays read-only and caller-selected. No new target execution, mutation or network effect. A digest still means bytes that were read. Output paths are out of scope. ### Options and tradeoffs **Proposed, not built.** Each stop includes the ones before it. - **S0: exit and pool.** No file-layer change, no dependency on #7487. One exit owner uses `process.execve` once output is delivered, covering fatal errors and signals. MCP shutdown gets a deadline and closes providers before it exits. Set `UV_THREADPOOL_SIZE ??= 16`, and add a test harness that holds a real thread. Windows stays as it is until tested. Tools still wait on a dead mount with no answer. - **S1: main openers.** Move `FileWork` to `src/filesystem/`. Add a `stalled` close state that joins the existing retained-close path. Reuse `AnalysisTimeoutError` and `AnalysisResourceConstraintError`, so no public schema changes. Covers a mount dead at open, in the main openers only. - **S2: all owned reads.** A multi-reader migration of 57+ files. Each operation gets a guarded view with a per-step limit, plus the caller's `signal` for the whole call, the same split as #7150. Needs question 2 settled first. Raw `fs` and sync calls stay unguarded at every stop. **Not proposed:** I/O worker threads (no exit benefit), a watchdog at tool dispatch (no owner), a file-broker or supervisor process (the only routes past an uninterruptible wait, but costly), io_uring (Linux only, off by default in libuv), and a metadata-keyed digest cache (REA's before-and-after digests are content-based change detection). ### Verification available so far I have a prototype or observed results ### Success criteria and open questions If this goes ahead: - A tool on a stalled path returns a typed failure within its limit, and cancellation keeps the caller's exact reason. - Completed digests and partial Evidence survive. - The built CLI and `rea mcp` exit with the exact code and complete output while a real thread is stuck. - Tests hold a real, releasable thread with a FIFO. - Before S2, the budget is measured through `rea mcp` on local and latency-bound mounts, cold and warm. No gain is claimed until that runs. ### References - #7487, #7150 - `src/filesystem/`, `src/artifacts/readStableArtifact.ts`, `docs/testing.md#lifecycle-regressions` - [`process.execve`](https://nodejs.org/docs/latest-v24.x/api/process.html#processexecvefile-args-env), [libuv thread pool](https://docs.libuv.org/en/v1.x/threadpool.html), [`fs/fuse/dev.c`](https://github.com/torvalds/linux/blob/master/fs/fuse/dev.c)
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#7776
No description provided.