# Activity trace schema (studio report joins)

Canonical types and helpers live in **`@x12i/activix-contracts`** (`buildActivityTraceBlock`, `normalizeActivityTraceExport`). Directory export (`exportRecordsToDirectory`) attaches a **`trace`** sibling on each JSON file when `traceShape` is enabled (default **true**).

---

## Field naming (unified export)

| Export field (`trace.*`) | Meaning | Do not confuse with |
|--------------------------|---------|---------------------|
| **`activixCollection`** | Mongo / playground **collection name** for this row | Optimixer constructor option `activixCollection` (same string value, different layer — config vs persisted row) |
| **`activityKind`** | Lifecycle / IO discriminator (`graph:start`, `node:start`, `optimixer:prediction`, …) | Legacy scattered `kind` on input, `outer.metadata`, or top-level `metadata` |
| **`activityType`** | Domain label from **`outer.metadata.type`** (`synthesis`, `task`, `import`, …) | Removed ActivityTracker **`activityType`** routing (use explicit Activix `collection` instead) |

Persisted documents may still contain legacy slots (`outer.input.kind`, `outer.metadata.kind`, `metadata.kind`). Exports add **`trace.activityKind`** so studio reports do not need to reimplement the resolution cascade.

---

## Correlation: `sessionId`, `jobId`, `masterSkillActivityId`

All correlation ids live in **`runContext`** at write time (see [run-context-object.md](./run-context-object.md)).

### Graph runs (`jobId` convention)

| Layer | Typical `runContext` | Notes |
|-------|---------------------|-------|
| Job / gateway | `sessionId`, `jobId`, `jobTypeId` | Gateway often sets **`sessionId` === `jobId`** for a single job boundary |
| Graph run | `jobId`, `graphId` (no `nodeId`) | Graph-level activity: `activityKind` e.g. `graph:start` |
| Node | `jobId`, `graphId`, `nodeId` | Node-level activity: `activityKind` e.g. `node:start` |
| Master skill | `masterSkillActivityId` | Product-defined: usually the Activix **`activityId`** of the master-skill activity row for this invocation — **not** auto-generated by Activix |

Rules:

1. Downstream services **inherit** upstream `sessionId` / `jobId`; do not mint new values per hop.
2. **`activityId`** is always the Activix primary key on **this** row; it is **not** copied into `runContext`.
3. Query graph timelines with **`getJobActivities({ jobId, graphId?, nodeId? })`** ([runtime-observability-querying.md](./runtime-observability-querying.md)).

Export mirror: `trace.correlation.sessionId`, `trace.correlation.jobId`, `trace.correlation.masterSkillActivityId`, etc.

---

## Join: graph-engine nodes ↔ ai-tasks `runTask.metadata.activityId`

This monorepo does not ship graph-engine or ai-tasks; consumers align on these join keys:

| Role | Activix row | Join key in export |
|------|-------------|-------------------|
| Graph-engine **node** activity | Written by graph-engine with `runContext: { jobId, graphId, nodeId }` | **`trace.join.activityId`** — studio matches ai-tasks metadata against this value |
| ai-tasks **runTask** child | Should set **`runTask.metadata.activityId`** to the parent graph node’s Activix `activityId` | Export exposes **`trace.join.parentActivityId`** when detected from `metadata.activityId`, `metadata.runTask.metadata.activityId`, or `runContext.parentActivityId` |

**Studio join (Mongo or export JSON):**

```text
graph_node.trace.join.activityId  ==  ai_task.trace.join.parentActivityId
```

(or equivalently `ai_task.metadata.runTask.metadata.activityId` on the raw row before export normalization).

---

## Observability: `synthesisEnabled`, `modelUsed`

Stable export path: **`trace.observability`**.

| Field | Resolution order (first wins) |
|-------|------------------------------|
| **`modelUsed`** | `metadata.modelUsed` → `metadata.model` → `config.model` → `outer.metadata.model` |
| **`provider`** | `metadata.provider` → `config.provider` → `outer.metadata.provider` |
| **`maxTokens`** | `config.maxTokens` / `config.max_tokens` → `outer.metadata.maxTokens` |
| **`synthesisEnabled`** | `metadata.synthesisEnabled` → `metadata.enableSynthesis` → `metadata.synthesis === true` → `outer.metadata.synthesisEnabled` → inferred **true** when `trace.activityType === "synthesis"` |

Top-level **`metadata`** remains the orchestrator tier ([activity-structure.md](./activity-structure.md)); exports surface the merged observability view without hoisting fields into `outer`.

---

## Example export fragment

```json
{
  "activityId": "act-node-abc",
  "status": "completed",
  "runContext": {
    "sessionId": "job-20240315-001",
    "jobId": "job-20240315-001",
    "graphId": "g1",
    "nodeId": "node-a"
  },
  "outer": {
    "input": { "kind": "node:start" },
    "output": { "ok": true },
    "metadata": { "type": "task", "kind": "node:start" }
  },
  "trace": {
    "activixCollection": "graph-engine-activities",
    "activityKind": "node:start",
    "activityType": "task",
    "correlation": {
      "sessionId": "job-20240315-001",
      "jobId": "job-20240315-001",
      "graphId": "g1",
      "nodeId": "node-a"
    },
    "join": {
      "activityId": "act-node-abc"
    },
    "observability": {
      "synthesisEnabled": false
    }
  }
}
```

---

## Disabling trace shaping

```ts
await activix.exportAllRecordsToDirectory('/tmp/export', { traceShape: false });
```

When disabled, JSON files contain the raw persisted document only (pre-P2 behavior).
