Memory across sessions

A new agent connection recovers where the last one left off through a small family of explicit calls — orient, catch_up, brief_me, saved session pages, an acknowledged-corrections loop, in-DB skills, and a persistent goal stack — not through anything that happens automatically.

Verified 2026-07-09 @ dfd671a3

What this is

Cross-session memory on Ouroboros is a family of explicit, callable primitives, not a hidden context window an agent inherits for free: sophia.orient bootloads a fresh connection, sophia.catch_up answers “what changed since I was last here,” sophia.brief_me composes a task-scoped context bundle, and sophia.save_agent_session / sophia.replay_session persist and replay a session’s own record. A separate per-session acknowledgment loop, an in-DB skill library, and a durable goal stack round out the surface.

Why it exists

Every new MCP connection starts as a fresh process with no memory of any prior turn — the model behind it has no more continuity than a new browser tab. Without a durable layer, that means re-deriving the same facts every session: what’s in the graph, what was already tried, what the user already corrected. The sophia:resume-session in-DB skill’s own evidence field names the cost directly: “Fresh agents burn 5-20 minutes re-deriving session context” before orient() shipped as a single bootload call. A session that starts from one orient call doesn’t just start cheaper — it starts with a clean, decision-dense context instead of one already crowded by an exploration sweep.

Re-deriving facts is one failure mode; re-breaking a correction is a worse one. Truth covers the write-time rule that a human correction structurally outranks a model’s later write. That rule only helps if the next session’s agent actually notices the correction happened — which is a session- continuity problem, not a write-time one, and is what orient()’s last_corrections plus sophia.acknowledge_correction exist to close.

How it works

sophia.orient is the single-call bootloader (proxy/src/mcp/orient.ts). One round trip returns sync_status, active_focus, hot_entities, goal_stack, last_corrections, relevant_skills, and next_likely_calls — replacing a chain of get_briefing + list_mutations + query_entities + query_knowledge calls a fresh agent used to make by hand. last_corrections is populated by listUnackedCorrections() (proxy/src/mcp/corrections.ts): corrections a human made that this session hasn’t yet acknowledged. Calling sophia.acknowledge_correction({ mutation_id, action_taken }) stops that correction resurfacing on later orient() calls in the session — the mutation itself only exists once, but every fresh session gets one more chance to notice and honor it until it’s explicitly acked.

sophia.catch_up (proxy/src/mcp/catchUp.ts, spec AX-2) answers “what changed since I was last here” across six read-view sections — sessions, mutations, knowledge, work, wiki, approvals — over existing tables, no new writes. Its cutoff resolves in three tiers: an explicit ISO timestamp always wins; otherwise it walks this connection’s own most recent save_agent_session wiki page, then mcp_connections.last_accessed_at, then falls back to 24 hours ago. entity_scope can only narrow the connection’s own scope, never widen it.

sophia.brief_me (proxy/src/mcp/briefMe.ts) answers a different question — not “what happened,” but “prime me for this task” — composing ranked documents, facts, and wiki pages from a short task description into one bundle with a token_budget_hint, replacing a hand-chained search_documents + query_knowledge + list_wiki_pages sequence.

sophia.save_agent_session writes a type: 'concept' wiki page (frontmatter.session: true) at the end of a work session — summary, decisions, learnings, open questions, optionally 3-5 typed insights. sophia.replay_session, covered from the mutation-journal side on Time Machine, reads a different table entirely — subscriber_agent_actions, the per-call tool-call log — to answer “what did an agent actually do,” not what it wrote.

Skills split into two layers. In-DB SophiaSkills — sophia.list_skills / sophia.load_skill — are markdown-bodied rows in the graph itself (proxy/src/mcp/skills.ts), scoped per user, filterable by bundle, incrementing an invocation counter on load. A separate methodology layer of skills lives in the calling harness (Claude Code’s own skill files, e.g. sophia-session-lifecycle) and is never rows in Sophia’s database; the in-DB skill sophia:mine-code-module’s own body names the split explicitly, pointing agents at “user-level methodology skills” it composes with but does not contain.

Goals (sophia.set_goal / complete_goal / abandon_goal, proxy/src/mcp/tools/goalTools.ts) push onto a persistent stack that survives session boundaries — orient()’s goal_stack field is that live stack, so a fresh agent sees what was in flight without asking.

fresh connection → bootstrap → work → saved record
flowchart LR
  N["New connection"] --> O["sophia.orient()<br/>sync_status + last_corrections + goal_stack"]
  O -->|has a prior session| C["sophia.catch_up()<br/>6-section delta since cutoff"]
  O --> B["sophia.brief_me(task)<br/>task-scoped bundle"]
  C --> W["Work this session"]
  B --> W
  W --> S["sophia.save_agent_session()<br/>wiki concept page"]
  S -.->|next connection's catch_up cutoff| C

What your agent does with it

// Real responses from this daemon, captured 2026-07-09:
const o = await sophia.orient({ goal: 'resume prior work' });
// → { goal_stack: [ 'Research memory-across-sessions facts...', ... ],
//   last_corrections: [ /* 5 unacked human corrections */ ],
//   relevant_skills: [ { id: 'sophia:resume-session', why: 'tag session' } ] }

const cu = await sophia.catch_up({ limit_per_section: 3 });
// → { cutoff_tier: 'session_record',
//   my_last_session: { slug: 'session-2026-07-08-c3a-delegated-minting-ship', ... },
//   sessions: { count: 0, items: [] },
//   knowledge: { new_count: 5, new_top: [ /* 3 rows, confidence 0.85 */ ] },
//   wiki: { count: 1, items: [ { slug: 'session-2026-07-08-c3a-delegated-minting-ship', ... } ] },
//   is_empty: false }

const skill = await sophia.load_skill({ skill_id: 'sophia:resume-session' });
// → { bundle: 'core', trigger: 'Fresh session wake-up...',
//   body_md: '## Steps\n\n1. Bootload. Call sophia.orient(...)...',
//   invocation_count: 3 }

That catch_up cutoff resolved to session_record — this connection’s own last save_agent_session page, not the 24-hour fallback — because a session was saved earlier the same day. The fresh-session sequence a real agent runs is: orient() first, always; catch_up() next when orient() names a prior session worth resuming (next_likely_calls says so directly when it applies); brief_me(task) when the work is task-scoped rather than delta-scoped; and save_agent_session() at the end, so the next connection’s catch_up has something to resolve against.

Boundaries

Memory here is per-graph, keyed by user_id, not per-model — every query in catchUp.ts and orient.ts is user_id-anchored, so switching which LLM sits behind a connection changes nothing about what that connection can recall, and a connection scoped to a different user’s graph recalls nothing at all. A new connection starts scoped, not omniscient: entity_scope narrows what orient, catch_up, and brief_me can see, and a caller-supplied entity_scope on catch_up can only filter down from the connection’s own scope — an id outside it is silently dropped, never escalated.

None of this is automatic recall. orient(), catch_up(), and brief_me() are calls an agent has to make; nothing pushes prior-session content into a model’s context unasked, and a session record that’s never saved via save_agent_session simply doesn’t exist for the next connection to find. The daemon and its storage layers are The State Layer; the row-level undo machinery and replay_session’s tool-call log live on Time Machine; the write-time correction rule this page’s acknowledgment loop sits on top of is Truth; where session pages actually land is The Wiki; and the full tool catalog these calls are drawn from is MCP Surface.