Interaction Trace — Parse-First Session Records with Structural-Only Validation and Optional Witness Binding
Accepted
Context
An agent or human session produces a stream of reasoning, decisions, file touches, and outcomes. Today that stream survives only as prose: the transcript, or at best a `.coder/` markdown entry whose substance lives in a free-text `content` field (coder.ncl::Record). Extracting "what decisions did we make across the last ten sessions", "which sessions touched the validator layer", or "which decisions are ADR candidates" means mining prose by hand — exactly the cost the rest of the protocol was built to eliminate. Every other project-defining artefact in ontoref is queryable NCL; the record of what an agent actually DID in a session is not.
The opening framing of this decision was stronger than what was adopted: make the agent emit its interaction in a *typed and validated* language — a contract over the reasoning itself. That framing was consciously narrowed. The goal is not to validate reasoning (undecidable, and an invitation to schema theatre where agents fill fields to satisfy a contract rather than to record signal); the goal is for the output to be *parseable and filterable* so a review need not escarbar through prose. The distinction is load-bearing and drives every constraint below.
Three adjacent decisions touch this surface but none owns it:
- ADR-024 (operations layer — agent action boundary) governs the *act*: the operation an agent invokes and the witness it emits. It says nothing about the agent's narration of the act. - ADR-026 (validation architecture — three planes) defines structural vs contextual validators. It is the plane this decision lives in, but it does not define an artefact for agent interaction. - ADR-031 (on+re constitutive duality) establishes the witness as the seam where reflection-axis acts bind to ontology-axis substance. This decision asks whether the agent's *interaction record* sits on that seam — and the answer is: optionally.
The user's reframe added a second property: the record need not relate to the witness at all. Absent a witness it still serves a review without deep digging. That makes the witness binding optional, which in turn makes the layer usable at every tier with different uses, rather than gated behind operations (tier-2).
Decision
Introduce the InteractionRecord — a parse-first trace of an agent/human session segment, stored as JSONL (one record per line), shipped as the reflection schema `reflection/schemas/interaction.ncl` (migration 0028).
DESIGN PROPERTIES (each is a constraint below):
1. Parse-first, not prose-analytic. Every dimension a review filters on is an enum or a typed array (events[], decisions[], actions[], insights[], files_touched[], tensions_engaged[], outcome). Free prose is confined to three fields — `summary`, `Decision.rationale`, `Event.note` — and those fields are NEVER a filtering axis. `jq` and `nu where` answer review questions without reading the prose.
2. Structural-only validation. The validator (ADR-026 structural plane) checks shape: required fields present, enum members in their allowed set, arrays shaped correctly. It MUST NOT inspect the truthfulness of the free prose. Asking a validator to certify that a rationale is honest or a summary is faithful is a Hard biconditional on a question core.ncl names as a Spiral tension — an ondaod forbidden pattern. Validation of prose content is therefore explicitly out of scope, permanently.
3. Optional witness binding — the layer is tier-orthogonal. The `witness` field (op_id, state_root, signature) is optional. Absent → tier-0 review aid (no substrate required). Present → tier-1/2, where op_ref on events and actions links the record to witnessed substrate acts; the record becomes the reflection-side companion of those acts. This is the ADR-031 seam, made optional so the layer is not gated behind operations adoption.
4. Vocabulary reuse, not forking. The schema reuses coder.ncl's canonical enums (ActorKind, Kind, Domain, optional Category) via `import "coder.ncl"` rather than redeclaring parallel vocabularies that would drift. The runtime validator parses enum members FROM the schema files at runtime, so the allowlist can never diverge from the contract.
5. Two emission paths, both supported. `ontoref interaction record` validates a record structurally before appending (the forced path; malformed records are rejected). Direct JSONL append by the agent is also permitted (the free path), audited post-hoc by `ontoref interaction validate` (pre-commit / CI gate). The `--no-validate` flag bridges the two on the same command.
6. Decisions materialise the adr? criteria as data. `Decision` carries `alternatives_rejected`, `reversible`, `adr_candidate`, and `tensions_engaged` (core.ncl node ids). `ontoref interaction decisions --adr-candidates` surfaces ADR candidates across sessions in one query, feeding the ADR lifecycle from observed decisions rather than recollection.
The layer is opt-in per ADR-029: a project that never emits interaction JSONL remains valid and pays zero adoption cost.
Constraints
- Soft Validation of InteractionRecord MUST remain structural (shape, enum membership, array shape) and MUST NOT inspect the truthfulness or quality of the free-prose fields (`summary`, `Decision.rationale`, `Event.note`). Adding a Hard check that gates on prose content is forbidden — it is a biconditional on a Spiral tension (reality-vs-intent) per ondaod.
- Hard The `witness` field on InteractionRecord MUST remain optional. Making it required gates the layer behind tier-2 (operations) adoption and discards the tier-0 review-aid use. The optional binding is what makes the layer tier-orthogonal per ADR-029.
- Hard The InteractionRecord schema MUST reuse coder.ncl's canonical vocabularies (ActorKind, Kind, Domain, Category) via `import "coder.ncl"` rather than redeclaring parallel enums. The runtime validator MUST derive enum membership from the schema files, not hardcode it.
- Hard The interaction schema (reflection/schemas/interaction.ncl) MUST be shipped to consumer data dirs as part of the reflection/ tree by install.nu, so adopting projects can resolve it. Migration 0028 is its propagation mechanism.
- Soft Free prose in InteractionRecord MUST stay confined to `summary`, `Decision.rationale`, and `Event.note`. New filterable facets MUST be added as enums or typed arrays, never as free-text fields that a review would have to parse. The schema marks `summary` as the only free-prose top-level field.
Alternatives considered
- Typed AND validated contract over the agent's reasoning (the opening framing) — rejected: Validating reasoning is undecidable and invites schema theatre: agents fill fields to satisfy the contract, not to record signal — the exact H7 dysfunction ADR-024 names. A Hard contract over prose truthfulness is also a biconditional on a Spiral question (reality-vs-intent), an ondaod forbidden pattern. The adopted design keeps the typing for parseability and drops the validation of content.
- Extend coder.ncl::Record instead of a new schema — rejected: Record's substance is a free-text `content` field — it is the prose artefact this layer is meant to complement, not the parse-first one. Bolting events/decisions/actions arrays onto Record would overload one type with two opposed purposes (prose memory vs structured trace) and muddy `coder triage`/`coder record` semantics. A sibling schema that REUSES Record's enums keeps both coherent and distinct.
- Require the witness — bind every interaction record to the substrate — rejected: Gates the layer behind operations adoption (tier-2) and discards the user's explicit requirement that the record serve a review even with no witness present. Optional binding delivers the tier-0 review-aid use AND the tier-2 witnessed-companion use from one schema; mandatory binding delivers only the latter.
- Store instances as per-record NCL files validated by nickel-export — rejected: NCL enums are tags ('AgentClaude), JSONL stores them as strings — applying the NCL contract to imported JSON fails on every enum field, and per-file NCL is the wrong shape for streaming filters at scale. JSONL + a structural Nushell validator (that reads enum members from the NCL schema) is the correct split: NCL is the contract, JSONL is the queryable instance store.
- Validate the free-prose fields with heuristics (length, keyword presence, sentiment) — rejected: Any check that gates on prose content is a proxy for truthfulness that the layer explicitly disclaims. Heuristic prose checks produce false confidence (a record passes because it is verbose, not because it is honest) and re-open the Spiral collapse this ADR closes. Prose quality stays a human review concern.
Anti-patterns
- Validating the Content of Free Reflection — A contributor adds a check that gates on the content of `summary`, `Decision.rationale`, or `Event.note` — length thresholds, required keywords, sentiment, or a claim that the rationale 'must be honest'. This is a biconditional on the reality-vs-intent Spiral and produces false confidence: a record passes because it is verbose, not because it is truthful.
- New Filterable Facet Added as Free Text — A new dimension a review wants to filter on (e.g. 'severity', 'subsystem') is added as a free-text field instead of an enum or typed array. Reviews must then string-match prose, re-introducing the prose-mining cost the layer exists to eliminate.
- Parallel Enum Vocabulary for Sessions — InteractionRecord redeclares its own ActorKind/Kind/Domain enums instead of importing coder.ncl, or the runtime validator hardcodes the allowlist. The two vocabularies drift the first time one is extended, and the structured trace silently disagrees with the prose Record about what an actor or kind is.