Skip to content
GitHub
Decisions

ADR-059: Operations Agent memory — workshop framing, scope-inheritance retention, identity-not-capability session vars

Status: Accepted (2026-04-25)

Context

The Operations Agent is the task-oriented collaborator the Spectral operations team uses inside the Operations app — drafting rule candidates, kicking off distillation, reviewing promotion queues, drafting release notes and World Model Cards. It is internal-only, never customer-facing. SPEC-268 S11 had established the logical model (operator-keyed three-tier; no cross-operator sharing; non-mirror discipline). TA-12 (ADR-058) had landed the cross-agent pattern in agent-memory-primitives.mdx. What was missing was the per-agent schema parameterization plus a few behavioral defaults that did not generalize.

Two reframings surfaced during disposition that changed how the primitives are documented for all agents (not just Ops):

  • Memory is a workshop, not a canonical-content cache. Semantic memory holds workflow meta-knowledge (“operator was working on conformity-gate coverage for World X”), not rule content (“the conformity gate says Y”). Rule interpretation lives with the rule corpus and its tools. The trigram trigger that TA-12 had positioned as primary defense against rule shadowing is, for Ops Agent, a defense-in-depth backstop — agent-design discipline is the primary defense, calibrated to memory consumption pattern.
  • Persistent vs transient is the durability axis, not “ephemeral by construction.” Transient tiers (interaction, session) inherit lifecycle from their enclosing scope (chat thread; operator session) rather than registering a literal TTL with the retention framework. Audit posture is action-linkage-based: any-tier memory load-bearing for audit is retained; transient-tier memory without action-linkage can be removed when its scope ends.

A third reframing constrained the identity contract between contexts:

  • Session vars name identity, not capability. Operators are entries in core.users carrying SCOPE_*_OPERATIONS; there is no separate operator identity layer. The session var that the Ops Agent’s RLS leans on is SESSION_VAR_USER_ID = "app.user_id", reusable across any per-user-keyed RLS use case — not SESSION_VAR_OPERATOR_ID, which would burn the var on a single use case.

This ADR captures the parameterization plus the three reframings as decisions on the Ops Agent and as primitives-doc edits referenced by ADR-058.

Decision

D1 — Schema location: platform.operations_agent_memory

Refines SPEC-268 S11, which placed the schema in apps/operations’s own data domain. Contexts are code boundaries, not schema boundaries (per ADR-031 + the three-schema model in TA-1). All Operations Agent code lives under src/spectral/platform/...; memory lands in the platform schema. Mirrors worlds.world_agent_memory (TA-12) and platform.spectral_agent_memory (SPEC-242 bookkeeping).

D2 — Anchor entities

  • Persistent: operator_id (FK → core.users(id) per TA-18 D4b)
  • Session: agent_session_id (UUID; new session allocated on first activity after 30-day idle per D9)
  • Interaction: agent_interaction_id (per chat thread; NULL at session/persistent tiers)

D3 — Per-tier typology distribution

  • Interaction: episodic
  • Session: episodic-dominant + procedural permitted (in-flight task state, draft drafts)
  • Persistent: procedural-dominant + semantic permitted (signal digests asked about, durable inferences)
  • No persistent-episodic produced (matches TA-12 doctrine; schema retains episodic in the typology enum so the cell exists; doctrine + repository wrapper, no CHECK)

D4 — Trigram trigger as defense-in-depth backstop

Trigger fires on typology = semantic at any tier. Reuses the TA-12 D8 contract: BEFORE INSERT OR UPDATE OF body_text on platform.operations_agent_memory; skip if length(body_text) < 100; pg_trgm similarity check against worlds.rules.body_text scoped by world_id_context; reject if max similarity > 0.85 (errcode 23514). Threshold and floor are contracts, not config.

For the World Agent the trigger is the primary defense (the agent reasons OVER rules to produce semantic claims); for the Ops Agent it is a backstop (the agent reasons WITH rules to drive workflow). The primary defense is agent-design discipline — semantic memory holds workflow meta-knowledge, not rule content. The trigger catches doctrine drift; it is not load-bearing.

world_id_context column on memory rows: required-when-typology-is-semantic; nullable for procedural and episodic (which do not require world-scoping). The trigger uses the column to scope the similarity check.

Procedural and episodic memory skip the trigger entirely — language shape (operator-pattern, conversation transcript) does not shadow rule body.

D5 — No inter-context role grants

platform.operations_agent_memory is internal to platform-side application code; operator access happens through the operations-app surface, not via reads from another context. No operations_agent_memory_reader role. ADR-063 (TA-27) is not load-bearing for this table — there is no access from another context in either direction.

D6 — Operator RLS via session-var; SESSION_VAR_USER_ID = "app.user_id" added to Core

Session vars name identity, not capability. account_id and workspace_id name customer-tenancy scopes; the principal concept is the authenticated user. Operators are entries in core.users carrying SCOPE_*_OPERATIONS (TA-18 D11 + TA-21 D9 Pattern A JWKS-local); there is no separate operator identity layer, so the session var must not name one.

The memory-table column stays semantically operator_id (column-naming choice; FK → core.users(id)). The RLS predicate is operator_id = current_setting('app.user_id')::uuid. Operations-app middleware sets SET LOCAL app.user_id = <jwt_subject> in operator-context transactions per TA-3.

App-layer enforcement primary per TA-1 D5; RLS backstop per TA-1 D6 — applied consistently to internal-only data, not skipped on the basis of internal-only-ness.

A contract-requirement test pinned the string under the discipline in force when this ADR landed (now superseded by ADR-065’s admission discipline). The inter-context requirement statement at PR time named platform.operations_agent_memory RLS as the immediate consumer plus general-user-keyed-RLS as the open-ended consumer pattern (Spectral Agent’s platform.spectral_agent_memory and any future per-user-owned RLS use case inherit the same constant).

D7 — Promotion semantics

  • Procedural: no tier-promotion. Procedural memory is the durable form per the primitives default; direct write at high-confidence threshold or operator-stated preference.
  • Episodic at session-close: compounding pass extracts procedural patterns to persistent (typology promotion), archives source episodes per the universal audit posture.
  • Semantic: corroboration-gated promotion per the primitives default.

D8 — Conflict resolution per typology; no first-class contradictions table

  • Episodic: NOOP — episodes accumulate.
  • Semantic: UPDATE with temporal validity — schema columns valid_at + invalid_at; old fact gets invalid_at = now(), new fact inserted with valid_at = now(); both retained.
  • Procedural: UPDATE with supersession — schema columns superseded_at + superseded_by; insert new row active, mark prior superseded_at = now(), superseded_by = <new_id>; active set query WHERE superseded_at IS NULL.

Both column families live on the same table; the typology discriminator routes which is in play. The repository wrapper enforces typology-appropriate use (no CHECK; doctrine pattern per TA-12).

No contradictions table (unlike the World Agent’s). World Agent contradictions are first-class because they connect to the TA-9 failure-cluster pipeline → operator triage → potentially rule revision; the Ops Agent has no analogous downstream consumer. Preference contradictions just supersede; semantic contradictions resolve via temporal-validity. If a future UX feature surfaces preference conflicts to the operator persistently, add the table then; do not pre-build.

D9 — Session retention via scope-inheritance, not direct TA-4 registration

SPEC-268 S11’s “T2 — 30 days rolling from last activity” is the operator session’s lifecycle property; session-tier memory inherits it.

The persistent-vs-transient axis is about durability of the memory itself, not literal storage intent. Transient tiers (interaction, session) are not “ephemeral by construction”; they inherit lifecycle from their enclosing scope (interaction → chat thread; session → operator session). At scope-close, the compounding pass evaluates promotion. Promoted rows move to persistent and adopt the persistent-tier RetentionPolicy. Non-promoted rows can be removed unless action-linked (rule promotion, change-set acceptance, published decision) — action-linkage holds rows for audit per the universal audit posture.

The universal audit posture’s “no hard DELETE” applies to persistent-tier memory inherently AND to any-tier memory load-bearing for audit. Transient-tier memory without action-linkage can be removed when its scope ends.

Implementation: session-close detection is application-side (lazy on next-activity check MAX(activity_at) < now - 30d, or nightly background job). Compounding pass at session-close. Persistent tier registers a TA-4 RetentionPolicy with typology-driven defaults (procedural = decay-exempt; semantic = validity-window-decay).

D10 — Reference-only invariant realized; no corroboration junction

Non-mirror list per the primitives Ops Agent row: world-model rule content (D4 trigger backstops; primary defense is workshop discipline); scan trace data (sanitization at write; doctrine + repository wrapper); customer PII (sanitization at write).

No corroboration junction (unlike TA-12 for World Agent). Ops Agent memory does not accumulate cross-source corroboration the way World Agent semantic memory does — a workflow preference is not multi-sourced; a signal digest is a snapshot, not a corroborated claim.

D11 — Schema implementation deferred to consumer epic

Per the precedent set by ADR-043 (TA-14) and ADR-058 (TA-12): application schemas land in the implementation epic that consumes them, not in the disposition ADR. This ADR’s contract is the doctrine (D1–D10, D12) plus the SESSION_VAR_USER_ID Core addition plus close-pass Codex rewrites. The implementation epic lands the migration (platform.operations_agent_memory + platform.operations_agent_memory_embeddings retrievable), the trigram trigger reusing the GIN/GIST trigram index on worlds.rules.body_text (provisioned by ADR-058’s implementation epic), the repository gateway, retention registration entries, and per-(tier, typology) partial HNSW indexes.

D12 — Primitives-doc edits queued

Three edits to apps/docs-codex/src/content/docs/system-design/agents/agent-memory-primitives.mdx apply to the cross-agent primitives, motivated here:

  1. Primitive 6 (Lifecycle / retention) — rephrase from “transient tiers don’t register — ephemeral by construction” to “transient tiers inherit lifecycle from their enclosing scope (interaction → chat thread; session → operator session); persistent tier registers a RetentionPolicy directly.”
  2. Audit and immutability section — clarify that “no hard DELETE” applies to persistent-tier memory inherently and to any-tier memory load-bearing for audit (action-linkage); transient-tier memory without action-linkage can be removed when its scope ends.
  3. New sub-section on per-agent leak-defense calibration: enforcement is calibrated to memory consumption pattern; the World Agent’s trigram trigger is primary defense; the Ops Agent’s same-shape trigger is defense-in-depth backstop with workshop discipline as primary.

Alternatives considered

TA-4 RetentionPolicy on session tier with literal 30-day TTL (the literal SPEC-268 S11 reading). Rejected per the durability-vs-scope-inheritance framing. Session is transient; lifecycle inheritance from the session itself is the right model and avoids reproducing the operator session’s lifecycle in two places.

SESSION_VAR_OPERATOR_ID = "app.operator_id". Rejected. Names a capability class, not an identity layer; burns the var on a single use case. Operators are users with operator scopes; there is no operator identity layer.

Reuse SESSION_VAR_ACCOUNT_ID for operator identity. Rejected. account_id is contractually customer-tenancy scope per TA-18 D13; conflating with operator identity creates a dual-meaning that breaks the contract.

Skip RLS on Ops Agent memory; rely on app-layer alone. Rejected. TA-1 D6 backstop pattern applies consistently; internal-only does not justify skipping the safety net.

Trigger fires on all typologies. Rejected. Procedural and episodic do not shadow rule content (language shape is operator-pattern / conversation transcript). False-positive cost without risk-coverage gain.

Trigger fires only at persistent-tier semantic, not session-tier semantic. Rejected. Session-tier digests can promote to persistent via compounding without re-triggering; cheaper to check at every semantic write. Per-write compute on Ops Agent semantic writes is low (procedural-dominant means semantic is a minority).

First-class contradictions table for Ops Agent. Rejected. No downstream consumer; supersession + temporal-validity handle conflict resolution. Reversibility is high if a UX consumer emerges.

Corroboration junction for Ops Agent. Rejected. Workflow state and preferences are not cross-source claims.

Consequences

  • Cross-agent pattern inherited cleanly. SPEC-268 S11 is ratified with two refinements made explicit (schema location + retention framing).
  • Workshop-not-canonical-cache framing crystallized as a project memory and a primitives-doc edit. Affects how future agent memory designs justify any rule-shadow defense.
  • Durability-vs-scope-inheritance framing crystallized as a project memory and a primitives-doc edit. Affects how transient tiers are documented and how the audit posture is scoped.
  • SESSION_VAR_USER_ID lands as a reusable identity contract. Future user-keyed RLS use cases (Spectral Agent memory ownership; audit columns; any per-user owned data pattern) inherit the same constant. Captured in commit 3ef3b36; test count moved 113 → 114.
  • Per-agent leak-defense calibration principle named. The primitives doc gains a sub-section so future agent design chooses enforcement level explicitly rather than copying TA-12 verbatim.
  • Application-side session-close detection is net-new code (modest — a query and a job). It does not reuse the TA-4 state-machine machinery for session lifecycle. Justified by the primitives’ transient-tier inheritance framing.
  • Primitives-doc edits at close-pass. Bounded scope; primitives is the source-of-truth for the cross-agent refinements.
  • Codex operations-agent.mdx Memory section needs rewrite at close-pass: tier-name standardization, schema-location refinement, retention reframe, workshop framing.
  • worlds.rules.body_text GIN/GIST trigram index (provisioned by SPEC-238 per TA-12) must land before the Ops Agent trigger does — SPEC-253 orders after SPEC-238’s index lands.
  • Operator offboarding semantics (operator leaves org → persistent-tier rows for that operator_id) are flagged as a forward-trigger and live outside this ADR (identity-management territory).

References

  • ADR-018 — three-tier memory framing (refined to universal interaction/session/persistent by TA-12)
  • ADR-065spectral.core admission discipline (the new SESSION_VAR_USER_ID ships under core)
  • ADR-031 — single-library + context-as-code-boundary
  • ADR-058 — TA-12 World Agent memory + cross-agent primitives
  • ADR-060 — TA-15 (workshop discipline at tool→memory boundary; D9)
  • ADR-063 — inter-context access pattern (not load-bearing here; noted for completeness)
  • TA-13 disposition — SPEC-316 comment 8786a91b
  • TA-13 verification — SPEC-316 comment 6b61feb0
  • src/spectral/core/db/session_vars.pySESSION_VAR_USER_ID contract (commit 3ef3b36)
  • tests/core/test_contract_db.py — pinning test (commit 3ef3b36)
  • Codex system-design/agents/agent-memory-primitives.mdx — primitives source-of-truth (D12 edits land at close-pass)
  • Codex system-design/operations/operations-agent.mdx — Memory section rewrite at close-pass
  • SPEC-253 — Operations Agent epic (schema + trigger + gateway + repository + retention + middleware)