/** * ChatV2MentionResolver — turn `mentions[]` from a chat message into a * deduplicated list of agent-session dispatch targets. * * Phase C BE.2 scaffolding for Slack-like team-chat mention dispatch * (SEALED §2.1 routing rules): * * - **`@agent` mention** (mention id matches a `TeamMember.id`): * resolves to that member's tmux/agent `sessionName`. The dispatcher * will deliver the message directly into that agent's session. * * - **`@team` mention** (mention id matches a `Team.id`): resolves to * that team's TL — the canonical primary responder per the spec * ("the Team Lead is the primary responder; if busy, queue in the * Team's Task Pool"). TL is selected as: first member with both * `hierarchyLevel === 1` and `canDelegate === true`, falling back * to any `canDelegate` member, then `members[0]` so the rule is * deterministic even on legacy teams without hierarchy fields. * * - **Unknown mention id** (not a team or a known member): skipped * with a debug log. The dispatcher must always succeed even when * the FE includes a stale/typo mention; the chat HTTP ack has * already been sent by the time we resolve. * * Phase C F2a (issue #333) — cross-tenant scope enforcement: when the * caller passes `context.teamId`, the resolver enforces the channel's * tenant boundary. `@agent` mentions to members of any other team are * dropped (cross-tenant leak vector pinned by Arch on PR #331); `@team` * mentions to any team still resolve (cross-team pings are a useful * coordination affordance), but a mismatch emits a warn for audit. * * The resolver is the boundary between the chat-v2 wire (id strings) * and the agent runtime (session names). It deliberately does NOT * touch the agent registry — it only produces the addresses that the * dispatcher will hand to `AgentMessageSink.sendMessageToAgent`. * * The team list is loaded via an injected `loadTeams()` callback so * tests can drive it without a real `StorageService`. Production wiring * passes `() => storageService.getTeams()`. * * @module services/chat-v2/chat-v2.mention-resolver */ import type { Team } from '../../types/index.js'; /** Kind of mention that resolved to a target. */ export type MentionTargetKind = 'agent' | 'team'; /** * One resolved dispatch target. The dispatcher will call * `sendMessageToAgent(sessionName, prompt)` for each unique entry. */ export interface MentionTarget { /** Whether this target came from an `@agent` (member) or `@team` mention. */ kind: MentionTargetKind; /** The original mention id as it appeared in the wire `mentions[]`. */ mentionId: string; /** Resolved member id. For `kind='team'`, this is the TL's member id. */ memberId: string; /** Resolved agent session name — the address `sendMessageToAgent` expects. */ sessionName: string; /** For `kind='team'`, the matched team's id. Undefined for `kind='agent'`. */ teamId?: string; } /** Optional context the resolver may use to disambiguate. */ export interface MentionResolverContext { /** * The channel's `teamId` (when the channel is `type='channel'`). When set, * the resolver enforces cross-tenant scope (Phase C F2a, issue #333): * * - `@agent` mentions are restricted to members whose owning team's * id equals `teamId`. Mentions to agents of any other team are * dropped with a debug log so a foreign-team agent never receives * a dispatch frame from a channel they are not a member of. * * - `@team` mentions to any team id still resolve to that team's TL * (they remain useful for cross-team coordination), but a mismatch * between the channel's `teamId` and the mentioned team's id emits * a warn so the cross-team ping is observable in audit/log. * * Leave undefined (or empty string) to disable scoping — used by the * legacy DM dispatch path and by tests that need the global resolver. */ teamId?: string; } /** * Dependencies. The team-list source is callable so tests don't need a * StorageService; the production path injects `() => storage.getTeams()`. */ export interface MentionResolverDeps { /** * Returns the current team list. May be sync or async — the resolver * awaits the result either way. Errors propagate; the resolver * surfaces an empty-target list on caller error so a transient * storage hiccup never crashes the chat dispatch path. */ loadTeams: () => Promise | Team[]; } /** * Stateless resolver — instances are cheap to construct. Each `resolve()` * call loads the team list fresh so dispatch always sees the latest * member/session bindings (a new agent comes online → next mention * resolves correctly without a restart). */ export declare class ChatV2MentionResolver { private readonly loadTeams; private readonly logger; constructor(deps: MentionResolverDeps); /** * Resolve `mentions` to dispatch targets. * * Behavior: * - Empty / whitespace mention ids are skipped silently. * - Each id is classified as team-first, then agent-first; ties go to * the team interpretation since team ids and member ids share the * same UUID-shape namespace and team mentions are higher-impact. * - Unknown ids are logged at debug level and skipped. * - Duplicate `sessionName`s are collapsed (first-seen wins, keeping * `kind` ordering stable for tests). * - Targets with empty `sessionName` are skipped — there is no agent * to dispatch to. * * Phase C F2a (#333) — when `context.teamId` is set: * - `@agent` mentions whose owning team !== `context.teamId` are * dropped at debug level (cross-tenant leak guard). * - `@team` mentions to a different team still resolve to that * team's TL but emit a warn so cross-team pings are auditable. * - A blank/whitespace `context.teamId` is treated as "no scope" * so DM dispatch and back-compat tests keep current behavior. * * @param mentions - The raw mention id list from `ChatMessageDTO.mentions`. * @param context - Optional resolver context (teamId etc.). * @returns Deduplicated dispatch targets, in input order with team-first ties. */ resolve(mentions: string[], context?: MentionResolverContext): Promise; } //# sourceMappingURL=chat-v2.mention-resolver.d.ts.map