takomi 2.1.26 → 2.1.28

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.
@@ -40,6 +40,28 @@ function cost(model, input, cache, output, additiveCache = true) { const p = PRI
40
40
  function fmtTokens(n) { if (n >= 1e9) return `${(n/1e9).toFixed(2)}B`; if (n >= 1e6) return `${(n/1e6).toFixed(1)}M`; if (n >= 1e3) return `${(n/1e3).toFixed(1)}K`; return String(Math.round(n || 0)); }
41
41
  function fmtMoney(n) { return `$${(n || 0).toFixed(n > 100 ? 0 : 2)}`; }
42
42
  function ms(n) { if (!n) return '-'; const s = Math.round(n/1000); if (s < 60) return `${s}s`; const m = Math.floor(s/60); if (m < 60) return `${m}m ${s%60}s`; const h = Math.floor(m/60); return `${h}h ${m%60}m`; }
43
+ const ACTIVE_GAP_THRESHOLD_MS = 15 * 60 * 1000;
44
+ function timestampMs(value) {
45
+ if (typeof value === 'number') return Number.isFinite(value) ? value : null;
46
+ if (typeof value === 'string' && value) {
47
+ const parsed = Date.parse(value);
48
+ return Number.isFinite(parsed) ? parsed : null;
49
+ }
50
+ return null;
51
+ }
52
+ function addTimestamp(target, value) {
53
+ const parsed = timestampMs(value);
54
+ if (parsed !== null) target.push(parsed);
55
+ }
56
+ function activeDuration(timestamps, maxGapMs = ACTIVE_GAP_THRESHOLD_MS) {
57
+ const sorted = [...new Set((timestamps || []).filter(Number.isFinite))].sort((a, b) => a - b);
58
+ let total = 0;
59
+ for (let i = 1; i < sorted.length; i += 1) {
60
+ const delta = sorted[i] - sorted[i - 1];
61
+ if (delta > 0 && delta <= maxGapMs) total += delta;
62
+ }
63
+ return total;
64
+ }
43
65
  function parseSince(value) {
44
66
  if (!value) return null;
45
67
  const raw = String(value).trim().toLowerCase();
@@ -88,14 +110,20 @@ async function files(root, suffix = '.jsonl') {
88
110
  await walk(root); return out;
89
111
  }
90
112
 
113
+ function pushTask(taskRows, task) {
114
+ if (!task?.end || task.end === task.start) return;
115
+ task.activeMs = activeDuration(task.activityTimestamps || []);
116
+ taskRows.push(task);
117
+ }
118
+
91
119
  async function scanPiSessions(root, source, events, sessionRows = [], taskRows = []) {
92
120
  for (const file of await files(root)) {
93
121
  let provider = 'unknown', model = 'unknown', session = path.basename(file, '.jsonl'), cwd = '', currentTask = null;
94
- const row = { key: session, session, source, file, project: projectKey(file), cwd, start: '', end: '', turns: 0, messages: 0, toolCalls: 0, subagentCalls: 0, roles: new Map(), stages: new Map(), workflows: new Map(), activeMs: 0 };
122
+ const row = { key: session, session, source, file, project: projectKey(file), cwd, start: '', end: '', turns: 0, messages: 0, toolCalls: 0, subagentCalls: 0, roles: new Map(), stages: new Map(), workflows: new Map(), activeMs: 0, activityTimestamps: [] };
95
123
  const text = await fs.readFile(file, 'utf8').catch(() => '');
96
124
  for (const line of text.split(/\r?\n/)) {
97
125
  const obj = safeJson(line); if (!obj) continue;
98
- if (obj.timestamp) { row.start ||= obj.timestamp; row.end = obj.timestamp; }
126
+ if (obj.timestamp) { row.start ||= obj.timestamp; row.end = obj.timestamp; addTimestamp(row.activityTimestamps, obj.timestamp); }
99
127
  if (obj.type === 'session') { session = obj.id || session; cwd = obj.cwd || cwd; row.key = session; row.session = session; row.cwd = cwd; }
100
128
  if (obj.type === 'model_change') { provider = obj.provider || provider; model = obj.modelId || model; }
101
129
  if (obj.type === 'custom' && obj.customType === 'takomi-runtime-state' && obj.data) {
@@ -112,12 +140,14 @@ async function scanPiSessions(root, source, events, sessionRows = [], taskRows =
112
140
  row.messages += 1;
113
141
  const ts = obj.timestamp || msg.timestamp || '';
114
142
  if (msg.role === 'user') {
115
- if (currentTask?.end && currentTask.end !== currentTask.start) taskRows.push(currentTask);
143
+ pushTask(taskRows, currentTask);
116
144
  row.turns += 1;
117
145
  const textPart = (msg.content || []).find(p => p?.type === 'text')?.text || '';
118
- currentTask = { source, file, session, project: projectKey(file), cwd, start: ts, end: ts, provider, model, turns: 1, toolCalls: 0, title: String(textPart).replace(/\s+/g, ' ').trim() };
146
+ currentTask = { source, file, session, project: projectKey(file), cwd, start: ts, end: ts, provider, model, turns: 1, toolCalls: 0, title: String(textPart).replace(/\s+/g, ' ').trim(), activityTimestamps: [] };
147
+ addTimestamp(currentTask.activityTimestamps, ts);
119
148
  } else if (currentTask && ts) {
120
149
  currentTask.end = ts;
150
+ addTimestamp(currentTask.activityTimestamps, ts);
121
151
  }
122
152
  for (const part of msg.content || []) {
123
153
  if (!part || part.type !== 'toolCall') continue;
@@ -135,8 +165,8 @@ async function scanPiSessions(root, source, events, sessionRows = [], taskRows =
135
165
  const u = msg && msg.usage;
136
166
  if (u) events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'usage', input: +u.input||0, cache: +u.cacheRead||0, output: +u.output||0, total: +u.totalTokens||0, cost: cost(model, +u.input||0, +u.cacheRead||0, +u.output||0, true) });
137
167
  }
138
- if (currentTask?.end && currentTask.end !== currentTask.start) taskRows.push(currentTask);
139
- row.activeMs = taskRows.filter(t => t.file === file).reduce((sum, t) => sum + taskDuration(t), 0);
168
+ pushTask(taskRows, currentTask);
169
+ row.activeMs = activeDuration(row.activityTimestamps);
140
170
  if (row.messages || row.toolCalls || row.turns) sessionRows.push(row);
141
171
  }
142
172
  }
@@ -336,9 +366,7 @@ function sessionDuration(row) {
336
366
  return row?.activeMs || 0;
337
367
  }
338
368
  function taskDuration(row) {
339
- const start = Date.parse(row?.start || '');
340
- const end = Date.parse(row?.end || '');
341
- return Number.isFinite(start) && Number.isFinite(end) && end >= start ? end - start : 0;
369
+ return row?.activeMs ?? activeDuration(row?.activityTimestamps || []);
342
370
  }
343
371
  function taskLabel(row, width = 34) {
344
372
  const label = row?.title || row?.project || row?.session || 'unknown';
@@ -379,7 +407,7 @@ function renderFocusedView(stats, opts = {}) {
379
407
  ` ${pc.cyan(ms(sessionDuration(r)))} ${pc.dim(r.turns + ' turns')} ${pc.dim(r.toolCalls + ' tools')}`,
380
408
  ` ${pc.dim(r.file || '')}`,
381
409
  ].join('\n'), limit);
382
- if (view === 'tasks-full' || view === 'task-full') return renderFullList('Longest Tasks — Full Prompts', stats.topTasks || [], (r, i) => [
410
+ if (view === 'tasks-full' || view === 'task-full') return renderFullList('Longest Active Turns — Full Prompts', stats.topTasks || [], (r, i) => [
383
411
  ` ${pc.dim(String(i + 1).padStart(2, '0') + '.')} ${pc.cyan(ms(taskDuration(r)))} ${pc.magenta(r.toolCalls + ' tools')} ${pc.dim(dayOf(r.start))}`,
384
412
  ` ${pc.white(indentWrap(r.title || r.project || r.session || 'unknown'))}`,
385
413
  ` ${pc.dim(r.project || '')}`,
@@ -426,7 +454,7 @@ function renderFocusedView(stats, opts = {}) {
426
454
  { width: 8, align: 'right', get: r => pc.cyan(String(r.toolCalls)) },
427
455
  { width: 8, align: 'left', get: () => pc.dim('tools') },
428
456
  ]],
429
- tasks: ['Longest Tasks', stats.topTasks || [], [
457
+ tasks: ['Longest Active Turns', stats.topTasks || [], [
430
458
  { width: 6, align: 'left', get: r => pc.dim(dayOf(r.start).slice(5)) },
431
459
  { width: 9, align: 'right', get: r => pc.cyan(ms(taskDuration(r))) },
432
460
  { width: 11, align: 'right', get: r => pc.magenta(`${r.toolCalls} tools`) },
@@ -518,9 +546,9 @@ export function renderTakomiStats(stats, opts = {}) {
518
546
  // ── Duration Cards ────────────────────────────────────────────────────
519
547
  lines.push('');
520
548
  const cards3 = [
521
- statCard(longestSession ? ms(sessionDuration(longestSession)) : '-', 'Active Session', pc.cyan),
549
+ statCard(longestSession ? ms(sessionDuration(longestSession)) : '-', 'Top Active Session', pc.cyan),
522
550
  statCard(longestSession ? String(longestSession.turns) : '-', 'Turns in Session', pc.cyan),
523
- statCard(longestTask ? ms(taskDuration(longestTask)) : '-', 'Longest Task', pc.magenta),
551
+ statCard(longestTask ? ms(taskDuration(longestTask)) : '-', 'Longest Active Turn', pc.magenta),
524
552
  statCard(longestTask ? String(longestTask.toolCalls) : '-', 'Tools in Task', pc.magenta),
525
553
  statCard(stats.mostSubagentsSession ? String(stats.mostSubagentsSession.subagentCalls) : '0', 'Max Subagents', pc.blue),
526
554
  ];
@@ -530,7 +558,7 @@ export function renderTakomiStats(stats, opts = {}) {
530
558
 
531
559
  // ── Info line ─────────────────────────────────────────────────────────
532
560
  lines.push('');
533
- const infoText = `Peak: ${peak?.key || '-'} · ${streaks.quietDays} quiet days · ${stats.totals.events.toLocaleString()} events${stats.since ? ` · since ${stats.since}` : ''}`;
561
+ const infoText = `Peak: ${peak?.key || '-'} · ${streaks.quietDays} quiet days · ${stats.totals.events.toLocaleString()} events · active gaps ≤15m${stats.since ? ` · since ${stats.since}` : ''}`;
534
562
  lines.push(center(pc.dim(infoText), W));
535
563
 
536
564
  lines.push('');
@@ -586,10 +614,10 @@ export function renderTakomiStats(stats, opts = {}) {
586
614
  ]));
587
615
  }
588
616
 
589
- // ── Longest Tasks ──────────────────────────────────────────────────────
617
+ // ── Longest Active Turns ───────────────────────────────────────────────
590
618
  if (stats.topTasks?.length) {
591
619
  lines.push('');
592
- lines.push(renderTable('Longest Tasks', stats.topTasks.slice(0, 5), [
620
+ lines.push(renderTable('Longest Active Turns', stats.topTasks.slice(0, 5), [
593
621
  { width: 6, align: 'left', get: r => pc.dim(dayOf(r.start).slice(5)) },
594
622
  { width: 9, align: 'right', get: r => pc.cyan(ms(taskDuration(r))) },
595
623
  { width: 11, align: 'right', get: r => pc.magenta(`${r.toolCalls} tools`) },