Annotations — threaded notes, @mentions, resolved state

Capture context next to a document, reply in threads, @mention a teammate to pull them in, and mark a thread resolved when it's done. Per-document, audit-logged, never duplicates the file.

Updated 2026-05-26

Annotations let you keep commentary on a document without altering its bytes or its content hash. Three kinds:

- Notes — freeform body text. The most common kind. Use them for "we agreed to remove §4 in the next pass", "reviewed by counsel 2024-08-12", "matches submittal 17 in Project Athena". - Highlights — colored span markers (Phase 2 v1 ships note-level commentary; in-text highlights land alongside the doc viewer's selection API). - Tags — structured single-token labels (e.g. "needs-review", "redacted").

## Threads

Open any document at /doc/<id> and use the Notes panel right above the History timeline. Notes thread one level deep: a root annotation can have replies, and replies can't themselves be parents. The composer at the bottom adds a new root; "Reply" under each open thread adds a reply.

## @mentions

Type `@their.email@kumokodo.ai` inside a note to tag a teammate. Kodori parses mentions server-side, validates each one is a member of your workspace (foreign / deleted addresses silently drop), persists the surviving list on the annotation row, and sends each mentioned member a Resend email with the doc name, your name, an excerpt of the note, and a deep link to the thread. The composer offers live autocomplete as you type.

## Resolved state

Threads are open by default. The author, anyone @mentioned in the root, or a workspace admin can mark the thread "Resolved" — Kodori records who resolved it and when on the annotation row, and the audit log gets an `annotation.resolved` event. The thread renders muted with a strikethrough on the root note. "Reopen" reverses it (audit logs `annotation.reopened`). The "Hide resolved" toggle filters resolved threads out of the panel for an active-work view.

## /mentions inbox

When you're @mentioned in any annotation thread, that mention also lands in your /mentions inbox — a persistent surface listing every readable doc where someone tagged you, newest-first. Default view shows only OPEN threads (resolved threads dropped); flip the chip to see resolved ones too. Each row links straight to the thread on the document page (#annotations anchor). Permission-trimmed: docs you've been walled off from drop out of the inbox even if a historical mention exists.

The email you got at @mention time is a notification; the inbox is the canonical surface — you can dismiss the email or lose it without losing the work. The inbox uses the gin jsonb_path_ops index on annotations.mentioned_user_ids (migration 0049) for fast containment lookup, so it's responsive even on tenants with hundreds of thousands of annotations.

## Audit and permissions

Adding a note appends `annotation.added` to the document's stream, with the parent_id (for replies) and the surviving mention list in the payload. Deleting hard-deletes the annotation; `annotation.removed` preserves the audit fact. Anyone with read access on the document can add notes and reply; the note's author or a workspace admin can delete. There is no "annotation hold" — if you need a note's content preserved for a matter, capture it in the document's metadata or in the matter's Collection description.

The agent can author notes too: "add a note on this doc that we reviewed it on 2024-08-12 and approved §3" calls createAnnotation under the hood. Notes authored by the agent show up the same way — author label is the agent identity rather than a user email.

## Auto-resolution of stale threads

A daily Inngest cron (annotation-stale-resolver at 04:00 UTC) auto-resolves root annotations with no activity in the last 30 days. The signal: root createdAt < now - 30 days AND no reply newer than 30 days AND the underlying document is still live. Auto-resolved threads emit `annotation.resolved` (the existing event type) with `autoResolved: true` + `staleDays: 30` in the payload — distinguishable from operator-resolved threads via that flag without polluting the EventTypeSchema.

Why: closed matters accumulate open @-mentions in the /mentions inbox forever, ping-fatiguing operators with no actionable signal. Auto-resolve clears those mentions once the surrounding matter activity dies down. The audit chain still records the original mention; the resolution is the cleanup, not the deletion.

Per-tenant configurable threshold (e.g. 60 days for matters that stay active longer) is a future revisit if customers need it. Today the threshold is hardcoded at 30 days as the standard "this conversation is over" window in collaborative tools.