@threadcrumbs/common 0.1.3 → 0.1.4
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/lib/claude-transcript.mjs +4 -0
- package/lib/opencode-store.mjs +30 -3
- package/lib/telemetry-fields.mjs +34 -0
- package/package.json +1 -1
- package/telemetry/README.md +1 -0
- package/telemetry/antigravity.mjs +1 -0
- package/telemetry/claude-code.mjs +2 -1
- package/telemetry/codex.mjs +6 -0
- package/telemetry/cursor.mjs +1 -0
- package/telemetry/hermes.mjs +1 -0
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
clipBlock,
|
|
11
11
|
diffHunk,
|
|
12
12
|
emptyActivity,
|
|
13
|
+
emptySample,
|
|
13
14
|
endCall,
|
|
14
15
|
fileList,
|
|
15
16
|
firstChange,
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
openTool,
|
|
21
22
|
patchLines,
|
|
22
23
|
planItems,
|
|
24
|
+
samplePoint,
|
|
23
25
|
settleCalls,
|
|
24
26
|
startCall,
|
|
25
27
|
textList,
|
|
@@ -176,6 +178,7 @@ function emptyState(sidechain, detail, words, sizes) {
|
|
|
176
178
|
cost: null,
|
|
177
179
|
plan: null,
|
|
178
180
|
prompts: 0,
|
|
181
|
+
contextSample: emptySample(),
|
|
179
182
|
uuids: new Set(),
|
|
180
183
|
}
|
|
181
184
|
}
|
|
@@ -440,6 +443,7 @@ function applyAssistant(state, record, at) {
|
|
|
440
443
|
for (const key of Object.keys(next)) state.tokens[key] += next[key] - (previous?.[key] || 0)
|
|
441
444
|
state.usageById.set(message.id, next)
|
|
442
445
|
state.context = next.input + next.cacheRead + next.cacheWrite + next.output
|
|
446
|
+
samplePoint(state.contextSample, state.context, { update: Boolean(previous) })
|
|
443
447
|
const cost = requestCost(message.model, usage)
|
|
444
448
|
let added = null
|
|
445
449
|
if (cost != null) {
|
package/lib/opencode-store.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
clip,
|
|
11
11
|
clipBlock,
|
|
12
12
|
emptyActivity,
|
|
13
|
+
emptySample,
|
|
13
14
|
endCall,
|
|
14
15
|
fileList,
|
|
15
16
|
joinedText,
|
|
@@ -19,6 +20,8 @@ import {
|
|
|
19
20
|
openTool,
|
|
20
21
|
planItems,
|
|
21
22
|
planSummary,
|
|
23
|
+
sampleList,
|
|
24
|
+
samplePoint,
|
|
22
25
|
settleCalls,
|
|
23
26
|
startCall,
|
|
24
27
|
text,
|
|
@@ -458,6 +461,7 @@ function session(context, row, turn, extra) {
|
|
|
458
461
|
tool: turn.open && turn.tool ? toolNow(turn.tool) : null,
|
|
459
462
|
tokens: turn.tokens ? { ...turn.tokens } : null,
|
|
460
463
|
context: turn.context ? { used: turn.context, limit: context.byModel.get(`${turn.provider}/${turn.model}`) || 0 } : null,
|
|
464
|
+
contextHistory: sampleList(turn.contextSample, turn.context),
|
|
461
465
|
costUsd: turn.cost,
|
|
462
466
|
lines: extra.lines,
|
|
463
467
|
plan: planSummary(extra.plan),
|
|
@@ -504,7 +508,7 @@ function legacySessions(db, tables, context) {
|
|
|
504
508
|
}
|
|
505
509
|
|
|
506
510
|
function legacyTurn(db, row, context) {
|
|
507
|
-
const turn = { busy: false, failed: false, prompts: 0, firstPrompt: '', model: '', provider: '', effort: '', tokens: null, cost: null, context: 0, lines: null, tool: null, turnStartedAt: 0 }
|
|
511
|
+
const turn = { busy: false, failed: false, prompts: 0, firstPrompt: '', model: '', provider: '', effort: '', tokens: null, cost: null, context: 0, contextSample: emptySample(), lines: null, tool: null, turnStartedAt: 0 }
|
|
508
512
|
const typed = new Set(
|
|
509
513
|
db
|
|
510
514
|
.prepare(`SELECT message_id FROM part WHERE session_id = ? AND data LIKE '%"type":"text"%' AND data NOT LIKE '%"synthetic":true%'`)
|
|
@@ -537,7 +541,10 @@ function legacyTurn(db, row, context) {
|
|
|
537
541
|
if (data.providerID) turn.provider = text(data.providerID)
|
|
538
542
|
if (data.variant) turn.effort = text(data.variant)
|
|
539
543
|
if (data.cost != null) turn.cost = (turn.cost || 0) + number(data.cost)
|
|
540
|
-
if (number(data.tokens?.output) > 0)
|
|
544
|
+
if (number(data.tokens?.output) > 0) {
|
|
545
|
+
turn.context = windowOf(data.tokens)
|
|
546
|
+
samplePoint(turn.contextSample, turn.context)
|
|
547
|
+
}
|
|
541
548
|
}
|
|
542
549
|
const usage = usageFromColumns(row)
|
|
543
550
|
if (usage) {
|
|
@@ -605,8 +612,28 @@ function currentSessions(db, tables, context) {
|
|
|
605
612
|
return out
|
|
606
613
|
}
|
|
607
614
|
|
|
615
|
+
function currentSample(db, sessionId, before) {
|
|
616
|
+
let { contextSample: sample, contextSeq: seq = -1, contextCount: count = 0 } = before || {}
|
|
617
|
+
const kept = sample && db.prepare(`SELECT COUNT(*) AS n, MAX(seq) AS top FROM session_message WHERE session_id = ? AND type = 'assistant' AND seq <= ?`).get(sessionId, seq)
|
|
618
|
+
if (!kept || number(kept.n) !== count || (count && number(kept.top) !== seq)) {
|
|
619
|
+
sample = emptySample()
|
|
620
|
+
seq = -1
|
|
621
|
+
count = 0
|
|
622
|
+
}
|
|
623
|
+
const newer = db.prepare(`SELECT seq, data FROM session_message WHERE session_id = ? AND type = 'assistant' AND seq > ? ORDER BY seq`).all(sessionId, seq)
|
|
624
|
+
for (const [i, message] of newer.entries()) {
|
|
625
|
+
const data = parse(message.data)
|
|
626
|
+
if (i === newer.length - 1 && !data.time?.completed) break
|
|
627
|
+
if (number(data.tokens?.output) > 0) samplePoint(sample, windowOf(data.tokens))
|
|
628
|
+
seq = number(message.seq)
|
|
629
|
+
count++
|
|
630
|
+
}
|
|
631
|
+
return { contextSample: sample, contextSeq: seq, contextCount: count }
|
|
632
|
+
}
|
|
633
|
+
|
|
608
634
|
function currentTurn(db, row, parts, context) {
|
|
609
|
-
const
|
|
635
|
+
const before = context.facts.get(`v2:${row.id}`)?.value
|
|
636
|
+
const turn = { busy: false, failed: false, prompts: 0, firstPrompt: '', model: '', provider: '', effort: '', tokens: null, cost: null, context: 0, tool: null, plan: null, turnStartedAt: 0, ...currentSample(db, row.id, before) }
|
|
610
637
|
turn.prompts = number(db.prepare(`SELECT COUNT(*) AS n FROM session_message WHERE session_id = ? AND type = 'user'`).get(row.id)?.n)
|
|
611
638
|
const lastUser = db.prepare(`SELECT time_created FROM session_message WHERE session_id = ? AND type = 'user' ORDER BY seq DESC LIMIT 1`).get(row.id)
|
|
612
639
|
turn.turnStartedAt = number(row.time_busy) || number(lastUser?.time_created)
|
package/lib/telemetry-fields.mjs
CHANGED
|
@@ -316,3 +316,37 @@ export function nextPlan(plan, tool, input) {
|
|
|
316
316
|
}
|
|
317
317
|
return next
|
|
318
318
|
}
|
|
319
|
+
|
|
320
|
+
export const CONTEXT_POINTS = 24
|
|
321
|
+
|
|
322
|
+
export const emptySample = () => ({ stride: 1, seen: 0, points: [], newest: null, newestKept: false })
|
|
323
|
+
|
|
324
|
+
export function samplePoint(sample, value, { update = false } = {}) {
|
|
325
|
+
const point = Math.round(number(value))
|
|
326
|
+
if (point <= 0) return
|
|
327
|
+
if (update && sample.newest != null) {
|
|
328
|
+
if (sample.newestKept) sample.points[sample.points.length - 1] = point
|
|
329
|
+
sample.newest = point
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
sample.newestKept = sample.seen % sample.stride === 0
|
|
333
|
+
if (sample.newestKept) {
|
|
334
|
+
sample.points.push(point)
|
|
335
|
+
if (sample.points.length >= CONTEXT_POINTS * 2) {
|
|
336
|
+
sample.points = sample.points.filter((_, i) => i % 2 === 0)
|
|
337
|
+
sample.stride *= 2
|
|
338
|
+
sample.newestKept = false
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
sample.seen++
|
|
342
|
+
sample.newest = point
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export function sampleList(sample, current) {
|
|
346
|
+
if (!sample || !(current > 0)) return null
|
|
347
|
+
const now = Math.round(current)
|
|
348
|
+
const older = sample.newestKept && sample.newest === now ? sample.points.slice(0, -1) : sample.points
|
|
349
|
+
const room = CONTEXT_POINTS - 1
|
|
350
|
+
const picked = older.length <= room ? older : Array.from({ length: room }, (_, i) => older[Math.round((i * (older.length - 1)) / (room - 1))])
|
|
351
|
+
return [...picked, now]
|
|
352
|
+
}
|
package/package.json
CHANGED
package/telemetry/README.md
CHANGED
|
@@ -59,6 +59,7 @@ adapter in `../harnesses/`, so a session has the same id in both apps. Add the h
|
|
|
59
59
|
| `tool` | `{ name, startedAt, target, kind }` or `null` | The tool running right now, only while working or delegating. `target` is its short file path, command, or search, as in `Detail.activity`, or `''` when the harness doesn't record one. `kind` is one of the [tool kinds](#tool-kinds) |
|
|
60
60
|
| `tokens` | `{ input, output, cacheRead, cacheWrite }` or `null` | Totals for the session's own thread. `input` leaves out cache reads and writes |
|
|
61
61
|
| `context` | `{ used, limit }` or `null` | Tokens in the window after the last reply; `limit` is `0` when unknown |
|
|
62
|
+
| `contextHistory` | number[] or `null` | The context after each reply, oldest first, for a sparkline: at most 24 whole numbers, evenly spaced across the session and always ending at `context.used`. A reply written twice counts once. Each reader keeps a running sample as it reads, never the whole history; OpenCode 2.x reads only the replies newer than the last one it saw, once the session changes, leaves the newest for the next read while it has no `time.completed`, and starts over if replies were removed. `null` for Cursor, Antigravity, and Hermes, whose session scan sees no per-reply counts, and before the first reply |
|
|
62
63
|
| `costUsd` | number or `null` | Spend as the harness records it; `null` when it doesn't |
|
|
63
64
|
| `lines` | `{ added, removed }` or `null` | Lines changed, as the harness records it |
|
|
64
65
|
| `plan` | `{ total, done, current }` or `null` | Plan progress, if the agent keeps one |
|
|
@@ -6,7 +6,7 @@ import { awaitedPeers, readLiveSessions, transcriptDetail, transcriptReader, tur
|
|
|
6
6
|
import { exists, listDirs, listFiles, num } from '../lib/fsutil.mjs'
|
|
7
7
|
import { contextBreakdown } from '../lib/context-breakdown.mjs'
|
|
8
8
|
import { recentReaders } from '../lib/jsonl-follower.mjs'
|
|
9
|
-
import { DETAIL_LIMITS, RECENT_MS, askOf, number, planSummary, text, titleFromPrompt, toolNow } from '../lib/telemetry-fields.mjs'
|
|
9
|
+
import { DETAIL_LIMITS, RECENT_MS, askOf, number, planSummary, sampleList, text, titleFromPrompt, toolNow } from '../lib/telemetry-fields.mjs'
|
|
10
10
|
|
|
11
11
|
export { nextPlan } from '../lib/telemetry-fields.mjs'
|
|
12
12
|
|
|
@@ -388,6 +388,7 @@ function toSession({ id, state, stat, running, subagents, workingSubagents, awai
|
|
|
388
388
|
tool: status === 'working' || status === 'delegating' ? currentTool(state.open) : null,
|
|
389
389
|
tokens: { ...state.tokens },
|
|
390
390
|
context: state.context ? { used: state.context, limit } : null,
|
|
391
|
+
contextHistory: sampleList(state.contextSample, state.context),
|
|
391
392
|
costUsd: state.unpriced || totals.unpriced ? (state.cost ? state.cost.usd : null) : state.spend + totals.spend,
|
|
392
393
|
lines: { added: state.lines.added + totals.added, removed: state.lines.removed + totals.removed },
|
|
393
394
|
plan: planSummary(state.plan),
|
package/telemetry/codex.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
clipBlock,
|
|
17
17
|
diffHunk,
|
|
18
18
|
emptyActivity,
|
|
19
|
+
emptySample,
|
|
19
20
|
endCall,
|
|
20
21
|
fileList,
|
|
21
22
|
inputKind,
|
|
@@ -27,6 +28,8 @@ import {
|
|
|
27
28
|
planItems,
|
|
28
29
|
planSummary,
|
|
29
30
|
relativePath,
|
|
31
|
+
sampleList,
|
|
32
|
+
samplePoint,
|
|
30
33
|
settleCalls,
|
|
31
34
|
startCall,
|
|
32
35
|
text,
|
|
@@ -147,6 +150,7 @@ function emptyRollout() {
|
|
|
147
150
|
lines: { added: 0, removed: 0 },
|
|
148
151
|
tokens: null,
|
|
149
152
|
context: null,
|
|
153
|
+
contextSample: emptySample(),
|
|
150
154
|
plan: null,
|
|
151
155
|
history: [],
|
|
152
156
|
contextParts: null,
|
|
@@ -451,6 +455,7 @@ function applyTokens(state, info, at) {
|
|
|
451
455
|
const sum = number(total?.total_tokens)
|
|
452
456
|
if (sum === state.lastTotal) return
|
|
453
457
|
state.lastTotal = sum
|
|
458
|
+
samplePoint(state.contextSample, last.total_tokens)
|
|
454
459
|
keepNewest(state.history, { at, context: number(last.total_tokens), output: number(last.output_tokens) }, DETAIL_LIMITS.history)
|
|
455
460
|
}
|
|
456
461
|
|
|
@@ -668,6 +673,7 @@ function toSession({ id, state, names, live, turnLive, fresh, locking, unread, s
|
|
|
668
673
|
tool,
|
|
669
674
|
tokens: state.tokens ? { ...state.tokens } : null,
|
|
670
675
|
context: state.context && state.context.used ? { ...state.context } : null,
|
|
676
|
+
contextHistory: sampleList(state.contextSample, state.context?.used),
|
|
671
677
|
costUsd: null,
|
|
672
678
|
lines: { ...state.lines },
|
|
673
679
|
plan: planSummary(state.plan),
|
package/telemetry/cursor.mjs
CHANGED
|
@@ -273,6 +273,7 @@ export function cursor({
|
|
|
273
273
|
tool: open && state.tool ? { ...toolNow(state.tool), target: clip(relativePath(state.tool.target, projectPath)) } : null,
|
|
274
274
|
tokens: null,
|
|
275
275
|
context: null,
|
|
276
|
+
contextHistory: null,
|
|
276
277
|
costUsd: null,
|
|
277
278
|
lines: null,
|
|
278
279
|
plan: planSummary(state.plan),
|
package/telemetry/hermes.mjs
CHANGED
|
@@ -423,6 +423,7 @@ function toSession({ db, profile, tip, root, chain, ids, open, errands, workingS
|
|
|
423
423
|
? { input: tokens[0] || 0, output: tokens[1] || 0, cacheRead: tokens[2] || 0, cacheWrite: tokens[3] || 0 }
|
|
424
424
|
: null,
|
|
425
425
|
context: used ? { used, limit: contextLimit(lengths, text(tip.model), tip.billing_base_url) } : null,
|
|
426
|
+
contextHistory: null,
|
|
426
427
|
costUsd: costs.some((c) => c != null) ? costs.reduce((total, c) => total + number(c), 0) : null,
|
|
427
428
|
lines: null,
|
|
428
429
|
plan: planSummary(latestPlan(db, ids, messages)),
|