# Appendix — Building a Remembering Agent # Book: Memory From First Principles # Chapter URL: https://programmer.ie/books/memory/20-chapter/ # Prose fingerprint (sha256, normalized): e6a9eabfc1c0b37874922a4b9636d20dd84d0a0bd90784d7384d457dea8a546c # Snapshot generated at build time by Hugo. This is the complete rendered prose # of the chapter; it is not a transcript of the source file. The book began with a distinction that sounds simple and becomes increasingly demanding the longer we follow it: Memory is when retained past experience changes what the system does now. A database is not memory. A vector index is not memory. A conversation transcript is not memory. Retrieval is not memory. All of those things may participate in a memory system, but none is sufficient by itself. A system begins to remember when something from its past survives, is found again, is interpreted in the right temporal and operational context, is judged appropriate to use, and changes present behaviour. The chapters of this book developed those distinctions one mechanism at a time. This appendix asks a different question: What happens when we actually put them into an agent people use? The concrete application is a plugin for a software-development agent. The implementation began with OpenCode because it provides a useful plugin surface and because a coding agent makes memory failures unusually visible. A coding agent works repeatedly inside the same repository. Decisions change. Tests fail. Migrations happen. Temporary approaches are abandoned. Architectural constraints accumulate. The same files and concepts reappear across days or weeks of work. That makes it an excellent place to test whether memory is doing anything useful. But this is not an appendix about OpenCode. OpenCode is the first host. The system should not depend conceptually on OpenCode, Claude, OpenAI, or any other company still existing five years from now. The implementation therefore separates two things that are easy to confuse: the agent that is using memory ≠ the memory system itself The host contributes observations and receives selected context. The memory engine owns remembering. If those two layers are kept separate, the same engine can sit behind a coding agent, research assistant, writing system, autonomous service, support agent, or future interface we have not invented yet. The plugin is therefore useful for two reasons. It gives us a working application. More importantly, it forces the architecture developed throughout this book to cross the boundary from experiment into software. A capstone should be smaller than the book There is an obvious way to implement a book about memory badly. Take every mechanism discussed in every chapter and put all of them into one enormous system. Add a graph. Add associative spreading. Add temporal state. Add evidence lineage. Add open loops. Add derived consequences. Add frames. Add trust. Add consolidation. Add procedural learning. Add another model to decide which of those mechanisms should run. The result would certainly look sophisticated. It would not necessarily remember better. The experiments in this book repeatedly argued against that approach. A mechanism should survive into the application because it repairs a measured failure, not because the mechanism is interesting. The plugin therefore starts with a narrower rule: Port the conclusions of the experiments, not the inventory of the book. Some mechanisms became core. Some became conditional. Some remained useful research directions. Some were beaten by simpler alternatives. The capstone should reflect that. The architecture we want is not: everything we know how to build It is closer to: strong retrieval + routing where retrieval alone is insufficient + temporal resolution where time matters + safe framing where the current objective matters + standing controls where remembered evidence may steer action + small, traceable context Every extra stage must be able to answer: What failure does this stage prevent? If the answer is unclear, the stage probably does not belong in the default path. The six questions become an API The six questions used throughout the book can now be read as increasingly demanding requirements on the plugin. Question What the implementation must provide Where did we discuss X? Strong retrieval What did we decide? Distinguish decisions and state from nearby text Why did we decide it? Provenance and evidence lineage Is it still true? Temporal state and supersession What did we leave unfinished? Open-loop interpretation when required What from the past matters right now? Routing, framing, standing and bounded context The important thing is that they do not all require the same machinery. A historical search should not be forced through the same policy as a current action. A query about a filename does not need a temporal reducer. A question about a superseded architecture probably does. An agent deciding whether to execute a remembered command requires stronger controls than an agent explaining that the command once existed. This leads to the first architectural rule of the implementation: Memory is a pipeline of separable decisions, not a single retrieval call. The product boundary During development, the experimental implementation and the plugin initially lived too close together. The research repository contained working implementations of retrieval, temporal memory, framing and the experiments that justified them. The easiest first integration was therefore: OpenCode ↓ plugin ↓ research repository ↓ PostgreSQL That was convenient for development. It was the wrong product boundary. A user installing a memory plugin should not also need to know about the repository that produced the book’s experiments. The final conceptual architecture is instead: flowchart TD H[Agent Host] --> A[Host Adapter] A --> M[Remembering Engine] M --> C[Canonical Capture] M --> R[Hybrid Retrieval] M --> RT[Recall / Influence Routing] M --> T[Temporal Resolution] M --> F[Frame Establishment] M --> S[Standing / Admission] M --> B[Bounded Context] M --> X[Context Trace] C --> DB[(PostgreSQL + pgvector)] R --> DB T --> DB F --> DB S --> DB B --> A A --> H For the concrete plugin: OpenCode ↓ opencode-remembering ↓ remembering engine ↓ PostgreSQL + pgvector For another coding agent: Claude Code ↓ Claude host adapter ↓ same remembering engine ↓ PostgreSQL + pgvector For an OpenAI-based application: OpenAI application ↓ application adapter ↓ same remembering engine ↓ PostgreSQL + pgvector Nothing important in the memory semantics should know which diagram it is in. What the host actually needs to provide The host boundary can be surprisingly small. A memory engine does not need the entire internal architecture of an agent framework. It needs observations. Conceptually, a host adapter needs operations resembling: interface MemoryHost { project(): ProjectIdentity; currentSignals(): Promise<WorkSignal[]>; onMessage(handler: (event: MessageEvent) => Promise<void>): void; onToolCall(handler: (event: ToolCallEvent) => Promise<void>): void; onToolResult(handler: (event: ToolResultEvent) => Promise<void>): void; injectContext(bundle: MemoryContext): Promise<void>; } The exact interface will differ by host. That does not matter. The important conversion is: host-specific event ↓ normalised memory signal Once the event crosses that boundary, the engine should stop caring whether it originated in OpenCode, Claude, a terminal, an HTTP service, or a scheduled job. That is what makes the design portable. Start with canonical history Before deciding what memory means, we need something more basic: What actually happened? The plugin automatically captures the ordinary events of work rather than waiting for the model to decide that something is worth remembering. That distinction matters. An explicit command such as: remember that we decided to use PostgreSQL is useful. It cannot be the foundation of memory. If memory only contains events an agent explicitly elected to preserve, the system has already thrown away the evidence needed to audit those decisions. The default path therefore captures canonical history. For a coding agent this may include: session identifier message identifier role message content model provider agent tool-call identifier tool name tool arguments tool result timestamp project identity Repository files supply another canonical source. The key rule is: Capture first. Interpret later. A session transcript can later produce a decision. A tool result can later become evidence. A failed test can later become a WorkSignal. A sequence of events can later reveal an unfinished transition. But those derived interpretations should never replace the original records. The architecture is: canonical history ↓ derived interpretations ↓ selected context not: derived summary ↓ overwrite history Raw history and derived memory have different authority This distinction becomes essential once AI begins interpreting its own history. Suppose an agent reads three sessions and derives: The project uses Redis for session storage. That derived statement may save work later. It may also be wrong. Perhaps Redis was proposed but rejected. Perhaps it was used only on a temporary branch. Perhaps PostgreSQL replaced it yesterday. The useful property of derived memory is that interpretation survives between queries. The dangerous property of derived memory is exactly the same thing. An interpretation that survives can become reusable intelligence. It can also become a reusable mistake. The plugin therefore keeps the epistemic boundary explicit: source event ↓ derived claim/state/frame ↓ source references remain attached Derived memory is rebuildable. Canonical history is not rewritten to make a derived interpretation look inevitable. Why PostgreSQL and pgvector? A prototype could store all of this in JSON files. Another prototype could use SQLite. A third could install a dedicated vector database. The implementation deliberately uses PostgreSQL plus pgvector. This is not because PostgreSQL is magically the correct memory technology. It is because one database can provide several things this system needs: ordinary relational state full-text search vector search transactionality JSON where useful indexes constraints inspectable SQL The implementation therefore avoids splitting the first production system into: document database + vector database + state database + metadata database before measurement says the separation is necessary. The memory system manages its tables automatically. A user provides: PostgreSQL a database credentials pgvector support The plugin creates its own per-project schema and structures. Conceptually: CREATE SCHEMA remembering_<project>; CREATE TABLE sources (...); CREATE TABLE chunks ( chunk_id TEXT PRIMARY KEY, source_id TEXT NOT NULL, text TEXT NOT NULL, embedding vector(1024) NOT NULL, tsv TSVECTOR NOT NULL, ... ); CREATE TABLE temporal_events (...); The important point is not the exact DDL. It is ownership. The person installing the plugin should not hand-create memory tables. Schema creation, indexes, metadata and compatibility checks belong to the application. One repository, one memory boundary Cross-project leakage is one of the easiest ways for useful memory to become dangerous memory. Suppose an agent has worked on two projects. In one: SQLite is the correct deployment choice. In another: SQLite is explicitly forbidden. Semantic retrieval may find both. Similarity does not understand organisational boundaries. The plugin therefore derives a deterministic project identity and, by default, an isolated PostgreSQL schema. Conceptually: canonical = canonical_path(project_root) project_hash = sha256(canonical) schema = "remembering_" + project_hash[:12] That is not the final philosophical answer to project identity. A future system may want multiple worktrees or machines to share one logical project identity. It is, however, a safe initial rule: Two unrelated repositories do not silently share a memory store. A schema also records the identity it belongs to. If configuration points a project at a schema owned by another project, the system fails rather than silently retrieving foreign memory. Failing closed here is more important than convenience. Retrieval earns its place first A serious memory system should begin by trying to prove itself unnecessary. That means comparing every new mechanism against strong retrieval. The plugin therefore starts with hybrid retrieval rather than a deliberately weak baseline. The retrieval pipeline is approximately: flowchart LR Q[Query] --> L[PostgreSQL FTS] Q --> E[Embedding] E --> D[pgvector Dense Search] L --> F[RRF Fusion] D --> F F --> RR[Optional Reranker] RR --> C[Candidate Set] The lexical path catches exact names, identifiers, error strings and code vocabulary. The dense path catches semantic similarity. Reciprocal-rank fusion combines the ranked lists without pretending that their scores share a meaningful scale. A simplified implementation looks like: def retrieve(query): lexical = store.lexical_search(query, lexical_k) vector = embedder.embed(query) dense = store.dense_search(vector, dense_k) fused = reciprocal_rank_fusion( lexical, dense, fusion_k=60, ) return rerank_if_configured(query, fused) The real system retains substantially more information than this snippet. Each stage goes into a trace: lexical candidates dense candidates fused candidates reranked candidates latency embedding identity configuration source ids chunk ids ranks scores A memory architecture that cannot tell whether vector search actually ran is difficult to evaluate. So the trace includes that too. Retrieval is allowed to retrieve the wrong thing That sentence is intentional. Suppose the history contains: July: Use SQLite for metadata. September: SQLite has been replaced by PostgreSQL. Now ask: Which database should I use? SQLite is semantically relevant. A good retriever may retrieve it. The retriever has not failed. The failure occurs if the system allows: relevant to silently become: valid current instruction This is one of the most important architectural separations in the plugin: Retrieval proposes evidence. Later stages decide how that evidence may be used. That is why stale candidates remain visible in traces even when they do not enter final action context. Search and remembering are different operations The plugin therefore exposes a distinction that is useful beyond coding agents. A search operation is primarily asking: What relevant evidence exists? A context operation is asking: What remembered evidence should be supplied for this purpose? Those are not the same call. Conceptually: search(query) returns broad, provenance-bearing candidates. Whereas: context( query=query, route="influence", temporal={"mode": "current"}, ) constructs a bounded working set after additional decisions. This separation becomes increasingly valuable as the policy grows. The first important fork: recall or influence? The plugin’s first genuine memory decision is not temporal. It is routing. Consider these two prompts: What database did we originally use for cache metadata? and: Which database should I use for cache metadata now? They may retrieve almost identical evidence. They should not use that evidence identically. The first is RECALL. The second is INFLUENCE. This distinction became important enough to make explicit in the system. flowchart TD Q[Current request] --> R{Route} R -->|RECALL| H[Historical interpretation] R -->|INFLUENCE| A[Present-action interpretation] H --> RET[Strong hybrid retrieval] A --> RET RET --> T[Temporal interpretation] T --> RH[Retain historically relevant evidence] T --> IA[Resolve evidence toward current applicable state] RH --> C1[Recall Context] IA --> C2[Influence Context] The crucial rule is: Recall preserves historical candidates and does not suppress superseded evidence merely because it is no longer current. Influence resolves evidence toward the applicable current state before it is allowed to steer behaviour. Recall therefore bypasses current-state suppression, not temporal interpretation itself. That last sentence matters. Recall is not atemporal. If I ask: What was true in July? time is central to the question. What recall bypasses is the rule: old therefore remove Old evidence may be exactly what historical recall needs. Make routing explicit The API therefore carries route information as data. For example: { "query": "What did we decide originally?", "route": "recall" } or: { "query": "What should we do now?", "route": "influence" } The caller can specify the route. For automatic context injection, the plugin can also derive one using a small deterministic router. The current design deliberately avoids asking another LLM: Is this recall or influence? before every memory request. A routing result is more useful when it is inspectable: { "route": "influence", "source": "deterministic", "reason": "present-action request", "ambiguous": false } Ambiguity is not hidden. The router may internally distinguish: RECALL INFLUENCE AMBIGUOUS If an automatically injected context is uncertain, conservative policy can be applied without pretending that the classifier was certain. This is a recurring design pattern throughout the system: Uncertainty should survive into the control plane. Then time becomes unavoidable Once recall and influence are separated, the next failure becomes much easier to see. Imagine the retrieved history is: July 1 SQLite becomes the metadata database. August 1 The team decides to move to PostgreSQL. September 1 PostgreSQL becomes the active metadata database. A single timestamp field cannot adequately represent this. The decision existed in August. The operational state did not change until September. Now add a fourth fact: October 1 The system learns that an incident actually occurred on August 15. We now need to distinguish at least three notions of time: when something happened when a state became valid when the memory system learned it The temporal event model therefore carries separate fields. A simplified envelope looks like: @dataclass(frozen=True) class EventEnvelope: event_id: str source_id: str source_seq: int event_type: str subject: str state_key: str value: str event_time: str recorded_at: str effective_from: str | None effective_to: str | None causal_parents: tuple[str, ...] supersedes: tuple[str, ...] corrects: tuple[str, ...] evidence_refs: tuple[str, ...] This is intentionally more elaborate than: updated_at because the system needs to answer different questions. Four temporal questions The API exposes four useful standpoints. Current What is the applicable state now? Valid at Given everything we know now, what was valid on July 20? As known What did we believe on August 1? Bitemporal Given what we knew on August 1, what did we believe was valid on July 20? Those can produce different correct answers. That is not inconsistency. It is the point of keeping the time axes separate. A context request can therefore look like: { "query": "What database were we using then?", "route": "recall", "temporal": { "mode": "bitemporal", "valid_at": "2026-07-20T00:00:00Z", "known_at": "2026-08-01T00:00:00Z" } } A present-action request normally uses: { "route": "influence", "temporal": { "mode": "current" } } A decision is not necessarily a state transition The distinction becomes concrete in code. Suppose PostgreSQL is chosen on August 1 but cutover is scheduled for September. The event may be: { "event_type": "DECISION_MADE", "subject": "cache.metadata.database", "value": "PostgreSQL", "event_time": "2026-08-01T10:00:00Z", "effective_from": "2026-09-01T00:00:00Z" } Before September, the reducer may represent: current = SQLite planned = PostgreSQL That is much more useful than pretending the decision instantly changed reality. Later: { "event_type": "STATE_CHANGED", "subject": "cache.metadata.database", "value": "PostgreSQL", "event_time": "2026-09-01T00:00:00Z", "supersedes": ["cache-db-sqlite"] } moves the effective state. The distinction matters whenever systems have: decisions deployments migrations approvals feature flags scheduled changes which is to say, nearly every real project. Supersession without deletion Suppose: A: SQLite is current. B: PostgreSQL supersedes A. The wrong implementation is: DELETE A A is still part of history. It may be exactly the evidence needed to answer: Why did the first version use SQLite? So the temporal system is append-oriented. The history becomes: A ────────► superseded by ────────► B The influence path can suppress A as present guidance. The recall path can retain it. That difference is visible in the trace. For example: { "candidate": "sqlite-chunk", "retrieved": true, "temporal_status": "superseded", "selected": false, "reason": "temporal.superseded_as_current", "superseded_by": ["cache-db-postgres"] } For historical recall, the same candidate may instead become: { "candidate": "sqlite-chunk", "temporal_status": "superseded", "selected": true, "reason": "temporal.historical_retained" } The evidence has not changed. Its legitimate use has. Late knowledge is different from changed history One particularly useful test involves knowledge that arrives late. Imagine: July 15: PostgreSQL actually became active. August 15: the memory system learns this. Ask: valid_at = July 20 known_at = August 1 The answer cannot use the PostgreSQL event. The event had happened, but the system did not know it yet. Now ask: valid_at = July 20 known_at = August 20 The answer may use it. This matters for audit questions: What should the agent have known when it made that decision? That cannot be answered accurately by reading today’s final database state. Corrections append Corrections use the same principle. Suppose the original record said: PostgreSQL 15 and later evidence establishes: PostgreSQL 16 The corrected state should not require erasing the first record. Instead: original event ↓ correction event The current interpretation uses the correction. Historical inspection can still show the original belief. The system can therefore answer both: What did we think at the time? and: What do we now believe was true? without falsifying its own history. Not every chunk receives temporal meaning This is an important restraint. A memory system that contains a temporal model may be tempted to classify every retrieved passage as: current stale superseded even when there is no evidence for that classification. The plugin does not do that. Ordinary project text without a structured temporal relationship can remain: not_temporally_modelled That evidence is not automatically current. It is not automatically stale. And it is not removed merely because the temporal engine cannot interpret it. This gives us another useful rule: Suppression requires evidence. Lack of temporal knowledge is not proof of obsolescence. The next problem is not time At this point we have: canonical history hybrid retrieval recall/influence routing temporal interpretation That still does not answer the sixth question: What from the past matters right now? Consider the same query in two situations: Review this architecture before implementation. and: Review this chapter before publication. The phrase: review this may be nearly identical. The relevant history is not. The first task wants architecture decisions, failed experiments, system constraints and implementation evidence. The second wants publication state, prose issues, citations and reader-facing consistency. The memory store should not need to stamp each historical record with one permanent: priority = high because priority depends on what we are doing. That is where frames enter. ProjectFrame and WorkFrame The design separates durable project policy from current work. A ProjectFrame describes relatively stable project-level context: project identity purpose standing objectives standing constraints known work types, with the terms that match each one evidence preferences per work type allowed project scope opt-in for hard exclusion of non-preferred evidence A WorkFrame describes the work being done now: current objective work type active constraints as-of time signals supporting the frame how strongly the frame is established The distinction is deliberate. The current prompt does not belong in the ProjectFrame. And a project-wide rule does not need to be copied into every WorkFrame. ProjectFrame is declared configuration, read from .remembering/project-frame.json — never inferred, never the current request. A malformed file disables framing while leaving baseline memory untouched; a frame that claims a different project’s identity fails closed rather than silently borrowing that project’s policy. Conceptually: @dataclass(frozen=True) class ProjectFrame: project_id: str version: str purpose: str objectives: tuple[str, ...] constraints: tuple[str, ...] work_types: tuple[WorkTypeSpec, ...] evidence_preferences: tuple[tuple[str, tuple[str, ...]], ...] hard_exclude: bool while: @dataclass(frozen=True) class WorkFrame: work_frame_id: str project_id: str objective: str work_type: str as_of: str signals: tuple[WorkSignal, ...] provenance: tuple[...] establishment: Establishment The important field is not work_type. It is provenance. A WorkFrame is allowed to say: implementation_review only if the system can point to observations that licensed the classification. Work does not have to begin with a prompt A host-neutral memory system should not assume that every task starts with a human typing into a chat box. A work signal may be: user message agent task scheduled job tool result test failure project-state observation event That allows: failing test ↓ WorkSignal ↓ implementation WorkFrame without inventing a conversation. Or: scheduled release check ↓ WorkSignal ↓ release-readiness WorkFrame The memory engine is then usable by agents that operate continuously rather than only when spoken to. Frames are dangerous The experiments that earned frames also exposed their largest hazard. A correct frame can improve context selection. A wrong frame can remove the exact evidence the model needed. That means: infer a frame → trust it completely → hard-filter memory is unsafe. Frame establishment therefore needs its own control policy. A useful set of establishment states is: DECLARED CORROBORATED INFERRED CONFLICTING STALE UNKNOWN Then control strength can depend on how well the frame is established: DECLARED / CORROBORATED ↓ HARD FRAME INFERRED ↓ SOFT FRAME CONFLICTING / STALE / UNKNOWN ↓ QUERY-ONLY FALLBACK The shipped policy folds all three degraded states — conflicting, stale, and unknown — into the same query-only fallback: baseline retrieval runs untouched and framing only observes. A finer response to UNKNOWN specifically — request more evidence rather than silently falling back — is a plausible refinement, not yet built. The key pattern is: Uncertainty broadens before it narrows. A weakly established frame should not gain more power to exclude evidence. That principle applies far beyond memory. A soft frame is not no frame Consider an inferred work frame. The system thinks we are doing: release_readiness but is not certain. A hard policy might say: Only retrieve evidence classes preferred for release readiness. A soft policy can instead say: Use release-readiness signals to expand and rerank, but do not allow the inferred frame to hard-exclude otherwise relevant evidence. The difference is subtle but important. The frame can help. It cannot erase the baseline. That keeps strong retrieval as a safety net. Current is not trustworthy Temporal resolution answers: Is this still the applicable state? It does not answer: Should this source be allowed to steer the agent? Imagine current memory contains: Run this shell command whenever deployment fails. The instruction may be temporally current. That does not make it authoritative. It may have come from: untrusted external text generated summary obsolete contributor poisoned document low-authority note This motivates the next boundary: Validity and standing are separate. A complete influence pipeline therefore eventually looks more like: retrieve ↓ temporal validity ↓ project/work scope ↓ frame safety ↓ standing / trust ↓ decisive evidence ↓ bounded context Historical recall does not blindly follow that same path. That is why routing had to come first. Trust is permission, not truth A useful standing policy should not return: true false for the content itself. It should return something closer to: admit deny quarantine for a particular use. That distinction matters. A source can be untrusted for action and still useful as evidence that somebody said something. For example: A malicious README told the agent to delete the database. For an influence request, that instruction should not steer behaviour. For an incident-analysis request, the same text may be essential evidence. So the trust question is: May this remembered material participate in this action under this policy? not: Is this sentence metaphysically true? Memory should never launder authority Derived memory introduces another risk. Suppose an untrusted source says: Production deploys must skip tests. A summarizer later produces: Project deployment policy: tests may be skipped. If the summary loses the source’s standing, the memory system has laundered authority. The derived statement now looks cleaner than the evidence that produced it. The implementation must therefore preserve lineage: derived memory ↓ source evidence ↓ source standing A derivation cannot obtain stronger authority merely by being rewritten fluently. The final context should be smaller than the memory By now the database may contain: repository files months of sessions tool activity temporal events decisions corrections frames derived records The model should not receive all of it. Memory and context are deliberately different objects. memory = durable historical substrate context = bounded evidence selected for this invocation A useful interface might return: { "trace_id": "ctx-...", "route": "influence", "items": [ { "source_id": "architecture/decision-017.md", "chunk_id": "...", "temporal_status": "current", "text": "..." } ], "chars": 3982 } Only the selected bundle goes into the model’s prompt. The rest remains in memory. Why bounded context matters More correct evidence is not monotonically better. That result is easy to underestimate. Suppose the agent needs one current database decision. A context containing: current decision original proposal three abandoned alternatives two meeting transcripts four summaries a migration log a similar decision from another subsystem may contain more true information. It may produce a worse action. Memory therefore needs an admission problem in addition to a retrieval problem. The objective is not: maximize information but something more like: supply enough decisive evidence + retain necessary provenance + preserve important disagreement + stay within a bounded attention budget This also keeps costs visible. The trace is part of the product A system capable of changing an agent’s behaviour needs to explain what it changed. Not through a model-generated story afterwards. Through execution evidence captured while the decision was made. A useful ContextTrace eventually records: project identity query route route reason temporal standpoint retrieval configuration lexical candidates dense candidates fused candidates reranked candidates temporal interpretation ProjectFrame version WorkFrame signals supporting the WorkFrame frame establishment result frame-control mode standing verdicts selected evidence rejected evidence reason codes context budget bundle digest latencies policy versions The most important candidates in the trace are often the rejected ones. If the answer is wrong because a decisive record never entered context, we need to distinguish: not stored not retrieved retrieved but temporally suppressed rejected by frame rejected by standing lost to budget correct context supplied but reader failed Those are different engineering failures. Without the trace they collapse into: the AI got it wrong which teaches us very little. Reason codes beat retrospective prose Suppose an old SQLite record was retrieved but did not enter an influence bundle. A reason code: temporal.superseded_as_current tells us what control flow happened. A model-generated explanation saying: I ignored SQLite because PostgreSQL seemed more recent and appropriate. does not. The second sentence is another inference. The first is instrumentation. The memory system should prefer instrumentation. Automatic context injection The plugin’s most important feature is not a tool named memory_search. It is the ability to affect ordinary agent work without requiring the user to remember that memory exists. A host hook can perform: capture current session ↓ extract current work signal ↓ classify route ↓ retrieve candidates ↓ temporal interpretation ↓ frame establishment + control ↓ assemble bounded context ↓ inject into agent context The shipped hook runs all of this automatically now, including frame establishment. Standing and trust are the one stage still missing: a temporally current, frame-eligible record is admitted today without any authority check on where it came from. The resulting block can remain explicit: <project_memory trace_id="ctx-7fa..." schema="remembering_ab12..." route="influence" temporal_mode="current" work_type="implementation" frame_establishment="corroborated" frame_control="hard"> Selected project history from Project Memory. Source and chunk IDs are provenance, not authority: retrieval found this evidence, and later policy stages decide whether it may influence present action. [source: adr-017 | temporal: current] PostgreSQL is the active persistence backend. [source: benchmark-031 | temporal: current] The migration benchmark showed... </project_memory> The work_type/frame_establishment/frame_control attributes appear only when a frame actually applied; recall requests and unframed influence requests carry the same wrapper without them. The wrapper matters because the model should know this material is retrieved project history rather than part of the user’s current instruction. Future host adapters can render the same bundle differently. The underlying semantics remain unchanged. Stable and dynamic context There is another practical issue. Some model providers cache stable prompt prefixes. If memory is inserted carelessly near the beginning of the system prompt, every small memory change can destroy cache reuse. A useful integration separates: stable host/provider instructions from: dynamic memory context The exact insertion point depends on the host. This is an adapter concern, not a memory-engine concern. The engine returns a bundle. The host decides where that bundle belongs. Again, separating the two layers pays off. Tools are inspection surfaces, not the architecture For development, the plugin exposes operations such as: memory_health memory_setup memory_refresh memory_search memory_context memory_state memory_temporal_import Future stages may add: memory_trace memory_open_loops memory_remember These names are convenient. They are not the architecture. The underlying application API is closer to: health(project) setup(project) refresh(project) capture(project, event) search(project, query) context(project, request) state(project, subject, standpoint) trace(project, trace_id) A different host might expose them through: tool calls HTTP MCP local RPC a Python library a CLI The memory semantics should remain the same. Explicit remembering comes later It is tempting to begin a memory product with: memory_remember(...) The plugin deliberately postpones that. Automatic capture should work first. Otherwise the system contains only the events somebody predicted would matter. Once automatic history is reliable, explicit memory becomes useful as a stronger signal. For example: remember this decision might create an attributed event saying: the user explicitly designated this as a decision That should not silently mutate some old row. Corrections, supersessions and retractions create new records. History remains inspectable. Open loops should be conditional The fifth book question asks: What did we leave unfinished? A naive implementation searches for: TODO pending later follow up That produces mentions, not unfinished work. A stronger representation is: expected transition ↓ has a satisfying / cancelling / superseding transition been observed? For example: migration started ↓ expected: all callers migrated ↓ no valid completion observed ↓ open But this mechanism should not necessarily run on every request. The plugin can expose it when the task requires unfinished-work reasoning. This is another example of the capstone using mechanisms conditionally instead of turning the architecture into a permanent maximal stack. The system should be able to say “I don’t know” Several stages introduce uncertainty: ambiguous route unknown temporal state incomplete event history weakly inferred WorkFrame conflicting frame signals missing provenance standing conflict A bad memory system hides these behind confident context. A better one preserves them. For low-risk work, uncertainty may broaden retrieval. For consequential work, uncertainty may justify requesting more evidence or abstaining from memory-driven action. The important principle is: Uncertainty is an input to policy, not an embarrassment to remove from the trace. Building the plugin in stages The implementation followed the same experimental discipline as the book. It did not begin with the final pipeline. Each stage had to work before the next control was introduced. Stage 1 — make retrieval real The first production slice established: PostgreSQL pgvector per-project schemas automatic initialization repository ingestion session capture Ollama bge-m3 embeddings full-text retrieval dense retrieval RRF bounded context The question was simply: Can this plugin preserve and retrieve real project evidence? Not: Can we already reproduce the entire book? That smaller question gave us a reliable substrate. Stage 2 — separate recall from influence Once strong retrieval worked, routing was added. The contract fixtures tested: 7 clear recall cases 7 clear influence cases 5 ambiguous cases plus two explicit route overrides. The frozen accounting was: 19 fixtures + 2 override checks = 21 checks all passing at that development checkpoint. The important output was not the count. It was the architectural boundary those tests froze. Stage 3 — add temporal state Temporal state was then introduced without changing retrieval or routing. The controlled contract covered: current state historical valid time known-at queries bitemporal queries planned vs effective state supersession correction out-of-order arrival incomplete history unmodelled evidence A particularly useful live fixture contained: SQLite ↓ superseded by ↓ PostgreSQL Strong retrieval continued finding both. That was intentional. The difference appeared later. For recall: SQLite retained PostgreSQL retained where relevant For influence: SQLite retrieved SQLite marked superseded SQLite excluded as current guidance PostgreSQL selected The temporal mechanism therefore repaired the specific failure it claimed to repair without weakening historical recall. That is what it means for a layer to earn its complexity. Stage 4 — safe project and work framing Framing came next, and it changes the shape of automatic injection, not just an explicit tool call. A ProjectFrame is declared configuration in .remembering/project-frame.json, never inferred, never the current request. A WorkFrame is built from a handful of current WorkSignals — the latest user message, the active agent task, the newest tool result, a test failure — never the whole history. Establishment stays deterministic, with no scalar confidence score standing in for it: DECLARED one decisive signal matches exactly one declared work type CORROBORATED two independent kinds of signal agree on the same type INFERRED only a weak, non-decisive signal matches CONFLICTING decisive signals point at two work types at once STALE a new decisive signal contradicts the prior established type UNKNOWN nothing observable matches a declared work type Even inside a hard frame, a non-preferred evidence class is excluded only if the ProjectFrame explicitly opts in; otherwise the frame reorders candidates but every one of them still survives. That asymmetry is the point: a frame the system is confident about may still be the wrong reason to make evidence disappear. The result travels into the same automatic hook that Stage 1 built, not into a separate call: every injected bundle now extracts current WorkSignals, establishes a WorkFrame from them, and carries work type, establishment, and control mode into the wrapper the model sees. Recall bypasses frame control entirely, the same way it already bypassed temporal current-state suppression. Stage 4 is a contract, not yet a behavioral result. Eight fixture categories pass deterministically — declared, corroborated, inferred, conflicting, stale, unknown, soft retention, and explicit override. Whether framing measurably improves outcomes under the book’s own intervention standard, rather than merely behaving as specified, is still open. That is Stage 10. What the second edition does not add to the plugin The promotion boundary investigated in Chapter 18 is deliberately absent from every stage above, and its absence is the design decision rather than an oversight. It would be easy to add propose_promotion, approve_promotion, and supersede to this API and to invent a memory/decisions/ or memory/policies/ tree beside the existing folders. None of that is here, for three reasons that are worth recording so a later stage does not reinvent them. First, no mechanism enters this appendix before a failure has demonstrated it needs to. Chapter 18’s mechanism is EXPERIMENTAL / DEFERRED, so it does not earn a stage, and the same rule that kept Stage 5 out is what keeps promotion out. Second, the interesting question is not where the file lives but who signs it. Chapter 18’s central finding is that promotion changes authority rather than representation, and a plugin that can write its own durable rules has handed itself the authority the book spent Chapter 17 restricting. An API that promotes is a trust decision wearing an interface. Third, the likely destination is not this subsystem. Ordinary project documentation is where a decision belongs if it is going to be reviewed by people who will maintain it, versioned alongside the code it describes, and read without running an agent. A plugin that invents a parallel agent-only filing system would reproduce the split this book argues against. So the boundary is recorded as a question rather than an endpoint: if promotion is ever earned, the first question is whether the promoted knowledge should leave the memory subsystem entirely and enter the project’s ordinary documentation — and only the questions after that concern an API. As of this writing, that is where the shipped plugin stops. Retrieval, routing, temporal resolution, and safe framing are implemented and covered by the fixtures above. Standing/trust, the full context trace, open-loop handling, explicit remembering, and the evaluation harness described in the rest of this appendix are the target architecture the plugin is being built toward — later stages, not yet-shipped ones, held to the same discipline: no stage is added before a preceding one has earned its place. A development checkpoint is not the product architecture The first implementation reused code directly from the research repository. That allowed the ideas to be validated quickly. It also exposed a packaging mistake. The application should not require: clone the book's experiment repository as an installation step. The production extraction therefore changes: plugin → research implementation into: plugin → bundled remembering engine while keeping the measured mechanisms unchanged. This is an important engineering lesson in its own right. Research architecture and product architecture do not have to be identical. The experiments tell us which behavior to preserve. Productization decides where that behavior should live. What belongs in the standalone engine? The plugin should carry only the runtime components it actually needs. Something approximately like: opencode-remembering/ │ ├── src/ │ ├── plugin.ts │ ├── client.ts │ ├── context.ts │ ├── config.ts │ └── ... │ ├── bridge/ │ └── remembering_bridge.py │ └── engine/ └── remembering/ ├── baseline/ │ ├── storage.py │ ├── ingest.py │ ├── embeddings.py │ ├── retrieval.py │ ├── routing.py │ ├── temporal.py │ └── ... │ ├── temporal/ │ ├── model.py │ ├── log.py │ ├── ordering.py │ ├── reducer.py │ ├── query.py │ └── postgres.py │ └── frame/ ├── model.py ├── project.py ├── establish.py ├── policy.py ├── signals.py └── evaluation.py It should not carry: book notebooks chapter fixtures frozen experiment reports unused GraphRAG experiments every research mechanism explored during the book The product inherits conclusions. It does not need to embed the laboratory. Why keep a Python engine? The OpenCode adapter is TypeScript. That does not require the entire memory engine to become TypeScript immediately. Rewriting already-tested memory semantics into another language while simultaneously changing packaging would change two variables: implementation language + deployment architecture If something broke, attribution would become harder. So the conservative productization step is: keep the verified engine semantics move them into the plugin remove the external runtime dependency That removal is enforced, not just documented. If a configuration still points at the old research-repository checkout, the plugin refuses to start rather than silently falling back to it: the dependency was not merely discouraged, it no longer exists as a code path. A later TypeScript rewrite can be evaluated separately if distribution cost justifies it. This is the same discipline used everywhere else in the book: Change one thing at a time when you want to know why the result changed. The bridge is not a weakness The implementation currently uses a small process boundary between TypeScript and Python. Conceptually: TypeScript host adapter ↓ JSON Python memory engine ↓ SQL PostgreSQL That boundary is deliberately narrow. For example: { "action": "context", "project": "...", "query": "...", "route": "influence", "temporal": { "mode": "current" } } and a structured response returns: { "trace_id": "...", "route": "influence", "items": [], "trace": {} } The same boundary could later become: stdio HTTP MCP embedded library without changing the memory model. That is exactly the kind of seam we want. A portable remembering protocol Once host concerns are removed, the engine’s conceptual contract becomes surprisingly general. A host needs to be able to submit: Project identity { "project_id": "...", "root": "..." } Canonical events { "event_id": "...", "kind": "tool_result", "content": "...", "observed_at": "..." } Search requests { "query": "..." } Context requests { "query": "...", "route": "recall | influence | auto", "temporal": {} } Work signals { "signal_id": "...", "kind": "user_message", "text": "...", "observed_at": "..." } And receive: retrieval candidates state resolutions context bundles traces That is not an OpenCode protocol. It is a remembering protocol. Applying the same engine outside coding The coding-agent example is useful because the history is concrete, but almost nothing in the architecture requires code. Consider a research assistant. Canonical history may contain: papers notes experiments analysis sessions review comments Temporal state can represent: hypothesis revised result superseded experiment rerun paper corrected Frames can distinguish: literature review experiment design result analysis paper drafting Influence policy can prevent an old rejected hypothesis from silently steering current analysis while retaining it for historical explanation. The same memory system applies. A writing system For a long-running writing project: canonical history = drafts editorial decisions source notes research review sessions Temporal events may record: chapter reordered example replaced claim corrected publication status changed WorkFrames may include: structural edit copy edit reference audit continuity review publication preparation The question: Why did we remove that example? is recall. The question: Should this example still appear in the final manuscript? is influence. Again, the underlying distinction survives the domain change. A support agent For support: canonical history = tickets customer messages product changes resolutions Temporal memory becomes particularly important. An old support answer can be semantically perfect and operationally wrong because the product changed. Recall may need to answer: What were customers told before version 4? Influence must answer: What should this customer be told now? That is the same SQLite/PostgreSQL problem in a different costume. A personal assistant For a personal agent, the sensitivity becomes higher. History may contain: preferences plans appointments documents messages decisions A preference such as: I am avoiding morning meetings this month. has temporal scope. A remembered instruction may no longer be current. Different sources may deserve different standing. And project or domain isolation becomes privacy isolation. The mechanisms therefore generalise, but their policy needs to become stricter rather than looser. The adapter should stay boring This is worth stating explicitly. The OpenCode-specific layer should mostly do things like: listen to OpenCode event normalise it send it to memory receive memory bundle render it into OpenCode context If we discover complex temporal reasoning inside: src/plugin.ts the boundary has failed. Likewise, an OpenAI adapter should not independently reimplement supersession. A Claude adapter should not invent its own route classifier. The engine owns semantics. Adapters own translation. Memory health is a first-class operation A system that silently loses its memory substrate is dangerous. memory_health therefore needs to answer more than: database connected: yes A useful health report includes: engine version PostgreSQL reachable pgvector available pg_trgm available project schema identity schema initialized embedding provider reachable embedding model available embedding dimension compatible FTS index present HNSW index present source count chunk count temporal store ready temporal event count temporal schema version temporal reducer version routing version sequence gaps unknown temporal references causality violations A healthy system can still have: 0 temporal events That is not an error. It means: the temporal subsystem is ready but this project has no structured temporal state yet Health should describe reality rather than collapse everything into green or red. Failure should be classified A missing database is not the same problem as a missing embedding model. Useful errors distinguish cases such as: DB_UNREACHABLE DB_MISSING VECTOR_EXTENSION_MISSING EMBEDDING_MODEL_MISSING SCHEMA_MISMATCH CONFIG_INVALID TEMPORAL_BAD_TIMESTAMP TEMPORAL_DUPLICATE_CONFLICT The exact names are implementation detail. The principle is not. A memory system is already an observability problem. Generic exceptions make it harder to tell whether memory failed at: capture storage indexing retrieval routing state interpretation policy context assembly which is exactly the ambiguity this book has tried to remove. How do we know the plugin remembers? A demonstration is not enough. Suppose the plugin retrieves: PostgreSQL for: Which database should we use? That is encouraging. It does not prove memory helped. The model might already have answered PostgreSQL. The query may have contained the answer. The retrieved evidence may have been irrelevant to the final action. The evaluation must therefore preserve the intervention logic used throughout the book. Hold constant: task model repository prompt reader and change the memory condition. For example: Condition Memory available N0 No memory N1 Strong hybrid RAG N2 Routed hybrid RAG N3 Routed + temporal N4 Routed + temporal + framing N5 Final standing/context policy Then measure: task success current-state correctness historical-state correctness stale-memory harm evidence correctness action differences context tokens latency The question is not: Did retrieval look impressive? It is: Did preserved experience cause a better present action? That returns us to the definition at the beginning of the book. The strongest test is a harmful memory A useful memory benchmark should contain cases where memory makes the answer worse. Otherwise the experiment mostly measures: memory present versus memory absent without testing admission. The SQLite/PostgreSQL fixture is useful precisely because the old memory is relevant and wrong for the current action. Consider: History: A: Use SQLite. B: PostgreSQL supersedes SQLite. Question: Which database should I use now? Strong RAG may retrieve both. A weak system may feed both to the model and hope. The temporal system must instead produce: SQLite: retrieved = true selected = false reason = superseded_as_current PostgreSQL: retrieved = true selected = true Now change the question: What database did we originally use? The correct behavior flips: SQLite: selected = true The same memory system has changed behavior because the task changed. That is much closer to remembering than merely returning similar text. What should remain out of the default path? The plugin should also inherit the book’s negative results. It should not automatically add a mechanism because there is an implementation available. For example, a future version may include persistent graph interpretation where repeated relational questions justify it. That does not mean every project needs GraphRAG. Associative spreading may help certain sparse retrieval problems. That does not make it the default search mode. Open-loop materialization may reduce query cost. That does not automatically make materialized open-loop state more authoritative than history. Consolidation may help long-running systems. That does not mean summaries should replace canonical evidence. The production architecture should preserve an important property: Optional mechanisms can be added without changing the meaning of the canonical layers beneath them. The final pipeline After the book’s results are translated into application boundaries, the full influence path becomes approximately: flowchart TD A[Canonical history] --> B[Hybrid retrieval] B --> C[Candidate evidence] C --> D[Task route] D -->|Recall| E[Temporal interpretation] E --> F[Historical bundle] D -->|Influence| G[Temporal current-state resolution] G --> H[Project / Work frame] H --> I[Frame establishment] I --> J[Standing / trust] J --> K[Decisive evidence + provenance] K --> L[Bounded context] L --> M[Agent action] C --> X[Context Trace] D --> X G --> X I --> X J --> X K --> X The recall path remains intentionally different. It should not inherit every action gate merely because those gates exist. This is one of the most important lessons of the entire implementation. What happens when a new host arrives? Imagine tomorrow we decide to support another agent. The wrong approach is: port the entire memory implementation The right task should be much smaller. Implement: HostAdapter for that environment. Map its events into: CanonicalEvent WorkSignal ProjectIdentity Call the same engine. Render its ContextBundle back into the host. A hypothetical adapter might be: class ClaudeAdapter implements MemoryHost { async project() { // map Claude environment to ProjectIdentity } async currentSignals() { // convert current request/tool state to WorkSignal[] } async injectContext(bundle: MemoryContext) { // insert host-appropriate context } } An OpenAI application may not even use a plugin API. It might call the memory service directly: bundle = memory.context( project=project, query=request.text, route="auto", ) response = client.responses.create( model=model, input=[ system_instructions, bundle.render(), request.text, ], ) The important part is that: memory.context(...) means the same thing in both systems. A memory engine is not a model feature This separation also protects the architecture from product churn. A provider may introduce: larger context windows native memory prompt caching agent state conversation storage Those features may become excellent implementation components. They do not remove the architectural questions. We still need to know: Which past evidence belongs to this project? Is the user asking about history or asking for action? Was this state superseded? What did we know at the time? Does this remembered instruction have standing? Why was this evidence selected? Why was that evidence rejected? A native memory API may answer some of those questions. It may not answer all of them. The design therefore treats provider capabilities as interchangeable mechanisms under a higher-level memory contract. If the provider disappears This is the practical version of the same argument. If the entire memory architecture is: call Vendor X's memory feature then the application’s memory semantics belong to Vendor X. If instead the architecture is: canonical events + explicit retrieval + route semantics + temporal semantics + frame semantics + standing policy + trace then a provider can be replaced. The next model may be better. The next host may expose a different API. The memory system still knows what: recall influence superseded current WorkFrame provenance admission mean. That semantic independence is more valuable than API independence. What OpenCode contributes OpenCode is still a useful first implementation host. It provides: a concrete project directory a session lifecycle messages tool calls tool results context hooks tool registration That lets the appendix move from architecture diagrams into a real application. It also gives us unusually good observability. A coding task has artifacts we can inspect afterwards: files changed tests run commands issued commit diff build result That makes behavioral memory easier to evaluate than in a vague conversational benchmark. Did memory cause the agent to edit the correct file? Did it prevent use of the superseded database? Did it remember the current architectural constraint? Did it avoid a previously failed approach? Those are concrete outcomes. But OpenCode is not the thesis If this appendix succeeds, a reader should be able to finish it and build: opencode-remembering but should also be able to build: research-assistant-memory writing-agent-memory customer-support-memory personal-project-memory without copying the chapter mechanically. The reusable mechanism is: capture durable evidence ↓ retrieve broadly ↓ understand the purpose of retrieval ↓ interpret state where necessary ↓ establish the current work frame ↓ control what may influence action ↓ supply bounded provenance-bearing context ↓ measure whether behavior improved That is the capstone. The plugin is one implementation. The code we deliberately did not include here This appendix has shown small pieces of code where a boundary becomes concrete. It has not reproduced the repository. That is intentional. Hundreds of lines implementing PostgreSQL error classification do not explain memory. Neither do all of the DTO declarations, test helpers, OpenCode hook signatures, or SQL index definitions. The code that matters pedagogically is the code that reveals a concept: a canonical event a retrieval pipeline a route a temporal event a standpoint a WorkSignal a ContextBundle a reason code The full implementation can change. Those boundaries should survive it. A useful implementation discipline The plugin development process can be summarised as a loop: QUESTION ↓ NAIVE BASELINE ↓ MEASURE ↓ DIAGNOSE THE FAILURE ↓ ADD THE SMALLEST MECHANISM THAT TARGETS IT ↓ VALIDATE AGAIN For this plugin: Can we find project history? → build strong hybrid retrieval. Why does stale history steer current actions? → distinguish recall from influence. Why does influence still use obsolete evidence? → add temporal resolution. Why does relevant evidence vary by current work? → add framing. Why can current relevant memory still be unsafe? → add standing/trust. Why is context still too large or ambiguous? → select decisive evidence with provenance. Each mechanism enters because a preceding system failed in a particular way. That is more important than any specific implementation language. Tests are architectural documentation The most durable description of the memory system may eventually be its acceptance tests. For example: Given: SQLite was once current. PostgreSQL explicitly superseded it. When: the route is recall and the question asks what was used originally Then: SQLite remains available. And: Given: the same history When: the route is influence and the question asks what should be used now Then: PostgreSQL is selected. SQLite remains in the trace. SQLite is suppressed as current guidance. Those tests say more about the architecture than: This project implements advanced AI memory. A test freezes a behavioral contract. Marketing freezes nothing. The same is true for frames A useful framing test does not merely assert: frame.work_type == "implementation_review" It should also test the dangerous case: Given: the system infers the wrong frame Then: the inferred frame may assist retrieval but may not hard-delete otherwise relevant baseline evidence. That test embodies the lesson. And for trust Likewise: Given: a current retrieved instruction whose provenance lacks standing When: the task is present-action influence Then: the instruction does not steer behavior. But: the record remains available for historical or forensic recall. That is the architecture expressed as behavior. Memory is a controlled path from past to action After all the mechanisms are stripped of their implementation names, this is what remains. A remembering agent has a path: PAST ↓ preserve ↓ retrieve ↓ interpret ↓ resolve ↓ admit ↓ select ↓ PRESENT ACTION Every arrow is a place where the system can fail. It can fail to preserve an event. It can preserve it but fail to retrieve it. It can retrieve it but misunderstand its temporal state. It can understand the state but apply the wrong work frame. It can identify the right evidence but give authority to the wrong source. It can make every decision correctly and still exceed the model’s useful context budget. Or it can supply perfect context and the model can still make the wrong decision. Those are different failures. The purpose of the architecture is to make them different in the implementation too. The capstone’s real output At first glance the output of this project is: an OpenCode plugin That is not quite right. The useful output is a working demonstration of a more general proposition: Long-lived AI systems need a memory boundary between retained history and present behaviour. Retrieval sits inside that boundary. It does not define it. Temporal reasoning sits inside it. It does not define it. Frames and trust sit inside it. They do not define it. The boundary is the controlled transformation: what happened before into: what this system is allowed to do differently now because of it That is why the implementation begins with raw history but ends with behaviour. A final walkthrough Suppose we have worked with an agent for three months. In June we write: The prototype can use SQLite. In July we discover concurrent writes are a problem. A benchmark is run. In August we decide: Move to PostgreSQL. The decision will take effect after migration. In September the migration succeeds. In October a developer asks: Why did we originally use SQLite? The system performs: route → RECALL retrieve → SQLite and PostgreSQL evidence temporal interpretation → SQLite is historical and superseded recall policy → historical evidence retained context → original rationale + transition evidence answer → historical explanation Five minutes later the developer asks: I'm adding a new persistence component. Which database should it use? The history is identical. The memory is identical. The behavior is different. route → INFLUENCE retrieve → SQLite and PostgreSQL evidence temporal interpretation → SQLite superseded → PostgreSQL current frame → implementation work standing → current project evidence admitted context selection → decisive PostgreSQL evidence → SQLite remains in trace but not current guidance action → implement against PostgreSQL That difference is the point of the entire system. The database did not remember. The embedding did not remember. The retriever did not remember. Even the temporal reducer did not remember on its own. The system remembered because retained experience changed present behavior through a controlled, inspectable path. What remains deliberately unfinished A capstone should also say what it does not establish. This implementation does not prove that every AI system needs the complete pipeline. It does not prove that PostgreSQL is the best substrate for all memory. It does not prove that every project needs structured temporal events. It does not prove that inferred frames are always reliable. Framing passes its own deterministic contract; whether it measurably improves outcomes, rather than merely behaving as specified, is a separate and still-open question. It does not prove that trust policy can determine truth. It does not prove that more persistent derived memory always helps. In fact, the book provides reasons to doubt each of those stronger claims. The narrower conclusion is more useful: When a particular class of remembered evidence can harm present behavior, make the decision governing that evidence explicit, measurable and traceable. That principle survives every implementation choice in this appendix. From plugin to system The OpenCode implementation is therefore best understood as an adapter around a remembering system. Today: OpenCode ↓ remembering engine ↓ PostgreSQL + pgvector Tomorrow perhaps: another coding agent ↓ same remembering engine or: writing environment ↓ same remembering engine or: autonomous service ↓ same remembering engine The host changes. The model changes. The embedding model may change. The database may eventually change. What should remain stable are the semantic questions: What history do we have? What evidence did retrieval surface? Is the caller reconstructing the past or asking the past to steer the present? What state was valid at the relevant time? What was actually known then? What work is being done now? What evidence has standing to influence that work? What entered context? What was rejected? Why? Did using memory improve the resulting behavior? Those questions are the portable part. The implementation in one page The entire appendix can finally be compressed into one architecture. CANONICAL PAST │ repository + sessions │ ▼ PostgreSQL + pgvector │ strong retrieval │ ▼ candidate evidence │ ▼ ┌──────────────────┐ │ task routing │ └──────────────────┘ │ │ RECALL INFLUENCE │ │ │ temporal state │ │ temporal interpretation │ │ │ project/work frame │ │ │ frame establishment │ │ │ standing / trust │ │ └──────┬─────┘ │ decisive evidence + provenance │ bounded context │ ▼ AI HOST │ ▼ present action │ ▼ EVALUATE Running alongside it: ContextTrace records how the path was taken. That is the architecture. Everything else is replaceable. Conclusion The implementation began with a practical ambition: Give a coding agent memory of the project it is working on. That goal turned out to contain almost every question in the book. What should be stored? What should be retrieved? What happened first? What changed later? What was known at the time? Which old evidence is still valid? Which old evidence is valuable precisely because it is no longer valid? What does the current task actually need? When should remembered material be allowed to influence an action? How do we know which memory changed the result? How do we recover when the system is wrong? The plugin does not answer those questions with one clever model call. It turns them into boundaries. storage ≠ retrieval retrieval ≠ temporal state temporal state ≠ current relevance current relevance ≠ authority memory ≠ context recalled evidence ≠ permission to act Those distinctions are the implementation. OpenCode merely gives us somewhere concrete to run them. The deeper result is a memory system that can outlive its first host. If OpenCode disappears, the memory model remains. If the model provider changes, the temporal events remain. If the embedding model changes, canonical history remains. If a derived interpretation becomes wrong, its evidence remains. If policy changes, old traces remain explainable. That is a useful standard for durable AI memory: The intelligence may change. The interface may change. The host may disappear. The remembered past should remain inspectable, and the rules by which it changes present behaviour should remain explicit. That is the capstone. Not a database that stores more. Not a model that sees more. The plugin itself — source, engine, and fixture contracts — is developed in the open at opencode-remembering. A system that can explain why the past mattered this time.