fix(rpc): sweep stale RPC state and name its directories - #83
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 8 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
0d9a24f to
e9c7c90
Compare
|
Reviewed against
One thing to fix before merge, narrow but silent when it hits.
Retrying Non-blocking: |
The RPC state tree only ever shrank as a side effect: a dead-pid port file was removed when a TUI happened to discover a server in that same project directory, and nothing removed the directories themselves. On this machine that left 14 project directories, 4 of them empty. The server now sweeps at startup, dropping dead-pid port files and removing directories left empty. It never touches a live pid, never removes the directory it is about to use, and a sweep failure is logged rather than propagated - collecting old state must not stop a session from starting. Writing a port file survives a concurrent sweep. Another project's process can legitimately rmdir this directory in the window after mkdir returns: it is empty, and it is not that process's active directory, so both of the sweep's guards pass. The write then failed ENOENT, the server never started, and the loader swallowed it - leaving a session whose modals all silently do nothing, which is the failure this change set out to remove. Create, write and rename are retried once as a unit, since the directory can vanish before any of them. Unparseable and malformed port files are collected too. A parse error was previously swallowed by both the sweep and discovery, so a corrupt file could be neither used nor removed, and a file containing the literal null parsed successfully then threw on the pid lookup, aborting the sweep for every project under the shared root. Reading, parsing, shape-checking and the liveness test are now separate stages with narrow handling, so malformed data cannot abort the tree while a genuine programming error stays visible. A file must carry both a finite pid and a finite port to be considered usable, matching what discovery already required - a pid-only file was previously unusable and uncollectable. Unlinking a corrupt file is safe because writePortFile writes to a .tmp name and renames, and only port-*.json is considered, so a parse failure means genuinely corrupt rather than mid-write. Discovery stays silent about them, since it runs on the TUI polling path where a per-poll line would be noise. Directories are named openai-auth-<hash> so an operator can see at a glance which state belongs to this plugin, and are created 0700. The port files inside were already 0600, but a world-readable directory still discloses which projects exist. Only directories matching the plugin's own naming are swept or chmod-ed; a configured override path may belong to something else and is left alone. Resolution deliberately does not consult liveness. An earlier revision kept using a legacy directory while it still held a live server, which reads as a safe migration but makes the answer depend on WHEN it is asked: a server could resolve to the legacy path, the old process could exit, and the TUI would then resolve to the new one and never find it. Both halves derive the path from the project directory alone, so any two resolutions agree regardless of what exited in between. Plugin changes are restart-gated, so a session cannot be running this resolver against an older build's server anyway. Legacy directories are still swept once nothing live remains in them.
e9c7c90 to
08bd18c
Compare
|
Fixed in The mkdir/rmdir window. What makes this one nasty is that neither sweep guard is wrong — I widened the retry beyond your suggestion after looking at where else the window reaches: Bounded at exactly two attempts — a persistent ENOENT fails fast on the second with the original error preserved, rather than looping against a genuinely broken parent path. There is a test pinning the attempt count, because "retry once" degenerating into "retry forever" is the obvious way this goes wrong later. The race test needs a seam: a one-line optional The pid-only file. Fixed — Worth recording: the first draft of that test used a dead pid, so it passed through the All hunks reverse-applied individually. The retry-bound test used an inverse mutation (widening the loop to five attempts) since reverse-removal cannot fail an assertion about not retrying more. |
|
Verified at 732 tests pass locally on the branch. Ready to merge from my side. |
Independent of the other open PRs — branched off
mainat v0.4.3, touches onlysrc/rpc/plus the two call sites that resolve the directory.The state tree only ever grew
A dead-pid port file was removed only as a side effect: when a TUI happened to discover a server in that same project directory. Nothing ever removed the directories. On my machine that left 14 project directories, 4 of them completely empty, and the count only goes up — every project you ever opened leaves one behind.
The server now sweeps at startup: dead-pid
port-*.jsonfiles, then directories left empty. It never touches a live pid, never removes the directory it is about to use, and a sweep failure is logged rather than propagated — collecting old state must not stop a session from starting.Corrupt port files are collected too. Both the sweep and discovery previously swallowed the parse error, so a truncated file could be neither used nor removed — permanent litter in the mechanism meant to prevent exactly that. Unlinking one is safe because
writePortFilewrites to a.tmpname and renames, and onlyport-*.jsonis considered, so a parse failure means genuinely corrupt rather than mid-write. Discovery stays silent about them since it runs on the TUI polling path, where a per-poll line would be noise.Directories say what they are, and stop being world-readable
22aebfef4e44976ftells an operator nothing and could collide with a sibling plugin sharing the tree. They are nowopenai-auth-<hash>, and created0700— the port files inside were already0600, but a world-readable directory still discloses which projects exist. Only directories matching the plugin's own naming are swept or chmod-ed; a configured override path may belong to something else and is left alone.Resolution deliberately does not consult liveness
Worth flagging, because the first version of this change did and it was wrong.
That version kept using a legacy directory while it still contained a live server — which reads as a considerate migration, but makes the answer depend on when it is asked:
Step 3→4 is just "the old session was closed after the new one started". The two halves are separate module instances, so when they disagree the TUI silently never finds the server and every modal breaks with no error anywhere.
Both halves now derive the path from the project directory alone, so any two resolutions agree regardless of what exited in between. Plugin changes are restart-gated, so a session cannot be running this resolver against an older build's server in the first place — the state the migration defended against cannot occur. Legacy directories are still swept once nothing live remains in them.
Testing
728 pass / 0 fail (from 714). Build, typecheck and biome clean.
Every production hunk was mutation-proven individually — ten of them, re-run independently by a cross-family reviewer rather than taken from the implementer's report. The mechanic differs by assertion type and is labelled per test: reverse-removal for positive assertions (a dead file is removed), and inverse mutation for absence assertions (a live file is not removed; a foreign directory is not chmod-ed; the active directory is not rmdir'd). Reverse-removal cannot prove an absence assertion — deleting code deepens compliance, so the test can never redden. Those three are the ones worth checking, since they are what stops "collect stale state" from degenerating into "collect state".
The divergence in step 1-4 above was also confirmed by test before being fixed, not argued from reasoning.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Sweeps stale RPC state at startup, switches to named private RPC directories, and hardens port-file writes against concurrent cleanup. Both server and TUI now resolve the same stable path from the project directory, preventing mismatches.
Bug Fixes
port-*.jsonfiles and remove emptied project dirs at server start (includes legacy-named dirs); never touch live PIDs or the active dir.New Features
openai-auth-<hash>and created with 0700; port files remain 0600.resolveRpcDirused by both halves; it returns the managed path with secure mode and sweep root. Override paths stay anchored but are not secured or swept.Written for commit 08bd18c. Summary will update on new commits.