Durable Context Outbox Queue & Background Delivery Worker #17
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implement the durable SQLite outbox table (
outbox), Tokio background delivery worker routed through the dedicated single database worker, operation status polling, asynchronous lifecycle state machine, and the foundational context & capture API routes (POST /v1/contextandPOST /v1/capturesfor task/event/context and mixed action+rationale, Scenario W02).Background & Technical Rationale
Life records semantic context, rationales, and user preferences in Hindsight. However, network services can experience outages, latency spikes, or intermittent failures. A core architectural principle of Life is strict decoupling between transactional calendar execution and semantic context delivery:
Calendar mutations (creating, updating, and completing tasks or events in Radicale) MUST be committed independently of Hindsight availability. If Hindsight is unreachable, down for maintenance, or returning HTTP 500/503 errors, the calendar mutation MUST succeed, commit to SQLite WAL and Radicale, and return success to the caller with
context_delivery: "pending"(or"unavailable"). Under NO circumstances may an unavailable Hindsight backend roll back, fail, or delay a confirmed calendar write.Life's outbox and API contracts strictly separate three distinct stages of context ingestion:
operation_id).To survive crashes and power loss without dropping context, outbound memory payloads are committed to a durable SQLite
outboxtable within the same local ACID transaction that records the calendar change inlife_events.When Hindsight returns HTTP 202 Accepted with an
operation_id, the worker MUST persist theupstream_operation_idand pollGET /v1/default/banks/{bank_id}/operations/{operation_id}rather than repeatedly resubmitting the memory. Resubmitting an in-flight operation wastes resources and can cause upstream lock contention.If the process crashes after Hindsight accepts a dispatch or during polling, the durable outbox record survives. Upon restart, the worker recovers the pending record, continues polling the known
upstream_operation_id, or resubmits under the same deterministicdocument_id(life-event:<id>) withupdate_mode: "replace".Exact Technical References & Specifications
Normative SQLite Outbox Schema
From
docs/technical/timeline-consistency.md(lines 97–112):Context Delivery & Outbox Contract
From
docs/technical/timeline-consistency.md(lines 160–165):From
docs/technical/hindsight.md(lines 31–38):Verification Matrix Scenarios
From
docs/technical/verification.md(lines 98, 117–118):Section Citations
docs/technical/timeline-consistency.md: Section Illustrative schema (lines 97–112), Section Context delivery and correction (lines 160–165).docs/technical/hindsight.md: Section Delivery states (lines 31–38).docs/technical/verification.md: Section Recovery scenarios (line 98, Scenario R12), Section Integration scenarios (lines 117–118, Scenarios I03, I04).docs/technical/cli-api.md: Section Command payloads and results (lines 82, 87, 93, 148).What to Build
1. Database Schema & Migration (
src/storage/schema.rs)outboxtable as specified intimeline-consistency.md.OutboxStateenum:2. Transactional Enqueue API (
src/hindsight/outbox.rs)enqueue_context_deliveryrouted through the dedicated database worker thread (DatabaseHandle):delivery_id(del_<nanoid>).outboxwithstate = 'pending',attempts = 0,schema_version = 1.UNIQUE(destination, document_id)constraint viaON CONFLICT(destination, document_id) DO UPDATEto overwrite pending payloads if a newer update occurs before dispatch.3. Asynchronous Outbox Delivery Worker (
src/hindsight/worker.rs)OutboxWorkerrunning as a background Tokio task supervised by the Life daemon, performing all database reads and updates viaDatabaseHandle:Query up to
Nrecords withstate IN ('pending', 'retry_wait')wherenext_attempt_at IS NULL OR next_attempt_at <= datetime('now').Call
client.retain(payload).Update
outboxrecord tostate = 'completed', record memory IDs inupstream_ids_json.Update
outboxrecord tostate = 'accepted', recordupstream_operation_id.Increment
attempts. Compute exponential backoff with full jitter: ParseRetry-Afterheader if present. Updatestate = 'retry_wait',next_attempt_at = now + \Delta t.Update
state = 'failed', recordlast_error_code, log structured error to stderr. Do not blindly retry fatal authentication/schema errors.Query records with
state = 'accepted'andupstream_operation_id IS NOT NULL.Call
client.poll_operation(upstream_operation_id)."completed":Update
state = 'completed', record settled memory IDs inupstream_ids_json."in_progress"or"pending":Leave in
accepted, schedule next poll after interval (e.g. 2s)."failed":Transition to
retry_wait(if retryable) orfailed.Listen on
cancel_token.cancelled(). Complete current in-flight HTTP request, ensure transaction state is cleanly committed, and exit without leaving database locked.4. API Endpoints & Unified Capture Foundation (
src/api/context.rs,src/api/captures.rs,src/cli/capture.rs)POST /v1/context(life context add --input -):{ text: String, item_id: Option<String>, reason: Option<String>, supersedes_context_id: Option<String> }.context_addedevent tolife_eventsand enqueues outbox record in one transaction.{ operation_id, source_event_id, delivery_state }.POST /v1/captures(life capture --input -):{ kind: "context" | "task" | "event", payload: serde_json::Value, context: Option<String> }.kind: "context"(Scenario W02): creates zero calendar items; enqueues context delivery; returns pending (exit 6) until upstream delivery confirms.pending/accepted/completed).5. Integration with API Mutation Responses
src/api.rs:context_delivery: "pending"in the JSON envelope.context_delivery: "delivered".context_delivery: "unavailable".context_delivery: "not_requested".5. Integration Test Suite (
tests/hindsight_outbox_tests.rs)life task addwith context rationale.context_delivery: "pending".outboxtable contains row withstate = 'retry_wait'.acceptedandupstream_operation_idis populated."completed".completed.outboxtable with record instate = 'accepted'and knownupstream_operation_id.Acceptance Criteria
outboxaspendingorretry_wait(Scenario I04).accepted), settled ingestion (completed), and background derived observation refresh (Scenario I03).upstream_operation_idare polled viaGET .../operations/{id}rather than repeatedly resubmitted while active.life capturewithkind: context) creates zero calendar items and returns pending delivery state (Scenario W02).Retry-Afterheaders.document_idunderupdate_mode: "replace"without generating new source events (Scenario R12).Notes
POST /v1/captures,kind: contextandtask|event+context), never implicitly by a mutation carryingreason—docs/technical/hindsight.mdforbids silently retaining every CRUD action.outcome/correctionkinds are refused as not-yet-implemented (#28/#29).idx_outbox_stateso fresh and pre-existing databases converge, with a test that fails without the drop./v1/tasks,/v1/events, update and lifecycle./v1/contextand/v1/context/searchare #18's.Cargo.tomlauthors chore stays pending.Blocked by
Issue #017: Durable Context Outbox Queue & Background Delivery Workerto Durable Context Outbox Queue & Background Delivery Worker