SessionInventory is the preview-only, owner-local index
behind the Dash sidebar and information view. It merges retained Pi
Daemon catalog records with approved Pi JSONL roots without hydrating an
SDK runtime or sending a model turn. Explicit writer transitions live in
the separate session ownership and
export service.
The implementation lives in src/session-inventory.ts and
is exported from the package root and ./session-inventory.
src/source-fingerprint.ts provides the small shared
formatSessionSourceFingerprint() helper used by inventory
and transcript projection.
resolveSessionInventoryConfig() maps
PiDaemonWebConfig.inventory.{roots,reconcileIntervalMs,maxSessions}
into inventory options. Raw YAML paths are resolved only through the
selected LoadedPiDaemonConfig.resolvePath() function. A
caller may add the selected Pi agent session root separately; roots are
deduplicated and bounded.
Operator config remains input policy. Browser-facing
DashboardLimits and SessionInventoryPage
remain negotiated transport resources.
The owner-private state directory contains:
STATE_DIR/web/
inventory-v1.head.snapshot # newest 101 rows, Node-major binary hot path
inventory-v1.head.json # portable hot-head fallback
inventory-v1.snapshot # full HMAC-authenticated Node-major snapshot
inventory-v1.json # canonical portable JSON recovery index
inventory-search-key-v1.json # private keyed-search/snapshot key
Startup validates and loads only the bounded binary hot head before
becoming available, falling back to the portable JSON hot head after an
absent, Node-major-incompatible, or quarantined snapshot. It then
hydrates the full snapshot on the next event-loop turn. While this is in
progress, pages truthfully report index.reconciling: true
and stale: true. The snapshot is an optimization: it is
authenticated with the private search key, bound to the Node major and
inventory format, byte bounded, and discarded on corruption or
incompatibility. Canonical JSON remains the portable fallback.
waitForFullIndex() is a service/test barrier; ordinary
bootstrap does not need to wait. After the full snapshot is installed,
request-path list/info calls use only immutable in-memory rows and
precomputed order positions.
reconcile() runs outside the request path and atomically
replaces the complete in-memory generation only after all validation and
persistence succeeds. start() performs one background
reconcile and repeats on the configured interval. Filesystem
notifications are not a correctness dependency; periodic reconcile
repairs missed changes.
Each pass:
O_NOFOLLOW, rechecks
owner/type/size on the open descriptor, and parses bounded UTF-8 JSONL
lines;If a configured root itself is missing, insecure, foreign-owned, writable by other users, or overlaps daemon state, reconciliation fails and keeps the prior index. A malformed individual session is skipped and counted by safe issue code without poisoning other sessions.
A file-backed inventoryId is a versioned SHA-256
identity over its canonical path, not a filesystem path. A retained
memory/managed-only row is derived from its canonical daemon session ID.
Duplicate Pi session UUIDs at different paths therefore remain
separately addressable.
Title precedence is:
session_info.name;Untitled session plus a short opaque ID.First-message title derivation refuses obvious credential-bearing
text. The public inventory row contains only bounded title, cwd
basename/project label, Pi/daemon identities, timestamps, counts,
activation eligibility, and orthogonal presence. modifiedAt
remains source/catalog modification truth; optional
activityAt is user-visible activation recency and defaults
to modifiedAt for older indexes. It never contains a
canonical path. The authenticated getInfo() resource may
include the full cwd, canonical source path, size/mtime/device/inode,
exact fingerprint, ownership, aliases, and safe diagnostics.
Inventory hashes every raw file byte while streaming and formats the digest as:
sha256:<base64url SHA-256 digest>
formatSessionSourceFingerprint(digest) validates the
32-byte digest and owns this exact encoding. The transcript projector
consumes the same helper, so expectedFingerprint compares
exact content rather than metadata or an implementation-specific
representation. Size, mtime, device, and inode remain additional
race/conflict guards.
The inventory never calls Pi's convenient
SessionManager.listAll() because that API materializes
allMessagesText. During background parsing it keeps only a
bounded ephemeral excerpt, transforms normalized trigrams and words into
a fixed-size keyed Bloom filter, and discards the excerpt before
persistence. The private key is generated once and never exposed to
browser resources.
Exact visible metadata (title, cwd basename, project label, Pi ID, daemon ID, name) is matched directly. Message search uses the Bloom filter, so it can admit a false-positive candidate but never a false negative within the bounded indexed excerpt. No full user/assistant message, tool output, system prompt, environment value, or provider credential is retained in the index.
Rows are preordered by descending activityAt (falling
back to modifiedAt), then source modification time and
inventoryId. Successful reuse/direct/fork activation
advances activity exactly once inside the durable activation work,
persists the reordered hot head/index/snapshot, and leaves the source
file mtime and fingerprint untouched. Duplicate reads of the same
succeeded activation ticket do not advance it again. Unfiltered first
pages and continuation use precomputed positions. Opaque cursors bind
the index revision, normalized filter digest, last activity time, and
last inventory ID. Reusing one with different filters or a new index
generation returns a typed stale-cursor error.
Search/filter scans stop after one extra result and yield to the event loop every 512 candidates. No request performs a filesystem scan or one synchronous O(total sessions) sort.
The opt-in manual performance acceptance builds and persists 10,000 managed records, launches clean Node processes for cold bootstrap samples, and asserts the normative contract budgets. Run it on an otherwise idle acceptance host:
npm run test:manual:inventory-performance
It is intentionally excluded from npm test, package
builds, Nix evaluation, and installation gates because host contention
changes wall-clock percentiles. Deterministic inventory behavior and
security invariants remain in the standard suite. A representative
manual development run measured:
| Measurement | p95 | Budget |
|---|---|---|
| persisted hot-head bootstrap + first 100 rows | 31.58 ms | < 50 ms |
| hot in-memory first 100 rows | 0.58 ms | < 150 ms |
| indexed search page | 23.65 ms | < 100 ms |
The manual acceptance also verifies the full index remains below 64 MiB. These timings are printed as diagnostics so an operator can compare isolated runs without making installation success depend on ambient machine load.