This repository was archived by the owner on Aug 10, 2026. It is now read-only.
fix(agent): poll convergence tears down a deleted def's live agent (orphan fix) - #125
Merged
Conversation
…rphan fix) Deleting an `#agent/definition` note out-of-band left the live agent + wake channel running until the next daemon restart — an orphan that kept answering messages after its def was gone. Root cause: there is no vault `deleted` trigger. The hub's connection engine maps only `note.created`/`note.updated` to vault-trigger verbs (`admin-connections` `eventToVaultEvents`), so a delete never fires the reactive `reload(...,"deleted")` teardown. The 60s poll is the ONLY automatic convergence path for a deletion — but `loadAll`→`pruneRemovedDefs` only pruned the removed def's GRANTS; it never deregistered the agent or removed the channel. Only the admin-SPA delete button (which calls `deleteDef` in-process) tore down correctly; any other delete path orphaned the agent. Fix: the poll's removed-def diff now does the SAME full teardown the reactive `confirmedRemoval` does — `deregisterByNote` (drop the programmatic registration + wake channel) THEN prune grants. Because that diff is now destructive, add a TRUNCATION GUARD: a def list at the page cap (`DEF_LIST_LIMIT`, 500) is treated as possibly-partial — NOT a confident set — so a truncated read can't be mistaken for deletions and tear down live agents. (This also hardens the pre-existing grant-prune, which had the same latent exposure.) Sits alongside the existing list-failure guard. Tests: a removed def is deregistered + channel-removed via the poll (the no-delete-trigger regression); a truncated page-cap read skips the diff (no spurious teardown); the existing #96 removal test now also asserts teardown. Gates: typecheck clean; `bun test ./src` 1019 pass / 0 fail; SPA 110/110. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mment Reviewer nits (PR #125, non-blocking): (1) the truncation test now does a third CONFIDENT pass proving the seen-set survived the truncated pass (a real deletion is still caught), making it a complete proof of the invariant; (2) an inline note at the `< DEF_LIST_LIMIT` guard boundary so the off-by-one reads unambiguously. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
Author
|
Folded the two diff-relevant reviewer nits inline (non-blocking):
Nit 3 (seen-set clearing relies on Gates after folding: typecheck clean; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Deleting an
#agent/definitionnote out-of-band (anything other than the admin-SPA delete button — a direct vault API/MCP delete, an Obsidian sync, another surface) left the live agent + wake channel running until the next daemon restart. The orphaned agent kept answering messages after its def was gone from the vault.Root cause
There is no vault
deletedtrigger. The hub's connection engine maps onlynote.created/note.updatedto vault-trigger verbs (parachute-hubadmin-connectionseventToVaultEvents—note.deletedhas no mapping and throws). So a delete never fires the reactivereload(...,"deleted")teardown.That leaves the 60s poll as the only automatic convergence path for a deletion — but
loadAll→pruneRemovedDefsonly pruned the removed def's grants (reconcileGrants(agent, [])). It never calledderegisterByNote, so the live programmatic registration + the wake channel survived. Only the admin-SPA delete button worked, becausedeleteDefdoes the teardown in-process; every other delete path orphaned the agent.Fix
pruneRemovedDefsnow does the full teardown the reactiveconfirmedRemovalalready does:deregisterByNote(drop the programmatic registration + wake channel) then prune grants.DEF_LIST_LIMIT = 500) is treated as possibly-partial — not a confident set — so a truncated read can't be mistaken for deletions and tear down live agents. Sits alongside the existing list-failure guard. This also hardens the pre-existing grant-prune, which had the same latent exposure.loadAll/pruneRemovedDefs/daemon-boot comments that wrongly claimed the poll already converged deletes.How it was found
Live end-to-end verification of the agent-to-agent callback feature (#123). While cleaning up two throwaway test agents, deleting their def notes did not deregister them (
/healthstill listed them 72s later, past the 60s poll). The reactivedeletedwebhook never fired → traced to the missing vault delete-trigger → the poll's grant-only convergence. (The callback feature itself verified clean: worker→callback→orchestrator chain with the full metadata contract, no loop.)Tests
loadAlltears down a removed def —deregister+removeChannelvia the poll (the no-delete-trigger regression).loadAllskips the removed-def teardown on a truncated (page-cap) read — no spurious deregister.tsc --noEmitclean;bun test ./src1019 pass / 0 fail; SPA (vitest) 110 / 110.Note for the merger
origin/ag-unforced-devcurrently carries two unmerged commits with no open PR (8aca779,9db9689— a "robustness hardening (PR #3)" series, looks like another session's WIP). To avoid clobbering/entangling that branch, this fix is isolated on a one-off branch offmain. It is independent: that series leavespruneRemovedDefsunchanged (still grant-only), so there's no semantic overlap — at most a trivial textual conflict inagent-defs.tswhichever lands second.Per RC versioning, no version bump in this PR (bump + tag on ship).