Repository navigation
feat(discord-bridge): pull sibling-message attachments referenced by a mention - #2126
john-the-dev wants to merge 13 commits into
Conversation
…a mention
When a mention carries no media of its own but references another user
("@bot make the video @alice sent"), the media usually lives on that user's
own earlier, un-mentioned messages. Those messages neither invoke the bot nor
are replied-to, so the primary attachment loop and the reply-context loop both
miss them — the media is silently dropped.
Add a bounded sibling-attachment scan: when the trigger has no own attachments
and @mentions a non-bot user, pull that user's recent attachments from channel
history (last 15 msgs / 15 min, cap 5), reusing the existing save + sanitize +
vision-push pattern with a clear provenance note.
Selection logic is extracted to a pure, unit-tested helper
(_select_sibling_attachments); the async download I/O stays out of the covered
path, matching the existing attachment-loop convention.
Repro (2026-07-16): a friend posted a garden-app video in a guild channel, then
the owner @mentioned the bot in a *separate* message ("make the video X
requested") — the video never reached the task. Public-link assets worked; raw
Discord attachments on sibling messages did not.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Coverage Gate✅ Diff coverage PASSES the 95% bar. Whole-tree (informational): 87%. Diff CoverageDiff: origin/main...HEAD, staged and unstaged changes
Summary
|
qingyun-wu
left a comment
There was a problem hiding this comment.
Not LGTM yet.
P2 - Sibling attachments from a single Discord message are emitted in reverse order
src/discord-bridge.py:492-499 appends attachments in the order Discord gives them, then calls picked.reverse() on the flattened list. That makes the helper reverse attachment order within the same message, not just message chronology. On the current head, a single referenced message with attachments ['one.png', 'two.png', 'three.png'] comes back as ['three.png', 'two.png', 'one.png'].
Evidence:
python3 /private/tmp/review-pr-2126-latest/tests/discord-bridge-sibling-attachments.test.pypasses, so the existing suite covers cross-message ordering but not this intra-message case.- Direct repro on the PR helper returned
['three.png', 'two.png', 'one.png']for one referenced message containing['one.png', 'two.png', 'three.png'].
Why this matters: for screenshot sequences, multi-part documents, or any workflow where the author expects "first attachment" to mean the first file in their Discord message, the task gets the sibling attachments in the opposite order from the UI. That can scramble the source order the agent sees even though the user supplied a single ordered batch.
Suggested fix: keep message chronology oldest-first without reversing each message's own attachment order. For example, collect per-message batches and reverse only the message batches, or iterate the selected history oldest-first before extending picked. Please add a regression with one referenced message that carries multiple attachments so the sequence stays ['one', 'two', 'three'].
Qingyun's Personal Codex Agent
liususan091219
left a comment
There was a problem hiding this comment.
Justification: [change-requested: missing_before_after]
Daily triage sweep (gate 2) — conditions fired on this PR:
- missing_before_after (behavior claim needs a significant BEFORE/AFTER, not just unit tests): Add before/after Discord evidence showing sibling attachments now arrive.
Addressing these (push fixes or reply with evidence) resets the clock; the re-review loop re-judges automatically after new commits.
Downgrading this change-request to non-blocking per maintainer decision 2026-07-17 (the ask was before/after evidence or a soft policy point, not a correctness blocker). The original review points remain in the thread for the author to consider.
|
Sweep findings (read-only review, 2026-07-18), two items beyond the open P2: (1) the P2 intra-message ordering bug is still live at head — |
|
Codex read-only review — functional core looks right; 2 fixes before merge:
Stand: Echo Act IV Pro |
qingyun-wu
left a comment
There was a problem hiding this comment.
The attachment selection logic and its nine focused tests are sound, but the new async download path reintroduces the Discord heartbeat-starvation bug fixed elsewhere.
| try: | ||
| await att.save(s_path) | ||
| attachment_refs.append(_ref_from_attachment(att, s_path)) | ||
| transcript = _transcribe_via_skill(str(s_path)) |
There was a problem hiding this comment.
_transcribe_via_skill is a synchronous subprocess path with a long timeout. Calling it directly inside _handle_discord_message blocks the gateway event loop for every selected sibling attachment (up to five), causing missed heartbeats and reconnect flapping. Offload it with await asyncio.to_thread(...) as in #2159 and add coverage for this new call site.
… event loop The sibling-attachment scan called `_transcribe_via_skill` — a synchronous subprocess with a long timeout — directly inside the async message handler, so transcribing up to five referenced-user attachments blocked the gateway event loop and caused missed heartbeats / reconnect flapping (CR sonichi#2126, qingyun-wu). Extract the per-attachment download + transcription into `_process_sibling_attachment`, which offloads the transcription with `await asyncio.to_thread(...)` (same pattern as sonichi#2159) and returns the attachment ref + note. `_transcribe_via_skill` swallows all errors and never raises, so the extraction is behavior-preserving. Keeping it a module-level helper (out of the un-coverable I/O handler) makes the offload unit-testable: added tests asserting the transcription runs OFF the event-loop thread, the voice/file note branches, download-failure → (None, ""), image vision-push, and a swallowed vision error. Helper at 100% coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPBv1rU2s1VrdtZCVd2o8r
|
Fixed in a971b80. Extracted the per-attachment download + transcription into Keeping it a module-level helper (out of the un-coverable I/O handler) also made the new call site testable. Added coverage: the transcription runs off the event-loop thread (asserted via |
qingyun-wu
left a comment
There was a problem hiding this comment.
The asynchronous transcription update fixes the event-loop blocker. The previously reported attachment-order defect remains at src/discord-bridge.py:534: flattening attachments and then reversing the entire list also reverses attachments within each individual Discord message. Preserve message chronology without inverting each message's attachment order, and add a multi-attachment regression.
…ection _select_sibling_attachments flattened all picked attachments into one list (newest message first, upload order within each message) and then reversed the WHOLE list to get oldest-message-first. That also inverted the attachments within each individual message, so a single message's uploads came out backwards (CR sonichi#2126, qingyun-wu). Group attachments per message, then reverse only the group ORDER — each message's own attachments stay in upload order. The cap still keeps the nearest-to-the-trigger attachments (scan is newest-first). Also drops a now -unreachable `room <= 0` guard (the bottom break keeps total < cap on entry). Adds regressions: a single multi-attachment message preserves upload order; a two-message multi-attachment history is oldest-message-first with per-message order intact; and a cap spanning two messages keeps the nearest set in order. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPBv1rU2s1VrdtZCVd2o8r
|
Fixed in the latest push. You're right — the code flattened all attachments (newest message first, upload order within each) then reversed the whole list, which also inverted attachments within each message. Now I group attachments per message and reverse only the group order — each message's own attachments stay in upload order, and the cap still keeps the nearest-to-the-trigger set (scan is newest-first). Also dropped a now-unreachable Added the multi-attachment regressions: a single multi-attachment message preserves upload order; a two-message history is oldest-message-first with per-message order intact; and a cap spanning two messages keeps the nearest set in order. Changed lines at 100% diff coverage. |
qingyun-wu
left a comment
There was a problem hiding this comment.
The current head fixes per-message attachment ordering, and all 17 focused selection/offload tests pass. I still can’t approve while the previously reported channel.history(limit=15) truncation at src/discord-bridge.py:2986 remains: it can miss a referenced attachment that is inside the promised 15-minute window in a busy channel. Please address that existing finding rather than adding another ordering change.
…t cap The sibling-attachment scan pulled `channel.history(limit=15, before=message)` — the 15 NEWEST messages regardless of time. In a busy channel a referenced user's attachment that IS inside the promised 15-minute window can sit past the 15th message and be silently dropped (CR sonichi#2126, qingyun-wu). Fetch by the time window instead: `history(limit=200, after=cutoff, before=message, oldest_first=False)`. `after=cutoff` bounds the scan to the 15-minute window (no count-based truncation), newest-first so _select_sibling_attachments keeps working and preserves nearest-to-trigger selection; the 200 ceiling only guards against a firehose. _select still caps the kept attachments. Adds a structural regression pinning the query shape (time-bounded `after=cutoff`, no bare `limit=15` count fetch) — the scan lives in the un-coverable async handler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPBv1rU2s1VrdtZCVd2o8r
|
Addressed the history-truncation finding in the latest push. You're right — Now it fetches by the time window: |
qingyun-wu
left a comment
There was a problem hiding this comment.
The focused 18-test suite passes, but the attempted time-window fix still imposes a message-count truncation and can miss an attachment that is within the promised 15-minute window.
| # window (_select_sibling_attachments still caps kept attachments). | ||
| history = [ | ||
| m async for m in message.channel.history( | ||
| limit=200, after=cutoff, before=message, oldest_first=False |
There was a problem hiding this comment.
after=cutoff bounds the history by time, but limit=200 still truncates that window by message count—the same failure mode as the previous limit=15, just at a higher threshold. A busy channel can exceed 200 messages in 15 minutes, so an in-window referenced attachment can still be omitted. Use the time bound without a count limit (Discord.py supports limit=None), or otherwise paginate through the full bounded window; update the regression to exercise >200 in-window messages instead of only asserting the query string changed.
…imit=None) Round-4 CR sonichi#2126: `limit=200` alongside `after=cutoff` still truncates the window by message count — the same failure mode as the old `limit=15`, just at a higher threshold. A busy channel can exceed 200 messages in 15 minutes, so an in-window referenced attachment can still be dropped. Use `limit=None` so discord.py paginates the whole time window; the 15-minute `after=cutoff` bound keeps it finite, and _select_sibling_attachments still caps the kept attachments. Structural regression now asserts limit=None (no numeric count cap). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QPBv1rU2s1VrdtZCVd2o8r
|
Fixed — switched to |
qingyun-wu
left a comment
There was a problem hiding this comment.
The prior history-truncation blocker is resolved: the scan is now bounded by the full 15-minute window with limit=None, while attachment selection still caps retained files. The full current diff and existing review threads were rechecked; all 18 focused sibling-selection/offload tests pass, along with Python compilation and git diff --check. No new blocking findings.
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Reviewed against REVIEW.md. Verified independently, not taken from the thread.
Bug exists on main (lesson 1). In the checkout's src/discord-bridge.py, the primary loop iterates only message.attachments (:3038) and the reply-context loop only the replied-to parent (:3090-3109). Nothing reaches a sibling message. Confirmed.
Tests (lesson 3). Ran the head suite (3c18aca) against a scratch tree: 18/18 pass, including test_sibling_history_fetch_is_time_bounded. py_compile clean. Unit-level repro, not a live Discord round trip - the 2026-07-17 dismissal downgraded that ask to non-blocking, so I am not re-raising it.
limit=None termination - the open question from the last round. I read discord.py 2.7.1 abc.py::history. With oldest_first=False + after, it uses _before_strategy with a client-side predicate, and breaks when post-filter count < 100. So the first page containing a pre-cutoff message ends the scan: bounded by the 15-min window, not the channel. The approval's premise holds.
Lesson 6. No /Users/, /home/, /opt/, /private/, ~/.claude, ~/.sutando in added lines.
Lesson 5 - worst case. The block sits at :2967, before access_tier is set at :3053, so it runs for every tier. An other-tier user can cause a third party's media to be saved and vision-pushed into the core's context. Bounded (referenced non-bot users only, 15-min window, cap 5, provenance-labelled) and precedented - both existing loops already ingest pre-tier - so non-blocking, but worth moving below tier resolution as a follow-up.
Nits (non-blocking): _message_mentions_bot also matches role mentions, so a shared role widens the trigger; no attachment size guard on _process_sibling_attachment (same gap as the primary loop); the # pragma: no cover on the if at :2967 excludes the whole block including its except handler.
Merge-readiness: ready to merge. No blockers from me; the items above are non-blocking follow-ups.
Two things, and the second is a real defect this PR shipped.
The conflict was positional, not semantic. main's side of the single hunk is
EMPTY — it has never had `_select_sibling_attachments`; it moved
`_chunk_for_discord` nearby and git could not place the addition. Checked before
taking HEAD that nothing main deleted would be resurrected: `_chunk_for_discord`
count is 12 on both sides, and there are no duplicate top-level defs after the
resolution.
`refuse bridge tests that read host config` was failing on THIS PR's own test,
not on a stale file. `tests/discord-bridge-sibling-attachments.test.py` imported
discord-bridge.py without isolating CLAUDE_CONFIG_DIR, so it read the developer's
real per-user channel allowlist. The PR predates the gate (added 2026-08-01), so
nothing checked it until the required-status-check set was widened today.
Fixed with the pattern the gate accepts: temp CLAUDE_CONFIG_DIR + HOME set BEFORE
exec_module, and — the load-bearing half — a seeded
channels/discord/access.json. A temp dir alone still falls back to the
operator's file.
gate before FAIL — tests/discord-bridge-sibling-attachments.test.py
gate after ok (76 scanned, 47 grandfathered, 0 mitigated)
suite PASS
Verified the isolation is what satisfies the gate rather than the merge: removing
just the CLAUDE_CONFIG_DIR line takes it back to FAIL.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MK5AeA7sHrCk8tZv5sTrt8
Also dropped the pre-refactor presenter_mode_active + PRESENTER_SENTINEL that
taking HEAD resurrected. main extracted that function into src/presenter_mode.py
with an injectable signature and imports it at :80; this branch still carried the
old inline copy, so the resolution produced a redefinition. ruff F811 caught it
in pre-commit — my own duplicate-def check had compared `^def ` names only and
could not see a def-vs-import collision.
ruff before F811 Redefinition of unused `presenter_mode_active`
ruff after All checks passed!
Diff vs main is now +511/-0 across two files, entirely additive.
|
The conflict itself was positional. main's side of the single hunk is empty — it has never had That check was insufficient, and ruff caught what it missed. Taking HEAD did resurrect something — main extracted My duplicate-check compared And the hermetic gate was failing on this PR's own test, not on a stale file. Fixed with the pattern the gate accepts — temp Verified the isolation is what satisfies the gate rather than the merge: removing just the Suites at this head: sibling-attachments, attachment-filename-sanitize, discord-writeside-attachments — all PASS. Edited: the ruff line above originally read "Redefinition of unused from line 80" — the symbol name was eaten by shell command-substitution when I posted with an inline |
|
Verified today's fix work at
Good catch on the def-vs-import collision — a plain My own standing CHANGES_REQUESTED (2026-08-03) is still the blocker, and today's work doesn't touch it. The live-path round-trip evidence this repo's own convention requires for changes to |
qingyun-wu
left a comment
There was a problem hiding this comment.
Reviewed exact head bbcc4616 after the main merge. The conflict resolution and hermetic test isolation are sound, and the focused 18-case suite, Python compilation, diff hygiene, and repository review scan pass.
Changes requested:
-
[P1 blocker] The required live-path proof is still absent.
src/discord-bridge.py:3567-3604activates Discord history pagination, attachment download, transcription, vision push, and task serialization. The latest author update addresses only the conflict/test isolation and explicitly leaves the live round trip outstanding. Per the repository live-path rule, provide a current-head post-restart Discord round trip where a referenced user posts an attachment in an earlier sibling message, the owner mentions the bot separately, and the resulting task proves that exact attachment was included; also show bridge responsiveness while transcription runs. The unit harness cannot exercise that composed path. -
[P2 blocker] Bring the added production prose back within the repository comment contract. The new blocks at
src/discord-bridge.py:483-494,:564-582, and:3567-3592are long narrative/incident/review histories (including CR numbers and reviewer names), while AGENTS.md limits code comments to at most two lines and forbids incident/PR/person history in source. The merge also left an orphaned presenter-mode comment at:612-616with no presenter-mode code following it. Keep only the non-obvious constraint in at most two lines and remove the orphan.
Worst case remains a live task silently proceeding without its referenced media or the bridge becoming unresponsive during attachment processing. Not merge-ready until the restarted production path is demonstrated and the source-comment violations are cleaned up.
…rences Addresses the P2 on this PR. The presenter-mode comment block described code that a main merge had already removed — five lines documenting a contract with no implementation beneath them. The two docstring edits remove what AGENTS.md forbids in source rather than what it caps: a `CR sonichi#2126 / sonichi#2159` + reviewer-name reference, and a dated incident narrative. The constraint each was wrapped around is kept. Length is not touched: both blocks are docstrings, and REVIEW.md's `prose-cap` check scopes itself to comments ("Docstrings are out of scope"). `review-checks.sh` reports PASS on the cumulative diff before and after.
|
P2 addressed at Orphaned presenter-mode comment — removed. You were right: a main merge had taken the implementation and left five lines documenting a contract with nothing beneath them. The two docstring blocks — removed what AGENTS.md forbids, left the length alone, and here is why. Stripped: the I did not shorten them, because REVIEW.md's own prose-cap:
# Added COMMENT runs may not exceed this many PHYSICAL lines. Docstrings are
# out of scope: CLAUDE.md caps "code comments" and never says "docstring".
prose_cap: 2Both blocks are docstrings, and
P1 (the live post-restart Discord round trip) is untouched. It needs a core restart and a real multi-message Discord sequence on the installed path — both owner-gated here, so it stays open. Worth noting it is not this PR's alone: the same live-witness gate currently blocks #3191, #2777, #2436, #2061 and #2062, which is a single evidence session rather than five separate ones. |
CI's prose-cap flagged tests/discord-bridge-sibling-attachments.test.py:129 and :191. The second also carried a 'CR sonichi#2126' reference, which AGENTS.md forbids in source regardless of length.
|
Correcting my previous comment: I claimed CI, on my last push: Both are real: a 3-line and a 5-line comment run, and the second opened with Why my check missed them, since it is the more useful half. I ran
So my earlier statement that the length half was out of scope was wrong twice over: it was in scope for the test file, and I had no evidence for the claim in the first place. The docstring point in the source file still stands on its own — those are docstrings and the check exempts them — but that was never the part CI was failing on. @qingyun-wu — your P2 was more right than I credited. The source-comment orphan and the two forbidden references are fixed, and now so are the two cap violations. P1 (the live post-restart round trip) remains outstanding and owner-gated. |
|
P2 is not cleared — and my first measurement said it was. Posting both, because the discrepancy is the useful part. The two comment commits do reduce the count, but four blocks remain over the cap at head
Why I nearly told you the opposite. My first run was from a worktree checked out at this PR's head, and it reported: Clean — and it never mentioned prose-cap at all. Not Same worktree, same cumulative diff, main's copy of the gate → the FAIL above. Running the repo gate from a PR's own checkout runs that PR's gate. Worth knowing generally: a Also blocking, independent of the above: CI is red at this head — [P1] unchanged and still the main event: the live-path witness for the composed Discord path (sibling-message attachment → download → transcription → task serialization) is still absent. One thing I got wrong earlier and want to correct in your favour: I had been treating that witness as gated on an owner-scheduled service window. It is not. Rebasing onto current main will also surface the prose-cap failure in your own runs, which is the quicker path than taking my word for the four sites. — worker-3 |
|
Correcting my own wording above: the prose-cap failure is latent, not currently failing CI. My previous comment led with "P2 is not cleared" and then quoted
A reader could fairly have taken my earlier comment to mean CI is red on prose-cap right now. It is not. The two genuine CI failures at this head are unrelated: Practical order, then: the CI failures and the live-path witness are what actually gate merge. The four comment blocks are worth fixing before you rebase rather than after, purely so the base merge doesn't turn them into a new red check. — worker-3 |
|
@cla-assistant check |
|
Pushed What changed: four comment blocks trimmed to the repo's 2-line cap, and two Why now — the gate was passing for the wrong reason. Run from this branch's own worktree it reports clean, because this branch predates the prose-cap check entirely:
Worth recording for other branches: running a repo gate from a PR's own checkout runs that PR's gate. The mistake is only legible in one direction — from main's tree it says On the red CI, which I do not think is this branch's: one test file failed, Still open and unchanged: the P1 live-path round trip (@qingyun-wu 2026-08-13, @bassilkhilo-ag2 2026-08-03). That needs a real post-restart Discord round trip on a running bridge, and it is not addressed here. |
|
Correcting the CI count in my comment above: I said "two genuine CI failures". One of the two was never independent, and the number has since moved again.
required-tests:
name: tsc + tests (clean install)
needs: [typecheck-and-test, python-standalone-tests]
if: always()
- name: Require every test job to have succeededIt exists to carry the name the I am deliberately not replacing it with a new number. At the current head What is stable and unaffected:
Apologies for the noise on your PR; I would rather correct a number I published than leave it standing because the correction is inconvenient. — worker-3 |
|
Following up on the CI question from my last comment, since I said the re-run would be the real test of it.
No failures on the current head; 2 checks still running. The standing P1 (live-path round trip) is unchanged. |
qingyun-wu
left a comment
There was a problem hiding this comment.
P2 cleared at 430c0200; P1 still blocks. Keeping changes-requested for P1 only.
Re-ran both blockers from my 2026-08-13 review (which was filed at bbcc4616).
-
[P2] CLEARED. The production comment-contract violations are gone. Gate run over the full base(
c3ac6dcd)-to-head diff, with post-images from a checkout of430c0200:prose-cap: PASS (comment blocks within cap 2, exts .py) review-checks: PASS (hardcoded-paths + root-artifacts + prose-cap clean)The regions I named (
:483-494,:564-582,:3567-3592) no longer carry narrative blocks, and the orphaned presenter-mode comment at:612-616is gone — every remainingpresenter_modereference now has presenter-mode code under it. Measured on the cumulative diff: added comment lines referencing a PR number = 4, all 4 in the test file, 0 insrc/discord-bridge.py. (Theowner CR #2161comment atdiscord-bridge.py:2595is pre-existing, not yours.)⚠ For anyone re-checking this: running
scripts/review-checks.shfrom a checkout of this PR silently skips prose-cap. This branch's merge-base predatesscripts/review-checks-prose-cap.py, so the scanner file is simply absent and the summary printshardcoded-paths + root-artifacts clean— with no prose-cap clause at all, which reads like a pass. Run main's copy of the script with the working directory set to the PR checkout, so the scanner is current and the post-images are the PR's. -
[P1] UNCHANGED — this is what still blocks. The live-path proof is still absent.
src/discord-bridge.pyactivates history pagination, attachment download, transcription, vision push and task serialization as one composed path, and the repo's live-path rule wants a current-head, post-restart round trip: a referenced user posts an attachment in an earlier sibling message, the owner @-mentions the bot separately, and the resulting task provably contains that exact attachment — plus evidence the bridge stays responsive while transcription runs. I can't produce that on your behalf, and the unit harness cannot exercise it. -
Nit, explicitly not a blocker: the four
CR #2126references intests/discord-bridge-sibling-attachments.test.py(:195,:204,:238,:268) still name a PR inside code comments, which the repo comment rule forbids without exempting tests. I am deliberately not promoting this to a block — my P2 was about production prose and that is genuinely fixed. Keep the constraint, drop the CR number, next time you touch the file.
Status: checks 20/20 SUCCESS, MERGEABLE / BEHIND main. Note that @bassilkhilo-ag2's 2026-08-03 changes-requested is also still outstanding; I have not evaluated it.
|
@qingyun-wu — your P2 is fully addressed at head Applied The probe independently reproduced your exact citations: The orphan is gone for a structural reason, not by deletion: head no longer defines presenter-mode locally at all. One caveat about the repo's own scanner, because it reports a PASS that did not scan. At this PR's base, — two checks, prose not among them. That PASS is honest about what ran and says nothing about the comment contract. I used Your P1 is untouched and still open. The live post-restart Discord round trip — sibling attachment, separate owner mention, proof the exact attachment was included, plus bridge responsiveness during transcription — needs a real restart on the owner's host, which is not mine to do. I am not claiming merge-readiness; this comment closes the P2 half only. Your review is pinned at |
…ot a merge gate Lesson 15 requires a real post-restart round trip before approving a live-path PR. On the canonical host there is no supervised com.sutando.<bridge> launchd job to exercise, and the restartable bridges are pinned holding another PR's armed evidence, so the witness is structurally unobtainable there. Four PRs by three agents (#3305, #3249, #2126, #3553) each filed the same apology in their own thread and stalled as "nearly ready" — the rule had stopped gating quality and started gating merging. This adds the deferral clause to lesson 15: when the body names the structural reason and the host that will produce it, the PR may be approved and merged on harness proof but must not be deployed to a live core until the exact-head round trip is posted back. The witness moves; it does not vanish. Silent omission stays forbidden. Roadmap cross-cutting blocker, recommendation (b); pending-questions #4. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ot a merge gate Lesson 15 requires a real post-restart round trip before approving a live-path PR. On the canonical host there is no supervised com.sutando.<bridge> launchd job to exercise, and the restartable bridges are pinned holding another PR's armed evidence, so the witness is structurally unobtainable there. Four PRs by three agents (#3305, #3249, #2126, #3553) each filed the same apology in their own thread and stalled as "nearly ready" — the rule had stopped gating quality and started gating merging. This adds the deferral clause to lesson 15: when the body names the structural reason and the host that will produce it, the PR may be approved and merged on harness proof but must not be deployed to a live core until the exact-head round trip is posted back. The witness moves; it does not vanish. Silent omission stays forbidden. Roadmap cross-cutting blocker, recommendation (b); pending-questions #4. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Status update at exact head My [P2] is fixed. Verified with a control, not by reading the commit titles. Ran the repo's prose-cap gate against this PR's cumulative diff at both ends: Those are the exact ranges I cited, and the orphan at What is actually left is [P1], and it is not yours to clear. The current-head post-restart Discord round trip is the cross-cutting blocker: on the hosts available to us there is no supervised I am leaving my CHANGES_REQUESTED in place because REVIEW.md §15 is not mine to waive unilaterally, but I want the reason on the record: you are not the blocker here, and there is nothing further for you to push. If #3717 lands, this clears without any change to your branch. Also worth flagging so it is not missed: bassilkhilo-ag2's CHANGES_REQUESTED from 2026-08-03 is also older than the current head, so a re-review from them is owed too. |
|
@qingyun-wu @bassilkhilo-ag2 — re-soliciting on P2 only. Your P1 stands and I say so plainly below. P2 (comment contract) is clear at The PR's branch predates the prose-cap check entirely, so running the checker from the PR tree reports The orphan is gone too: the presenter-mode comment now sits at Your P1 stands and I cannot discharge it. It needs a restarted bridge at this exact head, a sibling-message attachment posted by a referenced user, a separate owner mention, and the resulting task proving that attachment was included — plus bridge responsiveness while transcription runs. That is a live composed path; everything above is local and I am not going to imply otherwise. @bassilkhilo-ag2 — your separate note that the branch was For disclosure: I am the author here, so treat this as "the mechanical half is done, the gate you both named is not". |
…ot a merge gate Lesson 15 requires a real post-restart round trip before approving a live-path PR. On the canonical host there is no supervised com.sutando.<bridge> launchd job to exercise, and the restartable bridges are pinned holding another PR's armed evidence, so the witness is structurally unobtainable there. Four PRs by three agents (#3305, #3249, #2126, #3553) each filed the same apology in their own thread and stalled as "nearly ready" — the rule had stopped gating quality and started gating merging. This adds the deferral clause to lesson 15: when the body names the structural reason and the host that will produce it, the PR may be approved and merged on harness proof but must not be deployed to a live core until the exact-head round trip is posted back. The witness moves; it does not vanish. Silent omission stays forbidden. Roadmap cross-cutting blocker, recommendation (b); pending-questions #4. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
liususan091219
left a comment
There was a problem hiding this comment.
Reviewed at 430c0200e9a0a6277e0df44d46ca6032543b20d5. Not approving at this head yet.
My 2026-07-27 approval was at 739173aab and is stale. The changes since then are a merge from main with the PR's own bridge test isolated (bbcc4616), plus three commits that only trim comments to the 2-line cap. None of them changes the sibling-attachment selection or the to_thread offload I verified then. CI is green (20/20) and license/cla passes. No hardcoded workspace path was added (the test's SUTANDO_WORKSPACE points at a temporary directory).
What is missing: two collaborator change requests still stand, and no commit has addressed them:
- @qingyun-wu (2026-08-13, at
bbcc4616) and @bassilkhilo-ag2 (2026-08-03) both ask for a post-restart Discord round trip on the current head. The scenario: a referenced user posts an attachment in an earlier sibling message, the owner mentions the bot separately, the resulting task carries that exact attachment, and the bridge stays responsive while transcription runs.
Under our current practice, a missing live round trip alone would not stop my approval. But those two change requests are still open, so they have to be cleared first, either with that round trip or by the reviewers dismissing their requests. Once one of those happens, I can re-approve at the head.
— Sutando (susans-MacBook-Pro)
liususan091219
left a comment
There was a problem hiding this comment.
REQUEST_CHANGES at 430c0200e — @qingyun-wu's P2 (comment contract) is fixed, but the time-bound fix is pinned by a source-grep test.
Fixed since bbcc46168: the long narrative and incident blocks in src/discord-bridge.py and the orphaned presenter-mode comment are gone (834a46865, 75dca0d77, 430c0200e). mergeable is now MERGEABLE, CI is green and license/cla is success. Both remaining review threads (transcription offload, the limit=200 cap) are outdated and fixed in code.
[Blocking — inadequate_tests] tests/discord-bridge-sibling-attachments.test.py:189-200, test_sibling_history_fetch_is_time_bounded. It calls (REPO / "src" / "discord-bridge.py").read_text() and regex-matches channel\.history\(\s*limit=(\w+),\s*after=cutoff to assert the behavioral claim "the scan is time-bounded with no count cap". That's the anti-pattern in REVIEW.md lesson 14 and SECONDARY §3.3. It stays green if the call is disabled or guarded (if False and …), and it goes red on a harmless reformat. Ask: move the history fetch out of the handler into an importable async helper, e.g. _fetch_sibling_history(channel, cutoff, before), called at src/discord-bridge.py:3571. Test it with a fake channel that records its history() kwargs and serves more than 200 in-window messages. Assert the in-window attachment beyond any count is selected, and that limit=None, after=cutoff was passed. Keep a source pin only as a delegation pin (the handler calls the helper), paired with that behavior test.
Also, non-blocking but quick: the test comments at lines 195, 204, 238 and 268 cite "CR #2126 (round 4)" and similar. CLAUDE.md keeps PR and round history out of code comments, so state the constraint instead.
Live round trip (named, not blocking for me): an earlier sibling message with an attachment, then a separate @-mention, then a task that includes that exact attachment, at this head after a restart.
— Sutando (susans-MacBook-Pro)
Problem
When someone pings the bot with text but no media of their own and references another user — e.g.
@bot make the video @Alice sent— the media usually lives on that user's own earlier, un-mentioned messages. Those messages neither invoke the bot nor are replied-to, so the primary attachment loop (this message's attachments) and the reply-context loop (a replied-to parent) both miss them. The task is written blind to the source material the owner is pointing at.Real repro (2026-07-16): a friend posted a garden-app video in a guild channel, then the owner @mentioned the bot in a separate message ("make the video X requested"). The video was silently dropped — only the text arrived. (Public-link assets the friend shared worked fine; it's specifically raw Discord attachments on sibling messages that vanish.)
Fix
When the triggering mention has no own attachments and references a non-bot user, scan recent channel history (last 15 msgs / 15 min window, cap 5 attachments) for attachments posted by any referenced user and pull them in — reusing the existing
save + _safe_attachment_basename + _ref_from_attachment + vision-pushpattern with a clear provenance note ([File attached (from Alice's recent message): …]).Gated on referencing a non-bot user so a plain
@botdoesn't vacuum unrelated media; bounded by time + count so it can't over-fetch.Testing
Selection logic is extracted to a pure helper
_select_sibling_attachments(history, referenced_ids, cutoff, cap)— kept out of the async I/O so it's unit-testable without a live discord channel. Newtests/discord-bridge-sibling-attachments.test.py(9 cases): referenced-user filtering, time-window early-stop, cap, oldest-first ordering, attachmentless/empty handling, multi-user.The async download loop carries
# pragma: no cover(block-level), matching the existing primary/reply-context attachment loops — the behavioral logic under test is the pure selector.🤖 Generated with Claude Code