Skip to content
Steven Enamakel edited this page Sep 1, 2026 · 2 revisions

Recall

A turn reads about thirty messages of a desk whose log is unbounded. Everything older is, from that turn's point of view, gone — including the decision the room settled two hundred messages ago, which is why it gets re-litigated and sometimes answered differently the second time.

Enlarging the window moves the cliff without removing it, and charges every participant for it on every turn. Recall does the other thing: it makes the transcript queryable, keeps a small pinned working set arriving regardless, and states the budget each message is spending. The same move a recursive language model makes over a long context, applied to a shared transcript rather than one prompt.

Three mechanisms, no new port, no new stored state.

One ranking

Everything that picks — agents, people, desks, threads, messages — ranks through select, so an agent search and a desk search cannot disagree about what a better match is.

A match falls in a tier, and the score is that tier's base plus a density term worth at most 100:

tier base what it means
Exact 1000 the text is the query
Prefix 800 the text starts with it
WordPrefix 600 it starts a word inside the text
Substring 400 it appears somewhere
Subsequence 200 its characters appear in order

Tiers are 200 apart, so density orders candidates within a tier and never promotes one past another. All of it is fixed-point integer arithmetic: the same snapshot and query always produce the same list, on any machine.

use tinyhivemind_core::{find, select::SELECT_LIMIT};

let hits = find::agents("naka", &roster, SELECT_LIMIT);
let desks = find::desks("shipping", &desks, SELECT_LIMIT);

A candidate's label and id are matched at full weight and its description at half, so a desk named for the query always outranks one that merely mentions it.

Searching the transcript

Pickers work off snapshots the turn already holds. Searching messages has to wait on a read, so it lives behind the SessionLog port in search.

use tinyhivemind::{SearchQuery, search_messages};

let query = SearchQuery::new("rate limiter")
    .in_conversation(conversation)
    .by_author("alice");
let hits = search_messages(&log, &query).await?;

A hit carries the row's address, its author, its thread parent, and a whitespace-collapsed excerpt around the match — enough to decide whether to go read the row.

Two things differ from projection, deliberately:

  • A desk-scoped search reads the desk's whole interior, thread replies included. The projection is narrow so a turn stays readable; the search exists precisely to reach the reply buried three deep.
  • Reaching the scan bound is success, not failure. A search walks at most 2048 raw rows and is honest about it, exactly as project_session is.

search_threads ranks a desk's threads by their opening words, bounded the way the thread index is, because which threads are live is a recency question.

Regular expressions

A query written /…/ is an expression rather than a literal — one input box, two intents. It needs the crate's regex feature, which is off by default.

let hits = search_messages(&log, &SearchQuery::new("/^ship(ped)?\\b/")).await?;

An expression hit is read onto the same tiers, from the span the engine matched: covering the whole text is Exact, at offset zero is Prefix, at a word boundary is WordPrefix. So expression hits and literal hits rank in one comparable list.

Without the feature, a Regex pattern is Error::RegexUnsupported; one that does not compile is Error::InvalidPattern. It never quietly falls back to searching for the expression source as text, which would return confidently wrong results.

Pinning

Search makes an old message reachable. A pin makes it unavoidable.

!pin [^N] [#label] [free text]
!unpin ^N

!pin with no target pins the message carrying it — an agent marking the insight it just wrote. !unpin requires a target: a marker that removes something has to say what, and an incomplete one yields nothing at all, the same fail-closed rule the trace grammar uses.

The board is a fold over the log, not a record beside it. That is the charter's first rule: there is no second journal to keep consistent with the first, and a host that already stores the transcript stores the pins for free.

use tinyhivemind::{PIN_LIMIT, read_pinboard};

let board = read_pinboard(&log, &conversation, PIN_LIMIT).await?;
  • Later markers win: pinning something already pinned updates it rather than duplicating it.
  • The board holds twelve, most recently pinned first. Over that, the least recently pinned drops — a full board means something has to come off, and the oldest pin is the one the room stopped arguing about.
  • A desk board looks inside threads. A pin exists to lift a message out of the depth it is buried at.

SessionContext carries the board and initialize_session_with_context reads it, so a pinned message is in every turn's context for that conversation whether or not anybody searched for it.

The budget

The window is a shared resource. An agent that writes six paragraphs pushes five other messages out of everybody's view, sometimes including the one that answered the question.

TeamBriefing carries a BrevityPolicy — 600 characters against a 30-message window, by default — and states it in the briefing text next to the pin and search spellings, so an agent knows what it is spending.

overrun(content) reports the characters a message goes over by. It is reported, never applied: nothing here rewrites an authored message, because a transcript that disagrees with what was said is worse than a long one.

Read more

Clone this wiki locally