recdep: the queue contract
Read this to write a producer or a consumer.
telescreen consumes a filesystem queue. Anything that writes conforming records is a valid producer: an LLM agent, a deterministic poller in Go or shell, a webhook receiver. This document is the normative contract between the two sides. Terms are defined in the Vocabulary.
Roles
- Producer (reference implementation: minitrue, a headless agent run on
a systemd timer): polls upstream sources, writes records into
tube/, ownssince. - Consumer (telescreen, this repo): renders the queue and renames
records between drawers. Never touches the network, never writes
record content, never advances
since.
Layout
State root: ${XDG_STATE_HOME:-$HOME/.local/state}/recdep/
| Path | Contents |
|---|---|
since |
An ISO-8601 UTC instant, the moment the producer last polled through. Producer-owned; the consumer never reads or writes it. |
tube/ |
Landed, unseen records. Presence means unseen. |
desk/ |
Seen, still needs action. |
upsub/ |
Acted on, the other side's move is pending. |
files/ |
Closed, nothing more expected. |
intents/ |
Intents and approvals, written only by the consumer (the TUI). |
An intent (<record-name>.intent) records one dictation: an
entry <absolute record path> line, an action <mapped action> line,
then a guidance: section with free text, possibly empty. The
speakwrite agent consumes each intent and removes the file.
An approval (<record-name>.publish) is a single
entry <absolute record path> line, written only after an explicit
double-key approval on a drafted record: the recorded consent for the
one outward write. The actor (thinkpol) consumes each approval, posts
the draft, and removes the file. The consumer may also remove a pending
approval when the human discards the draft: the revocation of that
consent.
The producer writes only into tube/ and creates missing directories
at startup. The consumer moves files between the four drawers with
plain renames and creates missing directories, intents/ included, at
startup. Files, not sockets or databases, are the whole interface.
The queue is private to the user: components create directories 0700
and new files 0600 (recommended for existing ones too; telescreen
verify warns otherwise but never chmods). Encryption at rest is a
deployment choice (full-disk encryption, or a gocryptfs/fscrypt mount
of the state root) rather than a component concern, because append-only
plaintext is what keeps the queue auditable by cat.
Records
One file per hit, named <UTC>-<source>-<slug>.md:
<UTC>isYYYYMMDDTHHMMSSZ(UTC), so lexical order is time order.<source>is a short tag such asslack,github,linear.<slug>is a short kebab-case hint for humans readingls.
Body:
[<source>] <who>: <one-line summary>
<link>
seen <produce-run-time>
<preview>
- Line 1:
[<source>]tag, the author, a colon, and a one-line summary. - Line 2: the canonical URL of the triggering event.
- Line 3:
seen <ISO-8601 instant>, when the producer observed it. - Preview (optional): after one blank line, the triggering content
quoted verbatim (a Slack reply, a review comment body, a Linear
comment). Cap it at roughly 15 lines or 1000 characters and append
[...]when truncated.
The consumer parses lines 1 and 2 for the list view and shows the full body in the detail pane. Unknown or missing pieces degrade to empty fields, so a minimal producer can emit only line 1 and still render.
Producer obligations
- Write each hit exactly once. Dedupe across polls is the producer's
job (for example, a seen-list file next to
since). - File activity strictly after
since, then advancesinceto the poll's start time. - Skip activity authored by the watched person; the queue is for what others did.
- On a partial outage (an auth-less source, a failed poll), file a degraded record naming the gap rather than failing silently.
- Records are append-only once written, with one sanctioned addition:
a revalidation pass may append a single marker line
stale <reason> <ISO-8601 time>(kebab-case reason, e.g.merged,closed,already-reviewed) to records intube/,desk/, orupsub/. The marker must start on its own line: prepend a newline when the file lacks a trailing one. Never mark a record twice; never touchfiles/. Never delete records; drawers beyondtube/belong to the consumer. The producer marks, the human files.
Markers
A marker is an appended section in a record. Each marker starts with
--- on its own line, the same discipline as the stale marker:
prepend a newline when the file lacks a trailing one.
--- dictated <ISO-8601 time>
<the guidance, copied from the intent>
--- draft <ISO-8601 time>
<the draft text>
--- published <ISO-8601 time> <URL>
--- discarded <ISO-8601 time>
| Marker | Appended by | Meaning |
|---|---|---|
dictated |
the speakwrite agent | The guidance from the consumed intent, kept so the draft can be audited against what was asked. |
draft |
the speakwrite agent | The draft text. A new draft supersedes earlier ones. |
published |
the actor | The post happened; the URL is the resulting permalink. May carry a section body: the text actually posted, required whenever the enrolled actor adapted the draft rather than posting it verbatim, so the record never lies about what went out. A verbatim actor omits it (the draft is the post). |
discarded |
the consumer | The draft is rejected. This append is the one consumer write to record content besides renames. |
These four kinds are the only recognized markers: a --- line with
any other kind is section text (a quoted diff, for example), not a
marker. Markers accumulate append-only; the last marker wins for
presentation.
The published marker records the actor's publish write, the one
outward-facing action in the whole system. The publish procedure, the
actor's single rename to upsub/, and the failure behavior (record
untouched, approval removed, nothing retries silently) are normative in
the Actor contract; every other move between drawers
belongs to the consumer. The speakwrite agent's writes are the dictated
and draft markers and the intent removals; it never posts, renames, or
touches approvals.
Consumer obligations
- Read and rename only, with two exceptions: the delete in point 4 and the discarded marker append above. Never otherwise edit record content. Stale markers are rendered, not written, by the consumer.
- Never call the upstream sources; the queue is the only input.
- Treat a failed read as a race with a concurrent move and retry on the next refresh.
- One destructive action exists: delete, which removes a record from
files/only, behind a double keypress on the same record. The removal is permanent; nothing returns from the memory hole.