fix(acp): wake agents for mentions added by edits (#6131)

🤖
## 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>
This commit is contained in:
Logan Johnson
2026-10-02 09:50:02 -04:00
committed by GitHub
parent e4681e44a7
commit 47654215eb
6 changed files with 209 additions and 68 deletions

View File

@@ -86,6 +86,44 @@ test("edit save uses edit-target refs that resolve after edit-open", async () =>
});
});
test("an unresolved recipient does not re-notify an unchanged agent mention", async () => {
const { buildEditMentionState } = await import("../lib/draftMentionRefs.ts");
const agent = "c".repeat(64);
const originalContent = "@Agent @Missing User please chek";
const editTarget = buildEditMentionState(
originalContent,
[
["p", agent],
["p", UNRESOLVED_USER],
],
{ [agent]: { displayName: "Agent" } },
(pubkey) => pubkey === agent,
);
assert.deepEqual(
editTarget.mentionRefs.map((ref) => ref.pubkey),
[agent],
);
assert.deepEqual(editTarget.unresolvedMentionPubkeys, [UNRESOLVED_USER]);
const calls = [];
await submitMessageEdit({
...baseOptions(async (_content, tags, notifying) => {
calls.push(["save", tags, notifying]);
}),
content: "@Agent @Missing User please check",
originalContent,
editTarget,
extractMentionPubkeys: (text) => (text.includes("@Agent") ? [agent] : []),
revalidateMentionPubkeys: async (pubkeys) => {
calls.push(["revalidate", pubkeys]);
return pubkeys;
},
});
assert.deepEqual(calls[0], ["revalidate", []]);
const [, tags, notifying] = calls[1];
assert.deepEqual(notifying, []);
assert.ok(tags.some(([name, key]) => name === "mention" && key === agent));
});
test("edit save revalidates added mentions immediately before save", async () => {
const agent = "c".repeat(64);
const calls = [];

View File

@@ -117,10 +117,12 @@ export async function submitMessageEdit({
};
// Current picker bindings must not reinterpret the original body: selecting a
// different Scout would otherwise make that new key look already notified.
// With unresolved history, conservatively revalidate all current recipients.
const originalMentionPubkeys = editTarget.unresolvedMentionPubkeys?.length
? []
: (editTarget.mentionRefs ?? []).map((ref) => ref.pubkey);
// Resolved historical keys are already recipients, so an unresolved neighbor
// must not make them notify again. Unresolved keys have no trusted binding to
// the edited text; they stay out of this set and are revalidated if present.
const originalMentionPubkeys = (editTarget.mentionRefs ?? []).map(
(ref) => ref.pubkey,
);
let addedMentionPubkeys: string[];
try {
addedMentionPubkeys = diffAddedMentionPubkeys(