* fix(subscribers): stop an armed autopilot from subscribing its trigger's creator
An autopilot fires on its schedule, its agent files issues, and the member who
armed the trigger is auto-subscribed to every one of them.
The delegated-subscriber rule (MUL-5483) subscribes the human an agent-created
issue exists on behalf of, and it tested that by asking whether the origin run
carries an originator. That was sound when it was written: direct_human was the
only attribution root that left an originator behind, so "this run carries a
human" and "a human asked for this work" were the same statement.
MUL-6951 separated them. An armed schedule/webhook trigger now runs with its
creator's authorization (trigger_owner), and attribution COPIES that human down
every agent hop, so the rule started reading an automation's authorization as a
request. attribution.DelegatedSubscriber already documents this exact outcome as
one it must not produce — the origin_type='autopilot' exclusion says the
autopilot's own subscriber template, not whoever armed the trigger, is the
audience for its issues. The regression arrives one hop below that exclusion,
where the issue's origin is an ordinary agent_create.
The rule now asks what it means: did this chain BEGIN with a member acting. The
originator alone cannot answer that below the first hop, because a hop relabels
its own originator_source to 'delegation' — so GetDelegatedSubscriptionFacts
walks delegated_from_task_id back to the run that actually resolved the human and
returns that root's label alongside the originator, in one workspace-scoped read
that replaces the previous single-task load. Accepting only direct_human makes it
a whitelist: the next root that resolves a human some other way stays silent here
until someone decides otherwise, instead of switching this rule on for a
population nobody chose.
No schema change and no change to attribution itself: originator, accountable and
every authorization decision are untouched. Only who gets subscribed moves.
Verification (real PostgreSQL 17): internal/attribution, cmd/server pass; the
four new tests fail against the unfixed rule, and the tenant-guard test fails
when the workspace predicate is removed from the new query. internal/handler and
internal/service each keep one pre-existing failure that reproduces identically
on a clean tree (source-context object-store cleanup), as do cmd/multica,
internal/cli, internal/daemon and internal/daemon/execenv.
Co-authored-by: multica-agent <[email protected]>
* test(subscribers): close the review gaps in the lineage walk's coverage
Addresses Elon's review on #8031.
The 32-hop stop is accepted as a product limit (Bohan's call), so it is now
written down as behaviour instead of contradicted by it. DelegatedSubscriber
claimed "No depth cap", which was true of WHO the human is — the originator is
copied onto every run — but not of proving the chain began with them, which is
the part that walks. The query comment called the cap cycle protection only. Both
now say what actually happens: past 32 hops the root is unproven and nobody is
subscribed. TestDelegatedSubscribe_LineageWalkStopsAt32Hops pins both sides, so
the limit cannot drift silently; moving it to 3 reddens the 32-hop case.
The tenant test claimed to cover every hop and covered one. An issue pointing
straight at a foreign run only exercises the anchor's predicate, so deleting the
recursive branch's workspace_id left it green. It is now two cases, and the added
one — a local run naming a foreign parent — fails with that predicate removed
while the original case still passes, which is exactly the blind spot.
The chain fixture builds through internal/testutil instead of hand-writing an
INSERT with a matching cleanup, per the repository's test convention.
No production behaviour changes: the rule, the walk and every existing assertion
are untouched.
Verification (real PostgreSQL 17): cmd/server, internal/attribution,
internal/service pass; both new tests were run against a deliberately broken
query and fail there. gofmt, go vet, git diff --check clean.
Co-authored-by: multica-agent <[email protected]>
---------
Co-authored-by: J <[email protected]>
When an agent created a sub-issue while working on a human's behalf, that human
received no notifications for it at all. issue_subscriber modelled ACTOR
identity, so an agent-created, agent-assigned issue had a full subscriber list
and zero members to deliver to. The platform already knew who the work was for
(agent_task_queue.originator_user_id, MUL-4302); notification never asked.
- attribution.DelegatedSubscriber: one shared rule over the same origin
waterfall ClassifyDirect uses. agent_create subscribes the originator as
'delegated'; quick_create keeps the direct 'creator' tier; autopilot and
degraded attribution subscribe nobody.
- Delegated is a reduced delivery tier: in_review/done/cancelled/blocked plus
failures and mentions. Routine churn is suppressed, and the parent bubble
cannot re-deliver what the tier dropped.
- Unsubscribe becomes stateful: an unsubscribed_at tombstone survives later
rule passes, and opt_out_scope distinguishes "this issue" from "this subtree"
so a narrow opt-out no longer silently suppresses future children.
- Subtree unsubscribe is its own endpoint. A body flag cannot fail loudly
against an older backend (Go drops unknown fields); an unknown route 404s,
which the UI now surfaces with a distinct message.
- Eligibility and the write share one statement under a (workspace, user)
advisory lock that subtree unsubscribe and member revoke also take, closing
the check-then-insert races. Revoke additionally clears the departing
member's subscriptions in the same tx.
- UI explains a delegated subscription and offers both unsubscribe scopes.
Migrations 249/250 add the delegated reason, the opt-out tombstone, and the
opt-out scope, using NOT VALID + VALIDATE CONSTRAINT so the widened CHECK does
not scan issue_subscriber under an exclusive lock.
Reviewed across eight rounds; an earlier write-time subtree roll-up was built
and then removed in full once it proved unfixable without serializing every
topology mutation. The parent's own status transition already carries that
signal.
Closes MUL-5483.
- Add migration 106: CREATE INDEX CONCURRENTLY on member(user_id, workspace_id)
- Rewrite ListWorkspaces to drive from member table with explicit fields
- Regenerate all sqlc code with v1.31.1 (intentional version upgrade)
Co-authored-by: multica-agent <[email protected]>
Replace inbox_listeners.go with a subscriber-driven notification system:
- Add issue_subscriber table with auto-subscribe on create/assign/comment
- New subscriber_listeners.go: maintains subscriber data on domain events
- New notification_listeners.go: notifySubscribers (fanout to all subscribers
minus actor) and notifyDirect (targeted, punches through unsubscribe)
- Subscriber API: list/subscribe/unsubscribe endpoints
- Frontend: subscribers section in issue detail sidebar with real-time sync
- Frontend: inbox notification grouping by (issue_id, type, actor_id)
- Remove createInboxForIssueCreator from task.go (unified through event bus)
- 21 new Go tests, all passing
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>