1. Executive Summary
people-context is memory about people rather than about code: "who
someone is, how you know them, what you last agreed, and how they like
to be talked to", held in one local SQLite file with "[n]o account, no
cloud, no network calls". MIT, version 1.2.1, 238 commits since 16 July
2026, 33,465 lines of Python in src against 52,271 lines of
tests, plus a committed eval suite, an Obsidian plugin and an OpenClaw
plugin.
The subject matter is the first thing to say about it. This is a store of personal data about third parties who did not consent to being in it, and the project knows: sensitivity is a first-class field, CLI error paths are annotated with warnings that a rejected input echoes submitted values "which may be personal", and a validation error is noted as "not automatically safe to print". That care shows up as mechanism rather than as a policy document.
Disclosure is narrow by default and widened on
purpose. ORDINARY_SENSITIVITIES is
(PUBLIC, PERSONAL), documented as "[l]evels an ordinary
read may disclose, in the shared order used by every other read path",
with ALL_SENSITIVITIES kept for "the explicit local
opt-in". A fact defaults to PERSONAL. So a record marked
SENSITIVE or RESTRICTED is simply absent from
a timeline or an upcoming-reminder read until a caller asks for it — the
shape this atlas looks for in a scope key, applied here to
confidentiality rather than to tenancy.
Imports stage, review, then commit by name. The
import module's opening line states the intent: "[t]he lifecycle keeps
its review gate on purpose. A staged batch is durable review state."
pctx import review lists the staged candidates;
pctx import commit takes the ids a person selected, and
"only ids the batch actually staged are selectable, and a typo refuses
the whole selection rather than silently committing the part that
happened to parse." Refusing the whole selection on a typo is the
conservative choice and the uncommon one.
The staging model's docstring is the best piece of writing in the
repository and is worth reading for the principle rather than the code.
It explains which validations belong at the staging boundary and which
do not, and the rule it lands on for a restore is subtle and right:
re-check a bound only when refusing could not reject this installation's
own data. A trait's evidence budget and its stated_by
attribution are re-checked because they were unconditional at the input
boundary and their breach would be carried forward; an observation's
text and a fact's value keep their released
shape, because narrowing those "would refuse rows this
installation legitimately stored."
The eval suite scores what an answer must not say.
evals/suite/suite.json carries a fixture world and weighted
rubrics; the identity-disambiguation task seeds two
contacts sharing a first name and scores
names-the-right-priya and
states-employer-and-role beside
does-not-attribute-the-other-priya.
Two limits to state plainly. The store is one person's local file, so sensitivity is a discipline about what surfaces show, not a boundary between principals — there is no second principal to keep out. And a fact carries a validity period and a recording time, which the domain calls "bitemporal-lite"; no read path found here gates a query against a past instant, so the data supports a point-in-time question that the interface does not yet ask.
Three marks: scope_enforced, human_review,
negative_eval.
2. Mental Model
A person is the subject. Around them: facts (predicate, value, validity period, recorded time, confidence, sensitivity, provenance), observations, interactions, traits with typed evidence links, relationships from a vocabulary, groups, organizations, preferences and reminders.
A sensitivity is one of four levels. Ordinary reads see two.
A staged candidate is an import in review: not the shape it arrived in, not the shape it will be stored as, but a third thing with batch-local references rewritten to canonical ids.
Diagram source
%% caption: imports stage into durable review state and commit only by named id; ordinary reads disclose two of four sensitivity levels and the rest need an explicit opt-in
flowchart TB
SRC["transcript / source"] --> EXTRACT["extraction at a bounded boundary<br/>byte budgets apply here only"]
EXTRACT --> STAGE[("import_staging<br/>refs rewritten to canonical ids<br/>extra = forbid")]
STAGE --> REVIEW["pctx import review <batch><br/>every staged candidate shown"]
REVIEW --> PERSON{"person selects ids"}
PERSON -->|"typo in an id"| REFUSE["whole selection refused"]
PERSON -->|"valid ids"| COMMIT["pctx import commit --ids ..."]
COMMIT --> STORE[("SQLite: people, facts,<br/>observations, interactions,<br/>traits + evidence links,<br/>relationships, reminders")]
FACT["fact: predicate, value,<br/>validity period, recorded_at,<br/>confidence, sensitivity, provenance"] --> STORE
READ["timeline / upcoming / insights"] --> SENS{"sensitivity"}
STORE --> SENS
SENS -->|"PUBLIC, PERSONAL"| SHOW["disclosed by an ordinary read"]
SENS -->|"SENSITIVE, RESTRICTED"| HIDE["absent unless the caller<br/>opts in explicitly"]
EVAL["evals/suite: fixture world + rubrics"] --> MUSTNOT["scores must-say and must-not-say<br/>e.g. does-not-attribute-the-other-priya"]3. Architecture
| Area | Role |
|---|---|
src/people_context/domain/ |
The model — one file per concept, including
staged_candidate.py and trait_evidence.py |
src/people_context/app/ |
Use cases, including insights/timeline.py and
insights/upcoming.py where the sensitivity bound lives |
src/people_context/adapters/, ports/ |
The SQLite adapter behind ports |
src/people_context/cli/ |
pctx — people, groups, imports, insights, portability,
onboarding, maintenance |
evals/ |
A harness, a suite with a fixture world, and recorded results |
obsidian-plugin/, openclaw-plugin/,
mcpb/, skills/ |
Integrations |
4. Essential Implementation Paths
src/people_context/app/insights/timeline.py:66-74— the two sensitivity tuples and the comment that makes the default explicit.src/people_context/cli/imports.py:1-9, 200-260— the review gate and the all-or-nothing selection.src/people_context/domain/staged_candidate.py— the boundary reasoning.src/people_context/domain/fact.py:10-22— the "bitemporal-lite" fact.evals/suite/suite.json— the rubrics, including the must-not item.
5. Memory Data Model
One domain file per concept keeps the model readable, and two details
stand out. A trait's evidence link carries the record type as
well as the id, because "ids are opaque and unique only within their own
table, so a restored store may hold an observation and an interaction
sharing one id" — a restore-safety concern most systems discover later.
And extra="forbid" on the staged shape is justified as
"staging is where extraction output stops being prose, so a key nothing
here declares is unexplained text that review would display and every
later bundle would carry."
6. Retrieval Mechanics
Resolve the person first — the eval suite's system prompt instructs exactly that, and says "never guess an identity you could look up" — then read their stored context. Timelines and upcoming reminders are bounded by the ordinary sensitivity levels.
7. Write Mechanics
Direct writes through the CLI and MCP tools; imports through extraction, staging, review and a named commit. Error paths are written with the awareness that echoing a rejected value may print personal data.
8. Agent Integration
An MCP server plus pctx, with an Obsidian plugin, an
OpenClaw plugin, an mcpb bundle and skills. The pitch —
"[y]our agent already remembers your codebase. Now it can remember your
people" — is a fair description of the gap it fills in this corpus,
where almost every subject is about code or tasks.
9. Reliability, Safety, and Trust
The care about personal data is real and mechanical: a default
sensitivity of PERSONAL, ordinary reads bounded to two
levels, error paths annotated for what they may echo, and a review gate
before anything extracted becomes durable.
What the design cannot do is consent. The people in this store did not agree to be in it, and no software decision changes that; the honest framing is that the project reduces accidental disclosure rather than resolving the underlying question. Anyone deploying it should read the sensitivity levels as a tool for their own discipline.
The single-user local model is also why scope_enforced
here means something narrower than it does for a multi-tenant service:
the key is stored and the read path honours it, but the party being kept
out is the surface, not another principal.
10. Tests, Evals, and Benchmarks
52,271 lines of tests against 33,465 of source, organised to mirror
the package, plus evals/ with a harness, a committed
fixture world, a suite and recorded results. The eval design is the part
to copy: a small hand-built world with deliberately confusable contacts,
tasks with weighted rubric items, and rubric kinds that include both
answer_contains_all and a must-not pattern.
11. For Your Own Build
Steal
- Make the narrow set the default and name the wide
one. Two constants —
ORDINARY_SENSITIVITIESandALL_SENSITIVITIES— with a comment saying which one an ordinary read uses, is clearer than a boolean parameter. - Refuse the whole selection on a bad id. Committing the part that parsed is how a review gate quietly becomes a formality.
- Decide which validations belong at which boundary, and write the rule down. Re-check a bound on restore only when refusing could not reject your own stored data.
- Put the record type in an evidence citation. Ids unique only within a table collide after a restore, and a bare id renders two records as one.
- Seed an eval world with confusable entities and score what the answer must not say.
Avoid
- Printing a rejected value without thinking about what it contains. The CLI's annotations exist because the obvious error message leaks the thing the store is careful about.
Fit
Reach for this if you want an agent to hold relationship context locally with disclosure bounded by default. Look elsewhere if you need multi-principal scoping, or point-in-time reads over the validity periods it already stores.
12. Open Questions
- The fact carries a validity period and a recording time. Is an as-of read planned, and would it use both axes or only validity?
- Sensitivity bounds ordinary reads. Does the MCP surface expose the opt-in, and if so what stops an agent taking it by default?
- Trait evidence links name the record type. Is there a check that a cited record still exists after an erasure?
Appendix: File Index
| Path | What to read it for |
|---|---|
app/insights/timeline.py |
The sensitivity bound and its comment |
cli/imports.py |
The stage-review-commit gate and its refusal rule |
domain/staged_candidate.py |
Which validations belong where, and why |
domain/fact.py |
The "bitemporal-lite" assertion |
evals/suite/suite.json |
Rubrics including a must-not item |
History
2026-09-16 — e4afd375…
— first reading, at a commit dated 14 September 2026. Screened before
opening, from a shallow clone: twenty files, three auto-run surfaces (a
.claude-plugin/ directory, an .mcp.json and an
MCP server manifest), two build-time execution points, two unpinned
surfaces, nine dependency files inside the cooldown, and
AGENTS.md read as data. Nothing was installed, built or
run.