pleach
Operate

Lineage

Read-side cross-session dependency graph — typed edges between sessions, artifact provenance, and a queryable session-to-session graph for observability.

Lineage is one surface in the observability thematic island — siblings of observability (the orientation page), OTel spans, and runtime inspector. It's read-side, after the turn. For the write-side audit ledger (the per-call row that joins to lineage nodes on sessionId), see Audit ledger.

A session rarely exists in isolation. A user continues a previous conversation. A time-travel branch forks from a checkpoint. A subagent's output flows into a parent message. A long-running batch job's result imports into a fresh thread.

@pleach/core/lineage records those relationships explicitly: typed edges between sessions, plus optional artifact provenance, queryable as a graph.

import {
  LineageTracker,
  HARNESS_NATIVE_LINEAGE_ARTIFACT_TYPES,
} from "@pleach/core";
import type {
  SessionNode,
  LineageEdge,
  LineageRelation,
  LineageArtifact,
  SessionMetrics,
  HarnessNativeLineageArtifactType,
} from "@pleach/core";
Subpath@pleach/core/lineageSourcesrc/lineage/

LineageTracker

Producer + consumer in one surface. Lineage is opt-in: pass a LineageTracker as config.lineageTracker and the runtime records the session node at creation and a branched_from edge when a fork materializes a new session. The remaining relations (continued_from, used_result_of, delegated_from, planned_by) are recorded by the host at their trigger points — the tracker is a plain BaseStore-backed surface you call directly. Without an injected tracker, nothing is recorded. Consumers then walk the graph for "every session that depends on this one," "where did this artifact come from," "what forked off this turn."

const tracker = new LineageTracker(store, orgId, userId);

await tracker.registerSession({
  sessionId,
  chatId,
  intent: "research",
  createdAt: new Date().toISOString(),
});

await tracker.addEdge({
  fromSessionId: parentSessionId,
  toSessionId:   childSessionId,
  relation:      "branched_from",
  artifact:      { type: "checkpoint", sourceId: checkpointId, label: "Forked from checkpoint" },
  createdAt:     new Date().toISOString(),
});

The tracker is constructed with a BaseStore, an orgId, and a userId. Edges and nodes persist under the tenant-scoped key path the store enforces.

Edge relations

LineageRelation is a closed union. Each value carries semantic weight the runtime and consumers can rely on.

branched_from is recorded by the runtime on the fork path; the other four are recorded by the host at the trigger point shown.

RelationRecorded byTriggerConsumer use
continued_fromhostUser explicitly continues prior work"Show me the conversation history including the previous chat"
branched_fromruntimeFork / time-travel branch materializes a new session"What experiments forked off the same starting point"
used_result_ofhostSession B consumed a canvas card or job result from session AJob → chat provenance
delegated_fromhostSubagent spawned across a session boundarySubagent provenance UI
planned_byhostSession was created to fulfill a plan stepPlan → execution provenance

SessionNode

The graph's vertex shape, persisted at session creation via registerSession().

interface SessionNode {
  sessionId:    string;
  chatId:       string;
  intent?:      string;       // primary intent detected at session start
  agentSpec?:   string;       // agent spec used (if subagent session)
  summary?:     string;       // first ~200 chars of the opening message
  createdAt:    string;       // ISO-8601
  completedAt?: string;       // ISO-8601, set by completeSession()
  metrics?:     SessionMetrics;
}

Call completeSession(sessionId, metrics) at end-of-session to stamp completedAt and attach the aggregate metrics. This is a host call — the runtime does not invoke completeSession on your behalf, so completedAt/metrics stay unset until you record them.

Querying the graph

// All ancestors of a session (BFS, default depth 10):
const ancestors = await tracker.getAncestors(sessionId);

// All descendants:
const descendants = await tracker.getDescendants(sessionId);

// Self + ancestors + descendants + the edges between them:
const { nodes, edges } = await tracker.getChain(sessionId);

// Sessions that consumed a specific artifact:
const consumers = await tracker.findByArtifact("canvas_card", cardId);

getAncestors and getDescendants walk the graph breadth-first with a maxDepth parameter (default 10). getChain returns the full neighborhood plus the edges that connect it. findByArtifact filters edges where artifact.type and artifact.sourceId match — the read path for "show every session that used this card."

Artifact provenance

Artifacts ride on the edge — LineageArtifact names what was carried forward when one session feeds another.

interface LineageArtifact {
  type:     string;   // see HARNESS_NATIVE_LINEAGE_ARTIFACT_TYPES
  sourceId: string;   // identifier in the source session
  label:    string;   // human-readable label
}

The native artifact types ship as a readonly constant:

import { HARNESS_NATIVE_LINEAGE_ARTIFACT_TYPES } from "@pleach/core";

HARNESS_NATIVE_LINEAGE_ARTIFACT_TYPES;
// → ["canvas_card", "job_result", "plan_step", "file", "checkpoint"]

The type field is widened to string so host plugins can register domain-specific values. Consumers MUST NOT switch exhaustively on type — the union is open by design.

SessionMetrics

Per-session aggregates set by completeSession(). The shape is fixed:

interface SessionMetrics {
  toolCalls:        number;
  tokenUsage:       number;
  duration:         number;  // ms
  subagentsSpawned: number;
}

toolCalls, subagentsSpawned, and tokenUsage are the variable surface — they're what the lineage node carries that no structural invariant can derive.

Persistence

The tracker writes nodes and edges through BaseStore keyed on [orgId, userId, "lineage", "nodes" | "edges"]. Edges key on "${fromSessionId}→${toSessionId}" — re-writing the same edge overwrites in place; the tracker doesn't validate idempotency beyond key equality.

Use cases

ScenarioWhat lineage gives you
User asks "what did we talk about last time?"continued_from parents — getAncestors walks the chain
Stuck-session debugging forkbranched_from from a known-good checkpoint — getDescendants lists the experiments
Subagent transparencydelegated_from — show which parent spawned this session
Plan → execution provenanceplanned_by — every session created to fulfill a plan step
Compliance audit "where did this card come from"findByArtifact("canvas_card", cardId) — list every session that consumed it

Where to go next

On this page