fix(webhook): a dispatched session owns what it was spawned for - #194
Open
defangdevs wants to merge 1 commit into
Open
fix(webhook): a dispatched session owns what it was spawned for#194defangdevs wants to merge 1 commit into
defangdevs wants to merge 1 commit into
Conversation
local-webhook 0.10.0 declines to spawn a standing-watch session for a CI event that a live session peer's subscription already claims. It never found an owner among hook-* sessions, because a dispatched session subscribes to nothing: ~/.local/state/local-webhook/ held a filter file for every hand-driven session and none for any hook-*. So every event a hook session's own work produced spawned another agent onto it — the second event type one failing run emits (check_run.completed, then workflow_run a minute later: two sessions reading one job), or its pushed fix going red again. AGENTS.md asks sessions to subscribe; nothing made a spawned one do it. agent-box-webhook-spawn now seeds the new session's filter file before launching it, so the brake can see the ownership without the agent having to declare it, and the preamble tells the session what it holds. The seed is the event's own key, never the watch's topic — owning github:defangdevs/* would mute CI spawns for every repo in the org — and it is unpinned with renewOnEvent, so ownership holds while events keep arriving and lapses after silence rather than pinning a session that forgot to remove itself to a repo forever. Ownership ends with the pid: peer_scopes_live() checks liveness, so a leftover file claims nothing. Seeding is best effort and never blocks a spawn; a session without a filter behaves exactly as every hook session did before. Refs #192 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PsqGhkpjsb4kKAdVz25xKN
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.
Fixes half of #192: the half that lives in this repo.
Motivation
local-webhook 0.10.0 already refuses to spawn a standing-watch session for a CI event that a live session peer's own subscription claims. On this box it never found an owner among
hook-*sessions, for a dull reason: a dispatched session subscribes to nothing.~/.local/state/local-webhook/heldfilter.agent-{main,work,portability}.jsonand nofilter.agent-hook-*.jsonat all.So a hook session driving a fix is invisible to the brake, and every event its own work generates spawns another agent onto the same work. Yesterday, 15:02 and 15:03:
hook-…-c022(check_run.completed) andhook-…-6db6(workflow_runfailure) were the same run 30918663010, and both started reading job 92023216058 independently. Earlier the same day, three sessions converged on one failing run on PR #182.AGENTS.mdhas asked sessions to subscribe since the feature landed. Theportabilitysession did, correctly, with a note and everything. Prose works when an agent reads it and does nothing when it doesn't — and a spawned session's first move is to start triaging, not to file paperwork about what it owns.Change
agent-box-webhook-spawnwrites the new session's filter file before launching it, so ownership is a fact the dispatcher can read rather than something the agent has to declare. The preamble tells the session what it now holds, and to look at what else is running before duplicating someone's work.Decisions worth arguing with:
github:defangdevs/*watch must not hand one session ownership of the whole org; the seed isgithub:owner/repofor the repo the event came from.renewOnEvent: true,ttlHours: 2. Ownership holds while events keep arriving and lapses after silence. A pinned session filter is the documented anti-pattern (it keeps interrupting a session that forgot to remove itself); ownership also ends for good with the pid, sincepeer_scopes_live()checks liveness and a leftover file claims nothing.Known gap
The claim only counts once the session's plugin peer opens its socket, seconds after the wrapper returns. The dispatcher's own 60s spawn window covers that in practice; closing it properly wants a CI cooldown in
local-channels, tracked in #192 along with the object-granularity limit (the brake is CI-only because topics are repo-granular while ownership is object-granular).Not in this PR, also #192: the same incident had a second root cause, a 0.8.0/0.10.0 skew between the session plugin cache and the daemon's pinned
webhook.pythat made the brake inert even for sessions that did subscribe. Fixed operationally on the box; the recurrence risk is #193.Checks run
On the box (aarch64), all passing:
nix build .#checks.aarch64-linux.module-generated-up-to-datenix build .#checks.aarch64-linux.{multi-user,module-single-file,download-route,webhook-route}nix eval .#checks.x86_64-linux.webhook.drvPath— the VM test evaluates; it is x86-only, so CI runs it.The wrapper's generated shell was extracted and exercised directly against a temp state dir: seeded topic is the narrow key even when the watch topic is
github:defangdevs/*,ttlHours: 2/renewOnEvent: true, and both failure paths (noLOCAL_WEBHOOK_STATE_DIR, unwritable dir) still spawn with the subscription sentence omitted. The seeded file was then fed to 0.10.0's own reader with a live socket for that key:owned_by_live_sessionclaimsworkflow_runandcheck_runon the seeded repo, claims nothing on another repo, and claims nothing once the pid is gone.tests/webhook.nixgains three assertions: the seeded file's shape and the prompt sentence; an end-to-end pass where a stand-in peer for the dispatched session makes the same CI failure spawn nothing (asserting the named suppression in the journal, that the hook count stays 1, and that the event arrives in that session's channel with its seeded note); and a direct wrapper invocation proving a wildcard watch still seeds the narrow key.Security / cost
No new surface. The wrapper already ran as the user with payload-derived env; the seeded topic goes through
jq --arg, never shell orjqprogram text, and lands in the user's own 0700 state dir. Deliveries to the seeded topic are events for a repo the session was already spawned to work on. No AWS, IAM, or networking impact.🤖 Generated with Claude Code
https://claude.ai/code/session_01PsqGhkpjsb4kKAdVz25xKN