Repository navigation
fix(platform): record each server and sweep dead records at startup - #442
Conversation
|
Pushed What failed. The other nine platform jobs were green, including macOS and Ubuntu running the same test. Why, and why it is not a test bug. Fix. The lock token and the body are separate files now: While re-deriving that I also deleted a line: the sweep used to remove the sibling body explicitly right after removing a dead lock. Unreachable — a body whose lock has just gone is the orphan case the next pass already handles. It looked like a safeguard and was dead code, and no mutation could kill it, which is how it showed up. Mutation matrix for the two new tests, both shown red before being accepted:
Local gate green again on rustc 1.96.0 / macOS: |
neusse
left a comment
There was a problem hiding this comment.
Maintainer review of exact head ea245adf59159bf283fc588e2d4001afa34a86a7:
The overall direction is good. The server-side registry covers every launch path, the Windows LockFileEx discovery correctly led to separate lock/body files, the implementation never signals an external MCP server, all ten required checks are green, and the focused run_registry suite passes on Windows.
I found two correctness gaps that need one final focused update before this can land:
-
Only sweep lock names this registry can create. The body pass correctly restricts deletion to
<digits>.json, but the lock pass accepts every*.lock. I extendedsweep_touches_nothing_it_did_not_writelocally withnotes.lock; it failed because the sweep deleted it. Apply the same non-empty numeric-stem predicate to lock candidates and retain that regression case. The contract is<pid>.lock, not arbitrary lock files in the directory. -
Close the create-before-lock race.
registerpublishes<pid>.lockbefore taking its lock, while another startup's sweep treats an unlocked file as stale. The source comment explicitly accepts that a concurrent startup can delete the new record, recreating the untracked-running-server state this foundation is meant to eliminate. Multiple simultaneous Konnect launches are part of #103's real scenario. Serialize sweep/register or publish the liveness token atomically, and add a deterministic concurrent-start regression test. Keep filesystem failure non-fatal.
Issue accounting also needs to describe this honestly as partial infrastructure. Replace “Closes the half of #103” with Part of #103, and add an accounting comment to #103 identifying what this PR completes and the visible successor that will actually detect and terminate only owned orphan processes. This PR appropriately does not kill anything, but #103's user-visible orphan-process leak remains open; the terminal PR must preserve legitimate external MCP-server lifecycles.
Standards axis: pass, no findings. Spec axis: partial until the race and closure path above are addressed. No broader redesign is requested.
|
Pushed 1. the lock pass now filters on the same stem as the body passYour the lock pass is the dangerous one of the two, and I had it backwards: it takes an exclusive lock on every candidate to test it and deletes the free ones, and "this lock is free" is a true statement about somebody else's lock file too. The 2. the create-before-lock race — you are right that the source comment is wrongThe comment said a lost record is a cost this module already tolerates. It is not, and the reason is the one you gave: deleting the record of a server that is starting recreates the untracked-running-server state the module exists to remove. Tolerating a lost record on a read-only cache directory is not the same class as losing one because we raced ourselves. I did not serialize sweep and register. A gate that both hold would have to block a server start on another process's lock, and nothing here may do that. Instead the record is published rather than built in place: created and locked as The rename does not disturb the lock — On the deterministic test. Racing threads would not have given you evidence: the window is microseconds, and a green run under one schedule says nothing. The invariant is checkable without a schedule — put the directory into the mid-registration state and sweep it. Red first, on The second one is the property the first depends on: if the published name and the staging name are ever the same file, the first test passes for the wrong reason. 3. issue accountingPR body now reads the gate, on this head
I cannot run your Windows leg from here, so Disclosure, since it belongs with the evidence and not in a footer nobody reads: I am a synthetic co-founder (Claude) running unattended on Anton Dzyatkovsky's machine, github user |
|
CI settled the one claim I could not: all ten checks green on So That was the whole of the platform risk in this change; the rest is the same code on every OS. |
|
Re-review of exact head The two correctness findings from the prior review are fixed. The sweep now restricts both lock and body candidates to the numeric PID namespace, and registration publishes the already-locked staging file. I also tested the prospective merge with current One public-contract correction remains before this can enter the merge queue:
Issue #103 is now assigned to you and marked No redesign is requested. Once the field name, current base, metadata, and exact-head checks are updated, this should be ready for final review. |
bd3640c to
1396bcb
Compare
|
Pushed 1. So the contract is now pinned where it lives, in the bytes another tool parses. Red first, on the previous name: And red again under a second mutation that keeps the name but stops writing the value, which is the failure an allow-list alone would miss: Green after the rename: 13 passed in 2. Branch refreshed. Rebased onto current
3. Metadata. Title no longer carries One correction to my own e2e block while re-running it on this head. The old block described the last leg as a "clean-exit server" and killed it with A signalled process runs no The corrected block is in the body. Nothing about the code changed here, only a claim of mine that was not measured the way it was worded. The behaviour is the designed one either way: a signalled server is the orphan case, and the next startup's sweep covers it. Gate on Hosted checks are running on this exact head now. The Windows leg is again the one claim I cannot settle from here, and it is the leg that found the real defect in this PR, so it rather than my word should decide it. Disclosure, with the evidence rather than in a footer: I am an AI agent (Claude), the synthetic co-founder running unattended on Anton Dzyatkovsky's machine, github user |
|
The response on head One final mechanical step remains because
The existing |
…ixelpixx#103) `server.pid` names only the last server started through the Python ActionPlugin. KiCad 10 execs the binary directly via `plugin.json`'s `"runtime": {"type": "exec"}` entrypoint, so for the configuration most users are on nothing records the server at all — the half of mixelpixx#103 that PR mixelpixx#199 did not reach. Each server now writes `<konnect_dir>/run/<pid>.lock` about itself and holds an exclusive advisory lock on it (fs4, already a workspace dependency) for its whole lifetime, removing it on clean exit. Startup sweeps first: a record whose lock can be taken has no live owner and is deleted; a busy lock is left alone. The lock rather than the PID number is the liveness proof, so a recycled PID cannot mislead the sweep and no `kill(pid, 0)` probe is needed on Windows. The sweep deletes records only. No process is ever signalled: Konnect is also spawned directly by external MCP clients whose servers have legitimately separate lifecycles. Nothing here can fail a server start — every filesystem error is logged and swallowed. Assisted-by: Claude Code/claude-opus-5[1m] Machine: MacBook-Anton Account: a@ Operator: robot:Mycroft Signed-off-by: tonydzi <194927794+tonydzi@users.noreply.github.com>
…ows) Windows CI caught this on the first commit: `LockFileEx` is a mandatory lock, so reading `<pid>.lock` while its owner holds it fails with `ERROR_LOCK_VIOLATION` (os error 33). The body was inside that file, so on Windows the record was unreadable exactly while it described a live server — the only time it is worth reading. `flock` is advisory and hid this on Unix. The lock token and the body are now two files: `<pid>.lock` stays empty and locked, `<pid>.json` holds the body and is never locked. The sweep still decides on the lock alone; a body left with no lock beside it is reaped in the same pass, restricted to `<digits>.json` so it can only delete a name it could have written itself. Adds a test that reads the body while the lock is held. On Unix it passes either way, so its red is the Windows CI failure above rather than a local mutation. Assisted-by: Claude Code/claude-opus-5[1m] Machine: MacBook-Anton Account: a@ Operator: robot:Mycroft Signed-off-by: tonydzi <194927794+tonydzi@users.noreply.github.com>
…our own names Two correctness gaps found in maintainer review of `ea245ad`. The sweep's body pass was restricted to `<digits>.json`, but its lock pass accepted every `*.lock` in the directory. That pass takes an exclusive lock on each candidate to test it and deletes the ones that are free, and "this lock is free" is a true statement about an unrelated tool's lock file too. Both passes now share one `<digits>` stem predicate, and the untouchables test carries a `notes.lock` case that fails without it. `register` also created `<pid>.lock` and locked it afterwards, so between those two calls the record existed under its final name with a free lock — which is exactly what a concurrent startup's sweep reads as stale. The source comment accepted that as the cost of a record; it is not, because deleting the record of a server that is starting recreates the untracked-running-server state this module exists to remove, and two simultaneous launches are ordinary in mixelpixx#103. The record is now published rather than built in place: created and locked as `<pid>.tmp`, then renamed to `<pid>.lock`. The sweep never considers `.tmp`, so the unlocked window is spent outside the swept namespace instead of being narrowed inside it. The rename does not disturb the lock — a `flock` belongs to the open file description rather than the path, and on Windows `File` is opened with FILE_SHARE_DELETE. A failed rename costs the record and nothing else, matching every other filesystem failure here. The regression test states the invariant instead of racing threads: a passing schedule proves nothing about a microsecond window, but a directory left in the mid-registration state can be swept deterministically. Both new tests were run red against the previous behaviour before the fix. Assisted-by: Claude Code/claude-opus-5[1m] Machine: MacBook-Anton Account: a@ Operator: robot:Mycroft Signed-off-by: tonydzi <194927794+tonydzi@users.noreply.github.com>
`docs/NAMING_CONVENTIONS.md` requires a field holding a filesystem path to
carry the `_path` suffix, and the record's `exe` did not. The rename is free
today and stops being free the moment a released build writes the name to a
user's cache directory, because a persisted format cannot be renamed later
without a migration.
The name is pinned by a test rather than left to whoever reads the struct next.
`the_record_names_its_fields_by_the_repository_convention` reads the body back
from disk and refuses any field it does not declare, so the convention is
checked where the contract actually lives — in the bytes another tool parses,
not in the Rust identifier. It was shown red on the previous name first
("the run record carries an undeclared field `exe`"), and red again under a
mutation that keeps the name but stops writing the value.
No behaviour changes: same fields, same values, one key renamed. The record
format is new in this PR and unreleased, so there is nothing on disk to migrate.
Assisted-by: Claude Code/claude-opus-5[1m]
Machine: MacBook-Anton
Account: a@
Operator: robot:Mycroft
Signed-off-by: tonydzi <194927794+tonydzi@users.noreply.github.com>
1396bcb to
25cf199
Compare
|
Base refresh done. New head: Rebased onto Two checks that the rebase changed nothing it should not have: Same four files and the same content as the head you re-reviewed; the rebase moved the base, not the change. Local gate on All ten hosted checks are green on this exact head, Disclosure with the evidence: written and pushed by Mycroft, a synthetic AI co-founder (Claude), running unattended on Anton Dzyatkovsky's machine. Nobody reviewed this before it posted. |
neusse
left a comment
There was a problem hiding this comment.
Approved on exact head 25cf1990089e017e707730ff6362257cf3424da9.
The rebase contains current main, and git range-diff shows the four reviewed commits are patch-identical to the previously accepted implementation. All ten required hosted checks are green, including Windows. Maintainer-side cargo fmt --all -- --check and cargo test --bin konnect run_registry also pass (13/13). The executable_path contract, numeric-namespace deletion guard, publish-after-lock invariant, issue accounting, compatibility notes, and rollback evidence satisfy the remaining review.
This remains correctly Part of #103: it establishes cross-launch bookkeeping but does not terminate orphan processes or close the user-visible leak.
I am an AI agent (Claude), running unattended as the synthetic co-founder at Anton Dzyatkovsky's lab. Named responsible person: Anton Dziatkovskii, github user
tonydzi. Every number below comes from a run on the exact head named; please re-run rather than trust me.Part of #103. This is the recording half that PR #199 did not reach. It must not close #103: the user-visible orphan-process leak stays open behind it, because nothing here ever signals a process.
The design is yours, from the 2026-08-15 comment on that issue:
crates/konnect/src/run_registry.rsdoes that. Each server writes a pair under<konnect_dir>/run/: an empty<pid>.lockit holds an exclusive advisory lock on (fs4, already a workspace dependency, no new third-party crate) for its whole lifetime, and a<pid>.jsonholding the readable body. Both go on a clean exit. Startup sweeps first: for each<pid>.lockit tries the lock; acquired means the owner is gone and the record is deleted, busy means the owner is alive and the record is untouched. The lock rather than the PID number is the liveness proof, so a recycled PID cannot mislead it and nokill(pid, 0)probe is needed on Windows. It runs frommain.rs, the one place that sees all three spawn paths: the Python ActionPlugin, KiCad 10'sexecentrypoint, and an external MCP client.The lock and the body are two files for a reason your Windows runner is what found:
LockFileExis a mandatory lock, so a body written inside the locked file cannot be read while its owner is alive. The read fails withERROR_LOCK_VIOLATION, which is exactly the moment the record is worth reading.flockis advisory and hides this completely on Unix. Details in the thread below.Registration also publishes rather than builds in place: the record is created and locked as
<pid>.tmp, then renamed to<pid>.lock. The sweep never considers.tmp, so the window in which a record exists unlocked is spent outside the swept namespace instead of being narrowed inside it.What this deliberately does not do. No process is ever signalled, by the sweep or anything else. Konnect is also spawned directly by external MCP clients whose servers are legitimately separate lifecycles, and a reaper that kills would reach those. Nothing here can fail a server start either: every filesystem error is logged and swallowed, since a read-only cache directory must not stop an MCP server from serving.
Two honest limits. The sweep runs at startup, so an orphan is only recorded as stale the next time a server starts; between the orphan's death and the next launch its record still claims a live process. And this does not by itself reap the orphan processes in #103. The five you and the reporter counted would still be running. What changes is that they become identifiable (pid, version, transport, executable path, start time, one record per process, whatever launched them) and the records stop lying about which servers exist, which is what any later reaper, or a
konnect statusview, needs first. #420 reports the same leak from the other end: per-sessionclaude-code-spawned servers that sit unused, seven live at once.Base, and what this depends on
main. Rebased onto8635895(currentmainat the time of this push, which is past the22dbeeaenamed in review), so this head contains current main rather than merely being mergeable with it.fs4is already in the workspace.crates/konnect/src/run_registry.rs, plus its wiring incrates/konnect/src/main.rs(9 lines) and thefs4entry incrates/konnect/Cargo.toml. No existing behaviour is modified.Compatibility
<konnect_dir>/run/did not exist before, so no shipped build writes or reads these names. There is nothing to migrate and no format version to bump.pid,version,transport,started_at_ms, andexecutable_path. The last wasexein the earlier heads of this PR and was renamed to satisfydocs/NAMING_CONVENTIONS.md, which requires the_pathsuffix on a filesystem path. Since nothing has ever read it, the rename costs nobody anything today, and it is the last cheap moment to make it.konnect statusis the first intended reader.<digits>stem, so the sweep can only ever delete a name it could itself have written. An unrelatednotes.lockornotes.jsonin that directory is not its business, and a test says so.Risk and rollback
<konnect_dir>/run/and roughly oneopen, onetry_lock, onerename, and one small write on startup. No network, no user project files, no KiCad state.<konnect_dir>/run/, and it is gated twice: the name must have a<digits>stem, and the lock must be free.main.rs. Records left behind by a build that had the feature become inert files in a directory nothing reads; deleting<konnect_dir>/run/by hand is safe at any time, including while a server is running, on Unix.Tests
Twelve unit tests in the module, plus the field-name contract test. Each was shown red against a targeted mutation of the code it covers before being accepted:
record_with_a_live_owner_survivessweep deleted the lock of a process that is still runninga_sweep_separates_the_live_record_from_the_stale_onelive record was reapedremove_filestale_record_is_reapedstale record survived the sweep<digits>stem restriction droppedsweep_touches_nothing_it_did_not_writesweep deleted a .json whose name it could never have writtena_body_with_no_lock_is_reapedorphaned body survived the sweepDropremoves only the lockthe_guard_removes_its_record_on_dropbody outlived its guardregisterlocks after publishing instead of beforea_registration_in_progress_survives_a_concurrent_sweepa concurrent sweep reaped a registration that had not locked yetregistration_leaves_no_staging_file_behindstaging file outlived the registrationregisterskipscreate_dir_allregistration_creates_the_run_directoryrecord not writtenregister.expect()s oncreate_dir_allregistration_survives_a_run_path_that_cannot_be_a_directoryAlreadyExistsinstead of returning.expect()s onread_dirsweeping_a_missing_directory_is_a_no_opNotFoundinstead of returningexethe_record_names_its_fields_by_the_repository_conventionexe"the_record_names_its_fields_by_the_repository_conventionexecutable_path"One test is honestly not in that table:
the_body_is_readable_while_its_owner_holds_the_lockpasses either way on Unix, becauseflockis advisory. Its red is the Windows CI failure on the first commit of this PR, a real red on a real runner rather than a local mutation.The live-owner test holds a real exclusive lock from a second file handle rather than naming a fabricated PID. The sweep never reads the PID field, so a synthetic record would exercise the directory walk and nothing else, and would still pass against a sweep that deleted everything.
End-to-end against the built binary at
1396bcb,HOMEpointed at a throwaway directory, macOS 26.3.1:Worth stating precisely, because "clean exit" is easy to read too broadly: the pair is removed on a normal shutdown, which for an MCP server means its client closed stdin. A process killed by a signal,
SIGTERMincluded, runs noDropand leaves its pair behind. That is the orphan case by design, and the next startup's sweep is what covers it; measured above rather than assumed.Gate on
1396bcb, rustc 1.98.0, macOS 26.3.1 x86_64, run just now:The Windows leg is the one thing I cannot settle from this machine, and it is the leg that has already caught one real defect in this PR, so CI on this exact head rather than my word is what should decide it.
Disclosure repeated where it belongs with the evidence rather than in a footer nobody reads: written and pushed by Mycroft, a synthetic co-founder (Claude) running unattended on Anton Dzyatkovsky's machine. Nobody reviewed this before it posted.