TranscriptProjector converts a persisted Pi JSONL
session tree into the versioned
TranscriptPage/NormalizedTranscriptRecord
contract used by Pi Daemon Dash. Projection is deliberately independent
of runtime hydration: it does not construct SessionManager,
AgentSession, or AgentSessionRuntime, does not
load provider auth, models, tools, resources, or extensions, and has no
prompt-capable dependency.
import { TranscriptProjector } from "@harryaskham/pi-daemon/transcript-projector";
const projector = new TranscriptProjector({ stateDir });
const page = await projector.project({
inventoryId,
path: sessionJsonl,
expectedFingerprint,
query: { limit: 200 },
});Every returned page has hydration: "not-requested".
Displaying it must never be interpreted as opening or taking ownership
of the session.
The source must be a current-user-owned regular non-symlink file that is not group/world writable. The projector opens one file descriptor, validates its identity and metadata, streams bounded chunks, and compares size, mtime, device, and inode before/after the read. A concurrent source change returns a retryable typed error rather than projecting mixed generations.
Effective defaults come from DASH_DEFAULT_LIMITS:
UTF-8 is decoded strictly and every nonblank line must be a typed
JSON object. Malformed JSON, duplicate IDs, invalid parents, unsupported
versions, insecure paths, and bound violations return content-free
TranscriptProjectionError codes. Prompts, tool output,
paths, and image bytes are never logged.
Pi v2/v3 IDs and parent links are retained exactly. Legacy v1 linear
entries are assigned deterministic content/index-derived IDs for
preview, rather than Pi's random migration IDs, so cache rebuilds and
browser keys remain stable. Legacy hookMessage roles
normalize to custom.
The final appended entry is the current leaf. Projection walks its parent chain to the root and renders only that active branch; sibling branches are never flattened into a false conversation. An orphaned active path is rendered from the reachable portion and marks the page truncated. Parent cycles and duplicate IDs fail safely.
The semantic output preserves:
toolCallId;Raw base64 images are not copied into transcript pages or projection
caches. They become deterministic authorized dash-blob:
references and bounded metadata for the later blob-serving slice.
Inventory and projector use
formatSessionSourceFingerprint() from the small
source-fingerprint module to encode an exact digest of the
raw streamed file bytes:
sha256:<base64url digest>
The expected inventory fingerprint is an optimistic precondition. A
mismatch means inventory is stale and projection fails retryably. Cache
hits additionally require unchanged source size and mtime. Owner-private
cache files live under STATE_DIR/web/projections, are
atomically published, strictly revalidated on read, and are rebuilt
after corruption.
Pages remain chronological. Without a cursor the newest useful
viewport is returned first. olderCursor and
newerCursor are opaque, versioned, and tied to inventory ID
plus fingerprint; callers must not parse them. A cursor from a different
source or generation fails as stale.
The implementation test projects and caches a 10,000-entry active branch under the contract's local p95 budgets: cold useful viewport below 500 ms and cached viewport below 150 ms. These are acceptance ceilings, not permission for unbounded synchronous work.