Session provenance — trace inbox actions and spawned sessions to their origin
When many agents run at once it is not obvious which one caused an inbox item, a spawned session or an injected turn. Provenance answers that with a clickable link back to the origin: a generated-by line on approvals and alerts, a spawned-by note atop a new session, a children chip on the parent, a via-CLI tag on injected turns.
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:
- 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. - 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.
- 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.
- 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/sessionsor/project/<name>/new) get the pinned "Spawned by …" note linking to their parent.Handed-off successors — a session created by session handoff 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:
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-nows, 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 — sendsX-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)insrc/main/services/cli/cli-source-session.ts— in-app token session wins, otherwise the validatedX-AMC-Source-Session-Idheader. - Storage: nullable columns
cli_pending_actions.source_session_id,cron_jobs.source_session_id,automations.source_session_id, andsessions.parent_session_id(one timestamp-ledger migration). - Spawn note:
createSessionWithPrompt({ parentSessionId })pins anotable-system"Spawned by …" message first, and never adds it to the prompt the agent reads. It renders clickable viaparseSpawnedByNote+ the sharednavigateToSessionhelper, inMessageBubble's system-message branch. - Children (reverse link):
getChildSessions(parentId)over thesession:get-childrenIPC backs the desktopSpawnedSessionsChipheader 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
ProvenanceRowsrow of itsDetailsBlock(in the table, per the table-by-default standard);ApprovalPaneShell'ssourceSessionId/generatedAtprops still render the same "Generated by …" line viaSourceSessionLink(detail view only, never the compact row) for the whole-prose panes with no table and for the agent-alert detail viewAlertInboxViewer(src/renderer/src/features/alerts/AlertInboxViewer.tsx) renders the same line from the alert's ownsourceSessionIdsnapshot 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 sharednavigateToSessionhelper (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))insrc/main/services/cli/cli-require-source.ts— resolves a source session OR a validated active cron and writes a hard400(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 (viasessionService.sendResponse/deliverPeerMessage), whichMessageBubblerenders as theCliSourceChip("via CLI · from …")./schedule-responsedelivers later, so its origin is persisted in the nullablesessions.scheduled_response_cli_sourcecolumn (one ledger migration) and re-stamped on the delivered turn. - Full invariants + tests:
.claude/memory/contracts/session-provenance-contract.md.
Related
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 is the map.
Last verified 2026-10-05