feat(guild): <@id> mention mechanism

- parse <@user-id> outside backtick spans
- general: message with an at-list wakes only the at'd users (else all)
- report/triage/custom: mentions change nothing
- discuss/work: mention by current speaker pushes a sub-rotation frame
  (atList = mentions - sender, intersected with channel members); single
  linear pass (real/no-reply/force-proceed), then pop back to the saved
  parent pointer (resumes at the pusher); nested frames supported

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
h z
2026-05-15 15:27:35 +01:00
parent 182cfb3c41
commit 02b7c72e70
5 changed files with 193 additions and 45 deletions

View File

@@ -18,6 +18,7 @@ import { Message } from '../entities/message.entity';
import { IdempotencyRecord } from '../entities/idempotency-record.entity';
import { WakeMapping } from '../entities/wake-mapping.entity';
import { parseSlashCommand } from '../channels/slash-commands';
import { parseMentions } from '../channels/mentions';
import { TurnService } from '../channels/turn.service';
import { EventsService } from '../events/events.service';
import { clampLimit, computeNextExpectedSeq } from './pagination.util';
@@ -179,18 +180,24 @@ export class MessagingController {
data: responseBody,
});
// mentions: <@id> outside backtick spans
const mentionIds = parseMentions(body.content ?? '');
if (isRotating) {
// discuss/work: rotation decides the single wakeup target
const decision = await this.turn.onNormalMessage(channelId, authorUserId);
// discuss/work: rotation (incl. mention sub-frames) picks the target
const decision = await this.turn.onNormalMessage(channelId, authorUserId, mentionIds);
await this.realtime.emitMessageTargeted(channelId, responseBody, decision.wakeupUserId);
} else {
// general/report/triage/custom: wakeup from x_type + wake_mapping
// general/report/triage/custom: wakeup from x_type + wake_mapping;
// general also honors the message's at-list
const wakeRows = await this.wakeRepo.find({ where: { channelId } });
const wakeUserIds = new Set(wakeRows.map((w) => w.userId));
const mentionUserIds = new Set(mentionIds.filter((id) => id !== authorUserId));
await this.realtime.emitMessageCreated(channelId, responseBody, {
xType,
authorUserId,
wakeUserIds,
mentionUserIds,
});
}