Activity & Traces

A workspace keeps two records of what has happened in it. They answer different questions and deliberately do not overlap.

ActivityTraces
AnswersWhat changedWhat agents did
RecordsSkills, agents, workflows and artifacts added / updated / removed; members joining, leaving and changing role; tool proposals and their approvalRuns — chat conversations and sandbox skill executions — turn by turn, with status and cost
Endpoint/api/v1/activity/api/v1/traces
LiveWebSocket, via /api/v1/activity/subscribe-url

Why they are separate

They used to be one thing, and that was the bug.

Both records logged run lifecycle: an execution starting and finishing appeared in the change log and in the run index. One fact with two writers is a fact that drifts — the two would disagree about a cancelled run, or a rerun, or a run whose terminal hook fired twice, and there was no principled answer to which was right.

So the boundary is now enforced by the vocabulary itself. The activity type list has no member that can express a run. You cannot accidentally write one.

The rule of thumb:

  • “Who deleted that skill?” → activity
  • “What did that run cost?” → traces
  • “What has this workspace been up to?” → read both, merge on timestamp

Activity: the change log

Every row records a change to the workspace, who caused it, and when. Three families:

  • object.* — R2 object mutations. A skill, agent, workflow or artifact was created, updated or deleted. Each row carries the R2 key prefix it touched, so you can deep-link into the workspace filesystem without reconstructing the layout yourself.
  • member.* — joins, leaves, removals and role changes. A role change records the previous role too, so the log answers what changed, not just what it is now.
  • proposal.* — the approval-gated tool calls (running a skill, starting a workflow) and whether they were approved or rejected.

Rows are append-only. Nothing edits or deletes them; the log is the audit trail.

Attribution

Every row has an actor, and the actor is one of three kinds:

  • user — a person, with their display name resolved server-side
  • agent — an agent-initiated write with no attributable human
  • system — workspace seeding, migrations

There is no “unknown”. Writes that genuinely have no human behind them say so.

Live updates

The live channel carries one frame type, activity.appended, and its item is byte-identical to a row from the list endpoint. That is not a coincidence — it is why a client can prepend a live row into its first page without a refetch and without a second renderer that could drift.

The durable row is always written before the frame is broadcast. A dropped frame therefore costs you latency, never data: refetch page one on reconnect.

Traces: the run index

A trace is an index over stores that already own the turns, not a copy of them. A chat trace resolves its turns from the conversation store; an execution trace resolves them from the run’s own event log. Nothing is double-written, so no history had to be migrated for traces to exist.

Lineage

Each trace has a lineage key. A rerun inherits it and collapses into the trace it continues, so one lineage stays one story no matter how many times it was retried. A remix starts a fresh lineage and gets its own trace. That asymmetry is intentional: retrying is the same work, remixing is new work.

Handoff

Traces are how one agent picks up another’s work. POST /traces/:id/events appends turns — it never patches — so two agents that have never coordinated cannot overwrite each other. POST /traces/:id/continue returns the handle to resume the lineage, and deliberately starts nothing: looking at what happened and spending credits should not be the same gesture.

Visibility

Both records are visible to the whole workspace. Every member reads everything; there is no per-row scope, no sharing table, no grant.

Agents in a workspace already share its artifacts, memory and executions. Making one agent’s history opaque to another in the same workspace would buy nothing and cost a permission surface to configure, forget, and get wrong.

For OAuth clients the two scopes differ in sensitivity, and that difference is the reason they are separate scopes at all:

  • activity:read — not sensitive. Metadata about changes: what, by whom, when.
  • trace:read — sensitive. A trace is the full transcript of an agent’s work, including whatever a person typed into it.

A note on the dashboard heatmap

Tapping a day in the contribution heatmap filters the agent-activity panel to that day. The cell’s number and the list’s length will not match, and that is correct: the cell counts events (messages plus runs) from write-time counters, while the list counts lineages. A run retried three times is three events and one trace.