UUID refactoring: self-identification by member_id, mention filtering by ID instead of slug

This commit is contained in:
Markov 2026-03-02 09:03:09 +01:00
parent 7d7fe6df42
commit 9e8307d8bc
3 changed files with 31 additions and 13 deletions

View File

@ -46,7 +46,10 @@ async function startAgentWs(config: AgentConfig, client: TrackerClient): Promise
await wsTransport.start(); await wsTransport.start();
logger.info('Connected to tracker via WebSocket'); logger.info('Connected to tracker via WebSocket');
// Register project mappings for memory context // Register member ID and project mappings from auth.ok
if (wsTransport.memberId) {
router.setMemberId(wsTransport.memberId);
}
if (wsTransport.projects.length > 0) { if (wsTransport.projects.length > 0) {
router.setProjectMappings(wsTransport.projects); router.setProjectMappings(wsTransport.projects);
} }

View File

@ -21,6 +21,8 @@ export class EventRouter {
private wsTransport: WsClientTransport | null = null; private wsTransport: WsClientTransport | null = null;
/** chat_id → project_id mapping (populated from auth.ok) */ /** chat_id → project_id mapping (populated from auth.ok) */
private chatToProject: Map<string, string> = new Map(); private chatToProject: Map<string, string> = new Map();
/** Own member_id from auth.ok */
private memberId: string | null = null;
constructor( constructor(
private config: AgentConfig, private config: AgentConfig,
@ -40,6 +42,12 @@ export class EventRouter {
this.wsTransport = transport; this.wsTransport = transport;
} }
/** Set own member_id from auth.ok */
setMemberId(id: string): void {
this.memberId = id;
this.log.info('Member ID set: %s', id);
}
/** Register chat_id → project_id mappings from auth.ok */ /** Register chat_id → project_id mappings from auth.ok */
setProjectMappings(projects: Array<{ id: string; chat_id?: string | null }>): void { setProjectMappings(projects: Array<{ id: string; chat_id?: string | null }>): void {
for (const p of projects) { for (const p of projects) {
@ -108,7 +116,6 @@ export class EventRouter {
*/ */
private async handleMessageNew(data: Record<string, unknown>): Promise<void> { private async handleMessageNew(data: Record<string, unknown>): Promise<void> {
const content = (data.content as string) || ''; const content = (data.content as string) || '';
const authorSlug = (data.author_slug as string) || (data.sender_slug as string) || '';
const authorType = (data.author_type as string) || 'member'; const authorType = (data.author_type as string) || 'member';
const taskId = data.task_id as string | undefined; const taskId = data.task_id as string | undefined;
const chatId = data.chat_id as string | undefined; const chatId = data.chat_id as string | undefined;
@ -117,7 +124,14 @@ export class EventRouter {
// Extract author info from nested author object if present // Extract author info from nested author object if present
const author = data.author as Record<string, unknown> | undefined; const author = data.author as Record<string, unknown> | undefined;
const resolvedAuthorSlug = authorSlug || (author?.slug as string) || ''; const authorId = (data.author_id as string) || (author?.id as string) || '';
const authorSlug = (data.author_slug as string) || (author?.slug as string) || '';
// Extract mention IDs from mentions array
const mentions = (data.mentions || []) as Array<Record<string, unknown>>;
const mentionIds = new Set(mentions.map(m => (m.id as string) || '').filter(Boolean));
const isMentioned = (this.memberId && mentionIds.has(this.memberId)) ||
content.includes(`@${this.config.slug}`); // text fallback
if (!content) { if (!content) {
this.log.warn({ data }, 'message.new event missing content'); this.log.warn({ data }, 'message.new event missing content');
@ -125,25 +139,23 @@ export class EventRouter {
} }
// Ignore own messages (agent talking to itself) // Ignore own messages (agent talking to itself)
if (resolvedAuthorSlug === this.config.slug) { if ((this.memberId && authorId === this.memberId) || authorSlug === this.config.slug) {
this.log.info('│ Skipping own message'); this.log.info('│ Skipping own message');
return; return;
} }
// Ignore messages from other agents (unless explicitly @mentioned) // Ignore messages from other agents (unless explicitly mentioned)
// Prevents infinite reply loops between agents
if (authorType === 'agent') { if (authorType === 'agent') {
if (!content.includes(`@${this.config.slug}`)) { if (!isMentioned) {
this.log.info('│ Skipping agent message (not mentioned): @%s "%s"', resolvedAuthorSlug, content.slice(0, 80)); this.log.info('│ Skipping agent message (not mentioned): %s "%s"', authorSlug, content.slice(0, 80));
return; return;
} }
this.log.info('│ Processing agent message (mentioned): @%s', resolvedAuthorSlug); this.log.info('│ Processing agent message (mentioned): %s', authorSlug);
} }
// System messages: only process if agent is explicitly mentioned (@slug) // System messages: only process if agent is explicitly mentioned
// This lets assignment/mention notifications through, but ignores generic lifecycle events
if (authorType === 'system') { if (authorType === 'system') {
if (!content.includes(`@${this.config.slug}`)) { if (!isMentioned) {
this.log.info('│ Skipping system message (not mentioned): "%s"', content.slice(0, 100)); this.log.info('│ Skipping system message (not mentioned): "%s"', content.slice(0, 100));
return; return;
} }
@ -152,7 +164,7 @@ export class EventRouter {
// Build context-rich prompt for the agent // Build context-rich prompt for the agent
const ctx = taskId ? `[задача ${taskKey || taskId}]` : chatId ? '[чат]' : ''; const ctx = taskId ? `[задача ${taskKey || taskId}]` : chatId ? '[чат]' : '';
const from = authorType === 'system' ? '[система]' : `@${resolvedAuthorSlug}`; const from = authorType === 'system' ? '[система]' : `@${authorSlug}`;
const prompt = `${ctx} ${from}: ${content}`; const prompt = `${ctx} ${from}: ${content}`;
this.log.info('│ %s %s: "%s"', ctx, from, content.slice(0, 200)); this.log.info('│ %s %s: "%s"', ctx, from, content.slice(0, 200));

View File

@ -36,6 +36,8 @@ export class WsClientTransport implements TaskTracker {
projects: Array<{ id: string; slug: string; name: string; chat_id?: string }> = []; projects: Array<{ id: string; slug: string; name: string; chat_id?: string }> = [];
/** Online members from auth.ok */ /** Online members from auth.ok */
online: string[] = []; online: string[] = [];
/** Own member_id from auth.ok */
memberId: string | null = null;
/** Remote agent config from auth.ok (if agent) */ /** Remote agent config from auth.ok (if agent) */
remoteConfig: { remoteConfig: {
model?: string; model?: string;
@ -209,6 +211,7 @@ export class WsClientTransport implements TaskTracker {
private onAuthOk(msg: Record<string, unknown>): void { private onAuthOk(msg: Record<string, unknown>): void {
const data = (msg.data || msg.init || {}) as Record<string, unknown>; const data = (msg.data || msg.init || {}) as Record<string, unknown>;
this.memberId = (data.member_id as string) || null;
this.lobbyChatId = (data.lobby_chat_id as string) || null; this.lobbyChatId = (data.lobby_chat_id as string) || null;
this.projects = (data.projects as Array<{ id: string; slug: string; name: string; chat_id?: string }>) || []; this.projects = (data.projects as Array<{ id: string; slug: string; name: string; chat_id?: string }>) || [];
this.online = (data.online as string[]) || (data.agents_online as string[]) || []; this.online = (data.online as string[]) || (data.agents_online as string[]) || [];