ec-web 0.0.4 → 0.0.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.
- package/dist/server.js +63 -6
- package/dist/sources/msg.js +371 -14
- package/dist/sources/session-codex.js +115 -35
- package/dist/sources/session-origin.js +195 -0
- package/dist/sources/session.js +292 -41
- package/dist/sources/transcript-window.js +35 -0
- package/dist/static/app.js +456 -62
- package/dist/static/index.html +4 -5
- package/dist/static/style.css +153 -63
- package/dist/static/vendor/echarts-5.6.0.min.js +45 -0
- package/package.json +2 -2
|
@@ -12,7 +12,10 @@ import path from 'path';
|
|
|
12
12
|
import os from 'os';
|
|
13
13
|
import { createRequire } from 'module';
|
|
14
14
|
import { resolvePaths } from '../paths.js';
|
|
15
|
-
import { encodePath
|
|
15
|
+
import { encodePath } from '../fs-utils.js';
|
|
16
|
+
import { getAgentSelfNames } from './msg.js';
|
|
17
|
+
import { parseTranscriptResultKey, previewLargeToolResults, windowTranscriptTurns } from './transcript-window.js';
|
|
18
|
+
import { buildSessionBindMap, classifyTranscriptSource, isEvolcoreRoutedMessage, isSyntheticCliPrompt, resolveSessionBindSelfName, } from './session-origin.js';
|
|
16
19
|
const requireFromHere = createRequire(import.meta.url);
|
|
17
20
|
let _dlog = null;
|
|
18
21
|
export function setDebugLog(log) { _dlog = log; }
|
|
@@ -107,8 +110,10 @@ function listProjects() {
|
|
|
107
110
|
}
|
|
108
111
|
}
|
|
109
112
|
// ── 会话列表 ──
|
|
110
|
-
const CACHE_VERSION =
|
|
113
|
+
const CACHE_VERSION = 2;
|
|
111
114
|
const _metaCache = new Map();
|
|
115
|
+
const _detailCache = new Map();
|
|
116
|
+
const MAX_DETAIL_CACHE_ENTRIES = 6;
|
|
112
117
|
function cacheDir(encoded) {
|
|
113
118
|
const dataDir = resolvePaths().dataDir;
|
|
114
119
|
return path.join(dataDir, 'ecweb-cache', 'codex', encoded);
|
|
@@ -132,6 +137,16 @@ function writeDiskCache(encoded, id, mtime, size, meta) {
|
|
|
132
137
|
}
|
|
133
138
|
catch { }
|
|
134
139
|
}
|
|
140
|
+
function cacheTranscriptDetail(file, mtime, size, detail) {
|
|
141
|
+
_detailCache.delete(file);
|
|
142
|
+
_detailCache.set(file, { mtime, size, detail });
|
|
143
|
+
while (_detailCache.size > MAX_DETAIL_CACHE_ENTRIES) {
|
|
144
|
+
const oldest = _detailCache.keys().next().value;
|
|
145
|
+
if (!oldest)
|
|
146
|
+
break;
|
|
147
|
+
_detailCache.delete(oldest);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
135
150
|
function pruneCacheFiles(encoded, validIds) {
|
|
136
151
|
const dir = cacheDir(encoded);
|
|
137
152
|
try {
|
|
@@ -170,6 +185,7 @@ function extractMetaFromRollout(file, id) {
|
|
|
170
185
|
}
|
|
171
186
|
catch { }
|
|
172
187
|
let title = '', firstUser = '', gitBranch = '', version = '', userMsgs = 0, totalMsgs = 0;
|
|
188
|
+
let hasRealUserInput = false;
|
|
173
189
|
const lines = raw.split('\n');
|
|
174
190
|
for (const line of lines) {
|
|
175
191
|
if (!line.trim())
|
|
@@ -187,6 +203,8 @@ function extractMetaFromRollout(file, id) {
|
|
|
187
203
|
if (event.type === 'event_msg' && event.payload?.type === 'user_message' && event.payload.message) {
|
|
188
204
|
totalMsgs++;
|
|
189
205
|
userMsgs++;
|
|
206
|
+
if (!isSyntheticCliPrompt(event.payload.message, 'codex'))
|
|
207
|
+
hasRealUserInput = true;
|
|
190
208
|
if (!firstUser) {
|
|
191
209
|
const text = event.payload.message.trim().replace(/\s+/g, ' ');
|
|
192
210
|
firstUser = text.substring(0, 120);
|
|
@@ -216,15 +234,34 @@ function extractMetaFromRollout(file, id) {
|
|
|
216
234
|
totalMsgs,
|
|
217
235
|
sizeKB: Math.round(stat.size / 1024),
|
|
218
236
|
lastActivity: stat.mtimeMs,
|
|
237
|
+
hasRealUserInput,
|
|
238
|
+
backendSource: '',
|
|
239
|
+
threadSource: '',
|
|
240
|
+
evolcoreRouted: false,
|
|
219
241
|
};
|
|
220
242
|
}
|
|
243
|
+
function withThreadSource(meta, row) {
|
|
244
|
+
return {
|
|
245
|
+
...meta,
|
|
246
|
+
backendSource: typeof row.source === 'string' ? row.source : '',
|
|
247
|
+
threadSource: typeof row.thread_source === 'string' ? row.thread_source : '',
|
|
248
|
+
evolcoreRouted: isEvolcoreRoutedMessage(row.first_user_message),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function isCodexCliCandidate(meta) {
|
|
252
|
+
if (meta.threadSource === 'subagent')
|
|
253
|
+
return false;
|
|
254
|
+
if (meta.backendSource !== 'cli' && meta.backendSource !== 'vscode')
|
|
255
|
+
return false;
|
|
256
|
+
return meta.hasRealUserInput && !meta.evolcoreRouted;
|
|
257
|
+
}
|
|
221
258
|
function listTranscripts(encoded, cwd) {
|
|
222
259
|
const db = getDb();
|
|
223
260
|
if (!db)
|
|
224
261
|
return [];
|
|
225
262
|
try {
|
|
226
263
|
const rows = db.prepare(`
|
|
227
|
-
SELECT id, title, first_user_message, updated_at, rollout_path
|
|
264
|
+
SELECT id, title, first_user_message, updated_at, rollout_path, source, thread_source
|
|
228
265
|
FROM threads
|
|
229
266
|
WHERE cwd = ? AND archived = 0
|
|
230
267
|
ORDER BY updated_at DESC
|
|
@@ -248,6 +285,10 @@ function listTranscripts(encoded, cwd) {
|
|
|
248
285
|
totalMsgs: 0,
|
|
249
286
|
sizeKB: 0,
|
|
250
287
|
lastActivity: (row.updated_at || 0) * 1000,
|
|
288
|
+
hasRealUserInput: !!firstUser && !isSyntheticCliPrompt(firstUser, 'codex'),
|
|
289
|
+
backendSource: typeof row.source === 'string' ? row.source : '',
|
|
290
|
+
threadSource: typeof row.thread_source === 'string' ? row.thread_source : '',
|
|
291
|
+
evolcoreRouted: isEvolcoreRoutedMessage(row.first_user_message),
|
|
251
292
|
});
|
|
252
293
|
continue;
|
|
253
294
|
}
|
|
@@ -261,21 +302,21 @@ function listTranscripts(encoded, cwd) {
|
|
|
261
302
|
const mtime = stat.mtimeMs;
|
|
262
303
|
const mem = _metaCache.get(rolloutPath);
|
|
263
304
|
if (mem && mem.mtime === mtime) {
|
|
264
|
-
out.push(mem.meta);
|
|
305
|
+
out.push(withThreadSource(mem.meta, row));
|
|
265
306
|
hitMem++;
|
|
266
307
|
continue;
|
|
267
308
|
}
|
|
268
309
|
const disk = readDiskCache(encoded, id, mtime, stat.size);
|
|
269
310
|
if (disk) {
|
|
270
311
|
_metaCache.set(rolloutPath, { mtime, meta: disk });
|
|
271
|
-
out.push(disk);
|
|
312
|
+
out.push(withThreadSource(disk, row));
|
|
272
313
|
hitDisk++;
|
|
273
314
|
continue;
|
|
274
315
|
}
|
|
275
316
|
const meta = extractMetaFromRollout(rolloutPath, id);
|
|
276
317
|
_metaCache.set(rolloutPath, { mtime, meta });
|
|
277
318
|
writeDiskCache(encoded, id, mtime, stat.size, meta);
|
|
278
|
-
out.push(meta);
|
|
319
|
+
out.push(withThreadSource(meta, row));
|
|
279
320
|
parsed++;
|
|
280
321
|
}
|
|
281
322
|
dlog(`[codex] listTranscripts ${cwd.slice(-20)}: ${rows.length} threads (mem=${hitMem} disk=${hitDisk} parsed=${parsed})`);
|
|
@@ -288,25 +329,7 @@ function listTranscripts(encoded, cwd) {
|
|
|
288
329
|
}
|
|
289
330
|
// ── 绑定关系 ──
|
|
290
331
|
export function buildBindMap() {
|
|
291
|
-
|
|
292
|
-
try {
|
|
293
|
-
const p = resolvePaths();
|
|
294
|
-
for (const dir of scanChatDirs(p.sessionsDir)) {
|
|
295
|
-
const active = readJsonFile(path.join(dir.dirPath, 'active.json'));
|
|
296
|
-
if (!active || !active.agentSessionId)
|
|
297
|
-
continue;
|
|
298
|
-
map.set(active.agentSessionId, {
|
|
299
|
-
channelType: active.channelType || dir.channelType,
|
|
300
|
-
channelId: active.channelId || dir.channelId,
|
|
301
|
-
selfAID: active.selfAID || dir.selfAID,
|
|
302
|
-
peerName: (active.metadata && active.metadata.peerName) || null,
|
|
303
|
-
name: active.name,
|
|
304
|
-
updatedAt: active.updatedAt || 0,
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
catch { }
|
|
309
|
-
return map;
|
|
332
|
+
return buildSessionBindMap();
|
|
310
333
|
}
|
|
311
334
|
// ── 项目解析 ──
|
|
312
335
|
function resolveProject(params, projects) {
|
|
@@ -452,7 +475,7 @@ export function parseCodexToolEvent(event) {
|
|
|
452
475
|
}
|
|
453
476
|
return null;
|
|
454
477
|
}
|
|
455
|
-
function readTranscriptFile(threadId, cwd) {
|
|
478
|
+
function readTranscriptFile(threadId, cwd, readStats) {
|
|
456
479
|
const empty = {
|
|
457
480
|
turns: [],
|
|
458
481
|
totalTurns: 0,
|
|
@@ -485,6 +508,21 @@ function readTranscriptFile(threadId, cwd) {
|
|
|
485
508
|
}
|
|
486
509
|
if (!fs.existsSync(rolloutPath))
|
|
487
510
|
return empty;
|
|
511
|
+
let stat;
|
|
512
|
+
try {
|
|
513
|
+
stat = fs.statSync(rolloutPath);
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return empty;
|
|
517
|
+
}
|
|
518
|
+
const cached = _detailCache.get(rolloutPath);
|
|
519
|
+
if (cached && cached.mtime === stat.mtimeMs && cached.size === stat.size) {
|
|
520
|
+
_detailCache.delete(rolloutPath);
|
|
521
|
+
_detailCache.set(rolloutPath, cached);
|
|
522
|
+
if (readStats)
|
|
523
|
+
readStats.detailCacheHit = true;
|
|
524
|
+
return { ...cached.detail, title: empty.title || cached.detail.title, cwd };
|
|
525
|
+
}
|
|
488
526
|
let raw;
|
|
489
527
|
try {
|
|
490
528
|
raw = fs.readFileSync(rolloutPath, 'utf-8');
|
|
@@ -592,9 +630,8 @@ function readTranscriptFile(threadId, cwd) {
|
|
|
592
630
|
}
|
|
593
631
|
}
|
|
594
632
|
}
|
|
595
|
-
const
|
|
596
|
-
|
|
597
|
-
turns: shown,
|
|
633
|
+
const detail = {
|
|
634
|
+
turns,
|
|
598
635
|
totalTurns: turns.length,
|
|
599
636
|
userMsgs,
|
|
600
637
|
totalMsgs,
|
|
@@ -609,13 +646,41 @@ function readTranscriptFile(threadId, cwd) {
|
|
|
609
646
|
title: empty.title,
|
|
610
647
|
cwd,
|
|
611
648
|
};
|
|
649
|
+
cacheTranscriptDetail(rolloutPath, stat.mtimeMs, stat.size, detail);
|
|
650
|
+
return detail;
|
|
651
|
+
}
|
|
652
|
+
export function readCodexSessionResult(params) {
|
|
653
|
+
const projects = listProjects();
|
|
654
|
+
const project = resolveProject(params, projects);
|
|
655
|
+
const sessionId = typeof params.sessionId === 'string' ? params.sessionId : '';
|
|
656
|
+
const key = parseTranscriptResultKey(params.resultKey);
|
|
657
|
+
if (!project || !sessionId || !key || !/^[A-Za-z0-9-]+$/.test(sessionId))
|
|
658
|
+
return null;
|
|
659
|
+
const detail = readTranscriptFile(sessionId, project.cwd);
|
|
660
|
+
const block = detail.turns[key[0]]?.blocks?.[key[1]];
|
|
661
|
+
return block?.kind === 'tool_result' && typeof block.text === 'string' ? block.text : null;
|
|
612
662
|
}
|
|
613
663
|
// ── Snapshot ──
|
|
614
|
-
function buildSnapshot(params) {
|
|
664
|
+
async function buildSnapshot(params) {
|
|
665
|
+
const startedAt = Date.now();
|
|
666
|
+
const metrics = {
|
|
667
|
+
baseagent: 'codex',
|
|
668
|
+
projectMs: 0,
|
|
669
|
+
bindingsMs: 0,
|
|
670
|
+
transcriptListMs: 0,
|
|
671
|
+
detailMs: 0,
|
|
672
|
+
totalMs: 0,
|
|
673
|
+
detailCacheHit: false,
|
|
674
|
+
};
|
|
675
|
+
const projectStartedAt = Date.now();
|
|
615
676
|
const projects = listProjects();
|
|
677
|
+
metrics.projectMs = Date.now() - projectStartedAt;
|
|
678
|
+
const bindingsStartedAt = Date.now();
|
|
616
679
|
const bindMap = buildBindMap();
|
|
680
|
+
metrics.bindingsMs = Date.now() - bindingsStartedAt;
|
|
617
681
|
const project = resolveProject(params, projects);
|
|
618
682
|
if (!project) {
|
|
683
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
619
684
|
return {
|
|
620
685
|
baseagent: 'codex',
|
|
621
686
|
projects: [],
|
|
@@ -623,16 +688,22 @@ function buildSnapshot(params) {
|
|
|
623
688
|
transcripts: [],
|
|
624
689
|
turns: [],
|
|
625
690
|
sessionId: null,
|
|
691
|
+
metrics,
|
|
626
692
|
};
|
|
627
693
|
}
|
|
694
|
+
const transcriptListStartedAt = Date.now();
|
|
628
695
|
const metas = listTranscripts(project.encoded, project.cwd);
|
|
696
|
+
metrics.transcriptListMs = Date.now() - transcriptListStartedAt;
|
|
697
|
+
const selfNames = await getAgentSelfNames();
|
|
629
698
|
const transcripts = metas.map(m => {
|
|
630
699
|
const bind = bindMap.get(m.id);
|
|
700
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
631
701
|
return {
|
|
632
702
|
...m,
|
|
703
|
+
sourceType: classifyTranscriptSource(bind, isCodexCliCandidate(m)),
|
|
633
704
|
bound: !!bind,
|
|
634
705
|
boundChannel: bind?.channelType ?? null,
|
|
635
|
-
|
|
706
|
+
boundSelf: resolveSessionBindSelfName(bind, selfNames),
|
|
636
707
|
online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false,
|
|
637
708
|
};
|
|
638
709
|
});
|
|
@@ -651,10 +722,15 @@ function buildSnapshot(params) {
|
|
|
651
722
|
transcripts,
|
|
652
723
|
turns: [],
|
|
653
724
|
sessionId: null,
|
|
725
|
+
metrics,
|
|
654
726
|
};
|
|
655
727
|
}
|
|
656
|
-
const
|
|
728
|
+
const detailStartedAt = Date.now();
|
|
729
|
+
const detail = readTranscriptFile(sessionId, project.cwd, metrics);
|
|
730
|
+
metrics.detailMs = Date.now() - detailStartedAt;
|
|
731
|
+
const page = windowTranscriptTurns(detail.turns, params.turnLimit, params.turnBefore);
|
|
657
732
|
const bind = bindMap.get(sessionId);
|
|
733
|
+
const transcript = metas.find(meta => meta.id === sessionId);
|
|
658
734
|
const header = {
|
|
659
735
|
sessionId,
|
|
660
736
|
title: detail.title,
|
|
@@ -670,19 +746,23 @@ function buildSnapshot(params) {
|
|
|
670
746
|
outputTokens: detail.outputTokens,
|
|
671
747
|
contextTokens: detail.contextTokens,
|
|
672
748
|
costUsd: detail.costUsd,
|
|
749
|
+
sourceType: classifyTranscriptSource(bind, transcript ? isCodexCliCandidate(transcript) : false),
|
|
673
750
|
bound: !!bind,
|
|
674
751
|
boundChannel: bind?.channelType ?? null,
|
|
675
|
-
|
|
752
|
+
boundSelf: resolveSessionBindSelfName(bind, selfNames),
|
|
676
753
|
online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false,
|
|
677
754
|
};
|
|
755
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
678
756
|
return {
|
|
679
757
|
baseagent: 'codex',
|
|
680
758
|
projects: projList,
|
|
681
759
|
project: project.encoded,
|
|
682
760
|
transcripts,
|
|
683
|
-
turns:
|
|
761
|
+
turns: previewLargeToolResults(page.turns, page.turnPage.start),
|
|
762
|
+
turnPage: page.turnPage,
|
|
684
763
|
sessionId,
|
|
685
764
|
header,
|
|
765
|
+
metrics,
|
|
686
766
|
};
|
|
687
767
|
}
|
|
688
768
|
// ── Export ──
|
|
@@ -699,7 +779,7 @@ export function subscribeCodex(params, push) {
|
|
|
699
779
|
clearTimeout(debounce);
|
|
700
780
|
debounce = setTimeout(() => {
|
|
701
781
|
try {
|
|
702
|
-
|
|
782
|
+
void buildSnapshot(params).then(push).catch(() => { });
|
|
703
783
|
}
|
|
704
784
|
catch { }
|
|
705
785
|
}, 150);
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { resolvePaths } from '../paths.js';
|
|
4
|
+
import { readJsonFile, scanChatDirs } from '../fs-utils.js';
|
|
5
|
+
/** Resolve the local Agent's display name for a session binding. */
|
|
6
|
+
export function resolveSessionBindSelfName(bind, names) {
|
|
7
|
+
if (!bind?.selfAID)
|
|
8
|
+
return null;
|
|
9
|
+
return names.get(bind.selfAID) || bind.selfAID.split('.')[0] || bind.selfAID;
|
|
10
|
+
}
|
|
11
|
+
const bindingFileCache = new Map();
|
|
12
|
+
const SHARED_SYNTHETIC_CLI_PREFIXES = [
|
|
13
|
+
'Use Bash exactly once to run:',
|
|
14
|
+
'Use the Bash tool now to execute',
|
|
15
|
+
'Review this change for security vulnerabilities',
|
|
16
|
+
'You previously flagged these candidate vulnerabilities',
|
|
17
|
+
];
|
|
18
|
+
const CLAUDE_SYNTHETIC_CLI_PREFIXES = ['Base directory for this skill:'];
|
|
19
|
+
const CLAUDE_SYNTHETIC_TAG_RE = /^<\/?(?:local-command|command)-[a-z]+>/i;
|
|
20
|
+
const CLAUDE_SYNTHETIC_SLASH_RE = /^\/(?:resume|clear|compact|model|init|review|cost|config|status|help|agents?|context|memory|export|hooks?|mcp|ide|login|logout|doctor|bug|release-notes|vim|terminal-setup|add-dir|permissions)(?:\s|$)/i;
|
|
21
|
+
const EVOLCORE_ROUTE_PREFIX_RE = /^‹[^›]*·\s*from:[^›]*→[^›]*self:[^›]*›\s*/;
|
|
22
|
+
function normalizePeerType(value) {
|
|
23
|
+
if (typeof value !== 'string' || !value.trim())
|
|
24
|
+
return undefined;
|
|
25
|
+
const key = value.trim().toLowerCase();
|
|
26
|
+
if (key === 'human' || key === 'person' || key === 'user')
|
|
27
|
+
return 'human';
|
|
28
|
+
if (key === 'system' || key === 'trigger' || key === 'scheduler')
|
|
29
|
+
return 'system';
|
|
30
|
+
if (key === 'service' || key === 'daemon' || key === 'tool')
|
|
31
|
+
return 'service';
|
|
32
|
+
if (key === 'unknown')
|
|
33
|
+
return 'unknown';
|
|
34
|
+
return 'agent';
|
|
35
|
+
}
|
|
36
|
+
function classifyBinding(session) {
|
|
37
|
+
const channelType = String(session.channelType || '').toLowerCase();
|
|
38
|
+
const channelId = String(session.channelId || '');
|
|
39
|
+
const threadId = String(session.threadId || '');
|
|
40
|
+
if (channelType === 'daemon') {
|
|
41
|
+
return channelId.startsWith('trigger:') || threadId.startsWith('trigger:') ? 'trigger' : 'unknown';
|
|
42
|
+
}
|
|
43
|
+
if (channelType === 'aun') {
|
|
44
|
+
if (session.chatType === 'group')
|
|
45
|
+
return 'user';
|
|
46
|
+
const peerKind = normalizePeerType(session.metadata?.peerType);
|
|
47
|
+
if (peerKind === 'agent')
|
|
48
|
+
return 'agent';
|
|
49
|
+
if (peerKind === 'human')
|
|
50
|
+
return 'user';
|
|
51
|
+
return 'unknown';
|
|
52
|
+
}
|
|
53
|
+
return channelType ? 'user' : 'unknown';
|
|
54
|
+
}
|
|
55
|
+
function toBinding(session, fallback, currentlyBound = false) {
|
|
56
|
+
if (!session.agentSessionId)
|
|
57
|
+
return null;
|
|
58
|
+
const channelType = session.channelType || fallback.channelType;
|
|
59
|
+
const channelId = session.channelId || fallback.channelId;
|
|
60
|
+
const chatType = session.chatType || 'private';
|
|
61
|
+
return {
|
|
62
|
+
evolSessionId: session.id || null,
|
|
63
|
+
channelType,
|
|
64
|
+
channelId,
|
|
65
|
+
channelKey: session.channel || session.metadata?.channelKey || channelType,
|
|
66
|
+
selfAID: session.selfAID || fallback.selfAID,
|
|
67
|
+
chatType,
|
|
68
|
+
peerId: session.metadata?.peerId || channelId,
|
|
69
|
+
peerName: session.metadata?.peerName || null,
|
|
70
|
+
peerType: session.metadata?.peerType || null,
|
|
71
|
+
...(chatType === 'group' ? { groupId: session.metadata?.groupId || channelId } : {}),
|
|
72
|
+
groupName: session.metadata?.groupName || null,
|
|
73
|
+
displayName: null,
|
|
74
|
+
name: session.name || null,
|
|
75
|
+
threadId: session.threadId || '',
|
|
76
|
+
updatedAt: session.updatedAt || 0,
|
|
77
|
+
currentlyBound,
|
|
78
|
+
sourceType: classifyBinding({ ...session, channelType, channelId, chatType }),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function parseBindingJsonl(file, fallback) {
|
|
82
|
+
let stat;
|
|
83
|
+
try {
|
|
84
|
+
stat = fs.statSync(file);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
bindingFileCache.delete(file);
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
const cached = bindingFileCache.get(file);
|
|
91
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size)
|
|
92
|
+
return cached.bindings;
|
|
93
|
+
const byAgentSessionId = new Map();
|
|
94
|
+
let finalAgentSessionId = null;
|
|
95
|
+
try {
|
|
96
|
+
const lines = fs.readFileSync(file, 'utf8').trimEnd().split(/\r?\n/);
|
|
97
|
+
for (const line of lines) {
|
|
98
|
+
if (!line.trim())
|
|
99
|
+
continue;
|
|
100
|
+
let session;
|
|
101
|
+
try {
|
|
102
|
+
session = JSON.parse(line);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const agentSessionId = session.agentSessionId;
|
|
108
|
+
finalAgentSessionId = agentSessionId || null;
|
|
109
|
+
if (!agentSessionId)
|
|
110
|
+
continue;
|
|
111
|
+
const binding = toBinding(session, fallback);
|
|
112
|
+
if (!binding)
|
|
113
|
+
continue;
|
|
114
|
+
const existing = byAgentSessionId.get(agentSessionId);
|
|
115
|
+
if (!existing || binding.updatedAt >= existing.updatedAt)
|
|
116
|
+
byAgentSessionId.set(agentSessionId, binding);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
catch { }
|
|
120
|
+
const bindings = Array.from(byAgentSessionId, ([agentSessionId, binding]) => ({
|
|
121
|
+
agentSessionId,
|
|
122
|
+
binding: { ...binding, currentlyBound: agentSessionId === finalAgentSessionId },
|
|
123
|
+
}));
|
|
124
|
+
bindingFileCache.set(file, { mtimeMs: stat.mtimeMs, size: stat.size, bindings });
|
|
125
|
+
return bindings;
|
|
126
|
+
}
|
|
127
|
+
function bindingFiles(dirPath) {
|
|
128
|
+
const files = [];
|
|
129
|
+
for (const dir of [dirPath, path.join(dirPath, '_threads')]) {
|
|
130
|
+
let entries;
|
|
131
|
+
try {
|
|
132
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
for (const entry of entries) {
|
|
138
|
+
if (entry.isFile() && /^meta_.+\.jsonl$/.test(entry.name))
|
|
139
|
+
files.push(path.join(dir, entry.name));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return files;
|
|
143
|
+
}
|
|
144
|
+
export function buildSessionBindMap(sessionsDir = resolvePaths().sessionsDir) {
|
|
145
|
+
const map = new Map();
|
|
146
|
+
const liveFiles = new Set();
|
|
147
|
+
for (const dir of scanChatDirs(sessionsDir)) {
|
|
148
|
+
const fallback = { channelType: dir.channelType, channelId: dir.channelId, selfAID: dir.selfAID };
|
|
149
|
+
const active = readJsonFile(path.join(dir.dirPath, 'active.json'));
|
|
150
|
+
const activeBinding = active ? toBinding(active, fallback, true) : null;
|
|
151
|
+
if (activeBinding && active?.agentSessionId)
|
|
152
|
+
map.set(active.agentSessionId, activeBinding);
|
|
153
|
+
for (const file of bindingFiles(dir.dirPath)) {
|
|
154
|
+
liveFiles.add(file);
|
|
155
|
+
for (const { agentSessionId, binding } of parseBindingJsonl(file, fallback)) {
|
|
156
|
+
const existing = map.get(agentSessionId);
|
|
157
|
+
if (!existing
|
|
158
|
+
|| (binding.currentlyBound && !existing.currentlyBound)
|
|
159
|
+
|| binding.currentlyBound === existing.currentlyBound && binding.updatedAt >= existing.updatedAt) {
|
|
160
|
+
map.set(agentSessionId, binding);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
for (const file of bindingFileCache.keys()) {
|
|
166
|
+
if (path.resolve(file).startsWith(`${path.resolve(sessionsDir)}${path.sep}`) && !liveFiles.has(file)) {
|
|
167
|
+
bindingFileCache.delete(file);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return map;
|
|
171
|
+
}
|
|
172
|
+
export function isSyntheticCliPrompt(text, backend) {
|
|
173
|
+
if (typeof text !== 'string' || !text.trim())
|
|
174
|
+
return false;
|
|
175
|
+
const trimmed = text.trimStart();
|
|
176
|
+
const prefixes = backend === 'claude'
|
|
177
|
+
? SHARED_SYNTHETIC_CLI_PREFIXES.concat(CLAUDE_SYNTHETIC_CLI_PREFIXES)
|
|
178
|
+
: SHARED_SYNTHETIC_CLI_PREFIXES;
|
|
179
|
+
if (prefixes.some(prefix => trimmed.startsWith(prefix)))
|
|
180
|
+
return true;
|
|
181
|
+
return backend === 'claude'
|
|
182
|
+
&& (CLAUDE_SYNTHETIC_TAG_RE.test(trimmed) || CLAUDE_SYNTHETIC_SLASH_RE.test(trimmed));
|
|
183
|
+
}
|
|
184
|
+
export function isEvolcoreRoutedMessage(text) {
|
|
185
|
+
return typeof text === 'string' && EVOLCORE_ROUTE_PREFIX_RE.test(text.trimStart());
|
|
186
|
+
}
|
|
187
|
+
export function classifyTranscriptSource(binding, cliCandidate) {
|
|
188
|
+
if (binding?.currentlyBound)
|
|
189
|
+
return binding.sourceType;
|
|
190
|
+
if (cliCandidate)
|
|
191
|
+
return 'cli';
|
|
192
|
+
if (binding)
|
|
193
|
+
return binding.sourceType;
|
|
194
|
+
return 'unknown';
|
|
195
|
+
}
|