← back to Ticket System
harden @mention routing: only DM known agents (or broadcast); ignore stray @tokens with a notice (TK-00067)
fb0737fa9f727bd466a0ad60eec3ce01220a89b0 · 2026-07-26 20:38:30 -0700 · Steve Abrams
Files touched
Diff
commit fb0737fa9f727bd466a0ad60eec3ce01220a89b0
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Sun Jul 26 20:38:30 2026 -0700
harden @mention routing: only DM known agents (or broadcast); ignore stray @tokens with a notice (TK-00067)
---
lib.js | 11 ++++++++++-
tk | 12 ++++++++----
2 files changed, 18 insertions(+), 5 deletions(-)
diff --git a/lib.js b/lib.js
index b61f328..da46fe3 100644
--- a/lib.js
+++ b/lib.js
@@ -126,6 +126,15 @@ function parseMentions(text) {
return [...out];
}
+// Every agent identity that has ever acted (create/comment/action/assign) or been
+// party to a DM (from/to). Used to gate @mention routing so a stray "@agent" in
+// prose doesn't create a dead-letter DM to a nonexistent recipient.
+function knownAgents() {
+ const set = new Set();
+ for (const ev of readEvents()) { for (const k of ['agent', 'from', 'to']) if (ev[k] && !isBroadcast(ev[k])) set.add(ev[k]); }
+ return set;
+}
+
// Ticket ids are 5-digit zero-padded + a slug of the title (Steve, 2026-07-26):
// TK-00025-ga4-attribution-prep
// The numeric part is the tracking key; legacy short ids (TK-3, TK-17) remain
@@ -157,4 +166,4 @@ function resolveId(ref, map) {
}
module.exports = { withLock, append, tickets, nextId, resolveId, idNum, slugify, STATUSES, EVENTS,
- nextMid, messages, inbox, thread, resolveMid, threadParticipants, parseMentions, isBroadcast };
+ nextMid, messages, inbox, thread, resolveMid, threadParticipants, parseMentions, isBroadcast, knownAgents };
diff --git a/tk b/tk
index d26c098..4b6a830 100755
--- a/tk
+++ b/tk
@@ -23,7 +23,7 @@
// Also: an @agent mention inside a comment/note auto-DMs that agent, linked to the ticket.
// Agent identity defaults to $TK_AGENT, else "claude@<tty-or-pid>".
const { withLock, append, tickets, nextId, resolveId, STATUSES,
- nextMid, messages, inbox, thread, resolveMid, parseMentions } = require(require('path').join(__dirname, 'lib.js'));
+ nextMid, messages, inbox, thread, resolveMid, parseMentions, knownAgents, isBroadcast } = require(require('path').join(__dirname, 'lib.js'));
const argv = process.argv.slice(2);
const cmd = argv.shift();
@@ -48,10 +48,14 @@ if (cmd === 'new') {
const id = norm(argv.shift()); const text = argv.join(' ').trim(); if (!text) die('missing text');
if (!tickets().has(id)) die('no such ticket ' + id);
append({ ts, type: 'comment', id, kind: cmd, agent, text });
- // Route any @agent mentions to their inbox as a ticket-linked DM.
- const mentioned = parseMentions(text).filter(a => a !== agent);
+ // Route @agent mentions to their inbox as a ticket-linked DM — but only to KNOWN
+ // agents (or a broadcast), so a stray "@agent"/"@here" in prose isn't a dead letter.
+ const known = knownAgents();
+ const all = parseMentions(text).filter(a => a !== agent);
+ const mentioned = all.filter(a => isBroadcast(a) || known.has(a));
+ const skipped = all.filter(a => !mentioned.includes(a));
for (const to of mentioned) append({ ts, type: 'dm', mid: withLock(() => nextMid()), from: agent, to, text, ticket: id });
- console.log(`${cmd} added to ${id}${mentioned.length ? ' · dm→ ' + mentioned.join(', ') : ''}`);
+ console.log(`${cmd} added to ${id}${mentioned.length ? ' · dm→ ' + mentioned.join(', ') : ''}${skipped.length ? ' · (ignored @' + skipped.join(', @') + ' — unknown agent)' : ''}`);
} else if (cmd === 'log') {
const id = norm(argv.shift()); const text = argv.join(' ').trim(); if (!text) die('missing action text');
if (!tickets().has(id)) die('no such ticket ' + id);
← eac02d5 agent-to-agent DMs: tk dm/inbox/reply/thread + @mention auto
·
back to Ticket System
·
chore: harden DM thread-walk against cyclic re-chains (cycle fda28cb →