Skip to content

fix(gateway): a skip after [channel:] on an owner result is a skip - #5340

Merged
qingyun-wu merged 2 commits into
mainfrom
fix/5331-channel-skip-owner
Oct 11, 2026
Merged

qingyun-wu merged 2 commits into
mainfrom
fix/5331-channel-skip-owner

Conversation

@sonichi

@sonichi sonichi commented Oct 11, 2026 •

Copy link
Copy Markdown
Owner

Closes #5331

What changed, and why

An owner task result led by [channel: X] then [no-send] / [REPLIED] / [deduped:] was posted into room X as the literal marker text:

  • parse_markers keeps a skip after [channel:] as body text, on purpose: the team guard reads that to withhold a non-owner's redirect-plus-skip for owner review (team-guard-suppress-journaled-silent-close case e).
  • The gateway then put [channel: X] back in front, so the broker's prefix-only skip check (ag2space-backend services/shared/queue/results.py _SKIP_MARKERS) missed it too.

Fix, through the shared parser with no new parser copy:

  • parse_markers(text, skip_after_channel=False): the new flag makes a skip directly after [channel:] a skip. Default behaviour is byte-identical. The vendored packages/ag2-sparrow copy is synced (ag2-sparrow-drift passes).
  • The gateway is the one owner of the skip decision. It passes skip_after_channel=True on the guarded body at each delivery decision: the task drain (_post_ready_results), the orphan sweep, the undelivered-review path, and _proactive_route. Owner task: nothing is posted, the lease closes no_send with the canonical [no-send]/[REPLIED], [deduped:] goes through the dedup plan, and the reason is logged (archived <tid> (marker no-send, lease closed, not sent)).
  • The guard still calls the plain parser, so it still sees the redirect. A non-owner [channel:]+[no-send] is withheld for private owner review exactly as before. Test c below pins the outcome that main produces.
  • Voice parity: bodyIsSkipMarked in src/skip_marker_ownership.ts now mirrors the delivery verdict. Before this change a voice task's [channel: !r:s]\n[no-send]\n… result was spoken (see the before output).
  • The shared case table tests/fixtures/thread-ask-cases.json gains delivery_skip (default skip), checked by the Python parsers, the vendored copy and the TS helper.

Look first at: src/result_markers.py (the flag, about 3 lines), the four parse_markers(..., skip_after_channel=True) sites in packages/ag2-sparrow/ag2_sparrow/remote_gateway_bridge.py, and src/skip_marker_ownership.ts.

Not changed (possible follow-up):

tests/sparrow-result-guard.test.py source-order check: the needle is now parse_markers(body rather than parse_markers(body). The intent is the same: the guard still runs before the parser.

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

Before, at parent 9c60c80ae with this PR's tests copied in:

$ env -i HOME=$HOME PATH=/usr/bin:/bin:/opt/homebrew/bin python3 tests/gateway-channel-skip-owner.test.py
  FAIL a) [no-send]: one lease-close POST, no_send, canonical body; got [{'id': 'cs-a0', 'body': '[channel: !other:example.org]\n[no-send]\n
  FAIL a) [no-send]: neither the text nor the redirect reaches the wire; got [{"id": "cs-a0", "body": "[channel: !other:example.org]\n[no-se
  FAIL a) [no-send]: reason logged; got ['result cs-a0 accepted by gateway; Matrix delivery unconfirmed (provider=AG2SpaceResultProvider, ba
  FAIL a) [REPLIED]: one lease-close POST, no_send, canonical body; got [{'id': 'cs-a1', 'body': '[channel: !other:example.org]\n[REPLIED]\n
  FAIL a) [REPLIED]: neither the text nor the redirect reaches the wire; got [{"id": "cs-a1", "body": "[channel: !other:example.org]\n[REPLI
  FAIL a) [REPLIED]: reason logged; got ['result cs-a1 accepted by gateway; Matrix delivery unconfirmed (provider=AG2SpaceResultProvider, ba
  FAIL b) [deduped:]: never posted as text; got [{'id': 'cs-b', 'body': '[channel: !other:example.org]\n[deduped: cs-holder]\nvisible'}]
  FAIL b) [deduped:]: routed through the dedup plan; got ['result cs-b accepted by gateway; Matrix delivery unconfirmed (provider=AG2SpaceRe
  FAIL d) orphan sweep: one lease-close POST, no_send, canonical body; got [{'id': 'task-cs-d', 'body': '[channel: !other:example.org]\n(rec
  FAIL d) orphan sweep: neither the text nor the redirect reaches the wire; got [{"id": "task-cs-d", "body": "[channel: !other:example.org]\
  FAIL e) proactive: nothing posted; got [{'op': 'message', 'room_id': '!taskroom:example.org', 'body': '[no-send]\nvisible'}]
  FAIL e) proactive: _proactive_route drops it
FAIL: 12 failure(s)
$ npx tsx --test tests/task-bridge-thread-ask-strip.test.ts      # at 9c60c80ae
✖ result watcher: [thread] is stripped for speech; a skip right after it is a skip (12007.913333ms)
✖ bodyIsSkipMarked agrees with the delivery verdict of parse_markers on the leading block (26.019125ms)
✖ the shared table tests/fixtures/thread-ask-cases.json (also run by result-markers-thread) (1.153291ms)
ℹ pass 5
ℹ fail 3
✖ failing tests:

After, at head 587b9e453:

$ env -i HOME=$HOME PATH=/usr/bin:/bin:/opt/homebrew/bin python3 tests/gateway-channel-skip-owner.test.py
  ok   a) [no-send]: one lease-close POST, no_send, canonical body; got [{'id': 'cs-a0', 'body': '[no-send]', 'no_send': True}]
  ok   a) [no-send]: neither the text nor the redirect reaches the wire; got [{"id": "cs-a0", "body": "[no-send]", "no_send": true}]
  ok   a) [no-send]: archived, out of flight
  ok   a) [no-send]: reason logged; got ['result cs-a0 accepted by gateway; Matrix delivery unconfirmed (provider=AG2SpaceResultProvider, ba
  ok   a) [REPLIED]: one lease-close POST, no_send, canonical body; got [{'id': 'cs-a1', 'body': '[REPLIED]', 'no_send': True}]
  ok   a) [REPLIED]: neither the text nor the redirect reaches the wire; got [{"id": "cs-a1", "body": "[REPLIED]", "no_send": true}]
  ok   a) [REPLIED]: archived, out of flight
  ok   a) [REPLIED]: reason logged; got ['result cs-a1 accepted by gateway; Matrix delivery unconfirmed (provider=AG2SpaceResultProvider, ba
  ok   b) [deduped:]: never posted as text; got []
  ok   b) [deduped:]: routed through the dedup plan; got ['dedup requeue for cs-b (holder cs-holder is not a valid delivery for this task)']
  ok   c) team: the lease is closed no_send, as on main; got [{'id': 'cs-c', 'body': '[no-send]', 'no_send': True}]
  ok   c) team: neither the text nor the redirect reaches the wire; got [{'id': 'cs-c', 'body': '[no-send]', 'no_send': True}]
  ok   c) team: the withhold is logged; got ['withheld non-owner result for cs-c: result delivery control marker; pending private owner revi
  ok   d) orphan sweep: one lease-close POST, no_send, canonical body; got [{'id': 'task-cs-d', 'body': '[no-send]', 'no_send': True}]
  ok   d) orphan sweep: neither the text nor the redirect reaches the wire; got [{"id": "task-cs-d", "body": "[no-send]", "no_send": true}]
  ok   e) proactive: nothing posted; got []
  ok   e) proactive: _proactive_route drops it
  ok   f) redirect re-stitched as today; got {'id': 'cs-f', 'body': '[channel: !other:example.org]\nmoved'}

PASS: 0 failure(s)

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

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/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
  • 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.
  • Broader sweep: 128 marker, guard, gateway, proactive, sparrow, dedup and claude-md suites. 126 pass. Of the other two, sparrow-result-guard was fixed above. remote-gateway-bridge.test.py env-fallback: no env token and no file yields empty token fails identically on parent 9c60c80ae on this host, so it is unrelated.
  • bash scripts/review-checks.sh --diff <(git diff origin/main...HEAD): PASS. git diff --check: clean. eslint on the changed TS files: clean.

Live witness

Owed. This changes the gateway delivery loop. The evidence above comes from the production wrapper in-process, not from a live post-restart round trip. A live witness should run once at the head the reviewers approve: an owner task answered with [channel: <room>]\n[no-send]\n…, showing nothing posted in that room and the lease closed, not sent log line.

Docs

docs/claude-md-moved-detail.md (Result-marker semantics, [thread:] entry): the sentence about a skip after [channel:] now states the gateway and voice verdict and the guard's. The catalog last_verified date is already 2026-10-10.

Hardcoded paths

None added. review-checks passed.

🤖 Generated with Claude Code

An owner task result led by `[channel: X]` then `[no-send]`, `[REPLIED]` or
`[deduped:]` was posted into X as literal text: parse_markers keeps a skip
after [channel:] as body (so the team guard can withhold a non-owner's
redirect-plus-skip), the gateway re-stitched `[channel: X]` in front, and the
broker's prefix-only skip check missed it.

parse_markers gains skip_after_channel (default False). The gateway passes it
on the guarded body at every delivery decision (task drain, orphan sweep,
undelivered review, proactive route), so it alone decides the skip on the full
leading block: nothing is posted, the lease closes no_send, the reason is
logged. The guard still calls the plain parser, so a non-owner's
redirect-plus-skip is withheld for review exactly as before. Voice
(bodyIsSkipMarked) follows the same delivery verdict. Vendored copy synced;
the shared case table gains delivery_skip.

Closes #5331

Stand: Echo Act IV Pro
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonichi

sonichi commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

@qingyun-wu review requested at head 587b9e4. This is the #5331 follow-up your agent found while reviewing #5326: an owner task's [channel: X]+[no-send] result no longer posts the marker text into X. The non-owner withhold path is unchanged.

@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/result_markers.py (100%)

Summary

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

@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@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.

Approve at 982b9657 (on Qingyun's ask to review and approve).

Why it's right: one flag on the one parser, default byte-identical. The guard keeps calling the plain parser, so a non-owner [channel:]+skip is still withheld for owner review (test c pins main's behaviour); the four delivery decisions (_post_ready_results, the orphan sweep, _quarantine_unsent, _proactive_route) pass skip_after_channel=True on the already-guarded body, so an owner's [channel: X]\n[no-send] closes the lease no_send instead of posting the marker text into room X; [deduped:] after [channel:] goes through the dedup plan. skip_marker_ownership.ts now mirrors that verdict for voice, and the shared fixture gains delivery_skip so the TS copy can't drift from parse_markers. This closes the nit I raised on #5326 (#5331).

Ran at this head (env -i, fresh worktree): gateway-channel-skip-owner (PASS, 0 failures), result-markers-thread, result-markers, team-result-guard, team-guard-suppress-journaled-silent-close, ag2-sparrow-drift, sparrow-result-guard, dm-result-skip-markers, bridge-marker-no-leak: all pass. CI: 36/36 green.

Still owed, not a reason to hold the approval: the before/after is the in-process wrapper harness (fake _req), not a live post-restart round trip. Since this changes the gateway's delivery decisions, a real [channel: X]\n[no-send] owner result through a restarted gateway, read back as "nothing posted, lease closed no_send", is the lesson-15 witness; same class as #5326's. Fine to post it on this PR after the fact.

Nit: docs/claude-md-moved-detail.md still describes the skip rule in one sentence; worth adding "after [channel:] the gateway treats it as a skip on the guarded body" so the owner-facing text matches.

@qingyun-wu
qingyun-wu merged commit 0eff80f into main Oct 11, 2026
37 checks passed
@qingyun-wu
qingyun-wu deleted the fix/5331-channel-skip-owner branch October 11, 2026 02:41
@sonichi
sonichi requested a review from john-the-dev October 11, 2026 03:33

@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.

Post-merge review at 982b96577. It was merged at 02:40:57Z on one approval, and the PR body still lists the live witness as owed.

Verified:

  • Marker grammar stays in parse_markers, and the src and vendored copies are byte-identical. bridge-marker-no-leak passes.
  • The new test fails 12 checks on 487d7af9 and passes at the head.
  • Related suites are green: result-markers, team-result-guard, sparrow-result-guard, gateway thread-ask, outbox and the TS thread-ask test.
  • Edge cases: Python and TS skip verdicts agree on 11 of them, including blank lines, both marker orders, [thread:] between, mixed case, a marker mid-body, and an inline mention.
  • Mutants: M1–M4 and M6 are caught. Removing the flag in _quarantine_unsent (:4611) is not.

Finding: "non-owner unchanged" does not hold with [dm-only].

  • On a team task, [dm-only]\n[channel: X]\n[no-send]\n… has no redirect action under the plain parse, so the guard delivers it unchanged.
  • The drain (:4840, with the flag) then closes it no_send. But the suppression journal (:1180) keys on plain suppression_only, so it writes nothing.
  • In the PR's own harness, a plain team [no-send] writes suppressed-team-results/wr_*.json; this body writes none. [deduped:] behaves the same.
  • A guarded result now closes with no owner record. Filed as #5342, with a suggested fix and the missing case-table rows.

Please post the live witness.

@sonichi

sonichi commented Oct 11, 2026

Copy link
Copy Markdown
Owner Author

Post-merge live witness on main 0eff80fe2 (2026-10-10 9:02 PM PDT, gateway restarted from the pulled root checkout on the owner's word): an owner task in the owner's DM was answered with [channel: <that room>] + [no-send] + a test line. Gateway log: archived task-… (marker no-send, lease closed, not sent), then seen->retired 585ms; the room read back shows nothing posted after the owner's message. Before this PR that body posted the literal [no-send] text.

Stand: Echo Act IV Pro

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.

Owner-task result '[channel: X]' + '[no-send]' posts the literal [no-send] text to X

3 participants