ec-web 0.0.4 → 0.0.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/dist/server.js +47 -5
- package/dist/sources/msg.js +187 -5
- package/dist/sources/session-codex.js +43 -29
- package/dist/sources/session-origin.js +195 -0
- package/dist/sources/session.js +211 -33
- package/dist/static/app.js +285 -43
- package/dist/static/index.html +4 -5
- package/dist/static/style.css +55 -1
- package/dist/static/vendor/echarts-5.6.0.min.js +45 -0
- package/package.json +2 -2
|
@@ -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
|
+
}
|
package/dist/sources/session.js
CHANGED
|
@@ -13,8 +13,10 @@
|
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import { resolvePaths, ccProjectsDir } from '../paths.js';
|
|
16
|
-
import { encodePath
|
|
16
|
+
import { encodePath } from '../fs-utils.js';
|
|
17
17
|
import { snapshotCodex, subscribeCodex, setDebugLog as setCodexDebugLog } from './session-codex.js';
|
|
18
|
+
import { getAgentSelfNames } from './msg.js';
|
|
19
|
+
import { buildSessionBindMap, classifyTranscriptSource, isSyntheticCliPrompt, resolveSessionBindSelfName, } from './session-origin.js';
|
|
18
20
|
let _dlog = null;
|
|
19
21
|
export function setDebugLog(log) {
|
|
20
22
|
_dlog = log;
|
|
@@ -25,7 +27,7 @@ function dlog(line) { if (_dlog)
|
|
|
25
27
|
_dlog(line);
|
|
26
28
|
}
|
|
27
29
|
catch { } }
|
|
28
|
-
const CACHE_VERSION =
|
|
30
|
+
const CACHE_VERSION = 5;
|
|
29
31
|
const _metaCache = new Map();
|
|
30
32
|
function cacheDir(encoded) {
|
|
31
33
|
const dataDir = resolvePaths().dataDir;
|
|
@@ -120,7 +122,8 @@ function extractMeta(file, id, stat) {
|
|
|
120
122
|
raw = fs.readFileSync(file, 'utf-8');
|
|
121
123
|
}
|
|
122
124
|
catch { }
|
|
123
|
-
let title = '', firstUser = '', gitBranch = '', version = '', userMsgs = 0, totalMsgs = 0;
|
|
125
|
+
let title = '', firstUser = '', gitBranch = '', version = '', entrypoint = '', userMsgs = 0, totalMsgs = 0;
|
|
126
|
+
let hasRealCliMessage = false, internalSubagent = id.startsWith('agent-');
|
|
124
127
|
for (const line of raw.split('\n')) {
|
|
125
128
|
if (!line)
|
|
126
129
|
continue;
|
|
@@ -140,8 +143,20 @@ function extractMeta(file, id, stat) {
|
|
|
140
143
|
totalMsgs++;
|
|
141
144
|
else if (isUser) {
|
|
142
145
|
totalMsgs++;
|
|
143
|
-
if (o
|
|
144
|
-
|
|
146
|
+
if (o) {
|
|
147
|
+
if (isCountableUserMessage(o, id))
|
|
148
|
+
userMsgs++;
|
|
149
|
+
if (!entrypoint && typeof o.entrypoint === 'string')
|
|
150
|
+
entrypoint = o.entrypoint;
|
|
151
|
+
if (o.isSidechain || o.agentId)
|
|
152
|
+
internalSubagent = true;
|
|
153
|
+
const content = o.message?.content;
|
|
154
|
+
if (!isToolResultOnly(content)) {
|
|
155
|
+
const text = extractMessageText(content);
|
|
156
|
+
if (text.trim() && !isSyntheticCliPrompt(text, 'claude'))
|
|
157
|
+
hasRealCliMessage = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
145
160
|
}
|
|
146
161
|
if ((!gitBranch || !version || !firstUser) && (line.indexOf('gitBranch') !== -1 || line.indexOf('"version"') !== -1 || line.indexOf('ai-title') !== -1 || (isUser && !firstUser))) {
|
|
147
162
|
if (!o) {
|
|
@@ -165,7 +180,12 @@ function extractMeta(file, id, stat) {
|
|
|
165
180
|
}
|
|
166
181
|
}
|
|
167
182
|
}
|
|
168
|
-
return {
|
|
183
|
+
return {
|
|
184
|
+
id, title, firstUser, gitBranch, version, userMsgs, totalMsgs,
|
|
185
|
+
sizeKB: Math.round(stat.size / 1024),
|
|
186
|
+
lastActivity: stat.mtimeMs,
|
|
187
|
+
cliCandidate: !internalSubagent && (entrypoint === 'cli' || entrypoint === 'sdk-cli') && hasRealCliMessage,
|
|
188
|
+
};
|
|
169
189
|
}
|
|
170
190
|
function listTranscripts(encoded) {
|
|
171
191
|
const dir = path.join(ccProjectsDir(), encoded);
|
|
@@ -293,25 +313,162 @@ function resolveProject(params, projects) {
|
|
|
293
313
|
return projects[0] || null;
|
|
294
314
|
}
|
|
295
315
|
export function buildBindMap() {
|
|
296
|
-
|
|
316
|
+
return buildSessionBindMap();
|
|
317
|
+
}
|
|
318
|
+
// ── EvolCore runner 子任务事件 ──
|
|
319
|
+
const RUNNER_TASK_EVENT_TYPES = new Set([
|
|
320
|
+
'runner:task-started',
|
|
321
|
+
'runner:task-progress',
|
|
322
|
+
'runner:task-notification',
|
|
323
|
+
]);
|
|
324
|
+
const EVENT_LOG_RE = /^events(?:-.+)?\.log$/;
|
|
325
|
+
const _runnerLogCache = new Map();
|
|
326
|
+
function parseRunnerTaskLines(raw) {
|
|
327
|
+
const events = [];
|
|
328
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
329
|
+
if (!line.includes('"type":"runner:task-'))
|
|
330
|
+
continue;
|
|
331
|
+
try {
|
|
332
|
+
const event = JSON.parse(line);
|
|
333
|
+
if (RUNNER_TASK_EVENT_TYPES.has(event.type) && event.sessionId)
|
|
334
|
+
events.push(event);
|
|
335
|
+
}
|
|
336
|
+
catch { }
|
|
337
|
+
}
|
|
338
|
+
return events;
|
|
339
|
+
}
|
|
340
|
+
function readRunnerLogFile(file) {
|
|
341
|
+
let stat;
|
|
297
342
|
try {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
343
|
+
stat = fs.statSync(file);
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
_runnerLogCache.delete(file);
|
|
347
|
+
return [];
|
|
348
|
+
}
|
|
349
|
+
let cached = _runnerLogCache.get(file);
|
|
350
|
+
if (!cached || stat.size < cached.offset)
|
|
351
|
+
cached = { offset: 0, events: [] };
|
|
352
|
+
if (stat.size === cached.offset) {
|
|
353
|
+
_runnerLogCache.set(file, cached);
|
|
354
|
+
return cached.events;
|
|
355
|
+
}
|
|
356
|
+
const bytes = stat.size - cached.offset;
|
|
357
|
+
const buffer = Buffer.allocUnsafe(bytes);
|
|
358
|
+
let fd = null;
|
|
359
|
+
try {
|
|
360
|
+
fd = fs.openSync(file, 'r');
|
|
361
|
+
fs.readSync(fd, buffer, 0, bytes, cached.offset);
|
|
362
|
+
}
|
|
363
|
+
catch {
|
|
364
|
+
return cached.events;
|
|
365
|
+
}
|
|
366
|
+
finally {
|
|
367
|
+
if (fd != null)
|
|
368
|
+
try {
|
|
369
|
+
fs.closeSync(fd);
|
|
370
|
+
}
|
|
371
|
+
catch { }
|
|
372
|
+
}
|
|
373
|
+
// 只推进到最后一个完整 JSONL 行,避免 watcher 在写入中途触发时丢事件。
|
|
374
|
+
const lastBreak = buffer.lastIndexOf(0x0a);
|
|
375
|
+
if (lastBreak < 0) {
|
|
376
|
+
_runnerLogCache.set(file, cached);
|
|
377
|
+
return cached.events;
|
|
378
|
+
}
|
|
379
|
+
const parsed = parseRunnerTaskLines(buffer.subarray(0, lastBreak + 1).toString('utf8'));
|
|
380
|
+
cached = {
|
|
381
|
+
offset: cached.offset + lastBreak + 1,
|
|
382
|
+
events: cached.events.concat(parsed),
|
|
383
|
+
};
|
|
384
|
+
_runnerLogCache.set(file, cached);
|
|
385
|
+
return cached.events;
|
|
386
|
+
}
|
|
387
|
+
function runnerEventTimestamp(event) {
|
|
388
|
+
const timestamp = Number(event.timestamp);
|
|
389
|
+
if (Number.isFinite(timestamp) && timestamp > 0)
|
|
390
|
+
return timestamp;
|
|
391
|
+
const parsed = event.ts ? Date.parse(event.ts) : 0;
|
|
392
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
393
|
+
}
|
|
394
|
+
export function readRunnerTaskTurns(logsDir, evolSessionId) {
|
|
395
|
+
let files;
|
|
396
|
+
try {
|
|
397
|
+
files = fs.readdirSync(logsDir)
|
|
398
|
+
.filter(name => EVENT_LOG_RE.test(name))
|
|
399
|
+
.sort()
|
|
400
|
+
.map(name => path.join(logsDir, name));
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return [];
|
|
404
|
+
}
|
|
405
|
+
const validFiles = new Set(files);
|
|
406
|
+
for (const cachedFile of _runnerLogCache.keys()) {
|
|
407
|
+
if (path.dirname(cachedFile) === logsDir && !validFiles.has(cachedFile))
|
|
408
|
+
_runnerLogCache.delete(cachedFile);
|
|
409
|
+
}
|
|
410
|
+
const events = files
|
|
411
|
+
.flatMap(readRunnerLogFile)
|
|
412
|
+
.filter(event => event.sessionId === evolSessionId)
|
|
413
|
+
.sort((a, b) => runnerEventTimestamp(a) - runnerEventTimestamp(b));
|
|
414
|
+
const tasks = new Map();
|
|
415
|
+
for (const event of events) {
|
|
416
|
+
const taskId = event.taskId || event.toolUseId;
|
|
417
|
+
if (!taskId)
|
|
418
|
+
continue;
|
|
419
|
+
const at = runnerEventTimestamp(event);
|
|
420
|
+
let task = tasks.get(taskId);
|
|
421
|
+
if (!task) {
|
|
422
|
+
task = {
|
|
423
|
+
taskId,
|
|
424
|
+
taskKind: event.taskKind || 'agent',
|
|
425
|
+
toolUseId: event.toolUseId || null,
|
|
426
|
+
description: event.description || taskId,
|
|
427
|
+
subagentType: event.subagentType || null,
|
|
428
|
+
status: 'running',
|
|
429
|
+
terminalReason: '',
|
|
430
|
+
progress: '',
|
|
431
|
+
lastToolName: '',
|
|
432
|
+
totalTokens: 0,
|
|
433
|
+
toolUses: 0,
|
|
434
|
+
durationMs: 0,
|
|
435
|
+
startedAt: at,
|
|
436
|
+
updatedAt: at,
|
|
437
|
+
};
|
|
438
|
+
tasks.set(taskId, task);
|
|
439
|
+
}
|
|
440
|
+
if (event.type === 'runner:task-started') {
|
|
441
|
+
task.startedAt = task.startedAt ? Math.min(task.startedAt, at) : at;
|
|
442
|
+
task.status = 'running';
|
|
443
|
+
}
|
|
444
|
+
else if (event.type === 'runner:task-progress') {
|
|
445
|
+
task.progress = event.summary || event.description || task.progress;
|
|
311
446
|
}
|
|
447
|
+
else if (event.type === 'runner:task-notification') {
|
|
448
|
+
task.status = event.status || 'completed';
|
|
449
|
+
task.terminalReason = event.terminalReason || task.terminalReason;
|
|
450
|
+
}
|
|
451
|
+
task.updatedAt = Math.max(task.updatedAt || 0, at);
|
|
452
|
+
task.taskKind = event.taskKind || task.taskKind;
|
|
453
|
+
task.toolUseId = event.toolUseId || task.toolUseId;
|
|
454
|
+
task.description = event.type === 'runner:task-progress'
|
|
455
|
+
? task.description
|
|
456
|
+
: (event.description || task.description);
|
|
457
|
+
task.subagentType = event.subagentType || task.subagentType;
|
|
458
|
+
task.lastToolName = event.lastToolName || task.lastToolName;
|
|
459
|
+
task.totalTokens = event.usage?.totalTokens ?? event.totalTokens ?? task.totalTokens;
|
|
460
|
+
task.toolUses = event.usage?.toolUses ?? event.toolUses ?? task.toolUses;
|
|
461
|
+
task.durationMs = event.usage?.durationMs ?? event.durationMs ?? task.durationMs;
|
|
312
462
|
}
|
|
313
|
-
|
|
314
|
-
|
|
463
|
+
return Array.from(tasks.values())
|
|
464
|
+
.sort((a, b) => a.startedAt - b.startedAt)
|
|
465
|
+
.map(task => ({
|
|
466
|
+
role: 'system',
|
|
467
|
+
ts: task.startedAt,
|
|
468
|
+
uuid: `runner-task:${evolSessionId}:${task.taskId}`,
|
|
469
|
+
category: 'subtask',
|
|
470
|
+
blocks: [{ kind: 'subtask', ...task }],
|
|
471
|
+
}));
|
|
315
472
|
}
|
|
316
473
|
// ── 完整 transcript 解析(省略 tool 参数/chat send 检测等细节,保留费用计算)──
|
|
317
474
|
const PRICING = {
|
|
@@ -425,16 +582,24 @@ function readTranscriptFile(file) {
|
|
|
425
582
|
const shown = turns.length > 500 ? turns.slice(-500) : turns;
|
|
426
583
|
return { turns: shown, totalTurns: turns.length, userMsgs, totalMsgs, counts, inputTokens: inTok, outputTokens: outTok, contextTokens, costUsd, model, gitBranch: branch, version, title, cwd };
|
|
427
584
|
}
|
|
428
|
-
function buildSnapshot(params) {
|
|
585
|
+
async function buildSnapshot(params) {
|
|
429
586
|
const projects = listProjects();
|
|
430
587
|
const bindMap = buildBindMap();
|
|
431
588
|
const project = resolveProject(params, projects);
|
|
432
589
|
if (!project)
|
|
433
590
|
return { baseagent: 'claude', projects: [], project: null, transcripts: [], turns: [], sessionId: null };
|
|
434
591
|
const metas = listTranscripts(project.encoded);
|
|
592
|
+
const selfNames = await getAgentSelfNames();
|
|
435
593
|
const transcripts = metas.map(m => {
|
|
436
594
|
const bind = bindMap.get(m.id);
|
|
437
|
-
return {
|
|
595
|
+
return {
|
|
596
|
+
...m,
|
|
597
|
+
sourceType: classifyTranscriptSource(bind, m.cliCandidate),
|
|
598
|
+
bound: !!bind,
|
|
599
|
+
boundChannel: bind?.channelType ?? null,
|
|
600
|
+
boundSelf: resolveSessionBindSelfName(bind, selfNames),
|
|
601
|
+
online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false,
|
|
602
|
+
};
|
|
438
603
|
});
|
|
439
604
|
const projList = projects.map(p => ({ encoded: p.encoded, label: p.label, cwd: p.cwd, count: p.count }));
|
|
440
605
|
const sessionId = params.sessionId || null;
|
|
@@ -442,8 +607,13 @@ function buildSnapshot(params) {
|
|
|
442
607
|
return { baseagent: 'claude', projects: projList, project: project.encoded, transcripts, turns: [], sessionId: null };
|
|
443
608
|
const detail = readTranscriptFile(path.join(ccProjectsDir(), project.encoded, `${sessionId}.jsonl`));
|
|
444
609
|
const bind = bindMap.get(sessionId);
|
|
445
|
-
const
|
|
446
|
-
|
|
610
|
+
const taskTurns = bind?.evolSessionId ? readRunnerTaskTurns(resolvePaths().logs, bind.evolSessionId) : [];
|
|
611
|
+
const turns = detail.turns.concat(taskTurns).sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
612
|
+
const shownTurns = turns.length > 500 ? turns.slice(-500) : turns;
|
|
613
|
+
const counts = { ...detail.counts, subtask: taskTurns.length };
|
|
614
|
+
const transcript = metas.find(meta => meta.id === sessionId);
|
|
615
|
+
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
|
+
return { baseagent: 'claude', projects: projList, project: project.encoded, transcripts, turns: shownTurns, sessionId, header };
|
|
447
617
|
}
|
|
448
618
|
export const sessionSource = {
|
|
449
619
|
kind: 'session',
|
|
@@ -460,12 +630,12 @@ export const sessionSource = {
|
|
|
460
630
|
return subscribeCodex(params, push);
|
|
461
631
|
}
|
|
462
632
|
const p = resolvePaths();
|
|
463
|
-
let projectWatcher = null, sessionWatcher = null, debounce = null;
|
|
464
|
-
const fire = () => {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
633
|
+
let projectWatcher = null, sessionWatcher = null, logWatcher = null, debounce = null;
|
|
634
|
+
const fire = () => {
|
|
635
|
+
if (debounce)
|
|
636
|
+
clearTimeout(debounce);
|
|
637
|
+
debounce = setTimeout(() => { void buildSnapshot(params).then(push).catch(() => { }); }, 150);
|
|
638
|
+
};
|
|
469
639
|
const projects = listProjects();
|
|
470
640
|
const project = resolveProject(params, projects);
|
|
471
641
|
if (project) {
|
|
@@ -481,9 +651,17 @@ export const sessionSource = {
|
|
|
481
651
|
fire(); });
|
|
482
652
|
}
|
|
483
653
|
catch { }
|
|
654
|
+
if (params.sessionId) {
|
|
655
|
+
try {
|
|
656
|
+
logWatcher = fs.watch(p.logs, (_evt, filename) => { if (!filename || EVENT_LOG_RE.test(String(filename)))
|
|
657
|
+
fire(); });
|
|
658
|
+
}
|
|
659
|
+
catch { }
|
|
660
|
+
}
|
|
484
661
|
return () => { if (projectWatcher)
|
|
485
662
|
projectWatcher.close(); if (sessionWatcher)
|
|
486
|
-
sessionWatcher.close(); if (
|
|
663
|
+
sessionWatcher.close(); if (logWatcher)
|
|
664
|
+
logWatcher.close(); if (debounce)
|
|
487
665
|
clearTimeout(debounce); };
|
|
488
666
|
},
|
|
489
667
|
};
|