mocode-ai 1.2.4 → 1.2.6

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.
@@ -0,0 +1,409 @@
1
+ // memory 知识图谱层(Tier-2):Graphiti 式时序三元组,纯 JSON 文件存储。
2
+ // 叶子模块:仅依赖 node 标准库 + tools/constants(常量叶子)+ store.ts 的类型,
3
+ // 与 store.ts 同风格:同步读写、整文件 tmp+rename 原子落盘、静默容错。
4
+ //
5
+ // 两文件(镜像 store.ts 的双 scope):
6
+ // 全局 ~/.mocode/memory-graph.json
7
+ // 项目 <cwd>/.mocode/memory-graph.json
8
+ // 文件形如 {"entities":[...],"edges":[...]}。scope 以所在文件为准(loadAllGraph 归一化)。
9
+ //
10
+ // 时序语义(抄 Graphiti/Zep 的核心思想,文件实现):边带 validAt/invalidAt。
11
+ // 新三元组与既有 active 边同(src,dst,relation)且 fact 不同 → 旧边 invalidAt=now(不删,可追溯);
12
+ // fact 相同 → 幂等跳过。查询默认只看 active(invalidAt==null)。
13
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
14
+ import os from 'node:os';
15
+ import path from 'node:path';
16
+ import { MAX_GRAPH_EDGES, MAX_GRAPH_ENTITIES } from '../tools/constants.js';
17
+ // ── 路径 / 原子写(同 store.ts 风格)──────────────────────────────────────
18
+ function globalGraphPath() {
19
+ return path.join(os.homedir(), '.mocode', 'memory-graph.json');
20
+ }
21
+ function projectGraphPath() {
22
+ return path.join(process.cwd(), '.mocode', 'memory-graph.json');
23
+ }
24
+ function graphPathForScope(scope) {
25
+ return scope === 'global' ? globalGraphPath() : projectGraphPath();
26
+ }
27
+ function ensureDir(p) {
28
+ const dir = path.dirname(p);
29
+ if (!existsSync(dir))
30
+ mkdirSync(dir, { recursive: true });
31
+ }
32
+ function writeGraphAtomic(p, g) {
33
+ const tmp = p + '.tmp';
34
+ writeFileSync(tmp, JSON.stringify(g, null, 2), 'utf8');
35
+ renameSync(tmp, p);
36
+ }
37
+ function readGraphFile(p) {
38
+ if (!existsSync(p))
39
+ return { entities: [], edges: [] };
40
+ try {
41
+ const raw = readFileSync(p, 'utf8');
42
+ if (!raw.trim())
43
+ return { entities: [], edges: [] };
44
+ const obj = JSON.parse(raw);
45
+ return {
46
+ entities: Array.isArray(obj.entities) ? obj.entities : [],
47
+ edges: Array.isArray(obj.edges) ? obj.edges : [],
48
+ };
49
+ }
50
+ catch {
51
+ return { entities: [], edges: [] }; // 损坏文件不连累全局
52
+ }
53
+ }
54
+ function nowIso() {
55
+ return new Date().toISOString();
56
+ }
57
+ let entCounter = 0;
58
+ /** 实体名 → ASCII slug;纯 CJK 等空结果用 ent 前缀兜底(同 store.ts slugify 思路)。 */
59
+ function entitySlug(name) {
60
+ const s = name
61
+ .toLowerCase()
62
+ .replace(/[^a-z0-9]+/g, '-')
63
+ .replace(/^-+|-+$/g, '');
64
+ if (s)
65
+ return s;
66
+ entCounter++;
67
+ return 'ent-' + Date.now().toString(36) + entCounter.toString(36);
68
+ }
69
+ let edgeCounter = 0;
70
+ function nextEdgeId() {
71
+ edgeCounter++;
72
+ return 'edge-' + Date.now().toString(36) + edgeCounter.toString(36);
73
+ }
74
+ /** 读两文件并按文件归一化 scope。 */
75
+ export function loadAllGraph() {
76
+ const out = { entities: [], edges: [] };
77
+ for (const scope of ['global', 'project']) {
78
+ const g = readGraphFile(graphPathForScope(scope));
79
+ for (const e of g.entities) {
80
+ e.scope = scope;
81
+ out.entities.push(e);
82
+ }
83
+ for (const e of g.edges) {
84
+ e.scope = scope;
85
+ out.edges.push(e);
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+ function writeGraphForScope(scope, g) {
91
+ const p = graphPathForScope(scope);
92
+ ensureDir(p);
93
+ writeGraphAtomic(p, { entities: g.entities, edges: g.edges });
94
+ }
95
+ /** 单 scope 的完整图(读文件,scope 归一化)。 */
96
+ function loadScopeGraph(scope) {
97
+ const g = readGraphFile(graphPathForScope(scope));
98
+ for (const e of g.entities)
99
+ e.scope = scope;
100
+ for (const e of g.edges)
101
+ e.scope = scope;
102
+ return g;
103
+ }
104
+ // ── 实体 ─────────────────────────────────────────────────────────────────
105
+ const norm = (s) => s.trim().toLowerCase();
106
+ function findEntityIn(g, name) {
107
+ const q = norm(name);
108
+ if (!q)
109
+ return undefined;
110
+ return g.entities.find((e) => e.id === q || norm(e.name) === q || e.aliases.some((a) => norm(a) === q));
111
+ }
112
+ /** 全局找实体(两 scope,project 优先——项目事实比全局更具体)。 */
113
+ export function findEntity(name) {
114
+ const proj = findEntityIn(loadScopeGraph('project'), name);
115
+ if (proj)
116
+ return proj;
117
+ return findEntityIn(loadScopeGraph('global'), name);
118
+ }
119
+ /**
120
+ * upsert 实体:按 name/alias/id 匹配,命中则合并 alias/summary;未命中建新。
121
+ * scope 容量保护:超限先清孤儿实体(无任何 active 边相连);仍超 → 拒绝新实体。
122
+ */
123
+ export function upsertEntity(name, opts = {}) {
124
+ const scope = opts.scope === 'global' ? 'global' : 'project';
125
+ const trimmed = name.trim();
126
+ if (!trimmed)
127
+ return { id: '', created: false, rejected: 'empty-name' };
128
+ const g = loadScopeGraph(scope);
129
+ const hit = findEntityIn(g, trimmed);
130
+ if (hit) {
131
+ let dirty = false;
132
+ if (opts.alias && !hit.aliases.some((a) => norm(a) === norm(opts.alias))) {
133
+ hit.aliases.push(opts.alias.trim());
134
+ dirty = true;
135
+ }
136
+ if (opts.summary && opts.summary.trim() && opts.summary.trim() !== hit.summary) {
137
+ hit.summary = opts.summary.trim();
138
+ dirty = true;
139
+ }
140
+ if (dirty) {
141
+ hit.updatedAt = nowIso();
142
+ writeGraphForScope(scope, g);
143
+ }
144
+ return { id: hit.id, created: false };
145
+ }
146
+ // 新建:容量保护
147
+ if (g.entities.length >= MAX_GRAPH_ENTITIES) {
148
+ const linked = new Set();
149
+ for (const e of g.edges) {
150
+ if (!e.invalidAt) {
151
+ linked.add(e.src);
152
+ linked.add(e.dst);
153
+ }
154
+ }
155
+ g.entities = g.entities.filter((e) => linked.has(e.id));
156
+ if (g.entities.length >= MAX_GRAPH_ENTITIES) {
157
+ return { id: '', created: false, rejected: 'entity-cap' };
158
+ }
159
+ }
160
+ const now = nowIso();
161
+ const ent = {
162
+ id: entitySlug(trimmed),
163
+ name: trimmed,
164
+ aliases: opts.alias && norm(opts.alias) !== norm(trimmed) ? [opts.alias.trim()] : [],
165
+ summary: opts.summary?.trim() ?? '',
166
+ createdAt: now,
167
+ updatedAt: now,
168
+ scope,
169
+ };
170
+ // slug 碰撞兜底(两个不同名字 slug 相同)
171
+ while (g.entities.some((e) => e.id === ent.id))
172
+ ent.id += 'x';
173
+ g.entities.push(ent);
174
+ writeGraphForScope(scope, g);
175
+ return { id: ent.id, created: true };
176
+ }
177
+ /**
178
+ * 写一条三元组。src/dst 自动 upsert 为实体。
179
+ * 冲突策略:同 scope 内已有 active 边同(src,dst,relation):
180
+ * - fact 相同 → 幂等跳过(duplicate);
181
+ * - fact 不同 → 旧边 invalidAt=now(时序失效),新边入库。
182
+ * 容量保护:边超限先清已失效边;仍超 → 拒绝。
183
+ */
184
+ export function addTriple(input) {
185
+ const src = input.src?.trim();
186
+ const dst = input.dst?.trim();
187
+ const relation = input.relation?.trim().toLowerCase().replace(/\s+/g, '_');
188
+ if (!src || !dst || !relation)
189
+ return { ok: false, reason: 'missing src/relation/dst' };
190
+ if (src.length > 80 || dst.length > 80 || relation.length > 60) {
191
+ return { ok: false, reason: 'src/dst/relation too long' };
192
+ }
193
+ const scope = input.scope === 'global' ? 'global' : 'project';
194
+ const s = upsertEntity(src, { scope });
195
+ if (s.rejected)
196
+ return { ok: false, reason: `src entity: ${s.rejected}` };
197
+ const d = upsertEntity(dst, { scope });
198
+ if (d.rejected)
199
+ return { ok: false, reason: `dst entity: ${d.rejected}` };
200
+ const g = loadScopeGraph(scope);
201
+ const fact = (input.fact ?? '').trim();
202
+ const clash = g.edges.find((e) => !e.invalidAt && e.src === s.id && e.dst === d.id && e.relation === relation);
203
+ if (clash) {
204
+ if (!fact || clash.fact === fact) {
205
+ return { ok: true, edgeId: clash.id, superseded: 0, duplicate: true };
206
+ }
207
+ clash.invalidAt = nowIso(); // 时序失效,保留可追溯
208
+ }
209
+ // 容量保护:先清失效边
210
+ if (g.edges.length >= MAX_GRAPH_EDGES) {
211
+ g.edges = g.edges.filter((e) => !e.invalidAt);
212
+ if (g.edges.length >= MAX_GRAPH_EDGES) {
213
+ return { ok: false, reason: 'edge-cap' };
214
+ }
215
+ }
216
+ const edge = {
217
+ id: nextEdgeId(),
218
+ src: s.id,
219
+ dst: d.id,
220
+ relation,
221
+ fact,
222
+ validAt: nowIso(),
223
+ invalidAt: null,
224
+ sourceEntry: input.sourceEntry ?? null,
225
+ scope,
226
+ };
227
+ g.edges.push(edge);
228
+ writeGraphForScope(scope, g);
229
+ return { ok: true, edgeId: edge.id, superseded: clash ? 1 : 0 };
230
+ }
231
+ /** 实体关键词搜索(多词子串,name/alias/id 加权),返回命中实体 + 相连 active 边。 */
232
+ export function searchGraph(query, limit = 8) {
233
+ const g = loadAllGraph();
234
+ const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
235
+ const scored = g.entities
236
+ .map((e) => {
237
+ let sc = 0;
238
+ const name = e.name.toLowerCase();
239
+ const id = e.id.toLowerCase();
240
+ const aliases = e.aliases.map((a) => a.toLowerCase());
241
+ for (const t of terms) {
242
+ if (id.includes(t))
243
+ sc += 8;
244
+ if (name.includes(t))
245
+ sc += 10;
246
+ if (aliases.some((a) => a.includes(t)))
247
+ sc += 6;
248
+ if (e.summary.toLowerCase().includes(t))
249
+ sc += 2;
250
+ }
251
+ return { e, sc };
252
+ })
253
+ .filter((x) => (terms.length === 0 ? true : x.sc > 0))
254
+ .sort((a, b) => b.sc - a.sc)
255
+ .slice(0, Math.max(1, Math.min(limit, 20)));
256
+ const hitIds = new Set(scored.map((x) => x.e.id));
257
+ const edges = g.edges.filter((e) => !e.invalidAt && (hitIds.has(e.src) || hitIds.has(e.dst)));
258
+ return { entities: scored.map((x) => x.e), edges };
259
+ }
260
+ function buildAdjacency(g, relation) {
261
+ const adj = new Map();
262
+ for (const e of g.edges) {
263
+ if (e.invalidAt)
264
+ continue;
265
+ if (relation && e.relation !== relation)
266
+ continue;
267
+ const a = adj.get(e.src);
268
+ if (a)
269
+ a.push(e);
270
+ else
271
+ adj.set(e.src, [e]);
272
+ const b = adj.get(e.dst);
273
+ if (b)
274
+ b.push(e);
275
+ else
276
+ adj.set(e.dst, [e]);
277
+ }
278
+ return adj;
279
+ }
280
+ /**
281
+ * 邻居遍历:depth 1-3(BFS + 邻接表 O(V+E)),只看 active 边,边数封顶 40 防图爆炸。
282
+ * relation 可选:只沿该类型的边走(深跳聚焦用,如沿 depends_on 链追踪)。
283
+ */
284
+ export function neighborsOf(nameOrId, depth = 1, relation) {
285
+ const g = loadAllGraph();
286
+ const center = findEntityIn(g, nameOrId) ?? g.entities.find((e) => e.id === nameOrId);
287
+ if (!center)
288
+ return { center: null, entities: [], edges: [], truncated: false };
289
+ const rel = relation?.trim().toLowerCase().replace(/\s+/g, '_') || undefined;
290
+ const maxDepth = Math.max(1, Math.min(Math.floor(depth) || 1, 3));
291
+ const MAX_EDGES_OUT = 40;
292
+ const adj = buildAdjacency(g, rel);
293
+ const visited = new Set([center.id]);
294
+ const seenEdges = new Set();
295
+ const outEdges = [];
296
+ let frontier = [center.id];
297
+ let truncated = false;
298
+ outer: for (let d = 0; d < maxDepth; d++) {
299
+ const next = [];
300
+ for (const id of frontier) {
301
+ for (const e of adj.get(id) ?? []) {
302
+ if (seenEdges.has(e.id))
303
+ continue;
304
+ seenEdges.add(e.id);
305
+ if (outEdges.length >= MAX_EDGES_OUT) {
306
+ truncated = true;
307
+ break outer; // 截断立即退出,不再空扫剩余边
308
+ }
309
+ outEdges.push(e);
310
+ const other = e.src === id ? e.dst : e.src;
311
+ if (!visited.has(other)) {
312
+ visited.add(other);
313
+ next.push(other);
314
+ }
315
+ }
316
+ }
317
+ frontier = next;
318
+ }
319
+ const entities = g.entities.filter((e) => visited.has(e.id) && e.id !== center.id);
320
+ return { center, entities, edges: outEdges, truncated };
321
+ }
322
+ const MAX_PATH_DEPTH = 6;
323
+ /**
324
+ * 两实体间最短路径:active 边视为无向,双向 BFS(每次展开较小前沿),
325
+ * 总跳数封顶 MAX_PATH_DEPTH 防图爆炸。不连通 / 未知端 → null。
326
+ */
327
+ export function pathBetween(aNameOrId, bNameOrId) {
328
+ const g = loadAllGraph();
329
+ const from = findEntityIn(g, aNameOrId) ?? g.entities.find((e) => e.id === aNameOrId);
330
+ const to = findEntityIn(g, bNameOrId) ?? g.entities.find((e) => e.id === bNameOrId);
331
+ if (!from || !to)
332
+ return null;
333
+ if (from.id === to.id)
334
+ return { from, to, path: [from], edges: [] };
335
+ const fwd = new Map([[from.id, { prev: null, edge: null }]]);
336
+ const bwd = new Map([[to.id, { prev: null, edge: null }]]);
337
+ let fFront = [from.id];
338
+ let bFront = [to.id];
339
+ const adj = buildAdjacency(g);
340
+ let meet = null;
341
+ for (let d = 0; d < MAX_PATH_DEPTH && !meet; d++) {
342
+ const expandFwd = fFront.length <= bFront.length;
343
+ const mine = expandFwd ? fwd : bwd;
344
+ const theirs = expandFwd ? bwd : fwd;
345
+ const frontier = expandFwd ? fFront : bFront;
346
+ const next = [];
347
+ for (const id of frontier) {
348
+ for (const e of adj.get(id) ?? []) {
349
+ const other = e.src === id ? e.dst : e.src;
350
+ if (mine.has(other))
351
+ continue;
352
+ mine.set(other, { prev: id, edge: e });
353
+ next.push(other);
354
+ if (theirs.has(other)) {
355
+ meet = other;
356
+ break;
357
+ }
358
+ }
359
+ if (meet)
360
+ break;
361
+ }
362
+ if (expandFwd)
363
+ fFront = next;
364
+ else
365
+ bFront = next;
366
+ }
367
+ if (!meet)
368
+ return null;
369
+ // 重组:fwd 链 from→…→meet,再接 bwd 链 meet→…→to
370
+ const rev = [];
371
+ for (let cur = meet; cur; cur = fwd.get(cur)?.prev ?? null)
372
+ rev.push(cur);
373
+ rev.reverse(); // [from, ..., meet]
374
+ const pathIds = [...rev];
375
+ const pathEdges = [];
376
+ for (const id of rev.slice(1)) {
377
+ const e = fwd.get(id)?.edge;
378
+ if (e)
379
+ pathEdges.push(e);
380
+ }
381
+ for (let cur = meet;;) {
382
+ const t = bwd.get(cur);
383
+ if (!t?.prev)
384
+ break;
385
+ if (t.edge)
386
+ pathEdges.push(t.edge);
387
+ pathIds.push(t.prev);
388
+ cur = t.prev;
389
+ }
390
+ const byId = new Map();
391
+ for (const e of g.entities)
392
+ if (!byId.has(e.id))
393
+ byId.set(e.id, e);
394
+ const path = pathIds.map((id) => byId.get(id)).filter((e) => !!e);
395
+ return { from, to, path, edges: pathEdges };
396
+ }
397
+ export function graphStats() {
398
+ const g = loadAllGraph();
399
+ const count = (scope) => ({
400
+ entities: g.entities.filter((e) => e.scope === scope).length,
401
+ edges: g.edges.filter((e) => e.scope === scope && !e.invalidAt).length,
402
+ });
403
+ return {
404
+ entities: g.entities.length,
405
+ edgesActive: g.edges.filter((e) => !e.invalidAt).length,
406
+ edgesInvalid: g.edges.filter((e) => !!e.invalidAt).length,
407
+ byScope: { project: count('project'), global: count('global') },
408
+ };
409
+ }
@@ -1,5 +1,7 @@
1
- // Memory barrel: Tier-2 JSONL store + background reflection.
2
- // MOCODE.md is intentionally not loaded here: the system prompt only tells the agent
3
- // to read the workspace file on demand, keeping its full body out of every request.
1
+ // Memory barrel: Tier-2 JSONL store + knowledge-graph layer + background reflection.
2
+ // AGENTS.md is intentionally not loaded here either: config/index.ts
3
+ // (buildAgentsImportSection) auto-imports the workspace-root AGENTS.md body into the
4
+ // system prompt — independent of the memory switch — truncating it when it exceeds the cap.
4
5
  export { buildMemoryIndexSection, loadAll, gcMemories, } from './store.js';
6
+ export { addTriple, upsertEntity, findEntity, searchGraph, neighborsOf, pathBetween, graphStats, } from './graph.js';
5
7
  export { kickoffReflection, drainMemoryBackground, getLastReflectResult, clearLastReflectResult, snapshotTranscript, formatReflectResult, runReflection, } from './reflect.js';
@@ -13,6 +13,7 @@ import path from 'node:path';
13
13
  import { chat } from '../llm/index.js';
14
14
  import { config, isMemoryEnabled } from '../config/index.js';
15
15
  import { saveEntry, updateEntry, forgetEntry, loadAll, gcMemories, } from './store.js';
16
+ import { addTriple } from './graph.js';
16
17
  // ── 日志(静默容错,裁尾保最近)─────────────────────────────────────────────
17
18
  function logPath() {
18
19
  return path.join(process.cwd(), '.mocode', 'memory.log');
@@ -93,15 +94,16 @@ function buildMemorySample() {
93
94
  }
94
95
  const TYPES = 'decision | fact | pitfall | reference | feedback';
95
96
  const REFLECT_SYS = `You are mocode's memory reflector. Review the recent session and existing memories, producing **only** updates worth remembering long-term.
96
- Output strictly JSON (no markdown code blocks, no explanatory text): {"saves":[{"type":"...","name":"...","summary":"...","body":"..."}],"updates":[{"id":"...","reason":"...","summary":"...","body":"..."}],"forgets":[{"id":"...","reason":"..."}]}
97
- Empty arrays are valid (if nothing is worth saving, all three arrays are empty).
97
+ Output strictly JSON (no markdown code blocks, no explanatory text): {"saves":[{"type":"...","name":"...","summary":"...","body":"..."}],"updates":[{"id":"...","reason":"...","summary":"...","body":"..."}],"forgets":[{"id":"...","reason":"..."}],"triples":[{"src":"...","relation":"...","dst":"...","fact":"..."}]}
98
+ Empty arrays are valid (if nothing is worth saving, all arrays are empty).
98
99
  Rules:
99
100
  ① Only store non-obvious, cross-session-useful facts/decisions/pitfalls; do not store current bugs, temp files, undecided TODOs, or volatile items;
100
101
  ② Better to store less than to store trivially correct info (e.g. "keep it concise");
101
102
  ③ ids in updates/forgets must come from the "existing memories" list below; do not fabricate ids not listed there;
102
103
  ④ names in saves must be concise and not collide with existing ones; type ∈ {${TYPES}};
103
104
  ⑤ If an existing memory contradicts new facts or is outdated, update the old entry (modify summary/body) rather than creating a duplicate;
104
- ⑥ forgets are for memories clearly stale / superseded by a new entry (archive, not hard-delete).`;
105
+ ⑥ forgets are for memories clearly stale / superseded by a new entry (archive, not hard-delete);
106
+ ⑦ triples are knowledge-graph facts distilled from this session: concise entity names (lowercase snake_case or proper nouns), relation in snake_case (e.g. depends_on, decided_by, implemented_in, conflicts_with), plus a one-line fact. Only emit triples that are stable, non-obvious and cross-session-useful (2-6 at most); they may reference entities from saves/updates or existing memories.`;
105
107
  const REFLECT_USER = (transcript, sample) => `## Recent session\n${transcript}\n\n## Existing memories\n${sample}\n\nProduce JSON:`;
106
108
  function parsePlan(content) {
107
109
  if (!content)
@@ -141,6 +143,7 @@ export async function runReflection(transcript, signal) {
141
143
  saves: 0,
142
144
  updates: 0,
143
145
  forgets: 0,
146
+ triples: 0,
144
147
  gcDecayed: 0,
145
148
  gcCapped: 0,
146
149
  gcGced: 0,
@@ -200,8 +203,24 @@ export async function runReflection(transcript, signal) {
200
203
  forgets++;
201
204
  }
202
205
  }
206
+ // 知识图谱三元组:容错——单项失败跳过,不影响 saves/updates/forgets 已落地的结果。
207
+ let triples = 0;
208
+ if (Array.isArray(plan.triples)) {
209
+ for (const t of plan.triples.slice(0, 10)) {
210
+ if (!t?.src || !t?.relation || !t?.dst)
211
+ continue;
212
+ const r = addTriple({
213
+ src: String(t.src),
214
+ relation: String(t.relation),
215
+ dst: String(t.dst),
216
+ fact: t.fact ? String(t.fact) : undefined,
217
+ });
218
+ if (r.ok && !r.duplicate)
219
+ triples++;
220
+ }
221
+ }
203
222
  const gc = gcMemories();
204
- result = { ...result, saves, updates, forgets, gcDecayed: gc.decayed, gcCapped: gc.capped, gcGced: gc.gced };
223
+ result = { ...result, saves, updates, forgets, triples, gcDecayed: gc.decayed, gcCapped: gc.capped, gcGced: gc.gced };
205
224
  return result;
206
225
  }
207
226
  function normalizeType(t) {
@@ -215,9 +234,9 @@ function normalizeType(t) {
215
234
  // ── 后台编排:kickoff / drain / 缓存 ─────────────────────────────────────────
216
235
  let inflight = null;
217
236
  let lastReflectResult = null;
218
- /** 摘要串(供 repl flush):存N 改N 忘N;有错误附上。 */
237
+ /** 摘要串(供 repl flush):存N 改N 忘N 图N;有错误附上。 */
219
238
  export function formatReflectResult(r) {
220
- const parts = [`存${r.saves}`, `改${r.updates}`, `忘${r.forgets}`];
239
+ const parts = [`存${r.saves}`, `改${r.updates}`, `忘${r.forgets}`, `图${r.triples}`];
221
240
  if (r.gcDecayed || r.gcCapped || r.gcGced) {
222
241
  parts.push(`遗忘(衰减${r.gcDecayed}/封顶${r.gcCapped}/清除${r.gcGced})`);
223
242
  }
@@ -1,7 +1,7 @@
1
1
  import readline from 'node:readline/promises';
2
2
  import { emitKeypressEvents } from 'node:readline';
3
3
  import { stdin, stdout } from 'node:process';
4
- import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isSubAgentEnabled, updateSubAgentConfig, isFrontendToolsEnabled, updateFrontendToolsConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, hasCodegraphIndex, reinjectActivePlanIntoSystem, DEFAULT_CONTEXT_WINDOW_TOKENS, } from '../config/index.js';
4
+ import { config, updateModelConfig, isModelConfigured, updateMemoryConfig, isMemoryEnabled, isSubAgentEnabled, updateSubAgentConfig, isFrontendToolsEnabled, updateFrontendToolsConfig, updateLanguageConfig, languageFromShell, buildBasePrompt, getPlanModeSuffix, hasCodegraphIndex, reinjectSessionStateIntoSystem, DEFAULT_CONTEXT_WINDOW_TOKENS, } from '../config/index.js';
5
5
  import { getLanguage, normalizeLanguage, t, } from '../i18n/index.js';
6
6
  import { DEFAULT_BUDGET_POLICY } from '../context/budget.js';
7
7
  import { updateConfigKey, writeConfigKeys, CONFIG_PATH } from '../config/file.js';
@@ -66,9 +66,9 @@ function buildSlashCommands() {
66
66
  { name: 'off', value: '/memory_switch off', desc: d('commands.memoryOff') },
67
67
  { name: 'status', value: '/memory_status', desc: d('commands.memoryStatus') },
68
68
  { name: 'reflect', value: '/reflect', desc: d('commands.memoryReflect') },
69
- { name: 'init', value: '/init', desc: d('commands.memoryInit') },
70
69
  ],
71
70
  },
71
+ { name: '/init', desc: d('commands.memoryInit') },
72
72
  {
73
73
  name: '/subagent', desc: d('commands.subagent'), children: [
74
74
  { name: 'on', value: '/subagent on', desc: d('commands.subagentOn') },
@@ -173,7 +173,7 @@ function maskKey(k) {
173
173
  return `${'='.repeat(Math.min(k.length - 4, 20))}${k.slice(-4)}`;
174
174
  }
175
175
  /**
176
- * /init 指令:发给 agent 扫描项目并生成 MOCODE.md。已存在则让 agent 读后更新(不丢失事实)。写完供 memory 子系统下轮加载。
176
+ * /init 指令:发给 agent 扫描项目并生成 AGENTS.md。已存在则让 agent 读后更新(不丢失事实)。写完供 memory 子系统下轮加载。
177
177
  *
178
178
  * 函数化(非 const):.codegraph/ 索引是否存在的探测放在调用瞬间,没索引时不提 codegraph,
179
179
  * 避免 LLM 调出失败。/init 是冷启动动作,IO 开销可忽略。
@@ -182,14 +182,14 @@ function buildInitPrompt() {
182
182
  const cg = hasCodegraphIndex()
183
183
  ? '- 若有 .codegraph/:用 use_skill 加载 codegraph skill 后用 run_command 调 codegraph explore "<架构或入口符号>" 一次拿相关源码+调用路径,别逐文件读!!!\n'
184
184
  : '';
185
- return `分析当前项目(process.cwd()),生成 MOCODE.md 项目记忆文件,供 mocode 后续会话自动加载——目标是让后续会话无需重新摸索就能上手。
185
+ return `分析当前项目(process.cwd()),生成 AGENTS.md 项目记忆文件,供 mocode 后续会话自动加载——目标是让后续会话无需重新摸索就能上手。
186
186
 
187
187
  先探查(尽量少调用拿全貌):
188
188
  ${cg}- read_file package.json(或 Cargo.toml/pyproject.toml/go.mod 等):scripts、依赖、入口、模块类型。
189
189
  - glob 顶层目录;read_file 入口文件 + 各子系统 index.ts/README。
190
- - 若 MOCODE.md 已存在:read_file 读它,在其基础上更新(补缺、修正过时),不丢已有准确事实。
190
+ - 若 AGENTS.md 已存在:read_file 读它,在其基础上更新(补缺、修正过时),不丢已有准确事实。
191
191
 
192
- MOCODE.md 按以下结构写(每节简短,只写稳定、非显然的事实):
192
+ AGENTS.md 按以下结构写(每节简短,只写稳定、非显然的事实):
193
193
  ## 项目
194
194
  一两句:是什么、技术栈、运行环境。
195
195
  ## 命令
@@ -204,7 +204,7 @@ install / dev / build / test / typecheck / lint 等——从 package.json script
204
204
  硬要求:
205
205
  - 从实际代码提炼,引用具体文件名/命令/符号;不编造、不泛泛。
206
206
  - 总长 ≤ 3000 字;只写后续会话有用的稳定事实,不写易变项(当前 bug、临时文件、未决 TODO)。
207
- - 用 write_file 写入项目根 MOCODE.md。
207
+ - 用 write_file 写入项目根 AGENTS.md。
208
208
  - 写完简述:写了哪几节 + 从代码里发现的 2-3 条非显然关键约定(供用户校验)。`;
209
209
  }
210
210
  /** 临时 readline 读一行(cooked,用于子提问;主输入走 promptWithSlashMenu)。 */
@@ -768,7 +768,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
768
768
  //
769
769
  // 与开关联动:① base 用 buildBasePrompt() 取代 config.systemPrompt(后者是启动时一次性
770
770
  // 求值的常量,运行时 /memory_switch 不会刷新);② plan suffix 走 getPlanModeSuffix() 现拼;
771
- // ③ MOCODE.md 只在 base 中提示按需 read_file,不注入正文;④ Memory Index 按开关注入。
771
+ // ③ AGENTS.md 存在工作区根时由 base 无条件自动导入正文(超长截断,与 memory 开关无关);④ Memory Index 按开关注入。
772
772
  const buildSystemMessage = (planMode) => effectiveSystemPrompt(buildBasePrompt(currentSessionId) +
773
773
  (planMode ? getPlanModeSuffix() : '') +
774
774
  buildMemoryIndexSection(isMemoryEnabled()));
@@ -1199,7 +1199,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
1199
1199
  continue;
1200
1200
  }
1201
1201
  if (line === '/init') {
1202
- // /init:把 init 指令当 user 输入发给 agent(扫描项目 + 生成 MOCODE.md),fall through 走 runAgent
1202
+ // /init:把 init 指令当 user 输入发给 agent(扫描项目 + 生成 AGENTS.md),fall through 走 runAgent
1203
1203
  joined = buildInitPrompt();
1204
1204
  }
1205
1205
  if (line === '/upgrade' || line.startsWith('/upgrade ')) {
@@ -1438,7 +1438,7 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
1438
1438
  layout.contentWrite(` ${ui.accent}${e.id}${ui.reset} ${ui.dim}${e.name} — ${e.summary}${ui.reset}\n`);
1439
1439
  }
1440
1440
  if (active.length === 0)
1441
- layout.contentWrite(`${ui.dim}(无 active 记忆;用 memory_save 存,或 /init 生成 MOCODE.md)${ui.reset}\n`);
1441
+ layout.contentWrite(`${ui.dim}(无 active 记忆;用 memory_save 存,或 /init 生成 AGENTS.md)${ui.reset}\n`);
1442
1442
  layout.contentWrite(`${ui.dim}(详情用 memory_search;启动索引已注入 systemPrompt)${ui.reset}\n`);
1443
1443
  continue;
1444
1444
  }
@@ -1545,9 +1545,9 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
1545
1545
  // focus 透传到 compact_history action 的 LLM 摘要 prompt。
1546
1546
  // 返回 SchedulerRunLog 给 UI 显示决策;退化路径(开关关时)在 manualCompact 内部走 compactHistory。
1547
1547
  const log = await manualCompact(history, focus, { force });
1548
- // ② compact 后把活跃 plan 重注入系统提示(history[0]),避免 agent 因上下文压缩丢失执行计划。
1548
+ // ② compact 后把会话状态(plan + 笔记段)重注入系统提示(history[0]),避免 agent 因上下文压缩丢失计划与笔记。
1549
1549
  if (log.compactHistoryCalled)
1550
- reinjectActivePlanIntoSystem(history);
1550
+ reinjectSessionStateIntoSystem(history);
1551
1551
  const d = log.compactDetail;
1552
1552
  appendCurrentSessionRuntimeEvent('compact', {
1553
1553
  source: 'manual',
@@ -2186,9 +2186,9 @@ export async function startRepl(initialHistory, sessionId, sandboxRootOverride,
2186
2186
  // 写盘:mode 文件 values,/~/.mocode/config;writeConfigKeys 不会动其它键(主题 / 模型等)
2187
2187
  updateConfigKey('MEMORY_ENABLED', nextEnabled ? 'true' : 'false');
2188
2188
  const note = nextEnabled
2189
- ? `${ui.green}已开启记忆子系统${ui.reset} — memory_save/search/list/update/forget 进入工具表;` +
2189
+ ? `${ui.green}已开启记忆子系统${ui.reset} — memory_save/search/list/update/forget/graph 进入工具表;` +
2190
2190
  `Memory Index 段会在下次拼 system message 时注入。工具表本身的快照需要重启 REPL 才完整刷新。`
2191
- : `${ui.yellow}已关闭记忆子系统${ui.reset} — 五个 memory_* 工具将在下次拼 system message 时从工具表过滤;` +
2191
+ : `${ui.yellow}已关闭记忆子系统${ui.reset} — 六个 memory_* 工具将在下次拼 system message 时从工具表过滤;` +
2192
2192
  `Memory Index 段不再出现;plan-mode 提示词里的 memory_* 字样消失。重启 REPL 后工具表完全不出现。`;
2193
2193
  layout.contentWrite(`${note}\n`);
2194
2194
  layout.contentWrite(`${ui.dim}(写入 ${CONFIG_PATH}:MEMORY_ENABLED=${nextEnabled ? 'true' : 'false'};${ui.reset}` +
@@ -14,7 +14,7 @@ import { jailResolve, jailGlobPattern } from './jail.js';
14
14
  * Shift+Tab 等用户面触发,不经工具路径。
15
15
  */
16
16
  export const SANDBOX_EXEMPT_TOOLS = new Set([
17
- 'memory_save', 'memory_update', 'memory_forget', 'memory_search', 'memory_list',
17
+ 'memory_save', 'memory_update', 'memory_forget', 'memory_search', 'memory_list', 'memory_graph',
18
18
  'use_skill',
19
19
  'web_search', 'web_fetch',
20
20
  'ask_human',