Harvey Unified Memory — Implementation Plan

See memory-unified-design.md for the full design rationale and architecture decisions.

Phases are ordered so each one compiles, passes tests, and provides standalone value. No phase depends on a later phase being started.


Phase 1 — Config Restructure (split into 5 sub-phases)

Goal: Move RAG and KB configuration under MemoryConfig in both the Go structs and harvey.yaml. All functionality stays identical; this is a pure restructure with no behaviour change.

Each sub-phase compiles, passes all tests, and leaves Harvey fully functional. Sub-phases are designed to be completed in separate sessions.


Phase 1a — Additive: new MemoryConfig fields

Scope: config.go and config_test.go only. No call sites change. Nothing is removed.

Files to modify:

File Change
config.go Add BudgetPct float64 and RollingSummary RollingSummaryConfig to MemoryConfig. Add RollingSummaryConfig type (Enabled bool, WarnAtPct float64, KeepTurns int). Set defaults in DefaultConfig: BudgetPct: 0.25, RollingSummary: {Enabled: true, WarnAtPct: 0.80, KeepTurns: 6}. Add rolling_summary: sub-struct to memoryYAML and wire it in LoadHarveyYAML.
config_test.go Add test: DefaultConfig().Memory.BudgetPct == 0.25. Add round-trip test: marshal then unmarshal memoryYAML with rolling_summary fields; verify values survive.

Acceptance criteria: - go build ./... and go test ./... pass. - Harvey behaviour is unchanged.


Phase 1b — Mirror RAG config into MemoryConfig

Scope: config.go and config_test.go only. All existing Config.Rag* fields remain; they are kept in sync with Config.Memory.Rag* during load and save. No call sites change yet.

Files to modify:

File Change
config.go Add RagStores []RagStoreEntry, RagActive string, RagEnabled bool to MemoryConfig. Add memory.rag sub-struct to memoryYAML (reuse ragYAML type). In LoadHarveyYAML, after populating cfg.RagStores/Active/Enabled from old rag: or new memory.rag:, also copy those values into cfg.Memory.Rag*. Add ActiveRagStore(), RagStoreByName(), AddOrUpdateRagStore(), RemoveRagStore() methods to MemoryConfig (identical logic to the same methods on Config). Add SaveMemoryConfig(ws, cfg) that writes the full memory: section including memory.rag. Update SaveRAGConfig to call SaveMemoryConfig (becomes a thin wrapper).
config_test.go Add backward-compat test: parse YAML with old top-level rag: section; verify cfg.Memory.RagActive and cfg.RagActive both have the expected value. Add round-trip test for memory.rag YAML path.

Acceptance criteria: - go build ./... and go test ./... pass. - An existing workspace with old-format harvey.yaml loads without error. Running /rag status works normally. - SaveRAGConfig still writes a valid file; its output now includes memory.rag: alongside the old rag: section.


Phase 1c — Migrate RAG call sites

Scope: commands.go and harvey.go. Switch all RAG field accesses from cfg.Rag* / cfg.ActiveRagStore() etc. to cfg.Memory.Rag* / cfg.Memory.ActiveRagStore() etc. Switch SaveRAGConfigSaveMemoryConfig.

Files to modify:

File Change
commands.go Replace a.Config.RagStores, a.Config.RagActive, a.Config.RagEnabled, a.Config.ActiveRagStore(), a.Config.RagStoreByName(), a.Config.AddOrUpdateRagStore(), a.Config.RemoveRagStore() with a.Config.Memory.* equivalents throughout. Replace all SaveRAGConfig(...) calls with SaveMemoryConfig(...).
harvey.go Same substitutions for any RAG field accesses.

Acceptance criteria: - go build ./... and go test ./... pass. - All /rag subcommands work correctly end-to-end. - Old Config.Rag* fields on the Config struct still exist (not yet removed — that is Phase 1e). Harvey is not broken if any unvisited call site still uses the old path.


Phase 1d — Mirror KB into MemoryConfig and migrate call sites

Scope: config.go, commands.go, knowledge.go, knowledge_test.go. Moves KnowledgeDB and CurrentProjectID into MemoryConfig and migrates all call sites in one step (the KB footprint is much smaller than RAG’s).

Files to modify:

File Change
config.go Add KnowledgeDB string and CurrentProjectID int64 to MemoryConfig. Add knowledge_base: sub-struct to memoryYAML (db_path string, current_project string). In LoadHarveyYAML, populate both cfg.KnowledgeDB/cfg.CurrentProjectID and cfg.Memory.KnowledgeDB/cfg.Memory.CurrentProjectID from whichever YAML location is present.
commands.go Replace a.Config.KnowledgeDB and a.Config.CurrentProjectID with a.Config.Memory.* throughout.
knowledge.go Same substitutions.
knowledge_test.go Update field references.

Acceptance criteria: - go build ./... and go test ./... pass. - All /kb subcommands work correctly end-to-end.


Phase 1e — Final cleanup: remove old top-level fields

Scope: config.go and any remaining call sites. Remove Config.RagStores, Config.RagActive, Config.RagEnabled, Config.KnowledgeDB, Config.CurrentProjectID and the duplicate methods on Config. Remove the old top-level rag: write path from SaveMemoryConfig. Remove SaveRAGConfig.

Files to modify:

File Change
config.go Delete the five moved fields from Config. Delete ActiveRagStore(), RagStoreByName(), AddOrUpdateRagStore(), RemoveRagStore() from Config. Delete SaveRAGConfig. Update LoadHarveyYAML to only copy memory.rag values into Config.Memory.* (still reads old rag: for compat but no longer copies to now-absent Config.Rag*). Update SaveMemoryConfig to omit the legacy rag: top-level write.
config_test.go Remove any tests that reference the deleted Config.Rag* fields directly.

Acceptance criteria: - go build ./... and go test ./... pass with no errors. - harvey.yaml written by Harvey no longer contains a top-level rag: section; all RAG config is under memory.rag:. - Old harvey.yaml files with top-level rag: still load correctly (backward-compat read path remains).


Phase 2 — New Memory Types + Unified Retrieval + Token Budget

Goal: Add workspace_profile and project_fact types. Create UnifiedMemory. Replace injectMemoryContext with budget-aware unified injection. Add /memory recall.

Files to create

File Purpose
memory_unified.go UnifiedResult struct, UnifiedMemory struct and Recall method, token budget allocation logic, context injection formatting
memory_unified_test.go Unit tests for Recall (all silos empty, factual-only, mixed, budget truncation)

Files to modify

File Change
memory.go Add MemoryTypeWorkspaceProfile = "workspace_profile" and MemoryTypeProjectFact = "project_fact" constants. Add both to ValidMemoryTypes.
memory_store.go Add workspace_profile and project_fact to the subdirs slice in NewMemoryStore so the directories are created on startup.
harvey.go Replace injectMemoryContext body: instantiate UnifiedMemory, compute budgetTokens from OllamaContextLength * BudgetPct (fallback 512), call Recall, format and inject. Keep the function signature unchanged so no callers need updating.
commands.go Add "recall" to the cmdMemory dispatch switch. Implement cmdMemoryRecall: opens store, calls UnifiedMemory.Recall with a display budget (no cap), formats grouped output.
config.go Set BudgetPct default to 0.25 in DefaultConfig.
helptext.go Add recall to the /memory help text.

Notes

Acceptance criteria


Phase 3 — Workspace Profile Onboarding

Goal: On first use in a workspace, Harvey asks a few questions and creates workspace_profile and project_fact memory documents.

Files to create

File Purpose
memory_onboarding.go NeedsOnboarding(store *MemoryStore) bool, RunOnboarding(agent *Agent, store *MemoryStore, embedder Embedder, out io.Writer, in io.Reader) error, extractProjectFact(wsRoot string) string
memory_onboarding_test.go Unit tests for NeedsOnboarding, extractProjectFact (workspace with codemeta.json, go.mod, bare workspace)

Files to modify

File Change
harvey.go In Agent.Reset() (or at REPL startup, after the store is opened), call NeedsOnboarding; if true, call RunOnboarding before injectUnifiedContext.
commands.go Add "profile" subcommand to /memory for manual profile management: profile show (list workspace_profile docs), profile update (open most recent in $EDITOR).
helptext.go Document the profile subcommand in /memory help.

extractProjectFact logic

Checks in order; stops at first success:

  1. Parse codemeta.json in workspace root → extract name, description, programmingLanguage, developmentStatus.
  2. Parse go.mod → module name.
  3. Parse package.json → name, description.
  4. Read .git/configremote.origin.url.
  5. Return empty string (caller will ask the user).

Acceptance criteria


Phase 4 — Rolling Summary (Working Memory Compression)

Goal: When conversation history approaches the context window limit, warn and compress older turns so long sessions remain usable on small models.

Files to create

File Purpose
memory_rolling.go ShouldCompress(historyTokens, contextLen int, warnAtPct float64) bool, CompressHistory(agent *Agent, keepTurns int, out io.Writer) error
memory_rolling_test.go Unit tests for ShouldCompress; integration test stub for CompressHistory

Files to modify

File Change
harvey.go (or commands.go REPL loop) After writing each assistant reply to history, call ShouldCompress; if true, print the warning line and call CompressHistory.
config.go Set RollingSummary defaults in DefaultConfig: Enabled: true, WarnAtPct: 0.80, KeepTurns: 6. Add rolling_summary: sub-struct to memoryYAML and load it in LoadHarveyYAML.
helptext.go Document rolling summary in the /context or /compact help sections, or add a dedicated note in /help memory.

CompressHistory logic

  1. Split agent.History into older (all but last KeepTurns turns) and recent (last KeepTurns turns).
  2. Format older as plain dialogue text.
  3. Call the current model with the summariser prompt (see design doc). This call is not recorded (bypasses the session recorder).
  4. Replace agent.History with [summary message] + recent.
  5. Print [context ~N% full — compressing older turns] before the model call.

Acceptance criteria


Phase 2b — Adaptive Budget Tuning

Goal: Track per-session memory usage statistics and surface tuning suggestions in /memory status. Implemented alongside or immediately after Phase 2.

Files to modify

File Change
memory_store.go Add memory_stats table to memoriesSchema (columns: session_id, budget_tokens, injected_tokens, compressed, avg_tokens_per_sec, recorded_at). Add RecordSessionStats(sessionID string, budgetTokens, injectedTokens int, compressed bool, avgToksPerSec float64) error method to MemoryStore.
harvey.go At session end (before REPL exits), call store.RecordSessionStats(...) with values accumulated during the session. Track injectedTokens, compressed, and a running average of ChatStats.TokensPerSec as session-scoped variables.
commands.go In cmdMemoryStatus, query memory_stats for the last 10+ rows; compute budget saturation, compression frequency, and throughput trend; print the Budget advice: line (see design doc for all three signals).
memory_store.go Add BudgetStats(n int) (avgSaturation, compressionRate, avgToksPerSec float64, err error) query method.

Acceptance criteria


Phase 5 — Auto-mine on Session End

Goal: When the user exits or clears history in a session that had enough turns to be worth mining, automatically propose memory extraction without requiring a manual /memory mine run.

Trigger: Session ends (user types exit/quit//clear) AND the session has >= 10 user turns AND the session file is not already in the manifest.

Behaviour: Harvey prints a single line:

[auto-mining session for memories — use /memory mine to review manually]

Then runs the non-interactive path of Miner.Mine (accept all proposals without interactive review, score >= 0.70 threshold, skip near-duplicates). The user can review or revise afterward with /memory list and /memory forget.

Files to modify

File Change
harvey.go or REPL exit path After recording the session to disk, check turn count and manifest; call auto-mine if criteria met.
memory_miner.go Add MineAuto(ctx, sessionPath, agent, embedder, out) error that runs extraction + saves all proposals above a confidence threshold without interactive review.
commands.go Ensure /clear triggers the same check as exit (it already saves the session).

Acceptance criteria


Dependency Graph

Phase 1a (additive config fields)
    └─► Phase 1b (mirror RAG into MemoryConfig)
            └─► Phase 1c (migrate RAG call sites)
                    └─► Phase 1d (mirror + migrate KB)
                            └─► Phase 1e (remove old fields)
                                    └─► Phase 2 (Unified Retrieval)
                                            ├─► Phase 2b (Adaptive Budget Tuning)
                                            ├─► Phase 3 (Onboarding)
                                            └─► Phase 4 (Rolling Summary)
                                                    └─► Phase 5 (Auto-mine)

Phase 1 sub-phases are strictly sequential — each one must compile and pass tests before starting the next. Phases 2b, 3, 4, and 5 each depend on Phase 2 but are independent of each other and can be developed in any order after Phase 2 is complete.


New Module Dependencies

None. All implementation uses packages already imported by Harvey: database/sql, gopkg.in/yaml.v3, github.com/glebarez/go-sqlite, and the Go standard library.


Open Questions

None outstanding. Design decisions were settled during the planning session on 2026-05-26. Record any new decisions or reversals here as implementation proceeds.