IBEX Harness
DocsBenchmarksBlogChangelogRoadmap
GitHub
IBEX Harness

Documentation

Architecture Decision RecordsADR-0002: Repository foundation bootstrapADR-0003: Branch protection and merge policyADR-0004: Protobuf and code generation policyADR-0005: Postgres migration strategyADR-0006: Auth protobuf contract (`ibex.auth.v1`)ADR-0007: Auth token validation implementationADR-0008: Security scanning and CI quality gatesADR-0009: Permission bitmap layoutADR-0010: Cryptography policyADR-0011: Proxy auth gRPC client and middlewareADR-0012: Proxy request normalization (OpenAI chat)ADR-0013: Proxy input validation and stable error envelopeADR-0014: Core domain migration sequencingADR-0015: Proxy rate limit skeleton (Phase 1)ADR-0016: Proxy agent identity verification (Phase 1)ADR-0017: Request ID and trace context strategy (Phase 1)ADR-0018: Graceful shutdown contract (Phase 1)ADR-0019: OpenTelemetry provider configuration (Phase 1)ADR-0020: Shared package boundaries — `packages/config` and `packages/apierror`ADR-0021: Prometheus Metric Catalog (Phase 1)ADR-0022: Health check contract (Phase 1)ADR-0023: Docs site architecture (Phase 1.5)ADR-0024: Benchmark data publishing modelADR-0025: LLM provider abstractionADR-0026: OpenAI client designADR-0027: Streaming dual-write strategyADR-0028: Auth cache designADR-0029: Token revocation propagation via Redis pub/subADR-0030: Directive versioning strategyADR-0031: System prompt injection strategyADR-0032: Session data model and retentionADR-0033: ClickHouse llm_traces schema and retentionADR-0034: Proxy overhead performance measurement methodologyADR-0035: Chat Idempotency-Key Redis dedupeADR-0038: Context assembly service design and gRPC contractADR-0039: Proxy Postgres ownership for session and directive storesADR-0040: Anthropic provider adapterADR-0041: Model capability registryADR-0042: Self-hosted OpenAI-compatible LLM adapterADR-0043: Tokenizer registry architectureADR-0044: Non-streaming response pipelineADR-0045: Streaming response transformationADR-0046: Embedder interface and profile registryADR-0047: Memory temporal validity foundationADR-0048: Memory multi-label categoriesADR-0049: Memory relationship graph readinessADR-0050: MCP server skeleton (transport, auth, audit)ADR-0051: Local LGTM observability stack (Phase 2.5 exit pull-forward)ADR-0052: Memory schema v2 expand (HNSW, quality columns)ADR-0053: Vector store abstraction and composite scoring v2ADR-0054: In-process Presidio PII detection in memoryADR-0055: Memory write-path exact and near-duplicate dedupADR-0056: Temporal-interval-aware conflict detectionADR-0057: Memory write-path persistence and orchestrationADR-0058: Semantic search FTS fallback triggerADR-0059: Hot cache read pathADR-0060: Memory service tenant isolation architectureADR-0061: Retrieval-quality benchmark methodologyADR-0062: Worker task observability and dead-letter handlingADR-0063: Extraction output schema v2ADR-0064: Extraction provider registry and batch executionADR-0065: Extraction batch idempotency and turn pointer orderingADR-0066: Extraction quality evaluation harnessADR-0067: Context capability catalog generate-and-diff for token budgetsADR-0068: Composite scoring-time relevance gateADR-0069: Context packer bounded DP knapsackADR-0070: Context formatting locked ordering and session nonceADR-0071: Context assembly gRPC degradation ladder and retrieval deadlineADR-0072: Extraction enqueue via worker HTTP (not Celery Redis wire format)ADR-0073: Organization Lifecycle and Cascading SuspensionADR-0074 Provider Credential StorageADR-0075 Per-Org Model Routing PolicyADR-0076: Circuit Breaker per ProviderADR-0077: Provider Fallback RoutingADR-0078: Operator runtime topology & environment contract (4.P.0)ADR-0079: Operator session RS256 dual-verify and TOTP step-upIBEX Console service rename and boundaryADR-0081: Fail-closed Proxy runtime controlsD2 metadata-only trace read contractD2 publication, identity, query, and lifecycle contract
ADRs›ADR-0057: Memory write-path persistence and orchestration
ADRs

ADR-0057: Memory write-path persistence and orchestration

Architecture decision record 0057 — owns pipeline steps 7–9 (DB write, cache, index); insert+supersession transaction; unique-violation→bump; durable conflict escalations deferred to milestone 3.C.5.

ADR-0057: Memory write-path persistence and orchestration

  • Status: Accepted
  • Date: 2026-08-27
  • Authors: IBEX Harness team
  • Milestone: 3.C.5 Write-path persistence & orchestration
  • Tracking issue: #620

Context

Track C ships a documented nine-step write pipeline (tracks):

validate → PII → dedup → embed → near-dup → conflict → DB write → cache update → index

Milestones 3.C.1–3.C.3 implement stages 1–6 (ADR-0054, ADR-0055, ADR-0056). No milestone owned steps 7–9. Track D is read/ranking; 3.C.4 is multi-label only. Phase 3 exit gate 3.E.2 requires POST /v1/memories end-to-end.

Also: ESCALATE_PENDING and pending_supersede_targets exist only on ephemeral WriteContext today — a conflict worker has nothing durable to poll.

Options considered

1) Ownership of steps 7–9

  1. Expand 3.C.4 to include insert/cache/index — overloads multi-label scope.
  2. Pull write orchestration into Track D — Track D is read/ranking.
  3. New milestone 3.C.5 before 3.C.4 (chosen). Multi-label attaches labels at insert time and needs a real insert transaction.

2) Insert + supersession transaction shape

  1. Staged writes with compensating actions on partial failure — complex; easy to leave orphan edges or active duplicates.
  2. Single Postgres transaction wrapping insert + apply_supersession / relationship edge writes (chosen). Cache and vector upsert run after commit (best-effort / retryable); do not hold the DB txn open for Redis or ANN.

3) Exact-hash unique violation (race after ADR-0055)

Concurrent inserts against the partial unique index can fail after the exact-dedup stage passed. Options: surface a 500, or map to the existing bump-retrieval path and return the documented 409 DUPLICATE_CONTENT / bump semantics. Chosen: map to bump — never a bare 500.

4) Durable escalations for ESCALATE_PENDING

  1. Invent pending_review on memories.status — forbidden without migration; ADR-0056 already rejected inventing it casually.
  2. New relationship_type like contradicts_pending — overloads the committed graph with workflow state (memory_relationships has no status column).
  3. Dedicated org-scoped table memory_conflict_escalations (chosen design). Worker polls status = pending. Implementation deferred to milestone 3.C.5 (#620); this ADR locks the design so the gap is not silent.

Decision

  1. Milestone 3.C.5 owns steps 7–9 and the HTTP write handler that backs the existing POST /v1/memories contract (do not invent a new REST shape).

  2. Sequencing: 3.C.1 → 3.C.2 → 3.C.3 → **3.C.5** → 3.C.4.

  3. After conflict stage returns, the orchestrator:

    • Opens one DB transaction
    • Inserts the new memory (gets DB-generated id)
    • Applies each pending_supersede_targets id via apply_supersession (and inserts other edges as decided by conflict outcomes)
    • Commits
    • Then best-effort cache update + vector upsert/index refresh
  4. On unique violation for active (org_id, agent_id, content_hash): call the existing bump path and return 409 with existing_memory_id per API docs.

  5. Escalation durability — finalized schema (migration 000019):

    SQL
    CREATE TABLE ibex_core.memory_conflict_escalations (
        id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
        org_id              UUID NOT NULL REFERENCES ibex_core.organizations(id),
        new_memory_id       UUID NOT NULL,
        candidate_memory_id UUID NOT NULL,
        conflict_type       TEXT NOT NULL,   -- ConflictOutcome (e.g. escalate_pending)
        status              TEXT NOT NULL DEFAULT 'pending'
                            CHECK (status IN ('pending', 'resolved', 'dismissed')),
        subject_key         TEXT,
        reason              TEXT,
        created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
        resolved_at         TIMESTAMPTZ,
        resolved_by         UUID             -- nullable; future human/LLM consumer
    );

    Composite org-scoped FKs on (new_memory_id, org_id) and (candidate_memory_id, org_id) → memories (id, org_id) — same defense-in-depth pattern as memory_relationships (000016) and memory_labels (000015). Prevents cross-tenant edge attachment even if application code omits an org_id filter.

    RLS: ENABLE + FORCE + rls_org_visible(org_id) — identical to sibling tables.

    Worker poll index: partial index on (org_id, created_at DESC) WHERE status = 'pending'.

    Rows are written in the same transaction as the new memory insert for ESCALATE_PENDING decisions — an escalation must never be lost if the insert succeeds. Do not add pending_review to memories.status (ADR-0056 rejected).

  6. Unique-violation race (concurrent identical content after exact-dedup pre-check):

    Catch Postgres 23505 on idx_memories_org_agent_content_hash_active inside the persist transaction, roll back, call the existing bump-retrieval path, and return the same 409 DUPLICATE_CONTENT + existing_memory_id shape as the pre-check path. Callers cannot distinguish which path fired.

  7. After-commit cache and index (steps 8–9):

    Run strictly after the DB transaction commits. Synchronous inline best-effort — not Celery (memory service has no task queue). Failure to update Redis or the vector index does not fail the HTTP write; log + metric + retry-worthy.

    StepTarget
    Object cache{org_id}:memory:{memory_id} (JSON, TTL 3600s)
    Hot sorted set{org_id}:hot_memories:{agent_id} (ZADD composite_score, atomic trim top-50, TTL 3600s) — see ADR-0059
    Vector indexPgVectorStore.upsert (UPDATE embedding on existing row)

    Skip after-commit for quarantined writes (no embedding) and exact-duplicate 409s.

  8. HTTP wiring timing: Stages 1–6 were built and testable as a pure pipeline function without a DB insert path. Only with a real insert transaction does POST /v1/memories make sense — wired in 3.C.5 per the existing API contract.

Consequences

  • 3.C.4 and 3.E.2 depended on 3.C.5 for a correct write product surface — 3.C.5 shipped (ADR-0057); 3.C.4 multi-label write shipped (ADR-0048).
  • Conflict workers gain a pollable escalation table when 3.C.5 lands; consumption is deferred — tracked in #627.
  • Worker poll query shape and partial-index EXPLAIN verification live in services/memory/tests/integration/test_premerge_guards.py.

Implementation note (2026-08-28)

Milestone 3.C.5 landed POST /v1/memories, transactional insert + supersession, after-commit cache/index, and memory_conflict_escalations. Milestone 3.C.4 adds optional labels[] in the same insert transaction (insert_labels_session); scalar memories.category remains trigger-maintained only.

Milestone 3.D.2 wires the same CompositeInputs shape used by hot-cache ZADD into the search read path (find_similar composite ranking). Milestone 3.D.3 adds atomic top-50 trim and MemoryHotCacheReader (ADR-0059).

Was this page helpful?

Edit on GitHub

Last updated on

PreviousADR-0056: Temporal-interval-aware conflict detectionNextADR-0058: Semantic search FTS fallback trigger

On this page

  • Context
  • Options considered
  • 1) Ownership of steps 7–9
  • 2) Insert + supersession transaction shape
  • 3) Exact-hash unique violation (race after ADR-0055)
  • 4) Durable escalations for ESCALATE_PENDING
  • Decision
  • Consequences
  • Implementation note (2026-08-28)
0%