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.
- package/index.js +157 -80
- package/package.json +2 -2
- package/scripts/bin/bootstrap-worktree.sh +20 -0
- package/scripts/core/audit.js +190 -0
- package/scripts/core/handoff.js +780 -0
- package/scripts/core/handoff.test.js +1074 -0
- package/scripts/core/memory-model.js +183 -0
- package/scripts/core/memory-model.test.js +486 -0
- package/scripts/core/reactive-paths.js +44 -0
- package/scripts/core/reactive-paths.test.js +35 -0
- package/scripts/core/reactive-prompt.js +51 -0
- package/scripts/core/reactive-prompt.test.js +88 -0
- package/scripts/core/reactive-signal.js +40 -0
- package/scripts/core/reactive-signal.test.js +88 -0
- package/scripts/core/thread-chat-id.js +52 -0
- package/scripts/core/thread-chat-id.test.js +113 -0
- package/scripts/daemon-bridges.js +92 -38
- package/scripts/daemon-claude-engine.js +373 -444
- package/scripts/daemon-command-router.js +82 -8
- package/scripts/daemon-engine-runtime.js +7 -10
- package/scripts/daemon-reactive-lifecycle.js +100 -33
- package/scripts/daemon-session-commands.js +133 -43
- package/scripts/daemon-session-store.js +300 -82
- package/scripts/daemon-team-dispatch.js +16 -16
- package/scripts/daemon.js +21 -175
- package/scripts/deploy-manifest.js +90 -0
- package/scripts/docs/maintenance-manual.md +14 -11
- package/scripts/docs/pointer-map.md +13 -4
- package/scripts/feishu-adapter.js +31 -27
- package/scripts/hooks/intent-engine.js +6 -3
- package/scripts/hooks/intent-memory-recall.js +1 -0
- package/scripts/hooks/intent-perpetual.js +1 -1
- package/scripts/memory-extract.js +5 -97
- package/scripts/memory-gc.js +35 -90
- package/scripts/memory-migrate-v2.js +304 -0
- package/scripts/memory-nightly-reflect.js +40 -41
- package/scripts/memory.js +340 -859
- package/scripts/migrate-reactive-paths.js +122 -0
- package/scripts/signal-capture.js +4 -0
- 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
|
|
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 (
|
|
300
|
-
log('WARN', `scanClaudeSessions project ${proj}: ${
|
|
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 (
|
|
324
|
-
log('WARN', `scanClaudeSessions project ${proj}: ${
|
|
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 (
|
|
393
|
-
log('WARN', `scanClaudeSessions: ${
|
|
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
|
-
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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 (
|
|
453
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
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
|
|
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
|
|
169
|
+
const rp = resolveReactivePaths(targetProject, baseDir);
|
|
169
170
|
const sharedDir = pathMod.join(baseDir, 'memory', 'shared');
|
|
170
|
-
const targetNowPath =
|
|
171
|
-
const sharedNowPath = pathMod.join(
|
|
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(
|
|
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
|
|
260
|
+
// 1. Target state file (reactive/<target>/state.md)
|
|
258
261
|
try {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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 */ }
|