Webhook-driven @coilyco-ops summons: ack-on-receipt, poll-for-reply, full o11y #375
Labels
No labels
burndown-2026-06
coherence-core
consult
headless
interactive
P0
P1
P2
P3
P4
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
coilyco-flight-deck/infrastructure#375
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Why
The
@coilyco-opssummon system today (#374,scripts/coilyco_ops_summon.py) is a 30s polling timer that dispatches Goose advisory and posts the answer. Three separate bugs already left it silent on a live summon (coilysiren/inbox#110): a missing token scope, a 403 on the permission gate, and wrong kai-server paths. All three are fixed now, but the shape of the thing means it will be down a large fraction of the time - Goose flakes, the model host flakes, the poll misses, the clone fails. And when it's down, the failure mode is the worst one: the human comments into a void and gets nothing back.The fix is not "make the poller more reliable." It is "make silence impossible, and make the system observable enough to debug when it misbehaves" - because this is going to be buggy and is a prime o11y target.
What
A webhook-driven summon flow with an immediate acknowledgement and a bounded poll-for-reply, replacing the blind 30s timer's role as the primary path.
The webhook does not synchronously drive Goose ("does not push to claude"). It does the minimum fast work, returns 200, and hands off:
issue_comment/issueswebhook hits a tailnet-only receiver (no public surface; same network posture as the rest of the fleet). Verify the Forgejo HMAC signature, reject anything unsigned.is_authorized+COILYCO_OPS_SUMMON_ALLOWallowlist) fromcoilyco_ops_summon.py. Drop non-mentions, unauthorized authors, and the bot's own comments. Idempotent per delivery/comment id.run_goosethrowaway-clone path, still advisory-only, never pushes). The webhook handler does not block on it.X(configurable, default e.g. 15 min). On success, post the answer (or edit the ack into the answer). On timeout or error, post a visible failure comment with the reason and a correlation id. Silence is never an acceptable terminal state.Keep the existing poller as a reconcile backstop (webhooks get missed/dropped) on a slow cadence, not the primary latency path.
Carry-over correctness bug to fix in the rework: the current poller's idempotency marks a comment "processed" (
processed.add(comment["id"])) before the authorization check and Goose dispatch, and persists it at end of poll regardless of outcome. So any transient failure - a 403, a Goose crash, an exception mid-service - permanently burns that summon: the id is recorded as handled and the comment is never retried. Combined with "this fails a lot," that means failed summons silently never get answered. Idempotency must key on a successful reply posted, not on "seen." (Hit live: a 403 during gating recordedinbox#110's comment as processed, so the next clean run skipped it with0 serviced.)Observability - first-class, not bolted on
This is the explicit point of the issue. Treat the whole lifecycle as an o11y target.
received,signature_verified,gated,authorized,acked,dispatched,goose_started,goose_finished,replied,timed_out,errored), each carrying a correlation id (Forgejo delivery id + comment id + repo#index) so a single summon is greppable end to end.summons_received,acked,dispatched,replied,unauthorized,timed_out,failed{stage}) and histograms (ack_latency,goose_run_seconds,time_to_reply_seconds). Export to the fleet's existing metrics stack.goose run) wrapped; failures are categorized, surfaced as a comment (never swallowed into silence), retried with backoff where safe, and a repeated-failure / dead-letter condition fires the fleet's red-channel alert (Telegram).Second carry-over bug:
run_gooseinvokesgoose run --no-session --system ... -t taskwithout--no-profile, so it inherits whatever default Goose profile/extensions the host has. On the first live reply (inbox/#110comment 15366) that meant Goose loaded theapps/extensionmanagertoolset and had no file tools - it triedreadand got "Tool 'read' not found." The advisory run needs the developer/file extensions explicitly (or the anti-thrash--no-profile+ a pinned extension set, matching thegoose-triageharness), or the answers are useless even when the plumbing works.Open decisions for the implementer (flag, don't guess)
tailscale serve, or fold into an existing fleet service? Tailnet-only either way.Xpoll-window default and the ack copy./forgejo/coilyco-ops/webhook-secret, SecureString) and the per-repo webhook registration (script, mirroringprovision-coilyco-ops-bot.sh).Done when
@coilyco-opssummon produces an ack comment within seconds, and either a Goose answer or an explicit failure comment within the poll window - never silence.refs #374
Triage:
P3/consult- Webhook-driven summons; design + webhook infra first.(goose-style pass; claude as the judgment engine, applied 2026-06-23)