ec-web 0.0.3 → 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.
@@ -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, scanChatDirs, readJsonFile } from '../fs-utils.js';
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 = 4;
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 && isCountableUserMessage(o, id))
144
- userMsgs++;
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 { id, title, firstUser, gitBranch, version, userMsgs, totalMsgs, sizeKB: Math.round(stat.size / 1024), lastActivity: stat.mtimeMs };
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
- const map = new Map();
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
- const p = resolvePaths();
299
- for (const dir of scanChatDirs(p.sessionsDir)) {
300
- const active = readJsonFile(path.join(dir.dirPath, 'active.json'));
301
- if (!active || !active.agentSessionId)
302
- continue;
303
- map.set(active.agentSessionId, {
304
- channelType: active.channelType || dir.channelType,
305
- channelId: active.channelId || dir.channelId,
306
- selfAID: active.selfAID || dir.selfAID,
307
- peerName: (active.metadata && active.metadata.peerName) || null,
308
- name: active.name,
309
- updatedAt: active.updatedAt || 0,
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
- catch { }
314
- return map;
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 { ...m, bound: !!bind, boundChannel: bind?.channelType ?? null, boundPeer: bind ? (bind.peerName || bind.channelId) : null, online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false };
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 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: detail.counts, inputTokens: detail.inputTokens, outputTokens: detail.outputTokens, contextTokens: detail.contextTokens, costUsd: detail.costUsd, bound: !!bind, boundChannel: bind?.channelType ?? null, boundPeer: bind ? (bind.peerName || bind.channelId) : null, online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false };
446
- return { baseagent: 'claude', projects: projList, project: project.encoded, transcripts, turns: detail.turns, sessionId, header };
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 = () => { if (debounce)
465
- clearTimeout(debounce); debounce = setTimeout(() => { try {
466
- push(buildSnapshot(params));
467
- }
468
- catch { } }, 150); };
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 (debounce)
663
+ sessionWatcher.close(); if (logWatcher)
664
+ logWatcher.close(); if (debounce)
487
665
  clearTimeout(debounce); };
488
666
  },
489
667
  };