metame-cli 1.5.19 → 1.5.21

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.
Files changed (40) hide show
  1. package/index.js +157 -80
  2. package/package.json +2 -2
  3. package/scripts/bin/bootstrap-worktree.sh +20 -0
  4. package/scripts/core/audit.js +190 -0
  5. package/scripts/core/handoff.js +780 -0
  6. package/scripts/core/handoff.test.js +1074 -0
  7. package/scripts/core/memory-model.js +183 -0
  8. package/scripts/core/memory-model.test.js +486 -0
  9. package/scripts/core/reactive-paths.js +44 -0
  10. package/scripts/core/reactive-paths.test.js +35 -0
  11. package/scripts/core/reactive-prompt.js +51 -0
  12. package/scripts/core/reactive-prompt.test.js +88 -0
  13. package/scripts/core/reactive-signal.js +40 -0
  14. package/scripts/core/reactive-signal.test.js +88 -0
  15. package/scripts/core/thread-chat-id.js +52 -0
  16. package/scripts/core/thread-chat-id.test.js +113 -0
  17. package/scripts/daemon-bridges.js +92 -38
  18. package/scripts/daemon-claude-engine.js +373 -444
  19. package/scripts/daemon-command-router.js +82 -8
  20. package/scripts/daemon-engine-runtime.js +7 -10
  21. package/scripts/daemon-reactive-lifecycle.js +100 -33
  22. package/scripts/daemon-session-commands.js +133 -43
  23. package/scripts/daemon-session-store.js +300 -82
  24. package/scripts/daemon-team-dispatch.js +16 -16
  25. package/scripts/daemon.js +21 -175
  26. package/scripts/deploy-manifest.js +90 -0
  27. package/scripts/docs/maintenance-manual.md +14 -11
  28. package/scripts/docs/pointer-map.md +13 -4
  29. package/scripts/feishu-adapter.js +31 -27
  30. package/scripts/hooks/intent-engine.js +6 -3
  31. package/scripts/hooks/intent-memory-recall.js +1 -0
  32. package/scripts/hooks/intent-perpetual.js +1 -1
  33. package/scripts/memory-extract.js +5 -97
  34. package/scripts/memory-gc.js +35 -90
  35. package/scripts/memory-migrate-v2.js +304 -0
  36. package/scripts/memory-nightly-reflect.js +40 -41
  37. package/scripts/memory.js +340 -859
  38. package/scripts/migrate-reactive-paths.js +122 -0
  39. package/scripts/signal-capture.js +4 -0
  40. package/scripts/sync-plugin.js +56 -0
@@ -85,13 +85,69 @@ function createSessionStore(deps) {
85
85
  } = deps;
86
86
 
87
87
  const CLAUDE_PROJECTS_DIR = path.join(HOME, '.claude', 'projects');
88
- const CODEX_DB = path.join(HOME, '.codex', 'state_5.sqlite');
88
+ const GLOBAL_CODEX_DB = path.join(HOME, '.codex', 'state_5.sqlite');
89
89
  const _sessionFileCache = new Map(); // sessionId -> { path, ts }
90
90
  const _codexRolloutCache = new Map(); // sessionId -> { path, ts }
91
91
  let _sessionCache = null;
92
92
  let _sessionCacheTime = 0;
93
93
  const SESSION_CACHE_TTL = 30000; // 30s — scan is expensive, 10s was too frequent
94
94
 
95
+ function resolveCodexHomeForCwd(cwd) {
96
+ const safeCwd = sanitizeCwd(cwd);
97
+ if (!safeCwd || safeCwd === HOME) return path.join(HOME, '.codex');
98
+ return path.join(safeCwd, '.codex');
99
+ }
100
+
101
+ function resolveCodexDbForCwd(cwd) {
102
+ return path.join(resolveCodexHomeForCwd(cwd), 'state_5.sqlite');
103
+ }
104
+
105
+ function getKnownCodexDbPaths(preferredCwd = '') {
106
+ const paths = [];
107
+ const seen = new Set();
108
+ const add = (candidate) => {
109
+ const value = String(candidate || '').trim();
110
+ if (!value || seen.has(value)) return;
111
+ seen.add(value);
112
+ paths.push(value);
113
+ };
114
+
115
+ const preferredDb = preferredCwd ? resolveCodexDbForCwd(preferredCwd) : '';
116
+ if (preferredDb) add(preferredDb);
117
+
118
+ try {
119
+ const state = loadState() || {};
120
+ const sessions = state.sessions || {};
121
+ for (const raw of Object.values(sessions)) {
122
+ if (!raw || typeof raw !== 'object') continue;
123
+ const candidateCwd = sanitizeCwd(raw.cwd || '');
124
+ if (candidateCwd) add(resolveCodexDbForCwd(candidateCwd));
125
+ }
126
+ } catch { /* ignore */ }
127
+
128
+ add(GLOBAL_CODEX_DB);
129
+ return paths;
130
+ }
131
+
132
+ function queryCodexDbs(preferredCwd, queryFn) {
133
+ const dbPaths = getKnownCodexDbPaths(preferredCwd);
134
+ for (const dbPath of dbPaths) {
135
+ let db = null;
136
+ try {
137
+ if (!fs.existsSync(dbPath)) continue;
138
+ const { DatabaseSync } = require('node:sqlite');
139
+ db = new DatabaseSync(dbPath, { readonly: true });
140
+ const result = queryFn(db, dbPath);
141
+ db.close();
142
+ db = null;
143
+ if (result) return result;
144
+ } catch {
145
+ if (db) { try { db.close(); } catch { /* ignore */ } }
146
+ }
147
+ }
148
+ return null;
149
+ }
150
+
95
151
  function findSessionFile(sessionId) {
96
152
  if (!sessionId || !fs.existsSync(CLAUDE_PROJECTS_DIR)) return null;
97
153
  const cached = _sessionFileCache.get(sessionId);
@@ -177,6 +233,51 @@ function createSessionStore(deps) {
177
233
  }
178
234
  }
179
235
 
236
+ /**
237
+ * Strip thinking block signatures from a session JSONL file.
238
+ * This allows resuming sessions after switching models (e.g. MiniMax → Claude)
239
+ * without hitting "Invalid signature in thinking block" errors.
240
+ * Returns the number of signatures stripped, or 0 if nothing changed.
241
+ */
242
+ function stripThinkingSignatures(sessionId) {
243
+ try {
244
+ const sessionFile = findSessionFile(sessionId);
245
+ if (!sessionFile) return 0;
246
+ const fileContent = fs.readFileSync(sessionFile, 'utf8');
247
+ const lines = fileContent.split('\n');
248
+ let changed = 0;
249
+ const patched = lines.map(line => {
250
+ if (!line.trim()) return line;
251
+ try {
252
+ const obj = JSON.parse(line);
253
+ const content = obj && obj.message && obj.message.content;
254
+ if (!Array.isArray(content)) return line;
255
+ let lineChanged = false;
256
+ for (const block of content) {
257
+ if (block && block.type === 'thinking' && block.signature) {
258
+ delete block.signature;
259
+ lineChanged = true;
260
+ }
261
+ }
262
+ if (lineChanged) {
263
+ changed++;
264
+ return JSON.stringify(obj);
265
+ }
266
+ } catch { /* skip malformed lines */ }
267
+ return line;
268
+ });
269
+ if (changed > 0) {
270
+ fs.writeFileSync(sessionFile, patched.join('\n'), 'utf8');
271
+ _sessionFileCache.delete(sessionId);
272
+ log('INFO', `stripThinkingSignatures: patched ${changed} lines in ${path.basename(sessionFile)}`);
273
+ }
274
+ return changed;
275
+ } catch (e) {
276
+ log('WARN', `stripThinkingSignatures failed: ${e.message}`);
277
+ return 0;
278
+ }
279
+ }
280
+
180
281
  function invalidateSessionCache() { _sessionCache = null; }
181
282
 
182
283
  // 监听 ~/.claude/projects 目录,手机端新建 session 后桌面端无需重启即可感知
@@ -296,8 +397,8 @@ function createSessionStore(deps) {
296
397
  }
297
398
  } catch {}
298
399
  }
299
- } catch (err) {
300
- log('WARN', `scanClaudeSessions project ${proj}: ${err.message}`);
400
+ } catch (e) {
401
+ log('WARN', `[session-store] scanClaudeSessions project ${proj} index error: ${e.message}`);
301
402
  }
302
403
 
303
404
  try {
@@ -320,8 +421,8 @@ function createSessionStore(deps) {
320
421
  });
321
422
  }
322
423
  }
323
- } catch (err) {
324
- log('WARN', `scanClaudeSessions project ${proj}: ${err.message}`);
424
+ } catch (e) {
425
+ log('WARN', `[session-store] scanClaudeSessions project ${proj} file error: ${e.message}`);
325
426
  }
326
427
  }
327
428
 
@@ -389,37 +490,52 @@ function createSessionStore(deps) {
389
490
  } catch { /* non-fatal */ }
390
491
  }
391
492
  return all;
392
- } catch (err) {
393
- log('WARN', `scanClaudeSessions: ${err.message}`);
493
+ } catch (e) {
494
+ log('WARN', `[session-store] scanClaudeSessions failed: ${e.message}`);
394
495
  return [];
395
496
  }
396
497
  }
397
498
 
398
499
  function scanCodexSessions() {
399
- let db = null;
400
500
  try {
401
- if (!fs.existsSync(CODEX_DB)) return [];
402
- const { DatabaseSync } = require('node:sqlite');
403
- db = new DatabaseSync(CODEX_DB, { readonly: true });
404
- const rows = db.prepare(`
405
- SELECT
406
- id,
407
- cwd,
408
- title,
409
- first_user_message,
410
- source,
411
- rollout_path,
412
- created_at,
413
- updated_at,
414
- tokens_used,
415
- archived
416
- FROM threads
417
- ORDER BY updated_at DESC
418
- LIMIT 200
419
- `).all();
420
- db.close();
421
- db = null;
422
- return rows
501
+ const seen = new Set();
502
+ const merged = [];
503
+ for (const dbPath of getKnownCodexDbPaths()) {
504
+ if (!fs.existsSync(dbPath)) continue;
505
+ let db = null;
506
+ try {
507
+ const { DatabaseSync } = require('node:sqlite');
508
+ db = new DatabaseSync(dbPath, { readonly: true });
509
+ const rows = db.prepare(`
510
+ SELECT
511
+ id,
512
+ cwd,
513
+ title,
514
+ first_user_message,
515
+ source,
516
+ rollout_path,
517
+ created_at,
518
+ updated_at,
519
+ tokens_used,
520
+ archived
521
+ FROM threads
522
+ ORDER BY updated_at DESC
523
+ LIMIT 200
524
+ `).all();
525
+ db.close();
526
+ db = null;
527
+ for (const row of rows) {
528
+ const key = String(row && row.id || '').trim();
529
+ if (!key || seen.has(key)) continue;
530
+ seen.add(key);
531
+ merged.push(row);
532
+ }
533
+ } catch (e) {
534
+ log('WARN', `[session-store] scanCodexSessions failed for ${dbPath}: ${e.message}`);
535
+ if (db) { try { db.close(); } catch { /* ignore */ } }
536
+ }
537
+ }
538
+ return merged
423
539
  .filter((row) => {
424
540
  if (row.archived || !row.id || !row.cwd) return false;
425
541
  const seedText = String(row.first_user_message || row.title || '').trim();
@@ -449,31 +565,22 @@ function createSessionStore(deps) {
449
565
  };
450
566
  })
451
567
  .map((session) => enrichCodexSession(session));
452
- } catch (err) {
453
- if (db) { try { db.close(); } catch { /* ignore */ } }
454
- log('WARN', `scanCodexSessions ${CODEX_DB}: ${err.message}`);
568
+ } catch (e) {
569
+ log('WARN', `[session-store] scanCodexSessions failed: ${e.message}`);
455
570
  return [];
456
571
  }
457
572
  }
458
573
 
459
- function findCodexSessionFile(sessionId) {
460
- if (!sessionId || !fs.existsSync(CODEX_DB)) return null;
574
+ function findCodexSessionFile(sessionId, cwd = '') {
575
+ if (!sessionId) return null;
461
576
  const cached = _codexRolloutCache.get(sessionId);
462
577
  if (cached && Date.now() - cached.ts < 30000) return cached.path;
463
- let db = null;
464
- try {
465
- const { DatabaseSync } = require('node:sqlite');
466
- db = new DatabaseSync(CODEX_DB, { readonly: true });
578
+ const result = queryCodexDbs(cwd, (db) => {
467
579
  const row = db.prepare('SELECT rollout_path FROM threads WHERE id = ?').get(sessionId);
468
- db.close();
469
- db = null;
470
- const rolloutPath = row && row.rollout_path ? String(row.rollout_path) : null;
471
- _codexRolloutCache.set(sessionId, { path: rolloutPath, ts: Date.now() });
472
- return rolloutPath;
473
- } catch {
474
- if (db) { try { db.close(); } catch { /* ignore */ } }
475
- return null;
476
- }
580
+ return row && row.rollout_path ? String(row.rollout_path) : null;
581
+ });
582
+ _codexRolloutCache.set(sessionId, { path: result, ts: Date.now() });
583
+ return result;
477
584
  }
478
585
 
479
586
  function extractCodexMessageText(payload) {
@@ -550,15 +657,19 @@ function createSessionStore(deps) {
550
657
 
551
658
  function scanAllSessions() {
552
659
  if (_sessionCache && (Date.now() - _sessionCacheTime < SESSION_CACHE_TTL)) return _sessionCache;
553
- const all = [...scanClaudeSessions(), ...scanCodexSessions()];
554
- all.sort((a, b) => {
555
- const aTime = a.fileMtime || new Date(a.modified).getTime();
556
- const bTime = b.fileMtime || new Date(b.modified).getTime();
557
- return bTime - aTime;
558
- });
559
- _sessionCache = all;
560
- _sessionCacheTime = Date.now();
561
- return all;
660
+ try {
661
+ const all = [...scanClaudeSessions(), ...scanCodexSessions()];
662
+ all.sort((a, b) => {
663
+ const aTime = a.fileMtime || new Date(a.modified).getTime();
664
+ const bTime = b.fileMtime || new Date(b.modified).getTime();
665
+ return bTime - aTime;
666
+ });
667
+ _sessionCache = all;
668
+ _sessionCacheTime = Date.now();
669
+ return all;
670
+ } catch {
671
+ return [];
672
+ }
562
673
  }
563
674
 
564
675
  function listRecentSessions(limit, cwd, engine) {
@@ -573,6 +684,111 @@ function createSessionStore(deps) {
573
684
  return all.slice(0, limit || 10);
574
685
  }
575
686
 
687
+ function buildPendingStateSessions(engine, cwd) {
688
+ const safeEngine = normalizeEngineName(engine);
689
+ const normCwd = cwd ? path.resolve(cwd) : null;
690
+ const state = loadState();
691
+ const sessions = state && state.sessions && typeof state.sessions === 'object' ? state.sessions : {};
692
+ const items = [];
693
+
694
+ for (const [chatKey, raw] of Object.entries(sessions)) {
695
+ const upgraded = upgradeSessionRecord(raw || {}, safeEngine);
696
+ const slot = upgraded.engines && upgraded.engines[safeEngine];
697
+ const sessionCwd = upgraded.cwd ? path.resolve(upgraded.cwd) : null;
698
+ if (!slot || !sessionCwd) continue;
699
+ if (normCwd && sessionCwd !== normCwd) continue;
700
+
701
+ const compactContext = String(slot.compactContext || '').trim();
702
+ const hasBridgeContext = compactContext.length > 0;
703
+ const hasPlaceholder = safeEngine === 'codex' && slot.runtimeSessionObserved === false;
704
+ const hasId = !!String(slot.id || '').trim();
705
+ const validId = hasId ? isEngineSessionValid(safeEngine, slot.id, sessionCwd) : false;
706
+ if (validId) continue;
707
+ if (!hasBridgeContext && !hasPlaceholder) continue;
708
+
709
+ const ts = Number(raw && raw.last_active) || 0;
710
+ const compactLines = compactContext.split('\n').map(line => line.trim()).filter(Boolean);
711
+ const lastUserLine = compactLines.find(line => /^last user message:/i.test(line));
712
+ const lastAssistantLine = compactLines.find(line => /^last assistant reply:/i.test(line));
713
+ const summary = hasBridgeContext
714
+ ? (
715
+ lastUserLine
716
+ || lastAssistantLine
717
+ || compactLines[0]
718
+ || '待接续上下文'
719
+ )
720
+ : '待接续上下文';
721
+ items.push({
722
+ sessionId: '',
723
+ projectPath: sessionCwd,
724
+ fileMtime: ts,
725
+ modified: new Date(ts || Date.now()).toISOString(),
726
+ messageCount: '?',
727
+ customTitle: '待接续上下文',
728
+ summary,
729
+ firstPrompt: summary,
730
+ lastUser: '',
731
+ _enriched: true,
732
+ engine: safeEngine,
733
+ pendingState: true,
734
+ pendingChatKey: chatKey,
735
+ compactContext,
736
+ started: false,
737
+ runtimeSessionObserved: false,
738
+ ...(slot.sandboxMode ? { sandboxMode: slot.sandboxMode } : {}),
739
+ ...(slot.approvalPolicy ? { approvalPolicy: slot.approvalPolicy } : {}),
740
+ ...(slot.permissionMode ? { permissionMode: slot.permissionMode } : {}),
741
+ });
742
+ }
743
+
744
+ items.sort((a, b) => (b.fileMtime || 0) - (a.fileMtime || 0));
745
+ return items;
746
+ }
747
+
748
+ function findAttachableSession(options = {}) {
749
+ const safeEngine = normalizeEngineName(options.engine);
750
+ const cwd = options.cwd ? path.resolve(options.cwd) : null;
751
+ const preferredSessionId = String(options.preferredSessionId || '').trim();
752
+ const excludeSessionId = String(options.excludeSessionId || '').trim();
753
+ const limit = Number(options.limit) > 0 ? Number(options.limit) : 20;
754
+ const allowGlobalFallback = !!options.allowGlobalFallback;
755
+ const includePendingState = options.includePendingState !== false;
756
+
757
+ const matches = listRecentSessions(limit, cwd, safeEngine)
758
+ .filter((session) => !excludeSessionId || session.sessionId !== excludeSessionId);
759
+ if (preferredSessionId) {
760
+ const preferred = matches.find(session => session.sessionId === preferredSessionId);
761
+ if (preferred) return preferred;
762
+ }
763
+ if (matches.length > 0) return matches[0];
764
+
765
+ if (includePendingState) {
766
+ const pending = buildPendingStateSessions(safeEngine, cwd)
767
+ .filter((session) => !excludeSessionId || session.sessionId !== excludeSessionId);
768
+ if (preferredSessionId) {
769
+ const preferredPending = pending.find(session => session.sessionId === preferredSessionId);
770
+ if (preferredPending) return preferredPending;
771
+ }
772
+ if (pending.length > 0) return pending[0];
773
+ }
774
+
775
+ if (allowGlobalFallback) {
776
+ const global = listRecentSessions(limit, null, safeEngine)
777
+ .filter((session) => !excludeSessionId || session.sessionId !== excludeSessionId);
778
+ if (preferredSessionId) {
779
+ const preferredGlobal = global.find(session => session.sessionId === preferredSessionId);
780
+ if (preferredGlobal) return preferredGlobal;
781
+ }
782
+ if (global.length > 0) return global[0];
783
+ }
784
+
785
+ if (!includePendingState) return null;
786
+ if (!allowGlobalFallback) return null;
787
+ const globalPending = buildPendingStateSessions(safeEngine, null)
788
+ .filter((session) => !excludeSessionId || session.sessionId !== excludeSessionId);
789
+ return globalPending[0] || null;
790
+ }
791
+
576
792
  function loadSessionTags() {
577
793
  try {
578
794
  return JSON.parse(fs.readFileSync(path.join(HOME, '.metame', 'session_tags.json'), 'utf8'));
@@ -1063,33 +1279,33 @@ function createSessionStore(deps) {
1063
1279
  }
1064
1280
 
1065
1281
  function _isCodexSessionValid(sessionId, normCwd) {
1066
- let db = null;
1067
- try {
1068
- const { DatabaseSync } = require('node:sqlite');
1069
- db = new DatabaseSync(CODEX_DB, { readonly: true });
1070
- const row = db.prepare('SELECT cwd FROM threads WHERE id = ?').get(sessionId);
1071
- db.close();
1072
- db = null;
1073
- return !!row && path.resolve(row.cwd) === normCwd;
1074
- } catch (e) {
1075
- if (db) { try { db.close(); } catch { /* ignore */ } }
1076
- // Transient errors (DB locked, busy) should not invalidate a live session.
1077
- // Only treat "session truly not found" as invalid; infra failures are conservative.
1078
- const msg = (e && e.message) || '';
1079
- if (msg.includes('SQLITE_BUSY') || msg.includes('SQLITE_LOCKED')) return true;
1080
- return false;
1282
+ const preferredDb = resolveCodexDbForCwd(normCwd);
1283
+ for (const dbPath of getKnownCodexDbPaths(normCwd)) {
1284
+ let db = null;
1285
+ try {
1286
+ if (!fs.existsSync(dbPath)) continue;
1287
+ const { DatabaseSync } = require('node:sqlite');
1288
+ db = new DatabaseSync(dbPath, { readonly: true });
1289
+ const row = db.prepare('SELECT cwd FROM threads WHERE id = ?').get(sessionId);
1290
+ db.close();
1291
+ db = null;
1292
+ if (!row) continue;
1293
+ const rowCwd = path.resolve(String(row.cwd || ''));
1294
+ if (rowCwd === normCwd) return true;
1295
+ if (dbPath === preferredDb) return false;
1296
+ } catch (e) {
1297
+ if (db) { try { db.close(); } catch { /* ignore */ } }
1298
+ const msg = (e && e.message) || '';
1299
+ if (msg.includes('SQLITE_BUSY') || msg.includes('SQLITE_LOCKED')) return true;
1300
+ }
1081
1301
  }
1302
+ return false;
1082
1303
  }
1083
1304
 
1084
- function getCodexSessionSandboxProfile(sessionId) {
1085
- let db = null;
1305
+ function getCodexSessionSandboxProfile(sessionId, cwd = '') {
1086
1306
  try {
1087
1307
  if (!sessionId) return null;
1088
- const { DatabaseSync } = require('node:sqlite');
1089
- db = new DatabaseSync(CODEX_DB, { readonly: true });
1090
- const row = db.prepare('SELECT sandbox_policy, approval_mode FROM threads WHERE id = ?').get(sessionId);
1091
- db.close();
1092
- db = null;
1308
+ const row = queryCodexDbs(cwd, (db) => db.prepare('SELECT sandbox_policy, approval_mode FROM threads WHERE id = ?').get(sessionId));
1093
1309
  if (!row || !row.sandbox_policy) return null;
1094
1310
  const policy = JSON.parse(String(row.sandbox_policy));
1095
1311
  const sandboxMode = normalizeCodexSandboxMode(
@@ -1107,13 +1323,12 @@ function createSessionStore(deps) {
1107
1323
  permissionMode: sandboxMode || 'danger-full-access',
1108
1324
  };
1109
1325
  } catch {
1110
- if (db) { try { db.close(); } catch { /* ignore */ } }
1111
1326
  return null;
1112
1327
  }
1113
1328
  }
1114
1329
 
1115
- function getCodexSessionPermissionMode(sessionId) {
1116
- const profile = getCodexSessionSandboxProfile(sessionId);
1330
+ function getCodexSessionPermissionMode(sessionId, cwd = '') {
1331
+ const profile = getCodexSessionSandboxProfile(sessionId, cwd);
1117
1332
  return profile ? profile.permissionMode : null;
1118
1333
  }
1119
1334
 
@@ -1134,9 +1349,11 @@ function createSessionStore(deps) {
1134
1349
  findCodexSessionFile,
1135
1350
  clearSessionFileCache,
1136
1351
  truncateSessionToCheckpoint,
1352
+ stripThinkingSignatures,
1137
1353
  watchSessionFiles,
1138
1354
  stopWatchingSessionFiles,
1139
1355
  listRecentSessions,
1356
+ findAttachableSession,
1140
1357
  loadSessionTags,
1141
1358
  getSessionFileMtime,
1142
1359
  sessionLabel,
@@ -1161,6 +1378,7 @@ function createSessionStore(deps) {
1161
1378
  stripCodexInjectedHints,
1162
1379
  looksLikeInternalCodexPrompt,
1163
1380
  parseCodexSessionPreview,
1381
+ buildPendingStateSessions,
1164
1382
  },
1165
1383
  };
1166
1384
  }
@@ -15,6 +15,7 @@
15
15
  const fs = require('fs');
16
16
  const path = require('path');
17
17
  const os = require('os');
18
+ const { resolveReactivePaths } = require('./core/reactive-paths');
18
19
 
19
20
  const METAME_DIR = path.join(os.homedir(), '.metame');
20
21
 
@@ -165,12 +166,12 @@ function updateDispatchContextFiles({ fs: fsMod = fs, path: pathMod = path, base
165
166
  const logWarn = (msg) => {
166
167
  if (typeof logger === 'function') logger(msg);
167
168
  };
168
- const nowDir = pathMod.join(baseDir, 'memory', 'now');
169
+ const rp = resolveReactivePaths(targetProject, baseDir);
169
170
  const sharedDir = pathMod.join(baseDir, 'memory', 'shared');
170
- const targetNowPath = pathMod.join(nowDir, `${targetProject}.md`);
171
- const sharedNowPath = pathMod.join(nowDir, 'shared.md');
171
+ const targetNowPath = rp.state;
172
+ const sharedNowPath = pathMod.join(baseDir, 'memory', 'now', 'shared.md');
172
173
  const tasksFilePath = pathMod.join(sharedDir, 'tasks.md');
173
- fsMod.mkdirSync(nowDir, { recursive: true });
174
+ fsMod.mkdirSync(rp.dir, { recursive: true });
174
175
 
175
176
  const projects = (config && config.projects) || {};
176
177
  const actor = resolveDispatchActor((fullMsg && fullMsg.source_sender_key) || (fullMsg && fullMsg.from), projects);
@@ -192,6 +193,7 @@ function updateDispatchContextFiles({ fs: fsMod = fs, path: pathMod = path, base
192
193
 
193
194
  if (!isSharedTeamTask) return { targetNowPath, sharedNowPath: null, tasksFilePath: null };
194
195
 
196
+ fsMod.mkdirSync(pathMod.dirname(sharedNowPath), { recursive: true });
195
197
  fsMod.writeFileSync(sharedNowPath, buildSharedNowContent({
196
198
  actor, target, title, prompt, timeStr,
197
199
  dispatchId: fullMsg.id, taskId, scopeId, chain: fullMsg.chain,
@@ -252,14 +254,14 @@ function updateDispatchContextFiles({ fs: fsMod = fs, path: pathMod = path, base
252
254
  function buildEnrichedPrompt(target, rawPrompt, metameDir, opts = {}) {
253
255
  const base = metameDir || METAME_DIR;
254
256
  const includeShared = !!(opts && opts.includeShared);
257
+ const rp = resolveReactivePaths(target, base);
255
258
  let ctx = '';
256
259
 
257
- // 1. Target private now file
260
+ // 1. Target state file (reactive/<target>/state.md)
258
261
  try {
259
- const targetNowFile = path.join(base, 'memory', 'now', `${target}.md`);
260
- if (fs.existsSync(targetNowFile)) {
261
- const content = fs.readFileSync(targetNowFile, 'utf8').trim();
262
- if (content) ctx += `[当前进度 now/${target}.md]\n${content}\n\n`;
262
+ if (fs.existsSync(rp.state)) {
263
+ const content = fs.readFileSync(rp.state, 'utf8').trim();
264
+ if (content) ctx += `[当前进度 ${target}/state.md]\n${content}\n\n`;
263
265
  }
264
266
  } catch { /* non-critical */ }
265
267
 
@@ -274,13 +276,12 @@ function buildEnrichedPrompt(target, rawPrompt, metameDir, opts = {}) {
274
276
  } catch { /* non-critical */ }
275
277
  }
276
278
 
277
- // 3+5. Structured memory (L1+L2) OR legacy _latest.md fallback
279
+ // 3+5. Structured memory (L1+L2) OR legacy latest.md fallback
278
280
  // Single stat — structured memory supersedes raw last output
279
281
  let hasStructuredMemory = false;
280
282
  try {
281
- const memFile = path.join(base, 'memory', 'now', `${target}_memory.md`);
282
- if (fs.existsSync(memFile)) {
283
- const content = fs.readFileSync(memFile, 'utf8').trim();
283
+ if (fs.existsSync(rp.memory)) {
284
+ const content = fs.readFileSync(rp.memory, 'utf8').trim();
284
285
  if (content) {
285
286
  ctx += `[Memory Context]\n${content}\n\n`;
286
287
  hasStructuredMemory = true;
@@ -291,9 +292,8 @@ function buildEnrichedPrompt(target, rawPrompt, metameDir, opts = {}) {
291
292
  if (!hasStructuredMemory) {
292
293
  // Fallback: raw last output (for non-reactive projects without memory system)
293
294
  try {
294
- const latestFile = path.join(base, 'memory', 'agents', `${target}_latest.md`);
295
- if (fs.existsSync(latestFile)) {
296
- const content = fs.readFileSync(latestFile, 'utf8').trim();
295
+ if (fs.existsSync(rp.latest)) {
296
+ const content = fs.readFileSync(rp.latest, 'utf8').trim();
297
297
  if (content) ctx += `[${target} 上次产出]\n${content}\n\n`;
298
298
  }
299
299
  } catch { /* non-critical */ }