---
title: Session provenance — trace inbox actions and spawned sessions to their origin
---

# Session provenance — trace inbox actions and spawned sessions to their origin

## What it is

When you run many Claude agents at once, things show up in Omniscio's inbox — an approval to
review, a cron job to confirm, a new session that appeared — and it isn't always obvious _which_ agent caused
them. Session provenance answers that: any action an agent triggers through Omniscio's local control API, and any
session an agent spawns, carries a clickable link back to the agent that did it.

Four places you'll see it:

1. **In an inbox item's detail view** — open a pending approval (a cron job, an automation rule, a queued
   CLI action, a recipe step, a spawned-session request, …) and near the top you'll see **"Generated by
   <session name>"**. Click the name to jump straight to that session, and the same line also shows the exact
   **date and time** it was generated (e.g. _"Generated by <session> · July 11, 1:48 PM"_), so you can tell at
   a glance how long it has been waiting. An **agent-raised inbox alert** (a row an agent dropped via
   `POST /alert`) carries the same clickable link, but places it where a chat message puts its sender: an
   alert whose body is TEXT opens as the message it is, and the session's name sits in the message bubble's
   own top row beside its timestamp. A card whose session was since reaped keeps the link and shows a short
   id instead of the name — losing the answer path, never the origin.
2. **At the top of a spawned session** — when one agent spawns another, the new session's conversation opens
   with a pinned note: **"Spawned by <session name>"**, linking back to the parent. Follow the chain and you
   can walk the whole trail of who-spawned-what.
3. **On a parent session's header** — if a session spawned any children, a **"Spawned N"** chip appears next
   to its title (desktop; not on an agent-crew session, whose slim header leaves its helpers to the crew's
   roster in the Overseers hub). Click it for a dropdown of those child sessions — each with a status dot — and
   click any one to jump to it. The list is fetched fresh each time and includes archived children, so it is
   the reverse of the "Spawned by" note: walk from a parent down to everything it started.
4. **On a message an agent injected over the API** — when an agent sends a turn _into_ another session
   through the control API (a peer message, a nudge, a directly-sent turn, or a "send this later" reply), that
   turn's bubble carries a small **"via CLI · from <session name>"** tag linking back to the sender — so an
   injected message reads as _from that agent_ instead of an anonymous "You" turn. A turn you typed yourself
   shows nothing.

The link uses Omniscio's in-app session link (`omniscio://session/<id>`), the same one search results use — clicking
it switches you to that session, loading it from the archive if needed.

## Where to find it

You meet it in four places, and there is no screen of its own.

In an inbox item's detail view — a pending approval, or an alert an agent raised — the naming session is one click away: a "Generated by" line near the top on a pending approval (with the date and time it was generated), and the message's own sender row on an agent-raised TEXT alert. At the top of a spawned session's conversation there is a pinned "Spawned by" note linking back to its parent. A parent session's header carries a "Spawned N" chip on desktop (except an agent-crew session, whose header is kept slim); click it for a dropdown of the sessions it started, each with a status dot. And a turn another agent injected over the API carries a small "via CLI · from" tag on its message bubble, so it does not read as an anonymous turn of your own.

## How it behaves

### Why it sometimes shows nothing

Omniscio's control API uses one shared access token, so the server can't tell which agent made a call unless the
agent says so. Agents that Omniscio itself spawned know their own session id (Omniscio puts it in their environment as
`AMC_SESSION_ID`) and send it automatically, so their actions are traced. But a call made by hand (a `curl`
you typed), by an external script, or by a tool that doesn't send the id, simply records **no origin** — you'll
see no sender row and no "Generated by" line, and no spawn note. That's by design: provenance is a breadcrumb, not a security
check, so an unknown origin is shown as nothing rather than guessed.

**But the consequential actions now REQUIRE a source** — they don't just record it. Spawning a session and
changing a setting were the first; the requirement now also covers every command that **injects a turn into a
session** (a message, nudge, peer message, or scheduled reply) or that **spends money / spawns work
with no inbox approval** — running a recipe, a text-to-speech "speak", an email-summarizer backtest, starting
a coaching interview, kicking off a Nighty Tidy audit, or moving a session to another account. A command-line
call that doesn't identify its session is refused with a clear error instead of acting untraceably, so the
resulting approval card, activity-log row, or injected turn always names who asked — you never see an
anonymous one. (Omniscio's own agents always identify themselves, so this only ever trips a hand-typed or external
call that left the header off. Benign personal-preference commands — bookmarks, tags, keybindings,
scratchpads — are deliberately still optional, so local scripts keep working.)

For the same reason, the origin is trustworthy for Omniscio's own agents but is _self-declared_ — it isn't a
permission boundary, just a "who probably did this" hint. (The origin is self-declared, so never gate a security
or authorization decision on it — a provenance breadcrumb only.)

### What gets traced

- **Inbox actions created through the API** — the queue that holds approvals (tags, drip, away-mode, project
  docs, AI-coaching edits, intake sources, recipe runs, budget alerts, session pause/snooze/archive, and
  more) records the originating session on each item, so its detail view shows "Generated by …".
- **Cron jobs and automation rules** created through the API show their creator the same way.
- **Recipe approvals** show the session that ran the recipe.
- **Agent-raised inbox alerts** (`POST /alert`) record the session that raised them, so the alert's detail
  view shows "Generated by …" with a click-through to that session.
- **Spawned sessions** — sessions started via the API (`/agent/sessions` or `/project/<name>/new`) get the
  pinned "Spawned by …" note linking to their parent.
- **Handed-off successors** — a session created by [session handoff](session-handoff.md) gets the same pinned
  "Spawned by …" note pointing at the session it continues, and the parent gets a matching "Handed off to → …"
  note pointing forward. That pair is the recorded form of the link-back the operator used to keep by hand. Note
  that this is the same **self-declared trace breadcrumb** as every other row on this page, not a security
  boundary — but unlike an API caller's header, a handoff's parent link is written by Omniscio itself, so it
  cannot be spoofed by a caller.

- **Sessions started by a scheduled job or an automation rule** get their own pinned note — "Spawned by
  scheduled job ‹name›" / "Spawned by automation rule ‹name›" — naming the job or rule and linking straight
  to it. A scheduled job or automation rule is not a session, so there is no parent to point at; this is the
  non-session twin of the note above. It covers every way a job can start a session, including the ones that
  go through the approval queue first.

  It works in both directions: open a scheduled job (or expand an automation rule) and a chip shows the
  sessions **it** started, so you can go from a job to its work as easily as from a session to its cause.
  That list deliberately includes runs a job is set to keep hidden — inside the job's own panel the question
  is "what did THIS job start", and hiding its routine runs would blank the list for exactly the jobs that
  run most often.

  Two details worth knowing. A note saved before this feature existed still links, because Omniscio can
  recover the target from the note's own text. And on a phone, a scheduled-job link opens the job normally,
  but an automation-rule link tells you it is desktop-only rather than doing nothing — the Automations rules
  screen has no phone view yet.

Sessions Omniscio starts on its own with no such owner — a cron self-heal, the nightly tidy — have no origin
to record, so they correctly show none.

### For agents calling the API

If you are a Claude agent and want the actions you trigger to be traceable, send your session id as a header
on every call:

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  -H "X-AMC-Source-Session-Id: $AMC_SESSION_ID" \
  -H "Content-Type: application/json" \
  -d '{...}' http://127.0.0.1:19519/<endpoint>
```

`AMC_SESSION_ID` is in your environment if Omniscio spawned you. The header is optional for benign
personal-preference actions — leave it off and the action just records no origin — **but the consequential
ones are refused (a `400`) without it: spawning a session, changing a setting, injecting a turn (message /
nudge / peer-message / schedule-response), and the paid or session-spawning routes (`/recipes/run`,
`/voice/speak`, `/email-summarizer/backtest`, `/ai-coaching/interviews`, the Nighty Tidy `run-now`s, and
`/session/:id/move-account`).** (See the "omniscio-control" skill's Authentication section.)

**If you are a local program that has no session and never will**, do not invent a session id — the header is
checked against the real session list, so a made-up value is rejected exactly like sending nothing. Two
legitimate alternatives exist, each for one route family:

- A local **scheduled job** sends its own id in `X-AMC-Source-Cron-Job-Id` (validated against the job list) —
  that satisfies the spawn routes.
- A local **bootstrap** — today only `npm run setup`, applying this repo's team dev profile on a fresh install
  where no session exists yet — sends `X-AMC-Source-Origin: repo-setup`, which satisfies the **settings**
  route only. The accepted values are a short fixed list; anything not on it is rejected as before, so this is
  a second way to *name yourself*, not a way to skip the check. The approval card then reads
  "Requested by — Setup (command line)" instead of showing no origin at all.

**A `2xx` on a settings change means QUEUED, not applied.** A command-line settings change becomes an approval
in the user's inbox that they still have to accept, so never report it to them as already in effect.

## For agents

### Under the hood (for agents with repo access)

- Capture: `resolveSourceSessionId(req, auth)` in `src/main/services/cli/cli-source-session.ts` — in-app token
  session wins, otherwise the validated `X-AMC-Source-Session-Id` header.
- Storage: nullable columns `cli_pending_actions.source_session_id`, `cron_jobs.source_session_id`,
  `automations.source_session_id`, and `sessions.parent_session_id` (one timestamp-ledger migration).
- Spawn note: `createSessionWithPrompt({ parentSessionId })` pins a `notable-system` "Spawned by …" message
  first, and never adds it to the prompt the agent reads. It renders clickable via `parseSpawnedByNote` + the
  shared `navigateToSession` helper, in `MessageBubble`'s system-message branch.
- Children (reverse link): `getChildSessions(parentId)` over the `session:get-children` IPC backs the
  desktop `SpawnedSessionsChip` header chip — count + list from the authoritative DB fetch (archived
  included, account-agnostic), gated to the visible panel.
- Render: on an approval pane with a table, provenance is the LEADING `ProvenanceRows` row of its `DetailsBlock`
  (in the table, per the table-by-default standard); `ApprovalPaneShell`'s `sourceSessionId` / `generatedAt` props
  still render the same "Generated by …" line via `SourceSessionLink` (detail view only, never the compact row)
  for the whole-prose panes with no table and for the agent-alert detail view `AlertInboxViewer`
  (`src/renderer/src/features/alerts/AlertInboxViewer.tsx`) renders the same line from the alert's own
  `sourceSessionId` snapshot column. The link renders with a persistent accent color + underline (visible on
  touch, not hover-only) so it reads as a link. `SourceSessionLink`'s click routes through the shared
  `navigateToSession` helper (load-then-activate) — the same helper the "Spawned by" note uses — so an
  archived / out-of-project / evicted source opens reliably instead of a blank panel.
- Require-source guard: `requireCliSource(req, res, getAuthContext(req))` in
  `src/main/services/cli/cli-require-source.ts` — resolves a source session OR a validated active cron and
  writes a hard `400` (no off-switch) when neither is present. Applied per-route to the turn-injection +
  paid/spawning set (contract I11); benign personal-preference routes are deliberately left optional.
- Injected-turn origin (I12): the guarded route stamps `cliSourceMetadata(origin)` onto the turn it injects
  (via `sessionService.sendResponse` / `deliverPeerMessage`), which `MessageBubble`
  renders as the `CliSourceChip` ("via CLI · from …"). `/schedule-response` delivers later, so its origin is
  persisted in the nullable `sessions.scheduled_response_cli_source` column (one ledger migration) and
  re-stamped on the delivered turn.
- Full invariants + tests: `.claude/memory/contracts/session-provenance-contract.md`.

## Related

[session-handoff.md](session-handoff.md) is where the same spawned-by note comes from when a long chat is carried into a fresh session: the successor points back at what it continues, and the parent carries a matching forward link. For everything else this library holds, [INDEX.md](INDEX.md) is the map.
