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.
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.