Sokratos is designed to avoid silent data loss across its memory pipelines. Every path from ingestion to storage has retry mechanisms backed by PostgreSQL, so in-flight work survives process crashes and transient backend failures.
When a user sends a message, the exchange is not triaged immediately. Instead:
- The message loop writes the raw exchange to the
pending_triagetable viaEnqueueConversationTriage(). - During the next heartbeat tick,
DrainPendingTriage()reads pending items and runs the full triage pipeline (truncation, adaptive threshold lookup, salience scoring, contradiction check, memory save). - On success, the row is deleted.
- On failure, the retry counter is incremented. After 3 failures, the item is logged to
failed_operationsand deleted.
This ensures conversation data is never lost due to slot contention, LLM timeouts, or process crashes. The pending_triage table acts as a durable work queue.
Emails follow a similar pattern with an additional safety guarantee:
- New emails are fetched and filtered against
processed_emails. - Each email is triaged asynchronously via
TriageAndSaveEmailAsync(). - The email is only marked as processed (via an
onProcessedcallback) after triage succeeds or is enqueued for retry. - If triage fails and cannot be retried (no DB), the email is not marked as processed — it will be picked up on the next email check.
This prevents the scenario where an email is marked as "seen" but its content was never saved to memory.
When the conversation context exceeds the token budget, old messages are archived:
SlideAndArchiveContext()formats the trimmed messages and sends them for distillation (LLM extraction of lasting facts).- Distillation tries multiple backends in order: DTC queue → subagent queue → direct grammar call → direct subagent call.
- If all backends fail, the raw archive text is written to the
pending_distillationtable. - During heartbeat maintenance,
DrainPendingDistillation()retries pending items with available backends. - After 3 failed retries, the raw text is saved directly to memory as a
conversation_archive— less granular than distilled facts, but the data is preserved.
All final-attempt failures are logged to the failed_operations table with operation type, label, error message, and context data. The heartbeat includes recent failures in its context XML (<recent_failures>), so the supervisor is aware of persistent issues.
| Table | Purpose | Drained by |
|---|---|---|
pending_triage |
Deferred and failed triage items | Heartbeat tick (DrainPendingTriage) |
pending_distillation |
Failed archive distillation | Heartbeat maintenance (DrainPendingDistillation) |
failed_operations |
Terminal failures (observability) | Manual inspection / heartbeat context |
- Process crash during heartbeat drain: Items currently being processed (mid-triage, mid-distillation) may fail without being re-queued. The next drain will skip them since they're deleted only on success.
- Database unavailable: If PostgreSQL is down, enqueue calls log a warning but the item is not persisted. This is an infrastructure failure outside the application's control.
- Tool results in conversation: Tool call parameters and raw results are not archived during context sliding — only the assistant's natural language synthesis. If the assistant's response is vague, the raw tool data is lost.