Skip to content

A thread reply is read before it is answered; a cheap gate ahead of the door decides whether the bot was addressed, and that verdict survives a restart ​

The ask. Decide (the maintainer, before the plan is written): a reply without a mention, in a channel thread the bot is part of, is no longer answered by default. A cheap model turn ahead of the door, intake, decides whether the person was talking to the bot; a silent reply gets no 👀, no card and no run, and never reaches the door, today's router or record 0057's operator. A mention and a direct message are never gated. The verdict is written durably so a bot restart replays the decision rather than making it again. Reader: an engineer who knows the Slack adapter's trigger rules and the reconnect catch-up and has read record 0057. The maintainer's frame, 2026-09-17: every reply in a thread the bot was once mentioned in makes it chime in, wrong when other people are talking there; require a mention or let a small model decide, always answer a direct mention, and work with the crash-recovery paths built for the Workers and the bot. The maintainer also decided that no rule answers a reply because it came from the requester, who is as likely as anyone to be talking to a colleague in their own thread, and that the default mode is classify.

Accepted 2026-09-18 with the plan: the maintainer decided a standalone plan (0057's plan links to it later instead of lifting units), and that the maintainer and the author label the offline set with the maintainer adjudicating.

Amended 2026-09-18, after a three-arm adversarial review (crash recovery, the fit with records 0057 and 0051, the verdict's evaluation), the same day the record merged. The first version made silence the operator's sixth outcome; 0057's turn runs inside dispatch() after the 👀, so that could not be silent, and the two turns read different inputs. Intake is now a gate ahead of the door, channel-side, and the operator never sees a silent message. Also changed: one decider per process is kept by claiming before the first await, and the receipt insert says who won; a missing or unreachable ledger degrades to classify-without-receipt instead of silence; the offline labelled set is a smoke test with its interval stated, and the release gate is the live false-silence ratio the receipts produce; the fact botAskedLast is dropped for want of a source; the requester is the newest addressed run's person; this record owns the amendment to record 0051 and re-evaluates its criteria 1, 3, 4 and 5; config set thread is destructive class with a policy row. The note on 0057 is reworded to match.

Success criteria: (1) a mention or a direct message always starts a run, in every mode; (2) in the default mode, a reply between two people in a bot thread gets no reaction, no card and no run; (3) while a receipt exists, a bot restart inside the catch-up window never turns a silent verdict into an answer, nor an addressed one into silence; without one, a message is re-decided at most once per reconnect; (4) the behavior is a setting on the existing scopes plus the thread, and always restores today byte for byte; (5) every silent verdict is readable somewhere, with its reason; (6) intake never binds, routes or steers: an addressed reply enters the door exactly as any message does today.

TL;DR ​

In the newest 200 runs, 64 of the 166 a person started (39 percent) were replies inside an existing thread, and nothing records whether the person mentioned the bot or was answering a colleague. The bet is a cheap gate ahead of the door: for an unmentioned reply in a bot thread one fast-model turn answers addressed or silent from the thread's newest turns and five facts, the verdict is claimed once per process and written first-writer-wins on the run ledger before the 👀 fires, and the catch-up after a restart reads the verdict instead of asking again. It costs one fast-model call and one ledger round trip per unmentioned reply in a bot thread, about 130 a day at today's rate and under half a dollar a day, and a silent reply is never steered into a live run, which amends record 0051. Decided: the gate, its position before the ack, the receipt, the three modes with classify default, the mention as the override, the 👀 after the verdict, no code rule that reads the words. Doing nothing keeps every bot thread a place people cannot talk in.

Today at 0c9d9dbb ​

The delta from what a veteran expects; every claim names its file.

You would expectWhat is trueProof
A bot answers when mentionedAny reply in a channel thread is handled once the bot has posted in it or been mentioned anywhere in itsrc/channels/slackTriggers.ts classifyMessage, threadIncludesBot; slack-channel.md item 1(c)
The door decides, then acknowledgesThe adapter claims the message in its seen-set before its first await, acks with the 👀, downloads files, then calls dispatch(); the router runs inside dispatch() and is skipped for a thread with a live run; record 0057's operator, proposed and not built, sits in the same placesrc/channels/slack.ts receiveSlackMessage; src/channels/slack/dedupe.ts; routing-and-config.md item 21; record 0057 "The shape"
A restart replays from a durable cursorSlack is the record: each reconnect scans member channels 30 minutes back and re-dispatches every message that would have started a run live and has neither a bot reply after it nor a 👀 under 30 seconds old; an unmentioned reply in a bot thread qualifiessrc/channels/slackCatchUp.ts findMissed; slack-channel.md item 7
The thread page is the newest turnsconversations.replies with limit: 50 and no cursor returns the OLDEST 50; the catch-up pages a whole thread but hands handle() no threadslack.ts threadIfBotInIt, onMissed
A reply during a live run is a new requestIt is steered into the live run's durable inbox, unconditionally, and the run keeps its lease whatever the reply said; the harness drains the inbox and never re-reads the threadthread-admission.md items 1 and 5; record 0051 criterion 3
The bot knows what it is waiting onThe confirmation store (record 0044) keeps one row per thread for ten minutes on the config Durable Object, readable by offer id onlysrc/core/confirmations.ts; src/core/budgets.ts CONFIRMATION_TTL_MS
The run ledger is always thereIt is optional: a deployment on the file store, or with history off, runs a null write-through and keeps nothing across generationssrc/index.ts the ledger wiring; src/core/runLedgerWorker.ts
Settings have a thread scopeUser, channel and defaults only; config set takes me or channel; the policy table has config-scope rows for channel alonesrc/config.ts Scope; src/core/commands/config.ts; src/core/authz/policy.ts

The number nobody has: how many of those 64 replies mentioned the bot, and how many of the unmentioned ones were for it. The run record keeps the permalink and the thread key, not the mention. The receipt makes both countable.

The shape ​

Think of the shouldReply pattern from multiplayer chat bots: a cheap decision on whether to speak at all, ahead of the expensive call. The closest known system is Devin's Slack harness, where silence is the default and a legal exit of the loop. The one way this differs is that the verdict is written durably and first-writer-wins, because our bot is replaced on every deploy and replays Slack from a window rather than from a cursor.

Three pieces:

  1. The gate. The mode, mention, classify or always, decides which unmentioned replies in a bot thread are asked about at all. always is today: the reply takes today's path and no verdict exists. mention never asks: silent by setting, no model call. classify, the default, asks intake for a verdict. A mention, a direct message and a top-level post never pass through the gate; they are the door's as they are today. The gate reads the mention as the adapter already does, a <@…> entity in the message, which is channel encoding in record 0057's sense and lives in the trigger module it allowlists.
  2. The verdict. Intake is one fast-model turn in the core, called by the Slack adapter after the redelivery guard's claim and before the 👀. It is not the door and never becomes it: it binds nothing, routes nothing and steers nothing; it answers one question, and an addressed reply then enters dispatch() exactly as any message does today, and enters record 0057's operator the same way once that exists. Its inputs are channel-side: the thread's newest twelve turns each labelled bot, requester or person, the message, and five facts computed by code, none of which decides anything alone: whether a run is live in the thread and for how long, whether the replier is the thread's requester, how long since the bot last spoke, whether the reply mentions anyone, and whether a confirmation is pending in the thread and on whom. One forced tool call answers addressed, silent or unsure under the router's 8 second timeout; unsure, a timeout and an error are all silent. The turns enter the call inside record 0037's untrusted fence, as every quoted thread does. No code reads the words to decide: 0057 deletes the chat-side readers, so a typed command line, a mention of a colleague and an answer to a pending question are facts in the turn, never rules ahead of it.
  3. The receipt. Every verdict is one row on the run history Durable Object, keyed by the message's channel and timestamp, written as insert-if-absent, and the write answers whether this caller inserted it; only the inserter acts on addressed. The live path writes it before the 👀; the catch-up reads it before deciding anything about an unmentioned reply. Once 0057's thread session exists, a silent reply is also appended to it as a person's turn marked silent, so the operator's memory holds the conversation it did not answer; in mention mode nothing is appended, which is the price of that mode.

One trace: silent, then the bot rolls ​

A release deploys while a team is discussing in a thread the bot reviewed a pull request in an hour ago. Channel C_BACKEND, thread 1700000000.000100, mode classify.

  1. At 12:00:00 a teammate replies to the requester, unmentioned: "I'd rather we hold the rollout until the migration is verified."
  2. The trigger rules return handle-if-bot-in-thread; the redelivery guard claims C_BACKEND:1700000900.000200 in g41's seen-set before its first await. The thread's one page (12 replies) holds the bot's card from an hour ago and is also its newest turns.
  3. The adapter labels the turns, reads the thread's runs and the pending confirmation (none), and calls intake.
  4. Intake reads the ledger: no receipt.
  5. Intake calls the fast model with the twelve turns, the message, and the facts liveRun: none, replierIsRequester: false, botLastSpoke: 3600 s, mentionsOther: false, pendingConfirmation: none. The answer: silent, "a teammate answering the requester about the rollout".
  6. Intake inserts the receipt { verdict: silent, reason, mode, model, gen: g41 }; the insert answers inserted: true. The adapter sets intake: silent on the slack.receive span. No 👀, no card, no run.
  7. At 12:00:20 the deploy sends SIGTERM to g41. The drain closes the socket; the container exits; g42 boots.
  8. At 12:01:10 g42 connects and the catch-up scans C_BACKEND 30 minutes back; the thread's latest_reply is inside the window, so its replies are paged in full.
  9. findMissed sees the reply: a person's message, unmentioned, in a bot thread, no bot reply after it, no 👀. Today that is a missed message.
  10. In the act phase, before anything else for this candidate, the catch-up reads the receipt: silent, by g41. It marks the message in g42's seen-set, counts it under silenced on the slack.catch_up root, and moves on.
  11. Variant: g41 died at step 5 and wrote nothing. g42 finds no receipt and runs the same intake on the paged thread's tail. Slack redelivers the same event live to g42 at that moment; the guard's claim in the seen-set stops the second path before any await, so one decider runs. If the claim were ever lost, the insert's inserted answer would still let only one of the two act.
  12. Variant: g41's verdict was addressed and it died between the receipt and the 👀. g42 reads addressed, sees no bot reply after the message and no 👀 inside the grace, and dispatches it as caught up, without a second model call.

A verdict, once made, is made once, and only one decider per process acts on it. The catch-up re-decides only a message nobody decided, which is the one case where deciding now is right.

The difficulty map ​

Ranked by risk of being wrong, each pointing at its section.

  1. The verdict's error rates on our own threads (most work). A frontier model barely beats the majority baseline on addressee recognition; structural facts in the turn are what make it work, and the offline set is too small to prove a 5 percent rate, so the live ratio is the gate. "The verdict and its gate".
  2. The receipt against the rule that Slack is the durable record, and its own races. A silent verdict is invisible in Slack by design, so the one place it can live is the ledger; the seen-set's claim, the insert's answer and the degraded shape without a ledger decide whether a restart can flip it or double it. "The receipt survives the bot".
  3. What this does to a live run. Record 0051 says every plain reply is steered; this record delivers a silent one nowhere, and the lease burns meanwhile. "The live run, and what this amends".
  4. The first thread-scoped setting. "The mode is a setting".

The verdict and its gate ​

The constraint is that the question is hard and looks easy. On a 2025 benchmark of addressee recognition in three-party dialogue a frontier model scored 80.9 percent against a majority baseline of 80.1 percent, and only about one turn in five names its addressee explicitly. A prompt that sees only the words will look fine on a demo and be wrong on the case the maintainer described: the requester replying to a colleague in the thread their own run lives in.

So the turn sees structure, not only text. The requester is the person who started the thread's newest run a person addressed, read off the run's own userId from the same page the dispatcher reads later, independent of whether that run is live, finished or refused; a coordinator's child is never it, and a page with no such run yields no requester. The bot's own turns are the ones posted by the bot's user id, so a relay app's post counts as a person's. A thread whose parent is the bot's own post, a scheduled report, is a bot thread by today's rule and is marked as such in the facts, since "the bot posted here" and "the bot answered someone here" bias the verdict differently. The tool has three answers, and unsure is silent, because a person who was ignored can mention the bot and a person who was answered by mistake cannot un-ring the bell. The model is the deployment's fast one, resolved as the router's is, intake.model, else routing.model, else defaults.models.general, through the provider seam the router uses for a call outside a run loop, under the router's 8 seconds. A model card without tool choice (record 0052) takes the router's text-answer contract; a card that supports neither is refused at config load for classify, never silenced at runtime.

The newest turns are a separate read from the participation check. conversations.replies with a limit and no cursor returns the oldest replies, which is what participation needs, since the mention that made the bot part of the thread is old, and the opposite of what the verdict needs. For a thread of 50 replies or fewer the one page is both; past that the adapter fetches the newest page as well, and the catch-up, which pages the whole thread already, hands intake the tail of what it paged. Reading the thread's runs before the 👀 is a position change: today that read happens inside dispatch() after the admission claim; here it moves ahead of the ack for this message class and the page is handed on, so it is one read, earlier. Twelve turns is a design choice, revisable on the labelled set.

Latency is measured today: the router's decision runs under a dispatch.route span in production and the load harness prints its p50, p95 and p99 per operation, so the plan's first unit quotes the current numbers for the same model and prompt size before the 👀 waits on anything. The 8 second timeout is the ceiling; a 3 second p95 is the line that moves the 👀 ahead of the verdict.

The gate has two parts, and the first cannot carry the second. The offline part is a labelled set: at least 150 unmentioned replies from the last 30 days of bot threads in our workspace, read from the threads at labelling time, stratified three ways (a run live in the thread by the run records' start and end times, a confirmation pending, neither), the maintainer and the author labelling under one rubric, "a teammate reading the thread would expect the bot to answer this", the maintainer adjudicating. It publishes the false-silence rate over the addressed replies and the false-answer rate over the silent ones, with the Wilson interval beside each, and the unsure and timeout shares beside those, so a prompt cannot pass by abstaining. At about 50 replies per stratum and fewer addressed among them, the interval on a 5 percent rate runs past 15 percent, so this set is a smoke test that catches a broken prompt, not proof of a rate. The pending-confirmation stratum is synthetic, since those rows live ten minutes. The live part is the gate: a silent receipt followed within ten minutes by a mention from the same person in the same thread is a false silence the system labelled itself, and the receipts table yields that ratio over all silent verdicts weekly with no labelling; a false answer has no such signal and stays with the offline set and the always escape. The targets, 5 percent false silence and 10 percent false answer, are goals the live ratio is read against: the code's default is classify, the maintainer's decision, on this argument: the noise is the observed problem, and the two defaults fail asymmetrically, since a wrong classify costs one mention while a wrong always has no recovery at all; a live ratio above the goal changes the prompt, the facts or the model, and the receipts say which replies to look at. The replay row records the model ref it ran, and the receipt compares it to the production resolution, so "against the production prompt" is checked, not assumed. The labelled set stays private; the counts are the receipt.

Cost, an upper bound: about 130 unmentioned replies a day at the survey's rate, each some 2,500 input tokens and 50 output on the fast card, is under half a dollar a day at the pricing table's rates, less with the prompt half cached.

Invariants: a mention or a direct message never reaches the verdict; unsure, a timeout and a provider error are silent with a reason naming which; the receipt carries the reason, so every silent verdict reads back with its why; no module under the dispatch or channel directories decides the verdict from the words; the lint rule that enforces it over chat text is record 0057's plan's, and until it lands review holds the line, with the trigger module's entity scan the one allowed scan, as channel encoding. Failure mode: a channel finds the verdict too quiet, sets always, and is back to today, with the receipts saying which replies were silenced and why.

The alternative this beat is a heuristic gate ahead of the model: a question mark, a second-person pronoun, the requester replying, the bot having spoken recently, a colleague mentioned. Each fires on the requester answering a colleague, the case the maintainer named, and each is a reader record 0057 deletes. The heuristics are facts in the turn, not the decision.

The receipt survives the bot ​

The constraint is invariant 6 of the agent guide as the Slack channel spec applies it: no persisted cursor, "was this handled" re-derived from Slack on every reconnect. A silent verdict leaves nothing in Slack, so a reconnect inside the window would re-classify every silent reply in every bot thread, and a model asked twice does not answer twice the same way: a teammate's remark, correctly ignored at 12:00, answered at 12:01 by the next generation.

The design keeps the invariant's purpose and moves its store. The purpose was never "Slack only"; it was "nothing in the ephemeral container". The run history Durable Object is one object per store key, already outlives generations, already holds live_runs and the durable inbox run_inbox, and the catch-up already tells a live card from an orphaned one off a snapshot of its live rows. The receipt adds one table beside those two, intake_receipts, keyed by <channel>:<ts>, one row per verdict, reached through a route carrying the store key like every run route so the insert is atomic inside the object's transaction, and pruned after the larger of 24 hours and the configured window plus the drain deadline, since nothing clamps the window from above. The liveRun fact comes from the thread's runs page the adapter already reads.

One decider per process, then one verdict across processes. Today the guard claims <channel>:<ts> in the seen-set before its first await, and that synchronous claim is why a catch-up dispatch and a live redelivery of one message never both run; the first version of this record moved the mark after the model call and opened exactly that window. The claim stays where it is. The insert then answers inserted, and only the inserter acts on addressed; a caller that finds a row already there marks the message seen and takes the stored verdict, which for addressed means nothing, because the inserter is acting. A lost response to a successful insert is retried the way the claim's is, and the retry reads the same row. The catch-up reads the receipt in its act phase per candidate, since the pure selection cannot, and re-checks the seen-set after the read as it does today.

Without a ledger the design degrades, it does not fall silent. Silence needs no receipt to be safe, only to be stable, and an addressed reply needs none at all: the 👀 and the card are its receipt within seconds. So when the write or the read fails, or the deployment runs the null ledger (the file store, history off), intake calls the model anyway, acts on addressed with receiptWrite: failed or absent on the span, stays silent on silent, and a restart inside the window may ask once more per reconnect. The startup log names the degraded shape once. What is never done is turning a state-Worker outage into muting every unmentioned steer into a run live in this process, which today steers in memory when the durable push fails.

Invariants, stated so a test can check them:

  • An unmentioned message with a silent receipt is never dispatched by any generation while the receipt exists. An edit that adds a mention arrives as a subtype the live path skips; the catch-up handles it inside the window, since history returns the edited text; recovery from a false silence is a new mention, not an edit.
  • A message with an addressed receipt, no bot reply after it and no 👀 inside the grace is dispatched by the catch-up without a model call. A later bot post in the thread masks it, exactly as it masks an unanswered mention today; the row makes that detectable and the plan decides whether to tighten the rule.
  • A message with no receipt is decided by exactly the function the live path uses, and at most one decider per process acts on it.
  • In classify and mention modes, no 👀 precedes the receipt's write attempt. In always mode the verdict and the ledger are never touched.

Failure modes beyond the degraded shape. The drain does not count an in-flight verdict, so a SIGTERM during the call wastes it and the next generation re-decides; the plan may count verdicts in the drain's hold. A thread_broadcast reply and a deleted message never enter either path; a receipt for a deleted message is inert until the prune.

The alternative this beat is re-classifying on replay and accepting the flips. Temperature zero narrows the flip and does not remove it, and the cost of a flip is a late answer in a thread that moved on, the exact failure this record exists to remove.

The live run, and what this amends ​

Position. The verdict runs after the redelivery guard's claim, so a duplicate costs no model call; before the 👀 and the file downloads, so silence is silent and a silent reply's attachments are never read. The 👀 fires after the verdict, decided: the delay is the model's latency on unmentioned replies only, and the load harness's router numbers are the estimate until the slack.receive span carries the verdict's own.

What a false silence costs on a live run. A plain reply into a live run is folded into its durable inbox and read at the run's next step; the run keeps running to its lease either way. A silenced steer therefore spends the rest of the lease on the un-steered course, up to the lease's length, and under record 0051 an idle unit that spends a renewal only on the requester's message stays idle with no ack and no sign when that message is silenced. One mention recovers the message, not the minutes. This is the cost the live-run stratum and the live false-silence ratio exist to count, and why the false-silence goal is the tighter one.

The amendment to record 0051, which this record owns. 0051's plan lands its owner rule in one release; 0057's rollout is flagged and long; so this record's plan changes the steer first in every realistic order, and the unit that changes the live path writes the dated re-evaluation on 0051 in its pull request. The re-evaluation as this record sees it. 0051 was accepted so that what a person says to a thread reaches its owner instead of a fresh route or a by-hand re-issue, and it rejected running the router on every reply because of a model call per reply. Intake is a model call per unmentioned reply in a bot thread, cheap, and it decides only whether the owner hears the message, never who owns the thread, so the owner rule stands. Criterion 1, a plain reply into a unit-owned thread appends one event to the owner, becomes an addressed plain reply. Criterion 3, a plain reply during a live run is steered, becomes an addressed one is. Criterion 4, an idle unit spends a renewal only on the requester's message, is unchanged for addressed messages and gains the failure named above. Criterion 5's rejected alternative was a call that would also route; this call routes nothing. The regression to check is whether a reply meant for the owner stops reaching it, and that is the live false-silence ratio with the mention as the recovery; 0051's own measurement, the share of plain replies into owned threads that end in a by-hand re-issue, then counts addressed replies. Nothing 0051 built moves: the owner rule, the modes and the idle unit stay, behind the gate.

The pending question. Record 0044's confirmation and record 0054's question wait on a specific person in a specific thread, and the answer is the door's to read, today from the next message and under 0057 as the operator's answer decision from the session. Intake adds nothing ahead of that: the pending row and the person it waits on are facts in its turn, and a wrong silent on a "yes, on staging" is a false silence the live ratio counts. The pending-by-thread read the store lacks is this record's cost; 0057 reads its questions from the session and needs no such read.

Record 0057. Intake is not the operator and does not fold into it: the operator's turn runs inside dispatch() after the ack and reads the thread session; intake runs before the ack and reads the channel. That is the two-tier shape on purpose, a cheap turn deciding whether the expensive one runs, and it is not the two-interpreters disease 0057 diagnoses, because intake interprets nothing into a call. Under 0057's flag, where the operator's decision is logged beside the readers' on production traffic, silent verdicts are logged too, so the comparison sees every event. 0057's sentence that every event enters one model turn is qualified by its dated note: every event the gate admits.

The alternative this beat is a code rule for the requester of the live run, which a cold reading of this record proposed as the obvious exemption. The maintainer rejected it: the requester is the person most often talking to a colleague in their own thread, so the rule would answer exactly the case that prompted the record; and record 0057 deletes readers of this kind. The other alternative, letting today's router answer none, fails on position: the router runs inside dispatch(), after the 👀 and the downloads, and is skipped for a thread with a live run, which is where most unmentioned replies land.

The mode is a setting ​

intake.threadReplies takes mention, classify or always and lives on the existing scopes, a top-level intake block as the defaults layer, the channel, the user, and, new here, the thread. The thread scope exists because "be quiet in this thread" is what a person wants when a discussion outgrows the bot, and record 0051 rules out a typed word in prose for it. The mechanism is config set thread --intake.threadReplies mention, a registry command like config set channel, bound by the operator from words once it exists, stored in the runtime overrides keyed by the thread key, read by resolve() for this setting only, removed by config clear thread; a row for a thread nobody writes in again is inert. Precedence is thread, then user, then channel, then defaults; a thread setting beats a user's because it is the scope nearest the conversation it describes. The user scope is the replier's. A thread setting silences other people's replies, so by record 0057's rule (irreversible, or affecting people other than the requester) it is destructive; today the class is a boolean on the definition and record 0044's built-in write default already shows the button for a routed config set of any scope, so the destructive class over the bound input arrives with record 0057's plan and this record flips no boolean. The policy table gains a config-scope: thread row with the channel scope's grant, so whoever may set the channel may set a thread in it. always at the winning scope means the adapter never asks for a verdict: no model call, no receipt, the 👀 as today, and the catch-up reads the same mode from the same config, so it treats the reply exactly as it does now. Under mention, the line that turns a thread back on needs a mention, which is what the person asked for when they set it.

Invariant: with always at the defaults layer and no narrower setting, every adapter and catch-up test that passes today passes unchanged.

Why not X ​

Why not just require a mention? A mention-only bot is what Copilot's and Cursor's Slack agents do, and it would silence what our loops depend on: a plain reply that steers a live run (thread-admission item 1), a follow-up that continues a finished run's session without a directive (item 6), and the typed answer records 0054 and 0057 read from the next message. It stays as a mode; the maintainer rejected it as the default.

Why not make silence the operator's own outcome? The first version of this record did. The operator runs after the ack, so its silence would come after a 👀, and moving a 60,000-token turn ahead of every ack would make every event wait on it. A cheap gate ahead of the expensive turn is the shouldReply pattern as published, and it lets each turn read the inputs it needs.

Why not tell the answering model to stay quiet when it is not addressed? Devin tried the note "only contribute if you are needed" on each message and reports that a model trained to answer questions answers them. By the time our model runs, the 👀 and the card have posted and a workspace has attached; silence after a card is worse than an answer.

Why not answer only when the bot spoke recently? Recency is one of the five facts. As the rule it silences the requester coming back to a finished review an hour later and answers the teammate who replied a minute after the card.

Why not gate only idle threads and leave a live run's steering as 0051 wrote it? Because the steer is the costlier mistake: a colleague's remark during a coding run is folded into the run as an instruction at its next step, and the steer's own ack line is a reply in the thread. The live-run thread is where the verdict matters most, which is why the amendment is worth its cost.

Why not wait for the model-driven thread owner that record 0051 names next? That record decides which owner receives a message the bot handles; this one decides whether the bot handles it. Its gate is a measurement over a hundred owned threads or thirty days, and the noise is today's.

Boundaries ​

The verdict governs one message class: an unmentioned reply in a channel thread the bot is part of. It never gates a mention, a direct message, a top-level post, a thread_broadcast reply, an HTTP or MCP request, or a Linear session, whose events (mention, delegation, follow-up, stop) are addressed to the bot by construction. It does not decide which agent runs, who owns a thread, or whether an addressed message is steered or bound; those stay with the router today and the operator under 0057. Multi-bot arbitration is out of scope while the workspace has one bot. The prompt, the labelled set, the remaining validation rows and the thread page's rendering of silent receipts belong to the plan. No migration: the receipts table is new, and every message older than the window has no receipt and needs none.

What would change our mind ​

  • The live false-silence ratio sits above 5 percent after two weeks at classify. The prompt, the facts or the model change; the receipts name the replies to study. If it stays there after that, the default is reconsidered with the maintainer, with the ratio as the evidence.
  • People read the delayed 👀 as being ignored. If the first week's slack.receive spans show the verdict's p95 above three seconds, the 👀 moves before the verdict and is removed on silent, at the cost of a visible flicker.
  • Reversibility. One release carries the gate, the verdict, the receipts table and its catch-up read, the pending-by-thread read, the setting on four scopes with config set thread and its policy row, the span attributes and the 0051 amendment, behind the mode; the silent-receipt rows on the thread's page and the replay row follow in the next. intake: { threadReplies: always } at the defaults layer restores today with the table idle; a superseding record drops the table by migration.

Validation criteria ​

The rows below are the ones whose failure would falsify the record; the plan binds the rest (the facts, the mode's scopes, the page rows).

CriterionProof
A mention, a direct message and a top-level post never enter the verdict in any mode; always never calls it or the ledger and matches today's call sequence byte for byte against a golden recorded before the gate existed[gap] plan unit four: src/channels/slack.test.ts
No code decides the verdict from the words: the facts are inputs to the turn; the chat-text lint rule is record 0057's plan's, and until it lands review holds the line with the trigger module's entity scan as the one allowed scan[gap] review on plan units one and four; the rule's own test when 0057's plan lands it
The guard's claim precedes the first await; the receipt insert answers inserted; a live redelivery of a decided message and a catch-up candidate a live claim overtook each produce no second verdict and no second action in one process[gap] plan unit two: src/core/runLedger/inMemory.test.ts; plan unit four: src/channels/slack/dedupe.test.ts, src/channels/slack.test.ts; plan unit five: src/channels/slackCatchUp.test.ts
With the null ledger, or a failing write or read, classify still calls the model, acts on addressed, stays silent on silent, and marks the span; the startup log prints the degraded line once[gap] plan unit one: src/core/intake.test.ts; plan unit four: src/channels/slack.test.ts
The receipt's write attempt precedes the 👀 and the downloads in classify and mention; the verdict sees the newest turns of a thread longer than 50 replies on both paths[gap] plan unit four: src/channels/slack.test.ts; plan unit five: src/channels/slackCatchUp.test.ts
The catch-up reads the receipt before handle(), skips a silent one and marks it seen, dispatches an addressed one with no bot reply after it and no 👀 in grace without a model call and without a second decision, and decides a receipt-less one with the same function[gap] plan unit five: src/channels/slackCatchUp.test.ts, src/channels/slack.test.ts
config set thread resolves above user and channel, is authorized by the config-scope: thread row, and a routed call shows the button under record 0044's built-in write default; the destructive class over the bound input is record 0057's plan's[gap] plan unit six: src/config.test.ts, src/core/authz/policy.test.ts, src/core/dispatch/confirm.test.ts
Record 0051 carries a dated re-evaluation section, appended in the pull request that first changes steering[gap] plan unit four: decisions:check on the amended record
The offline set publishes both conditional rates with Wilson intervals and the unsure and timeout shares, and records the model ref it ran; the live false-silence ratio is computed from the receipts weekly[gap] plan unit eight: src/load/intakeReplay.test.ts, load -- intake, counts recorded on the tracker; human-gated labelling
Live: a silent verdict followed by a bot roll inside the window produces no run from the next generation[agent] a staging channel set to mention, an unmentioned reply from a person, POST /admin/crash, the next generation's slack.catch_up line showing silenced: 1; recorded on the tracker

Sources ​

  • The maintainer's frame and decisions, 2026-09-17, in the session that wrote this record; the run sample is the newest 200 runs on the dashboard's list at survey time, 11.5 hours of activity; the three-arm adversarial review of 2026-09-18, whose findings the amendment lists.
  • slack-channel.md items 1, 2, 7 and 9; thread-admission.md items 1, 5 and 6; routing-and-config.md items 3 and 21; run-history.md items 35 to 42; records 0012, 0037, 0044, 0046, 0051, 0052, 0054, 0057.
  • Devin, "Devin's Slack Etiquette"; Matt Webb, "Multiplayer AI chat and conversational turn-taking" (the shouldReply pattern); "An LLM Benchmark for Addressee Recognition in Multi-modal Multi-party Dialogue" (2025); Anthropic's Slack app's per-channel "respond automatically" toggle.

Amended 2026-09-18 — built and live; two rows stay open on the tracker ​

All eight units of the plan are merged and running in production 1.252.0, so the status flips to implemented. The record was accepted to stop every bot thread being a place people cannot talk in, gated on the verdict's error rates; the gate, the receipt, the catch-up read, the thread scope, the thread view's silent rows and the replay row all landed as written. The re-evaluation against that reasoning: two validation rows are open on the tracker rather than done. The labelled replay behind load -- intake runs, but no real labelled set exists yet: the channels hold four unmentioned replies, all one shape, too few and too uniform to stratify or to bound either rate, so the offline rates it prints are not yet evidence and the live false-silence ratio from the receipts is the only gate in force. And the live crash-survival row — a silent verdict followed by a bot roll inside the window produces no run from the next generation — is human-gated as written and has not yet been run against production. What could regress while they stay open: a prompt or model change judged only by the replay's four replies, and a restart regression the unit tests model but no live receipt yet proves.