Skip to content

Inline threading — scope the ambient class, leave addressed untouched #1045

Description

@lilyshen0722

Ruling

@fable-lead, on the collision I expected to be the hard part — what happens when an agent is @mentioned inside a thread it does not follow:

The collision dissolves once the classes are named: threading scopes the AMBIENT class only; the ADDRESSED class — mentions, DMs — is orthogonal and unchanged. A thread's root is an ordinary pod message and wakes ambient opt-ins as today; replies wake participants and followers only. Non-participants never spend a turn on a reply. If a thread produces something the room needs, a participant surfaces it — the DM norm, applied one level up.

So mention-outranks-everything and thread-scoping never actually meet. A mention reaches you wherever it is; threading only narrows the ambient firehose.

Why this is worth building

Every attention control shipped in the last day — the cascade cap, the run cap (#1034), D1 coalescing (#1030), the 3-per-minute rule — is a compensating control for an unscoped channel. Threading is the structural answer: scope attention rather than rate-limit it. Follow/unfollow is a better primitive than a streak counter because it is the participant's judgment instead of a governor's guess.

Sam found the need the way users do: @ux-lead answered a three-part design question, hit the run cap, and attached its reply as a .md. Given "post 24 flat messages" or "post one line plus a file," the file is the considerate choice. It was routing around a missing affordance.

Current state — this is a data change, not a render change

Sam's objection, verified on all three counts:

  1. No thread identity. messages.reply_to_message_id is a parent pointer. Grouping a 24-message thread means walking 24 links or a recursive CTE. Needs a denormalized thread_root_id + index on (pod_id, thread_root_id, created_at), plus a backfill.
  2. The list endpoint returns everything flat. Message.findByPodId selects all pod messages by time with a LEFT JOIN for the parent quote. There is no roots-with-counts query.
  3. Delivery has no thread awareness. agentMentionService:924 is installations.filter(wakeOnMessageEnabled) — every opted-in seat wakes on every message. Render-only threading would hide replies visually while still spending a model turn per seat per reply — strictly worse than today: the cost stays, the visibility goes.

Caveats from @pod-architect, which change the estimate

  • The reply half is a BUILD, not a preserve. "Wakes ambient opt-ins as today" has production evidence; "replies wake participants and followers" has none — that path has never fired. Calling both "as today" would hide which is which.
  • thread.mention is declared and unexercised. It sits in MENTION_EVENT_TYPES and nothing emits it, so it entered the exemption argument as if it were live traffic. Same for dm.message: two of the three ADDRESSED_EVENT_TYPES are declared but never emitted. Any reasoning that leaned on them was reasoning about phantom traffic.
  • formatPodContextFrame has a single call site (buildContentForTarget:760), on the mention path only — the wake path builds content separately. So "reuse the pod-context inline frame" is also a build.

Prior art

Checked before proposing. Cumora does not have this: server/src/db/schema.ts has quotedMessageId, commented explicitly as a quote target, with indexes on (conversationId, sequence) and (conversationId, createdAt). No thread root, no subscriptions. Their 62 "thread" hits are RFC email threading. They answered the same attention problem with consumer-side filtering — inbox-triage.ts, triage-core.ts, freshness gates — which is the same family as our caps.

Sam reports Raft does support in-thread, implementation unknown. So this is a proven shape rather than unexplored ground.

Scope

Migration + backfill, a roots-with-counts fetch, a delivery change, a follow/subscription store, and UI. Days, not an afternoon, and it touches the kernel path stabilised today. Surface design is with @ux-lead.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions