Chapter 19 of 19

Appendix — Building a Remembering Agent

Concepts

Appendix — Building a Remembering Agent

Source: 19-chapter.md

What this appendix is really about

This is not another investigation chapter. Chapters 1–18 asked what remembering requires and earned an architecture through measured failure. This appendix asks a different question: what does it cost, in real software, to build that architecture behind a host an agent actually uses? Its deepest move is separating the host adapter from the remembering engine — the plugin only translates OpenCode events into a small neutral protocol; the engine owns capture, retrieval, routing, temporal resolution, framing, and eventually admission. The hidden thesis: a memory architecture that only works inside one host was never actually earned — it was just convenient.

Current build state (grounded in the live opencode-remembering repository)

Shipped and fixture-tested

  • Stage 1 — retrieval. PostgreSQL + pgvector, per-project schema isolation, automatic ingestion, Ollama bge-m3 embeddings, hybrid retrieval (FTS + dense + RRF), bounded provenance-bearing context, automatic canonical session capture.
  • Stage 2 — recall/influence routing. Deterministic router; 19 recall/influence/ambiguous fixtures plus 2 explicit-override checks, 21 checks passing. Both routes share one retriever; nothing is filtered by route at this stage.
  • Stage 3 — temporal resolution. Bitemporal event model (event_time/recorded_at/effective_from/effective_to); four standpoints (current, valid_at, as_known, bitemporal); recall preserves superseded evidence, influence suppresses it with a reason code.
  • Stage 3.5 — standalone extraction. The engine is bundled at engine/remembering; the research-repository checkout is no longer imported at runtime, and a stale project_memory_root setting now fails closed instead of silently falling back to it.
  • Stage 4 — safe project/work framing. Declared ProjectFrame (.remembering/project-frame.json, fail-closed on project-identity mismatch), evidence-backed WorkFrame built only from current WorkSignals, six deterministic establishment states (declared/corroborated/inferred/conflicting/stale/unknown), three control tiers (hard/soft/query-only), hard exclusion only with explicit opt-in. Eight fixture categories pass. Wired into the automatic context hook, not only the explicit tool.

Explicitly out of scope for the default path

The plugin deliberately does not port the book’s persistent graph, associative propagation, or Nexus-style routing layer into its default path — matching Chapter 18’s own demotion of those mechanisms from the earned architecture. “Port the conclusions of the experiments, not the inventory of the book” is stated as an explicit design rule, not an oversight.

Target architecture, not yet built

  • Standing/trust (admit / deny / quarantine).
  • The full auditable ContextTrace (memory_trace).
  • Open-loop handling (memory_open_loops).
  • Explicit remembering (memory_remember).
  • The paired-intervention evaluation harness (the N0–N5 memory-condition ladder).

What “shipped” does and does not prove

A passing fixture contract is a claim about specified behaviour, not about behavioural value. Stage 4’s eight categories prove framing behaves as designed under controlled inputs; they do not show that framing changes outcomes for the better once a real reader is running against real project history — that is exactly the distinction Chapter 12 forced onto the rest of the book, and the appendix inherits it rather than relaxing it for its own mechanisms. The same caveat applies to Stage 2’s 21 routing checks: they freeze a contract, not a measured behavioural result.

Where this document and the plugin repository can drift

This file, like the chapter it describes, is a snapshot from one review pass. The plugin is still under active, separate development. Treat HANDOFF.md’s dated status entries in opencode-remembering as the more current source once this text is more than a few sessions old, and re-verify specific stage numbers, fixture counts, and file layouts against the live repository before citing them — the same discipline the book applies to its own frozen experimental numbers applies here to a codebase that, unlike the book’s fixtures, is not frozen.

Concepts worth developing after the book

  • A real evaluation run of Stage 4 framing under the book’s own N0–N5 intervention design, on OpenCode tasks rather than controlled fixtures.
  • Standing/trust integration once framing is in place, including how frame control and trust admission should compose — a hard-framed but untrusted candidate; a trusted candidate outside every declared work type.
  • Whether the host/engine boundary holds cleanly for a second real host, or whether it is currently just an architecture diagram with one working example behind it.
  • Cross-project or cross-worktree project identity, which the appendix itself defers as “not the final philosophical answer” to project isolation.

Open questions this appendix leaves for the book, not for itself

  • Does the six-question spine generalise to a host that is not a coding agent, where “what happened” is harder to observe than a diff or a failing test?
  • Does the earned architecture’s ordering — retrieval, routing, temporal, framing, standing — hold as a build order as well as an investigation order, or would a different host force a different sequence?

Explain this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

Apply this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

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.


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.