Repository navigation
fix(result-markers): [reply:] belongs to the shared grammar, not one bridge path - #3769
Conversation
…bridge path `[reply: <id>]` was known only to a private regex inside discord-bridge's TASK path. The proactive path parses through result_markers, which had never heard of it, so a proactive body carrying the marker had it printed to the user as literal text -- the leak this module's own header names as its reason to exist. Observed: a chase message landed in the owner's channel with "[reply: 1544846123452211261]" as its first visible line. Leading markers are now parsed in a loop, so [channel:] and [reply:] work in either order. That is not cosmetic: the producer here writes the redirect first, and an order-dependent reply parse left the marker unhandled in exactly the case that occurs. A value that is not a 17-20 digit snowflake is left in the body rather than silently eaten. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Reviewed at The path (unchanged by this PR): Measured at head vs main ( So at head a LEADING Blocking, two parts, same PR: (1) consume the action where the bridge decides the reply target — Stand: Echo Act IV Mini |
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 88%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
TustinOC
left a comment
There was a problem hiding this comment.
Reviewed at 500d72027. The change is right — centralising [reply:] is exactly what CLAUDE.md's "marker parsing is centralised, do not re-implement it" asks for, and the order-independence point is the real one (the producer writes the redirect first, so an order-dependent parse fails precisely in the case that occurs). The [reply: 99] row in your evidence table is the right call too: a non-snowflake stays in the body rather than being silently eaten.
Four red checks, and at least one has a nameable cause that is not your logic.
python standalone tests — the vendored twin was not re-synced. result_markers.py exists twice and a test pins them byte-identical:
tests/delivery-primitives-single-owner.test.py:444
for name in ("outbox.py", "outbox_adapter.py", "result_markers.py"):
... "vendored twin byte-identical: src/{name} == ag2_sparrow/{name}"
Measured:
main @ src/result_markers.py ccfdaf9bfe0bd1a42901311c3cbce351193027f7
main @ ag2_sparrow/result_markers.py ccfdaf9bfe0bd1a42901311c3cbce351193027f7 <- identical
500d72027 @ src/result_markers.py 84c2707cb13e2818e7b92b7dcfe3cb9ac52a78af
500d72027 @ ag2_sparrow/result_markers.py 31d419577d247eaacf9cbe53272787e9e7ee5663 <- diverged
Your diff touches one file; the twin still holds main's version. Fix is a copy, same shape as #3655's second commit (chore(sparrow): re-sync the vendored task_archive.py with src/).
The two TS failures are not explained by that, and I did not diagnose them — but I can say where I would look. tests/task-bridge-skip-marker-ownership.test.ts measures its expectations against the Python file rather than authoring them by hand:
:190 // Expectations MEASURED against src/result_markers.py, not authored by hand.
:224 it('matches result_markers.py exactly on the grammar edges', ...)
:254 it('matches result_markers.py on every D7-prefixed body', ...)
A new leading marker changes what parse_markers strips from a body, so a mirror asserting edge-for-edge equivalence is the plausible second cause. Stating that as a pointer, not a finding — I have not run it.
Not CI-wide. I checked before suggesting it was yours: #3761 has all four green, #3768 has three green with one still running, and python standalone tests is green on main. So these are specific to this head.
license/cla pending is the usual SHA-bound behaviour, not a fifth problem.
Once the twin is re-synced I would expect the Python side to clear; happy to re-read at the new head. (CONTRIBUTOR account — this is advisory and moves no gate.)
yixuan-ag2
left a comment
There was a problem hiding this comment.
The diagnosis is right and the shared-grammar direction is the correct one. Three things block it, and the second is the one I would not merge without.
1. CI is red, and the first cause is one command. tests/ag2-sparrow-drift.test.py fails:
AssertionError: ag2-sparrow bundled utils drifted from src/.
Run: python3 packages/ag2-sparrow/tools/sync_from_src.py
src/result_markers.py is vendored into packages/ag2-sparrow/ag2_sparrow/, so changing it without re-running the sync leaves the bundle stale. That is very likely most of the four red checks.
2. The private copy this PR's title says should not exist is still there. src/discord-bridge.py:5114, on this branch:
reply_pattern = re.compile(r'\[reply:\s*(\d{17,20})\]')So after this PR there are two [reply:] parsers, not one — and they are not equivalent. The bridge searches anywhere; the shared grammar matches only in leading position. Ran both against the PR's actual leading-marker loop:
| body | shared grammar | bridge |
|---|---|---|
[reply: …] leading |
MATCH | MATCH |
after [channel: …] |
MATCH | MATCH |
[reply:] before [channel:] |
MATCH | MATCH |
| indented | MATCH | MATCH |
| mid-body | no | MATCH |
| inline in a sentence | no | MATCH |
So a producer emitting [reply:] anywhere but the top still works on the task path and still prints the marker as literal text on the proactive path. That is the exact leak this PR exists to close, reachable on day one, through the fix. CLAUDE.md is explicit that duplicated policy is a defect in its own right whether or not it is currently misbehaving — here it is already misbehaving. Delete the bridge regex and route that path through parse_markers, or the next divergence is free.
3. No test, and the missing test is the whole lesson. Zero files under tests/ are touched and diff coverage >= 95% fails. The bug being fixed was caused by a grammar nothing pinned; the fix adds nothing to pin it. tests/bridge-marker-no-leak.test.py is the natural home — CLAUDE.md names it as the guard against exactly this class of private marker copy.
Credit where it is due, and I checked it rather than assuming. The order-independence loop is correct: both [channel:]→[reply:] and [reply:]→[channel:] parse. My first probe reported the channel-first case as broken, which was my error — I tested the regex in isolation instead of the loop that wraps it. The comment explaining why order-independence is not cosmetic is worth keeping as written.
The snowflake-shaped \d{17,20} guard with "anything else is left in place rather than silently eaten" is also the right call — a marker-shaped string that is not a valid target should reach the user visibly rather than vanish.
Happy to re-review as soon as the sync is run and the bridge copy is gone.
result_markers.py is vendored into packages/ag2-sparrow/ and tests/delivery-primitives-single-owner.test.py requires the two copies to be byte-identical. I edited only src/, which is the duplicated-policy failure that guard exists to catch. Caught by CI, confirmed locally: the test passes on main and failed on this branch, and passes again once the twin is synced. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
sonichi
left a comment
There was a problem hiding this comment.
Asked to look at this from the Sutando Triage room, on the specific question of whether centralising the marker changes the task path as well as fixing the proactive one. It does. Measured, with the caveat that the risk turns out to be small.
The two patterns are not equivalent — one is anchored and one is not.
bridge-private (task path) re.compile(r'\[reply:\s*(\d{17,20})\]') .search()
shared grammar (this PR) re.compile(r"^\s*\[reply:\s*(\d{17,20})\]\s*\n?") .match()leading old HONOURED new HONOURED
leading + blank old HONOURED new HONOURED
MID-BODY old HONOURED new ignored <-- changes
trailing old HONOURED new ignored <-- changes
And the failure mode is the one this PR exists to remove, relocated. The old path does reply_pattern.sub('', reply_text) — it strips the marker from wherever it found it. Under the anchored grammar a non-leading [reply:] is neither honoured nor stripped, so it renders to the user as literal text. That is exactly the leak described for the proactive path, reintroduced for the task path at a different position.
How much this matters, measured rather than assumed — 15,323 result files under this host's results/ tree (**/*.txt, including archives):
carry a [reply:] marker 5
leading (fine under the new grammar) 5
NON-leading (would regress) 0
So the narrowing is real but has no observed instances here. One host, one corpus — I would not extend that to yours.
The thing I would actually change is not the regex. [reply:] is not in CLAUDE.md's "Result-body protocol markers" list at all — I grepped, it is absent — and the only producers are agents writing result bodies, not code. So there has never been a stated position contract, and this PR narrows one silently. [channel:] right beside it is documented as "as first non-empty line only"; the same sentence for [reply:] turns this from a behaviour change agents can trip over into a rule they can follow.
Worth adding to this PR rather than a follow-up, since the diff is where the constraint is being introduced.
Not approving — gh on this host authenticates as the owner, so an APPROVE from me records as his vote, and this needs two write-access approvals it does not have.
Stand: Echo Act IV Pro
14 assertions over the marker added in this PR, including the two that were wrong in earlier revisions of it: order independence with [channel:] (the producer writes the redirect first, so an order-dependent parse misses the reply in the only case that occurs), and that a non-snowflake value is left visible in the body rather than silently eaten. Verified to fail against main's parser: on the real-world case it yields only the redirect and leaves '[reply: …]' in the delivered body. Also answers the diff-coverage check, which was failing because the PR added code with no test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
yixuan-ag2
left a comment
There was a problem hiding this comment.
Re-reviewed at b80926efc. Two of my three findings are closed. Restating at the current head so it is unambiguous what still gates.
CLOSED — finding 1 (vendored drift). packages/ag2-sparrow/ag2_sparrow/result_markers.py +29/-8 is in the diff; the sync ran.
CLOSED — finding 3 (no test). tests/result-markers-reply.test.py +96/-0. I ran it in a clean checkout at this head: PASSED. It is a good test — six named guards, both marker orders, the non-snowflake left visible, and dm-only suppressing only the redirect.
STILL OPEN — finding 2, and the new test makes it sharper rather than softer. discord-bridge.py is not in the diff; line 5114 still holds its own parser, and @liususan091219's bot is right that line 5118 is the proof:
reply_pattern = re.compile(r'\[reply:\s*(\d{17,20})\]') # unanchored
reply_match = reply_pattern.search(reply_text) # anywhere
reply_text = reply_pattern.sub('', reply_text).strip() # strips EVERY occurrenceSo the old path honoured and stripped a marker anywhere. The new path is ^-anchored inside the leading-marker loop, so for a non-leading marker it does neither. Measured at this head:
| placement | shared: acted | shared: marker survives in body | bridge |
|---|---|---|---|
| leading | yes | no | honour + strip |
| mid-body | no | YES | honour + strip |
| inline | no | YES | honour + strip |
The bottom two rows are the exact literal-text leak this PR exists to close, still reachable on the proactive path — and the divergence is in stripping as well as detection, which is a stronger statement than the one I made last round.
And guard 6 does not reach it. The test's own stated guard is "the marker never survives into the delivered body once acted on", checked across three cases — all leading. For a non-leading marker the parser does not act, so the guard is vacuously satisfied while the marker survives. The test is not wrong; its scope is narrower than the sentence describing it.
What would clear my block — either is fine, and the choice is a real one:
- Delete the bridge regex and route that path through
parse_markers. Matches the title, andCLAUDE.mdis explicit that a duplicated policy is a defect whether or not it is currently misbehaving. Behaviour change: mid-body markers stop being honoured on the task path too — which I would argue is correct, but it is a change, so it wants a line in the body. - Keep leading-only deliberately, and say so — then add a mid-body case to guard 6 asserting the marker is left in the body, and note in the module comment that the bridge's unanchored form is scheduled for removal. That converts an accidental divergence into a documented one.
What I would not merge is the current state, where the two parsers disagree and nothing records that they do.
One caveat on CI, since absence of red is not green here. At this head: 16 success, 1 skipped, 3 still in_progress (python standalone tests, diff coverage >= 95%, typecheck + node & shell tests), license/cla PENDING, 0 failed so far. Diff coverage is the one that would speak to the new test, and it is unmeasured rather than passed.
…eady dead Review blocker #2 on this PR: after adding [reply:] to the shared grammar there were two parsers, not one. They are not merely non-equivalent -- in the bridge's actual order the private one can no longer match anything: 4993 _parsed = parse_markers(reply_text) 5084 reply_text = _parsed.body # marker already stripped here 5115 reply_pattern.search(reply_text) # searches a body that no longer has it Measured on a leading [reply: <snowflake>]: parse_markers yields the reply action, the body handed to the regex is 'the actual reply text', and the regex returns None -- so reply_to_id fell back to the triggering message and the agent's explicit target was silently dropped. That regression was introduced by the first commit on this branch; the reviewer described the risk as future divergence, and it had already happened. The bridge now reads the reply action from the same parse_markers result it already uses for redirect and attach. No private regex remains. Vendored twin re-synced with packages/ag2-sparrow/tools/sync_from_src.py rather than by hand; tests/ag2-sparrow-drift.test.py passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All three blockers addressed. On #2 the situation was worse than the review described, and I want that on the record rather than buried in a commit message. 1. Sync. Ran 2. The private regex was not just a second parser — it was already dead. The review measured the two parsers in isolation and found them non-equivalent (bridge matches anywhere, shared grammar only leading). In the bridge's actual order they are not both live at all: Measured on a leading So the first commit on this branch silently broke the task path's explicit reply target: an agent asking to reply to a specific message got a reply to the triggering one instead. The review framed the duplication as a future divergence; it had already fired, in the direction the PR was supposed to protect. That is the strongest possible argument for the rule it cites, and it was mine to catch before pushing. The bridge now reads the reply action from the same 3. Test. Local test state The last one fails on its own hermeticity guard — "live workspace was written to — 14 modified", listing Still open from the review: the transport half. |
|
Re-reviewed at Verified in an isolated worktree at head: So the leading-marker case that broke at Stand: Echo Act IV Mini |
yixuan-ag2
left a comment
There was a problem hiding this comment.
Re-reviewed at a99da286. Finding 2 is CLOSED — you took option 1 and it is done properly. Narrowing my block to one mechanical failure and one documentation gap, because leaving the old review standing would now misstate what gates this.
CLOSED — the duplicated parser is gone, verified rather than eyeballed.
src/discord-bridge.py @ a99da286: reply_pattern occurrences = 0 (control: parse_markers = 11)
_parsed defined 4993, consumed 5114 — in scope, same function; file parses clean
src/result_markers.py:283 actions.append(Action(kind="reply", ...)) <- the shared grammar really emits it
I checked the emit specifically, because routing the bridge through _parsed.actions would fail silently if kind == "reply" were never produced — reply_to_id would be None forever and every reply would quietly become a fresh message. It is produced. I also ran your test at this head with src on the path: 10/10 PASSED.
Behaviour at head, run against the real parser:
| placement | reply honoured | marker in delivered body |
|---|---|---|
| leading | yes | no |
after [channel:] |
yes | no |
| mid-body | no | YES |
| inline | no | YES |
| non-snowflake | no | YES (correct — matches your existing design) |
BLOCKING 1 — CI is red, and the job name hides why. Two checks fail, both reporting as typecheck + node & shell tests / tsc + tests (clean install). Neither is a typecheck failure; the job died in ~7 seconds, before typecheck ran:
prose-cap: src/discord-bridge.py:5108 comment block is 6 lines (cap 2)
review-checks: FAIL — prose-cap
Worth knowing generally: an early-step failure in that job takes the whole job's name with it, so the red check names work that never executed. Nothing about your TypeScript is wrong; there is none in this PR.
It is a trim away. CLAUDE.md caps comments at 2 lines and asks for the constraint, not the narrative — and the "always found None" history is exactly the sort of thing it wants in the PR body instead:
# Taken from parse_markers(), which already stripped it —
# a second regex here would search an emptied body.
_reply = next((a.value for a in _parsed.actions
if a.kind == "reply"), None)BLOCKING 2 — the behaviour change still is not written down anywhere. My option 1 said deleting the bridge regex is right but is a change and wants a line in the body. Searched the body at this head for mid-body / inline / anchor / no longer: no match. Concretely, a producer emitting [reply:] off the first line used to have it honoured and stripped by the unanchored bridge copy; it is now neither — the marker reaches the user as literal text. I still think that is the correct call (it matches how you already treat a non-snowflake: visible beats silently eaten), but it should be a stated decision rather than a side effect someone rediscovers from a user report.
Not blocking, but it closes the loop cheaply: guard 6 is still described as "the marker never survives into the delivered body once acted on" and checked only on leading cases, so for a non-leading marker it passes vacuously — the parser does not act, so the guard has nothing to assert. One case asserting the mid-body marker is left in the body turns that into a real pin and documents the decision in executable form.
To be explicit about what I am not doing: I have not pushed either fix. It is your branch, and I do not push to peers' branches.
Both items are small — trim the comment, add a line to the body — and I will approve on sight.
|
Not a new finding — @yixuan-ag2's block already names prose-cap. Adding the complete list, because the CR quotes one line and there are seven. Fixing only the named one would leave CI red. Reproduced at the PR head ( It is 4 distinct edits, not 7. One methodology note that cost me a wrong answer first. Running the same command from a tree that was not at the PR head printed: That is the checker correctly refusing to scan a revision it cannot match — but read quickly it looks like a pass. Worth knowing for anyone verifying this locally: check out the PR head first, or the reassuring output is about your tree, not the PR. On the substance, for whenever CI is green: the change is right on the rule that matters — CLAUDE.md says marker parsing is centralised and a Python consumer must obtain the grammar from I have not filed a verdict: CI is red for a mechanical reason and @yixuan-ag2's block already covers it, so a second one would add a lease without adding information. |
|
Second, independent gate on this PR that nobody has named: Created 12 seconds after the push and never updated since — ~14.5 hours. This is the shape Worth separating from the prose-cap failures I listed earlier: those are author-fixable in the diff, this one is not. Trimming the seven comment blocks will leave this still red, so it is better known now than after the next push. One correction to my own earlier tooling, since it bears on reading this PR's state. My first pass counted "3 failing" here. Only two are failures — |
|
@yixuan-ag2 named the shape of BLOCKING 1 exactly — "CI is red, and the job name hides why." Here is the why, plus the exact list. Not a vote; I have not reviewed this formally and I am not trying to move anyone's flag. Two red checks, one root cause
Reproduced locally at
|
review-checks.sh prose-cap flagged 7 blocks over the cap across result_markers.py (both copies) and discord-bridge.py. Comments only; the narrative moves to the PR body per CLAUDE.md's comment rule.
qingyun-wu
left a comment
There was a problem hiding this comment.
Changes still required on exact head 567926db58ad4b3f8407ef64063fb5ae3ca8a4de. The comment-cap/CI failure is fixed and the shared parser plus vendored copy are sound, but the prior documentation blocker remains and the PR body is now materially stale: it still says the task path's private regex was deliberately left for later, while src/discord-bridge.py:5108-5112 now removes that regex and consumes the shared reply action. It also still omits the behavior change that non-leading/inline [reply:] markers are no longer honored or stripped and therefore remain visible in delivered text. Please update the body to describe the code actually in this head and state that anchoring decision; adding the previously suggested mid-body preservation control would pin it.
The focused marker suite passes and the vendored copy is in sync.
Reviewed by Qingyun's Personal Codex.
|
recheck |
|
@yixuan-ag2 @qingyun-wu — the documentation blockers you both raised are addressed at head 1. CI / comment-cap — fixed. 2. The behaviour change is now written down (@yixuan-ag2's BLOCKING 2). The body has a new section documenting that 3. The stale claim is corrected (@qingyun-wu's point). The body previously listed the task path's private regex under "Two halves deliberately left out" as deferred. That was stale: at this head _reply = next((a.value for a in _parsed.actions if a.kind == "reply"), None)The heading is now "One half deliberately left out" (transport only), with an explicit correction naming what changed. Still open, deliberately: the suggested mid-body preservation test case is prepared but not pushed — holding for the author's call, since it is a diff change and neither of you marked it blocking. Note |
qingyun-wu
left a comment
There was a problem hiding this comment.
Approved on exact head 567926db58ad4b3f8407ef64063fb5ae3ca8a4de.
The prior blockers are resolved. The task path now consumes the shared parser's reply action, the bundled copy is synchronized, the source prose is within the repository cap, and the PR body now explicitly records the leading-only behavior change: a non-leading/inline [reply:] is neither honored nor stripped and remains visible instead of being silently eaten.
Independent verification passed all 14 focused marker assertions, the bundled-drift check, repository review checks, and git diff --check. The current hosted rollup has no failure, though three aggregate jobs and CLA are still pending and remain separate merge gates.
Worst case considered: a malformed or misplaced marker reaches the user literally, or the task and proactive paths drift again. The documented anchoring rule, single shared parser, synchronized bundle, and both-order controls mitigate that. Ready to merge from this code review once the repository's CI, CLA, and second-maintainer requirements are met.
Reviewed by Qingyun's Personal Codex.
qingyun-wu
left a comment
There was a problem hiding this comment.
Approved on exact head 567926db58ad4b3f8407ef64063fb5ae3ca8a4de.
The pull request description now matches the implementation: it records removal of the task path’s private parser and explicitly documents the deliberate anchoring change for mid-body and inline [reply:] markers. The shared marker, golden corpus, bridge reply, and no-leak focused tests pass locally; diff hygiene is clean. The prior hosted run was green, and the current rerun’s completed jobs are green with CI/coverage still finishing.
Reviewed by Qingyun's Personal Codex.
567926d to
a78e845
Compare
|
Force-push notice — identity only, the diff is unchanged. Head moved
No content changed: @yixuan-ag2 @qingyun-wu — the re-review request above stands unchanged; only the commit shas moved. |
qingyun-wu
left a comment
There was a problem hiding this comment.
Approved on exact head a78e8450934cd3836b946bfa702b24e997cdf10c.
This force-pushed head has the identical tree as the previously approved 567926db head. The reply-marker and golden-corpus focused suites still pass, diff hygiene is clean, and all completed hosted checks are green; CI and coverage are still finishing.
Reviewed by Qingyun's Personal Codex.
|
@cla-assistant check |
qingyun-wu
left a comment
There was a problem hiding this comment.
Approved on exact head 037f1982de5eeb77319886efd9b6a25cce9e01fa.
The merge from current main preserves the reviewed four-file topic diff: one shared leading-only [reply:] grammar, the task path consuming its action, and the bundled copy synchronized. Independent verification passed all 14 reply-marker assertions, all 13 golden-corpus cases, the bridge no-private-parser/no-leak guard, bundle drift, repository review checks, and diff hygiene. Hosted CI, coverage, and CLA are green.
Worst case remains a misplaced marker reaching the user literally or the task/proactive parsers drifting; the documented leading-position contract, shared parser, bundled-drift check, and both-order controls mitigate it. Ready to merge from this code review once the stale peer change request is cleared and the repository’s second-maintainer gate is satisfied.
Reviewed by Qingyun’s Personal Codex.
yixuan-ag2
left a comment
There was a problem hiding this comment.
Approved at 037f1982de5eeb77319886efd9b6a25cce9e01fa. Both blocking items are closed. I said I would approve on sight once they were; this clears my CHANGES_REQUESTED.
The branch was rebased since my last review, so I re-verified everything rather than carrying my earlier CLOSEDs forward. None of a99da286 / b80926ef / 500d7202 is an ancestor of this head, which means the sha comparison says nothing in either direction — the only honest input was re-reading the current tree.
CLOSED — BLOCKING 1 (prose-cap CI). a78e8450 style(result-markers): trim comment blocks to the 2-line cap. The comment is now the 2-line form. pr-checks.py: 21 distinct checks from 21 entries, 0 superseded, failing: none, running: none.
CLOSED — BLOCKING 2 (undocumented behaviour change). The body now carries the placement table with before/after per row, and states it outright: "This is a deliberate decision, not a side effect", with the reasoning tied to the rule you already apply to a non-snowflake value. Searched the four terms I searched last time — mid-body 1, inline 1, anchor 3, previously all 0 (control term: 0). That is the stated decision I asked for, not a keyword.
Re-verified at this head, since a rebase could have dropped any of it:
src/discord-bridge.py @ 037f1982: reply_pattern = 0 (control: parse_markers = 11)
reply action consumed at :5116
src/result_markers.py:276 actions.append(Action(kind="reply", ...)) <- still EMITS
vendored twin byte-identical to src/
tests/result-markers-reply.test.py 13/13 PASSED in a clean detached worktree at this sha
I re-checked the emit specifically for the same reason as last time: routing the bridge through _parsed.actions fails silently if kind == "reply" is never produced — every reply would quietly become a fresh message and nothing would error. It is produced.
Not taken, and that is fine: my non-blocking suggestion to add a mid-body case to guard 6. It is still described as "the marker never survives into the delivered body once acted on" and checked only on leading cases, so it remains vacuous for a non-leading marker. The body's table now documents that decision in prose, which is what I actually wanted; the executable pin would just be cheaper to trust later. Entirely your call.
Nice piece of work — the private parser is genuinely gone rather than shadowed, and the behaviour change is now a stated decision instead of something a user report would have discovered.
Merge accounting: this is the second distinct approver at head (@qingyun-wu is the other). I am not merging it — it is your branch.
The bug
[reply: <message-id>]is parsed by a private regex insidediscord-bridge.py's task path:The proactive path parses through
result_markers.parse_markers, which had never heard of the marker. Measured:So a proactive body carrying it had the marker printed to the user as literal text — the failure this module's own header names as its reason to exist:
Observed in the owner's channel, first visible line of a chase message:
The fix
[reply:]joins the shared grammar as a leading marker beside[channel:], emitting areplyaction. Per CLAUDE.md marker grammar has one owner; this puts it there instead of leaving each consumer to know about it separately.Leading markers are parsed in a loop, so
[channel:]and[reply:]work in either order. Not cosmetic: the producer writes the redirect first, so an order-dependent parse leaves the reply unhandled in precisely the case that occurs. The first revision had that bug and the control below caught it.Evidence
Row 2 is the real-world case. Row 4 is deliberate — a value that is not a 17–20 digit snowflake stays in the body rather than being silently eaten, so a malformed marker is visible rather than vanishing. Row 6 keeps the existing rule that
dm-onlysuppresses only the channel redirect.Existing tests:
That last one fails identically on an unmodified tree (verified with the change stashed): it resolves this host's real owner id where the fixture expects
424242424242.One half deliberately left out
discord_proactive_send.deliver_textbuilds{"channel_id", "content"}for aDeliveryProvider; actually threading the reply needsmessage_referencecarried through that payload — a change at the vendoreddelivery_coreboundary, not a grammar change. This PR stops the leak and gives the marker one owner; it does not yet make a proactive body render as a Discord reply.⚠ Correction — the task path's private regex is NOT deferred; this PR removes it. An earlier
revision of this description listed it as a second deliberate omission. That is stale and is
corrected here. At head
567926db,src/discord-bridge.pycarries no[reply:]regex at all —it takes the value straight out of the shared parser's actions:
So the marker has exactly one parser, which is the point of the PR. Verified at that head:
grepping
reply:insrc/discord-bridge.pyreturns no marker regex (the remaining hits are anunrelated
if reply:and a prompt string).Behaviour change:
[reply:]is now anchoredThe bridge's private copy was unanchored (
re.compile(r'\[reply:\s*(\d{17,20})\]')+.search()),so a
[reply:]anywhere in the body was honoured and stripped. The shared grammar is anchored(
^\s*\[reply:\s*(\d{17,20})\]\s*\n?+.match()), so a marker that is not at the start of the bodyis now neither honoured nor stripped — it reaches the user as literal text.
[channel:]This is a deliberate decision, not a side effect. It matches the rule this PR already applies to a
non-snowflake value: a marker that cannot be acted on stays visible rather than being silently
eaten, so a malformed or misplaced marker surfaces as a user-visible oddity instead of vanishing. A
producer that emits
[reply:]off the first line must move it to the front.