ec-web 0.0.5 → 0.0.7

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.
@@ -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 shown = turns.length > 500 ? turns.slice(-500) : turns;
606
- return {
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 detail = readTranscriptFile(sessionId, project.cwd);
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: detail.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 ──
@@ -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 shown = turns.length > 500 ? turns.slice(-500) : turns;
583
- return { turns: shown, totalTurns: turns.length, userMsgs, totalMsgs, counts, inputTokens: inTok, outputTokens: outTok, contextTokens, costUsd, model, gitBranch: branch, version, title, cwd };
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
- return { baseagent: 'claude', projects: [], project: null, transcripts: [], turns: [], sessionId: null };
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
- return { baseagent: 'claude', projects: projList, project: project.encoded, transcripts, turns: [], sessionId: null };
608
- const detail = readTranscriptFile(path.join(ccProjectsDir(), project.encoded, `${sessionId}.jsonl`));
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 shownTurns = turns.length > 500 ? turns.slice(-500) : turns;
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
- return { baseagent: 'claude', projects: projList, project: project.encoded, transcripts, turns: shownTurns, sessionId, header };
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(() => { }); }, 150);
710
+ debounce = setTimeout(() => { void buildSnapshot(params).then(push).catch(() => { }); }, 300);
638
711
  };
639
712
  const projects = listProjects();
640
713
  const project = resolveProject(params, projects);
@@ -447,7 +447,7 @@ function getPeerInfo(peerKey) {
447
447
  if (target.startsWith('group.')) {
448
448
  // 群聊:读取群信息
449
449
  const { sessionsDir } = resolvePaths();
450
- const groupDir = path.join(sessionsDir, 'aun', encodeSegment(agentAid), encodeSegment(target));
450
+ const groupDir = path.join(sessionsDir, encodeSegment(agentAid), 'sessions', encodeSegment(`aun#${agentAid}#main`), encodeSegment(target));
451
451
  const activeJsonPath = path.join(groupDir, 'active.json');
452
452
  if (fs.existsSync(activeJsonPath)) {
453
453
  const activeData = JSON.parse(fs.readFileSync(activeJsonPath, 'utf-8'));
@@ -0,0 +1,35 @@
1
+ export function windowTranscriptTurns(turns, requestedLimit, requestedBefore, defaultLimit = 100, maxLimit = 200) {
2
+ const requested = Number(requestedLimit);
3
+ const limit = Number.isFinite(requested)
4
+ ? Math.min(Math.max(Math.trunc(requested), 20), maxLimit)
5
+ : defaultLimit;
6
+ const beforeValue = requestedBefore == null ? Number.NaN : Number(requestedBefore);
7
+ const before = Number.isFinite(beforeValue) && beforeValue >= 0 ? Math.trunc(beforeValue) : null;
8
+ const end = before == null ? turns.length : Math.min(Math.max(before, 0), turns.length);
9
+ const start = Math.max(0, end - limit);
10
+ return {
11
+ turns: turns.slice(start, end),
12
+ turnPage: { limit, before, nextBefore: start > 0 ? start : null, hasMore: start > 0, start },
13
+ };
14
+ }
15
+ export function previewLargeToolResults(turns, start, previewChars = 4_000) {
16
+ return turns.map((turn, turnOffset) => ({
17
+ ...turn,
18
+ blocks: (turn.blocks || []).map((block, blockIndex) => {
19
+ if (block.kind !== 'tool_result' || typeof block.text !== 'string' || block.text.length <= previewChars)
20
+ return block;
21
+ return {
22
+ ...block,
23
+ text: block.text.slice(0, previewChars),
24
+ resultKey: `${start + turnOffset}:${blockIndex}`,
25
+ truncated: true,
26
+ };
27
+ }),
28
+ }));
29
+ }
30
+ export function parseTranscriptResultKey(value) {
31
+ const match = /^(\d+):(\d+)$/.exec(String(value || ''));
32
+ if (!match)
33
+ return null;
34
+ return [Number(match[1]), Number(match[2])];
35
+ }