ec-web 0.0.5 → 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 +16 -1
- package/dist/sources/msg.js +188 -13
- package/dist/sources/session-codex.js +72 -6
- package/dist/sources/session.js +84 -11
- package/dist/sources/transcript-window.js +35 -0
- package/dist/static/app.js +171 -19
- package/dist/static/index.html +1 -1
- package/dist/static/style.css +98 -62
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -20,7 +20,7 @@ import { ipcQuery } from './ipc-client.js';
|
|
|
20
20
|
import { setDebugLog, dlog } from './debug-log.js';
|
|
21
21
|
import { aidSource } from './sources/aid.js';
|
|
22
22
|
import { msgSource } from './sources/msg.js';
|
|
23
|
-
import { sessionSource, buildBindMap } from './sources/session.js';
|
|
23
|
+
import { sessionSource, buildBindMap, readClaudeSessionResult } from './sources/session.js';
|
|
24
24
|
import { cacheSource } from './sources/cache.js';
|
|
25
25
|
import { systemSource } from './sources/system.js';
|
|
26
26
|
import { triggersSource } from './sources/triggers.js';
|
|
@@ -34,6 +34,7 @@ import { relationRolesSource, handleRelationRolesApi, handlePeerRoleApi } from '
|
|
|
34
34
|
import { roleDefinitionsSource, handleRoleDefinitionsApi } from './sources/role-definitions.js';
|
|
35
35
|
import { handleModelsApi } from './sources/models.js';
|
|
36
36
|
import { detectBaseAgents } from './sources/baseagent-detector.js';
|
|
37
|
+
import { readCodexSessionResult } from './sources/session-codex.js';
|
|
37
38
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
38
39
|
const STATIC_DIR = path.join(__dirname, 'static');
|
|
39
40
|
const TOKEN_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30天(滑动窗口:每次有效访问刷新 lastActive 自动续期)
|
|
@@ -553,8 +554,14 @@ function handleConnection(ws, req, log) {
|
|
|
553
554
|
params.aid = msg.aid;
|
|
554
555
|
if (msg.peer)
|
|
555
556
|
params.peer = msg.peer;
|
|
557
|
+
if (msg.before != null)
|
|
558
|
+
params.before = msg.before;
|
|
556
559
|
if (msg.sessionId)
|
|
557
560
|
params.sessionId = msg.sessionId;
|
|
561
|
+
if (msg.turnBefore != null)
|
|
562
|
+
params.turnBefore = msg.turnBefore;
|
|
563
|
+
if (msg.turnLimit != null)
|
|
564
|
+
params.turnLimit = msg.turnLimit;
|
|
558
565
|
if (msg.project)
|
|
559
566
|
params.project = msg.project;
|
|
560
567
|
if (msg.agent)
|
|
@@ -569,6 +576,14 @@ function handleConnection(ws, req, log) {
|
|
|
569
576
|
await switchSubscription(msg.view, params);
|
|
570
577
|
return;
|
|
571
578
|
}
|
|
579
|
+
if (msg.type === 'session.result') {
|
|
580
|
+
const params = msg.params && typeof msg.params === 'object' ? msg.params : {};
|
|
581
|
+
const result = params.baseagent === 'codex'
|
|
582
|
+
? readCodexSessionResult(params)
|
|
583
|
+
: readClaudeSessionResult(params);
|
|
584
|
+
send({ type: 'session.result', requestId: msg.requestId, data: result == null ? null : { text: result } });
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
572
587
|
if (msg.type === 'menu' && msg.payload) {
|
|
573
588
|
const p = resolvePaths();
|
|
574
589
|
const timeoutMs = Number.isFinite(Number(msg.timeoutMs)) && Number(msg.timeoutMs) > 0
|
package/dist/sources/msg.js
CHANGED
|
@@ -16,6 +16,7 @@ let peerDisplayResolverPromise = null;
|
|
|
16
16
|
const aunGroupNameCache = new Map();
|
|
17
17
|
const aunGroupNameRequests = new Map();
|
|
18
18
|
let agentDisplayNamesCache = null;
|
|
19
|
+
const messageFileCache = new Map();
|
|
19
20
|
function getContextModules() {
|
|
20
21
|
if (!contextModulesPromise) {
|
|
21
22
|
contextModulesPromise = Promise.all([
|
|
@@ -119,13 +120,27 @@ function getAunGroupNames(aid) {
|
|
|
119
120
|
aunGroupNameRequests.set(aid, request);
|
|
120
121
|
return request;
|
|
121
122
|
}
|
|
122
|
-
async function hydrateAunGroupNames(chats) {
|
|
123
|
+
async function hydrateAunGroupNames(chats, waitForRefresh = false) {
|
|
123
124
|
const aids = [...new Set(chats
|
|
124
125
|
.filter(chat => chat.channelType === 'aun' && chat.chatType === 'group')
|
|
125
126
|
.map(chat => chat.selfAID))];
|
|
126
127
|
if (!aids.length)
|
|
127
128
|
return;
|
|
128
|
-
const directories = new Map(
|
|
129
|
+
const directories = new Map();
|
|
130
|
+
if (waitForRefresh) {
|
|
131
|
+
for (const [aid, names] of await Promise.all(aids.map(async (aid) => [aid, await getAunGroupNames(aid)]))) {
|
|
132
|
+
directories.set(aid, names);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
for (const aid of aids) {
|
|
137
|
+
const cached = aunGroupNameCache.get(aid);
|
|
138
|
+
if (cached)
|
|
139
|
+
directories.set(aid, cached.names);
|
|
140
|
+
if (!cached || cached.expiresAt <= Date.now())
|
|
141
|
+
void getAunGroupNames(aid);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
129
144
|
for (const chat of chats) {
|
|
130
145
|
if (chat.channelType !== 'aun' || chat.chatType !== 'group')
|
|
131
146
|
continue;
|
|
@@ -207,6 +222,131 @@ function findMessageFiles(root) {
|
|
|
207
222
|
visit(root);
|
|
208
223
|
return out;
|
|
209
224
|
}
|
|
225
|
+
function fileSignature(file) {
|
|
226
|
+
try {
|
|
227
|
+
const stat = fs.statSync(file);
|
|
228
|
+
return `${stat.mtimeMs}:${stat.size}`;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return '';
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function chatMetadataSignature(dirPath) {
|
|
235
|
+
const parts = [
|
|
236
|
+
`active:${fileSignature(path.join(dirPath, 'active.json'))}`,
|
|
237
|
+
];
|
|
238
|
+
let rootEntries = [];
|
|
239
|
+
try {
|
|
240
|
+
rootEntries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
241
|
+
}
|
|
242
|
+
catch { }
|
|
243
|
+
for (const entry of rootEntries) {
|
|
244
|
+
if (entry.isFile() && entry.name.startsWith('meta_') && entry.name.endsWith('.jsonl')) {
|
|
245
|
+
parts.push(`${entry.name}:${fileSignature(path.join(dirPath, entry.name))}`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const threadsDir = path.join(dirPath, '_threads');
|
|
249
|
+
let threadEntries = [];
|
|
250
|
+
try {
|
|
251
|
+
threadEntries = fs.readdirSync(threadsDir, { withFileTypes: true });
|
|
252
|
+
}
|
|
253
|
+
catch { }
|
|
254
|
+
for (const entry of threadEntries) {
|
|
255
|
+
if (entry.isFile() && (entry.name === 'thread-index.json' || entry.name.endsWith('.jsonl'))) {
|
|
256
|
+
parts.push(`threads/${entry.name}:${fileSignature(path.join(threadsDir, entry.name))}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return parts.sort().join('|');
|
|
260
|
+
}
|
|
261
|
+
function appendMessageRows(file, cached, metrics) {
|
|
262
|
+
let stat;
|
|
263
|
+
try {
|
|
264
|
+
stat = fs.statSync(file);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
return false;
|
|
268
|
+
}
|
|
269
|
+
if (stat.size < cached.offset || (stat.size === cached.offset && cached.mtimeMs && stat.mtimeMs !== cached.mtimeMs)) {
|
|
270
|
+
cached.offset = 0;
|
|
271
|
+
cached.messages = [];
|
|
272
|
+
}
|
|
273
|
+
if (stat.size === cached.offset)
|
|
274
|
+
return false;
|
|
275
|
+
const bytes = stat.size - cached.offset;
|
|
276
|
+
const buffer = Buffer.allocUnsafe(bytes);
|
|
277
|
+
let fd = null;
|
|
278
|
+
try {
|
|
279
|
+
fd = fs.openSync(file, 'r');
|
|
280
|
+
fs.readSync(fd, buffer, 0, bytes, cached.offset);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
finally {
|
|
286
|
+
if (fd != null)
|
|
287
|
+
try {
|
|
288
|
+
fs.closeSync(fd);
|
|
289
|
+
}
|
|
290
|
+
catch { }
|
|
291
|
+
}
|
|
292
|
+
let completeBytes = buffer.length;
|
|
293
|
+
const lastBreak = buffer.lastIndexOf(0x0a);
|
|
294
|
+
if (lastBreak !== buffer.length - 1) {
|
|
295
|
+
const trailing = buffer.subarray(lastBreak + 1).toString('utf8').trim();
|
|
296
|
+
try {
|
|
297
|
+
JSON.parse(trailing);
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
completeBytes = lastBreak + 1;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
if (completeBytes <= 0)
|
|
304
|
+
return false;
|
|
305
|
+
let parsedLines = 0;
|
|
306
|
+
for (const line of buffer.subarray(0, completeBytes).toString('utf8').split(/\r?\n/)) {
|
|
307
|
+
if (!line)
|
|
308
|
+
continue;
|
|
309
|
+
try {
|
|
310
|
+
const message = JSON.parse(line);
|
|
311
|
+
parsedLines++;
|
|
312
|
+
if (!isTransientProtocolMessage(message))
|
|
313
|
+
cached.messages.push(message);
|
|
314
|
+
}
|
|
315
|
+
catch { }
|
|
316
|
+
}
|
|
317
|
+
cached.offset += completeBytes;
|
|
318
|
+
cached.mtimeMs = stat.mtimeMs;
|
|
319
|
+
metrics.changedFiles++;
|
|
320
|
+
metrics.parsedLines += parsedLines;
|
|
321
|
+
metrics.parsedBytes += completeBytes;
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
function cachedMessageFile(file, metrics) {
|
|
325
|
+
let cached = messageFileCache.get(file);
|
|
326
|
+
if (!cached) {
|
|
327
|
+
cached = {
|
|
328
|
+
offset: 0,
|
|
329
|
+
mtimeMs: 0,
|
|
330
|
+
messages: [],
|
|
331
|
+
active: {},
|
|
332
|
+
threadSessions: [],
|
|
333
|
+
mainSessionIds: new Set(),
|
|
334
|
+
metadataSignature: '',
|
|
335
|
+
};
|
|
336
|
+
messageFileCache.set(file, cached);
|
|
337
|
+
}
|
|
338
|
+
appendMessageRows(file, cached, metrics);
|
|
339
|
+
const dirPath = path.dirname(file);
|
|
340
|
+
const metadataSignature = chatMetadataSignature(dirPath);
|
|
341
|
+
if (metadataSignature !== cached.metadataSignature) {
|
|
342
|
+
cached.active = readJsonFile(path.join(dirPath, 'active.json')) ?? {};
|
|
343
|
+
cached.threadSessions = loadThreadSessions(dirPath);
|
|
344
|
+
cached.mainSessionIds = loadMainSessionIds(dirPath, cached.active);
|
|
345
|
+
cached.metadataSignature = metadataSignature;
|
|
346
|
+
metrics.metadataRefreshes++;
|
|
347
|
+
}
|
|
348
|
+
return cached;
|
|
349
|
+
}
|
|
210
350
|
function readLastJsonlObject(file) {
|
|
211
351
|
const rows = readAllJsonlLines(file);
|
|
212
352
|
return rows.length ? rows[rows.length - 1] : null;
|
|
@@ -467,17 +607,22 @@ function chatDisplayId(active, messages) {
|
|
|
467
607
|
groupName: null,
|
|
468
608
|
};
|
|
469
609
|
}
|
|
470
|
-
function loadChats(peerDisplayResolver) {
|
|
610
|
+
function loadChats(peerDisplayResolver, metrics) {
|
|
471
611
|
const paths = resolvePaths();
|
|
472
612
|
const sessionsDir = paths.sessionsDir;
|
|
473
613
|
const configuredAids = listConfiguredAgentAids(paths.root);
|
|
474
614
|
const files = findMessageFiles(sessionsDir);
|
|
615
|
+
const liveFiles = new Set(files);
|
|
616
|
+
for (const file of messageFileCache.keys()) {
|
|
617
|
+
if (!liveFiles.has(file))
|
|
618
|
+
messageFileCache.delete(file);
|
|
619
|
+
}
|
|
620
|
+
metrics.files = files.length;
|
|
475
621
|
const chats = [];
|
|
476
622
|
for (const file of files) {
|
|
477
623
|
const dirPath = path.dirname(file);
|
|
478
|
-
const
|
|
479
|
-
const messages =
|
|
480
|
-
.filter(m => !isTransientProtocolMessage(m));
|
|
624
|
+
const cached = cachedMessageFile(file, metrics);
|
|
625
|
+
const { active, messages } = cached;
|
|
481
626
|
if (!messages.length)
|
|
482
627
|
continue;
|
|
483
628
|
const identity = normalizeChatIdentity(sessionsDir, dirPath, active, messages);
|
|
@@ -489,9 +634,9 @@ function loadChats(peerDisplayResolver) {
|
|
|
489
634
|
const peerName = chatType === 'group'
|
|
490
635
|
? display.peerName
|
|
491
636
|
: resolveContactPeerName(peerDisplayResolver, identity, display.peerId, display.peerName);
|
|
492
|
-
const threadSessions =
|
|
637
|
+
const threadSessions = cached.threadSessions;
|
|
493
638
|
const threadById = new Map(threadSessions.map(item => [item.threadId, item]));
|
|
494
|
-
const mainSessionIds =
|
|
639
|
+
const mainSessionIds = cached.mainSessionIds;
|
|
495
640
|
for (const item of partitionMessages(messages, threadSessions, mainSessionIds)) {
|
|
496
641
|
if (!item.messages.length)
|
|
497
642
|
continue;
|
|
@@ -650,7 +795,7 @@ function selectedChats(chats, scope) {
|
|
|
650
795
|
return [];
|
|
651
796
|
return chats.filter(chat => scopeId(chat) === scope);
|
|
652
797
|
}
|
|
653
|
-
function messagesFor(chats, peer) {
|
|
798
|
+
function messagesFor(chats, peer, before, limit) {
|
|
654
799
|
const selected = peer
|
|
655
800
|
? chats.filter(chat => chatKey(chat) === peer)
|
|
656
801
|
: chats;
|
|
@@ -665,7 +810,13 @@ function messagesFor(chats, peer) {
|
|
|
665
810
|
threadName: chat.threadName,
|
|
666
811
|
})));
|
|
667
812
|
messages.sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
668
|
-
|
|
813
|
+
const end = before == null ? messages.length : Math.min(Math.max(before, 0), messages.length);
|
|
814
|
+
const start = Math.max(0, end - limit);
|
|
815
|
+
return {
|
|
816
|
+
messages: messages.slice(start, end),
|
|
817
|
+
nextBefore: start > 0 ? start : null,
|
|
818
|
+
hasMore: start > 0,
|
|
819
|
+
};
|
|
669
820
|
}
|
|
670
821
|
function latestInbound(chat) {
|
|
671
822
|
for (let i = chat.messages.length - 1; i >= 0; i--) {
|
|
@@ -745,8 +896,20 @@ async function buildSessionContext(chat) {
|
|
|
745
896
|
};
|
|
746
897
|
}
|
|
747
898
|
async function buildSnapshot(params) {
|
|
899
|
+
const startedAt = Date.now();
|
|
900
|
+
const metrics = {
|
|
901
|
+
files: 0,
|
|
902
|
+
changedFiles: 0,
|
|
903
|
+
parsedLines: 0,
|
|
904
|
+
parsedBytes: 0,
|
|
905
|
+
metadataRefreshes: 0,
|
|
906
|
+
scanMs: 0,
|
|
907
|
+
totalMs: 0,
|
|
908
|
+
};
|
|
748
909
|
const peerDisplayResolver = await getPeerDisplayResolver();
|
|
749
|
-
const
|
|
910
|
+
const scanStartedAt = Date.now();
|
|
911
|
+
const chats = loadChats(peerDisplayResolver, metrics);
|
|
912
|
+
metrics.scanMs = Date.now() - scanStartedAt;
|
|
750
913
|
const scopes = buildScopes(chats, await getAgentSelfNames());
|
|
751
914
|
const requestedScope = params.scope || params.aid || null;
|
|
752
915
|
const scope = requestedScope && !scopes.some(s => s.id === requestedScope)
|
|
@@ -759,18 +922,30 @@ async function buildSnapshot(params) {
|
|
|
759
922
|
if (scope)
|
|
760
923
|
await hydrateAunGroupNames(inScope);
|
|
761
924
|
const peers = scope ? mergePeerInfo(inScope) : [];
|
|
762
|
-
const
|
|
925
|
+
const requestedLimit = Number(params.limit);
|
|
926
|
+
const limit = Number.isFinite(requestedLimit) ? Math.min(Math.max(Math.trunc(requestedLimit), 20), 200) : 100;
|
|
927
|
+
const requestedBefore = params.before == null ? Number.NaN : Number(params.before);
|
|
928
|
+
const before = Number.isFinite(requestedBefore) && requestedBefore >= 0 ? Math.trunc(requestedBefore) : null;
|
|
929
|
+
const page = scope && peer ? messagesFor(inScope, peer, before, limit) : { messages: [], nextBefore: null, hasMore: false };
|
|
763
930
|
const currentChat = peer ? inScope.find(chat => chatKey(chat) === peer) : undefined;
|
|
764
931
|
const sessionContext = await buildSessionContext(currentChat);
|
|
932
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
765
933
|
return {
|
|
766
934
|
scopes,
|
|
767
935
|
aids: scopes,
|
|
768
936
|
peers,
|
|
769
|
-
messages,
|
|
937
|
+
messages: page.messages,
|
|
938
|
+
messagePage: {
|
|
939
|
+
limit,
|
|
940
|
+
before,
|
|
941
|
+
nextBefore: page.nextBefore,
|
|
942
|
+
hasMore: page.hasMore,
|
|
943
|
+
},
|
|
770
944
|
scope,
|
|
771
945
|
aid: scope,
|
|
772
946
|
peer,
|
|
773
947
|
sessionContext,
|
|
948
|
+
metrics,
|
|
774
949
|
};
|
|
775
950
|
}
|
|
776
951
|
export const msgSource = {
|
|
@@ -14,6 +14,7 @@ import { createRequire } from 'module';
|
|
|
14
14
|
import { resolvePaths } from '../paths.js';
|
|
15
15
|
import { encodePath } from '../fs-utils.js';
|
|
16
16
|
import { getAgentSelfNames } from './msg.js';
|
|
17
|
+
import { parseTranscriptResultKey, previewLargeToolResults, windowTranscriptTurns } from './transcript-window.js';
|
|
17
18
|
import { buildSessionBindMap, classifyTranscriptSource, isEvolcoreRoutedMessage, isSyntheticCliPrompt, resolveSessionBindSelfName, } from './session-origin.js';
|
|
18
19
|
const requireFromHere = createRequire(import.meta.url);
|
|
19
20
|
let _dlog = null;
|
|
@@ -111,6 +112,8 @@ function listProjects() {
|
|
|
111
112
|
// ── 会话列表 ──
|
|
112
113
|
const CACHE_VERSION = 2;
|
|
113
114
|
const _metaCache = new Map();
|
|
115
|
+
const _detailCache = new Map();
|
|
116
|
+
const MAX_DETAIL_CACHE_ENTRIES = 6;
|
|
114
117
|
function cacheDir(encoded) {
|
|
115
118
|
const dataDir = resolvePaths().dataDir;
|
|
116
119
|
return path.join(dataDir, 'ecweb-cache', 'codex', encoded);
|
|
@@ -134,6 +137,16 @@ function writeDiskCache(encoded, id, mtime, size, meta) {
|
|
|
134
137
|
}
|
|
135
138
|
catch { }
|
|
136
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
|
+
}
|
|
137
150
|
function pruneCacheFiles(encoded, validIds) {
|
|
138
151
|
const dir = cacheDir(encoded);
|
|
139
152
|
try {
|
|
@@ -462,7 +475,7 @@ export function parseCodexToolEvent(event) {
|
|
|
462
475
|
}
|
|
463
476
|
return null;
|
|
464
477
|
}
|
|
465
|
-
function readTranscriptFile(threadId, cwd) {
|
|
478
|
+
function readTranscriptFile(threadId, cwd, readStats) {
|
|
466
479
|
const empty = {
|
|
467
480
|
turns: [],
|
|
468
481
|
totalTurns: 0,
|
|
@@ -495,6 +508,21 @@ function readTranscriptFile(threadId, cwd) {
|
|
|
495
508
|
}
|
|
496
509
|
if (!fs.existsSync(rolloutPath))
|
|
497
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
|
+
}
|
|
498
526
|
let raw;
|
|
499
527
|
try {
|
|
500
528
|
raw = fs.readFileSync(rolloutPath, 'utf-8');
|
|
@@ -602,9 +630,8 @@ function readTranscriptFile(threadId, cwd) {
|
|
|
602
630
|
}
|
|
603
631
|
}
|
|
604
632
|
}
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
turns: shown,
|
|
633
|
+
const detail = {
|
|
634
|
+
turns,
|
|
608
635
|
totalTurns: turns.length,
|
|
609
636
|
userMsgs,
|
|
610
637
|
totalMsgs,
|
|
@@ -619,13 +646,41 @@ function readTranscriptFile(threadId, cwd) {
|
|
|
619
646
|
title: empty.title,
|
|
620
647
|
cwd,
|
|
621
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;
|
|
622
662
|
}
|
|
623
663
|
// ── Snapshot ──
|
|
624
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();
|
|
625
676
|
const projects = listProjects();
|
|
677
|
+
metrics.projectMs = Date.now() - projectStartedAt;
|
|
678
|
+
const bindingsStartedAt = Date.now();
|
|
626
679
|
const bindMap = buildBindMap();
|
|
680
|
+
metrics.bindingsMs = Date.now() - bindingsStartedAt;
|
|
627
681
|
const project = resolveProject(params, projects);
|
|
628
682
|
if (!project) {
|
|
683
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
629
684
|
return {
|
|
630
685
|
baseagent: 'codex',
|
|
631
686
|
projects: [],
|
|
@@ -633,12 +688,16 @@ async function buildSnapshot(params) {
|
|
|
633
688
|
transcripts: [],
|
|
634
689
|
turns: [],
|
|
635
690
|
sessionId: null,
|
|
691
|
+
metrics,
|
|
636
692
|
};
|
|
637
693
|
}
|
|
694
|
+
const transcriptListStartedAt = Date.now();
|
|
638
695
|
const metas = listTranscripts(project.encoded, project.cwd);
|
|
696
|
+
metrics.transcriptListMs = Date.now() - transcriptListStartedAt;
|
|
639
697
|
const selfNames = await getAgentSelfNames();
|
|
640
698
|
const transcripts = metas.map(m => {
|
|
641
699
|
const bind = bindMap.get(m.id);
|
|
700
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
642
701
|
return {
|
|
643
702
|
...m,
|
|
644
703
|
sourceType: classifyTranscriptSource(bind, isCodexCliCandidate(m)),
|
|
@@ -663,9 +722,13 @@ async function buildSnapshot(params) {
|
|
|
663
722
|
transcripts,
|
|
664
723
|
turns: [],
|
|
665
724
|
sessionId: null,
|
|
725
|
+
metrics,
|
|
666
726
|
};
|
|
667
727
|
}
|
|
668
|
-
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);
|
|
669
732
|
const bind = bindMap.get(sessionId);
|
|
670
733
|
const transcript = metas.find(meta => meta.id === sessionId);
|
|
671
734
|
const header = {
|
|
@@ -689,14 +752,17 @@ async function buildSnapshot(params) {
|
|
|
689
752
|
boundSelf: resolveSessionBindSelfName(bind, selfNames),
|
|
690
753
|
online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false,
|
|
691
754
|
};
|
|
755
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
692
756
|
return {
|
|
693
757
|
baseagent: 'codex',
|
|
694
758
|
projects: projList,
|
|
695
759
|
project: project.encoded,
|
|
696
760
|
transcripts,
|
|
697
|
-
turns:
|
|
761
|
+
turns: previewLargeToolResults(page.turns, page.turnPage.start),
|
|
762
|
+
turnPage: page.turnPage,
|
|
698
763
|
sessionId,
|
|
699
764
|
header,
|
|
765
|
+
metrics,
|
|
700
766
|
};
|
|
701
767
|
}
|
|
702
768
|
// ── Export ──
|
package/dist/sources/session.js
CHANGED
|
@@ -16,6 +16,7 @@ import { resolvePaths, ccProjectsDir } from '../paths.js';
|
|
|
16
16
|
import { encodePath } from '../fs-utils.js';
|
|
17
17
|
import { snapshotCodex, subscribeCodex, setDebugLog as setCodexDebugLog } from './session-codex.js';
|
|
18
18
|
import { getAgentSelfNames } from './msg.js';
|
|
19
|
+
import { parseTranscriptResultKey, previewLargeToolResults, windowTranscriptTurns } from './transcript-window.js';
|
|
19
20
|
import { buildSessionBindMap, classifyTranscriptSource, isSyntheticCliPrompt, resolveSessionBindSelfName, } from './session-origin.js';
|
|
20
21
|
let _dlog = null;
|
|
21
22
|
export function setDebugLog(log) {
|
|
@@ -29,6 +30,8 @@ function dlog(line) { if (_dlog)
|
|
|
29
30
|
catch { } }
|
|
30
31
|
const CACHE_VERSION = 5;
|
|
31
32
|
const _metaCache = new Map();
|
|
33
|
+
const _detailCache = new Map();
|
|
34
|
+
const MAX_DETAIL_CACHE_ENTRIES = 6;
|
|
32
35
|
function cacheDir(encoded) {
|
|
33
36
|
const dataDir = resolvePaths().dataDir;
|
|
34
37
|
const currentRoot = path.join(dataDir, 'ecweb-cache');
|
|
@@ -60,6 +63,16 @@ function writeDiskCache(encoded, id, mtime, size, meta) {
|
|
|
60
63
|
}
|
|
61
64
|
catch { }
|
|
62
65
|
}
|
|
66
|
+
function cacheTranscriptDetail(file, mtime, size, detail) {
|
|
67
|
+
_detailCache.delete(file);
|
|
68
|
+
_detailCache.set(file, { mtime, size, detail });
|
|
69
|
+
while (_detailCache.size > MAX_DETAIL_CACHE_ENTRIES) {
|
|
70
|
+
const oldest = _detailCache.keys().next().value;
|
|
71
|
+
if (!oldest)
|
|
72
|
+
break;
|
|
73
|
+
_detailCache.delete(oldest);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
63
76
|
function pruneCacheFiles(encoded, validIds) {
|
|
64
77
|
const dir = cacheDir(encoded);
|
|
65
78
|
try {
|
|
@@ -486,8 +499,23 @@ function pricingFor(model) {
|
|
|
486
499
|
return PRICING['claude-haiku-4-5-20251001'];
|
|
487
500
|
return PRICING['claude-opus-4-8'];
|
|
488
501
|
}
|
|
489
|
-
function readTranscriptFile(file) {
|
|
502
|
+
function readTranscriptFile(file, readStats) {
|
|
490
503
|
const empty = { turns: [], totalTurns: 0, userMsgs: 0, totalMsgs: 0, counts: { userInput: 0, modelOutput: 0, toolCall: 0, toolResult: 0, msgSend: 0 }, inputTokens: 0, outputTokens: 0, contextTokens: 0, costUsd: 0, model: '', gitBranch: '', version: '', title: '', cwd: '' };
|
|
504
|
+
let stat;
|
|
505
|
+
try {
|
|
506
|
+
stat = fs.statSync(file);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return empty;
|
|
510
|
+
}
|
|
511
|
+
const cached = _detailCache.get(file);
|
|
512
|
+
if (cached && cached.mtime === stat.mtimeMs && cached.size === stat.size) {
|
|
513
|
+
_detailCache.delete(file);
|
|
514
|
+
_detailCache.set(file, cached);
|
|
515
|
+
if (readStats)
|
|
516
|
+
readStats.detailCacheHit = true;
|
|
517
|
+
return cached.detail;
|
|
518
|
+
}
|
|
491
519
|
let raw;
|
|
492
520
|
try {
|
|
493
521
|
raw = fs.readFileSync(file, 'utf-8');
|
|
@@ -575,20 +603,60 @@ function readTranscriptFile(file) {
|
|
|
575
603
|
else {
|
|
576
604
|
category = hasToolUse ? 'tool_call' : 'model_output';
|
|
577
605
|
}
|
|
606
|
+
// Claude can emit assistant events that contain usage metadata but no visible blocks.
|
|
607
|
+
// They are not useful transcript entries and previously rendered as empty model output.
|
|
608
|
+
if (o.type === 'assistant' && blocks.length === 0)
|
|
609
|
+
continue;
|
|
578
610
|
counts[category === 'user_input' ? 'userInput' : category === 'model_output' ? 'modelOutput' : category === 'tool_call' ? 'toolCall' : 'toolResult']++;
|
|
579
611
|
turns.push({ role: o.type, ts: o.timestamp ? Date.parse(o.timestamp) : 0, uuid: o.uuid, category, blocks });
|
|
580
612
|
}
|
|
581
613
|
}
|
|
582
|
-
const
|
|
583
|
-
|
|
614
|
+
const detail = { turns, totalTurns: turns.length, userMsgs, totalMsgs, counts, inputTokens: inTok, outputTokens: outTok, contextTokens, costUsd, model, gitBranch: branch, version, title, cwd };
|
|
615
|
+
cacheTranscriptDetail(file, stat.mtimeMs, stat.size, detail);
|
|
616
|
+
return detail;
|
|
617
|
+
}
|
|
618
|
+
export function readClaudeSessionResult(params) {
|
|
619
|
+
const projects = listProjects();
|
|
620
|
+
const project = resolveProject(params, projects);
|
|
621
|
+
const sessionId = typeof params.sessionId === 'string' ? params.sessionId : '';
|
|
622
|
+
const key = parseTranscriptResultKey(params.resultKey);
|
|
623
|
+
if (!project || !sessionId || !key || !/^[A-Za-z0-9-]+$/.test(sessionId))
|
|
624
|
+
return null;
|
|
625
|
+
const file = path.join(ccProjectsDir(), project.encoded, `${sessionId}.jsonl`);
|
|
626
|
+
if (!file.startsWith(`${path.join(ccProjectsDir(), project.encoded)}${path.sep}`))
|
|
627
|
+
return null;
|
|
628
|
+
const detail = readTranscriptFile(file);
|
|
629
|
+
const bind = buildBindMap().get(sessionId);
|
|
630
|
+
const taskTurns = bind?.evolSessionId ? readRunnerTaskTurns(resolvePaths().logs, bind.evolSessionId) : [];
|
|
631
|
+
const turns = detail.turns.concat(taskTurns).sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
632
|
+
const block = turns[key[0]]?.blocks?.[key[1]];
|
|
633
|
+
return block?.kind === 'tool_result' && typeof block.text === 'string' ? block.text : null;
|
|
584
634
|
}
|
|
585
635
|
async function buildSnapshot(params) {
|
|
636
|
+
const startedAt = Date.now();
|
|
637
|
+
const metrics = {
|
|
638
|
+
baseagent: 'claude',
|
|
639
|
+
projectMs: 0,
|
|
640
|
+
bindingsMs: 0,
|
|
641
|
+
transcriptListMs: 0,
|
|
642
|
+
detailMs: 0,
|
|
643
|
+
totalMs: 0,
|
|
644
|
+
detailCacheHit: false,
|
|
645
|
+
};
|
|
646
|
+
const projectStartedAt = Date.now();
|
|
586
647
|
const projects = listProjects();
|
|
648
|
+
metrics.projectMs = Date.now() - projectStartedAt;
|
|
649
|
+
const bindingsStartedAt = Date.now();
|
|
587
650
|
const bindMap = buildBindMap();
|
|
651
|
+
metrics.bindingsMs = Date.now() - bindingsStartedAt;
|
|
588
652
|
const project = resolveProject(params, projects);
|
|
589
|
-
if (!project)
|
|
590
|
-
|
|
653
|
+
if (!project) {
|
|
654
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
655
|
+
return { baseagent: 'claude', projects: [], project: null, transcripts: [], turns: [], sessionId: null, metrics };
|
|
656
|
+
}
|
|
657
|
+
const transcriptListStartedAt = Date.now();
|
|
591
658
|
const metas = listTranscripts(project.encoded);
|
|
659
|
+
metrics.transcriptListMs = Date.now() - transcriptListStartedAt;
|
|
592
660
|
const selfNames = await getAgentSelfNames();
|
|
593
661
|
const transcripts = metas.map(m => {
|
|
594
662
|
const bind = bindMap.get(m.id);
|
|
@@ -603,17 +671,22 @@ async function buildSnapshot(params) {
|
|
|
603
671
|
});
|
|
604
672
|
const projList = projects.map(p => ({ encoded: p.encoded, label: p.label, cwd: p.cwd, count: p.count }));
|
|
605
673
|
const sessionId = params.sessionId || null;
|
|
606
|
-
if (!sessionId)
|
|
607
|
-
|
|
608
|
-
|
|
674
|
+
if (!sessionId) {
|
|
675
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
676
|
+
return { baseagent: 'claude', projects: projList, project: project.encoded, transcripts, turns: [], sessionId: null, metrics };
|
|
677
|
+
}
|
|
678
|
+
const detailStartedAt = Date.now();
|
|
679
|
+
const detail = readTranscriptFile(path.join(ccProjectsDir(), project.encoded, `${sessionId}.jsonl`), metrics);
|
|
680
|
+
metrics.detailMs = Date.now() - detailStartedAt;
|
|
609
681
|
const bind = bindMap.get(sessionId);
|
|
610
682
|
const taskTurns = bind?.evolSessionId ? readRunnerTaskTurns(resolvePaths().logs, bind.evolSessionId) : [];
|
|
611
683
|
const turns = detail.turns.concat(taskTurns).sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
612
|
-
const
|
|
684
|
+
const page = windowTranscriptTurns(turns, params.turnLimit, params.turnBefore);
|
|
613
685
|
const counts = { ...detail.counts, subtask: taskTurns.length };
|
|
614
686
|
const transcript = metas.find(meta => meta.id === sessionId);
|
|
615
687
|
const header = { sessionId, title: detail.title, model: detail.model, gitBranch: detail.gitBranch, version: detail.version, cwd: detail.cwd || project.cwd, totalTurns: detail.totalTurns, userMsgs: detail.userMsgs, totalMsgs: detail.totalMsgs, counts, inputTokens: detail.inputTokens, outputTokens: detail.outputTokens, contextTokens: detail.contextTokens, costUsd: detail.costUsd, sourceType: classifyTranscriptSource(bind, transcript?.cliCandidate === true), bound: !!bind, boundChannel: bind?.channelType ?? null, boundSelf: resolveSessionBindSelfName(bind, selfNames), online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false };
|
|
616
|
-
|
|
688
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
689
|
+
return { baseagent: 'claude', projects: projList, project: project.encoded, transcripts, turns: previewLargeToolResults(page.turns, page.turnPage.start), turnPage: page.turnPage, sessionId, header, metrics };
|
|
617
690
|
}
|
|
618
691
|
export const sessionSource = {
|
|
619
692
|
kind: 'session',
|
|
@@ -634,7 +707,7 @@ export const sessionSource = {
|
|
|
634
707
|
const fire = () => {
|
|
635
708
|
if (debounce)
|
|
636
709
|
clearTimeout(debounce);
|
|
637
|
-
debounce = setTimeout(() => { void buildSnapshot(params).then(push).catch(() => { }); },
|
|
710
|
+
debounce = setTimeout(() => { void buildSnapshot(params).then(push).catch(() => { }); }, 300);
|
|
638
711
|
};
|
|
639
712
|
const projects = listProjects();
|
|
640
713
|
const project = resolveProject(params, projects);
|