memory-continuity~/research/memory-continuityAll projects

Type a section name and press Enter. Use “help” for the list.

Figures: softened for dark viewingDisplay only; image files are unchanged.
← The work

Project record · Reviewed October 9, 2026

Memory, continuity, and the vault

What needs to survive when an agent returns to a project?

Current stateImplemented observation storage, resume briefs, synthesis, and vault capture tooling

Approach

Returning to a project should recover the decisions and corrections that shaped it. Memory stores observations about an explicit subject; continuity assembles project context and draws connections across observations; the vault keeps the surrounding decisions, narratives, and activity records.

The subject of a note is independent of where I learned it. A general coding preference recorded under one project should become available in every context where it applies. I call that correction of scope promotion; it can be warranted by a single memory. Combining several related memories into a new statement is a separate synthesis operation.

The vault captures activity so I can reconstruct where time and work went across projects and machines. Generated notes are readable views of that record; capture does not depend on a daily logging habit.

Why this approach

Capture, retrieval, and synthesis have different jobs

Scope determines where a remembered correction can resurface. The vault provider can read an entity’s observations together with those inherited from broader scopes. Filing something too narrowly can therefore hide it from other work where it matters.

The intended promotion path corrects that scope and returns the result to ordinary memory, with links to its sources. It should remain available for later recall and further revision. The implemented cross-project synthesis pass currently writes separate records; connecting that output to normal recall and adding single-memory scope correction are still distinct pieces of work.

Where it stands

The memory plugin provides typed, append-only entries through a provider interface, CLI, and MCP. Its vault provider places entries under the subject entity and updates the entry and index under one provider-root lock. Session-start briefs surface context; a session recorder can combine a transcript digest with the agent’s accumulated notes and decline to write when there is nothing worth retaining.

Continuity builds a project resume brief from recent narrative sections, decisions, journal references, and ranked memory observations. It resolves nested project identities, supports memory-backed insight writes, and exposes cross-project synthesis through CLI and MCP. Ranking currently uses explicit recency and surfacing-frequency rules.

vault-cli extracts sessions, produced artifacts, and Git activity into an append-only event log. Host-specific write partitions keep captures separate, while normalized repository identities connect the same work across clones and machines. Daily and weekly narratives are readable views over captured activity; their tool-free narrator is excluded from harvesting so summaries do not recursively become evidence of new work.

What the work has shown

A captured observation has both a subject and a discoverable record

Memory writes validate the entry, resolve its subject, and keep the file write and index append inside the same provider lock. The vault provider uses an existing entity’s .memory directory and rejects an unknown subject instead of silently depositing it elsewhere.

The informed session recorder adds an automatic capture path: it gives the authoring step a transcript digest and existing session notes, then saves through the ordinary writer. Its explicit nothing-worth-recording outcome keeps a session ending from automatically producing an empty recap.

Resume context preserves nested project identity

Project discovery handles a top-level name, an explicit nested path, or an unambiguous nested leaf. Ambiguous names require disambiguation. The resume composer keeps the original project identifier when reading the vault, so a nested project with a name shared by its parent does not silently receive its parent’s narrative.

The brief bounds what it surfaces: two narrative sections, up to five decisions from the last thirty days, three days of journal references, and ten ranked memory observations. Those limits make resumption practical while leaving the underlying records available for closer inspection.

The implemented synthesis pass keeps its supporting observations

The implemented cluster-merging pass requires at least two observations from at least two subjects. It checks existing coverage, resolves a shared scope, and asks a drafting step whether the material consolidates. Accepted results add a record with source references while leaving the observations intact.

The code calls these outputs promotions and currently stores them in separate vault directories. They do not yet rejoin the ordinary memory reader. Correcting the scope of a single memory and making the result available through normal recall remain separate goals.

Artifacts and records

Implementation artifact: write

The memory write entry point validates an entry, stores it through the selected provider, and updates the index under a shared lock. This excerpt shows the persistence contract, not a claim about later recall quality.

Inspect the source excerpt
def write(
    name: str,
    description: str,
    type: str,
    subject: str,
    body: str,
    provider: Provider | None = None,
) -> str:
    """Validate, store via provider, and append a MEMORY.md bullet.

    Returns the stored path/id reported by the provider.

    Raises:
        ValueError: if `type` is not one of VALID_TYPES, or if `name`,
            `description`, or `subject` is empty/whitespace.
        MemoryCollisionError: re-raised from the provider when the resolved
            location already exists.
        MemoryAmbiguousSubjectError: re-raised from the provider when subject
            resolution is ambiguous.
        MemorySubjectNotFoundError: re-raised from the provider when subject
            does not resolve to any PARA entity directory (and no alias
            in `<vault>/.memory-aliases.yaml` redirects it to one).
    """
    if type not in VALID_TYPES:
        raise ValueError(
            f"invalid type: {type!r}; expected one of {sorted(VALID_TYPES)!r}"
        )
    for field_name, val in (
        ("name", name),
        ("description", description),
        ("subject", subject),
    ):
        if not val or not val.strip():
            raise ValueError(f"{field_name} is required")

    # The MEMORY.md bullet format embeds every field verbatim on a
    # single line, with this shape:
    #   - [[<path>|<name>]] · type:T subject:S · <description>
    # A field containing a character that conflicts with the format
    # produces a bullet that the parser silently skips — put() succeeds
    # but the entry never appears in list()/get() results.
    #
    # Reject the conflicting characters at the writer (the only
    # supported entry point) so a malformed entry never reaches the
    # index.
    if any(c in name for c in "]|\r\n"):
        raise ValueError(
            f"name must not contain ']', '|', or newline characters "
            f"(they break the [[path|name]] bullet link); got {name!r}"
        )
    if "\r" in description or "\n" in description:
        raise ValueError(
            f"description must not contain newline characters "
            f"(bullets are one line each); got {description!r}"
        )
    if any(c.isspace() for c in subject):
        raise ValueError(
            f"subject must not contain whitespace; got {subject!r}"
        )
    if "\u00b7" in subject:
        raise ValueError(
            f"subject must not contain the index separator '\u00b7'; "
            f"got {subject!r}"
        )

    provider = provider if provider is not None else _get_provider()
    # Always source vault_root from the provider, not from the env, so an
    # injected provider with a custom root keeps its index file and entry
    # files in the same directory tree. Previously memory_writer pulled
    # vault_root from MEMORY_VAULT_DIR independently, which silently
    # misaligned index and storage when a caller passed a provider with
    # a different root.
    vault_root = Path(provider.root)

    with memory_lock(vault_root, context="memory_writer.write"):
        entry = Entry(
            name=name,
            description=description,
            type=type,
            subject=subject,
            body=body,
        )
        path = provider.put(entry)

        # Convert to a vault-relative path for the index. If the provider stored
        # the entry outside vault_root (e.g. a test FilesystemProvider rooted
        # elsewhere), fall back to the raw path the provider returned.
        try:
            rel_path = Path(path).relative_to(vault_root).as_posix()
        except ValueError:
            rel_path = path

        index.append(vault_root, name, type, subject, rel_path, description)
        return path
Download the verbatim excerpt ↓

What remains open

  • Correct the scope of individual memories and return promoted or synthesized knowledge through ordinary recall, preserving its lineage.
  • Measure whether automatically retrieved corrections change later actions.
  • Refine earlier conclusions when new observations extend or contradict them.
  • Use the activity record to reconstruct work across projects and machines, keeping observed events distinguishable from inferred activity.

Questions this work contributes to