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";@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.
| Relation | Recorded by | Trigger | Consumer use |
|---|---|---|---|
continued_from | host | User explicitly continues prior work | "Show me the conversation history including the previous chat" |
branched_from | runtime | Fork / time-travel branch materializes a new session | "What experiments forked off the same starting point" |
used_result_of | host | Session B consumed a canvas card or job result from session A | Job → chat provenance |
delegated_from | host | Subagent spawned across a session boundary | Subagent provenance UI |
planned_by | host | Session was created to fulfill a plan step | Plan → 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
| Scenario | What lineage gives you |
|---|---|
| User asks "what did we talk about last time?" | continued_from parents — getAncestors walks the chain |
| Stuck-session debugging fork | branched_from from a known-good checkpoint — getDescendants lists the experiments |
| Subagent transparency | delegated_from — show which parent spawned this session |
| Plan → execution provenance | planned_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
Observability
OTel, Datadog, Honeycomb, Prometheus — the wider read-side wiring this graph sits inside.
OTEL observability
Span types whose `pleach.session_id` joins to lineage nodes.
Checkpointing
`branched_from` edges fire when a checkpoint restore creates a branch.
Subagents
`delegated_from` edges record subagent → parent provenance.
Audit ledger
The write-side per-call row carries the `sessionId` that joins to lineage.
Playground & devtools
What ships today for inspecting sessions, walking checkpoints, and tailing the event log — plus the explicit trade we made on "hosted dashboard".
What Pleach writes to your database
Every table the runtime and its plugins write — what's on by default, what you opt into, and what each row carries. The destination is your database, not ours.