feat(iios): @mentions via inbox fan-out + my-annotations query (pins/saves)

Mentions ride the existing event-driven inbox — no chat parsing in the kernel:
- send() carries an OPAQUE mentions[] (userIds) into the message event; the kernel
  never parses "@". The app supplies the notify-list.
- InboxProjector fans out a MENTION inbox item (new generic inbox kind) to each
  mentioned *participant* (never the sender), idempotent per source message; reading
  the thread resolves the reader's MENTION + NEEDS_REPLY items to DONE.
- New MENTION value in the generic IiosInboxItemKind taxonomy (migration).

Pins/saves reuse the annotation primitive; new generic query powers a Saved list:
- MessageService.listMyAnnotated(principal, type) + GET /v1/threads/my-annotations
  returns the caller's annotated messages (type "save") with thread context.

Tests: 182 pass (+4: mention fan-out, resolve-on-read, saved query, opaque mentions).
Verified live: mention→inbox delivery, read→resolve, pin/save persistence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-07 01:35:30 +05:30
parent f3c4ba72b5
commit 8c814d9b86
8 changed files with 181 additions and 11 deletions
@@ -8,6 +8,7 @@ import {
HttpCode,
Param,
Post,
Query,
} from '@nestjs/common';
import { ThreadsService } from './threads.service';
import { MessageService } from '../messaging/message.service';
@@ -28,6 +29,12 @@ export class ThreadsController {
return this.messages.listThreads(this.principal(auth));
}
/** Generic "my annotated messages" (e.g. ?type=save for a personal bookmarks list). */
@Get('my-annotations')
async myAnnotations(@Headers('authorization') auth?: string, @Query('type') type = 'save') {
return this.messages.listMyAnnotated(this.principal(auth), type);
}
/** Create a thread; `membership`/`creatorRole`/`subject` are opaque, app-supplied attributes the kernel stores but never interprets. */
@Post()
@HttpCode(201)