A project notebook that knows when to ask again

Yesterday's workshop venue was Harbor Hall. Today the organizer moved it to Library Room, pending confirmation before invitations go out. An assistant should retain that correction between sessions without treating it as permanently true.

Input: replace the workshop's venue with Library Room, using organizer note 2, valid until demo day 3. Finished result: on day 1 the notebook returns Library Room with its source and revision. On day 3 it returns RECHECK, not the old Harbor Hall. A separate website project's venue never appears in the workshop context.

Your artifact is project-memory.sqlite3, a local database, plus a complete Python program to update and read it. Memory here means information saved outside a conversation and retrieved later. It does not mean changing a model's training. Context is the information supplied for the current task. A revision is a counter showing which saved version you are updating.

This is the context-and-memory track in our practical agent learning path. Edition 20 built a read-only tool. Today we decide what a future assistant should retrieve before answering. Both are components, not a complete deployed agent.

Why not save every conversation?

A Redis community report about contradictory memories, opened March 13, 2026, describes older and newer project facts being returned together. A separate Memoria report about stale task assumptions, opened that day, describes temporary conclusions influencing later work. These are individual reports from different projects. We inspected their descriptions, not their underlying experiments, and make no failure-rate claim.

Research question: Can a small notebook preserve an approved correction across a restart, reject an update based on an obsolete revision, and stop returning a fact when its review deadline arrives?

For one workshop, a shared document is probably enough. Explore this database when software needs repeatable update and recall rules. A person still judges sources and deadlines. The fictional exercise calls no model and measures neither model accuracy nor time saved.

1. Prepare a clean folder and a fixed clock

You need Python 3.10 or newer, a terminal and a text editor. Check python --version; substitute python3 if needed. In an empty folder, save the code as lab.py, then run python lab.py. No packages, account or API key are required. Allow 30 minutes.

Whole demo days make the result reproducible. Days 0, 1 and 3 are test inputs, not real dates. A fact is eligible from its recorded day until, but not including, its expiry day. Production software needs an authoritative clock and timezone-aware timestamps.

2. Save and run the notebook

SQLite stores a database in one file. The table has one current row per project and field, limited to format and venue, rather than a free-form conversation archive.

import argparse
from pathlib import Path
import re
import sqlite3

PATH = Path("project-memory.sqlite3")
FIELDS = ("format", "venue")
SCHEMA = """CREATE TABLE facts (
    project TEXT, field TEXT, value TEXT NOT NULL,
    source TEXT NOT NULL, until_day INTEGER NOT NULL, recorded_day INTEGER NOT NULL,
    revision INTEGER NOT NULL, PRIMARY KEY (project, field)
)"""


def check_clock(day):
    if type(day) is not int or day < 0:
        raise ValueError("day must be a nonnegative integer")


def check_project(project):
    if type(project) is not str or not re.fullmatch(r"[a-z][a-z0-9-]{0,31}", project):
        raise ValueError("invalid project label")


def remember(db, project, field, value, source, until_day, expected, day):
    check_project(project)
    check_clock(day)
    if field not in FIELDS:
        raise ValueError("unknown field")
    if any(type(x) is not str or not x.strip() or len(x) > 80 for x in (value, source)):
        raise ValueError("value and source need 1 to 80 characters")
    if type(until_day) is not int or until_day <= day:
        raise ValueError("expiry must be after the write day")
    if type(expected) is not int or expected < 0:
        raise ValueError("expected revision must be a nonnegative integer")
    try:
        with db:
            if expected == 0:
                db.execute("INSERT INTO facts VALUES (?, ?, ?, ?, ?, ?, 1)",
                           (project, field, value, source, until_day, day))
            else:
                changed = db.execute("""UPDATE facts
                    SET value=?, source=?, until_day=?, recorded_day=?, revision=revision+1
                    WHERE project=? AND field=? AND revision=? AND recorded_day<=?""",
                    (value, source, until_day, day, project, field, expected, day))
                if changed.rowcount != 1:
                    raise ValueError("revision changed or fact is missing; reread first")
    except sqlite3.IntegrityError as error:
        raise ValueError("fact already exists; reread its revision") from error


def context(db, project, day):
    check_project(project)
    check_clock(day)
    rows = db.execute("""SELECT field, value, source, revision FROM facts
        WHERE project=? AND recorded_day<=? AND until_day>? ORDER BY field""",
        (project, day, day)).fetchall()
    found = {field: (value, source, revision) for field, value, source, revision in rows}
    lines = [f"PROJECT {project}; demo day {day}"]
    for field in FIELDS:
        item = found.get(field)
        if item is None:
            lines.append(f"{field}: RECHECK (missing or expired)")
        else:
            value, source, revision = item
            lines.append(f"{field}: {value} [{source}; revision {revision}]")
    return "\n".join(lines)


def checks():
    db = sqlite3.connect(":memory:")
    db.execute(SCHEMA)
    base = ("workshop", "venue", "Library Room", "note-2", 3, 0, 1)
    remember(db, *base)
    bad = [base, (*base[:5], 9, 1),
           ("workshop", "venue", "Room", "note", 1, 1, 1),
           ("workshop", "venue", "Room", "note", True, 1, 0),
           ("workshop", "venue", "", "note", 3, 1, 1),
           ("workshop", "venue", "Room", "", 3, 1, 1),
           ("workshop", "venue", "Room", "note", 3, True, 1),
           ("workshop", "execute", "Room", "note", 3, 0, 1),
           ("bad project", "venue", "Room", "note", 3, 0, 1),
           ("workshop", "venue", "Old Room", "old-note", 9, 1, 0)]
    for args in bad:
        try:
            remember(db, *args)
        except ValueError:
            pass
        else:
            raise AssertionError("invalid write accepted")
    assert "Library Room" in context(db, "workshop", 2)
    assert "Library Room" not in context(db, "workshop", 3)
    assert context(db, "website", 1).count("RECHECK") == 2
    assert context(db, "workshop", 0).count("RECHECK") == 2
    assert db.execute("SELECT count(*) FROM facts").fetchone()[0] == 1
    db.close()
    return len(bad)


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--read-day", type=int)
    args = parser.parse_args()
    if args.read_day is not None:
        try:
            check_clock(args.read_day)
            db = sqlite3.connect("file:project-memory.sqlite3?mode=ro", uri=True)
            print(context(db, "workshop", args.read_day))
            db.close()
        except (ValueError, sqlite3.Error) as error:
            parser.error(str(error))
        return
    try:
        PATH.open("xb").close()  # Refuse to overwrite an existing notebook.
    except FileExistsError:
        parser.error("notebook exists; use --read-day or a new empty folder")
    db = sqlite3.connect(PATH)
    db.execute(SCHEMA)
    remember(db, "workshop", "venue", "Harbor Hall", "organizer-1", 10, 0, 0)
    remember(db, "workshop", "format", "hands-on", "organizer-1", 10, 0, 0)
    remember(db, "website", "venue", "Online", "web-note", 10, 0, 0)
    remember(db, "workshop", "venue", "Library Room", "organizer-2", 3, 1, 1)
    db.close()
    db = sqlite3.connect("file:project-memory.sqlite3?mode=ro", uri=True)
    print("OFFLINE MEMORY LAB: no model or external service called")
    print("REOPENED: committed facts survived a connection restart")
    print(context(db, "workshop", 1))
    print(context(db, "workshop", 3))
    db.close()
    print(f"NEGATIVE WRITE CHECKS: {checks()} passed")
    print("SAVED: project-memory.sqlite3; expired facts remain stored")


if __name__ == "__main__":
    main()

Expected complete output:

OFFLINE MEMORY LAB: no model or external service called
REOPENED: committed facts survived a connection restart
PROJECT workshop; demo day 1
format: hands-on [organizer-1; revision 1]
venue: Library Room [organizer-2; revision 2]
PROJECT workshop; demo day 3
format: hands-on [organizer-1; revision 1]
venue: RECHECK (missing or expired)
NEGATIVE WRITE CHECKS: 10 passed
SAVED: project-memory.sqlite3; expired facts remain stored

The database survives exit. Committed writes are read through a reopened, read-only connection, not a reused Python dictionary. The ten negative checks use a separate in-memory database, leaving your notebook unchanged.

3. Trace a correction without reviving the old value

Expected revision 0 means “this field must not exist yet.” The correction supplies expected revision 1 and succeeds only if that version is current; the counter becomes 2. A writer still holding revision 1 must reread before updating revision 2.

SQL is the database query language. Its update condition compares the revision and rejects backdated writes. Question-mark placeholders bind values as data rather than inserting them into query text.

There is no fuzzy matching or automatic contradiction detection. The operator explicitly identifies the same project and field. “Library Room replaces Harbor Hall” is a correction because the sample organizer made that decision, not because the program inferred it from language.

The old value is overwritten. When Library Room expires, the notebook cannot fall back to Harbor Hall. RECHECK means a currently eligible value is missing. It is a request to consult the organizer, not evidence that the venue no longer exists. Reading does not extend a fact's deadline.

4. Read it from a new process

After the first run finishes, enter python lab.py --read-day 3. It must print the three lines beginning PROJECT workshop; demo day 3 in the expected block. The format remains hands-on, and the venue needs rechecking. python lab.py --read-day 2 must return Library Room at revision 2.

Running python lab.py again without a read option must refuse to overwrite the existing notebook. Keep the database if you want to inspect it. For a fresh replay, use another empty folder and copy only lab.py there. Do not delete a notebook containing your edits just to recover the sample output.

The project label is chosen by this local program. It is not authentication. A hosted assistant must derive permitted project access from the signed-in user and enforce it outside the model. A model-provided project name alone is not permission to read that project's memory.

5. Use the recalled facts, not yesterday's prompt

Optionally copy the day-3 output into a fresh model conversation. Ask for a planning summary using these supplied notes, leaving the venue unresolved and listing a question for the organizer. Omit yesterday's venue response. Check that the model preserves RECHECK rather than inventing a room.

That optional model step is unexecuted and unevaluated here. An API integration would retrieve context before each model call and separately test its use. Retrieved values remain data, not permission to execute instructions inside a note.

To adapt it, choose your project's fields, replace fictional writes with approved notes, and set deadlines according to how quickly each fact changes. Venue confirmations and writing preferences need not share a lifetime. A review deadline does not prove a fact stays true until then.

Troubleshooting and completion

“Notebook exists” means the protection worked; use a read command or a clean folder. “Revision changed” means reread before proposing another correction. An expired value cannot be fixed by silently extending its deadline. Obtain fresh evidence. If the database is missing, read-only mode fails rather than creating an empty replacement.

Completion check: match complete stdout, read in a second process, explain the revisions and show day-3 expiry. In a fresh copy, change the correction's deadline from 3 to 5: day 3 must show Library Room. Restore the code for the original output.

This current-state notebook is not an audit log or historical reconstruction. It overwrites prior values, retains expired rows and does not erase backups. It implements no authentication, encryption, semantic retrieval or memory-poisoning defense. Use synthetic data. Next experiment: test ten project questions for appropriate clarification instead of answers from outdated history.

References you can use

Anthropic's context-engineering discussion, September 29, 2025, explains structured notes outside a context window. LangChain's memory overview distinguishes conversation scope from longer-lived namespaces. These motivate the separation, not this implementation's correctness. Python's SQLite interface documents parameter binding and transactions; SQLite's UPDATE reference describes conditional row updates. Sources checked October 17, 2026. Code and fictional notes are original educational material.