peon-mem 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -108
- package/bin/peon-mem.mjs +36 -4
- package/dist/brain.js +15 -2
- package/dist/entities.js +25 -1
- package/dist/global-memory.js +6 -1
- package/dist/quality.js +48 -0
- package/docs/assets/social-preview.png +0 -0
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# 🧠 Peon — a memory brain for your AI coding agents
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/peon-mem) [](LICENSE) [](https://www.npmjs.com/package/peon-mem) [](LICENSE) [](test/)
|
|
4
4
|
|
|
5
5
|
**Local-first, hierarchical, self-improving memory for Claude Code, Codex, and any MCP client.**
|
|
6
6
|
|
|
7
|
-
Your AI forgets everything between sessions. Peon doesn't. It records your sessions,
|
|
8
|
-
them into typed *beliefs
|
|
9
|
-
|
|
7
|
+
Your AI forgets everything between sessions. Peon doesn't. It records your sessions, uses an
|
|
8
|
+
LLM to consolidate them into typed *beliefs*, and injects the relevant ones back into every
|
|
9
|
+
prompt. It runs as a daemon on your machine, and nothing leaves it.
|
|
10
10
|
|
|
11
11
|
```
|
|
12
12
|
PEON GLOBAL BRAIN ← user-level facts & preferences, inherited everywhere
|
|
@@ -16,25 +16,30 @@ automatically, from a daemon that never leaves your machine.
|
|
|
16
16
|
|
|
17
17
|
## Why Peon
|
|
18
18
|
|
|
19
|
-
- **Hierarchical brains
|
|
20
|
-
isolated child brain
|
|
21
|
-
- **Two memory layers, honestly measured
|
|
22
|
-
artifacts
|
|
23
|
-
summaries drop
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
- **
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
19
|
+
- **Hierarchical brains.** One global parent brain holds who you are, your rules, and your
|
|
20
|
+
tools. Each project gets an isolated child brain. Every injection carries both.
|
|
21
|
+
- **Two memory layers, honestly measured.** Consolidated *beliefs* (decisions, preferences,
|
|
22
|
+
facts, artifacts) give you the gist. An *episodic* verbatim layer recovers the exact details
|
|
23
|
+
that lossy summaries drop. On LongMemEval, raw-episodic recall scored 61% where belief-only
|
|
24
|
+
scored 17%.
|
|
25
|
+
- **Automatic capture and injection.** Claude Code hooks record messages and events, then
|
|
26
|
+
inject a query-ranked memory block (with an `⚠ MOST RELEVANT` headline) into every prompt.
|
|
27
|
+
You never have to remember to save anything.
|
|
28
|
+
- **Cost-gated consolidation.** An LLM distills sessions into beliefs only once enough new
|
|
29
|
+
memory accumulates. It can supersede, merge, and flag conflicts. Nothing is destructively
|
|
30
|
+
deleted.
|
|
31
|
+
- **Hybrid retrieval.** Lexical and semantic RRF fusion, MMR diversity, reinforcement, and
|
|
32
|
+
recency. Query embeddings are cached to disk, so repeat prompts cost nothing.
|
|
33
|
+
- **The Neural Universe.** A live monitor at `localhost:3737/monitor` renders every belief as
|
|
34
|
+
a star. Projects are galaxies, search makes matches flare, and autonomous curation pulses.
|
|
35
|
+
- **A daily self-audit (STL).** Peon checks itself every day: what it recorded, what it
|
|
36
|
+
injected, what failed, what consolidation did. Then it files a report with a verdict.
|
|
37
|
+
- **Eval-gated development.** A committed results ledger (git SHA, qrels, and brain
|
|
38
|
+
fingerprint per row) means retrieval changes are proven, not asserted. Negative results stay
|
|
39
|
+
documented.
|
|
40
|
+
- **Local-first and locked down.** Plain JSONL you can read, a loopback-only daemon with
|
|
41
|
+
DNS-rebinding protection, secret redaction at the injection boundary, and path-traversal
|
|
42
|
+
guards.
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
|
|
@@ -43,19 +48,20 @@ automatically, from a daemon that never leaves your machine.
|
|
|
43
48
|
*The live monitor: 18k real beliefs rendered as stars. Type to make matching beliefs flare; click one to inspect it.*
|
|
44
49
|
|
|
45
50
|

|
|
51
|
+
|
|
46
52
|
*Ask the field: typing "wulver cluster" makes 400+ matching beliefs flare while the rest dim, and the camera flies to them.*
|
|
47
53
|
|
|
48
54
|
## Why "Peon"?
|
|
49
55
|
|
|
50
|
-
The name comes from Indian offices. Every office had a **peon
|
|
56
|
+
The name comes from Indian offices. Every office had a **peon**, the person who walked desk to
|
|
51
57
|
desk all day: collect a file from this table, note who needs what, carry it to the next table,
|
|
52
|
-
remember where everything is.
|
|
53
|
-
|
|
58
|
+
remember where everything is. He wasn't the boss or the star, but the whole office quietly ran
|
|
59
|
+
on him. Nothing moved without him, and he never forgot where anything was.
|
|
54
60
|
|
|
55
|
-
That's
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
That's this framework, with AI. Peon walks between your sessions and your projects. It collects
|
|
62
|
+
what happened at one desk (a session), files it in the right cabinet (a project brain), carries
|
|
63
|
+
the relevant papers to the next desk before you ask (injection), and keeps the master ledger
|
|
64
|
+
upstairs (the global brain). Quiet clerk. Perfect memory. The office runs on him.
|
|
59
65
|
|
|
60
66
|
## How Peon differs from existing memory tools
|
|
61
67
|
|
|
@@ -71,9 +77,29 @@ ledger upstairs (the global brain). Quiet clerk. Perfect memory. The office runs
|
|
|
71
77
|
| Observability | **live Neural Universe monitor + daily self-audit (STL) + serve telemetry** | dashboard | — | — | — |
|
|
72
78
|
| Verification | **committed eval ledger; negative results kept** | vendor benchmarks | — | vendor benchmarks | — |
|
|
73
79
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
minutes, watch it think
|
|
80
|
+
The short version: mem0 and Zep are memory platforms for products you build. Peon is memory
|
|
81
|
+
for the coding agents you already use. It plugs into Claude Code or Codex in about five
|
|
82
|
+
minutes, and you can watch it think and audit every number it claims.
|
|
83
|
+
|
|
84
|
+
## Your agents share one brain
|
|
85
|
+
|
|
86
|
+
If you run more than one coding agent — Claude Code and Codex, say — they usually each live in
|
|
87
|
+
their own bubble. Whatever you work out with one is gone when you switch to the other.
|
|
88
|
+
|
|
89
|
+
Point them at the same project and Peon dissolves that wall. Both agents resolve to the same
|
|
90
|
+
`.peon/` brain (Peon canonicalizes the project path, so the hook, a direct MCP call, and Codex
|
|
91
|
+
all land on one store). So the memory flows between them:
|
|
92
|
+
|
|
93
|
+
- Codex works out how your build pipeline runs and records it. Next time you open Claude Code
|
|
94
|
+
in that repo, it's already in the injected context.
|
|
95
|
+
- Claude Code hits a gotcha and files it. Codex sees it the moment it calls `get_context` or
|
|
96
|
+
`search_memory`.
|
|
97
|
+
- The global brain sits above both, so your preferences and rules follow you into every agent,
|
|
98
|
+
in every project.
|
|
99
|
+
|
|
100
|
+
They aren't chatting in real time. It's a shared notebook both write in and both read from, so
|
|
101
|
+
a decision made in one agent shows up in the other without you re-explaining it. One project,
|
|
102
|
+
one memory, however many agents.
|
|
77
103
|
|
|
78
104
|
## Quickstart
|
|
79
105
|
|
|
@@ -91,12 +117,13 @@ npm install -g peon-mem && peon-mem install
|
|
|
91
117
|
The guided setup asks four things:
|
|
92
118
|
|
|
93
119
|
1. **Where your global brain lives** (default: `~/Library/Application Support/Peon`)
|
|
94
|
-
2. **Which LLM
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
120
|
+
2. **Which LLM**: OpenRouter (one key, any model) · OpenAI · Anthropic · **Ollama (100% local
|
|
121
|
+
& free)** · or skip
|
|
122
|
+
3. Installs the **daemon** as an auto-start service (launchd on macOS, systemd user unit on Linux)
|
|
123
|
+
4. **Detects your AI apps** and wires the MCP server (plus hooks for Claude Code) into the
|
|
124
|
+
ones you pick. Auto-configured: Claude Code, Claude Desktop, Codex, Gemini CLI, Cursor,
|
|
125
|
+
Windsurf, VS Code (Copilot MCP), Zed, LM Studio. Detected with in-app instructions: ChatGPT
|
|
126
|
+
Desktop, Perplexity Desktop. Every touched config gets a `.peon-backup`.
|
|
100
127
|
|
|
101
128
|
That builds the package, starts the daemon as a service, wires your Claude Code hooks + MCP
|
|
102
129
|
server (with a backup of your settings), and writes a config template. Then add your key to
|
|
@@ -127,25 +154,28 @@ node dist/daemon-cli.js # serves 127.0.0.1:3737
|
|
|
127
154
|
|
|
128
155
|
Then wire your agent (the installer prints these filled in for your paths):
|
|
129
156
|
|
|
130
|
-
- **Claude Code
|
|
157
|
+
- **Claude Code**: add the hook to `~/.claude/settings.json` (SessionStart / UserPromptSubmit /
|
|
131
158
|
SessionEnd → `scripts/claude-peon-hook.mjs`) and the MCP server (`dist/index.js`).
|
|
132
|
-
- **Codex / any MCP client
|
|
159
|
+
- **Codex / any MCP client**: register `dist/index.js` as a stdio MCP server; 16 tools
|
|
133
160
|
(`start_session`, `get_context`, `search_memory`, `record_message`, `process_memory`, …).
|
|
134
161
|
|
|
135
162
|
Open `http://127.0.0.1:3737/monitor` and watch your brain grow.
|
|
136
163
|
|
|
137
164
|
## How it works
|
|
138
165
|
|
|
139
|
-
1. **Record
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
166
|
+
1. **Record.** Hooks stream messages, events, and tool calls into `<project>/.peon/raw/`
|
|
167
|
+
(append-only).
|
|
168
|
+
2. **Consolidate.** Past a size gate, an LLM turns the session delta into typed belief records
|
|
169
|
+
in `.peon/brain/memories.jsonl` (importance/confidence scores, entities, provenance
|
|
170
|
+
pointers), reconciling against existing beliefs: supersede, merge, conflict-flag.
|
|
171
|
+
Recoverable, never deleted.
|
|
172
|
+
3. **Retrieve + inject.** On every prompt, beliefs are ranked (RRF lexical+semantic, MMR,
|
|
173
|
+
reinforcement) and injected alongside episodic verbatim matches and inherited global
|
|
174
|
+
beliefs.
|
|
175
|
+
4. **Self-curate.** A background brain pass reinforces recalled beliefs, compresses stale
|
|
176
|
+
clusters, and resolves duplicates. Every action is logged and undoable.
|
|
177
|
+
5. **Self-audit (STL).** A daily job reports what was recorded, what was injected, what went
|
|
178
|
+
wrong, and whether consolidation behaved, with serve-latency telemetry and a health verdict.
|
|
149
179
|
|
|
150
180
|
|
|
151
181
|
## Full install (copy-paste)
|
|
@@ -177,7 +207,9 @@ launchctl load ~/Library/LaunchAgents/com.peon.daemon.plist
|
|
|
177
207
|
curl http://127.0.0.1:3737/health # → {"ok":true}
|
|
178
208
|
```
|
|
179
209
|
|
|
180
|
-
Linux:
|
|
210
|
+
Linux: the installer writes `~/.config/systemd/user/peon-mem.service` and enables it. Manual
|
|
211
|
+
equivalent: `systemctl --user enable --now peon-mem.service`, plus `loginctl enable-linger $USER`
|
|
212
|
+
so it survives logout. (Linux support is young — [issue #1](https://github.com/VineetV2/peon-mem/issues/1) tracks real-machine reports.)
|
|
181
213
|
|
|
182
214
|
### 2. Claude Code — hooks (auto capture + injection)
|
|
183
215
|
|
|
@@ -214,7 +246,7 @@ args = ["/ABSOLUTE/PATH/TO/peon/dist/index.js"]
|
|
|
214
246
|
PEON_DAEMON_URL = "http://127.0.0.1:3737"
|
|
215
247
|
```
|
|
216
248
|
|
|
217
|
-
Codex has no hooks
|
|
249
|
+
Codex has no hooks, so add usage rules to `~/.codex/AGENTS.md` telling it to call
|
|
218
250
|
`start_session` + `get_context` at session start and `record_message` for durable facts
|
|
219
251
|
(example block in [docs/](docs/)).
|
|
220
252
|
|
|
@@ -225,8 +257,8 @@ curl "http://127.0.0.1:3737/context?projectPath=$PWD&query=test" # JSON contex
|
|
|
225
257
|
open http://127.0.0.1:3737/monitor # the Neural Universe
|
|
226
258
|
```
|
|
227
259
|
|
|
228
|
-
Start a Claude Code session in any project, say something decision-shaped, end the session
|
|
229
|
-
|
|
260
|
+
Start a Claude Code session in any project, say something decision-shaped, and end the session.
|
|
261
|
+
Within a minute the monitor shows the belief. The next session injects it.
|
|
230
262
|
|
|
231
263
|
### MCP tools exposed
|
|
232
264
|
|
|
@@ -245,17 +277,17 @@ launchctl unload ~/Library/LaunchAgents/com.peon.daemon.plist
|
|
|
245
277
|
|
|
246
278
|
## Troubleshooting / FAQ
|
|
247
279
|
|
|
248
|
-
- **No injection appearing?** `curl http://127.0.0.1:3737/health`; check hook is registered
|
|
249
|
-
(`claude
|
|
250
|
-
- **431 errors on huge prompts?** Handled
|
|
280
|
+
- **No injection appearing?** `curl http://127.0.0.1:3737/health`; check the hook is registered
|
|
281
|
+
(run any prompt in `claude`, then look at the monitor's Systems page for the request).
|
|
282
|
+
- **431 errors on huge prompts?** Handled. The hook caps the retrieval query at 2k chars.
|
|
251
283
|
- **No OpenRouter key?** Everything still runs; retrieval is lexical + episodic only
|
|
252
284
|
(semantic ranking and consolidation need a model). `PEON_EMBEDDING_MODE=ollama` works too.
|
|
253
|
-
- **Cost?** Consolidation is gated (default: fires per ~6k new chars,
|
|
254
|
-
flash-lite). Query embeddings are cached to disk
|
|
255
|
-
- **Multiple machines?** Brains are plain files in your repos
|
|
256
|
-
memory to travel (redact first: raw layer contains session text).
|
|
285
|
+
- **Cost?** Consolidation is gated (default: fires per ~6k new chars, around cents/day with
|
|
286
|
+
flash-lite). Query embeddings are cached to disk, so repeats are free.
|
|
287
|
+
- **Multiple machines?** Brains are plain files in your repos. Commit `.peon/` if you want
|
|
288
|
+
memory to travel (redact first: the raw layer contains session text).
|
|
257
289
|
- **Is my data sent anywhere?** Only consolidation/embedding calls to your configured model
|
|
258
|
-
provider. No telemetry, no cloud store.
|
|
290
|
+
provider. No telemetry, no cloud store. The daemon rejects non-loopback callers.
|
|
259
291
|
|
|
260
292
|
## Configuration (env)
|
|
261
293
|
|
|
@@ -275,55 +307,67 @@ launchctl unload ~/Library/LaunchAgents/com.peon.daemon.plist
|
|
|
275
307
|
|
|
276
308
|
## Project brains
|
|
277
309
|
|
|
278
|
-
- A brain lives in `<project>/.peon/`
|
|
279
|
-
your choice (`.gitignore` ships ignoring it).
|
|
280
|
-
- `.peon/root` marks a brain boundary. New brains are born rooted
|
|
281
|
-
swallow a project's memory.
|
|
310
|
+
- A brain lives in `<project>/.peon/` as human-readable JSONL and markdown. Commit it or
|
|
311
|
+
ignore it; your choice (`.gitignore` ships ignoring it).
|
|
312
|
+
- `.peon/root` marks a brain boundary. New brains are born rooted, so a parent directory can
|
|
313
|
+
never swallow a project's memory.
|
|
282
314
|
- The global brain lives in `~/Library/Application Support/Peon/global/` (macOS).
|
|
283
315
|
|
|
284
316
|
## Using Peon with NO AI at all
|
|
285
317
|
|
|
286
|
-
Some people want a memory system that never calls a model
|
|
287
|
-
embeddings
|
|
288
|
-
|
|
318
|
+
Some people want a memory system that never calls a model: no API keys, no local LLM, no
|
|
319
|
+
embeddings. Peon supports that. Pick **skip** in the install wizard, or set two env vars in
|
|
320
|
+
`<memory-home>/.env`:
|
|
289
321
|
|
|
290
322
|
```
|
|
291
323
|
PEON_AI_MODE=off
|
|
292
324
|
PEON_EMBEDDING_MODE=off
|
|
293
325
|
```
|
|
294
326
|
|
|
295
|
-
|
|
327
|
+
The pipeline looks like this without a model. Hooks still capture everything you do into
|
|
328
|
+
`<project>/.peon/raw/` as plain JSONL. As each event arrives, rule-based extraction files it
|
|
329
|
+
into readable brain files: a line that starts with "we decided" or "always use" lands in
|
|
330
|
+
`decisions.md`, preferences land in `preferences.md`, file paths land in `artifacts.md`. No
|
|
331
|
+
model reads it; it's pattern matching, and you can open the files to see exactly what it
|
|
332
|
+
caught. At your next session start, the hook asks the daemon for context, the daemon ranks
|
|
333
|
+
those records by keyword match, recency, and importance, and injects the best ones into your
|
|
334
|
+
prompt. The only step that disappears entirely is consolidation, the nightly LLM pass that
|
|
335
|
+
compresses raw history into deduplicated beliefs. Without it your memory is a well-indexed
|
|
336
|
+
journal instead of a distilled one. Search still finds things; nothing summarizes them.
|
|
296
337
|
|
|
297
|
-
|
|
338
|
+
What still works (all of it deterministic code, no model anywhere):
|
|
339
|
+
|
|
340
|
+
- **Capture.** Hooks record every prompt, tool call, and session event to plain JSONL in
|
|
298
341
|
`<project>/.peon/raw/`.
|
|
299
|
-
- **Real-time brain files
|
|
342
|
+
- **Real-time brain files.** Decisions, preferences, open questions, and artifacts are written
|
|
300
343
|
live to readable `.md` files by rule-based extraction as events arrive.
|
|
301
|
-
- **Injection
|
|
344
|
+
- **Injection.** Session-start context comes from those real-time files, query-focused and
|
|
302
345
|
budgeted, same as always.
|
|
303
|
-
- **Search
|
|
304
|
-
No embeddings needed
|
|
305
|
-
|
|
306
|
-
- **
|
|
346
|
+
- **Search.** Lexical retrieval (RRF over keyword rank, recency, importance, and type priors).
|
|
347
|
+
No embeddings needed. This is the same degrade path the semantic stack falls back to, so it
|
|
348
|
+
gets exercised constantly.
|
|
349
|
+
- **Episodic recall.** Verbatim what-was-said lookup is lexical by design, so it is unaffected.
|
|
350
|
+
- **Monitor UI, token tracking, cross-project search, backups.** All model-free.
|
|
307
351
|
|
|
308
|
-
|
|
352
|
+
What you give up: consolidation (raw events are never distilled into deduplicated beliefs, so
|
|
309
353
|
memory grows as an append-only journal), semantic search (paraphrased queries need shared
|
|
310
354
|
keywords), automatic entity extraction, and stale-shadow demotion (it compares embeddings).
|
|
311
355
|
|
|
312
|
-
|
|
356
|
+
Two escape hatches if you want curation without external AI:
|
|
313
357
|
|
|
314
|
-
1. `process_memory` accepts a pre-built `aiResult
|
|
358
|
+
1. `process_memory` accepts a pre-built `aiResult`. The coding agent you already run (Claude
|
|
315
359
|
Code, Codex) can do the distillation itself in-session and hand Peon the structured result.
|
|
316
360
|
Memory stays curated, and Peon itself never spends a token.
|
|
317
|
-
2. Everything is plain JSONL/Markdown on disk
|
|
318
|
-
monitor's memory endpoints
|
|
361
|
+
2. Everything is plain JSONL/Markdown on disk. You can edit beliefs by hand or through the
|
|
362
|
+
monitor's memory endpoints, and Peon backs up before every mutation.
|
|
319
363
|
|
|
320
364
|
## Measured: does memory actually save tokens?
|
|
321
365
|
|
|
322
|
-
A/B test
|
|
323
|
-
hooks active (memory injected at session start)
|
|
324
|
-
reading files
|
|
325
|
-
15 clean ON/OFF pairs survived tooling issues. Token
|
|
326
|
-
session transcripts.
|
|
366
|
+
A/B test with real `claude -p` sessions: one question per session, same repo, same model.
|
|
367
|
+
ON means Peon hooks active (memory injected at session start). OFF means `PEON_DISABLED=1`,
|
|
368
|
+
so the agent falls back to reading files. 20 questions across procedures, past results,
|
|
369
|
+
decisions, and current-state facts; 15 clean ON/OFF pairs survived tooling issues. Token
|
|
370
|
+
counts come from Claude Code's own session transcripts.
|
|
327
371
|
|
|
328
372
|
| paired, n=15/arm | ON (Peon) | OFF | delta |
|
|
329
373
|
|---|---|---|---|
|
|
@@ -332,38 +376,60 @@ session transcripts.
|
|
|
332
376
|
| cache-read tokens | 81.6k | 110.5k | 1.35× less |
|
|
333
377
|
| cheaper arm | **ON wins 12/15** | | |
|
|
334
378
|
|
|
335
|
-
Answer quality
|
|
336
|
-
1 both
|
|
337
|
-
conversation (a professor's citation policy from an email)
|
|
338
|
-
|
|
379
|
+
Answer quality, graded against repo ground truth: 8 ties, 1 clear Peon win, 5 baseline wins,
|
|
380
|
+
1 where both were weak. The Peon win is the interesting one. A rule that was only ever stated
|
|
381
|
+
in a conversation (a professor's citation policy, from an email) got *"no such rule found"*
|
|
382
|
+
from the baseline; Peon recited it exactly. Conversation-borne knowledge has no file to grep.
|
|
339
383
|
|
|
340
384
|
Honest caveats: the test repo has unusually good docs (a maintained research log), which makes
|
|
341
|
-
the baseline strong
|
|
342
|
-
known answers, not sampled from real usage.
|
|
343
|
-
superseded belief outranking the newer truth, and token rows lost when
|
|
344
|
-
the hook timeout
|
|
345
|
-
consolidation).
|
|
385
|
+
the baseline strong, and most repos aren't like that. n=15 is small. Questions were picked to
|
|
386
|
+
have known answers, not sampled from real usage. The test also exposed two real bugs in Peon
|
|
387
|
+
itself: a stale superseded belief outranking the newer truth, and token rows getting lost when
|
|
388
|
+
consolidation outlived the hook timeout. Both are fixed (stale-shadow demotion at retrieval;
|
|
389
|
+
usage logged before consolidation).
|
|
346
390
|
|
|
347
391
|
## Honesty section
|
|
348
392
|
|
|
349
|
-
Peon's development is eval-gated and keeps its negative results
|
|
350
|
-
built, measured (−2.9% Recall@10), and turned OFF by default. Consolidation is lossy by
|
|
351
|
-
that's why the episodic layer exists and is regression-tested. The eval harness
|
|
352
|
-
ledger (`npm run eval`) let you verify retrieval changes on your own brain.
|
|
393
|
+
Peon's development is eval-gated and keeps its negative results. An associative entity graph
|
|
394
|
+
was built, measured (−2.9% Recall@10), and turned OFF by default. Consolidation is lossy by
|
|
395
|
+
design; that's why the episodic layer exists and is regression-tested. The eval harness and
|
|
396
|
+
committed ledger (`npm run eval`) let you verify retrieval changes on your own brain.
|
|
353
397
|
|
|
354
398
|
## Security
|
|
355
399
|
|
|
356
|
-
-
|
|
400
|
+
- The daemon binds `127.0.0.1` only and rejects non-loopback `Host`/`Origin` headers
|
|
401
|
+
(DNS-rebinding defense).
|
|
357
402
|
- Secrets (API keys, tokens, JWTs) are redacted at the injection boundary.
|
|
358
|
-
- Path
|
|
359
|
-
before destructive-adjacent
|
|
403
|
+
- Path traversal is guarded. Writes use per-project locks and atomic tmp+rename, with
|
|
404
|
+
automatic backups before anything destructive-adjacent. Nothing is hard-deleted.
|
|
360
405
|
|
|
361
406
|
## License
|
|
362
407
|
|
|
363
408
|
MIT © Vineet Vora
|
|
364
409
|
|
|
365
|
-
## Contributing
|
|
410
|
+
## Contributing — I'd love your help
|
|
411
|
+
|
|
412
|
+
Peon is built and maintained by one person, and I'm open to help making it better. Issues,
|
|
413
|
+
PRs, ideas, criticism of the architecture, or just telling me where it broke on your machine:
|
|
414
|
+
all of it is useful.
|
|
415
|
+
|
|
416
|
+
Where help would matter most right now:
|
|
417
|
+
|
|
418
|
+
- **Windows & Linux support.** The daemon install is macOS launchd today. The systemd and Task
|
|
419
|
+
Scheduler equivalents need real users to test them.
|
|
420
|
+
- **More agent integrations.** The wizard covers 11 apps, but hooks-level capture (like the
|
|
421
|
+
Claude Code integration) for Codex, Cursor, and others would make memory richer everywhere.
|
|
422
|
+
- **Retrieval quality.** The eval harness (`npm run eval`) makes experiments cheap: better
|
|
423
|
+
consolidation prompts, smarter staleness handling, local embedding models worth defaulting
|
|
424
|
+
to.
|
|
425
|
+
- **Benchmarks.** Run the token A/B on your repo and share the numbers, especially where Peon
|
|
426
|
+
loses. Negative results are first-class here.
|
|
427
|
+
- **Docs and onboarding.** If the README or the wizard confused you, that confusion is a bug
|
|
428
|
+
report.
|
|
429
|
+
|
|
430
|
+
Open an issue at [github.com/VineetV2/peon-mem/issues](https://github.com/VineetV2/peon-mem/issues)
|
|
431
|
+
or send a PR directly.
|
|
366
432
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
433
|
+
Rules of the house: every retrieval/quality change ships with a test and an eval-ledger run
|
|
434
|
+
(`npm run eval`); negative results get documented, not deleted; nothing may hard-delete user
|
|
435
|
+
memory. `npm test` must stay green.
|
package/bin/peon-mem.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* peon-mem doctor health + config check
|
|
9
9
|
*/
|
|
10
10
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
11
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, rmSync } from "node:fs";
|
|
12
12
|
import { homedir, platform } from "node:os";
|
|
13
13
|
import { dirname, join } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
@@ -16,7 +16,8 @@ import { createInterface } from "node:readline/promises";
|
|
|
16
16
|
|
|
17
17
|
const PKG = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
18
18
|
const HOME = homedir();
|
|
19
|
-
|
|
19
|
+
// PEON_FORCE_PLATFORM lets CI and maintainers exercise the other OS's install path.
|
|
20
|
+
const MAC = (process.env.PEON_FORCE_PLATFORM || platform()) === "darwin";
|
|
20
21
|
const DEFAULT_HOME = MAC ? join(HOME, "Library", "Application Support", "Peon") : join(HOME, ".local", "share", "peon");
|
|
21
22
|
const PLIST = join(HOME, "Library", "LaunchAgents", "com.peon.daemon.plist");
|
|
22
23
|
const HOOK = join(PKG, "scripts", "claude-peon-hook.mjs");
|
|
@@ -156,8 +157,34 @@ function installService(memoryHome) {
|
|
|
156
157
|
execFileSync("launchctl", ["load", PLIST]);
|
|
157
158
|
});
|
|
158
159
|
} else {
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
// Linux: write a real systemd user unit instead of printing a recipe.
|
|
161
|
+
const unitDir = join(HOME, ".config", "systemd", "user");
|
|
162
|
+
const unitFile = join(unitDir, "peon-mem.service");
|
|
163
|
+
const unit = `[Unit]
|
|
164
|
+
Description=Peon memory daemon (local-first memory for AI coding agents)
|
|
165
|
+
After=network.target
|
|
166
|
+
|
|
167
|
+
[Service]
|
|
168
|
+
ExecStart=${NODE} ${DAEMON}
|
|
169
|
+
WorkingDirectory=${memoryHome}
|
|
170
|
+
Restart=always
|
|
171
|
+
RestartSec=2
|
|
172
|
+
|
|
173
|
+
[Install]
|
|
174
|
+
WantedBy=default.target
|
|
175
|
+
`;
|
|
176
|
+
act("daemon service (systemd user unit) → " + unitFile, () => {
|
|
177
|
+
mkdirSync(unitDir, { recursive: true });
|
|
178
|
+
writeFileSync(unitFile, unit);
|
|
179
|
+
// Enable + start when systemd is actually available; on failure fall back to instructions.
|
|
180
|
+
const r = spawnSync("systemctl", ["--user", "daemon-reload"], { stdio: "ignore" });
|
|
181
|
+
if (r.status === 0) {
|
|
182
|
+
spawnSync("systemctl", ["--user", "enable", "--now", "peon-mem.service"], { stdio: "ignore" });
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
log(" → if the daemon isn't running yet:");
|
|
186
|
+
log(" systemctl --user daemon-reload && systemctl --user enable --now peon-mem.service");
|
|
187
|
+
log(" loginctl enable-linger $USER # keeps it running after logout");
|
|
161
188
|
}
|
|
162
189
|
}
|
|
163
190
|
|
|
@@ -249,6 +276,11 @@ if (cmd === "install") {
|
|
|
249
276
|
rl?.close();
|
|
250
277
|
} else if (cmd === "uninstall") {
|
|
251
278
|
if (MAC && existsSync(PLIST)) act("stop + remove daemon service", () => spawnSync("launchctl", ["unload", PLIST], { stdio: "ignore" }));
|
|
279
|
+
const UNIT = join(HOME, ".config", "systemd", "user", "peon-mem.service");
|
|
280
|
+
if (!MAC && existsSync(UNIT)) act("stop + remove daemon service", () => {
|
|
281
|
+
spawnSync("systemctl", ["--user", "disable", "--now", "peon-mem.service"], { stdio: "ignore" });
|
|
282
|
+
rmSync(UNIT, { force: true });
|
|
283
|
+
});
|
|
252
284
|
const settings = join(HOME, ".claude", "settings.json");
|
|
253
285
|
try {
|
|
254
286
|
const s = JSON.parse(readFileSync(settings, "utf8"));
|
package/dist/brain.js
CHANGED
|
@@ -53,12 +53,25 @@ export function resolveConflicts(records, now, protectGlobalScope = true) {
|
|
|
53
53
|
// to active, loser archived (recoverable).
|
|
54
54
|
const candidates = records.filter((r) => r.status === "active" || r.status === "conflicted");
|
|
55
55
|
const conflicts = detectMemoryConflicts(candidates);
|
|
56
|
-
if (conflicts.length === 0)
|
|
57
|
-
return { records: [...records], actions: [] };
|
|
58
56
|
const byId = new Map(records.map((r) => [r.id, r]));
|
|
59
57
|
const archived = new Set();
|
|
60
58
|
const reactivated = new Set();
|
|
61
59
|
const actions = [];
|
|
60
|
+
// Every belief that is part of a CURRENTLY-detected conflict. Anything still flagged
|
|
61
|
+
// "conflicted" but not in this set is an orphan — the consolidator (or an older, looser
|
|
62
|
+
// detector) benched it, but it no longer collides with anything. Left alone it stays out of
|
|
63
|
+
// recall forever; the July→August backlog was 100+ such orphans. Reactivate them below.
|
|
64
|
+
const inLiveConflict = new Set();
|
|
65
|
+
for (const c of conflicts) {
|
|
66
|
+
inLiveConflict.add(c.leftId);
|
|
67
|
+
inLiveConflict.add(c.rightId);
|
|
68
|
+
}
|
|
69
|
+
for (const r of records) {
|
|
70
|
+
if (r.status === "conflicted" && !inLiveConflict.has(r.id)) {
|
|
71
|
+
reactivated.add(r.id);
|
|
72
|
+
actions.push({ type: "resolve_conflict", detail: `reactivated stale conflict flag on "${r.content.slice(0, 40)}"`, affectedIds: [r.id] });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
62
75
|
for (const conflict of conflicts) {
|
|
63
76
|
const left = byId.get(conflict.leftId);
|
|
64
77
|
const right = byId.get(conflict.rightId);
|
package/dist/entities.js
CHANGED
|
@@ -18,6 +18,25 @@
|
|
|
18
18
|
const SRC_ROOTS = new Set(["src", "lib", "scripts", "test", "tests", "app", "apps", "packages", "dist", "bin"]);
|
|
19
19
|
const FILE_EXT_RE = /\.(ts|tsx|js|jsx|mjs|cjs|json|md|mdx|html|css|scss|py|ipynb|pdf|txt|yml|yaml|toml|sh|sql|rs|go|java|rb|c|cpp|h)$/i;
|
|
20
20
|
const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*(?:[.#][A-Za-z_$][\w$]*)*$/;
|
|
21
|
+
// Bare common words that are never useful DOMAIN entities. Without this gate a lowercase token
|
|
22
|
+
// like "not" / "use" / "no" falls through the identifier branch and becomes a domain concept —
|
|
23
|
+
// then two unrelated beliefs that merely both contain that word "share an entity" and get
|
|
24
|
+
// false-flagged as conflicting. Only applies to plain lowercase words: file paths, product
|
|
25
|
+
// acronyms (BIRD, DTS-SQL), and code symbols carry caps/digits/separators and never land here.
|
|
26
|
+
const ENTITY_STOPWORDS = new Set([
|
|
27
|
+
"the", "this", "that", "these", "those", "a", "an", "it", "its", "we", "i", "you", "he", "she",
|
|
28
|
+
"they", "them", "our", "your", "their", "his", "her",
|
|
29
|
+
"if", "when", "then", "than", "for", "and", "but", "or", "nor", "so", "to", "in", "on", "of",
|
|
30
|
+
"at", "by", "as", "is", "are", "was", "were", "be", "been", "being", "do", "does", "did", "done",
|
|
31
|
+
"not", "no", "yes", "none", "null", "na", "nan", "true", "false",
|
|
32
|
+
"use", "used", "using", "avoid", "add", "added", "fix", "fixed", "make", "made", "set", "run",
|
|
33
|
+
"ran", "get", "got", "put", "new", "old", "now", "also", "with", "without", "from", "into",
|
|
34
|
+
"enable", "enabled", "disable", "disabled", "allow", "allowed", "deny", "forbidden",
|
|
35
|
+
"required", "optional", "about", "what", "which", "how", "why", "who", "where"
|
|
36
|
+
]);
|
|
37
|
+
function isEntityStopword(key) {
|
|
38
|
+
return ENTITY_STOPWORDS.has(key.toLowerCase());
|
|
39
|
+
}
|
|
21
40
|
/** Canonicalize one raw entity string. Returns null for junk (empty, too long, pure noise). */
|
|
22
41
|
export function canonicalizeEntity(raw) {
|
|
23
42
|
const s = (raw ?? "").trim().replace(/^[`'"]+|[`'"]+$/g, "").trim();
|
|
@@ -55,10 +74,15 @@ export function canonicalizeEntity(raw) {
|
|
|
55
74
|
return { key: s.toLowerCase(), name: s, kind: "concept", namespace: "domain" };
|
|
56
75
|
if (/[A-Z_]/.test(s.slice(1)))
|
|
57
76
|
return { key: s, name: s, kind: "symbol", namespace: "code" };
|
|
58
|
-
// lowercase single token (e.g. "vllm", "ollama") — treat as a domain concept
|
|
77
|
+
// lowercase single token (e.g. "vllm", "ollama") — treat as a domain concept, unless it's a
|
|
78
|
+
// bare common word ("not", "use", "no") that would only create noise and false conflicts.
|
|
79
|
+
if (isEntityStopword(s))
|
|
80
|
+
return null;
|
|
59
81
|
return { key: s.toLowerCase(), name: s, kind: "concept", namespace: "domain" };
|
|
60
82
|
}
|
|
61
83
|
// Multi-word phrase / proper noun → domain concept.
|
|
84
|
+
if (isEntityStopword(s))
|
|
85
|
+
return null;
|
|
62
86
|
return { key: s.toLowerCase(), name: s, kind: "concept", namespace: "domain" };
|
|
63
87
|
}
|
|
64
88
|
// Common capitalized words that START sentences / clauses — not domain entities.
|
package/dist/global-memory.js
CHANGED
|
@@ -9,7 +9,12 @@ export class PeonGlobalMemoryStore {
|
|
|
9
9
|
this.globalDir = globalDir;
|
|
10
10
|
}
|
|
11
11
|
static defaultDirectory() {
|
|
12
|
-
|
|
12
|
+
// PEON_GLOBAL_DIR relocates the global brain. Tests set it to a temp dir so they never read
|
|
13
|
+
// or write the real global store at ~/Library/Application Support/Peon/global — without this
|
|
14
|
+
// a tool created without an explicit globalMemoryDir falls back to the real store, which made
|
|
15
|
+
// cross-project isolation tests flaky and let test runs pollute the user's actual global brain.
|
|
16
|
+
const override = process.env.PEON_GLOBAL_DIR;
|
|
17
|
+
return override && override.trim() ? override.trim() : PeonGlobalMemoryStore.defaultGlobalDir;
|
|
13
18
|
}
|
|
14
19
|
static async open(options = {}) {
|
|
15
20
|
const store = new PeonGlobalMemoryStore(options.globalDir ?? PeonGlobalMemoryStore.defaultDirectory());
|
package/dist/quality.js
CHANGED
|
@@ -37,6 +37,13 @@ export function detectMemoryConflicts(records) {
|
|
|
37
37
|
const reason = opposingLanguageReason(left.content, right.content);
|
|
38
38
|
if (!reason)
|
|
39
39
|
continue;
|
|
40
|
+
// Same-topic gate. A shared entity + one opposing word-pair somewhere in two long beliefs
|
|
41
|
+
// is a weak signal: on a research brain, dozens of beliefs all mention "BIRD" and one says
|
|
42
|
+
// "use X" while an unrelated one says "avoid Y". They don't contradict — they're just both
|
|
43
|
+
// about BIRD. Require the two beliefs to actually be discussing the same thing before
|
|
44
|
+
// calling it a conflict: several shared entities, or real content overlap beyond the token.
|
|
45
|
+
if (!sameTopic(left, right))
|
|
46
|
+
continue;
|
|
40
47
|
conflicts.push({
|
|
41
48
|
entity,
|
|
42
49
|
leftId: left.id,
|
|
@@ -228,6 +235,47 @@ function sharedEntity(left, right) {
|
|
|
228
235
|
}
|
|
229
236
|
return undefined;
|
|
230
237
|
}
|
|
238
|
+
const TOPIC_STOP = new Set([
|
|
239
|
+
"the", "a", "an", "and", "or", "but", "to", "of", "in", "on", "for", "with", "is", "are", "was",
|
|
240
|
+
"were", "be", "as", "at", "by", "it", "this", "that", "we", "our", "use", "used", "using", "from",
|
|
241
|
+
"not", "no", "yes", "do", "does", "did", "so", "if", "then", "than", "when", "which", "what"
|
|
242
|
+
]);
|
|
243
|
+
/** Content tokens (lowercased, ≥3 chars, minus stopwords) — the topical fingerprint of a belief. */
|
|
244
|
+
function contentTokens(text) {
|
|
245
|
+
const out = new Set();
|
|
246
|
+
for (const raw of (text ?? "").toLowerCase().split(/[^a-z0-9]+/)) {
|
|
247
|
+
if (raw.length >= 3 && !TOPIC_STOP.has(raw))
|
|
248
|
+
out.add(raw);
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* True when two beliefs are actually discussing the same thing — the precondition for their
|
|
254
|
+
* opposing language to be a real contradiction rather than a coincidence. Satisfied by either
|
|
255
|
+
* multiple shared entities (a strong same-subject signal) or meaningful content overlap
|
|
256
|
+
* (Jaccard of content tokens above a floor). Prevents "both mention BIRD, one says use / one
|
|
257
|
+
* says avoid, about unrelated things" from being flagged.
|
|
258
|
+
*/
|
|
259
|
+
function sameTopic(left, right) {
|
|
260
|
+
const le = new Set(left.entities.map(normalizeEntity));
|
|
261
|
+
const re = new Set(right.entities.map(normalizeEntity));
|
|
262
|
+
let sharedEntities = 0;
|
|
263
|
+
for (const e of le)
|
|
264
|
+
if (re.has(e))
|
|
265
|
+
sharedEntities += 1;
|
|
266
|
+
if (sharedEntities >= 2)
|
|
267
|
+
return true;
|
|
268
|
+
const lt = contentTokens(left.content);
|
|
269
|
+
const rt = contentTokens(right.content);
|
|
270
|
+
if (lt.size === 0 || rt.size === 0)
|
|
271
|
+
return false;
|
|
272
|
+
let inter = 0;
|
|
273
|
+
for (const t of lt)
|
|
274
|
+
if (rt.has(t))
|
|
275
|
+
inter += 1;
|
|
276
|
+
const jaccard = inter / (lt.size + rt.size - inter);
|
|
277
|
+
return jaccard >= 0.25;
|
|
278
|
+
}
|
|
231
279
|
function opposingLanguageReason(left, right) {
|
|
232
280
|
const leftText = normalizeMemory(left);
|
|
233
281
|
const rightText = normalizeMemory(right);
|
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "peon-mem",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
],
|
|
37
37
|
"repository": {
|
|
38
38
|
"type": "git",
|
|
39
|
-
"url": "https://github.com/VineetV2/peon-mem"
|
|
39
|
+
"url": "git+https://github.com/VineetV2/peon-mem.git"
|
|
40
40
|
},
|
|
41
41
|
"engines": {
|
|
42
42
|
"node": ">=20"
|
|
@@ -53,5 +53,6 @@
|
|
|
53
53
|
"docs/assets",
|
|
54
54
|
"README.md",
|
|
55
55
|
"LICENSE"
|
|
56
|
-
]
|
|
56
|
+
],
|
|
57
|
+
"mcpName": "io.github.VineetV2/peon-mem"
|
|
57
58
|
}
|