Skip to content

Memory pipeline: capture to distill to recall to effectiveness

Argus's universal memory hub. Capture is pure transport (no LLM call); distillation and dedup are the two LLM-gated write paths; recall has two independent entry points (context injection at session start, and relevance-gated prompt injection per turn); effectiveness is an observational grading loop, not an automatic feedback loop -- nothing here writes graded results back into the ranking weights.

flowchart TD
    CAP["Capture hook\n(Stop/SessionEnd/PreCompact)"] --> POST["POST /capture"]
    POST --> STORE["Store.Capture"]
    STORE --> DUPE{"dedup_key\nalready seen?"}
    DUPE -->|yes| RETURN["return existing id\nduplicate=true"]
    DUPE -->|no| RAW[("raw_items")]

    RAW --> SEG["segment.go:\nturns -> episodes\n(size/time-gap/topic-shift)"]
    SEG --> PROC["ProcessPending\n(advisory-lock orchestrator)"]

    PROC --> GATE{"distill.go/gate.go:\npoison / grounding /\ntitle-length checks"}
    GATE -->|fail| REJ[("gate_rejections")]
    GATE -->|pass| EMB["embed.go:\nembedNotesBatch (768-d)"]

    PROC -->|"infra/content error"| PARK{"DistillFailureCap\nexceeded (5)?"}
    PARK -->|yes| DEAD[("distill_failures\ndead-letter")]
    DEAD -.->|"UnparkUnit"| PROC

    EMB --> DEDUP["dedup.go:dedupOne\ncosine kNN, floor 0.80"]
    DEDUP -->|distinct| KEEPBOTH["keep both, log only"]
    DEDUP -->|duplicate| TOMBNEW["tombstone new"]
    DEDUP -->|refine| REPLACE["keep new, tombstone old\nconfidence bump"]
    DEDUP -->|"supersede,\nold confidence < 0.70"| SUPER["tombstone old"]
    DEDUP -->|"supersede,\nold confidence >= 0.70"| FLAG["flagged for\nhuman review"]

    KEEPBOTH --> NOTES[("distilled_notes")]
    REPLACE --> NOTES
    SUPER --> NOTES

    NOTES --> CTX["ContextNotes:\nSessionStart, deterministic\nimportance/recency, limit 12"]
    CTX --> INJ1["ContextInject\nprompt_mode=false"]

    NOTES --> PR["PromptRecall:\nUserPromptSubmit"]
    PR --> FLOOR{"substanceScore < 5\nor system-turn tag?"}
    FLOOR -->|yes, skip| SKIP1["no recall call"]
    FLOOR -->|no| HYBRID["HybridRecall:\nsemantic + FTS, RRF fuse"]

    HYBRID --> BOOST["type-prior -> recency ->\nproject-boost -> co-retrieval ->\nimportance-demotion -> rerank"]
    BOOST --> CUTOFF["adaptive-K cutoff\nor MMR diversify"]
    CUTOFF --> INJ2["RecallInject\nprompt_mode=true"]
    INJ2 --> CFLOOR{"client-side\nrelevance floor 0.015?"}
    CFLOOR -->|below| DROP["dropped before\nadditionalContext"]
    CFLOOR -->|above| EMIT["emitted as\nadditionalContext"]

    INJ1 --> INJROW[("injections")]
    INJ2 --> INJROW

    INJROW --> GRADE["POST /recall/grade:\nGradeRecall (scheduled)"]
    GRADE --> JUDGE["judgeRelevance:\nLLM precision judge"]
    GRADE --> PROBE["miss-probe: wide rerun,\njudge non-injected notes"]
    JUDGE --> HIST[("recall_grading\n+ recall_grade_history")]
    PROBE --> HIST
    HIST --> ROLLUP["InjectionRollup:\nSessionsWithNotesPct,\nDeadWeightPct, HotNotes"]
    ROLLUP --> DASH["/memory/effectiveness.json"]
    DASH -.->|"human reads dashboard,\nissues PATCH /config"| TUNE["config_store.go:\nrecall_floor, project_boost,\ndedup_threshold"]
    TUNE -.-> HYBRID

Node annotations

Capture hook / POST /capture

cmd/capture-hook/main.go reads the transcript JSONL by byte offset, filters tool blocks and poison text, and posts to POST /capture -> internal/memory/server.go:handleCapture -> internal/memory/capture.go: Store.Capture (L105). source is one of claude-code, codex, telegram, task, bookmark, mcp-http; kind is one of turn, document, event, synthesis (validSources/validKinds, L50-64). Pure transport: no LLM call at this stage.

dedup_key check

capture.go:105-140. A matching dedup_key returns the existing row id with duplicate=true rather than inserting again. deriveProject (L88) uses the explicit project field if given, else path.Base(cwd).

segment.go

internal/memory/segment.go, invoked from /process. Groups raw turns into episodes using a size cap (20), a time gap (30 minutes), or a topic-shift cosine boundary -- sitting between capture and distillation.

ProcessPending

internal/memory/process.go:ProcessPending (L88), self-excluded via a Postgres advisory lock (processAdvisoryLock, L67) so only one worker distills at a time. Branches on unit type: closed episodes (kind=turn, whole episode as context) vs non-turn raw items (processed per-item).

gate checks

Poison-regex, grounding (source_ref must be a verbatim span of the source), and title-length checks in distill.go/gate.go. Rejections go to gate_rejections -- never silently dropped.

DistillFailureCap / dead-letter

process.go:23 (DistillFailureCap=5). Infra vs content errors are classified separately; a unit that keeps failing is parked in distill_failures (retryable via UnparkUnit, L549) rather than retried forever inline.

embed.go

internal/memory/embed.go:embedNotesBatch. 768-dimension embeddings (EmbedDim=768), batched, one transaction per batch, called from within ProcessPending.

dedup.go: dedupOne

internal/memory/dedup.go:dedupOne (L140). Exact-duplicate pre-check (L162), then cosine kNN over live notes above dedupSimilarityFloor=0.80 (L27-36), judged by an LLM into one of four verdicts (L250-330): distinct (keep both), duplicate (tombstone the new note), refine (keep new, tombstone old, bump confidence), supersede (tombstone old unless its confidence is >= dedupFlagConfidence=0.70, L323, in which case it is flagged for human review in review.go instead of auto-applied). Every verdict is logged to dedup_log.

ContextNotes (session-start recall)

cmd/recall-hook/main.go (context mode) -> GET /recall/context -> internal/memory/context.go:ContextNotes. Deterministic: importance DESC, then valid_from DESC, own-project-first-then-global, capped at 12. inject.go:ContextInject (L45) logs an injections row with prompt_mode=false.

PromptRecall (per-turn recall)

cmd/recall-hook/main.go (prompt mode) -> internal/memory/prompt_hook.go:PromptRecall (L205). Gated in recallMemoryLines (L280): system-turn tags (<task-notification, <system-reminder) skip recall outright (L294); a substanceScore below promptSubstanceFloor=5 also skips (L308).

HybridRecall

internal/memory/hybrid.go:HybridRecall (L159), reached via POST /recall/notes -> server.go:handleRecallNotes (L323). Fuses a semantic (pgvector cosine) track and a keyword (FTS) track by reciprocal-rank fusion (rrfK=60), then applies, in order: type-prior, recency half-life, same-project boost (DefaultProjectBoost=1.25, itself floor-gated so it can reorder but never admit a sub-floor note, L285), structural co-retrieval boost (co_retrieval.go, same floor gate, L292), importance demotion (DefaultImportanceWeight=0.5, L298), an optional reranker (rerank.go), then either an adaptive-K cutoff (L318) or MMR diversification (mmrSelect, L339) for the final cap.

Client-side relevance floor

inject.go:RecallInject (L20) logs every call (even zero-hit) to injections (L31, prompt_mode=true). Back in the hook client, a second gate (PromptRelevanceFloor=0.015) drops any still-sub-floor notes (recallMemoryLines, L361-363) before they reach hookSpecificOutput.additionalContext.

GradeRecall (effectiveness)

internal/memory/recall_grade.go:GradeRecall (L124), reached only via POST /recall/grade -- not called automatically from ProcessPending or HybridRecall. For up to recallGradeBudget=10 ungraded injections: judgeRelevance (L70) LLM-judges precision of what was injected; a miss-probe (L185-227) reruns HybridRecall wide (missProbeLimit=20) and judges the notes that were not injected, to count relevant-but-dropped misses.

InjectionRollup / effectiveness.json

Results persist to recall_grading and roll into recall_grade_history (project + global trend, L259-274), surfaced via RecallGradeTrend (L296), RecallCallRates (hit rate / noise rate, L354), and audit_rollup.go:InjectionRollup (SessionsWithNotesPct, DeadWeightPct, HotNotes), feeding /memory/effectiveness.json (ARGUS-614).

The loop is not closed automatically

Nothing in the code path writes grading results back into hybrid.go's ranking weights or config_store.go's tunables (recall_floor, project_boost, dedup_threshold, read via ConfigValues/ configDefault, L46-64). Those only change via a human-issued PATCH /config after reading the dashboard -- effectiveness here is a human-in-the-loop calibration signal, not an auto-tuning loop.

Documentation drift found (and left for a human to fix)

  • docs/reference/MEMORY-PIPELINE.md states promptHookMaxNotes = 5 (constants table) and "top-5 injected"; the code (prompt_hook.go, L38) has raised this to 12 (comment cites ARGUS-37/75, 5 -> 8 -> 12). The doc's constants table is stale.
  • internal/memory/recall.go:Recall (keyword-only FTS over raw_items) is a legacy/simple path explicitly superseded by hybrid.go's notes-based hybrid recall, per its own code comment -- still reachable at GET /recall but not part of the hook-driven pipeline the docs describe.