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.userscarryingSCOPE_*_OPERATIONS; there is no separate operator identity layer. The session var that the Ops Agent’s RLS leans on isSESSION_VAR_USER_ID = "app.user_id", reusable across any per-user-keyed RLS use case — notSESSION_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 getsinvalid_at = now(), new fact inserted withvalid_at = now(); both retained. - Procedural: UPDATE with supersession — schema columns
superseded_at+superseded_by; insert new row active, mark priorsuperseded_at = now(), superseded_by = <new_id>; active set queryWHERE 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:
- 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
RetentionPolicydirectly.” - 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.
- 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_IDlands 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 commit3ef3b36; 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.mdxMemory section needs rewrite at close-pass: tier-name standardization, schema-location refinement, retention reframe, workshop framing. worlds.rules.body_textGIN/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-065 —
spectral.coreadmission discipline (the newSESSION_VAR_USER_IDships 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.py—SESSION_VAR_USER_IDcontract (commit3ef3b36)tests/core/test_contract_db.py— pinning test (commit3ef3b36)- 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)