Config manifest
The content-addressable snapshot of the runtime substrate — which plugins, prompts, nodes, channels, and filters were active — written once per session and referenced by every event-log row.
The config manifest records which substrate ran. A
fingerprint binds a turn's inputs to a hash;
the manifest binds the substrate that processed them — the plugin
set, the system prompts, the graph node bodies, the channel
definitions, the post-stage filters. One row per distinct
substrate, content-addressable, written to harness_config_manifest.
Every event-log row carries a manifest_hash
foreign reference to that row. So a full replay of any historical
session needs exactly two things: the event stream and the
manifest hash that was active when each event fired. That tuple is
self-contained — no live runtime, no ambient git HEAD, no
deploy-record lookup.
Rolling out additively
The harness_config_manifest table and the
harness_event_log.manifest_hash column land as additive schema
files — manifest_hash is nullable during rollout so rows written
before the manifest substrate stay valid. Existing sessions keep
working; new sessions stamp the reference. The cutover to a
required column is a follow-up migration after every write path
stamps the hash. See Schema for the bundle file.
Why it exists
Determinism promises a turn replays
byte-identical against the same package version and the same input.
"Same package version" is the load-bearing phrase — but a
deployment's effective substrate is more than the npm version. It
includes the plugins you registered, the prompt templates you
shipped, the graph nodes your build composed, the channels and
filters in play. Two deployments on the same @pleach/core
version can run different substrates.
The fingerprint doesn't capture that. It binds the turn input. The manifest captures the rest, so the replay contract becomes precise: same manifest, same input, same output.
The five surfaces
The manifest is a snapshot of five substrate axes. Each is content-hashed independently, then rolled up into one top-level hash.
| Surface | Column | What it captures |
|---|---|---|
| Plugins | plugin_manifest | The registered plugin set and the hooks each contributes |
| Prompts | prompt_manifest | The system-prompt facets and templates in effect |
| Nodes | node_manifest | The graph node bodies (by content hash) and their stage assignment |
| Channels | channel_manifest | The channel (state) definitions and their reducers |
| Filters | filter_manifest | The post-stage filter declarations |
Each surface gets its own child hash (plugin_hash,
prompt_hash, …). Per-surface hashes are what make the manifest
dedup well: in a multi-tenant deployment most tenants load the
same plugin set, so the plugin_manifest blob is shared across
their manifest rows even when their prompts differ.
The table
CREATE TABLE harness_config_manifest (
manifest_hash TEXT PRIMARY KEY, -- top-level Merkle roll-up
plugin_hash TEXT NOT NULL, -- per-surface child hash
prompt_hash TEXT NOT NULL,
node_hash TEXT NOT NULL,
channel_hash TEXT NOT NULL,
filter_hash TEXT NOT NULL,
plugin_manifest JSONB NOT NULL, -- full snapshot, not a delta
prompt_manifest JSONB NOT NULL,
node_manifest JSONB NOT NULL,
channel_manifest JSONB NOT NULL,
filter_manifest JSONB NOT NULL,
reference_count INTEGER NOT NULL DEFAULT 1, -- live event references
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
tenant_id TEXT NOT NULL DEFAULT 'default'
);The primary key is the content hash, so two sessions that ran the
same substrate write the same row — the insert is
ON CONFLICT DO NOTHING. Each per-surface hash gets its own index
for the cross-tenant query shapes below, plus a tenant_id index
for per-tenant scans.
Row-level security ships the same two-policy template as the rest
of the schema bundle: a service-role
bypass for server-side adapters, and a tenant_id = current_tenant()
isolation policy for scoped clients.
Foreign reference on the event log
ALTER TABLE harness_event_log
ADD COLUMN manifest_hash TEXT -- nullable during rollout
REFERENCES harness_config_manifest (manifest_hash)
ON DELETE SET NULL
DEFERRABLE INITIALLY DEFERRED;
CREATE INDEX harness_event_log_manifest_hash_idx
ON harness_event_log (manifest_hash);The event log carries only the top-level manifest_hash; the
per-surface child hashes live on the manifest row, reached by
joining. The column is a real foreign key, but two clauses keep it
cheap and rollout-safe:
DEFERRABLE INITIALLY DEFERREDis load-bearing. A session writes its manifest fire-and-forget and writes its first event in the same transaction; deferring the constraint to commit time lets those two writes race without a transient FK violation.ON DELETE SET NULLmeans a retention GC that drops a manifest nulls the reference rather than cascading a delete into the audit trail — the event survives, it just loses its substrate pointer.
The constraint is nullable during rollout so rows written before the
manifest substrate stay valid. Two gates back the reference:
audit:config-manifest-fk-constraint-applied asserts the migration
carries every canonical clause, and
audit:config-manifest-referential-integrity asserts zero orphans
online (and the SessionRuntime wiring offline).
The hash
Each surface is serialized to canonical JSON (RFC 8785 — sorted keys, no insignificant whitespace) and hashed with SHA-256. The top-level hash is a Merkle roll-up over the five child hashes, ordered, with a versioned prefix:
manifest_hash = sha256_hex(
"pleach.manifest.v1" ␟
"plugin:" ␟ plugin_hash ␟
"prompt:" ␟ prompt_hash ␟
"node:" ␟ node_hash ␟
"channel:" ␟ channel_hash ␟
"filter:" ␟ filter_hash
)␟ is the ASCII unit separator (0x1F). This mirrors the
hash chain's pleach.c9.v1 canonicalization —
same separator, same versioned-prefix discipline, same hex output
shape. The version prefix lets a future schema add a sixth surface
without colliding with v1 manifests.
The manifest builder is a pure function of its inputs — no
Date.now(), no environment reads — so the same substrate always
produces the same hash. The audit:plugin-content-hash-stability
gate runs two consecutive builds and diffs each of the five child
hashes independently; a Date.now() leak in the prompt builder
fails prompt_hash and points at the prompt surface specifically,
not at "something in the manifest moved."
Querying
The manifest turns three audit questions into index-backed queries.
Replay by config — every event under the substrate active in a session:
SELECT events.*
FROM harness_event_log events
WHERE events.manifest_hash = $1
ORDER BY events.sequence_number;Cross-tenant audit by plugin set — every event across all sessions that ran with a given plugin set, regardless of prompt, node, channel, or filter drift:
SELECT events.*
FROM harness_event_log events
JOIN harness_config_manifest manifest
ON events.manifest_hash = manifest.manifest_hash
WHERE manifest.plugin_hash = $1;Substrate reconstruction — the full substrate behind a single event, all five surfaces returned as JSONB:
SELECT manifest.*
FROM harness_config_manifest manifest
JOIN harness_event_log events
ON events.manifest_hash = manifest.manifest_hash
WHERE events.id = $1
LIMIT 1;A manifest lookup is a primary-key hit, so JOIN cost is dominated by the event-log scan, not the manifest cardinality. A typical incident window of a thousand events references only a handful of distinct manifests.
Full snapshots, not deltas
Each row stores the complete substrate, not a diff against a prior manifest. The storage cost is small — content-addressing dedups identical substrates, and per-surface hashes dedup the shared parts — and the payoff is that replay reads one row. A delta encoding would force the replay tool to walk a parent chain, which breaks the moment retention drops a parent, and it would turn the round-trip audit into a multi-step materialization. One row per manifest keeps replay offline-able.
Retention
Manifests are reference-counted against the event log: a manifest survives as long as any event references it. The default floor is forever — the compliance posture is "keep the audit trail," not "expire it." Cost-sensitive tenants can opt into a per-tenant retention floor (planned) that drops manifests older than N days and referenced by no surviving event.
This couples manifest retention to whatever event-log retention you run, automatically:
| Event-log retention | Manifest retention |
|---|---|
| Forever | Forever |
| N-day TTL | Manifest drops within a GC pass of the last referencing event |
| Per-tenant policy | Manifest follows per tenant |
The audit:config-manifest-retention-completeness gate runs after
a GC pass and fails if any event references a missing manifest —
the guard against a GC racing a concurrent event write.
One snapshot per session
The manifest is captured once, at session construction — never mid-stream. The content-addressable contract requires a stable snapshot, so a config change mid-session is out of scope: the session represents the substrate it started with. Mutating manifest-relevant state mid-session is the kind of thing the replayability contract is designed to forbid, not silently absorb.
Where it sits in the replay story
The fingerprint binds the inputs; the manifest binds the
substrate; the event-log row carries both. Replay reads the
stream, dereferences each manifest_hash, reconstructs the
substrate, and re-runs the events against it. Same manifest, same
input, same output.
Where to go next
Schema
The schema bundle this table lands in, and the RLS template it shares.
Event log
The stream whose rows carry the `manifest_hash` reference.
Determinism
The replay contract the manifest completes — same manifest, same input, same output.
Fingerprint
The input-binding half; the manifest is the substrate-binding half.
Event-log projections
GraphProjection<T> — folding harness_event_log rows into typed runtime state. The contract is stable; the shipped projections are in soak.
Stream events
Every event type executeMessage yields — message deltas, tool lifecycle, jobs, interrupts, checkpoints, sync, errors. The full catalog with payload shapes.