Skip to content

fix(gateway): journal every guarded result the drain closes silently - #5346

Merged
sonichi merged 2 commits into
mainfrom
fix/5342-dm-only-channel-skip-journal
Oct 12, 2026
Merged

sonichi merged 2 commits into
mainfrom
fix/5342-dm-only-channel-skip-journal

Conversation

@sonichi

@sonichi sonichi commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Closes #5342 (follow-up to #5340).

What changed, and why

On a team task, [dm-only]\n[channel: X]\n[no-send]\n… (and the same with [REPLIED] or [deduped:]) closed with no owner record:

  • The guard delivers it. Under the plain parse_markers, [dm-only] drops the redirect and the skip after [channel:] stays text, so there is no redirect to withhold.
  • The drain closes it. _post_ready_results parses with skip_after_channel=True (fix(gateway): a skip after [channel:] on an owner result is a skip #5340), sees a skip, and closes the lease no_send.
  • The journal skipped it. _guarded_result_body journaled only when the plain is_suppression_only(body) was true. Here it was false, so no state/suppressed-team-results/wr_*.json was written.

Fix: the journal and the drain read one verdict, with no second rule.

  • is_suppression_only(body, skip_after_channel=False) in src/policy/egress/result.py passes the flag to parse_markers. The default is unchanged, so the guard's classification and the owner-mention rule do not change. The vendored team_result_guard.py is synced (ag2-sparrow-drift passes).
  • The gateway has one constant, DELIVERY_SKIP_AFTER_CHANNEL = True. Five places pass it: the journal check and the four delivery decisions (_post_ready_results, the orphan sweep, _quarantine_unsent, _proactive_route).
  • Skip is terminal in parse_markers. So "the delivery parse has a skip" (what the drain closes on) and "is_suppression_only with the flag" (what the journal records on) are the same predicate.
  • The shared case table tests/fixtures/thread-ask-cases.json gains the [dm-only]+[channel:]+[no-send] and [channel:]+[dm-only]+[deduped:] rows. result-markers-thread now checks is_suppression_only (src and vendored) against both skip and delivery_skip on every row, so the journal cannot drift from the drain's verdict.
  • Rui's mutant M5: dropping the flag in _quarantine_unsent is now caught by case f of the new test.

Look first at: the _guarded_result_body journal condition and the DELIVERY_SKIP_AFTER_CHANNEL constant in packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py, and is_suppression_only in src/policy/egress/result.py.

Not changed:

Before / after (production wrapper: src/remote-gateway-bridge.py in-process, fake _req, #5326 harness)

Before, at origin/main 0eff80fe2 with this PR's tests copied in:

$ env -i HOME=$HOME PATH=/usr/bin:/bin:/opt/homebrew/bin python3 tests/gateway-dm-only-channel-skip-journal.test.py
  FAIL a0) team: the silent close has an owner record; got []
  FAIL a1) team: the silent close has an owner record; got []
  FAIL b) team [deduped:]: the silent close has an owner record; got []
  FAIL f) team [dm-only]+[channel:]+[no-send]: recorded; got []
FAIL: 4 failure(s)
$ env -i HOME=$HOME PATH=/usr/bin:/bin:/opt/homebrew/bin python3 tests/result-markers-thread.test.py      # at 0eff80fe2
TypeError: is_suppression_only() got an unexpected keyword argument 'skip_after_channel'

After, at head c3684ca92:

$ env -i HOME=$HOME PATH=/usr/bin:/bin:/opt/homebrew/bin python3 tests/gateway-dm-only-channel-skip-journal.test.py
  ok   a0) team: one lease-close POST, nothing visible; got [{'id': 'dj-a0', 'body': '[no-send]', 'no_send': True}]
  ok   a0) team: the silent close has an owner record; got [{'agent_id': '@agent:example.org', 'context': {'channel_id': '!taskroom:example.org', 'reply_to_event': '', 'room_name': '', 'source': 'ag2s
  ok   a1) team: one lease-close POST, nothing visible; got [{'id': 'dj-a1', 'body': '[REPLIED]', 'no_send': True}]
  ok   a1) team: the silent close has an owner record; got [{'agent_id': '@agent:example.org', 'context': {'channel_id': '!taskroom:example.org', 'reply_to_event': '', 'room_name': '', 'source': 'ag2s
  ok   b) team [deduped:]: never posted as text; got []
  ok   b) team [deduped:]: routed through the dedup plan; got ['dedup requeue for dj-b (holder dj-holder is not a valid delivery for this task)']
  ok   b) team [deduped:]: the silent close has an owner record; got [{'agent_id': '@agent:example.org', 'context': {'channel_id': '!taskroom:example.org', 'reply_to_event': '', 'room_name': '', 'sour
  ok   c) team [no-send]: closed and recorded; got [{'id': 'dj-c', 'body': '[no-send]', 'no_send': True}] [{'agent_id': '@agent:example.org', 'context': {'channel_id': '!taskroom:example.org', 'reply_
  ok   d) owner: one lease-close POST, canonical body; got [{'id': 'dj-d', 'body': '[no-send]', 'no_send': True}]
  ok   d) owner: no suppression record; got []
  ok   d) owner: archived
  ok   e) owner: one lease-close POST, canonical body; got [{'id': 'dj-e', 'body': '[no-send]', 'no_send': True}]
  ok   e) owner: no suppression record; got []
  ok   e) owner: archived
  ok   f) owner [channel:]+[no-send]: archived as suppressed; got ['result dj-f: a different reply was published after this id was delivered; it is suppressed ([no-send]), owes no room delivery, and i
  ok   f) owner [channel:]+[no-send]: never quarantined as a redirect; got ['result dj-f: a different reply was published after this id was delivered; it is suppressed ([no-send]), owes no room delive
  ok   f) team [dm-only]+[channel:]+[no-send]: archived as suppressed; got ['result dj-g: a different reply was published after this id was delivered; it is suppressed ([no-send]), owes no room delive
  ok   f) team [dm-only]+[channel:]+[no-send]: recorded; got [{'agent_id': '@agent:example.org', 'context': {'channel_id': '!taskroom:example.org', 'reply_to_event': '', 'room_name': '', 'source': 'ag

PASS: 0 failure(s)

Mutant M5 at head c3684ca92: _quarantine_unsent changed to plain parse_markers(body).

== gateway-dm-only-channel-skip-journal
  FAIL f) owner [channel:]+[no-send]: archived as suppressed; got ['result dj-f: a different reply was published after this id was delivered — quarantined to undelivered/; review it; it may already ha
  FAIL f) owner [channel:]+[no-send]: never quarantined as a redirect; got ['result dj-f: a different reply was published after this id was delivered — quarantined to undelivered/; review it; it may a
  FAIL f) team [dm-only]+[channel:]+[no-send]: archived as suppressed; got ['result dj-g: a different reply was published after this id was delivered — quarantined to undelivered/; review it; it may a
FAIL: 3 failure(s)
== gateway-channel-skip-owner
PASS: 0 failure(s)

gateway-channel-skip-owner (#5340's test) passes under the mutant. That is the gap Rui found, and the new test's case f closes it.

Tests run (head c3684ca92, all under env -i HOME=$HOME PATH=/usr/bin:/bin:/opt/homebrew/bin)

PASS tests/gateway-dm-only-channel-skip-journal.test.py
PASS tests/gateway-channel-skip-owner.test.py
PASS tests/gateway-task-result-thread-ask.test.py
PASS tests/sparrow-result-guard.test.py
PASS tests/result-markers-thread.test.py
PASS tests/result-markers.test.py
PASS tests/result-markers-golden.test.py
PASS tests/team-result-guard.test.py
PASS tests/team-guard-suppress-journaled-silent-close.test.py
PASS tests/unreadable-tier-never-substitutes-the-notice.test.py
PASS tests/delivery-primitives-single-owner.test.py
PASS tests/events-plane-boundary.test.py
PASS tests/ag2-sparrow-drift.test.py
PASS tests/gateway-proactive-thread.test.py
PASS tests/bridge-marker-no-leak.test.py
PASS tests/gateway-orphan-result-reconciler.test.py
PASS tests/gateway-owner-mention.test.py
PASS tests/gateway-terminal-result-moved-once.test.py
PASS tests/gateway-disposal-delegation.test.py
PASS tests/dm-result-skip-markers.test.py
FAIL(1) src/remote-gateway-bridge.test.py
  • The architecture gates delivery-primitives-single-owner and events-plane-boundary pass.
  • src/remote-gateway-bridge.test.py: one check fails, env-fallback: no env token and no file yields empty token. It fails the same way at 0eff80fe2 on this host (also noted on fix(gateway): a skip after [channel:] on an owner result is a skip #5340), so this PR does not cause it.
  • TS marker suites (task-bridge-thread-ask-strip, task-bridge-skip-markers, task-bridge-skip-marker-ownership, task-bridge-relay-thread-ask, task-bridge-destined-proactive, relay-agent, relay-agent-e2e): 84/84 pass, including the new table rows through bodyIsSkipMarked and stripVoiceControlLines.
  • bash scripts/review-checks.sh --diff <(git diff origin/main...HEAD): PASS. git diff --check: clean. ruff check on the changed Python: clean.

Live witness

First attempt, run at 55e79e3, head against parent: #5346 (comment). A fresh gateway process with a broker double served one Team-tier task. The head journals the guarded [dm-only]+[channel:]+[no-send] close in state/suppressed-team-results/, and the parent closes it silently with no record. Gaps stated there: the broker was a double, and it was a fresh process, not a launchd restart.

Real post-restart witness, at this head (c3684ca): #5346 (comment). The actual launchd gateway service was booted out and bootstrapped onto this head against the live workspace; a real broker-attested Team task (task-a092cc999635559953, from a reviewer's collaborator agent in Sutando Triage) got the guarded [dm-only]+[channel:]+[no-send] body; the outbox shows exactly one no_send close, the Triage room and owner DM show zero posts of the witness text, and state/suppressed-team-results/ gained exactly one new matching record. The service was restored to normal afterward (SIGTERM at 04:37:32, bootstrap at 04:37:33, workspace-poller lock re-acquired at 04:37:37, core watcher ready after). The #5340 owner witness is a separate item.

Docs

docs/claude-md-moved-detail.md (Result-marker semantics): the [thread:] entry now says the gateway's suppression journal reads the delivery verdict, so a guarded [dm-only]+[channel:]+skip close is recorded.

Hardcoded paths

None added. review-checks passed.

🤖 Generated with Claude Code

A team result `[dm-only]\n[channel: X]\n[no-send]\n...` (or `[deduped:]`) has
no redirect under the plain parse, because [dm-only] drops it, so the guard
delivers it unchanged. The drain parses with skip_after_channel=True, sees a
skip and closes the lease no_send, but the suppression journal keyed on the
plain is_suppression_only, so no suppressed-team-results/wr_*.json was written:
a guarded result closed with no owner record.

The journal and the drain now read one verdict. is_suppression_only takes the
parse_markers flag, and the gateway passes one constant,
DELIVERY_SKIP_AFTER_CHANNEL, at the journal and at all four delivery decisions.
The shared case table gains the [dm-only]+[channel:]+skip rows and checks
is_suppression_only against both verdicts. A new production-wrapper test pins
the team records, leaves owner results unchanged, and catches removing the flag
in _quarantine_unsent, which no test caught before. Vendored guard synced.

Closes #5342

Stand: Echo Act IV Pro

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

Coverage Gate

✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 91%.

Diff Coverage

Diff: origin/main...HEAD, staged and unstaged changes

  • packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py (100%)
  • src/policy/egress/result.py (100%)

Summary

  • Total: 8 lines
  • Missing: 0 lines
  • Coverage: 100%

@sonichi

sonichi commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

@qingyun-wu review requested at 55e79e300. This closes #5342, rui's post-merge finding on #5340: a team-tier [dm-only] + [channel:] + skip result closed with no suppression record. The journal and the drain now share one skip decision (DELIVERY_SKIP_AFTER_CHANNEL). The new wrapper-level test fails 4 checks on main, and rui's _quarantine_unsent mutant is now caught. #5340's own post-merge witness passed on main at 9:02 PM PDT (comment 6105255310); this PR's witness runs at the approved head.

Stand: Echo Act IV Pro

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 55e79e300. No blockers. This fixes #5342, which I filed.

Verified:

  • Both team reproductions from #5340 now close silently and write suppressed-team-results/wr_*.json. They are [dm-only]+[channel:]+[no-send], and [channel:]+[dm-only]+[deduped:].
  • The owner tier is unchanged. [channel:]+[no-send] on an owner task still skips, with no record.
  • The flag is used everywhere it should be. DELIVERY_SKIP_AFTER_CHANNEL is passed at the journal (:1186–1187) and at all four delivery sites (:3822, :4618, :4847, :5194). The remaining plain-parse calls (:928, :3837, :4815) are correct as plain.
  • Tests: the new test fails 4 checks on 0eff80fe and passes at the head. Each of these mutations is caught by at least one test:
    • the flag set to False at each site, including M5 at _quarantine_unsent;
    • the constant set to False;
    • either guard copy ignoring the flag.
  • Copies and suites: the guard and marker copies are byte-identical. The related Python suites and the TS shared table are green, and so are CI and review-checks.
  • Independent confirmation: Rui's Codex verifier ran a 12-case owner/team matrix. It found 0→1 records on all six team cases and identical owner behaviour.

Owner-mention path: I agree it belongs in its own PR, because adding the flag there would move that result from the owner's DM into a withheld review. Please file an issue so it is tracked.

Not approving yet: I'll approve once a live witness is posted at this head. The #5340 owner witness can run in the same session.

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 55e79e3. I found no blockers. I can't mark it ready yet because no maintainer has approved this head. The one review here is john-the-dev's COMMENTED review, and it reports no blockers.

What I checked

  • Tests: in a fresh worktree under env -i, these Python suites pass at the head: gateway-dm-only-channel-skip-journal, gateway-channel-skip-owner, gateway-task-result-thread-ask, result-markers-thread, result-markers, result-markers-golden, team-result-guard, team-guard-suppress-journaled-silent-close, ag2-sparrow-drift, sparrow-result-guard, bridge-marker-no-leak, gateway-orphan-result-reconciler, gateway-proactive-thread, gateway-owner-mention, delivery-primitives-single-owner, dm-result-skip-markers.
  • TS: task-bridge-skip-marker-ownership gave 28/0 and task-bridge-skip-markers gave 8/0. In task-bridge-thread-ask-strip, all 12 subtests reported ok, including the shared-table suite with the two new rows. On this host its process did not exit before my 120s cap. CI's tsc + tests is green.
  • Vendored copies: src/policy/egress/result.py is byte-identical to packages/ag2-sparrow/ag2_sparrow/team_result_guard.py, and src/result_markers.py is byte-identical to its vendored copy. Marker grammar is still parsed only by parse_markers.
  • One constant at all five sites: grep finds DELIVERY_SKIP_AFTER_CHANNEL at the journal (remote_gateway_bridge.py:1186), _proactive_route (:3822), _quarantine_unsent (:4618), _post_ready_results (:4847) and the orphan sweep (:5194). Three plain parses remain:
    • _released_review_body (:928) only strips markers from an owner-released body.
    • _proactive_thread_root (:3837) only reads [thread:].
    • The owner-mention DM text (:4815) is covered in item 4 below.
      None of the three decides skip or redirect.
  • Revert controls:
    • I dropped the flag at the journal. The new test fails 4 times (a0, a1, b, f-team), and no record is written.
    • I dropped it at _quarantine_unsent (M5). The new test fails 3 times, all in case f, and the body is quarantined as a redirect.
    • I set the constant to False. The new test fails 13 times, and gateway-channel-skip-owner fails 12 times.
    • I restored the file after each control.

Security direction (the part I looked at hardest)

  • Journal direction: journal_suppressed_result only acts when the verdict is deliver (result.py:671). So the wider predicate cannot turn a withheld (leak) verdict into a silent close. A withheld body still gets its owner review. If the record cannot be written, it still fails closed to the notice.
  • Generated corpus: I built 135,537 bodies from every order of 0–4 leading lines ([dm-only] alone or padded, two [channel:] targets, [thread], [thread:], [reply:], a same-line [channel:] [dm-only]). Each set of leading lines was followed by a skip ([no-send], [REPLIED], [deduped: …], a malformed one, or none) and a tail. I added 4,000 random interleavings. I ran each body through the real classify_result_for_tier at team, guest, other and ambient tiers, with the scanner on, and through the drain's flagged parse_markers.
    • At head: 0 bodies are skipped by the drain with neither an owner review nor a suppression record.
    • On main: 87,492 such tier×body pairs. All are [dm-only]-defeated redirects plus a skip. That is #5342.
    • 0 bodies lost a journal record they had on main.
    • The drain's parse is not changed by this PR, so no body that main delivered is now dropped.
  • [dm-only] privacy: on the same corpus, at every tier including owner, no body that carries [dm-only] yields a redirect, under the flagged parse or the plain one. Nothing reaches X.

Follow-ups (not blocking)
4. The owner-mention path stays on the plain verdict (owner_mention_result_refused_by_room, result.py:259). Concretely, on an owner-mention task, a body like [dm-only]\n[channel: !r]\n[no-send]\nx reads as not suppression-only. So the room lease closes and the owner gets a DM containing [no-send]\nx, neutralized, for a result the agent meant to skip. That is extra visibility, not a leak and not an unrecorded close. The PR body's reason holds: the flag would move [channel:]+skip from the owner DM to a withheld review. It is a noise gap worth its own PR.
5. dedup_recovery.classify_holder_delivery (src/dedup_recovery.py:92) is still plain. In the gateway it mostly does not matter. A holder closed no_send has an outbox payload with the canonical lease-close body ([no-send]/[REPLIED], :4905), and that body classifies correctly. The raw body is read only before the outbox has a payload. In that window [dm-only]\n[channel: X]\n[no-send] reads as pending, or as failed when the file is gone, instead of missing. It converges once the holder closes. This is unchanged from main.
6. Done here: #5340's follow-ups 1 (a test at _quarantine_unsent) and 2 (table rows that combine [channel:] with another leading marker and a skip).

Gates: all CI checks pass, with only PowerShell/publish skipped. license/cla is success (1 context).

Not verified: the live post-restart round trip. The PR body names it as owed. It is named here, not blocking.

— Sutando (susans-MacBook-Pro)

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 55e79e3009f7eae39606f91ae2fb49d8f1b95ad8.

Changes requested: the exact-head live-path witness is still missing. I found no code blocker in the implementation. This PR changes the gateway delivery/journal loop, so REVIEW.md lesson 15 requires a real post-restart round trip before approval. The PR body currently marks that witness as Owed, and the thread contains no witness for this head. The cited #5340/main run is a different head/path and does not exercise this PR's team-tier suppression journal.

Evidence I verified:

  • The new production-wrapper regression fails 4 checks on parent 0eff80fe2 and passes at this head. Removing the delivery flag specifically from _quarantine_unsent makes its three case-f assertions fail, so the previously uncovered M5 path is now caught.
  • The journal and all four delivery decisions use DELIVERY_SKIP_AFTER_CHANNEL; the source and vendored guards are byte-identical. Independent team/guest probes also confirmed a suppression record is written before the canonical no_send close, while a journal-write failure stays visible rather than silently closing.
  • Seventeen focused marker/guard/gateway suites passed, including orphan reconciliation, owner mention, fail-closed handling, and vendored drift. A separate 15-case tier/marker matrix produced the expected journal counts. review-checks and git diff --check passed.
  • Current CI and license/cla are green. No hardcoded host path was added.

The worst case is a guarded reply being silently retired without any owner record. The code now mitigates that through journal-before-close ordering plus a visible-notice fallback, but a harness cannot prove the restarted service actually executes that path.

Please post a witness from a gateway restarted at this exact head: submit a real team-tier task whose result is [dm-only]\n[channel: <room>]\n[no-send]\n…, then show (1) nothing was posted, (2) the lease closed no_send, and (3) exactly one state/suppressed-team-results/wr_*.json contains that task/body. Include the task header that proves the tier, and confirm the normal service was restored. If this needs an owner-scheduled window, name the service, expected downtime, and whether inbound work is replayed.

Once that witness is posted and the head, CI, and CLA remain current, this is ready for re-review.

@sonichi

sonichi commented Oct 12, 2026

Copy link
Copy Markdown
Owner Author

Live witness at 55e79e3

A freshly started gateway process ran the real src/remote-gateway-bridge.py, with the head compared against its parent. Each commit had its own detached worktree and its own scratch workspace inside that worktree, selected by a temporary sutando.config.local.json and removed afterwards. The gateway got a broker double on 127.0.0.1 that served one Team-tier task. The live gateway, the live workspace and every real room were untouched.

  • head 55e79e3009f7eae39606f91ae2fb49d8f1b95ad8
  • parent 0eff80fe2966d2a2526d7c2a6a59354c9c083a58 (git merge-base origin/main 55e79e300)
  • harness: run-gateway-witness-5346.sh <worktree> <head|parent>. The gateway ran under env -i with only PATH HOME USER LOGNAME TMPDIR LANG TERM CLAUDE_CONFIG_DIR, a dummy REMOTE_TASK_TOKEN, REMOTE_TASK_URL=http://127.0.0.1:<port>, REMOTE_TASK_PROVIDER=remote, REMOTE_TASK_POLL_WAIT=2 and REMOTE_OUTBOUND_SCAN_S=1.
  • Before serving the task, the harness checked that resolve_workspace() returned the scratch workspace and that the gateway's own tasks= line pointed at it: tasks=<wt>/workspace-witness-5346/tasks.
  • task file as written by the gateway: access_tier: team
  • result published by the harness, the way the core writes one (synthetic test text):
[dm-only]
[channel: <other-room>]
[no-send]
witness: synthetic team reply that must never be posted

Head (55e79e3)

The task was served at 03:48:18Z, and the lease closed in the same second. The run took 7 s from gateway start to the end.

broker double, every POST /v1/results:
  1 {"mode": "accept", "id": "task-w5346-…", "body": "[no-send]", "no_send": true}
gateway:
  [remote-gateway-bridge] queued task-w5346-…
  [remote-gateway-bridge] result task-w5346-… accepted by gateway; Matrix delivery unconfirmed (…)
  [remote-gateway-bridge] archived task-w5346-… (marker no-send, lease closed, not sent)
state/suppressed-team-results/:
  issued
  wr_b211aad2a1705305.json
    "status": "suppressed_silent_close",
    "suppressed_body": "[dm-only]\n[channel: <other-room>]\n[no-send]\nwitness: synthetic team reply that must never be posted",
    "context": {"source": "remote-gateway", "channel_id": "<team-room>", "user_id": "<teammate>", …},
    "task_id": "task-w5346-…", "record_id": "wr_b211aad2a1705305", "schema_version": 1

Parent (0eff80f)

Everything else is identical: the same single {"body": "[no-send]", "no_send": true} POST, and the same archived … (marker no-send, lease closed, not sent) line.

state/suppressed-team-results/:
  (directory absent)

What this shows

  • At both commits the guarded close is honoured. The double received exactly one POST, the no_send lease close, and no room message or result body was ever sent. The task archived as marker no-send, lease closed, not sent.
  • Only the head writes the suppression journal record for the Team task, and the record holds the exact body. At the parent the same close happens silently with no record. This is the accountability gap the PR fixes.

Gaps

  • The broker is a local double, not the real AG2 Space backend. A real Team-tier sender isn't available on demand, and the double lets the run serve exactly one attested-shape Team task without touching live rooms. Everything after the broker reply is the real gateway code: the task write, guard, marker parse, drain, close and journal.
  • Each run starts a fresh gateway process. It isn't a restart of the long-running launchd service.

@sonichi
sonichi requested a review from keweichen October 12, 2026 03:49

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed the new witness at exact head 55e79e3009f7eae39606f91ae2fb49d8f1b95ad8.

This is useful process-level integration evidence: the real gateway entrypoint consumed a broker-shaped task in an isolated workspace, and the real result-file → guard/journal → drain/outbox path showed the intended parent/head delta. At the head the guarded body was recorded before the canonical no_send close; at the parent it was not. I also reconfirmed that current main fails the production-wrapper regression in the expected four places, the head and a clean current-main synthetic merge pass the focused suites, and CI/CLA remain green. I found no new code blocker.

It does not close the existing live-path blocker, however:

  • The local broker double fabricated the Team-tier input; this was not a real broker-attested Team task.
  • The harness wrote the synthetic result, so the normal task → core execution → result-producing leg was bypassed.
  • A fresh side process used a scratch workspace while the live gateway remained untouched. The shipped service role was neither stopped/replaced at this head nor restored afterward.
  • The localhost double receiving one no_send POST does not exercise the real backend/Matrix boundary. The gateway's own success log explicitly says “Matrix delivery unconfirmed”, so this run cannot establish that nothing was posted by the real backend.

That is still harness proof, which REVIEW.md lesson 15 and the CONTRIBUTING live-path requirement explicitly distinguish from a real post-restart round trip.

The remaining witness is the one already requested: replace/restart the actual gateway service role at this exact head against the live workspace and real backend; send a real broker-attested Team task through the normal core/result path; show no room post, exactly one upstream no_send close, and exactly one matching suppression record; then show the normal service was restored. If that requires coordination, name the service window, expected downtime, and whether inbound work replays.

Verdict: not merge-ready; the existing exact-head CHANGES_REQUESTED remains in force. I am recording this as a comment review rather than duplicating the same decisive review on the same SHA.

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-reviewed at 7793618 (merge of main into 55e79e3). No code blockers; not ready to approve yet.

  • Head delta is main only: the +/- lines of git diff <merge-base> HEAD are identical to git diff 0eff80fe2 55e79e300. Main's 18 commits touch remote_gateway_bridge.py (+484/-79, e.g. #5285) but add no new marker decision; the flagged sites are still :1192, :4111, :4925, :5171, :5529, and the plain ones :934, :4126, :5133 decide no skip or redirect.
  • Tests under env -i at this head: 16 Python suites pass (gateway-dm-only-channel-skip-journal, gateway-channel-skip-owner, team-result-guard, team-guard-suppress-journaled-silent-close, ag2-sparrow-drift, gateway-orphan-result-reconciler, gateway-owner-mention, result-markers*, and others).
  • Mutation controls on the merged head: the constant set to False, the flag dropped at the journal (:1193), and the flag dropped at _quarantine_unsent (:4925) each make the new test fail. Restoring main's three source files also makes it fail. The test asserts on journal records it reads back, not on source text.
  • Vendored copies are byte-identical (result.py vs team_result_guard.py, result_markers.py). review-checks PASS; git diff --check clean; no hardcoded path in added lines.
  • license/cla: status endpoint success, 1 context. CI: 26 pass, 8 still pending, 0 failing at the time of writing.

What is missing:

  1. No APPROVED review exists at any head. The four reviews are COMMENTED x3 and CHANGES_REQUESTED x1, all at 55e79e3.
  2. keweichen's CHANGES_REQUESTED (5482087033) is unresolved. It asks for a live witness from the real gateway service, restarted at the exact head. The only witness is the harness run at 55e79e3 (issue comment 6117396818), and 5486475872 says that run does not satisfy the request. Nothing newer was posted, and the head has since moved. Under our policy a missing live round trip alone would not block. But his CR stays in force until he dismisses it or a witness is posted at 7793618.
  3. Wait for the 8 pending checks.

— Sutando (susans-MacBook-Pro)

@sonichi
sonichi force-pushed the fix/5342-dm-only-channel-skip-journal branch from 7793618 to c3684ca Compare October 12, 2026 04:19
@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@sonichi

sonichi commented Oct 12, 2026 •

Copy link
Copy Markdown
Owner Author

Live post-restart witness at c3684ca (real service, real backend, real Team task)

This is the witness @keweichen asked for in his comment review at 04:11Z. The owner approved the service window.

Head: git rev-parse HEAD in the detached worktree → c3684ca92436997d7527469ba57e2c0354a7be72.

1. Swapping the real gateway service to the head (UTC times):

  • 04:25:07: deliberate-restart stamp, then launchctl bootout of the gateway service. The old process exited by 04:25:09.
  • 04:25:10: the worktree's gateway-bridge-wrapper.sh started under env -i with only the plist's environment, against the live workspace (the worktree's workspace symlinked to it). The log shows acquired workspace poller lock. Polling gap ≈ 3 s.

2. A real broker-attested Team task. A reviewer's collaborator agent (Team tier) posted one message mentioning this agent in the Sutando Triage room. The real backend delivered it to the gateway:

04:31:34Z [remote-gateway-bridge] queued task-a092cc999635559953
task headers: source: ag2space · access_tier: team · collaborator: true · room_name: Sutando Triage

No sensitive_data_filter: false stamp, so the guard is active.

3. A normal result through the normal path. The worker that holds that room wrote this result file atomically. The body was written for this witness:

[dm-only]
[channel: <owner DM>]
[no-send]
#5346 witness: guarded team result, must not be posted.

The gateway, running the head:

04:37:02Z result task-a092cc999635559953: attributed to worker <worker>
04:37:02Z result task-a092cc999635559953 accepted by gateway; Matrix delivery unconfirmed (provider=AG2SpaceResultProvider, ...)
04:37:02Z archived task-a092cc999635559953 (marker no-send, lease closed, not sent)

4. Outcomes:

  • Upstream: exactly one close. Outbox item task-a092cc999635559953.49dc2a0a28e4affb, status DELIVERED, payload {"id": "task-a092cc999635559953", "body": "[no-send]", "no_send": true, ...}. "Matrix delivery unconfirmed" is the standard line for an accepted result; the payload shows it was a no_send close, not a message.
  • Rooms: no post. The last 25 messages of the Triage room and of the owner DM contain the witness text 0 times.
  • Journal: exactly one new record. state/suppressed-team-results/ went from 2712 to 2714 files during the window. The other new record (wr_f624a73b0d77d80d, 04:32:33) is an unrelated real Team-tier [no-send] close of a different task in the same room, handled by the head's gateway. wr_49dc2a0a28e4affb.json holds "status": "suppressed_silent_close", "task_id": "task-a092cc999635559953", and "suppressed_body" equal to the exact guarded body above.

5. Restored (times from the gateway log):

  • 04:37:32: the witness wrapper got TERM, and the head's gateway logged received SIGTERM — exiting.
  • About 04:37:33: deliberate-restart stamp, then launchctl bootstrap.
  • 04:37:37: the restored service logged singleton: acquired workspace poller lock (acquired) and started from <repo>/src/remote-gateway-bridge.py in the main checkout. Its launched_via=bare matches earlier launchd starts.
  • 04:37:42: it closed the next live result.
  • launchctl print reports state = running.
  • The core's task watcher reports ready afterwards (watcher_identity.py role-present session --ready → yes).

The earlier "04:37:44" was the time my restore script observed the lock line, not when the bootstrap happened. That's why it appeared to follow the 04:37:42 close.

Gaps, stated plainly:

  • Inbound tasks arriving during the 12-minute window were handled by the head's gateway, which is the point of the witness.
  • A core-watcher readiness blip during the window traced to a standby watcher for another worker's inbox stamping the core's sentinel, which is unrelated to this PR. It's being filed separately.

@keweichen keweichen left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at exact head c3684ca92436997d7527469ba57e2c0354a7be72.

APPROVE — the prior live-path blocker is resolved. I found no code blocker. The code and required witness are ready from this review; the PR is not yet merge-ready under the repository gate because it still needs a second counting maintainer approval.

Evidence I verified:

  • Current main tip 2f2d72f3e672e2d1ec7f5e7fa5066730007a740c is both the merge-base and the head's second parent. A clean merge-tree reproduced the head tree, the topic patch ID is unchanged from 55e79e300, and there is no merge-resolution drift.
  • With only the new regression copied onto current main, the production-wrapper test fails the four missing-journal checks (a0, a1, b, and f). At this head all 18 checks pass. The marker table, team guard, fail-closed journal, owner, orphan, owner-mention, vendored-drift, and delivery-ownership suites also pass.
  • The journal and all four gateway delivery decisions read DELIVERY_SKIP_AFTER_CHANNEL. The policy and marker vendored copies are byte-identical. Journal failure remains fail-closed to a visible generic notice; owner-tier behavior is unchanged.
  • The exact-head live witness exercised the actual gateway service role against the live workspace/backend with a real broker-attested Team task and normal result path. Its unique delivered outbox item carries the canonical {"body":"[no-send]","no_send":true} close, and exactly one matching suppression record carries the task ID and guarded body.
  • I independently read the live room timeline: the publication-safe source ping appears once (positive control), while the guarded witness body appears zero times. A bounded owner-DM read also found zero copies. The post-bootstrap launchctl running state, main-checkout process path, and fresh workspace-poller lock independently establish restoration.
  • git diff --check and review-checks pass; a synthetic /Users/... positive control makes the scanner fail as expected. Exact-head CI is complete (35 success, 3 expected conditional skips), diff coverage is 100%, and license/cla is successful.

Worst case: the added durable journal write can add latency or a generic-notice fallback when storage is unhealthy. It cannot silently retire the guarded body without a record, expose the suppressed body, change owner delivery, or introduce a migration/config/startup change.

Non-blocking evidence correction: the witness's final “next task” timestamp (04:37:42Z) precedes its stated bootstrap (04:37:44Z), so that line should be corrected or identified as clock skew. It is not load-bearing because the running job, normal-checkout process, and acquired live-workspace lock prove restoration directly.

This formal approval replaces my stale CHANGES_REQUESTED from 55e79e300.

@liususan091219 liususan091219 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to approve.

Re-checked at c3684ca. Same tree as 7793618, which I reviewed in 5486506796: git rev-parse <head>^{tree} gives 1e1e6f920aba for both, and both have the same parents (55e79e3 + main 2f2d72f). The only difference is the merge commit's message trailer, so my code findings carry over: 16 suites pass under env -i; three mutations (constant False, flag dropped at the journal, flag dropped at _quarantine_unsent) are each caught by the new test; vendored copies byte-identical; no hardcoded paths.

What blocked "ready" last time is now resolved:

  • keweichen APPROVED at this exact head (5486665572), replacing his CHANGES_REQUESTED 5482087033.
  • Live post-restart witness on the real gateway service at this head: comment 6117847193. Real service swap, real broker-attested Team task, and the guarded result journalled and not posted.
  • CI 36 pass, 3 skipped, none failing; license/cla success (1 context).

— Sutando (susans-MacBook-Pro)

@john-the-dev john-the-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve at c3684ca9. Rui's Codex core verified this exact head.

The fix holds. Every guarded suppression path in the drain now writes a suppression-journal entry before its silent close. Covered: the skip / no-send / REPLIED close, the dedup honour / requeue / report branches, orphan recovery, and _quarantine_unsent. All of them go through _guarded_result_body, and their journal classification uses DELIVERY_SKIP_AFTER_CHANNEL. Codex found no missing call.

Evidence (Codex):

  • Removing only the bridge's journal call makes the new regression fail 5 cases (a0, a1, b, c, f).
  • Seven suites pass: gateway-dm-only-channel-skip-journal, result-markers-thread, team-guard-suppress-journaled-silent-close, gateway-task-result-thread-ask, gateway-orphan-result-reconciler (55), gateway-terminal-result-moved-once, team-result-guard.
  • Canonical src/policy/egress/result.py and the vendored team_result_guard.py are byte-identical. src/remote-gateway-bridge.py is a loader for the canonical sparrow bridge, which the production-loader harness exercised.
  • Exact-head CI: 35 pass, 3 expected skips, CLA green. I also read the author's post-restart witness.

My earlier criteria were wrong. I had asked Codex to check that the journal is "metadata-only" and "non-blocking". Neither matches the policy this PR builds on, and both behaviours are inherited, not introduced here:

  • Full body is stored. result.py:688 deliberately keeps the full suppressed body in a mode-0600 record, for owner accountability. That is private local retention, not disclosure.
  • The write is fail-closed. The record write holds _ledger_lock across filesystem I/O and fsync, but not across network calls, so a stalled disk write pauses the drain. A writer error is caught, and a generic visible fallback replaces the silent close without exposing the payload.

If anyone wants metadata-only or best-effort journaling instead, that is a separate policy change, not a defect in this PR.

Scope note: "every silent archive" is broader than what's journaled. Owner-tier results, authenticated redelivery controls, owner-mention DM routing, duplicate retirement and terminal quarantine each have their own handling.

Not covered: detect-secrets was absent locally, so the scanner used its repo-rule fallback. Codex didn't independently restart a live service.

@qingyun-wu qingyun-wu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at c3684ca. Approving.

Claim: every silent close of a guarded result is journaled. The journal is one call in _guarded_result_body (remote_gateway_bridge.py:1192). Every task-result delivery decision runs that function on the raw body: _post_ready_results (:5163/5171, including the dedup honour and requeue branches), the orphan sweep (:5523/5529), and _quarantine_unsent (:4921/4925).

The other silent closes are either already recorded or outside the guard:

  • Withheld and leak verdicts carry a review record. journal_suppressed_result returns early on non-DELIVER, so nothing is journaled twice.
  • Owner-mention closes are excluded on purpose and named as a follow-up.
  • _proactive_route handles the core's own files.
  • Owner tier is unguarded by design.

Raw body vs guarded body. The journal reads the raw body and the drain reads the guarded one, but the two can't disagree:

  • parse_markers returns a single [skip] action whenever it finds a skip (result_markers.py:273, :349), so the two predicates are the same.
  • A DELIVER verdict returns the body unchanged, so the raw and guarded bodies are identical whenever the journal can fire.

Journal storage. Files are 0600 in a 0700 dir and keyed on the body digest, so a retry writes nothing new. A storage error fails closed to the generic notice and never crashes the drain.

Ran (fresh HOME, env -i):

  • The new test at main 2f2d72f fails 4 checks ("got []") and passes at head. result-markers-thread at main, with the head's test, raises TypeError ... skip_after_channel.
  • 20 suites at main and at head: identical except the new test. src/remote-gateway-bridge.test.py gives 321 ok at both, and the env-fallback failure did not reproduce here.
  • Mutations, each caught by at least one test:
Mutant Caught by
Journal flag set to False new test
_proactive_route flag off gateway-channel-skip-owner
_quarantine_unsent flag off new test
_post_ready_results flag off new test and #5340's test
Orphan sweep flag off gateway-channel-skip-owner
is_suppression_only ignores the flag, in src/ and vendored together new test and result-markers-thread
The same change in src/ only ag2-sparrow-drift and result-markers-thread
The shared constant set to False both gateway tests
Journal call removed new test
  • The vendored guard is byte-identical, and sync_from_src --check passes. review-checks.sh --diff passes and git diff --check is clean. No delivery machinery is added outside the outbox.
  • CI: 35 passed and 3 conditional skips. CLA green.

Nits (non-blocking)

  1. The new test has no orphan-sweep case for a team [dm-only]+[channel:]+skip that asserts the journal record. It is covered only indirectly.
  2. classify_result_for_tier (result.py:711) still uses the plain is_suppression_only, while the journal uses the delivery flag. So a team [dm-only]+[channel:]+skip body containing a secret goes to private owner review, not a journaled silent close. That fails safe, but a one-line note on the split would help.
  3. The new test's docstring and comments cite #5326 and #5340. CLAUDE.md keeps PR references out of comments.
  4. For this body shape, an unwritable journal now posts the generic notice where it used to close silently. This is the intended fail-closed behaviour.

Not verified: I did not run a live witness; I relied on keweichen's approval and comment 6117847193. The TS marker suites didn't exit locally (12 passes before I stopped them), but they are green in CI.

@sonichi
sonichi merged commit 3275662 into main Oct 12, 2026
39 checks passed
@sonichi
sonichi deleted the fix/5342-dm-only-channel-skip-journal branch October 12, 2026 06:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

team-tier [dm-only]+[channel:]+skip result closes silently with no suppression record (#5340 follow-up)

5 participants