---
id: PRD-0013
title: "M12 — message history and catch-up (7d retention, read-since)"
status: done
owner: "Aaron Roney"
created: 2026-07-03
updated: 2026-07-03

depends_on:
- PRD-0012

principles:
- "Catch-up is agent-driven: the agent computes the right read-since watermark and asks; the server just answers."
- "Retention is bounded and boring: 7 days, enforced server-side, no per-channel knobs in v1."
- "History honors the same visibility/ACL rules as live fan-out — no retroactive leaks after a kick/ban."
- "Channels only: whisper history is excluded from v1 by decision (2026-07-03) — a whisper is ephemeral."

references:
- name: "DESIGN.md §15 (persistence), §8 (delivery semantics)"
  url: https://github.com/twitchax/conclave/blob/main/docs/DESIGN.md

acceptance_tests:
- id: uat-001
  name: "A message posted while a session is offline is returned by read-since after it reconnects"
  command: cargo make test
  uat_status: verified
- id: uat-002
  name: "Messages older than the retention window are purged and never returned"
  command: cargo make test
  uat_status: verified
- id: uat-003
  name: "read-since on a channel the caller cannot see (or was banned from) is refused visibility-uniformly"
  command: cargo make test
  uat_status: verified

tasks:
- id: T-001
  title: "Persist channel messages with server-stamped timestamps (7d retention, background purge)"
  priority: 1
  status: done
  notes: "New message table keyed (channel, ts); cascade on channel delete/rename like bans. Whispers are NOT persisted (excluded from v1)."
- id: T-002
  title: "ReadSince wire op + server handler (channel scope, paginated/capped)"
  priority: 1
  status: done
  notes: "Append-only protocol evolution (new request variant; response only sent to callers that asked). Cap the page size; enforce membership/visibility at read time."
- id: T-003
  title: "Bridge tool `catch_up(server?, channel?, since?)` — agent computes the watermark"
  priority: 2
  status: done
  notes: "Default since = last time this session saw the channel (bridge can track last-delivered per channel); the injected result frames each message like live traffic (untrusted data)."
- id: T-004
  title: "CLI: `conclave tail --since <duration>`, plus tail reconnect + friendly link-loss errors"
  priority: 2
  status: done
  notes: "Human parity with the agent flow; reuses the same wire op. Also fixes the live-observed wart: tail died with a raw rustls error on server restart — it now reconnects with backoff and re-reads from its watermark."
- id: T-005
  title: "Docs + skill: the 'catch up' flow, retention semantics, privacy note"
  priority: 3
  status: done
  notes: "Skill: 'ask your agent to catch up on #ops'. DESIGN: retention window, purge, and the visibility rules history inherits."
---

# Summary

The server holds 7 days of channel traffic (whispers stay ephemeral), and clients can ask for
"everything since X". A session that was offline (bridge death, machine asleep) stops missing the conversation: the
user asks their agent to "catch up", the agent computes the right read-since watermark (e.g. the
last message it saw, or "24h"), makes one call, and gets the backlog injected in order.

# Problem

Delivery today is fan-out-only, at-most-once, to live sessions (DESIGN §8). A joining or
reconnecting session sees nothing prior — observed immediately in live testing: every fresh
`join_channel` lands in an apparently-empty room, and any bridge downtime is a permanent gap.
Agents are exactly the clients that can make good use of history (they can summarize a backlog
instead of spamming the human), but there is nothing to read.

# Goals

1. No permanent gaps: a reconnecting session can recover everything inside the retention window.
2. An agent-friendly catch-up flow: one tool call, ordered results, framed as untrusted data.
3. Bounded storage: old traffic ages out server-side; no unbounded tables.

# Technical Approach

Sketch (to be refined when scheduled): a `message` table (scope key, server timestamp, from-path,
payload envelope) written on the fan-out path; a background purge task (retention 7d, constant);
an appended `ReadSince` request/response pair in the protocol (append-only evolution — responses
only flow to callers that sent the new request, so old peers are unaffected); a bridge `catch_up`
tool that resolves the watermark from per-channel last-delivered state; ACL/visibility checks at
read time so history never outlives membership. The E2E-ready payload envelope already on the wire
(`Payload`) is stored verbatim, so PRD-0010 (E2E encryption) composes: the server stores ciphertext
it cannot read.

# Assumptions

- 7d and the page cap are constants in v1; per-channel retention knobs are future work.

# Constraints

- Storage growth must be bounded (retention purge is part of T-001, not a follow-up).
- History reads must be visibility-uniform (a non-member learns nothing about a private channel's
  existence, consistent with PRD-0007 hardening).

# References to Code

- `src/server/hub.rs` (`post` / `whisper` fan-out paths), `src/store.rs` (schema + purge),
  `src/protocol.rs` (append-only evolution pattern), `src/bridge.rs` (tools + per-channel state).

# Non-Goals (MVP)

- Whisper history (excluded by decision 2026-07-03: whispers stay ephemeral, fan-out-only).
- Read receipts / unread counts; server-tracked per-session cursors (the agent owns the watermark).
- Editing, deletion, or search of history.
- Retention configuration surface (flags/env) — constant in v1.

# History

- 2026-07-03: Drafted from live-testing feedback ("a client can ask their agent to catch up").
- 2026-07-03: Scoped to channels only (whisper history excluded); absorbed the tail reconnect fix into T-004; activated.
- 2026-07-03: All five tasks landed: message retention (7d, hourly purge, cascades), ReadSince wire op + visibility-uniform server reads, the catch_up bridge tool with watermark defaults, tail --since + reconnect-with-resume, and docs. Gate green (198 tests, 11 new).
