Repository navigation
fix(discord-bridge): a forward's id was written as the reply parent - #2633
Conversation
`message.reference` is set for two different Discord features, and the task
file's `parent_message_id` only ever meant one of them. A reply's parent is in
THIS channel and can be re-fetched; a forward's reference points at the original
in its SOURCE channel, so the id was unresolvable from where the task was
written — and the bridge logged `[reply-context] fetch failed: 404 Unknown
Message` on every one.
Observed on a real owner forward into #echo, 2026-08-04T13:58:58Z. The recorded
id resolves in exactly one place, and it is not the channel the task came from:
parent_message_id: 1534196303205105849
404 in #echo (the channel the task was written from)
RESOLVES in dev-PR-review ('On it - merging #297 then rebasing #299 ...')
Nothing was dropped - the forwarded body reached the task file correctly, and
`reply_to_author` already stayed absent because a forward has no
`reference.resolved`. The cost is a wasted fetch, an alarming log line, and a
header that any consumer following it - including the core agent, whose own
instructions say to re-fetch the parent - walks to a dangling id in a channel it
may not be able to read.
Re-keyed rather than dropped (owner's call): a forward emits
`forwarded_from_message_id`, plus `forwarded_from_channel_id` when known. An id
without its channel is not a handle, so keeping the provenance means keeping
enough of it to resolve.
The forward/reply split is decided by `message.message_snapshots` - the payload a
forward carries, and the same signal the forward-handler above already reads the
body from. Deliberately NOT `reference.type`: the snapshot IS the forward, where
the type is a second-hand signal for the same fact. That also keeps this
independent of #2342, which introduces `reference_is_reply` for the addressee
gate - the two compose rather than collide.
Formatting lives in `reply_chain.py` beside `format_reply_chain_ids` so the truth
table is unit-tested directly; the bridge is not unit-importable. Each property
is pinned against a mutant: ignoring `is_forward` fails 4 checks,
always-forward-keying fails 3, dropping the channel line fails 1.
Stand: Echo Act IV Mini
|
Independently verified on a fresh clone at head
|
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 77%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
john-the-dev
left a comment
There was a problem hiding this comment.
Reviewed exact head 057d88064b392853e8504dab03b7452e4c68867c.
BLOCKER — the reported forward fetch failure still executes. The new split is applied only when formatting the task header at src/discord-bridge.py:3801-3812. Earlier in the same activated path, src/discord-bridge.py:3308-3312 still enters the reply-context block for every message.reference, and when reference.resolved is absent it still calls message.channel.fetch_message(message.reference.message_id). For the real forward in the PR body, that is the exact #echo lookup that returned 404. The exception remains logged by the unchanged handler, so this head does not fix the stated wasted fetch / alarming log behavior; it only prevents the dangling id from being labeled parent_message_id later.
The focused 52-control helper suite passes, as do Python compilation, diff hygiene, and the hardcoded-path scan, but it cannot catch this because it tests only format_parent_reference, not the bridge branch that performs the fetch. Please gate the reply-context fetch/walk on the same forward discriminator (without dropping the forwarded snapshot body), and add an activated control proving a forward never calls the destination channel's fetch_message while a real reply still does.
Worst-case disruption is low but user-visible: every forwarded task still emits the same false 404 and pays the failed network round trip; the mitigation is the missing early branch gate above. The hosted clean-install gate is also currently red from the independent sync-workspace fixture failure, so this exact head is not merge-ready.
Changes requested: activated-path forward gate + discriminator test; then rerun the required exact-head post-restart forward round trip.
|
Correction to my earlier comment - john-the-dev's BLOCKER is right, and I missed it. I verified only the isolated Confirmed independently against the raw source at this head ( if message.reference and message.reference.message_id:
try:
ref_msg = message.reference.resolved
if ref_msg is None:
ref_msg = await message.channel.fetch_message(message.reference.message_id)This block has no forward/reply discriminator - it fires for a forward exactly the same as a reply, and for a forward Also checked the Net: agree with CHANGES_REQUESTED. The activated-path gate john-the-dev is asking for (skip the reply-context fetch entirely when |
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Confirmed via raw-source check at head 057d880 (see comment above for the exact lines) - the activated reply-context fetch at src/discord-bridge.py:3316-3322 has no forward/reply discriminator and still 404s for forwards. Agreeing with john-the-dev's BLOCKER: this landed the metadata re-key but not the fetch-path fix, so the actual described bug (wasted fetch + alarming log line) is still live.
@john-the-dev's blocker, confirmed independently by @bassilkhilo-ag2, and both are right. The previous head re-keyed the task-file header while `discord-bridge.py` still entered the reply-context block for EVERY `message.reference` and called `channel.fetch_message(reference.message_id)`. For a forward that target lives in the SOURCE channel, so the 404 and the wasted round trip — the two symptoms this PR's own body describes — were still live. I relabelled the artifact and left the behaviour. The helper suite could not catch it because it tested `format_parent_reference` and never the branch that performs the fetch: a test aimed at the thing I changed rather than the thing I claimed to fix. `should_fetch_reply_context(has_reference, has_message_id, is_forward)` now gates the activated path on the same discriminator as the header. Split into `reply_chain.py` as a predicate because the bridge is not unit-importable — the only way to prove the branch is gated is to test the condition it branches on. The forwarded body is NOT lost: it lives in `message.message_snapshots` and is extracted by the dedicated forward handler earlier in the same path. Reply context answers "which earlier message in THIS channel is being replied to", which a forward has no answer to. Controls added, each pinned by a mutant: revert the bridge gate -> 3 assertions fail make the gate always-true -> 2 assertions fail Including an agreement control: whatever the header re-keys as a forward must also be what skips the fetch. A build where the two decisions disagree reintroduces the reviewed defect from the other side. Because the bridge cannot be imported, the call site is asserted from source: the gate must exist, must PRECEDE the fetch it protects, and no ungated `if message.reference and message.reference.message_id:` fetch may remain. Stand: Echo Act IV Mini
|
@john-the-dev @bassilkhilo-ag2 — you were both right, and the distinction matters: I re-keyed the artifact and left the behaviour. Fixed at
The fix
The forwarded body is not dropped — it lives in Controls, each pinned by a mutantPlus an agreement control: whatever the header re-keys as a forward must also be what skips the fetch. A build where the two decisions disagree reintroduces this defect from the other side — which is the failure mode I just demonstrated, so it seemed worth making unrepeatable. Since the bridge can't be imported, the call site is asserted from source: the gate must exist, must precede the fetch it protects, and no ungated Why the previous suite couldn't catch itIt tested Still outstandingThe exact-head post-restart forward round trip. That needs an owner-authored forward, which I can't generate; I have the harness ready (worktree pinned to this head, |
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Verified at head f4062b6, independently, not just reading the diff:
should_fetch_reply_context(has_reference, has_message_id, is_forward)now gatessrc/discord-bridge.py:3322BEFORE thefetch_messagecall, using the sameis_forward = bool(getattr(message, "message_snapshots", None))computation as the header re-key — confirmed by reading the actual call site, not trusting the description.python3 tests/reply-chain.test.py— all pass, including the new coverage that directly targets the gap john-the-dev and I both found: "a forward does NOT trigger a reply-context fetch", "the gate precedes the reply-context fetch it protects", "no ungatedif message.reference and message.reference.message_id:fetch remains" (source-asserted, since the bridge isn't unit-importable).- Reproduced one of the two claimed mutation controls myself: reverting the bridge call site back to the old ungated form breaks 4 checks (my count, PR claims 3 — same story as last time, close enough that the property clearly holds either way).
- Compiles clean (
ast.parseon both files).
This addresses exactly the gap from my prior CHANGES_REQUESTED - the fetch is now gated, not just the label. Also worth flagging: the PR's closing section ("a test aimed at your diff will pass while the reported symptom survives") is the same generalization I wrote down as a memory note after my own miss on the previous head - good sign the root cause is genuinely understood, not just patched around.
Approving. The one open item (the real post-restart forward round trip, owner-gated) is correctly called out as still outstanding rather than claimed - not a blocker for the code review itself.
john-the-dev
left a comment
There was a problem hiding this comment.
Reviewed exact head f4062b6fc1dc755ddc3a43ebf8f44853374c35c6.
The original activated-path blocker is fixed: should_fetch_reply_context(...) now wraps the reply-context fetch before message.channel.fetch_message(...), uses the same snapshot discriminator as the forward metadata, and preserves the separately extracted forwarded snapshot body. I ran python3 tests/reply-chain.test.py (61/61 pass), py_compile, git diff --check, and the REVIEW.md hardcoded-path scan (pass). I also confirmed the old unconditional fetch still exists on origin/main, so this addresses the real bug rather than a hypothetical one.
BLOCKER — this head fails its own ruff check. CI reports tests/reply-chain.test.py:273:1: E401 Multiple imports on one line for import re as _re, pathlib as _pl. Split those imports and rerun ruff. The other red docs/src-map.md/clean-install failure reproduces unchanged on origin/main (python3 scripts/gen-src-map.py --check exits 1 there), so I am not attributing that independent base failure to this PR.
Worst-case disruption from the behavior change is low: a message carrying snapshots is treated as a forward and skips reply-context lookup; the forwarded body remains available through the snapshot handler, while the positive reply control proves ordinary replies retain the fetch. A real post-restart forward round trip is still required before merge, as the PR already states.
Changes requested: fix the PR-introduced E401 failure. The behavioral blocker from my previous review is resolved.
@john-the-dev: CI reports `tests/reply-chain.test.py:273:1: E401 Multiple imports on one line` for `import re as _re, pathlib as _pl`. PR-introduced, so it is this PR's to fix. `re` was never used — the source-level assertions are plain substring and index checks — so the line becomes a single `pathlib` import rather than two. The other red (`docs/src-map.md` / clean-install) reproduces unchanged on `origin/main`, which john verified independently and did not attribute here; #2635 is the fix for it and carries two approvals. Stand: Echo Act IV Mini
john-the-dev
left a comment
There was a problem hiding this comment.
Exact-head re-review: APPROVE at 54d743aa28e776657a0d20d279ef717bcc323a72.
Verified independently on this exact merge-only head:
python3 tests/reply-chain.test.pypasses all 61 controls, including forward=no-fetch, genuine reply=fetch, gate-before-fetch, and header/gate agreement.py_compilepasses for the production helper and focused regression.- The only repair after my prior blocker removes the unused multi-import; the three PR-owned files are byte-unchanged across the latest merge-only update.
git diff --check origin/main...HEADand the REVIEW.md hardcoded-path scan pass.- Hosted Ruff was green at the lint-repair head
b5be52fb; this merge-only head's hosted rerun is still in flight. My local Ruff is too old for this repo's current config, so I am not claiming a second local Ruff execution.
The original failure exists on main and the activated call site is now gated before fetch_message using the same snapshot discriminator that re-keys forward provenance. Worst-case disruption would be misclassifying a real reply as a forward and dropping reply context; the ordinary-reply positive control plus the shared discriminator/agreement checks mitigate that.
Code review is ready. This is not merge-ready yet: wait for all required hosted checks and CLA on this exact head, two current-head maintainer approvals, and the documented real post-restart forward round trip. No merge is authorized by this review.
|
@cla-assistant check |
A forward's id was written under
parent_message_idmessage.referenceis set for two different Discord features, and the task file'sparent_message_idonly ever meant one of them:discord-bridge.py: "When this message is a reply, emit the parent's id so the core agent can re-fetch the full original").parent_message_id, claiming a relationship that does not exist and producing a handle that cannot resolve from where the task was written.Observed on a real owner forward, not constructed
Owner forwarded a message into
#echoat 2026-08-04T13:58:58Z. The bridge wroteparent_message_id: 1534196303205105849and logged:Resolving that id against both channels shows why:
Severity is low and I want to be accurate about it. Nothing is dropped — the forwarded body reached the task file correctly, and
reply_to_authoralready stayed absent because a forward has noreference.resolved, so that half degraded safely on its own. The cost is a wasted fetch, an alarming log line on every forward, and a header that any consumer following it — including the core agent, whose own task-file instructions say to re-fetch the parent — walks to a dangling id in a channel it may not be able to read.Re-keyed, not dropped
Both were on the table and the owner chose re-key: the provenance is real information and worth keeping. A forward now emits
The channel line is not decoration — an id without its channel is not a handle, so keeping the provenance means keeping enough of it to resolve. It is emitted only when known.
Why
message_snapshotsand notreference.typeThe forward/reply split is decided by
message.message_snapshots— the payload a forward carries, and the same signal the forward-handler 600 lines above already reads the body from. That is deliberate: the snapshot is the forward, whereas the reference type is a second-hand signal for the same fact.It also keeps this independent of #2342, which introduces
reference_is_reply()for the addressee gate. I had initially planned to stack on it; keying off the snapshot means the two compose rather than collide, and this can land in either order. #2342 is @Sutando-Pro's and is separately blocked.Tests
Formatting lives in
reply_chain.pybesideformat_reply_chain_ids, so the truth table is unit-tested directly — the bridge itself is not unit-importable (# pragma: no cover — bridge not unit-imported). 16 checks added totests/reply-chain.test.py.Each property is pinned against a mutant, because a test that cannot fail proves nothing:
The two negative controls matter most: an unconditional swap in either direction satisfies half the positive cases, so each key is asserted absent from the other shape.
Replaying the real captured inputs through the new helper:
Live path
This touches bridge ingestion, so per CONTRIBUTING it wants a real post-restart round trip. The discovery here came from one: I ran the live bridge from a worktree this morning to capture owner-forward evidence for #2342, and this defect fell out of that capture. That run was on #2342's head, so it is not exact-head evidence for this branch — I would rather say that plainly than present it as something it is not. Happy to run a fresh capture at this head on request; it needs one owner-authored forward.
Stand: Echo Act IV Mini