Anamnesis Protocol
SkillDev toolsResolve vague recall into recognized context through AI-guided contextual scan and user-validated recognition — one session, or the line of work, topic, or settled concept spread across several.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Anamnesis Protocol skill
What this skill tells your AI
The instructions your AI receives, as published by jongwony/epistemic-protocols in anamnesis/skills/recollect/SKILL.md and read by ahel’s review.
Resolve vague recall into recognized context through AI-guided contextual scan and user-validated recognition. Type: (RecallAmbiguous, AI, RECOGNIZE, VagueRecall) → RecalledContext.
Definition
Anamnesis (ἀνάμνησις): A dialogical act of resolving vague recall into recognized context. The user holds an empty intention toward a past discussion they cannot name; AI reads the cue out of the utterance and the accumulated context, finds the candidates the hypomnesis INDEX and the record spines suggest (memory/ is a non-scanned, user-curated realization-layer adjunct — see STORE TOPOLOGY), opens each member's own record for the excerpt the cue reaches, and presents that excerpt with its source so the user can identify it. The INDEX narrative is a cue that wakes recall; the excerpt and its locator are the evidence; the act that makes a RecalledContext is the user's identification. Recognition over retrieval, grounded in the source rather than in the index.
── FLOW ──
Anamnesis(V) → Detect(V) →
not-empty_intention(V): relay(finding) → proceed (no activation)
empty_intention(V): Cue(V, Σ) → V.trace, V.unit → set(scan_scope = spine, attempts = 0) →
Find(Store ⊕ (scan_scope = full_text ? SSOT_body : ∅), V) → O[ranked] →
|O[]| = 0 ∧ attempts = 0: Ask(V) → Stop → H → recue(V, H) → set(attempts = attempts + 1) → Find
|O[]| = 0 ∧ attempts > 0 ∧ fulltext_unscanned: Qx(StoreExpansion) → Stop → X →
ExpandFullText: set(scan_scope = full_text) → Find
StopAtSpine: NullMatch → inform(V, Σ) → deactivate
|O[]| = 0 ∧ attempts > 0 ∧ fulltext_scanned: NullMatch → inform(V, Σ) → deactivate
|O[]| > 0: O[top] := Ground(O[top]) →
¬grounded(O[top]): O[] := O[] \ {O[top]} → Ground the new head, or fall to the |O[]| = 0 branches above when none is left -- no member's record opened: nothing to present from
grounded(O[top]): set(presented = O[top]) → Present(Λ.presented) → Stop → U →
Identified: emit(RecalledContext(Λ.presented)) → converge
Corrected(c) ∧ attempts < max: recue(V, c) → set(attempts = attempts + 1) → Find
Corrected(c) ∧ attempts = max: surface(Λ.presented) → deactivate -- AttemptsExhausted: the closest grounded candidate, no identification claimed
Withdrawn: deactivate -- the user moved on; no terminal claimed
── MORPHISM ──
VagueRecall
→ detect(empty_intention) -- recognize vague recall state
→ cue(trace, unit) -- what is meant, and at which whole: one session, or the line, topic, or concept above it
→ find(Store, cue) -- INDEX gist and record spines as cues (see STORE TOPOLOGY); above session scope the candidates are joined by read-time inferred edges (── FIND ABOVE SESSION SCOPE ──)
→ ask(user)? -- one open question when the first find returns nothing; a zero result expands or terminates only after at least one user round-trip — that question's answer or a correction, either of which is new cue information
→ expand(SSOT_body, user)? -- only after a round-trip and a spine-scope miss, at the user's election
→ ground(recognizable) -- open each member's own record: the excerpt the cue reaches, its locator, its handle; the narrative is composed from the excerpts
→ present(recognizable) -- one shape at every scope; the turn yields
→ identify(recognizable, user) -- synthesis of identification (Husserl CM §18) fulfilling the empty horizon (CM §19): the user's observable act, never inferred from silence
→ emit(RecalledContext) -- the identified recognizable with its excerpts and locators, as session text
→ RecalledContext
requires: empty_intention(V) -- phenomenological trigger
deficit: RecallAmbiguous -- activation precondition (Layer 1/2)
preserves: Store -- SSOT ⊕ INDEX are read-only; V is recued during the protocol
invariant: Recognition over Retrieval
── TYPES ──
V = VagueRecall { trace: RecallTrace, unit: Unit, cues: List(Cue) } -- the cue: what the user gave, at the whole they mean; cues accumulate what each answer and correction added
RecallTrace = { keywords: Set(String), temporal: Optional(String),
associations: Set(String), identifiers: Set(IdentifierTuple) }
Cue = String -- what the user adds: the answer to the open question, or a correction; recue folds it into the trace
Unit ∈ {session, line, topic, concept} -- the whole the intention names, read from V + Σ at Phase 0 and re-read by recue when a cue names a different whole. Closed on a stated premise: the three values above session are the three ways candidates join — succession, shared topic, a settled concept — and a recall matching none of them is at session scope
InputType ∈ {StructuredIdentifier, NaturalRecall, Mixed} -- Find's own dispatch, read from V + Σ; not a phase
Track ∈ {entropy, salience, hybrid} -- dispatched from InputType inside Find (dispatch bindings in ── ENTROPY EXTRACTION ── and ── SALIENCE MARKERS ──)
Source = String -- opaque: store location identifier (substrate-agnostic)
IdentifierTuple = { literal: String, source: Source, source_namespace: String, precision: ℝ[0,1] } -- entropy-track anchor
-- source_namespace determines which claim kinds it can authorize (via the registry); a literal anchors ranking only when its namespace authorizes the recall trace's claim kind
-- claim_kind is NOT a tuple field — it is determined by source_namespace at scan time, so the writer materializes only source_namespace (the extractor's namespace), not a per-literal claim_kind
MarkerProfile = { coinage: Set(Token), actor: Set(Entity),
temporal: Set(TimeRef), emotional: Set(Marker),
cognitive: Set(Marker), singularity: Set(Event) } -- salience-track profile
DateAnchor = String -- ISO 8601 date; reference point for salience-track temporal normalization (e.g., session start). Optionality is carried at the use site (DateAnchor?)
EvidenceMode = {user_constituted, attested, observed, inferred} -- totally ordered tier of the content's evidential STANDING (who stands behind it: user-authored verbatim > party-asserted with verbatim witness > mechanically present without assertion > LLM-synthesized) — NOT extractor reliability (that is extraction_method's concern). Assigned at write time by construction of each artifact's production path (deterministic metadata, never LLM-judged). Ranks recall weight only — NEVER suppresses/excludes; introduces no automatic effectivity (any downstream status change stays user-gated).
SourceScan = { skipped_lines: Nat, unverified_user_turns: Nat, omitted_chars: Nat } -- how completely the record's own source reached the extractor: transcript lines that failed to parse, human turns absent from the runtime's own cross-check channel, and characters a bounded extraction dropped from the middle of a long session. Any count non-zero ⇒ the record was built from less than its whole source, so the emit says so (`Recalled context currency is not fidelity`)
Store = SSOT ⊕ INDEX -- see ── STORE TOPOLOGY ── block
Candidate = { session_id: Optional(SessionId),
runtime: Source,
record: Optional(Source), -- the path to THIS candidate's own record, as the spine read declared it (── STORE TOPOLOGY ──: SSOT_spine yields "whatever handle the realization needs to reopen the record"). Ground opens this and never a path re-resolved at grounding time: the spine scan is not partition-scoped, so the active session's partition does not locate a candidate found in another one. Null ⇒ no record can be opened for this member — Ground yields an empty excerpt and the prose says so
recency: Optional(DateAnchor), -- the record's own last-activity time, as the spine read declares it (Claude: the file's modification time; Codex: the rollout's session_meta timestamp). Where the record is gone but its INDEX entry survives, the entry's own timestamp supplies it; Null only where neither has one — NEUTRAL in ranking exactly as evidence_mode and source_scan are, never a penalty. Optional because a candidate whose record no longer exists must still be constructible: it is precisely the candidate the Ungroundable path exists to receive, and a mandatory field sourced from the missing record would make that path unreachable. Read by Rank for ordering, and above session scope by edge inference — which is what makes references/supra-session.md's "reads only these fields" true of a field Candidate actually has
cwd: Optional(String),
topic: String,
keywords: Set(String),
fingerprint: Prose, -- the INDEX gist: a cue, never evidence
cross_refs: List(Anchor),
evidence_mode: Optional(EvidenceMode), -- highest tier among the signals that matched this candidate at scan time; Null ⇒ INDEX entry predates evidence-mode capture — Null is NEUTRAL in ranking (no contribution), never a penalty
source_scan: Optional(SourceScan), -- capture-time integrity of the record's own source, published by the writer; Null ⇒ the entry predates integrity capture. NEUTRAL in ranking exactly as evidence_mode is: it qualifies what the emit says about a candidate, never what the candidate scores
fork_marker: Bool, -- true ⇒ the id is a sidechain/fork with no top-level SSOT (SidechainNoSSOT); its own id is not a valid resume handle. Invariants: fork_marker = false ⇒ parent_pointer = Null ∧ parent_cwd = Null ; parent_pointer = Null ⇒ parent_cwd = Null (parent_cwd requires parent_pointer; parent_pointer present with parent_cwd = Null is valid — parent identified but its cwd is unknown)
parent_pointer: Optional(SessionId), -- orchestrating parent session for a fork candidate, read directly from the fork's own record; the resumable handle when the parent's top-level SSOT still exists (Null ⇒ parent record absent → non-resumable)
parent_cwd: Optional(String) } -- parent session's working directory, paired with parent_pointer to build the parent resume handle (Null ⇒ parent record absent, OR parent identified but its cwd metadata is unknown — parent transcript predates cwd capture)
Anchor = StructuredAnchor | LegacyAnchor -- Candidate.cross_refs element; extends-edge sediment (context-adding annotation)
StructuredAnchor = { kind: ∈ {memory, github_issue, github_pr}, ref: String, channel: ∈ {user, transcript} }
-- the stored form tells a later reader WHAT kind of reference and from WHICH utterance channel; it confers NO effectivity — a structured anchor is never authoritative by form, it remains a recall clue requiring the same re-verification as any recalled context
-- kind reuses the entropy source_namespace vocabulary where overlapping (github_issue, github_pr); memory is sediment-local. StructuredAnchor ≠ IdentifierTuple: no precision, no compatible_anchor authorization — shared value vocabulary, disjoint machinery (annotation vs anchoring)
-- ref stores the canonicalized literal (issue/PR numbers normalized to "#N", memory paths prefixed "memory/"); canonical-form grep over INDEX is form-invariant (a search for "#309" hits ref: "#309") — the canonical form is the dedup key, so raw surface variants ("PR 309") collapse into it
LegacyAnchor = String -- opaque: memory path, URL, session ID, doc path — entries written before structured anchors; read as kind-unknown extends edges, never rejected, no migration
Prose = String -- source-agnostic NL description
Recognizable = { unit: Unit, members: NonEmpty(Candidate), narrative: Prose, excerpts: List(Excerpt), assembly: Optional(Assembly) }
-- the one object at every scope; unit = session ⟺ |members| = 1. As Find returns it, narrative is the members' index gist and excerpts = ∅ — enough to order, never to assert. After Ground, excerpts holds one entry per member and narrative is composed FROM them, in the unit's shape (one session: origin → direction → outcome; a line: origin → development → arrival; a topic: fragments → standing; a concept: forged by → settled at); nothing the excerpts do not carry is asserted
Assembly = { joined: the inferred edges that joined this recognizable's members, skipped: the links that resolved to no written record } -- references/supra-session.md TYPES: Edge. Null at unit = session, where one member joins nothing. Carried on the object rather than in Λ so that surface(Λ.presented) at AttemptsExhausted still has the coverage after the yield — the recognizable is already the thing that crosses it
Excerpt = { member: Candidate, text: Prose, locator: Source, handle: Optional(ResumeHandle) } -- the record's own words at the span the cue reaches, where that record is, and the resume command the runtime reference validates (Null ⇒ non-resumable, stated in the prose). A member whose record cannot be opened yields text = ∅ and contributes nothing the narrative may assert
ResumeHandle = String -- the literal command the runtime reference emits (references/claude.md, references/codex.md; fork members per references/fork-resume.md)
O[] = List(Recognizable) -- Find's ranked result at V.unit; O[top] is its head
Find = (Store, V) → List(Recognizable) -- Scan_{Track} over the compact INDEX and the record spines (── STORE TOPOLOGY ──), joined above session scope (── FIND ABOVE SESSION SCOPE ──), ordered by Rank
Rank = (List(Recognizable), RecallTrace) → List(Recognizable) -- ordering only, computed from how well each recognizable matches the cue — no stored tier is read, because no writer publishes one: track-primary signal dominates, evidence_mode is a secondary tie-break (never a filter; Null neutral), edge connectivity joins it above session scope. No gate reads the order; it decides only which recognizable Ground opens first
Ground = Recognizable → Recognizable -- one read per member of O[top]: open the member's own record at member.record, resolved by its runtime reference, take the excerpt the cue reaches, bind its locator and handle (a fork member per references/fork-resume.md), then compose the narrative from the excerpts. Bounded by |members(O[top])|, never by the store — it opens named records, it does not scan bodies. Idempotent: a member already read is not re-read
grounded = predicate; grounded(o) ≡ ∃ e ∈ o.excerpts : e.text ≠ ∅ -- at least one member's record opened. A recognizable no member's record could be opened for carries no evidence at all, and since the narrative is composed FROM the excerpts there is nothing it may assert — so it is never presented
Ask = V → Question -- one open question when Find returns nothing: what else the user remembers, in their own words; no dimension taxonomy, no candidate to regenerate
Question = String
H = Cue -- the answer to the open question
U = Response ∈ {Identified, Corrected(Cue), Withdrawn} -- read from the user's next utterance after Present: Identified when they say so or proceed on that context; Corrected when the utterance turns the cue — "not that one", an earlier one, the whole line — and the utterance IS the cue; Withdrawn when it leaves the recall behind. Premise: one utterance carries one disposition, and silence is none of them — a turn that has not come back has decided nothing
X = StoreExpansion ∈ {ExpandFullText, StopAtSpine}
ScanScope ∈ {spine, full_text} -- spine = INDEX ⊕ SSOT_spine (the initial scope); full_text additionally admits SSOT_body
fulltext_unscanned ≡ Λ.scan_scope = spine
fulltext_scanned ≡ Λ.scan_scope = full_text
recue = (V, Cue) → V -- folds the cue into V.trace and V.cues; a cue naming a different whole re-reads V.unit
emitted(x) = predicate; the emit(x) has fired in session text
RecalledContext = session text carrying the identified Recognizable: its narrative, each member's excerpt with locator and handle, and the currency caveat below
-- recall establishes IDENTITY (this WAS discussed/decided), not current-reality FIDELITY (it still HOLDS). Store-currency (the INDEX entry is fresh) ⊂ fidelity-to-current-reality: a recalled decision may be superseded, a recalled path renamed, a recalled convention revised. RecalledContext describes a PAST state; downstream consumers re-verify against current state before commit rather than treating it as confirmed current context.
find_empty = predicate; find_empty ≡ |O[ranked]| = 0 at the Phase 1 branch point -- the guard that reaches Ask, Qx, and NullMatch; a predicate over it is evaluated where the guard holds, in the same pass as the empty Find, never reconstructed after a yield
NullMatch = predicate; canonical definition in ── CONVERGENCE ──
AttemptsExhausted = predicate; canonical definition in ── CONVERGENCE ── -- the candidate-in-hand terminal, distinct from NullMatch's nothing-found one
Phase ∈ {0, 1, 2, 3}
max = the recall-try cap LOOP fixes -- a bound on user attention, not a sufficiency criterion
── V-BINDING ──
bind(V) = explicit_arg ∪ colocated_expr ∪ prev_user_turn -- priority: explicit_arg > colocated_expr > prev_user_turn
/recollect "text" → V.trace = extract_trace("text", Σ)
"recall... topic" → V.trace = extract_trace(text before trigger, Σ)
/recollect (alone) → V.trace = extract_trace(previous user message, Σ)
Edge cases:
- Multiple vague references: bind to first, note others; re-invoke after NullMatch: fresh V, no carryover
- Composition (/recollect * /inquire): V from Anamnesis, Aitesis receives RecalledContext via session text
── PHASE TRANSITIONS ──
Phase 0: V → Detect(V) → empty_intention(V)? -- trigger (silent)
[¬empty_intention(V)] relay(finding) → proceed -- zero-signal: present activation finding, proceed without activation
→ Cue(V, Σ) → V.trace, V.unit → set(scan_scope = spine, attempts = 0) -- the cue and the whole it names; Find's dispatch (InputType → Track) is read here too; initial scope + recall-try budget (silent)
Phase 1: V → Find(INDEX ⊕ SSOT_spine ⊕ (scan_scope = full_text ? SSOT_body : ∅), V) → O[ranked] -- index + spine always; bodies too once ExpandFullText widened the scope, so a re-entry after a correction does not narrow back to spine and report a body-scoped miss; above session scope Find joins its candidates into recognizables (── FIND ABOVE SESSION SCOPE ──) [Tool]
|O[ranked]| > 0 → O[top] := Ground(O[top]) → -- one read per member of O[top]: excerpt, locator, handle; the narrative is composed from the excerpts before anything is presented [Tool]
grounded(O[top]) → Phase 2
¬grounded(O[top]) → O[ranked] := O[ranked] \ {O[top]} → Ground the new head; when none is left the |O[ranked]| = 0 guards below receive it, and the records that could not be opened are named in whatever those guards emit — the open question's framing, Qx's pre-gate text, or the NullMatch diagnosis. All of them fire in this same turn, so the list needs no carrier
|O[ranked]| = 0 ∧ attempts = 0 → Ask(V) → Stop → H → recue(V, H) → set(attempts = attempts + 1) → Phase 1 -- one open question [Tool]
|O[ranked]| = 0 ∧ attempts > 0 ∧ fulltext_unscanned → Qx(StoreExpansion) → Stop → X -- store-expansion checkpoint; its pre-gate text reports the coverage searched, above session scope the traversal too [Tool]
ExpandFullText → set(scan_scope = full_text) → Phase 1 -- re-enters Find with the widened scope: index, spines, and bodies together, so a spine candidate can still join a body one
StopAtSpine → NullMatch → inform → deactivate
|O[ranked]| = 0 ∧ attempts > 0 ∧ fulltext_scanned → NullMatch → inform → deactivate
Phase 2: O[top] → set(presented = O[top]) → Present(Λ.presented) → Stop → U -- the one presentation shape [Tool]: narrative from the excerpts, each member's excerpt with its locator and handle, the adjacent candidates named when |O[ranked]| > 1, the remaining budget, the currency caveat; the turn yields and the next utterance is read as U. The grounded recognizable is written to Λ BEFORE the yield — it is the only thing Phase 3 still needs and the yield is what it has to survive
Phase 3: U → integrate(U, V, Σ) → -- integration reads Λ.presented, written at Phase 2 before the yield
Identified → emit(RecalledContext(Λ.presented)) → converge
Corrected(c) ∧ attempts < max → recue(V, c) → set(attempts = attempts + 1) → Phase 1 -- the correction is the next cue; a cue naming a different whole re-reads V.unit
Corrected(c) ∧ attempts = max → surface(Λ.presented) → deactivate -- AttemptsExhausted (CONVERGENCE): name the closest grounded candidate and the coverage searched, claim no identification
Withdrawn → deactivate -- no terminal claimed; a later recall starts fresh
── LOOP ──
Phase 1 → Phase 2 → Phase 3 →
Identified: converge
Corrected: recue → Phase 1 -- the user's words are the new cue; above session scope a correction may narrow the whole, widen it, or name a different one
Withdrawn: stand down, nothing claimed
Phase 1 zero result before any round-trip (attempts = 0): Ask once → recue → Phase 1; a zero result after a round-trip — the question's answer or a correction — at spine scope → StoreExpansion:
ExpandFullText: set scan_scope = full_text → Phase 1 with index, spines, and bodies
StopAtSpine: NullMatch → deactivate
Max 3 recall attempts; `attempts` starts at 0 in Phase 0, the open question's answer and each correction spends one, and a Find's own result — at any scope — spends nothing. Exhausted with a candidate in hand: AttemptsExhausted — surface O[top] as the closest found, without claiming identification → deactivate. A nothing-found exhaustion terminates as NullMatch instead (both in CONVERGENCE). The counter witnesses round-trips, not questions: a correction carries new cue information exactly as an answer to the open question does, so a zero result may expand or terminate after either, and the open question is owed only while no round-trip has happened.
Convergence evidence: (VagueRecall → [cues] → Recognizable(grounded) → Identified → RecalledContext).
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 161
- Forks
- 15
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
recollect- Source
- github.com/jongwony/epistemic-protocols