---
title: "What is tundish?"
description: "Queues, subscriptions, local storage, delivery, and retention"
type: explanation
canonical_url: https://tundish.eng.rip/what-is-tundish/
---

# What is tundish?

tundish is a local inbox for agents. A program publishes JSON items into
a queue. A subscription selects items from that queue and renders them
as text. A hook pulls the text into an agent's session.

The publisher and the agent do not need to run at the same time. Items
stay in the queue until retention removes them. Reading advances the
subscriber's delivery state. An agent can receive a CI result or a Slack
message at its next hook event without keeping a connection to the publisher.

## Queues and subscriptions

A queue groups items from a source or for a purpose. An item has an id,
a JSON payload, and a creation time. A publisher can add a key, such as
`slack:C123`, to label items from one channel. The key is a label for
filtering. Repeated keys still produce separate items.

A subscription defines a view of one queue. Its filter decides which
items match. Its template decides what text the agent receives. Two
subscriptions can read the same queue with different filters or delivery
rules. The publisher does not need to know about either subscription.

A subscriber is a stable name, often an agent role such as `reviewer`.
It binds to a subscription. Its position records progress across pulls and
across sessions. With the default rules, each subscriber has its own
position. With `share one`, the subscription has one forward position
for all its subscribers. Pulls move it past each new item evaluated,
including items filtered out or skipped after a filter error. A held
lease does not keep other subscribers from receiving new work.

With `share all`, a new subscriber starts with every item still kept.
The `start new` rule starts it after the existing items. With `share one`,
this rule sets the subscription's initial position. Later subscribers
join at that position's current value. The `latest` selection rule serves
another purpose: it delivers a recent selection and skips older unseen
items. It suits a status digest where an old item is no longer useful.

Changing a filter keeps the position and existing claims. Items behind
the position do not return if they start matching the new filter. A
change to `share all` carries the position and clears leases. A change
to `share one` starts from the lowest subscriber position.

## Delivery and responsibility

`share` determines who receives an item. `consume` determines whether
delivery is enough or the item needs an ack.

The default, `all` with `on-read`, suits information for several agent
roles. Each role receives its own copy through its subscription binding.
Reading it does not require a further command.

With `all` and `on-ack`, several subscribers can see the same work. An
ack records who took responsibility. A second ack by another subscriber
exits 3 and names the owner. That subscriber can leave the item instead
of acting on it too. A repeat ack by the owner succeeds.

With `one` and `on-ack`, the pull itself grants a temporary lease. Every
subscriber, including the holder, skips the item while the lease holds.
If the holder stops without acking, the next pull retries the item
ahead of new work, within the item limit. The filter does not run again
on a retry. The item matched when first leased.

When the last allowed lease expires without an ack, tundish parks the
item and reports it once in a pull. This limits repeated delivery of
work that no subscriber finishes. A parked item is never retried, even
if the maximum number of attempts is raised later.

Each subscription owns its acks and leases. An ack in a `triage`
subscription does not take an item away from a separate `audit`
subscription on the same queue. The [delivery rules](cli-reference.md#delivery-rules)
list the combinations of `share` and `consume`.

## One local database

A hook already supplies the moment to run a pull. tundish does not need
a daemon to wait for that moment. A publish or pull opens the SQLite
database, does its work, and exits. SQLite holds the items, subscription
rules, positions, acks, and leases across those short processes.

SQLite uses WAL mode, and writes run in transactions. Concurrent hooks
therefore share one stored view of claims and positions. No network
listener or server sits between a publisher and the file. A publisher
that receives Slack events can hold its own network connection and call
the same Rust library through Python.

Each user has one database, set by `database:` in
`~/.config/tundish/config.yml`. It defaults to
`~/.local/state/tundish/tundish.db`. The publisher, handler, and agent
read the same config and reach the same database. The
[configuration](cli-reference.md#configuration) also controls retention.

## Pulls and hook events

A hook can call `pull --due` after every tool batch. See
[Due pulls](cli-reference.md#due-pulls) for delivery intervals and budget
rules. The hook supplies the occasion to deliver. An idle session
receives nothing until another hook event.

## At-least-once delivery

tundish prints and flushes the batch before committing the delivery.
If the process fails between those steps, the database still treats the
items as unread. A later pull can print them again. Committing first
would risk marking an item delivered before any output reached the hook.

gaff gives the handler a deadline through `GAFF_TIMEOUT_MS`. If that
deadline passes before commit, tundish rolls back and exits 1. gaff
discards the failed handler's output. A later pull can retry delivery.

The transaction covers delivery state inside tundish. It does not cover
an agent's actions in another program. An ack records responsibility;
it cannot prove that an external action finished or prevent that action
from happening twice.

## Retention

Retention limits the queue's stored history. The count limit waits for
unread items, so a slow subscriber can keep the queue above the configured
count. Once read positions permit it, a later publish removes the excess.
An unacked item already shown under `share all` has passed the read
position and can be removed by the count limit.

With `share one`, the count limit protects both the subscription's
position and its lowest open lease. Items from that lease onward stay
available while the work is unacked and not parked. A parked item no
longer holds retention. A subscription must have a bound subscriber to
hold retention through its position or leases.

An age limit makes a different choice. It removes old items even when
a subscriber has not read them. tundish records the number of unread
matching items lost. For `share one`, this includes items removed while
under an open lease. A pull reports that count once as `dropped=` in the
status line, even without delivering an item from
the subscription. That count distinguishes an empty queue from missing
history.

Retention runs on publish. With no daemon, an idle queue does not remove
items at the instant they reach the age limit. A queue absent from the
config uses a count limit of 10000 and no age limit. The
[configuration reference](cli-reference.md#configuration) defines the
per-queue settings.
