peon-mem 1.0.2 → 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 CHANGED
@@ -48,6 +48,7 @@ prompt. It runs as a daemon on your machine, and nothing leaves it.
48
48
  *The live monitor: 18k real beliefs rendered as stars. Type to make matching beliefs flare; click one to inspect it.*
49
49
 
50
50
  ![Search flare — type a query and matching beliefs light up across every galaxy](docs/assets/search-flare.gif)
51
+
51
52
  *Ask the field: typing "wulver cluster" makes 400+ matching beliefs flare while the rest dim, and the camera flies to them.*
52
53
 
53
54
  ## Why "Peon"?
@@ -80,6 +81,26 @@ The short version: mem0 and Zep are memory platforms for products you build. Peo
80
81
  for the coding agents you already use. It plugs into Claude Code or Codex in about five
81
82
  minutes, and you can watch it think and audit every number it claims.
82
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.
103
+
83
104
  ## Quickstart
84
105
 
85
106
  Requirements: Node 20+, macOS or Linux. An [OpenRouter](https://openrouter.ai) API key is
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.
@@ -9,7 +9,12 @@ export class PeonGlobalMemoryStore {
9
9
  this.globalDir = globalDir;
10
10
  }
11
11
  static defaultDirectory() {
12
- return PeonGlobalMemoryStore.defaultGlobalDir;
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);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peon-mem",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -21,7 +21,7 @@
21
21
  "dependencies": {
22
22
  "@modelcontextprotocol/sdk": "^1.23.0"
23
23
  },
24
- "description": "Local-first hierarchical memory brain for AI coding agents \u2014 auto-capture, LLM consolidation into beliefs, hybrid retrieval, per-prompt injection, a living Neural Universe monitor, and a daily self-improvement loop. Works with Claude Code, Codex, and any MCP client.",
24
+ "description": "Local-first hierarchical memory brain for AI coding agents auto-capture, LLM consolidation into beliefs, hybrid retrieval, per-prompt injection, a living Neural Universe monitor, and a daily self-improvement loop. Works with Claude Code, Codex, and any MCP client.",
25
25
  "license": "MIT",
26
26
  "keywords": [
27
27
  "memory",
@@ -55,4 +55,4 @@
55
55
  "LICENSE"
56
56
  ],
57
57
  "mcpName": "io.github.VineetV2/peon-mem"
58
- }
58
+ }