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-0065: Extraction batch idempotency and turn pointer ordering
ADRs

ADR-0065: Extraction batch idempotency and turn pointer ordering

Architecture decision record 0065 — identity-based X-Idempotency-Key for batch extraction writes, mandatory SessionStore, per-turn last_extracted_turn advance, and explicit residual limits.

ADR-0065: Extraction batch idempotency and turn pointer ordering

  • Status: Accepted
  • Date: 2026-09-03
  • Authors: IBEX Harness team
  • Milestone: 3.5.B.3 Incremental turn tracking and idempotency
  • Depends on: ADR-0064, ADR-0035, ADR-0055

Context

3.5.B.2 ships batch session-close extraction: the worker calls an LLM, then writes memories over HTTP to POST /v1/memories (services/memory), then updates ibex_core.sessions.last_extracted_turn via a separate worker Postgres connection (ADR-0064).

The milestone planning sketch proposed wrapping memory writes and pointer update in one worker-local async with db.transaction() block. That is architecturally impossible: memory rows commit inside services/memory after a separate HTTP request completes; a worker Postgres transaction cannot roll back rows another process already committed.

Pre-3.5.B.3, the worker derived X-Idempotency-Key from LLM output content (org:session:turn_index:memory.content). Retries after partial failure with non-deterministic LLM wording produced different keys and different content_hash values — defeating both the HTTP idempotency layer and exact dedup (migration 000018).

Memory-service idempotency (ADR-0035, memory_write_support.py) fingerprints POST|/v1/memories|{body}. Same key + different body → 409 IDEMPOTENCY_CONFLICT, not replay. Identity-based keys alone are insufficient unless the worker treats that conflict (and DUPLICATE_CONTENT) as definitive success so first-commit wording wins.

Decision

1) Identity-based idempotency keys (slot identity, not content)

Derive X-Idempotency-Key from extraction slot identity, not LLM output text and not a batch content fingerprint:

key_material = f"{org_id}:{session_id}:{turn_index}:{memory_ordinal}"
digest = sha256(key_material).hexdigest()
X-Idempotency-Key = str(uuid5(NAMESPACE_DNS, digest))
  • memory_ordinal = 0-based index within that turn's memories[] in a single provider result.
  • Retries with different LLM wording for the same turn+ordinal reuse the same key.

Residual limitation (accepted): if a retry's LLM call returns a different number of memories for the same turn, ordinals can misalign. Exact/near-dedup in services/memory is defense-in-depth for that residual case — not solved by this key formula.

Implementation: services/worker/app/extraction/memory_writer.py (memory_idempotency_digest, memory_idempotency_key).

2) Memory HTTP success semantics on retry

ResponseWorker behavior
2xx (incl. X-Idempotency-Replayed: true)success
409 IDEMPOTENCY_CONFLICTsuccess (first wording wins)
409 DUPLICATE_CONTENTsuccess (exact-hash backstop, ADR-0055)
409 IDEMPOTENCY_IN_PROGRESSretryable (ExtractionTransportError)
408/429/5xxretryable (ExtractionTransportError)
other 4xxhard failure

3) Mandatory SessionStore + per-turn pointer advance

extract_session_memories requires database_url / SessionStore. Missing DB config is a hard ValueError (fail closed), not a silent skip of pointer tracking.

Advance last_extracted_turn after each turn's memories succeed (not after the whole batch). Rationale: a mid-batch crash keeps completed turns durable in the pointer so the next run skips them via turn_index > last_extracted_turn, shrinking rework without claiming cross-service atomicity.

  • Still non-atomic with HTTP writes (memory commits in another process).
  • UPDATE uses GREATEST(last_extracted_turn, :turn) so concurrent/out-of-order advances cannot move the pointer backward.
  • Pointer update failures (SQLAlchemyError, OSError) map to ExtractionTransportError so Celery autoretry_for retries the task.

Crash windows (bounded):

  • After some turns' writes+pointer succeed, crash mid-batch → pointer holds the last completed turn; retry filters already-advanced turns.
  • After a turn's writes succeed but before that turn's pointer UPDATE → retry re-POSTs the same identity keys (safe via §1–2); may incur one additional provider.extract for remaining work. Zero-cost retry would require an outbox / 2PC — out of scope.

4) Non-goals

  • No distributed two-phase commit or outbox between worker and memory service.
  • No batch write API on services/memory.
  • No change to memory-service dedup algorithms.
  • No change to worker→memory PAT auth (static Bearer memory:write).
  • Content-hash exact dedup remains a complementary backstop, not a substitute for identity keys.

Consequences

  • Same turn+ordinal retries collide on idempotency keys regardless of LLM wording drift.
  • First-commit memory wording wins when retry wording differs (IDEMPOTENCY_CONFLICT treated as success).
  • Operators must provision worker DATABASE_URL; extraction without SessionStore is rejected.
  • Per-turn pointer advance reduces rework on partial batch failure versus post-batch-only updates.
  • Idempotency records expire after 24h (memory Redis TTL); keys older than TTL behave as new writes — same as all POST /v1/memories idempotency.

Was this page helpful?

Edit on GitHub

Last updated on

PreviousADR-0064: Extraction provider registry and batch executionNextADR-0066: Extraction quality evaluation harness

On this page

  • Context
  • Decision
  • 1) Identity-based idempotency keys (slot identity, not content)
  • 2) Memory HTTP success semantics on retry
  • 3) Mandatory SessionStore + per-turn pointer advance
  • 4) Non-goals
  • Consequences
0%