feat(client): Agent Skills — re-reconcile on delivery with watch_skills - #81
Open
XieX wants to merge 2 commits into
Open
feat(client): Agent Skills — re-reconcile on delivery with watch_skills#81XieX wants to merge 2 commits into
XieX wants to merge 2 commits into
Conversation
write_skills is a one-shot reconcile, so a revocation takes effect at the next process restart. watch_skills runs that reconcile now and again whenever the configured store reports a change, so a revoked skill's files leave the disk within a debounce interval of the store learning about it. It is wired to the SkillStore interface, not to any one transport: it needs a store that implements add_listener and nothing more, and refuses loudly when the store does not, since a watcher that silently never fires looks exactly like one whose skills never changed. Above the interface, remove_listener joins add_listener as the optional second half of change notification, on the SkillStore contract and on InMemorySkillStore. SkillWatcher.close needs it to detach; without it a store held every watcher ever created for the rest of its life. The watcher probes for it, so a store without it keeps working. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit dd2d55c. Configure here.
…oncile watch_skills awaited write_skills and only then constructed SkillWatcher, which is where the store listener attaches. The reconcile snapshots the store as its first step and then spends the rest of its time on the filesystem, so every write, fsync, prune and manifest rewrite in that first pass ran with nothing listening. A change delivered in that window was never seen, and since nothing re-reconciles on a timer, a revocation that landed there waited for the next unrelated change — on a quiet root, the next restart. Exactly the gap watch_skills exists to close. The watcher now attaches its listener before the initial reconcile and starts its worker after. notify only sets an event, so a change arriving mid-reconcile is recorded and picked up by the worker's first pass, while holding the thread back keeps write_skills's one-root-one-reconcile contract: the worker cannot race the caller's own reconcile over the same manifest. A reconcile that raises detaches the listener on the way out, since the caller is handed an exception rather than a watcher to close. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.

Stacked on #66 (
split/skills-review-closeout). First of three PRs split out of #69; the FDv2 protocol layer and the transport follow on top of this one.write_skillsis a one-shot reconcile, so a revocation takes effect at the next process restart.watch_skillsruns that reconcile now and again whenever the configured store reports a change, so a revoked skill'sSKILL.mdleaves the disk within a debounce interval of the store learning about it, rather than at the next restart.What's here
skills_watch.py—watch_skills/SkillWatcher. Wired to theSkillStoreinterface, not to any one transport: it needs a store that implementsadd_listenerand nothing more. It refuses loudly when the store does not, because a watcher that silently never fires looks exactly like one whose skills never changed. The listener itself only sets an event; the reconcile runs on a single worker thread, debounced, so a burst of changes coalesces into one pass and a slow disk never stalls the store's delivery thread. A reconcile that raises is logged and the watcher continues.on_unavailable="keep"stays the default: an outage must not read as "everything was revoked".remove_listener(kind, fn)joinsadd_listeneras the optional second half of change notification, on theSkillStorecontract and onInMemorySkillStore.SkillWatcher.close()needs it to detach; without it a store held every watcher ever created for the rest of its life. The watcher probes for it and skips detaching when a store does not implement it, so a customer's own store keeps working.Tests
test_skills_watch.pydrives the watcher throughInMemorySkillStore, whoseputnotifies synchronously, and through small store doubles: initial reconcile, coalescing, refusal of a store withoutadd_listener, detaching on close, and a store withoutremove_listener. Three tests onInMemorySkillStore.remove_listenerjointest_skills.py. The end-to-end case, adelete-objectarriving over a live connection and pruning a file, lands with the transport PR where the fake endpoint lives.🤖 Generated with Claude Code
Note
Overview
Adds
watch_skillsandSkillWatcherso agent skill files stay in sync with the configured skill store without waiting for a process restart. The API runs an initialwrite_skillsreconcile, then listens for store delivery changes and re-reconciles on a debounced background thread (listener only wakes the worker; disk I/O never blocks the delivery thread). Revocations can pruneSKILL.mdwithin the debounce window.on_unavailable="keep"remains the default so transport outages are not treated as mass revocations.Extends the optional
SkillStorechange-notification contract withremove_listener(kind, fn), implemented onInMemorySkillStore, soSkillWatcher.close()can detach; stores without it still work but keep listeners registered.watch_skillsraises if no store is configured or the store lacksadd_listener, and cleans up the listener if the initial reconcile fails.Public exports and README/agents docs are updated; new tests cover watcher behavior (coalescing, mid-startup revocations, close/detach) and
remove_listenersemantics.Reviewed by Cursor Bugbot for commit 29ad3fe. Bugbot is set up for automated code reviews on this repo. Configure here.