metame-cli 1.5.3 → 1.5.5
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 +60 -18
- package/index.js +352 -79
- package/package.json +2 -2
- package/scripts/agent-layer.js +4 -2
- package/scripts/bin/dispatch_to +178 -90
- package/scripts/daemon-admin-commands.js +353 -105
- package/scripts/daemon-agent-commands.js +434 -66
- package/scripts/daemon-bridges.js +477 -68
- package/scripts/daemon-claude-engine.js +1267 -674
- package/scripts/daemon-command-router.js +205 -27
- package/scripts/daemon-command-session-route.js +118 -0
- package/scripts/daemon-default.yaml +7 -0
- package/scripts/daemon-engine-runtime.js +96 -20
- package/scripts/daemon-exec-commands.js +108 -49
- package/scripts/daemon-file-browser.js +64 -7
- package/scripts/daemon-notify.js +18 -4
- package/scripts/daemon-ops-commands.js +16 -2
- package/scripts/daemon-remote-dispatch.js +55 -1
- package/scripts/daemon-runtime-lifecycle.js +87 -0
- package/scripts/daemon-session-commands.js +102 -45
- package/scripts/daemon-session-store.js +497 -66
- package/scripts/daemon-siri-bridge.js +234 -0
- package/scripts/daemon-siri-imessage.js +209 -0
- package/scripts/daemon-task-scheduler.js +10 -2
- package/scripts/daemon.js +697 -179
- package/scripts/daemon.yaml +7 -0
- package/scripts/docs/agent-guide.md +36 -3
- package/scripts/docs/hook-config.md +134 -0
- package/scripts/docs/maintenance-manual.md +162 -5
- package/scripts/docs/pointer-map.md +60 -5
- package/scripts/feishu-adapter.js +7 -15
- package/scripts/hooks/doc-router.js +29 -0
- package/scripts/hooks/hook-utils.js +61 -0
- package/scripts/hooks/intent-doc-router.js +54 -0
- package/scripts/hooks/intent-engine.js +72 -0
- package/scripts/hooks/intent-file-transfer.js +51 -0
- package/scripts/hooks/intent-memory-recall.js +35 -0
- package/scripts/hooks/intent-ops-assist.js +54 -0
- package/scripts/hooks/intent-task-create.js +35 -0
- package/scripts/hooks/intent-team-dispatch.js +106 -0
- package/scripts/hooks/team-context.js +143 -0
- package/scripts/intent-registry.js +59 -0
- package/scripts/memory-extract.js +59 -0
- package/scripts/memory-nightly-reflect.js +109 -43
- package/scripts/memory.js +55 -17
- package/scripts/mentor-engine.js +6 -0
- package/scripts/schema.js +1 -0
- package/scripts/self-reflect.js +110 -12
- package/scripts/session-analytics.js +160 -0
- package/scripts/signal-capture.js +1 -1
- package/scripts/team-dispatch.js +315 -0
|
@@ -2,6 +2,78 @@
|
|
|
2
2
|
|
|
3
3
|
const crypto = require('crypto');
|
|
4
4
|
|
|
5
|
+
function normalizeCodexSandboxMode(value, fallback = null) {
|
|
6
|
+
const text = String(value || '').trim().toLowerCase();
|
|
7
|
+
if (!text) return fallback;
|
|
8
|
+
if (text === 'read-only' || text === 'readonly') return 'read-only';
|
|
9
|
+
if (text === 'workspace-write' || text === 'workspace') return 'workspace-write';
|
|
10
|
+
if (
|
|
11
|
+
text === 'danger-full-access'
|
|
12
|
+
|| text === 'dangerous'
|
|
13
|
+
|| text === 'full-access'
|
|
14
|
+
|| text === 'full'
|
|
15
|
+
|| text === 'bypass'
|
|
16
|
+
|| text === 'writable'
|
|
17
|
+
) return 'danger-full-access';
|
|
18
|
+
return fallback;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeCodexApprovalPolicy(value, fallback = null) {
|
|
22
|
+
const text = String(value || '').trim().toLowerCase();
|
|
23
|
+
if (!text) return fallback;
|
|
24
|
+
if (text === 'never' || text === 'no' || text === 'none') return 'never';
|
|
25
|
+
if (text === 'on-failure' || text === 'on_failure' || text === 'failure') return 'on-failure';
|
|
26
|
+
if (text === 'on-request' || text === 'on_request' || text === 'request') return 'on-request';
|
|
27
|
+
if (text === 'untrusted') return 'untrusted';
|
|
28
|
+
return fallback;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function normalizeCodexPermissionMeta(meta = {}) {
|
|
32
|
+
const sandboxMode = normalizeCodexSandboxMode(
|
|
33
|
+
meta.sandboxMode || meta.sandbox_mode || meta.permissionMode,
|
|
34
|
+
null
|
|
35
|
+
);
|
|
36
|
+
const approvalPolicy = normalizeCodexApprovalPolicy(
|
|
37
|
+
meta.approvalPolicy || meta.approval_policy,
|
|
38
|
+
null
|
|
39
|
+
);
|
|
40
|
+
if (!sandboxMode && !approvalPolicy) return null;
|
|
41
|
+
return {
|
|
42
|
+
sandboxMode: sandboxMode || 'danger-full-access',
|
|
43
|
+
approvalPolicy: approvalPolicy || 'never',
|
|
44
|
+
permissionMode: sandboxMode || 'danger-full-access',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeEngineName(name) {
|
|
49
|
+
return String(name || 'claude').trim().toLowerCase() === 'codex' ? 'codex' : 'claude';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function stripCodexInjectedHints(text) {
|
|
53
|
+
return String(text || '')
|
|
54
|
+
.replace(/\r\n/g, '\n')
|
|
55
|
+
.replace(/\n*System hints \(internal, do not mention to user\):[\s\S]*/i, '')
|
|
56
|
+
.replace(/\n*\[Respond in Simplified Chinese[\s\S]*/i, '')
|
|
57
|
+
.replace(/\n*\[Agent memory snapshot:[\s\S]*/i, '')
|
|
58
|
+
.replace(/\n*\[Relevant facts:[\s\S]*/i, '')
|
|
59
|
+
.trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function looksLikeInternalCodexPrompt(text) {
|
|
63
|
+
const clean = stripCodexInjectedHints(text).trim();
|
|
64
|
+
if (!clean) return true;
|
|
65
|
+
return (
|
|
66
|
+
/^you are a metame\b/i.test(clean)
|
|
67
|
+
|| /^you are a meta ?me\b/i.test(clean)
|
|
68
|
+
|| /^you are a session reflection assistant\b/i.test(clean)
|
|
69
|
+
|| /^you are a metacognition pattern detector\b/i.test(clean)
|
|
70
|
+
|| /^you are codex, based on gpt-5\b/i.test(clean)
|
|
71
|
+
|| /^\[nightly-reflect]/i.test(clean)
|
|
72
|
+
|| /^\[self-reflect]/i.test(clean)
|
|
73
|
+
|| /^\[memory-/i.test(clean)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
5
77
|
|
|
6
78
|
function createSessionStore(deps) {
|
|
7
79
|
const {
|
|
@@ -16,7 +88,9 @@ function createSessionStore(deps) {
|
|
|
16
88
|
} = deps;
|
|
17
89
|
|
|
18
90
|
const CLAUDE_PROJECTS_DIR = path.join(HOME, '.claude', 'projects');
|
|
91
|
+
const CODEX_DB = path.join(HOME, '.codex', 'state_5.sqlite');
|
|
19
92
|
const _sessionFileCache = new Map(); // sessionId -> { path, ts }
|
|
93
|
+
const _codexRolloutCache = new Map(); // sessionId -> { path, ts }
|
|
20
94
|
let _sessionCache = null;
|
|
21
95
|
let _sessionCacheTime = 0;
|
|
22
96
|
const SESSION_CACHE_TTL = 30000; // 30s — scan is expensive, 10s was too frequent
|
|
@@ -42,6 +116,7 @@ function createSessionStore(deps) {
|
|
|
42
116
|
function clearSessionFileCache(sessionId) {
|
|
43
117
|
if (!sessionId) return;
|
|
44
118
|
_sessionFileCache.delete(sessionId);
|
|
119
|
+
_codexRolloutCache.delete(sessionId);
|
|
45
120
|
}
|
|
46
121
|
|
|
47
122
|
function truncateSessionLastTurn(sessionId) {
|
|
@@ -160,10 +235,9 @@ function createSessionStore(deps) {
|
|
|
160
235
|
return '';
|
|
161
236
|
}
|
|
162
237
|
|
|
163
|
-
function
|
|
164
|
-
if (_sessionCache && (Date.now() - _sessionCacheTime < SESSION_CACHE_TTL)) return _sessionCache;
|
|
238
|
+
function scanClaudeSessions() {
|
|
165
239
|
try {
|
|
166
|
-
if (!fs.existsSync(CLAUDE_PROJECTS_DIR))
|
|
240
|
+
if (!fs.existsSync(CLAUDE_PROJECTS_DIR)) return [];
|
|
167
241
|
const projects = fs.readdirSync(CLAUDE_PROJECTS_DIR);
|
|
168
242
|
const sessionMap = new Map();
|
|
169
243
|
const projPathCache = new Map();
|
|
@@ -212,13 +286,7 @@ function createSessionStore(deps) {
|
|
|
212
286
|
} catch { /* skip */ }
|
|
213
287
|
}
|
|
214
288
|
|
|
215
|
-
const all = Array.from(sessionMap.values());
|
|
216
|
-
all.sort((a, b) => {
|
|
217
|
-
const aTime = a.fileMtime || new Date(a.modified).getTime();
|
|
218
|
-
const bTime = b.fileMtime || new Date(b.modified).getTime();
|
|
219
|
-
return bTime - aTime;
|
|
220
|
-
});
|
|
221
|
-
|
|
289
|
+
const all = Array.from(sessionMap.values()).map((entry) => ({ ...entry, engine: 'claude' }));
|
|
222
290
|
const ENRICH_LIMIT = 20;
|
|
223
291
|
for (let i = 0; i < Math.min(all.length, ENRICH_LIMIT); i++) {
|
|
224
292
|
const s = all[i];
|
|
@@ -276,7 +344,173 @@ function createSessionStore(deps) {
|
|
|
276
344
|
s._enriched = true; // [M1] 标记已完成富化,下次跳过
|
|
277
345
|
} catch { /* non-fatal */ }
|
|
278
346
|
}
|
|
347
|
+
return all;
|
|
348
|
+
} catch {
|
|
349
|
+
return [];
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
function scanCodexSessions() {
|
|
354
|
+
let db = null;
|
|
355
|
+
try {
|
|
356
|
+
if (!fs.existsSync(CODEX_DB)) return [];
|
|
357
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
358
|
+
db = new DatabaseSync(CODEX_DB, { readonly: true });
|
|
359
|
+
const rows = db.prepare(`
|
|
360
|
+
SELECT
|
|
361
|
+
id,
|
|
362
|
+
cwd,
|
|
363
|
+
title,
|
|
364
|
+
first_user_message,
|
|
365
|
+
source,
|
|
366
|
+
rollout_path,
|
|
367
|
+
created_at,
|
|
368
|
+
updated_at,
|
|
369
|
+
tokens_used,
|
|
370
|
+
archived
|
|
371
|
+
FROM threads
|
|
372
|
+
ORDER BY updated_at DESC
|
|
373
|
+
LIMIT 200
|
|
374
|
+
`).all();
|
|
375
|
+
db.close();
|
|
376
|
+
db = null;
|
|
377
|
+
return rows
|
|
378
|
+
.filter((row) => {
|
|
379
|
+
if (row.archived || !row.id || !row.cwd) return false;
|
|
380
|
+
const seedText = String(row.first_user_message || row.title || '').trim();
|
|
381
|
+
const safeSource = String(row.source || '').trim().toLowerCase();
|
|
382
|
+
if (!seedText) return safeSource === 'cli';
|
|
383
|
+
if (safeSource === 'cli') return true;
|
|
384
|
+
return !looksLikeInternalCodexPrompt(seedText);
|
|
385
|
+
})
|
|
386
|
+
.map((row) => {
|
|
387
|
+
const updatedMs = Number(row.updated_at || row.created_at || 0) * 1000;
|
|
388
|
+
const firstPrompt = stripCodexInjectedHints(row.first_user_message || row.title || '');
|
|
389
|
+
const customTitle = stripCodexInjectedHints(row.title || '');
|
|
390
|
+
if (row.rollout_path) {
|
|
391
|
+
_codexRolloutCache.set(String(row.id), { path: String(row.rollout_path), ts: Date.now() });
|
|
392
|
+
}
|
|
393
|
+
return {
|
|
394
|
+
sessionId: String(row.id),
|
|
395
|
+
projectPath: String(row.cwd),
|
|
396
|
+
fileMtime: updatedMs || 0,
|
|
397
|
+
modified: new Date(updatedMs || Date.now()).toISOString(),
|
|
398
|
+
messageCount: row.tokens_used ? '?' : 1,
|
|
399
|
+
customTitle,
|
|
400
|
+
firstPrompt,
|
|
401
|
+
lastUser: firstPrompt,
|
|
402
|
+
_enriched: false,
|
|
403
|
+
engine: 'codex',
|
|
404
|
+
};
|
|
405
|
+
})
|
|
406
|
+
.map((session) => enrichCodexSession(session));
|
|
407
|
+
} catch {
|
|
408
|
+
if (db) { try { db.close(); } catch { /* ignore */ } }
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function findCodexSessionFile(sessionId) {
|
|
414
|
+
if (!sessionId || !fs.existsSync(CODEX_DB)) return null;
|
|
415
|
+
const cached = _codexRolloutCache.get(sessionId);
|
|
416
|
+
if (cached && Date.now() - cached.ts < 30000) return cached.path;
|
|
417
|
+
let db = null;
|
|
418
|
+
try {
|
|
419
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
420
|
+
db = new DatabaseSync(CODEX_DB, { readonly: true });
|
|
421
|
+
const row = db.prepare('SELECT rollout_path FROM threads WHERE id = ?').get(sessionId);
|
|
422
|
+
db.close();
|
|
423
|
+
db = null;
|
|
424
|
+
const rolloutPath = row && row.rollout_path ? String(row.rollout_path) : null;
|
|
425
|
+
_codexRolloutCache.set(sessionId, { path: rolloutPath, ts: Date.now() });
|
|
426
|
+
return rolloutPath;
|
|
427
|
+
} catch {
|
|
428
|
+
if (db) { try { db.close(); } catch { /* ignore */ } }
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function extractCodexMessageText(payload) {
|
|
434
|
+
if (!payload) return '';
|
|
435
|
+
if (typeof payload === 'string') return payload;
|
|
436
|
+
if (Array.isArray(payload)) {
|
|
437
|
+
return payload.map(item => extractCodexMessageText(item)).filter(Boolean).join('\n').trim();
|
|
438
|
+
}
|
|
439
|
+
if (typeof payload !== 'object') return '';
|
|
440
|
+
if (typeof payload.text === 'string') return payload.text;
|
|
441
|
+
if (typeof payload.message === 'string') return payload.message;
|
|
442
|
+
if (payload.type === 'input_text' || payload.type === 'output_text') return String(payload.text || '');
|
|
443
|
+
if (payload.type === 'message' && Array.isArray(payload.content)) return extractCodexMessageText(payload.content);
|
|
444
|
+
if (Array.isArray(payload.content)) return extractCodexMessageText(payload.content);
|
|
445
|
+
if (payload.payload) return extractCodexMessageText(payload.payload);
|
|
446
|
+
return '';
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function parseCodexSessionPreview(sessionFile) {
|
|
450
|
+
try {
|
|
451
|
+
if (!sessionFile || !fs.existsSync(sessionFile)) return null;
|
|
452
|
+
const lines = fs.readFileSync(sessionFile, 'utf8').split('\n').filter(Boolean);
|
|
453
|
+
let firstUser = '';
|
|
454
|
+
let lastUser = '';
|
|
455
|
+
let lastAssistant = '';
|
|
456
|
+
let fallbackAssistant = '';
|
|
457
|
+
for (const line of lines) {
|
|
458
|
+
let entry;
|
|
459
|
+
try {
|
|
460
|
+
entry = JSON.parse(line);
|
|
461
|
+
} catch {
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (entry.type === 'response_item' && entry.payload && entry.payload.type === 'message') {
|
|
465
|
+
const role = String(entry.payload.role || '').toLowerCase();
|
|
466
|
+
const text = stripCodexInjectedHints(extractCodexMessageText(entry.payload.content || entry.payload));
|
|
467
|
+
if (!text) continue;
|
|
468
|
+
if (role === 'user') {
|
|
469
|
+
if (!firstUser) firstUser = text;
|
|
470
|
+
lastUser = text;
|
|
471
|
+
} else if (role === 'assistant') {
|
|
472
|
+
lastAssistant = text;
|
|
473
|
+
}
|
|
474
|
+
} else if (entry.type === 'event_msg' && entry.payload && entry.payload.type === 'agent_message') {
|
|
475
|
+
const text = stripCodexInjectedHints(extractCodexMessageText(entry.payload.message));
|
|
476
|
+
if (text) fallbackAssistant = text;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (!lastAssistant) lastAssistant = fallbackAssistant;
|
|
480
|
+
return (firstUser || lastUser || lastAssistant)
|
|
481
|
+
? { firstUser, lastUser, lastAssistant }
|
|
482
|
+
: null;
|
|
483
|
+
} catch {
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function enrichCodexSession(session) {
|
|
489
|
+
if (!session || session._enriched) return session;
|
|
490
|
+
try {
|
|
491
|
+
const sessionFile = findCodexSessionFile(session.sessionId);
|
|
492
|
+
const preview = parseCodexSessionPreview(sessionFile);
|
|
493
|
+
if (preview) {
|
|
494
|
+
if (preview.firstUser) session.firstPrompt = preview.firstUser.slice(0, 120);
|
|
495
|
+
if (preview.lastUser) session.lastUser = preview.lastUser.slice(0, 120);
|
|
496
|
+
}
|
|
497
|
+
session._enriched = true;
|
|
498
|
+
return session;
|
|
499
|
+
} catch {
|
|
500
|
+
session._enriched = true;
|
|
501
|
+
return session;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
279
504
|
|
|
505
|
+
function scanAllSessions() {
|
|
506
|
+
if (_sessionCache && (Date.now() - _sessionCacheTime < SESSION_CACHE_TTL)) return _sessionCache;
|
|
507
|
+
try {
|
|
508
|
+
const all = [...scanClaudeSessions(), ...scanCodexSessions()];
|
|
509
|
+
all.sort((a, b) => {
|
|
510
|
+
const aTime = a.fileMtime || new Date(a.modified).getTime();
|
|
511
|
+
const bTime = b.fileMtime || new Date(b.modified).getTime();
|
|
512
|
+
return bTime - aTime;
|
|
513
|
+
});
|
|
280
514
|
_sessionCache = all;
|
|
281
515
|
_sessionCacheTime = Date.now();
|
|
282
516
|
return all;
|
|
@@ -285,11 +519,15 @@ function createSessionStore(deps) {
|
|
|
285
519
|
}
|
|
286
520
|
}
|
|
287
521
|
|
|
288
|
-
function listRecentSessions(limit, cwd) {
|
|
522
|
+
function listRecentSessions(limit, cwd, engine) {
|
|
289
523
|
let all = scanAllSessions();
|
|
290
524
|
if (cwd) {
|
|
291
525
|
all = all.filter(s => s.projectPath === cwd);
|
|
292
526
|
}
|
|
527
|
+
if (engine) {
|
|
528
|
+
const safeEngine = String(engine).trim().toLowerCase() === 'codex' ? 'codex' : 'claude';
|
|
529
|
+
all = all.filter(s => (s.engine || 'claude') === safeEngine);
|
|
530
|
+
}
|
|
293
531
|
return all.slice(0, limit || 10);
|
|
294
532
|
}
|
|
295
533
|
|
|
@@ -308,22 +546,34 @@ function createSessionStore(deps) {
|
|
|
308
546
|
return null;
|
|
309
547
|
}
|
|
310
548
|
|
|
549
|
+
function getSessionDisplayTimeMs(session) {
|
|
550
|
+
const realMtime = getSessionFileMtime(session.sessionId, session.projectPath);
|
|
551
|
+
if (Number.isFinite(realMtime) && realMtime > 0) return realMtime;
|
|
552
|
+
if (Number.isFinite(session.fileMtime) && session.fileMtime > 0) return session.fileMtime;
|
|
553
|
+
const modifiedMs = session.modified ? new Date(session.modified).getTime() : NaN;
|
|
554
|
+
if (Number.isFinite(modifiedMs) && modifiedMs > 0) return modifiedMs;
|
|
555
|
+
return Date.now();
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function getSessionRelativeTimeLabel(session) {
|
|
559
|
+
return formatRelativeTime(new Date(getSessionDisplayTimeMs(session)).toISOString());
|
|
560
|
+
}
|
|
561
|
+
|
|
311
562
|
function sessionLabel(s) {
|
|
312
563
|
const name = s.customTitle;
|
|
313
564
|
const proj = s.projectPath ? path.basename(s.projectPath) : '';
|
|
314
|
-
const
|
|
315
|
-
const timeMs = realMtime || s.fileMtime || new Date(s.modified).getTime();
|
|
316
|
-
const ago = formatRelativeTime(new Date(timeMs).toISOString());
|
|
565
|
+
const ago = getSessionRelativeTimeLabel(s);
|
|
317
566
|
const shortId = s.sessionId.slice(0, 4);
|
|
567
|
+
const engineTag = (s.engine || 'claude') === 'codex' ? '[codex] ' : '';
|
|
318
568
|
|
|
319
|
-
if (name) return `${ago} [${name}] ${proj} #${shortId}`;
|
|
569
|
+
if (name) return `${engineTag}${ago} [${name}] ${proj} #${shortId}`;
|
|
320
570
|
|
|
321
571
|
let title = (s.summary || '').slice(0, 20);
|
|
322
572
|
if (!title && s.firstPrompt) {
|
|
323
573
|
title = s.firstPrompt.slice(0, 20);
|
|
324
574
|
if (s.firstPrompt.length > 20) title += '..';
|
|
325
575
|
}
|
|
326
|
-
return `${ago} ${proj ? proj + ': ' : ''}${title || ''} #${shortId}`;
|
|
576
|
+
return `${engineTag}${ago} ${proj ? proj + ': ' : ''}${title || ''} #${shortId}`;
|
|
327
577
|
}
|
|
328
578
|
|
|
329
579
|
function sessionDisplayTitle(s, maxLen, sessionTags) {
|
|
@@ -367,11 +617,10 @@ function createSessionStore(deps) {
|
|
|
367
617
|
sessionTags = sessionTags || loadSessionTags();
|
|
368
618
|
const title = sessionDisplayTitle(s, 50, sessionTags);
|
|
369
619
|
const proj = s.projectPath ? path.basename(s.projectPath) : '~';
|
|
370
|
-
const
|
|
371
|
-
const timeMs = realMtime || s.fileMtime || new Date(s.modified).getTime();
|
|
372
|
-
const ago = formatRelativeTime(new Date(timeMs).toISOString());
|
|
620
|
+
const ago = getSessionRelativeTimeLabel(s);
|
|
373
621
|
const shortId = s.sessionId.slice(0, 8);
|
|
374
622
|
const tags = (sessionTags[s.sessionId] && sessionTags[s.sessionId].tags || []).slice(0, 3);
|
|
623
|
+
const engineLabel = (s.engine || 'claude') === 'codex' ? 'codex' : 'claude';
|
|
375
624
|
|
|
376
625
|
// [M2] 转义 markdown 特殊字符,防止用户历史消息破坏渲染
|
|
377
626
|
const escapeMd = (t) => t.replace(/[_*`\\]/g, '\\$&');
|
|
@@ -379,7 +628,7 @@ function createSessionStore(deps) {
|
|
|
379
628
|
const snippetRaw = s.lastUser || (s.firstPrompt || '').replace(/<[^>]+>/g, '').replace(/\[System hints[\s\S]*/i, '').trim().slice(0, 80);
|
|
380
629
|
let line = `${index}. ${title}${title.length >= 50 ? '..' : ''}`; // [M4] title 已有 sessionId 兜底,不会为空
|
|
381
630
|
if (tags.length) line += ` ${tags.map(t => `#${t}`).join(' ')}`;
|
|
382
|
-
line += `\n 📁${proj} · ${ago}`;
|
|
631
|
+
line += `\n 📁${proj} · ${ago} · ${engineLabel}`;
|
|
383
632
|
if (snippetRaw && snippetRaw.length > 2) {
|
|
384
633
|
const snippet = escapeMd(snippetRaw.replace(/\n/g, ' ').slice(0, 60));
|
|
385
634
|
line += `\n 💬 ${snippet}${snippetRaw.length > 60 ? '…' : ''}`;
|
|
@@ -395,15 +644,14 @@ function createSessionStore(deps) {
|
|
|
395
644
|
if (i > 0) elements.push({ tag: 'hr' });
|
|
396
645
|
const title = sessionDisplayTitle(s, 60, sessionTags);
|
|
397
646
|
const proj = s.projectPath ? path.basename(s.projectPath) : '~';
|
|
398
|
-
const
|
|
399
|
-
const timeMs = realMtime || s.fileMtime || new Date(s.modified).getTime();
|
|
400
|
-
const ago = formatRelativeTime(new Date(timeMs).toISOString());
|
|
647
|
+
const ago = getSessionRelativeTimeLabel(s);
|
|
401
648
|
const shortId = s.sessionId.slice(0, 6);
|
|
402
649
|
const tags = (sessionTags[s.sessionId] && sessionTags[s.sessionId].tags || []).slice(0, 4);
|
|
650
|
+
const engineLabel = (s.engine || 'claude') === 'codex' ? 'codex' : 'claude';
|
|
403
651
|
// [M2] 转义 markdown 特殊字符;[M4] title 已有 sessionId 兜底
|
|
404
652
|
const escapeMd = (t) => t.replace(/[_*`\\]/g, '\\$&');
|
|
405
653
|
const snippetRaw = s.lastUser || (s.firstPrompt || '').replace(/<[^>]+>/g, '').replace(/\[System hints[\s\S]*/i, '').trim().slice(0, 80);
|
|
406
|
-
let desc = `**${i + 1}. ${title}**\n📁${proj} · ${ago}`;
|
|
654
|
+
let desc = `**${i + 1}. ${title}**\n📁${proj} · ${ago} · ${engineLabel}`;
|
|
407
655
|
if (tags.length) desc += `\n${tags.map(t => `\`${t}\``).join(' ')}`;
|
|
408
656
|
if (snippetRaw && snippetRaw.length > 2) {
|
|
409
657
|
const snippet = escapeMd(snippetRaw.replace(/\n/g, ' ').slice(0, 60));
|
|
@@ -451,22 +699,59 @@ function createSessionStore(deps) {
|
|
|
451
699
|
function getSessionForEngine(chatId, engine) {
|
|
452
700
|
const raw = getSession(chatId);
|
|
453
701
|
if (!raw) return null;
|
|
454
|
-
const safeEngine =
|
|
702
|
+
const safeEngine = normalizeEngineName(engine);
|
|
455
703
|
if (!raw.engines) return { cwd: raw.cwd, engine: safeEngine, id: raw.id || null, started: !!raw.started };
|
|
456
704
|
const slot = raw.engines[safeEngine] || {};
|
|
457
|
-
return {
|
|
705
|
+
return {
|
|
706
|
+
cwd: raw.cwd,
|
|
707
|
+
engine: safeEngine,
|
|
708
|
+
...slot,
|
|
709
|
+
id: slot.id || null,
|
|
710
|
+
started: !!slot.started,
|
|
711
|
+
};
|
|
458
712
|
}
|
|
459
713
|
|
|
460
|
-
function
|
|
714
|
+
function upgradeSessionRecord(raw = {}, fallbackEngine = 'claude') {
|
|
715
|
+
const safeEngine = normalizeEngineName(fallbackEngine);
|
|
716
|
+
if (raw.engines && typeof raw.engines === 'object') {
|
|
717
|
+
return {
|
|
718
|
+
cwd: sanitizeCwd(raw.cwd),
|
|
719
|
+
engines: { ...raw.engines },
|
|
720
|
+
...(raw.last_active ? { last_active: raw.last_active } : {}),
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
const slot = {
|
|
724
|
+
id: raw.id || null,
|
|
725
|
+
started: !!raw.started,
|
|
726
|
+
};
|
|
727
|
+
if (safeEngine === 'codex') {
|
|
728
|
+
const permissionMeta = normalizeCodexPermissionMeta(raw);
|
|
729
|
+
if (permissionMeta) Object.assign(slot, permissionMeta);
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
cwd: sanitizeCwd(raw.cwd),
|
|
733
|
+
engines: { [safeEngine]: slot },
|
|
734
|
+
...(raw.last_active ? { last_active: raw.last_active } : {}),
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function createSession(chatId, cwd, name, engine = 'claude', meta = {}) {
|
|
461
739
|
const state = loadState();
|
|
462
|
-
const safeEngine =
|
|
740
|
+
const safeEngine = normalizeEngineName(engine);
|
|
463
741
|
const safeCwd = sanitizeCwd(cwd);
|
|
464
742
|
const sessionId = crypto.randomUUID();
|
|
465
|
-
const existing = state.sessions[chatId] || {};
|
|
743
|
+
const existing = upgradeSessionRecord(state.sessions[chatId] || {}, safeEngine);
|
|
466
744
|
const existingEngines = existing.engines || {};
|
|
745
|
+
const nextSlot = { id: sessionId, started: false };
|
|
746
|
+
if (safeEngine === 'codex') {
|
|
747
|
+
nextSlot.runtimeSessionObserved = false;
|
|
748
|
+
const permissionMeta = normalizeCodexPermissionMeta(meta);
|
|
749
|
+
if (permissionMeta) Object.assign(nextSlot, permissionMeta);
|
|
750
|
+
}
|
|
467
751
|
state.sessions[chatId] = {
|
|
468
752
|
cwd: safeCwd,
|
|
469
|
-
engines: { ...existingEngines, [safeEngine]:
|
|
753
|
+
engines: { ...existingEngines, [safeEngine]: nextSlot },
|
|
754
|
+
last_active: Date.now(),
|
|
470
755
|
};
|
|
471
756
|
saveState(state);
|
|
472
757
|
invalidateSessionCache();
|
|
@@ -475,6 +760,67 @@ function createSessionStore(deps) {
|
|
|
475
760
|
return getSessionForEngine(chatId, safeEngine);
|
|
476
761
|
}
|
|
477
762
|
|
|
763
|
+
function restoreSessionFromReply(chatId, mapped = {}) {
|
|
764
|
+
if (!chatId || !mapped) return null;
|
|
765
|
+
const safeEngine = normalizeEngineName(mapped.engine);
|
|
766
|
+
const state = loadState();
|
|
767
|
+
if (!state.sessions) state.sessions = {};
|
|
768
|
+
const logicalChatId = String(mapped.logicalChatId || '').trim();
|
|
769
|
+
const targetChatId = logicalChatId || String(chatId);
|
|
770
|
+
const base = upgradeSessionRecord(state.sessions[targetChatId] || {}, safeEngine);
|
|
771
|
+
const logicalBase = logicalChatId
|
|
772
|
+
? upgradeSessionRecord(state.sessions[logicalChatId] || {}, safeEngine)
|
|
773
|
+
: null;
|
|
774
|
+
const logicalSlot = logicalBase && logicalBase.engines
|
|
775
|
+
? (logicalBase.engines[safeEngine] || null)
|
|
776
|
+
: null;
|
|
777
|
+
const effectiveMapped = (logicalSlot && logicalSlot.id)
|
|
778
|
+
? {
|
|
779
|
+
...mapped,
|
|
780
|
+
...logicalSlot,
|
|
781
|
+
id: String(logicalSlot.id),
|
|
782
|
+
cwd: logicalBase.cwd || mapped.cwd,
|
|
783
|
+
engine: safeEngine,
|
|
784
|
+
logicalChatId,
|
|
785
|
+
}
|
|
786
|
+
: mapped;
|
|
787
|
+
const resolvedId = String(effectiveMapped.id || '').trim();
|
|
788
|
+
const resolvedCwd = sanitizeCwd(effectiveMapped.cwd || base.cwd);
|
|
789
|
+
if (!resolvedId && !resolvedCwd) return null;
|
|
790
|
+
const restoredSlot = {
|
|
791
|
+
...(base.engines[safeEngine] || {}),
|
|
792
|
+
...(resolvedId ? { id: resolvedId } : {}),
|
|
793
|
+
started: true,
|
|
794
|
+
};
|
|
795
|
+
if (safeEngine === 'codex') {
|
|
796
|
+
restoredSlot.runtimeSessionObserved = !!resolvedId;
|
|
797
|
+
const permissionMeta = normalizeCodexPermissionMeta(effectiveMapped) || normalizeCodexPermissionMeta(restoredSlot);
|
|
798
|
+
if (permissionMeta) Object.assign(restoredSlot, permissionMeta);
|
|
799
|
+
}
|
|
800
|
+
const restoredRecord = {
|
|
801
|
+
cwd: resolvedCwd,
|
|
802
|
+
engines: {
|
|
803
|
+
...base.engines,
|
|
804
|
+
[safeEngine]: restoredSlot,
|
|
805
|
+
},
|
|
806
|
+
last_active: Date.now(),
|
|
807
|
+
};
|
|
808
|
+
state.sessions[targetChatId] = restoredRecord;
|
|
809
|
+
if (String(chatId) !== targetChatId) {
|
|
810
|
+
const aliasBase = upgradeSessionRecord(state.sessions[chatId] || {}, safeEngine);
|
|
811
|
+
state.sessions[chatId] = {
|
|
812
|
+
cwd: restoredRecord.cwd,
|
|
813
|
+
engines: {
|
|
814
|
+
...aliasBase.engines,
|
|
815
|
+
[safeEngine]: { ...(aliasBase.engines[safeEngine] || {}), ...restoredSlot },
|
|
816
|
+
},
|
|
817
|
+
last_active: restoredRecord.last_active,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
saveState(state);
|
|
821
|
+
return getSessionForEngine(targetChatId, safeEngine);
|
|
822
|
+
}
|
|
823
|
+
|
|
478
824
|
function getSessionName(sessionId) {
|
|
479
825
|
try {
|
|
480
826
|
if (!fs.existsSync(CLAUDE_PROJECTS_DIR)) return '';
|
|
@@ -517,39 +863,47 @@ function createSessionStore(deps) {
|
|
|
517
863
|
function getSessionRecentContext(sessionId) {
|
|
518
864
|
try {
|
|
519
865
|
const sessionFile = findSessionFile(sessionId);
|
|
520
|
-
if (
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
try {
|
|
526
|
-
fs.readSync(fd, buf, 0, tailSize, stat.size - tailSize);
|
|
527
|
-
} finally {
|
|
528
|
-
fs.closeSync(fd);
|
|
529
|
-
}
|
|
530
|
-
const lines = buf.toString('utf8').split('\n').reverse();
|
|
531
|
-
// [M3] 复用共享函数,统一截取逻辑
|
|
532
|
-
const lastUser = extractLastUserFromLines(lines);
|
|
533
|
-
let lastAssistant = '';
|
|
534
|
-
for (const line of lines) {
|
|
535
|
-
if (!line.trim()) continue;
|
|
866
|
+
if (sessionFile) {
|
|
867
|
+
const stat = fs.statSync(sessionFile);
|
|
868
|
+
const tailSize = Math.min(262144, stat.size); // 256KB for better coverage of tool-heavy sessions
|
|
869
|
+
const buf = Buffer.alloc(tailSize);
|
|
870
|
+
const fd = fs.openSync(sessionFile, 'r');
|
|
536
871
|
try {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
872
|
+
fs.readSync(fd, buf, 0, tailSize, stat.size - tailSize);
|
|
873
|
+
} finally {
|
|
874
|
+
fs.closeSync(fd);
|
|
875
|
+
}
|
|
876
|
+
const lines = buf.toString('utf8').split('\n').reverse();
|
|
877
|
+
// [M3] 复用共享函数,统一截取逻辑
|
|
878
|
+
const lastUser = extractLastUserFromLines(lines);
|
|
879
|
+
let lastAssistant = '';
|
|
880
|
+
for (const line of lines) {
|
|
881
|
+
if (!line.trim()) continue;
|
|
882
|
+
try {
|
|
883
|
+
const d = JSON.parse(line);
|
|
884
|
+
if (!lastAssistant && d.type === 'assistant' && d.message) {
|
|
885
|
+
const content = d.message.content;
|
|
886
|
+
if (Array.isArray(content)) {
|
|
887
|
+
for (const c of content) {
|
|
888
|
+
if (c.type === 'text' && c.text && c.text.trim().length > 2) {
|
|
889
|
+
lastAssistant = c.text.trim().slice(0, 80);
|
|
890
|
+
break;
|
|
891
|
+
}
|
|
545
892
|
}
|
|
546
893
|
}
|
|
547
894
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
895
|
+
if (lastAssistant) break;
|
|
896
|
+
} catch { /* skip bad line */ }
|
|
897
|
+
}
|
|
898
|
+
return (lastUser || lastAssistant) ? { lastUser, lastAssistant } : null;
|
|
551
899
|
}
|
|
552
|
-
|
|
900
|
+
const codexFile = findCodexSessionFile(sessionId);
|
|
901
|
+
const preview = parseCodexSessionPreview(codexFile);
|
|
902
|
+
if (!preview) return null;
|
|
903
|
+
return {
|
|
904
|
+
lastUser: (preview.lastUser || '').slice(0, 80),
|
|
905
|
+
lastAssistant: (preview.lastAssistant || '').slice(0, 80),
|
|
906
|
+
};
|
|
553
907
|
} catch { return null; }
|
|
554
908
|
}
|
|
555
909
|
|
|
@@ -558,11 +912,19 @@ function createSessionStore(deps) {
|
|
|
558
912
|
const s = state.sessions[chatId];
|
|
559
913
|
if (!s) return;
|
|
560
914
|
if (s.engines) {
|
|
561
|
-
const safeEngine =
|
|
915
|
+
const safeEngine = normalizeEngineName(engine);
|
|
562
916
|
if (!s.engines[safeEngine]) s.engines[safeEngine] = {};
|
|
563
|
-
s.engines[safeEngine]
|
|
917
|
+
const slot = s.engines[safeEngine];
|
|
918
|
+
if (safeEngine === 'codex' && slot.runtimeSessionObserved === false) {
|
|
919
|
+
s.last_active = Date.now();
|
|
920
|
+
saveState(state);
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
slot.started = true;
|
|
924
|
+
s.last_active = Date.now();
|
|
564
925
|
} else {
|
|
565
926
|
s.started = true; // old flat format
|
|
927
|
+
s.last_active = Date.now();
|
|
566
928
|
}
|
|
567
929
|
saveState(state);
|
|
568
930
|
}
|
|
@@ -581,6 +943,27 @@ function createSessionStore(deps) {
|
|
|
581
943
|
return !!valid;
|
|
582
944
|
}
|
|
583
945
|
|
|
946
|
+
function _readClaudeSessionMetadata(sessionFile) {
|
|
947
|
+
const content = fs.readFileSync(sessionFile, 'utf8');
|
|
948
|
+
const lines = content.split('\n').filter(l => l.trim());
|
|
949
|
+
const metadata = {
|
|
950
|
+
lines,
|
|
951
|
+
cwd: '',
|
|
952
|
+
model: '',
|
|
953
|
+
};
|
|
954
|
+
for (const line of lines.slice(0, 20)) {
|
|
955
|
+
try {
|
|
956
|
+
const entry = JSON.parse(line);
|
|
957
|
+
const fileCwd = entry.cwd || (entry.message && entry.message.cwd);
|
|
958
|
+
if (!metadata.cwd && fileCwd) metadata.cwd = fileCwd;
|
|
959
|
+
const sessionModel = entry && entry.message && entry.message.model;
|
|
960
|
+
if (!metadata.model && sessionModel && sessionModel !== '<synthetic>') metadata.model = sessionModel;
|
|
961
|
+
if (metadata.cwd && metadata.model) break;
|
|
962
|
+
} catch { /* skip non-JSON lines */ }
|
|
963
|
+
}
|
|
964
|
+
return metadata;
|
|
965
|
+
}
|
|
966
|
+
|
|
584
967
|
// Claude backend: JSONL files under ~/.claude/projects/<hash>/
|
|
585
968
|
// Best approach: read cwd directly from session file content (not from dir name)
|
|
586
969
|
function _isClaudeSessionValid(sessionId, normCwd) {
|
|
@@ -588,10 +971,12 @@ function createSessionStore(deps) {
|
|
|
588
971
|
const sessionFile = findSessionFile(sessionId);
|
|
589
972
|
if (!sessionFile) return false;
|
|
590
973
|
|
|
591
|
-
// Try to read cwd from session JSONL file content (most reliable)
|
|
592
|
-
const
|
|
593
|
-
|
|
594
|
-
|
|
974
|
+
// Try to read cwd/model from session JSONL file content (most reliable)
|
|
975
|
+
const metadata = _readClaudeSessionMetadata(sessionFile);
|
|
976
|
+
if (metadata.model && !metadata.model.startsWith('claude-')) return false;
|
|
977
|
+
if (metadata.cwd && path.resolve(metadata.cwd) === normCwd) return true;
|
|
978
|
+
if (metadata.cwd) return false;
|
|
979
|
+
for (const line of metadata.lines.slice(0, 20)) { // preserve tolerant parsing for malformed heads
|
|
595
980
|
try {
|
|
596
981
|
const entry = JSON.parse(line);
|
|
597
982
|
const fileCwd = entry.cwd || (entry.message && entry.message.cwd);
|
|
@@ -624,8 +1009,6 @@ function createSessionStore(deps) {
|
|
|
624
1009
|
}
|
|
625
1010
|
}
|
|
626
1011
|
|
|
627
|
-
// Codex backend: SQLite index at ~/.codex/state_5.sqlite
|
|
628
|
-
const CODEX_DB = path.join(HOME, '.codex', 'state_5.sqlite');
|
|
629
1012
|
function _isCodexSessionValid(sessionId, normCwd) {
|
|
630
1013
|
let db = null;
|
|
631
1014
|
try {
|
|
@@ -645,6 +1028,42 @@ function createSessionStore(deps) {
|
|
|
645
1028
|
}
|
|
646
1029
|
}
|
|
647
1030
|
|
|
1031
|
+
function getCodexSessionSandboxProfile(sessionId) {
|
|
1032
|
+
let db = null;
|
|
1033
|
+
try {
|
|
1034
|
+
if (!sessionId) return null;
|
|
1035
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
1036
|
+
db = new DatabaseSync(CODEX_DB, { readonly: true });
|
|
1037
|
+
const row = db.prepare('SELECT sandbox_policy, approval_mode FROM threads WHERE id = ?').get(sessionId);
|
|
1038
|
+
db.close();
|
|
1039
|
+
db = null;
|
|
1040
|
+
if (!row || !row.sandbox_policy) return null;
|
|
1041
|
+
const policy = JSON.parse(String(row.sandbox_policy));
|
|
1042
|
+
const sandboxMode = normalizeCodexSandboxMode(
|
|
1043
|
+
policy && (policy.type || policy.mode || policy.sandbox_mode || policy.sandboxMode),
|
|
1044
|
+
null
|
|
1045
|
+
);
|
|
1046
|
+
const approvalPolicy = normalizeCodexApprovalPolicy(
|
|
1047
|
+
(policy && (policy.approval_policy || policy.approvalPolicy || policy.ask_for_approval)) || row.approval_mode,
|
|
1048
|
+
null
|
|
1049
|
+
);
|
|
1050
|
+
if (!sandboxMode && !approvalPolicy) return null;
|
|
1051
|
+
return {
|
|
1052
|
+
sandboxMode: sandboxMode || 'danger-full-access',
|
|
1053
|
+
approvalPolicy,
|
|
1054
|
+
permissionMode: sandboxMode || 'danger-full-access',
|
|
1055
|
+
};
|
|
1056
|
+
} catch {
|
|
1057
|
+
if (db) { try { db.close(); } catch { /* ignore */ } }
|
|
1058
|
+
return null;
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function getCodexSessionPermissionMode(sessionId) {
|
|
1063
|
+
const profile = getCodexSessionSandboxProfile(sessionId);
|
|
1064
|
+
return profile ? profile.permissionMode : null;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
648
1067
|
function isEngineSessionValid(engine, sessionId, cwd) {
|
|
649
1068
|
if (!sessionId || !cwd || sessionId === '__continue__') return true;
|
|
650
1069
|
const normCwd = path.resolve(cwd);
|
|
@@ -659,6 +1078,7 @@ function createSessionStore(deps) {
|
|
|
659
1078
|
|
|
660
1079
|
return {
|
|
661
1080
|
findSessionFile,
|
|
1081
|
+
findCodexSessionFile,
|
|
662
1082
|
clearSessionFileCache,
|
|
663
1083
|
truncateSessionToCheckpoint,
|
|
664
1084
|
watchSessionFiles,
|
|
@@ -673,11 +1093,22 @@ function createSessionStore(deps) {
|
|
|
673
1093
|
getSession,
|
|
674
1094
|
getSessionForEngine,
|
|
675
1095
|
createSession,
|
|
1096
|
+
restoreSessionFromReply,
|
|
676
1097
|
getSessionName,
|
|
677
1098
|
writeSessionName,
|
|
678
1099
|
markSessionStarted,
|
|
679
1100
|
getSessionRecentContext,
|
|
680
1101
|
isEngineSessionValid,
|
|
1102
|
+
getCodexSessionSandboxProfile,
|
|
1103
|
+
getCodexSessionPermissionMode,
|
|
1104
|
+
_private: {
|
|
1105
|
+
_readClaudeSessionMetadata,
|
|
1106
|
+
_isClaudeSessionValid,
|
|
1107
|
+
upgradeSessionRecord,
|
|
1108
|
+
stripCodexInjectedHints,
|
|
1109
|
+
looksLikeInternalCodexPrompt,
|
|
1110
|
+
parseCodexSessionPreview,
|
|
1111
|
+
},
|
|
681
1112
|
};
|
|
682
1113
|
}
|
|
683
1114
|
|