Provenance: who did what
Not every turn in Paddock is typed by a human. A chat can be started by a schedule firing on cron, or spawned by another chat that fanned out work; a message can be injected into an already-open chat by a schedule or by a sibling chat reporting back. In the UI these look identical to something you did yourself — so Paddock records provenance: who or what caused each chat, and each machine-added turn, to exist.
This page explains the model. To see the tool-level detail of what Claude did once a chat is running, see Reading Claude’s work.
The provenance marker: origin + depth
Section titled “The provenance marker: origin + depth”Every server-initiated turn carries a small marker. Two fields are always present:
origin— how the chat came to exist:human,scheduled,spawned,hook, oradopted.depth— how many spawn hops it is from the human (or scheduled) root of its tree. A human-started chat isdepth: 0; a chat it spawns isdepth: 1; that child’s own children aredepth: 2; and so on.
Two more appear when the chat has an identifiable parent —
parentSessionId and parentProject, naming that chat. depth says
how far a chat is from its root; these say which chat it came from, and
that’s what lets the sidebar draw the tree
(below).
So a human-started chat is { origin: human, depth: 0 } — the root of any
fan-out tree. A chat Claude creates with a self-management tool becomes
{ origin: spawned, depth: parent.depth + 1 }, and a cron-fired chat is
{ origin: scheduled, depth: 0 } — a schedule is a root trigger, just like a
human. A chat imported from your terminal claude
history is
{ origin: adopted, depth: 0 }: nothing here created it, so it is a root too.
The marker is stamped once, at chat creation, and is never overwritten by a
later turn. Resuming, waking, or sending a message into an existing chat leaves
its recorded provenance intact — provenance describes how a chat was born, not
what last happened in it. It’s persisted in a per-chat server sidecar
(RunProvenanceStore), the same durable side-metadata pattern Paddock uses for a
chat’s archived flag and your read state.
Chat-list badges
Section titled “Chat-list badges”The per-project chat list turns the marker into a small, subtle icon badge, so the “ran without me” chats stand out at a glance while ordinary human chats stay unadorned:

- Scheduled chats — a schedule started them — show an amber clock.
- Spawned chats — another chat created them — show a violet branch icon (with a note of how many levels deep, when it’s more than one).
- Hook chats — an event hook fired them — show a sky bolt.
- Imported chats — brought in from your Claude Code CLI history — show an emerald terminal.
- Human chats — the default — show no badge, so only the chats you did not start here draw the eye.
The same origin colors reappear in the project’s History tab, which lists recent runs (You / Scheduled / Spawned / Hook / Imported) so the work that happened while you were away is easy to find and open.
Per-message attribution
Section titled “Per-message attribution”A single chat can interleave turns you typed with turns a machine injected — a schedule firing into the chat it owns, or a sibling chat sending a message. So provenance also works at the per-message level: a machine-injected user turn gets a subtle attribution line above its bubble, while your own messages stay unlabelled (the quiet default).

The wording names the source:
- ⏰ scheduled by ⟨name⟩ — a schedule fire injected the turn.
- ↩ sent by ⟨chat⟩ — another chat sent it; the chat name is a link straight to the sender.
- ⚡ triggered by hook ⟨name⟩ — an event hook fired it.
- ⚠ continued after a background task was terminated — Paddock’s chat recovery nudged the turn.
Under the hood this is a separate sidecar (MessageProvenanceStore) that records,
per chat, an ordered list of injections with their sender and the exact text
injected; at render time Paddock matches each machine-injected message to the next
recorded injection. Because the injected prompt lands verbatim as the message
content, the match is stable — and a human-typed message never matches, so it’s
never mislabelled.
Injected turns stream in live
Section titled “Injected turns stream in live”Attribution isn’t only a history feature. When a message is injected into a chat you already have open, it now streams in immediately over the WebSocket — you see the incoming turn and its attribution appear in place, rather than only the reply showing up and having to refresh to learn where it came from.
From badge to structure
Section titled “From badge to structure”Provenance used to only decorate the chat list. It now shapes it: a chat with a recorded parent is drawn nested underneath that parent, so a fan-out reads as one foldable family instead of a wall of sibling rows. The chat-list guide covers what that looks like; this is where the edge comes from.
Paddock resolves each chat’s parent in two tiers, behind one override — three
checks in this order (makeParentResolver in chat-dto.ts):
- An explicit detach. If you detached the chat from its parent, it is a root, full stop — checked ahead of both tiers.
- The recorded edge.
parentSessionId+parentProjecton the chat’s own provenance marker, written when the chat was created. Authoritative. - An inferred edge. Failing that, Paddock looks at the chat’s message provenance and takes the first turn injected by another chat — because a spawned chat’s kickoff prompt is injected by whoever spawned it. This is a best guess, not a recorded fact.
Why detach needs its own override rather than just clearing the edge: most live
edges are inferred (tier 2), and inference re-derives the edge from message
provenance on every load. Clearing a recorded edge would therefore accomplish nothing
— the next list render would hand the chat straight back to its old parent. So detach
is stored as a positive flag in its own sidecar (parent-detach.json) and consulted
first. Nothing is destroyed: the recorded edge stays put, the override simply wins.
Tier 2 exists because tier 1 is new. There is no migration: chats created before nesting shipped have no recorded parent and never will, so the inference is what recovers most of their lineage at read time. It recovers a lot, but not everything — a chat forked with no kickoff prompt injected nothing into its child, so there is no signal at all and that child stays a permanent root.
One guard rides on top of tier 2: a chat with a recorded root marker is skipped
outright rather than falling through to inference. It isn’t missing an edge — it has
none. Without that guard the documented report-back workflow re-parents your own chat:
a manager spawns a child, the child send_messages its report home, the manager now
carries a chat-injected turn and infers its own child as its parent. Both edges then
point at each other and the tree builder’s cycle guard decides, per render, which one
gets promoted to a root.
Which paths record a parent:
| How the chat was created | Parent recorded? | Where it lands |
|---|---|---|
Claude forks a chat (fork_chat, or the UI’s Fork) | Yes | Under the chat it was forked from — not under whoever ran the tool |
Claude spawns a chat (create_chat) | Yes | Under the chat that spawned it |
| You start a new chat | No | A root, correctly |
| A schedule or an event hook fires one | No | A root — a trigger is its own origin, not a child |
An external MCP client calls create_chat over /mcp | No | A root — the caller isn’t a chat, so there’s nothing to nest under |
create_chat used to rely on inference; since #509 it records the edge like
fork_chat does, so inference is now purely a backfill for pre-nesting chats rather
than the live mechanism for spawned ones. Both tools resolve the parent the same way —
the chat the tool is running in — which is also why the external /mcp path records
nothing: there is no current chat to name.
Why it matters
Section titled “Why it matters”As Claude does more unattended work — scheduled triage, a manager chat fanning out sub-tasks, hooks reacting to events — a project’s chat list stops being purely “conversations I had.” Provenance keeps it legible: at a glance you can tell your own threads from the ones that ran on a timer or were spawned by another chat, and within a chat you can tell which turns a machine added. It’s the connective tissue that makes autonomous work reviewable instead of mysterious.
Next steps
Section titled “Next steps”- Reading Claude’s work — the tool-level view of what a chat actually did.
- Chats are sessions — what a chat is, and how forking copies one.
- The sweeper — the post-turn curation agent, itself a non-human actor.