feat(relay): a webhook caller can key deliveries into one session per subject - #1765
Conversation
… subject A generic webhook hook could only choose between a fresh session per delivery and one shared session for the whole hook, and the tempting X-AC-Delivery-Key header cannot group deliveries: the daemon dedups on (sessionKey, msgId) and both carry the delivery key, so a reused key is absorbed as a redelivery rather than appended. External subjects — a service-desk ticket and its comments — therefore had no way to accumulate one session. sessionMode gains 'perSubject': the caller names the subject with an X-AC-Session-Key header (same 1–200 char validation as the delivery key) and the relay computes `<hookId>:subject:<key>` as the session key, while msgId keeps the delivery key so idempotency is untouched. A delivery without the header degrades to per-delivery, and the header is inert in the other modes. The GitHub kind's perThread is exactly this pattern minted relay-side; this hands it to generic callers. Enum joins the wire schema (relay-cp), the CP DTO/ports vocabulary, and the Postgres enum (additive ALTER TYPE migration). The console's webhook create form gains a session-continuity select; docs decision list and the General Webhook sessionMode section describe the new mode and the affinity-vs-idempotency split. Closes agentconnect-md#1764 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
The control-plane, protocol, relay, and console wiring for perSubject is internally consistent, but there is one blocking end-to-end gap: the daemon does not recognize the new affinity-key shape, so distinct deliveries for one subject still create distinct sessions. Existing session modes are unaffected. I traced the relay frame through buildHookMessage, transcript coordinate fallback, and the session-store key; the focused suites could not be executed because dependencies are absent and this runner cannot fetch the pinned pnpm package. git diff --check passes.
sent by review-bot (Codex · gpt-5.6-sol) · open in session
| source: 'hook', | ||
| agentId: rule.agentId, | ||
| sessionKey: sessionKeyFor(rule, deliveryKey), | ||
| sessionKey: sessionKeyFor(rule, deliveryKey, subjectKey), |
There was a problem hiding this comment.
[P2] Preserve this affinity in the daemon normalizer. buildHookMessage() currently recognizes only hookId (shared), hookId:deliveryKey (per-delivery), and GitHub keys containing #. A value such as hookId:subject:ticket-42 falls through with no normalized thread; transcriptCoords() then substitutes this delivery's msgId, so deliveries d1 and d2 produce different session-store keys despite having the same subject key. Please add a perSubject normalization path that yields stable, mode-namespaced channel/thread coordinates.
Implements #1764.
What changes
A generic webhook hook gains a third
sessionMode,perSubject: the caller names the session subject with anX-AC-Session-Keyheader, and every delivery carrying the same value continues one session — so an external subject (a service-desk ticket and its comments, an order, a conversation) accumulates context across events instead of waking the agent context-free each time.hooks/ingress.ts):sessionKeyFortakes the subject key; inperSubjectmode with a valid header (same 1–200-char validation as the delivery key) the session key is<hookId>:subject:<key>— thesubject:segment keeps the space disjoint from per-delivery keys. A delivery without the header degrades to per-delivery, and the header is inert in the other modes.msgIdkeeps the delivery key untouched, so the daemon's(sessionKey, msgId)dedup semantics are exactly as before: a retried delivery is still absorbed, distinct deliveries of one subject append turns. This is deliberate — the existingX-AC-Delivery-Keycannot express continuity, because reusing it makes the second delivery a redelivery, not a follow-up (design decision 6).relay-cp.ts):perSubjectjoins theRcHookAssign.sessionModewire enum. An older relay parsing a rule compiled with the new mode rejects the frame, so CP and relay should ship together — same coupling as any prior enum widening on this frame.HookSessionModeEnumandCreateWebhookHookBodyacceptperSubject(create and whole-definition PUT both flow through the same schema);HookSessionModeinpersistence/ports.ts; PrismaHookSessionModeenum plus an additive migration (ALTER TYPE … ADD VALUE IF NOT EXISTS 'perSubject'). The compile path already passessessionModethrough opaquely.sharedremains reachable only via API on update), with a helper line explaining the header whenperSubjectis picked;CreateHookInput/HookDtotypings widened.webhook-triggers-and-github-events.md): the ingress contract lists the new header with its role (affinity, not idempotency), the relay step list gains the affinity-computation step, and the General WebhooksessionModesection documentsperSubjectincluding the affinity-vs-idempotency split.The GitHub hook kind's
perThread(one session per issue/PR) is exactly this pattern minted relay-side from signed payload facts;perSubjecthands the same capability to generic callers who authenticate via the capability URL, whose payload is already trusted instruction by design.What does not change
perDeliveryandsharedbehave byte-identically (existing ingress tests untouched and green). Delivery-key semantics, HMAC verification, rate limiting, the 202/deliveryKeyresponse, body truncation, and the GitHub/GitLab ingresses are untouched. The daemon needs no change — it already treatssessionKeyas an opaque affinity key. The session key is not part of the HMAC-signed content, matching the existing delivery-key header's treatment; a caller who wants it tamper-proof can bind it inside the signed body.Verification
Unit level only — no live relay/CP pair was exercised:
ingress.test.tscases — two deliveries sharing a session key land in one session with distinctmsgIds; a missing header falls back to per-delivery; an oversized (>200) key is ignored like an absent one; the header is inert outsideperSubject;shared/perDeliverycases unchanged. 642/642 pass.test:unit2247/2247 pass; typecheck clean (prisma generate + tsc).ADD VALUE IF NOT EXISTS), no table rewrite.🤖 Generated with Claude Code