pleach
Architecture

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.

substrate contract locked · rolling out
Subpath@pleach/core/schemaSourcesrc/schema/postgres/Sourcesrc/eventLog/hashChain.ts

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.

SurfaceColumnWhat it captures
Pluginsplugin_manifestThe registered plugin set and the hooks each contributes
Promptsprompt_manifestThe system-prompt facets and templates in effect
Nodesnode_manifestThe graph node bodies (by content hash) and their stage assignment
Channelschannel_manifestThe channel (state) definitions and their reducers
Filtersfilter_manifestThe 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 DEFERRED is 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 NULL means 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 retentionManifest retention
ForeverForever
N-day TTLManifest drops within a GC pass of the last referencing event
Per-tenant policyManifest 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

On this page