1. Executive Summary
AgentOS is a Python agent platform — FastAPI, a tool-calling runtime, planning, delegation to sub-agents, local file and shell tools behind a sandbox, and web tools — published by one author in fourteen commits, all on 11 September 2026, as version 0.1, which its README calls the engineering base. MIT-licensed. It is unrelated to openEuler's AgentOS.
Its long-term memory is 290 lines: one SQLite table, a
remember tool the model calls, a recall tool,
and an automatic recall that runs once at the start of each run and
places the matches in the system prompt for every model call in it.
Retrieval is keyword matching, and the module says so in its first
paragraph — no embeddings, no model call, a known cost in recall for
paraphrase and cross-language queries. Chinese queries are split into
overlapping two-character bigrams, because, as the code notes, SQLite's
built-in FTS5 tokenizer does not recall two-character Chinese words.
It carries one of the atlas's seven marks,
scope_enforced, and the shape of the rest is unchanged: a
memory is a row of text with no status, no second timestamp and no
record of its removal. The store is now multi-tenant.
_visibility_clause
(src/agentos/runtime/repositories.py:901-917) returns a
predicate whose every branch carries workspace_id = ?, and
it is interpolated into the WHERE of all six statements
that read or delete a memory. The tenant ids reach it from the request
context through _scope_ids, and the model's two memory
tools take only content or query and a
scope string — they cannot name a workspace or a user. The
branch that matters most is the one for a caller with no user id: it
narrows to scope = 'workspace' rather than widening to
everything, which is the direction this failure usually runs.
An audit log arrived and the memory path does not use
it. src/agentos/runtime/audit.py is 170 lines
answering four fixed questions — who, when, what to what, with what
result — persisted to audit_logs with its context taken
from contextvars so a caller cannot forget to pass it. API keys,
workspaces, agents, quota, tool policy and the dashboard all write to
it. long_term_memory.py, memory_tools.py and
memory.py contain no reference to it at all, so
audit_log is withheld on a mechanism that exists and is
wired everywhere except here.
2. Mental Model
A fact becomes a memory when the model decides to call
remember with it. From then on it is recalled into any
conversation whose words overlap with it, until someone deletes it by id
or clears the store. There is no state between written and deleted, no
check on who wrote it or from what, and no boundary around which
conversations can see it.
Diagram source
%% caption: the model writes memories into one global table, and the runtime recalls keyword matches into the system prompt of every later session by default, so anything the agent reads can end up there
flowchart TD
U["user message"] --> RT["runtime turn"]
W["fetch_url result, always registered"] --> RT
RT -->|"model calls remember"| T["memories table, session_id never set"]
N["next session, any user"] --> Q["automatic recall with the new message"]
T --> Q
Q -->|"LIKE matches on words and CJK bigrams, top 5"| S["system prompt: long-term memory, for reference"]
S --> RT2["model call"]
D["DELETE by id or clear all"] --> T3. Architecture
One FastAPI process with the runtime in it. api/app.py
builds the LongTermMemory when
memory.long_term_enabled is true — the default — and passes
it both to the tool registry, which adds remember and
recall, and to the runtime, which uses it for automatic
recall. The database lives at .agentos/memory.db relative
to the working directory unless configured. Short-term memory is
separate and in-process: runtime/memory.py keeps each
session's history in an LRU with turn-based truncation that keeps
tool-call pairs together.
4. Essential Implementation Paths
- Write.
RememberTool.run(src/agentos/runtime/memory_tools.py:36-38) callsLongTermMemory.remember(content)with no session id.remember(long_term_memory.py:125-147) strips, truncates to 4,000 characters, refuses an empty string, and inserts. There is no deduplication: the same sentence remembered twice is two rows. - Recall.
recall(:149-178) extracts terms — lowercased English words of two or more characters, and for each run of Chinese characters the run itself plus its bigrams — and scores each row as the sum of the lengths of the terms it contains, bylower(content) LIKE %term%, returning rows with a positive score, best first, then newest. - Automatic recall. Once per run,
AgentRuntime._recall_long_term(src/agentos/runtime/runtime.py:397-408, called at:232) recalls with the incoming message and, if anything matches, formats it under a long-term memory — historical records relevant to the current question, for reference header;_rebuild_system_promptplaces that same text after the base prompt and before the current plan on every model call of the run (:263). - Delete.
forget(id)andclear()runDELETE; the REST routes inapi/routes/memories.pyexpose list, create and delete.
5. Memory Data Model
memories(id INTEGER PRIMARY KEY, content TEXT NOT NULL, session_id TEXT, workspace_id INTEGER, user_id INTEGER, scope TEXT NOT NULL DEFAULT 'workspace', created_at TEXT NOT NULL),
indexed on (workspace_id, created_at DESC) and
(workspace_id, user_id, scope). That is the whole model: no
type, no source, no confidence, no status, no owner.
6. Retrieval Mechanics
Keyword only, and scored by the length of what matched rather than by frequency, so a query that shares one long word with a memory outranks one that shares two short ones. The limit defaults to five and is capped at twenty. Because matching is substring, a two-letter English term matches inside longer words, and every Chinese bigram of a long query is a separate term, which raises recall at the price of precision. The substring approach avoids FTS5's tokenizer and pays for it with a full scan per query; at the scale a v0.1 single-process service reaches, that is not yet a cost.
7. Write Mechanics
A remember call blocks for one SQLite insert and is
retrievable immediately through the recall tool. The
automatic recall picks it up at the start of the next run, because it
recalls once per run rather than per model call. No background process
rewrites or consolidates anything.
8. Agent Integration
The model sees remember and recall as
ordinary tools beside fetch_url, web_search
when a key is configured, sandboxed local file and shell tools, planning
tools, and delegate_to_agent. The memory is also injected
without the model asking, through the automatic recall. There is no MCP
surface and no SDK.
9. Reliability, Safety, and Trust
No scope, by design. The module documents long-term
memory as shared across sessions. The session_id column is
never populated by the tool and is not used in any WHERE
clause, and there is no user, tenant or agent key, so every session —
and every user of a shared deployment — reads the same memories. For one
person on one machine that is the intended behaviour; it is the reason
the store is unsafe for anything else.
The write is the model's, and the read is automatic.
A memory written because a user stated a preference and a memory written
because a fetched page contained an instruction are indistinguishable
rows, and both are recalled into the system prompt by default under a
header that asks the model to use them for reference. The
fetch_url tool has an SSRF guard for where it may connect;
nothing guards what it brings back from reaching
remember.
Deletion is final and unrecorded.
forget and clear remove rows with no history,
and nothing prevents the model from writing the same content again.
10. Tests, Evals, and Benchmarks
Fourteen test files, 3,642 lines, for the whole platform; no CI configuration is committed. I did not run them.
tests/test_long_term_memory.py covers term extraction
for English and Chinese, write, truncation, listing order, forget and
clear, persistence across instances, recall of short Chinese and English
queries, the limit, length-weighted ranking, empty queries, the two
tools, automatic injection into the system prompt with and without a
base prompt, its absence when disabled, and the REST routes.
tests/test_memory.py covers the short-term session store
thoroughly, including truncation that does not split a tool-call
pair.
The one negative case is
test_recall_returns_empty_for_unrelated_query, which
asserts recall(...) == []. A retriever that returned
nothing for every query would pass it, so it does not establish that
anything specific was kept out of a populated result, and
negative_eval is withheld. No test writes memories in two
sessions and asserts either one's isolation, which is consistent with
there being none.
11. For Your Own Build
Steal
- CJK bigrams when FTS5 will not tokenise Chinese. A run of Chinese characters plus its overlapping pairs, matched as substrings, is a small, dependency-free way to make keyword recall work for Chinese, and the module states its limits honestly.
Avoid
- A global memory the model writes and every session reads by default. Scope the store before turning automatic recall on, and record where each memory came from so recall can treat a user's statement and a tool result differently.
- Recalled memory in the system prompt without framing. Mark it as data the model should weigh, not instruction it should follow.
Fit
A readable starting point for a single-user, local experiment with agent memory, and a clear illustration of why scope comes first. Not something to run for more than one person, or with web access, until the store is scoped and recall is framed.
12. Open Questions
- Whether the planned evaluation module, listed in the README as not yet started, will include isolation or poisoning cases for the long-term store.
Appendix: File Index
- Long-term memory:
src/agentos/runtime/long_term_memory.py; tools:src/agentos/runtime/memory_tools.py. - Automatic recall and prompt assembly:
src/agentos/runtime/runtime.py. - Short-term session memory:
src/agentos/runtime/memory.py. - Wiring and defaults:
src/agentos/api/app.py,src/agentos/runtime/builtin_tools.py,src/agentos/core/config.py. - Tests:
tests/test_long_term_memory.py,tests/test_memory.py.
Searches recorded for the negative claims
Re-run at this pin.
rg -n "session_id" src/agentos/runtime/long_term_memory.py # still written on insert and never in a WHERE
rg -n "UPDATE memories|tombstone|history" src/agentos/runtime/long_term_memory.py # 0: no edit and no record of deletion
rg -n "audit" src/agentos/runtime/long_term_memory.py src/agentos/runtime/memory_tools.py src/agentos/runtime/memory.py # 0: the audit log has no producer on the memory path
rg -n "workspace_id|user_id" src/agentos/runtime/long_term_memory.py # 24, where the previous pin had none
ls .github/workflows # ci.yml — CI exists, where the previous pin had none
History
2026-09-21 — re-pinned to 08a3af64…,
46 commits on. Screened before reading: two VS Code entries, neither
carrying runOptions.runOn: folderOpen, one
conftest.py that executes at pytest collection, and three
dependency surfaces changed the same day — pyproject.toml
and both frontend manifests, all inside the seven-day cooldown. A
.gitattributes appeared and was read before checkout:
line-ending normalisation and binary markers, no filter=.
Nothing was installed and nothing was run.
Every file the report cites changed, and nine modules are new.
scope_enforced is earned, where the
previous pin had no marks at all: memories gained
workspace_id, user_id and a scope
column, and the predicate reaches all six read and delete statements.
The design note the previous reading quoted — that a global store was
deliberate — no longer describes the code.
Two other negatives move. .github/workflows/ci.yml
exists, so the recorded ls .github result is superseded.
And audit.py is new, which turns a plain absence into a
producer finding: the audit log is real and six subsystems write to it,
while the three memory modules reference it nowhere.
Unchanged and re-checked: no edit path, no deletion record,
session_id still written and never read, one timestamp, and
no status column — so tombstone, trust_state
and bitemporal stay withheld. negative_eval
stays withheld on a distinction worth stating: the suite does assert
that a populated store yields no memory block when
long_term_auto_recall=False, against a positive at
:229-235, but that tests a configuration flag rather than a
predicate keeping particular material out of a populated result, and no
committed case asserts that one workspace cannot read another's
rows.
2026-09-11 — cea6420c…
— first reading. Screened with scripts/screen_repo.py: two
editor-configuration findings, .vscode/settings.json
enabling pytest discovery and .vscode/tasks.json defining
six manual tasks, none set to run on folder open; one
conftest.py; one manifest inside the seven-day cooldown and
one unpinned surface; an AGENTS.md of project conventions
for coding assistants, read as data. The folder was not opened in an
editor, and nothing was installed or run.