docs(server): forge self-origin notification suppression record (RIG-3326) - #913
Closed
rigel-mintaka wants to merge 1 commit into
Closed
Conversation
|
Compass engineering docs preview: https://compass-forge-rig-3326-suppr.compass-eng-docs.pages.dev Deployed from Changed pages: |
rigel-mintaka
force-pushed
the
compass-forge/rig-3326-suppression-design
branch
7 times, most recently
from
September 6, 2026 05:25
7b868ee to
200edb4
Compare
rigel-mintaka
marked this pull request as ready for review
September 6, 2026 05:25
…3326)
Design record for suppressing self-origin forge notifications at the
notify-router fan-out: an agent no longer wakes on its own COMMENT / REVIEW /
OPENED / STATE on a subscribed artifact, matched on owner-qualified Compass
handles, failing open on any ambiguity. CHECKS is never suppressed. A suppressed
notification advances delivered_revision only when the subscriber was already
caught up (CAS-guarded, artifact-scope only), so the reconcile sweep neither
resurrects it nor masks a prior undelivered real notification.
The owner half of that identity is not on the wire today: AgentAttribution is
{agent_handle} only, so the parse discards the owner it already produces and the
COMMENT/REVIEW arm would be structurally inert. T0 restores owner_handle and
adds an internal ForgeEvent.Actor carrier for STATE (RIG-3331 populates it).
Sibling of Record A (forge self-delegate write path, PR #900); depends on
RIG-3331 (agent-driven state-transition write op) for the STATE arm to suppress
on the real transition actor rather than an author-row proxy.
Ledger-impact: adds DL-334 (self-origin notification suppression) and DL-335
(owner_handle carriage, amending the DL-094 clause DL-186 carried forward;
DL-186 stays Active).
Co-authored-by: Matt Wilkinson <matt@rigel.build>
resolve
rigel-mintaka
force-pushed
the
compass-forge/rig-3326-suppression-design
branch
from
September 6, 2026 05:47
200edb4 to
15e8655
Compare
This was referenced Sep 6, 2026
Open
rigel-mintaka
added a commit
that referenced
this pull request
Sep 7, 2026
Both load-bearing Open Questions are ruled, so the record flips Draft -> Active and its ledger rows land in this push. - OQ-1: the actor carrier is the consumable `forge_state_transitions` memo, not a synthetic STATE event. RIG-3326 is the side whose text bends -- its STATE arm resolves the actor through a memo lookup rather than off the event body. T5 keeps its subject and T4's memo half is in scope. - OQ-2: Linear default-state resolution defaults only where the target is unambiguous and fails `invalid_argument` naming the candidates otherwise, replacing the positional tie-break. A mandatory `workflow_state` was also rejected: it forces a provider-aware state fetch into every callsite and splits GitHub/Linear ergonomics. Measured at freeze, the Rigel team has exactly one `completed`, one `unstarted` and one `backlog` state, so the original "silently picks a board column" premise had no choice to make -- the rejection arm is a guard against future drift, covered by fixture because no current board reproduces it. All nine task slices are now unconditional; T3 gains the multi-candidate rejection fixture. DL-342/DL-343 appended with the real ruling date, ids re-verified next-free against main (highest DL-337) and every open design PR (#913 338/339, #900 340, #932 341). Refs RIG-3331 Co-authored-by: Matt Wilkinson <matt@rigel.build>
trunk-io Bot
pushed a commit
that referenced
this pull request
Sep 7, 2026
* docs(design): forge state-transition write op record (RIG-3331)
Design record for the forge state-transition write op: an agent sets an
issue/PR state on both GitHub and Linear through the same attribution
chokepoint every other forge write rides.
Portable {open, closed} core plus per-provider refinements (close_reason on
GitHub, workflow_state on Linear), fail-loud invalid_argument on a
refinement/provider mismatch, ErrUnsupported on the Linear PR half.
Transitions mutate an existing coordinate, so they are neither F3-deduped nor
DL-055-recorded, following the comment-arm precedent.
The load-bearing contract is actor attribution: the emitted STATE event must
carry the acting agent so RIG-3326's suppression arm can key on the real
transition actor rather than the DL-055 author-row proxy, which would wrongly
eat a human-close notification to the author.
Two load-bearing Open Questions are held for Matt: the actor-carrier mechanism
(OQ-1, a cross-record contract with RIG-3326) and the Linear default
workflow-state rule (OQ-2, a product-behavior call).
Refs RIG-3331
Co-authored-by: Matt Wilkinson <matt@rigel.build>
* docs(design): close the review findings on the state-transition record (RIG-3331)
Adversarial review returned 4 high, 5 medium, 3 low. Fixes, each verified
against the cited source:
Two fabricated citations removed (rule://planning-evidence). The record named
translateAttribution as an existing symbol - it does not exist anywhere in the
tree; the real path is stripBodyToRef plus the detect-path attribution
assignment. It also quoted 'deliberately never imports the store' as if from
the ingest package; the actual term of art is the no-store rule. Both
underlying claims were true, only the citations invented.
The F3/DL-055 argument reached the right conclusion from the wrong premise.
The comment arm is excluded because a comment is unrepresentable in the store
index; a transition targets a coordinate that IS representable and often
already occupied. The record inverted the risk as 'no row exists at the
coordinate at all'. The real hazard is a row that DOES exist:
RecordAuthoredArtifact's ON CONFLICT DO UPDATE overwrites client_request_id,
which backs the F3 memo through a unique partial index, so routing a
transition through record would destroy the original create's idempotency
memo. Conclusion kept, reasoning replaced, hazard named.
T1 was red by construction: it widened Provider while extending only the fake,
but four compile-time satisfaction assertions live in that package, so the
GitHub and Linear ones break immediately and T2/T3 (the repair) come after.
T1 now lands the interface with all four implementors.
The transition memo omitted tenant_id. Its sibling forge_authored_artifacts is
tenant-keyed and defended by a two-tenants-same-coordinate test, so without it
one tenant's memo could attribute another's STATE event. The RLS catalog test
audits tables that carry the column, so an omission is invisible to it - the
column now ships with three explicit obligations instead of a reliance on the
guard.
Three tasks silently depended on unruled Open Questions while the checklist
presented nine unconditional slices. T3 is OQ-2-contingent, T4's memo half and
all of T5 are OQ-1-contingent, each now tagged with what changes under the
alternative ruling.
Also: ruled the ambiguous-workflow-state-name case (Linear does not enforce
name uniqueness within a team) rather than leaving an implementer to invent a
rule on the write path; specified the workflow-state cache separately from the
invalidation-free teamIDs cache, since humans rename and reorder states;
named the webhook/sweep double-emission and cursor-advance races; stated the
memo read's tenant context; pinned the table to 0001_init.sql per the
collapsed-migration convention; corrected the DL enumeration to include PR
#932's DL-341; resolved the T8 contradiction (the rows ship in the freeze
push, because DL-343's substance is what OQ-1 asks Matt to rule, and stamping
'Active (Matt)' now would attribute a decision he has not made); and flipped
Status to Draft.
Refs RIG-3331
Co-authored-by: Matt Wilkinson <matt@rigel.build>
* docs(design): freeze forge state-transition record (RIG-3331)
Both load-bearing Open Questions are ruled, so the record flips Draft ->
Active and its ledger rows land in this push.
- OQ-1: the actor carrier is the consumable `forge_state_transitions` memo,
not a synthetic STATE event. RIG-3326 is the side whose text bends -- its
STATE arm resolves the actor through a memo lookup rather than off the
event body. T5 keeps its subject and T4's memo half is in scope.
- OQ-2: Linear default-state resolution defaults only where the target is
unambiguous and fails `invalid_argument` naming the candidates otherwise,
replacing the positional tie-break. A mandatory `workflow_state` was also
rejected: it forces a provider-aware state fetch into every callsite and
splits GitHub/Linear ergonomics. Measured at freeze, the Rigel team has
exactly one `completed`, one `unstarted` and one `backlog` state, so the
original "silently picks a board column" premise had no choice to make --
the rejection arm is a guard against future drift, covered by fixture
because no current board reproduces it.
All nine task slices are now unconditional; T3 gains the multi-candidate
rejection fixture. DL-342/DL-343 appended with the real ruling date, ids
re-verified next-free against main (highest DL-337) and every open design
PR (#913 338/339, #900 340, #932 341).
Refs RIG-3331
Co-authored-by: Matt Wilkinson <matt@rigel.build>
---------
Co-authored-by: Matt Wilkinson <matt@rigel.build>
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 PRs:
mainDesign record for suppressing self-origin forge notifications at the
notify-router fan-out: an agent no longer wakes on its own COMMENT / REVIEW /
OPENED / STATE on a subscribed artifact, matched on owner-qualified Compass
handles, failing open on any ambiguity. CHECKS is never suppressed. A suppressed
notification advances delivered_revision only when the subscriber was already
caught up (CAS-guarded, ARTIFACT-scope only), so the reconcile sweep neither
resurrects it nor masks a prior undelivered real notification.
Sibling of Record A (forge self-delegate write path, PR #900); depends on
RIG-3331 (agent-driven state-transition write op) for the STATE arm to suppress
on the real transition actor rather than an author-row proxy. That is a
dispatch-ordering dependency, not a freeze blocker — the STATE arm stays
interim-open until RIG-3331 lands.
Ledger-impact: adds DL-338 (self-origin notification suppression) and DL-339
(
compass.v1.AgentAttributionregainsowner_handle, field 2).Why DL-339
The review of record found the primary arm structurally inert: the owner half of
the owner-qualified handle never reaches the router.
AgentAttributioncarriesagent_handleonly, so all three CommentRef-build sites discard the owner thatStripOwneralready parses, leaving every COMMENT/REVIEW actor unqualified —which fails open, by design, and therefore never suppresses.
DL-094 dropped the field for a display consumer, and DL-186 carried that clause
forward while reclaiming the field numbers. Neither weighed an identity-matching
consumer: a bare handle is unique only per owner, so it cannot key a match.
Matt ruled to re-add the field. DL-339 amends the display-scoped clause and
leaves DL-186 Active, since its wire-compat-strip clause — the row's actual
subject — is untouched (the DL-308/DL-324 precedent).
T0 lands the proto field, populates it at the three parse sites, updates the UI
adapter that hardcodes
ownerHandle: "", and addsForgeEvent.Actoras theSTATE arm's carrier.
Review of record
Six rounds, one reviewer, every finding dispositioned — zero deferred, no
follow-up issues owed.
The instructive thread:
scopehad to traverse a hop chain across a mirroredtype boundary, and enumerating hops kept failing — each round named one more hop
and the next round found the one above it (the go/server adapter, then a second
ForgeNotifySubscriberproducer on the sweep lane). What terminated it was atype-quantified invariant plus an exhaustive constructor grep: prove the set is
closed rather than list its members.
Co-authored-by: Matt Wilkinson matt@rigel.build