-
Notifications
You must be signed in to change notification settings - Fork 2
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.
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.
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_sessionis.
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.
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.
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 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.
-
docs/specs/recall.md— the full specification, including what is deliberately not here. - Threads — the other answer to a busy desk.
- Transcript projection — the window recall works around.
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