-
Notifications
You must be signed in to change notification settings - Fork 2
Threads
A desk is not a flat channel. Messages hang off roots, and two things follow from that: what one viewer should see of a busy desk, and how an agent that has been away finds out what is going on.
project_session behaves differently depending on what the query asks for.
A thread-scoped query returns the root and its direct replies. That is the obvious case.
A channel-level query returns every root and each root's first reply. An agent reading a desk sees the answers rather than a run of unanswered questions, and it still never reads a second thread's interior.
The narrowing cannot be a fold a caller applies afterwards. Picking each root's first reply needs the parent link and chronological order, and the walk runs newest first, so it happens inside the projection where both are available.
Two rules follow from the bounded scan:
- A reply whose parent is itself a reply is a thread interior and is never promoted.
- A reply whose parent fell outside the scan cannot be shown to belong to a root, so it is dropped rather than flattened into the channel.
The sharing delta keeps the narrower roots-only rule, because it decides row by row and the promotion needs the whole slice. A first reply reaches an agent on the next re-seed rather than the next tick.
See
docs/specs/thread-scoped-conversations.md.
An agent that has just cold-started, or that has been away, gets asked "what are we doing?" and can only answer from whatever fit its window. That window is a recency bound over rows, so a thread quiet for a page is invisible even when it is the one that matters.
The index answers the question directly: the roots in this desk, what each one
opened with, how much reply traffic it drew, and when it was last touched. It
lives in
src/threads/.
pub struct ThreadLine {
pub root: Sequence,
pub opening: String,
pub replies: usize,
pub latest: Sequence,
pub landed: Option<String>,
}| item | what it is |
|---|---|
fold_thread_index(rows, limit) |
the pure fold over a chronological slice of one desk |
read_thread_index(log, conversation, limit) |
the fold plus its bounded read |
THREAD_INDEX_LIMIT |
rows described to a viewer by default, 5 |
THREAD_OPENING_CHARS |
characters of a root kept as its opening, 60 |
THREAD_INDEX_SCAN |
maximum raw rows inspected for one index, 256 |
The fold is separate from the read on purpose. It is the part worth testing and it needs no fixture, which is the charter's rule applied inside one module rather than across two crates.
landed belongs to the host. Where a thread's work ended up is board state
this crate does not hold and must not learn, so the fold always returns None
and the host fills it in afterwards. Nothing else on the row needs the host.
The scan bound is deliberately small: 256 rows against SCAN_LIMIT's 2048. The
index is a recency view, and paying a full scan to surface a thread nobody has
touched in two thousand messages is the wrong trade. The visible consequence is
that a thread whose root fell outside the bound is absent even when its replies
are recent, because a reply alone cannot supply an opening.
A blank root is not a thread. The row exists to say what a thread is about, and a blank one says nothing, so neither it nor its replies are indexed. This differs from the channel-level projection, where a blank root still anchors its first reply: there the root's job is to be an anchor rather than to be read.
There is no index inside a thread. read_thread_index returns empty, without
reading, for a conversation that already has a thread_root. A viewer inside a
thread is not choosing between them.
Ordering is total: latest descending, then root descending. Sequences are
unique per thread, so the tie-break is unreachable through the fold and exists
to keep the ordering total by construction.
Into SessionContext, beside whatever the host adds:
pub struct SessionContext {
pub threads: Vec<ThreadLine>,
pub notes: Vec<BriefingNote>,
}A BriefingNote is one host-supplied block this crate cannot compute: board
state, open work, attachments. It carries a heading and lines in the host's own
order.
Both are carried beside the operator's message and never appended to it, so
nothing downstream has to cut them back off before reasoning about what was
actually asked. Both also stay separate from SessionInitialization::history,
which is the log and only the log.
pub struct SessionInitialization {
pub briefing: TeamBriefing, // ephemeral team context, not in the log
pub context: SessionContext, // threads and host notes, typed
pub history: Vec<SessionMessage>, // chronological, attributed, from the log
}initialize_session_with_context is the entry point that populates all three.
tinyhivemind is GPL-3.0-only. Built by @senamakel.
Start here
The algebra
- Shared medium
- Desks and rosters
- Mentions
- Cross-desk referral
- Transcript projection
- Threads
- Recall
- Responder ladder
Hive mechanics
Working on it
Reference