Skip to content

fix(acp): steer running turns with edited mentions - #6132

Merged
loganj merged 3 commits into
mainfrom
larry/pr-4741-native-lifecycle
Oct 2, 2026
Merged

loganj merged 3 commits into
mainfrom
larry/pr-4741-native-lifecycle

Conversation

@loganj

@loganj loganj commented Aug 17, 2026 •

Copy link
Copy Markdown
Collaborator

🤖

Summary

Some agents can take a new message into a turn that is already running. This is called "native steering." Before this PR, an edit that arrived during a turn always cancelled the turn and started it again. Now the edit is steered into the running turn, like any other new message. The steered edit still points at the original message and its thread, so the agent replies in the right place.

  • The steer text carries the edit's route: Edit of: …, plus the original's thread root.
  • Steer only within the same reply thread. A native steer keeps the running turn's <context>, so the reply goes where that <context> points. The queue now records each running turn's reply thread. A message is steered natively only when it replies in that same thread. Under channel sessions one session spans threads, so an edit for another thread (or another top-level message) takes the cancel-and-restart path. Its re-prompt carries the message's own full <context>, with channel, project, and profile data. The same rule applies to ordinary steered messages, which had the same gap before this PR.
  • Per-thread sessions: the edit steers only a turn running in the same session as its original message. An edit in thread A is never added to a turn running in thread B. If no turn is running in that session, the edit waits in the queue as its own item.
  • Channel sessions (the default): the channel has one session. A running turn receives the edit natively only when the edit replies in that turn's thread.
  • If native steering is not available, or the transport fails or the agent rejects the steer, the existing cancel-and-restart fallback runs. If the agent returns another error, or the turn has already finished, the edit goes back to the queue for normal delivery. In every case the queued edit keeps its route.

Details

  • fix(acp): preserve routing for edited messages #4741 looks up the original message once, before the edit is queued. This PR passes that stored route to the existing steer path, and uses the same session the edit was queued under. No new lookup or preparation step is needed.
  • Removal, re-add, and fallback all use the existing lifecycle for withheld steers.

Stack

3 of 3. #6131 is merged; this PR is now based on main.

Related issue

Part of #2540 (fixed by #6131). This PR adds the running-turn behavior.

Testing

  • cargo test -p buzz-acp --all-targets passes. The one test that fails when the git setting nostr.keyfile is set is described in fix(acp): preserve routing for edited messages #4741.
  • Tests check that the steer text carries the original's route, and that the listener gives the steer path the same route and session it queues.
  • End-to-end tests send a routed edit through the listener's steer decision under channel sessions. An edit in the running turn's thread, and an edit of the running top-level message, steer natively with no cancel. An edit for thread B during a thread-A turn, and an edit of another top-level message, send no native steer and take cancel+merge. Removing the same-thread check makes both cross-thread tests fail.

@loganj
loganj marked this pull request as ready for review August 19, 2026 11:12
@loganj
loganj requested a review from a team as a code owner August 19, 2026 11:12
@loganj
loganj force-pushed the larry/pr-4741-activation branch 3 times, most recently from 9dd506f to ff25d69 Compare August 19, 2026 14:13
@loganj
loganj force-pushed the larry/pr-4741-native-lifecycle branch 2 times, most recently from 9e2b81a to b6a5612 Compare August 19, 2026 14:14
@loganj
loganj force-pushed the larry/pr-4741-activation branch from ff25d69 to 4a6fe5c Compare August 19, 2026 14:45
@loganj
loganj force-pushed the larry/pr-4741-native-lifecycle branch from b6a5612 to d333f64 Compare August 19, 2026 14:45
@loganj
loganj force-pushed the larry/pr-4741-activation branch from 4a6fe5c to 681ae2c Compare August 19, 2026 15:07
@loganj
loganj force-pushed the larry/pr-4741-native-lifecycle branch from d333f64 to 82e0415 Compare August 19, 2026 15:07

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: APPROVE

Reviewed 681ae2c4d34dccf1adb9fb82908fd9eb88fcf3d1..82e041561d5a5cf04b54ec983fd208202c8ae2f6 at exact head 82e041561d5a5cf04b54ec983fd208202c8ae2f6.

Risk: high — this changes messaging correctness and the ACP agent-runtime lifecycle across asynchronous preparation, live-turn steering, acknowledgements, membership changes, and fallback delivery.

Findings: no blocking or non-blocking defect found.

Behavior/contracts traced:

  • Prepared edits carry membership generation, originating turn ID, and the exact queue reservation; all three must still match before native steering is sent (crates/buzz-acp/src/lib.rs:1550-1582,3504-3555). Mutation-removing the turn-ID fence caused prepared_edit_is_stale_after_replacement_turn_starts to fail, then the tree was restored clean.
  • Membership removal/re-add drains or invalidates old work, advances generation, and prevents stale acknowledgements from mutating queue state, reactions, deadlines, fallback, or the delivery ledger (crates/buzz-acp/src/lib.rs:1614-1631,2745-2862,3585-3597,4445-4482; crates/buzz-acp/src/queue.rs:764-812).
  • Result/ack ordering is loss-averse: a reservation is marked sent before watcher spawn, completion remains fenced until the sent acknowledgement settles, and failure/unknown outcomes release to ordinary delivery or cancel-and-merge rather than dropping the edit (crates/buzz-acp/src/lib.rs:3602-3750,4150-4193; crates/buzz-acp/src/queue.rs:440-469,932-1012).
  • Edit enrichment resolves the signed original and thread before formatting; reactions and failure notices remain attached to the original visible message rather than exposing the auxiliary edit as a reply anchor (crates/buzz-acp/src/pool.rs:3470-3506,3526-3591; crates/buzz-acp/src/queue.rs:1704-1724; crates/buzz-acp/src/lib.rs:1634-1649,3721-3727).
  • The exact diff is confined to buzz-acp Rust and does not alter Desktop rendering, focus, keyboard, or accessibility surfaces.

Validation at exact clean head:

  • cargo test -p buzz-acp --all-targets — PASS: 856 library tests plus 9 lifecycle integration tests, 0 failures.
  • cargo fmt --all -- --check — PASS.
  • cargo clippy -p buzz-acp --all-targets -- -D warnings — PASS.
  • git diff --check — PASS.
  • Targeted mutation of the replacement-turn fence — expected regression test failure; restored tree clean at the reviewed head.
  • Fresh GitHub state: head remains pinned, merge state CLEAN, 26 checks successful and 4 intentionally skipped.

Manual/native evidence: no real Goose, Claude, or Codex process was observed accepting an edited mention end to end through relay enrichment, native steer, visible reply, and reaction cleanup. Deterministic transport/lifecycle tests cover the changed seams, but do not prove real-adapter timing.

Residual risk: acp.rs retains an existing reader-first biased select that can defer a ready steer while stdout remains continuously ready; its pre-select deadline check bounds the delay, but fairness was not live-probed. That mechanism is outside this PR's exact diff, so it is a pre-existing follow-up risk rather than author rework.

Author action: none.

Verification owner: reviewer/tooling for optional live-adapter observation and a separate fairness probe; CI/release gate for normal post-merge artifact validation.

@loganj
loganj force-pushed the larry/pr-4741-activation branch from 681ae2c to a168da6 Compare September 30, 2026 16:55
@loganj
loganj force-pushed the larry/pr-4741-native-lifecycle branch from 82e0415 to 52d1981 Compare September 30, 2026 16:55
@loganj loganj changed the title fix(acp): steer edited mentions with lifecycle fencing fix(acp): steer running turns with edited mentions Sep 30, 2026
@loganj
loganj force-pushed the larry/pr-4741-native-lifecycle branch from 52d1981 to d9a0d57 Compare September 30, 2026 17:34

@wpfleger96 wpfleger96 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🤖 thanks for the rewrite, this is a lot smaller now that it reuses #4741's route. combined review at d9a0d573: two independent source passes plus a live run against a local relay with signed events and a scripted ACP peer on both steer transports (_session/steering and Goose's run-ID method).

no runtime defect in the incremental change. edits really do take the native path live, keep the original's route through success, delayed acks, and fallback, and a steer only ever lands in the original's own session (idle threads queue, busy siblings aren't touched, a failed steer cancels only its own thread).

one blocker, inline: the tests don't pin either behavior this PR adds. restoring the old "edits never steer" rule, or dropping the route right at the send, both leave the full buzz-acp suite green.

inherited from earlier in the stack, not new here:

  • when the running turn is itself an edit and native steer isn't available, the #4741 interrupt bug still applies (old edit re-runs before the correction). native success avoids that path but doesn't fix it.
  • a message delivered by a successful steer keeps its 👀, since reaction cleanup only covers the turn's original batch. for edits that 👀 sits on the original. this predates the stack.

nonblocking, description only: "if native steering fails, the cancel-and-restart fallback runs" is broader than what the code does. unsupported, transport, and rejected outcomes cancel and restart, but other agent errors and turn-completed outcomes just requeue the edit for normal delivery. that split is existing behavior, so this is just wording.

Comment thread crates/buzz-acp/src/lib.rs
@loganj
loganj force-pushed the larry/pr-4741-activation branch from 46fc851 to 70f4d4b Compare September 30, 2026 20:41
@loganj
loganj force-pushed the larry/pr-4741-native-lifecycle branch from d9a0d57 to b28022b Compare September 30, 2026 20:41
@loganj

loganj commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator Author

On the inherited interrupt issue in your review: #4741 now fixes it (e079ba6). An interrupted edit merges into the next turn as earlier work, the new request gets the supersede framing, and the old edit does not run again. This PR's head b5d201a includes that fix. The 👀 cleanup gap for steered messages predates the stack; I left it out of scope.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

:bot: Jude’s code review agent — APPROVE exact head b5d201a2a811a4cdfe9721b99bead37e2b975bf2 against base 70f4d4b3c0e9437bdca695db7fff5223294878c1.

No author-actionable defect remains.

The refreshed implementation preserves one routing authority: after normal authorization/subscription admission, the signed original is fetched under channel/message-kind constraints and verified, and the resulting ResolvedEdit travels in the same queued BatchEvent used by native steering (crates/buzz-acp/src/lib.rs:604-739,3503-3576; edit_routing.rs:23-93). Scope selection is derived from that verified route, so per-thread sessions cannot steer/cancel another thread, while channel-scoped sessions retain their intended behavior (scope.rs:135-159; lib.rs:4252-4386; pool.rs:1146-1162).

Failure handling remains loss-averse: the queued edit is synchronously withheld before the acknowledgement watcher; success records delivery and removes it, while unsupported/rejected/transport/application/completion/drop/timeout paths release it for cancel+merge or ordinary dispatch (lib.rs:3819-3975,4341-4376; queue.rs:921-1050). The native steer body reuses ordinary event formatting, retaining Edit of: <original> and the verified original thread root (lib.rs:4389-4399; queue.rs:1402-1489).

The replacement regression reaches the production decision seam: it establishes a running scope, submits a routed edit, invokes steer_or_interrupt, and asserts the actual SteerRequest carries the original ID and thread root without cancellation (lib.rs:9535-9616). This is causally coupled: restoring the old edit exclusion prevents the request, and dropping the resolved route breaks the root assertion.

Validation at clean exact head:

  • Exact-head CI: 50 successful, 23 intentional skips, including Rust lint/unit/Windows, relay/backend/PostgreSQL/Desktop integration and builds, security, and DCO.
  • cargo fmt --all -- --check: pass.
  • git diff --check 70f4d4b3...b5d201a2: pass.
  • Diff audit: no production unwrap/expect, unsafe, schema/migration, IPC, Desktop, or packaging changes.
  • Local package tests/clippy could not execute because the reviewer host linker exits 69 with the Xcode license unaccepted. Exact-head CI owns those required gates and is green; this is not author rework.

Residual confidence gap: no fresh real Goose/Codex/Claude edited-mention run was observed on this head. Optional live-adapter observation belongs to reviewer/tooling. Successful native steers may also retain the admission-time reaction; source tracing shows this behavior predates the PR and applies to ordinary native steers, so it is a separate follow-up rather than required scope expansion here.

Author action: none.
Verification owner: reviewer/tooling for optional live-adapter observation and inherited reaction cleanup; CI/release gates for normal artifact validation.

loganj added a commit that referenced this pull request Oct 1, 2026
🤖
## Summary
When someone edits a message to add an @mention, the app does not change
the original message. It publishes a separate hidden "edit" event that
points back at it. An agent that handles the edit must work on the
**original message**: it reacts, replies, and posts any error notice on
that message or in its thread. Before this PR, an agent treated the
hidden edit as a new message. It replied in the wrong place, and with
per-thread sessions it started a new, empty conversation.

This is the first of three PRs. It sets where agents reply to edits. It
does not yet make edits wake agents by default (#6131 does that).

- The agent looks up the original message once, when it accepts the
edit. Every later step uses that result: the session, reactions, typing
indicator, reply target, and failure notices.
- **Per-thread sessions** (the `thread` session policy, where each
thread has its own agent conversation): the edit joins the original
message's thread session. It does not start a new session.
  - The original is a reply in a thread: the edit joins that thread.
- The original is a top-level message: the edit joins the thread that
starts at the original.
- **Channel sessions** (the default policy) and DMs: nothing changes.
The whole channel shares one conversation.
- Setup mode (the reply that asks the owner to finish configuring the
agent) now accepts edits. It replies at the original message or its
thread. If the original could not be fetched, the reply posts at the top
of the channel.
- Error notices for an edit (sign-in failure, hard timeout, retries used
up) now appear in the original's thread, or as a reply to a top-level
original. Before, they appeared at the top of the channel. If the
original could not be fetched when the edit was accepted, the notice
looks it up once more. If it is still unknown, the notice posts at the
top of the channel.
- If a newer request interrupts a turn that is working on an edit, the
next turn treats the edit as earlier work that the new request replaces.
The old edit does not run again.

## Details
- The lookup has a 2-second limit. It runs only for edits that already
matched a subscription rule (by default, edits that newly mention this
agent). It must finish before the edit is queued, because the session is
chosen at that point.
- If the lookup fails, the agent still targets the edited message's ID,
never the hidden edit event. Notices and setup replies never claim that
message as a thread root, because the relay rejects a root that does not
match the message's real thread. **Known gap:** if that message is a
reply in a thread, the agent cannot learn the thread root. So under
per-thread sessions it uses a separate session rooted at that reply, and
it loses the thread's earlier context. Its reply still goes to the right
message. Closing this gap needs a second lookup, which is left out on
purpose.
- The original is fetched with an explicit kind (stream message or its
edit) and a channel filter, because the relay rejects lookups without a
kind.
- Each queued event carries its own route, so an edit shares a batch
with other events like any message. Batch order is unchanged from
`main`.

### Stack
**1 of 3**, based on `main`. Next: #6131 (edits wake agents by default),
then #6132 (edits join a turn that is already running).

### Related issue
Part of #2540. The issue is fixed when #6131 merges.

### Testing
- `cargo test -p buzz-acp --all-targets` passes. One existing test,
`session_new_forwards_complete_git_block_without_duplicate_names`, fails
in a shell where the git setting `nostr.keyfile` is set. It passes when
the setting is removed. This PR does not touch that test.
- Session tests: an edit uses the original's thread for a threaded
original, a top-level original, and an original that could not be
fetched. It never uses the edit event.
- Tests check the signed events that the agent publishes: setup-mode
replies and failure notices, each for a threaded original, a top-level
original, and an original that could not be fetched (including a notice
whose second lookup finds the original). If either routing fix is
reverted, these tests fail.
- A queue-to-prompt test interrupts a turn on an edit with a newer
request. The next prompt has the edit as earlier work and the new
request with the "replaces earlier work" framing, and nothing is left to
run again.

---------

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
@loganj
loganj force-pushed the larry/pr-4741-activation branch from 70f4d4b to 06d5a51 Compare October 1, 2026 16:04
@loganj
loganj force-pushed the larry/pr-4741-native-lifecycle branch from b5d201a to 8aa6164 Compare October 1, 2026 16:07

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

:bot: Jude’s code review agent — blocking review finding at exact head 8aa61647f318bd4286132248420f00c99d81ca44 against base 06d5a51289f7495d22b93a723b68fc92d59fc1f3.

Major — the steered edit can retain the running turn's old authoritative reply destination

Under the default channel session policy, a running turn from thread A can accept an edited mention whose verified original belongs to thread B. The new native-steer payload includes B's event metadata, but it does not replace the live turn's authoritative <context> routing metadata:

  • Normal dispatch derives thread_tags, routing_event_id, and the human-aware reply_anchor, then emits them through format_context_hints (crates/buzz-acp/src/queue.rs:2108-2122, 2148-2180).
  • The base prompt explicitly tells the agent that <context> is authoritative for routing and the reply destination (crates/buzz-acp/src/base_prompt.md:4-6, 56-64).
  • Native steering emits only <new-message-arrived-while-you-were-working> plus a <buzz-event> (crates/buzz-acp/src/lib.rs:4317-4333, 4400-4410). Despite the comment claiming the edit is “anchored exactly as a dispatched one would be,” the payload contains no replacement <context> and therefore cannot supersede thread A's authoritative reply destination.

Failure scenario: a channel-scoped turn starts from human thread A; an edited mention for an original in human thread B is natively steered into it. The model sees B's root in supporting event metadata while its authoritative routing context still points to A, so following the harness contract can post the response in A. That violates this PR's central promise to preserve the edited original's thread route.

Required fix: make the native-steer delta carry authoritative routing/reply-destination context derived by the same logic as normal dispatch. Add a production-seam regression with a running thread-A turn and a thread-B edited original under SessionPolicy::Channel; assert the effective reply destination is B and A is absent. Mutation-prove that removing the routing update makes the test fail.

Integrated evidence and verification

The systems lane found no loss, stale-session, or cross-scope lifecycle defect: exact SessionScope task selection, capacity-one steering, withheld-event acknowledgement/release, deadline recovery, cancel+merge fallback, and delivery persistence remain coherent. That does not resolve the product lane's distinct prompt-contract defect: transport can safely deliver an event to the correct channel session while leaving that session's reply anchor stale.

Exact-head CI reports 50 successful and 23 skipped checks. Local package test and clippy attempts were blocked by reviewer tooling (cc exit 69 from an unaccepted Xcode license), not by this PR. No Desktop rendering/accessibility surface changed. A real adapter journey was not run.

Author action: update authoritative routed context and add the causal cross-thread regression above.

Verification owner: author for the fix and mutation-proven test; reviewer for exact-head lifecycle/fallback re-review and optional live adapter observation.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

:bot: Jude’s code review agent

Verdict: REQUEST CHANGES
Reviewed: 06d5a51289f7495d22b93a723b68fc92d59fc1f3..8aa61647f318bd4286132248420f00c99d81ca44 (exact head 8aa61647f318bd4286132248420f00c99d81ca44)
Risk: high — cross-thread agent reply routing during an in-flight channel-scoped turn.

Behavior/contracts traced: edited-event identity and original-route resolution; SessionScope task selection; native-steer payload construction; withheld-event acknowledgement/release; exact-scope cancel+merge fallback; reply/reaction anchoring; ACP-session delivery lifecycle.

Finding — blocking: a native-steered edit does not replace the running turn’s authoritative reply destination. Under the default channel session policy, an edit targeting thread B may steer a turn originally started from thread A (crates/buzz-acp/src/lib.rs:3535-3575; the new test uses SessionPolicy::Channel at lib.rs:9850-9884). Normal dispatch computes the routed reply anchor and writes it into <context> (crates/buzz-acp/src/queue.rs:2108-2122,2148-2180), and the base prompt declares that context authoritative (crates/buzz-acp/src/base_prompt.md:6,58-64). Native steer sends only a framed <buzz-event> delta, omitting an updated <context> (crates/buzz-acp/src/lib.rs:4317-4333,4400-4409). The new test checks Edit of: and root= text but not the effective reply destination (lib.rs:9896-9904). Consequently, following the harness contract can send the response for B into A.

Author action: make the native-steer delta carry the authoritative routed reply destination for the edited original—preferably using the same anchor/context computation as normal dispatch—and add a production-seam regression for running thread A plus edited original in thread B under channel policy. Assert the effective --reply-to is B and A is absent; mutation-prove that dropping the routing update fails.

Verification owner: author for code and causal regression; reviewer/tooling for exact-head lifecycle/fallback re-review and optional live adapter observation.

Validation: live GitHub head/base and authenticated identity rechecked immediately before delivery; merge state CLEAN; exact-head CI reports 50 successful / 23 intentionally skipped checks; both lanes report clean git diff --check. Local package test/clippy were blocked before PR-code compilation by this review host’s unaccepted Xcode license (exit 69), a non-author tooling gap. CI success does not resolve the routing contradiction.

Residual risk: no real Goose/Codex/Claude native-steer journey was observed at this head. No Desktop/UI files changed, so visual/accessibility evidence is not applicable.

loganj added a commit that referenced this pull request Oct 2, 2026
🤖
## Summary
Today an agent does not wake up when someone edits an existing message
to @mention it. With this PR, adding the mention in an edit is enough to
reach the agent. The agent then works on the original message, as #4741
set up.

- Agents now receive message edits by default. Before, they subscribed
only to new messages (and a few other event types).
- The desktop app puts a mention tag on an edit only for people whom
that edit newly mentions. So an agent wakes once, for the new mention.
It does not wake again when the same message is edited later. This PR
also fixes one case in the desktop app: when the original also mentioned
someone whose profile was not loaded, every mention counted as new, so a
typo fix woke the agent again. Now the recipients that the app knows
from the original always count as already notified.
- This default applies in normal mode, to channels the agent joins while
it is running, and in setup mode.
- If you give an explicit `--kinds` list, it is used without change.
- Sessions do not change in this PR. An edit uses the session that #4741
chooses: with per-thread sessions, the original message's thread; with
channel sessions, the channel.

## Details
- The change adds the edit event kind (40003) to one shared default
list. The startup rules, the relay subscription filters, and the setup
rules all use that list.
- An agent accepts an event only if both the relay subscription and its
local rules allow it. The normal listener's startup rules now come from
one function, `startup_subscription_rules`. The harness and the tests
both call it, so the tests check the rules that actually run.

### Stack
**2 of 3**, based on #4741. Next: #6132.

### Related issue
Fixes #2540.

### Testing
- `cargo test -p buzz-acp --all-targets` passes. The one test that fails
when the git setting `nostr.keyfile` is set is described in #4741.
- Admission tests run on the rules the harness installs, for the normal
listener and for setup mode. An edit that mentions the agent is
accepted. An edit that does not mention it is rejected.
- I removed kind 40003 from each of the two defaults, one at a time.
Each time, these tests failed.
- Config tests check that edits are in the default kinds for startup
channels and for channels joined later.
- Desktop regression: `@Agent @missing User please chek` edited to
`check`, with only the agent's profile loaded, sends no new
notification. It fails on the old code.

---------

Signed-off-by: npub1em3jmyn4vu57urqf03txrwreccvejvwdy5c4er8nnrwt7rc4tncscs3ssu <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz>
Signed-off-by: Larry <8cf5a83f590ec0955b11647d1c88f796a98e088c30a492c58e0e46c3026ae7a4@buzz.block.builderlab.xyz>
Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Co-authored-by: npub1em3jmyn4vu57urqf03txrwreccvejvwdy5c4er8nnrwt7rc4tncscs3ssu <cee32d92756729ee0c097c5661b879c6199931cd25315c8cf398dcbf0f155cf1@buzz.block.builderlab.xyz>
Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Base automatically changed from larry/pr-4741-activation to main October 2, 2026 13:50
@loganj

loganj commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Thanks, the finding was correct. Fixed in b8cd11c59.

  • The native steer delta now carries its own <context> section. It is built by the same function as a normal prompt's <context> (queue::format_routed_context), so a steered edit routes to its original's thread with the same reply-anchor rule. The steer has no profiles, so the sender counts as human and always gets an anchor. That is the existing fail-open rule.
  • base_prompt.md now says that a message that arrives during work can carry its own <context>, and the newest one replaces the earlier one.
  • The fix is in the shared steer body, so it also covers steered ordinary messages from another thread under channel sessions. That gap existed before this PR.
  • routed_edit_steers_running_turn_with_original_route now runs a thread-A turn under SessionPolicy::Channel, steers an edit whose original is in thread B, and checks that the steer's <context> has Thread root: B and --reply-to B, and that A's id does not appear. When I removed {context} from the steer body, this test failed. I then restored it.

Local: cargo fmt --check and cargo clippy -p buzz-acp --all-targets -D warnings pass. The steer, queue and edit tests pass. Some acp::tests timing tests and the nostr.keyfile test fail on this machine with or without this change, so they are a machine issue. CI runs the full suite.

Larry added 2 commits October 2, 2026 10:12
An edit that arrives during an agent turn now uses the native steer
path like any other event. The steer carries the edit's resolved
original-message route, so the steered turn replies and reacts in the
original's thread instead of treating the edit as a new top-level
message.

The route is resolved at admission, so native steer needs no separate
preparation, reservation or membership fence. Fallback, cancellation
and channel removal reuse the existing withheld-steer lifecycle, which
already keeps the queued edit and its route together.

Replaces the earlier 22-commit native-lifecycle slice (old head
82e0415), which resolved edits asynchronously after admission.

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
The new test pushes an admitted edit into a running scope and calls the
listener's steer decision. It checks that the sent steer request carries
the original's route and that the running turn gets no cancel signal.
Restoring the old edit gate, or dropping the route at the send, fails it.

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
@loganj
loganj force-pushed the larry/pr-4741-native-lifecycle branch from b8cd11c to ee30c0f Compare October 2, 2026 14:13
@loganj
loganj deployed to codex-review October 2, 2026 14:13 — with GitHub Actions Active

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

:bot: Jude’s code review agent — blocking review finding at exact head ee30c0f5e883218df0431e0a27c576609a1143e8 against base 47654215ebe44829941e6d7edc635ef401e8d8e8.

The prior thread-A → thread-B reply-anchor blocker is fixed: the production steering seam now emits B's <context>, the base contract says the newest context wins, and the regression asserts B's reply target while excluding A (crates/buzz-acp/src/lib.rs:669-677,4330-4438,10028-10123; crates/buzz-acp/src/base_prompt.md:6).

Major — the replacement steer context is lossy

The new authoritative context corrects the reply anchor by silently deleting other authoritative routing and project metadata.

Normal dispatch and native steering call the same format_routed_context helper, but with materially different inputs. Normal dispatch supplies channel metadata and the fetched profile lookup (crates/buzz-acp/src/queue.rs:2162-2174). Native steering supplies channel_info=None, ConversationContextStatus::Absent, and profile_lookup=None (crates/buzz-acp/src/queue.rs:2309-2376; crates/buzz-acp/src/lib.rs:4424-4438). Because base_prompt.md:6 defines the newest <context> as replacing the earlier one, omitted fields are erased rather than inherited.

This breaks two authoritative contracts:

  1. Project/channel authority is erased. channel_info=None drops the channel name/description and project-home identity, default-repository routing, and duplicate-project guard emitted by append_project_home (queue.rs:1694-1728). The base prompt relies on these fields to keep project work bound correctly and prevent duplicate project creation (base_prompt.md:22-31). A successful steer in a project home can therefore replace a valid project context with UUID-and-reply-route-only context immediately before a request to create project work.
  2. Agent-only reply topology changes. Normal dispatch uses profile data to identify agent senders/mentions and intentionally avoids a forced --reply-to for agent-only coordination (queue.rs:1540-1590; base_prompt.md:62). With profile_lookup=None, native steer treats the unknown sender as human and emits a root anchor. Installing that as the newest context can flatten intentional agent-to-agent nesting.

The shared helper is syntactically reused, but its load-bearing inputs are not. The A→B test has neither project metadata nor profiles, so it cannot catch either divergence.

Required fix: make replacement semantics field-safe. Preserve the full authoritative non-routing channel/project metadata and the same identity classification or pre-resolved reply-anchor decision used by normal dispatch, or explicitly define a routing-only context delta whose omissions do not invalidate unchanged fields. Add production-seam regressions for (a) project-home steering retaining project/default-repository/duplicate-project protections while routing to B and (b) agent-only steering retaining no forced human root anchor. Mutation-prove the native inputs/call site.

Integrated validation

Both assigned lanes independently identified the same lossy-context defect. Systems review found the prior A→B fix and lifecycle/fallback ownership coherent; no separate loss, stale-session, acknowledgement, persistence, or release defect was found. Product review confirmed no Desktop rendering/accessibility surface changed.

git diff --check passed. Local cargo test -p buzz-acp --all-targets -- --test-threads=1 could not compile PR code because the reviewer host has not accepted the Xcode license (cc exit 69); that is a tooling confidence gap, not author rework. Exact-head CI was still running at review time with no completed failure observed. No real Goose/Codex/Claude adapter journey was run.

Author action: preserve authoritative metadata/identity semantics and add the causal regressions above.

Verification owner: author for implementation and tests; reviewer for changed-head lifecycle/fallback re-review, exact-head gates, and optional live-adapter observation.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

:bot: Jude’s code review agent

Verdict: REQUEST CHANGES
Reviewed: 47654215ebe44829941e6d7edc635ef401e8d8e8..ee30c0f5e883218df0431e0a27c576609a1143e8 (exact head ee30c0f5e883218df0431e0a27c576609a1143e8)
Risk: high — authoritative routing and project ownership metadata are replaced during native steering.

The prior thread-A → thread-B blocker is fixed: the steer now emits B’s context and the regression excludes A. Both assigned lanes independently found a new blocking defect in the replacement semantics.

Blocking finding: normal dispatch supplies channel/project metadata and profile lookup to format_routed_context (crates/buzz-acp/src/queue.rs:2162-2174), while native steer supplies channel_info=None, absent conversation context, and profile_lookup=None (queue.rs:2309-2376; crates/buzz-acp/src/lib.rs:4424-4438). Because base_prompt.md:6 says the newest <context> replaces the old one, this erases project-home/default-repository/duplicate-project protections (queue.rs:1694-1728; base_prompt.md:22-31) and can change agent-only deep nesting into a forced human root anchor (queue.rs:1540-1590; base_prompt.md:62). The A→B regression has no project metadata or profiles, so it cannot catch either divergence.

Author action: make replacement semantics field-safe: preserve the authoritative channel/project metadata and normal identity/reply-anchor classification, or define a routing-only delta whose omissions do not invalidate unchanged fields. Add production-seam regressions for (a) project-home steering retaining project/default-repository/duplicate-project protections while routing to B and (b) agent-only steering retaining no forced human root anchor. Mutation-prove the native inputs/call site.

Verification owner: author for implementation and causal tests; reviewer/tooling for changed-head lifecycle/fallback re-review, exact-head gates, and optional live-adapter observation.

Validation: both lanes traced the production seam and independently reproduced the source-contract defect; git diff --check passed; live head/base and authenticated identity were rechecked. Local cargo test -p buzz-acp --all-targets -- --test-threads=1 was blocked before PR-code compilation by the review host’s unaccepted Xcode license (exit 69), a non-author tooling gap. Exact-head CI remains in progress with no completed failure currently observed.

Residual risk: no real Goose/Codex/Claude adapter journey was run; no Desktop rendering/accessibility surface changed.

@github-actions github-actions Bot added the codex-security-review-current The posted Codex security review matches its recorded range. label Oct 2, 2026
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

🔐 Codex Security Review

Status: review required for the current range.

The current range is 4f51b9e1010e086a16c099cd8d8218ca974a5e18...a41e15c2ad4ecbb75d4f493ca964ddf8fa54ec3f.
A new review must complete for this exact range. When manual authorization
is required, a user with write access must comment exactly
@buzz-security-review a41e15c2ad4ecbb75d4f493ca964ddf8fa54ec3f to authorize a new review.
Any previous review applies only to its recorded range.

A native steer adds a message to a running turn without a new <context>,
so the turn keeps replying where its own <context> points. Under the
channel session policy one session spans threads, so a message for
another thread (an edit of a thread-B message during a thread-A turn)
would get its reply in thread A.

Record each in-flight turn's reply thread in the queue and steer natively
only a message in that thread. Any other message takes the cancel+merge
path, whose re-prompt carries the message's own full <context> with
channel, project, and profile metadata. The reply thread uses the same
rule as the thread-session key.

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
@loganj
loganj force-pushed the larry/pr-4741-native-lifecycle branch from ee30c0f to a41e15c Compare October 2, 2026 14:51
@github-actions github-actions Bot removed the codex-security-review-current The posted Codex security review matches its recorded range. label Oct 2, 2026
@loganj

loganj commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Thanks. Both findings were correct. I replaced the approach in a41e15c2a.

Lossy replacement <context> (Jude, and Codex MEDIUM): I removed the steer's <context> and the "newest <context> wins" sentence in base_prompt.md. The steer can't rebuild a full <context> without channel, project, and profile data. So now a message is steered natively only when it replies in the running turn's own thread:

  • The queue records the reply thread of each running turn, from the last event in its batch. That event's <context> routes the turn. The record is cleared on complete and on expiry.
  • steer_or_interrupt steers natively only when the new message has the same reply thread. Otherwise it takes the existing cancel+merge path. That re-prompt runs through format_prompt with full channel and project data, profiles, and the agent-only anchor rule. Native steers therefore never replace or remove <context> fields.
  • reply_thread is the thread-session key SessionScope::derive_routed already used. Thread sessions do not change. Under channel sessions, only same-thread messages steer natively.

Codex HIGH (forged <context> in event text): this PR no longer adds the precedence rule that made a later <context> take over. Raw event content inside semantic sections is not escaped. That existed before this PR and also affects normal prompts, so it needs its own follow-up and is not fixed here.

Tests (in the listener's steer decision, under SessionPolicy::Channel): an edit in the running thread steers natively with no cancel. An edit of the running top-level message steers natively. An edit for thread B during a thread-A turn takes cancel+merge and sends no native steer. An edit of another top-level message does the same. Removing the same-thread check makes both cross-thread tests fail.

Local: fmt, clippy -D warnings, and cargo test -p buzz-acp --lib pass. The only failures are the 6 acp::tests timing tests, which also fail on this machine without the change.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

:bot: Jude’s code review agent — APPROVE at exact head a41e15c2ad4ecbb75d4f493ca964ddf8fa54ec3f against base 448407a972ca9da0c1e13d49ee2c2170821be8a2.

Verdict

No author-actionable defect remains. Both prior blockers are resolved by refusing native steering across reply-thread boundaries rather than replacing the running turn's authoritative <context>.

  • The running turn records reply-thread identity from the same final BatchEvent normal prompt dispatch uses for routing (crates/buzz-acp/src/queue.rs:495-535,2175-2248). Edits use their verified original route; top-level identity falls back to the visible original/trigger (queue.rs:1173-1241; scope.rs:135-156).
  • Native steer now requires exact SessionScope ownership and equal reply-thread identity (crates/buzz-acp/src/lib.rs:652-683). Thread A → B and distinct top-level A → B therefore take exact-scope cancellation and cancel+merge redispatch, where normal formatting rebuilds full channel/project/profile context (lib.rs:4275-4305,5063-5086; queue.rs:2175-2307).
  • Same-thread edits keep the admitted original route in the queued and steered BatchEvent, so the delta can identify the edited original without replacing authoritative context (lib.rs:687-748,4332-4435). This removes the lossy project/profile replacement introduced by the previous head.
  • Reply-thread state is installed for each flush and cleared on completion/expiry (queue.rs:414-535,556-586,768-795). Native-steer withholding, acknowledgement, failure release, and fallback remain loss-averse and scope-exact (lib.rs:3842-3998,4308-4421).
  • Production-seam regressions cover same-thread steering, the running top-level trigger, cross-thread cancel+merge, and distinct-top-level cancel+merge (lib.rs:10020-10149). Removing the same-thread guard necessarily routes both mismatch cases to the native request instead of their asserted cancellation signal, making the tests causally coupled at source level.

Validation and residual risk

  • PASS: cargo fmt --all -- --check.
  • PASS: git diff --check 448407a...a41e15c; both lane worktrees were clean at the pinned head.
  • PASS: changed-range policy audit found no production unwrap/expect, unsafe, schema/persistence, Desktop/UI, packaging, migration, binary, or public-API documentation issue.
  • Exact-head GitHub snapshot at submission: 39 successful, 26 intentionally skipped, 7 pending, no completed failures. Pending CI remains the terminal gate.
  • Local package tests and clippy could not compile PR code because the reviewer host has not accepted the Xcode license (cc exit 69). A clean Linux mutation run was still executing at lane close, so executable mutation proof remains a reviewer/tooling confidence gap rather than author rework.
  • No live Goose/Codex/Claude edited-mention adapter journey was observed. No Desktop rendering/accessibility surface changed.

Author action: none.

Verification owner: CI/integration owner for exact-head terminal gates; reviewer/tooling for completing the same-thread-guard mutation and optional live-adapter observation. Any red mutation or CI result invalidates this approval.

@jedwards27 jedwards27 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

:bot: Jude’s code review agent

Verdict: APPROVE
Reviewed: 448407a972ca9da0c1e13d49ee2c2170821be8a2..a41e15c2ad4ecbb75d4f493ca964ddf8fa54ec3f (exact head a41e15c2ad4ecbb75d4f493ca964ddf8fa54ec3f)
Risk: high — edited-message native steering, cross-thread routing, cancellation, and delivery lifecycle.

Behavior/contracts traced: running reply-thread identity; same-thread and top-level matching; exact SessionScope ownership; native-steer admission/withholding/acknowledgement; cross-thread cancel+merge fallback; full channel/project/profile context reconstruction; completion/expiry cleanup.

Findings: no unresolved author-actionable defect. Both prior blockers are resolved without lossy context replacement: native steering now requires the running turn’s exact reply-thread identity; thread A → B and distinct top-level work take scope-exact cancel+merge, after which normal dispatch rebuilds full authoritative context. Same-thread edits retain verified original routing. Production-seam regressions cover same-thread, same-top-level, cross-thread, and distinct-top-level behavior.

Author action: none.

Verification owner: CI/integration owner for terminal exact-head gates; reviewer/tooling for completing the same-thread-gate mutation receipt and optional real-adapter journey.

Validation: both assigned lanes independently traced the refreshed production seam; cargo fmt --all -- --check and git diff --check passed on clean exact-head trees; changed-range policy audit found no production unwrap/expect, unsafe, schema, persistence, Desktop, or packaging change. Current exact-head checks: 39 successful, 26 intentional skips, 7 still pending, no completed failure. Local package suite/clippy were blocked before PR-code compilation by the review host’s unaccepted Xcode license (exit 69), which is reviewer tooling rather than author rework.

Manual/native evidence: no Desktop/UI surface changed; no real Goose/Codex/Claude adapter journey was observed.

Residual risk: executable mutation proof and live adapter timing remain unobserved. If a required exact-head gate fails, this approval no longer establishes merge readiness; the named gate owns that outcome.

@jedwards27

Copy link
Copy Markdown
Contributor

:bot: Jude’s code review agent — verification follow-up for approved exact head a41e15c2ad4ecbb75d4f493ca964ddf8fa54ec3f.

The outstanding same-reply-thread mutation proof completed successfully on isolated Linux:

  • Baseline library suite: 1029 passed, 0 failed, 3 ignored.
  • Removing only the same_reply_thread native-steer gate: 1027 passed, 2 failed, 3 ignored.
  • The two expected failures were routed_edit_for_another_thread_cancels_and_merges and routed_edit_for_another_top_level_message_cancels_and_merges; both failed specifically because native steering crossed the forbidden reply-thread boundary.
  • The mutation was restored; final HEAD remained the approved SHA and the worktree was clean.

This is behavioral causal evidence for the guard and strengthens the approval. The later --all-targets baseline encountered two unrelated environment/config failures in git_bootstrap when https://relay.invalid/... attempted terminal-disabled credentials; the changed library seam had already passed cleanly.

Author action: none. Verification owner: CI/integration for the remaining exact-head gates; reviewer/tooling only for optional live-adapter observation.

@loganj
loganj deployed to codex-review October 2, 2026 16:48 — with GitHub Actions Active
@loganj
loganj merged commit 133fb98 into main Oct 2, 2026
87 of 88 checks passed
@loganj
loganj deleted the larry/pr-4741-native-lifecycle branch October 2, 2026 17:24
wpfleger96 pushed a commit that referenced this pull request Oct 2, 2026
…del-labels

* origin/main:
  fix(acp): steer running turns with edited mentions (#6132)
  fix(mcp): support Goose discovery handshake (#8037)
  test(agent): synchronize handoff steering with tool approval (#8042)

Signed-off-by: Duncan <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
loganj pushed a commit that referenced this pull request Oct 2, 2026
#6132 allowed a native steer only when the new message replied in the
running turn's thread, keyed by thread root or, for a top-level message,
the message's own id. That is right for channels, where a top-level
mention is answered in a new thread rooted at it. A top-level DM's
<context> names no reply target, so every top-level DM message replies
in the same place. Keying each one by its own id made every mid-turn DM
follow-up cancel and restart the turn instead of steering into it.

Record each running turn's reply route (thread root and reply thread)
and decide steer compatibility in one place: DMs compare thread roots,
including none; channels keep comparing reply threads.

Signed-off-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>

This branch was successfully deployed

1 active deployment
codex-review — a41e15c2 Deployed Oct 2, 2026 by loganj via Run Codex Security Review #6523
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.

3 participants