@threadcrumbs/common 0.1.2 → 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/README.md +45 -12
- package/lib/claude-transcript.mjs +108 -5
- package/lib/context-breakdown.mjs +149 -0
- package/lib/jsonl-follower.mjs +11 -0
- package/lib/opencode-store.mjs +130 -37
- package/lib/telemetry-fields.mjs +109 -2
- package/lib/tool-kinds.mjs +256 -0
- package/package.json +1 -1
- package/telemetry/HARNESS-NOTES.md +246 -0
- package/telemetry/README.md +137 -11
- package/telemetry/antigravity.mjs +29 -4
- package/telemetry/claude-code.mjs +59 -23
- package/telemetry/codex.mjs +162 -28
- package/telemetry/cursor.mjs +45 -13
- package/telemetry/hermes.mjs +54 -18
package/lib/opencode-store.mjs
CHANGED
|
@@ -6,21 +6,30 @@ import {
|
|
|
6
6
|
RECENT_MS,
|
|
7
7
|
activityDetail,
|
|
8
8
|
addFileChange,
|
|
9
|
+
askOf,
|
|
9
10
|
clip,
|
|
10
11
|
clipBlock,
|
|
11
12
|
emptyActivity,
|
|
13
|
+
emptySample,
|
|
12
14
|
endCall,
|
|
13
15
|
fileList,
|
|
16
|
+
joinedText,
|
|
14
17
|
keepNewest,
|
|
18
|
+
keepText,
|
|
15
19
|
number,
|
|
20
|
+
openTool,
|
|
16
21
|
planItems,
|
|
17
22
|
planSummary,
|
|
23
|
+
sampleList,
|
|
24
|
+
samplePoint,
|
|
18
25
|
settleCalls,
|
|
19
26
|
startCall,
|
|
20
27
|
text,
|
|
28
|
+
textList,
|
|
21
29
|
titleFromPrompt,
|
|
22
30
|
toolFile,
|
|
23
31
|
toolInput,
|
|
32
|
+
toolNow,
|
|
24
33
|
toolTarget,
|
|
25
34
|
} from './telemetry-fields.mjs'
|
|
26
35
|
|
|
@@ -185,12 +194,12 @@ function childList(rows, turnOf) {
|
|
|
185
194
|
const turn = turnOf(r)
|
|
186
195
|
return {
|
|
187
196
|
id: r.id,
|
|
188
|
-
name:
|
|
197
|
+
name: childName(r, turn),
|
|
189
198
|
type: '',
|
|
190
199
|
status: turn.open ? 'working' : turn.failed ? 'error' : 'done',
|
|
191
200
|
model: turn.model,
|
|
192
201
|
tokens: turn.tokens ? { ...turn.tokens } : null,
|
|
193
|
-
tool: turn.open && turn.tool ?
|
|
202
|
+
tool: turn.open && turn.tool ? toolNow(turn.tool) : null,
|
|
194
203
|
startedAt: number(r.time_created),
|
|
195
204
|
lastActivityAt: number(r.time_updated),
|
|
196
205
|
}
|
|
@@ -215,6 +224,7 @@ function legacyDetail(db, tables, row, context) {
|
|
|
215
224
|
.map((r) => r.message_id),
|
|
216
225
|
)
|
|
217
226
|
const modelOf = new Map()
|
|
227
|
+
const messageAt = new Map()
|
|
218
228
|
const messageUsage = []
|
|
219
229
|
for (const message of db.prepare('SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created, rowid').all(row.id)) {
|
|
220
230
|
const data = parse(message.data)
|
|
@@ -232,6 +242,7 @@ function legacyDetail(db, tables, row, context) {
|
|
|
232
242
|
if (data.role !== 'assistant') continue
|
|
233
243
|
const model = text(data.modelID)
|
|
234
244
|
modelOf.set(message.id, model)
|
|
245
|
+
messageAt.set(message.id, at)
|
|
235
246
|
messageUsage.push([model, data.tokens, data.cost])
|
|
236
247
|
if (number(data.tokens?.output) > 0) {
|
|
237
248
|
const output = number(data.tokens.output) + number(data.tokens.reasoning)
|
|
@@ -266,6 +277,17 @@ function legacyDetail(db, tables, row, context) {
|
|
|
266
277
|
endCall(activity, callId, number(state.time?.end), failed, text(state.error) || text(state.output))
|
|
267
278
|
}
|
|
268
279
|
}
|
|
280
|
+
const thoughts = []
|
|
281
|
+
const replies = []
|
|
282
|
+
const said = db.prepare(`SELECT message_id, data FROM part WHERE session_id = ? AND (data LIKE '%"type":"text"%' OR data LIKE '%"type":"reasoning"%') ORDER BY rowid`)
|
|
283
|
+
for (const part of said.all(row.id)) {
|
|
284
|
+
if (!messageAt.has(part.message_id)) continue
|
|
285
|
+
const data = parse(part.data)
|
|
286
|
+
if (data.synthetic || data.ignored) continue
|
|
287
|
+
const at = number(data.time?.end) || number(data.time?.start) || messageAt.get(part.message_id)
|
|
288
|
+
if (data.type === 'reasoning') keepText(thoughts, 'thoughts', { id: part.message_id, at, text: text(data.text) })
|
|
289
|
+
else if (data.type === 'text') keepText(replies, 'replies', { id: part.message_id, at, text: text(data.text) })
|
|
290
|
+
}
|
|
269
291
|
const turnOf = (r) => cached(context, `v1:${r.id}`, number(r.time_updated), () => legacyTurn(db, r, context))
|
|
270
292
|
if (!turnOf(row).open) settleCalls(activity)
|
|
271
293
|
|
|
@@ -284,6 +306,9 @@ function legacyDetail(db, tables, row, context) {
|
|
|
284
306
|
subagents: childList(sessionRows(db, 'session', 'parent_id', row.id), turnOf),
|
|
285
307
|
files: files.size || !context.liveSummary ? fileList(files, directory) : null,
|
|
286
308
|
prompts,
|
|
309
|
+
thoughts: textList(thoughts),
|
|
310
|
+
replies: textList(replies),
|
|
311
|
+
contextBreakdown: null,
|
|
287
312
|
errors,
|
|
288
313
|
}
|
|
289
314
|
}
|
|
@@ -296,13 +321,15 @@ function currentDetail(db, tables, row, context) {
|
|
|
296
321
|
const errors = []
|
|
297
322
|
const models = new Map()
|
|
298
323
|
let contextParts = null
|
|
299
|
-
|
|
324
|
+
const replied = []
|
|
325
|
+
for (const message of db.prepare('SELECT id, type, time_created, data FROM session_message WHERE session_id = ? ORDER BY seq').all(row.id)) {
|
|
300
326
|
const data = parse(message.data)
|
|
301
327
|
const at = number(message.time_created)
|
|
302
328
|
if (message.type === 'user') {
|
|
303
329
|
const prompt = text(data.text).trim()
|
|
304
330
|
if (prompt) keepNewest(prompts, { at, text: clipBlock(prompt, DETAIL_LIMITS.promptText) }, DETAIL_LIMITS.prompts)
|
|
305
331
|
} else if (message.type === 'assistant') {
|
|
332
|
+
replied.push({ id: message.id, at, content: list(data.content) })
|
|
306
333
|
addModelUsage(models, text(data.model?.id), data.tokens, data.cost)
|
|
307
334
|
if (number(data.tokens?.output) > 0) {
|
|
308
335
|
keepNewest(history, { at, context: windowOf(data.tokens), output: number(data.tokens.output) + number(data.tokens.reasoning) }, DETAIL_LIMITS.history)
|
|
@@ -314,21 +341,34 @@ function currentDetail(db, tables, row, context) {
|
|
|
314
341
|
}
|
|
315
342
|
|
|
316
343
|
const parts = tables.has('session_part') && ['session_id', 'message_id', 'type', 'data'].every((c) => columnsOf(db, 'session_part').has(c))
|
|
317
|
-
|
|
344
|
+
const rows = new Map()
|
|
318
345
|
if (parts) {
|
|
319
|
-
for (const part of db.prepare(`SELECT id, data FROM session_part WHERE session_id = ? AND type
|
|
346
|
+
for (const part of db.prepare(`SELECT id, message_id, type, data FROM session_part WHERE session_id = ? AND type IN ('tool', 'reasoning', 'text') ORDER BY rowid`).all(row.id)) {
|
|
320
347
|
const data = parse(part.data)
|
|
348
|
+
if (!rows.has(part.message_id)) rows.set(part.message_id, [])
|
|
349
|
+
rows.get(part.message_id).push({ ...data, type: text(data.type) || part.type, rowId: part.id })
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
let plan = []
|
|
353
|
+
const thoughts = []
|
|
354
|
+
const replies = []
|
|
355
|
+
for (const message of replied) {
|
|
356
|
+
const content = rows.get(message.id) || message.content
|
|
357
|
+
content.forEach((data, i) => {
|
|
358
|
+
if (data?.type !== 'tool') return
|
|
321
359
|
const state = data.state || {}
|
|
322
|
-
const callId = text(data.callID) || text(data.id) ||
|
|
360
|
+
const callId = text(data.callID) || text(data.id) || data.rowId || `${message.id}:${i}`
|
|
323
361
|
const call = { id: callId, name: text(data.name), target: toolTarget(state.input, directory), input: toolInput(state.input), ...toolFile(state.input, directory) }
|
|
324
|
-
if (!startCall(activity, { ...call, startedAt: number(data.time?.ran) || number(data.time?.created) }))
|
|
362
|
+
if (!startCall(activity, { ...call, startedAt: number(data.time?.ran) || number(data.time?.created) || message.at })) return
|
|
325
363
|
if (state.status === 'completed' || state.status === 'error') {
|
|
326
364
|
endCall(activity, callId, number(state.time?.end) || number(data.time?.completed), state.status === 'error', text(state.error) || text(state.output))
|
|
327
365
|
}
|
|
328
366
|
if (data.name === TODO_TOOL && state.status === 'completed' && Array.isArray(state.input?.todos)) {
|
|
329
367
|
plan = state.input.todos.map((t) => ({ content: text(t?.content), status: text(t?.status) }))
|
|
330
368
|
}
|
|
331
|
-
}
|
|
369
|
+
})
|
|
370
|
+
keepText(thoughts, 'thoughts', { at: message.at, text: joinedText(content, 'reasoning', 'text') })
|
|
371
|
+
keepText(replies, 'replies', { at: message.at, text: joinedText(content, 'text') })
|
|
332
372
|
}
|
|
333
373
|
const turnOf = (r) => cached(context, `v2:${r.id}`, number(r.time_updated), () => currentTurn(db, r, parts, context))
|
|
334
374
|
if (!turnOf(row).open) settleCalls(activity)
|
|
@@ -343,6 +383,9 @@ function currentDetail(db, tables, row, context) {
|
|
|
343
383
|
subagents: childList(sessionRows(db, 'session_v2', 'parent_id', row.id), turnOf),
|
|
344
384
|
files: null,
|
|
345
385
|
prompts,
|
|
386
|
+
thoughts: textList(thoughts),
|
|
387
|
+
replies: textList(replies),
|
|
388
|
+
contextBreakdown: null,
|
|
346
389
|
errors,
|
|
347
390
|
}
|
|
348
391
|
}
|
|
@@ -373,14 +416,23 @@ function cached(context, key, updated, compute) {
|
|
|
373
416
|
|
|
374
417
|
function statusOf({ open, failed, errands, tool, lastActivityAt, read, at }) {
|
|
375
418
|
if (open) {
|
|
376
|
-
if (tool?.name === QUESTION_TOOL) return 'asking'
|
|
377
|
-
if (errands && (!tool || SUBAGENT_TOOLS.has(tool.name))) return 'delegating'
|
|
378
|
-
return 'working'
|
|
379
|
-
}
|
|
380
|
-
if (errands) return 'delegating'
|
|
381
|
-
if (failed) return 'error'
|
|
382
|
-
if (read) return read.unread ? 'done' : 'idle'
|
|
383
|
-
return at - lastActivityAt < ACTIVE_WINDOW_MS ? 'done' : 'idle'
|
|
419
|
+
if (tool?.name === QUESTION_TOOL) return ['asking', 'question']
|
|
420
|
+
if (errands && (!tool || SUBAGENT_TOOLS.has(tool.name))) return ['delegating', 'subagents']
|
|
421
|
+
return ['working', 'turn-open']
|
|
422
|
+
}
|
|
423
|
+
if (errands) return ['delegating', 'subagents']
|
|
424
|
+
if (failed) return ['error', 'turn-failed']
|
|
425
|
+
if (read) return read.unread ? ['done', 'unread'] : ['idle', 'read']
|
|
426
|
+
return at - lastActivityAt < ACTIVE_WINDOW_MS ? ['done', 'recent'] : ['idle', 'quiet']
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const childName = (row, turn) => text(row.title).trim() || (turn.firstPrompt ? titleFromPrompt(turn.firstPrompt) : '') || 'Subagent'
|
|
430
|
+
|
|
431
|
+
function workingChildren(rows, parent, turnOf, context) {
|
|
432
|
+
return rows
|
|
433
|
+
.filter((r) => r.parent_id === parent.id && context.at - number(r.time_updated) < SUBAGENT_WINDOW_MS && turnOf(r).open)
|
|
434
|
+
.sort((a, b) => number(a.time_created) - number(b.time_created))
|
|
435
|
+
.map((r) => ({ id: r.id, name: childName(r, turnOf(r)) }))
|
|
384
436
|
}
|
|
385
437
|
|
|
386
438
|
function session(context, row, turn, extra) {
|
|
@@ -389,7 +441,8 @@ function session(context, row, turn, extra) {
|
|
|
389
441
|
const lastActivityAt = number(row.time_updated)
|
|
390
442
|
const viewed = number(row.time_viewed)
|
|
391
443
|
const read = viewed > 0 ? { unread: number(row.time_idle) > viewed } : null
|
|
392
|
-
const
|
|
444
|
+
const errands = extra.working.length
|
|
445
|
+
const [status, statusReason] = statusOf({ ...turn, errands, lastActivityAt, read, at: context.at })
|
|
393
446
|
return {
|
|
394
447
|
id: `${context.id}:${row.id}`,
|
|
395
448
|
harness: context.id,
|
|
@@ -405,14 +458,20 @@ function session(context, row, turn, extra) {
|
|
|
405
458
|
live: turn.open,
|
|
406
459
|
startedAt: number(row.time_created),
|
|
407
460
|
lastActivityAt,
|
|
408
|
-
tool: turn.open && turn.tool ?
|
|
461
|
+
tool: turn.open && turn.tool ? toolNow(turn.tool) : null,
|
|
409
462
|
tokens: turn.tokens ? { ...turn.tokens } : null,
|
|
410
463
|
context: turn.context ? { used: turn.context, limit: context.byModel.get(`${turn.provider}/${turn.model}`) || 0 } : null,
|
|
464
|
+
contextHistory: sampleList(turn.contextSample, turn.context),
|
|
411
465
|
costUsd: turn.cost,
|
|
412
466
|
lines: extra.lines,
|
|
413
467
|
plan: planSummary(extra.plan),
|
|
414
|
-
subagents:
|
|
468
|
+
subagents: errands,
|
|
469
|
+
workingSubagents: extra.working,
|
|
415
470
|
prompts: turn.prompts,
|
|
471
|
+
turnStartedAt: turn.open ? turn.turnStartedAt || null : null,
|
|
472
|
+
statusReason,
|
|
473
|
+
ask: askOf(status, { calls: turn.open && turn.tool ? [turn.tool] : [], errorText: turn.errorText }),
|
|
474
|
+
usageLimits: null,
|
|
416
475
|
}
|
|
417
476
|
}
|
|
418
477
|
|
|
@@ -431,7 +490,7 @@ function legacySessions(db, tables, context) {
|
|
|
431
490
|
for (const row of rows) {
|
|
432
491
|
if (row.parent_id || row.time_archived != null) continue
|
|
433
492
|
const turn = turnOf(row)
|
|
434
|
-
const
|
|
493
|
+
const working = workingChildren(rows, row, turnOf, context)
|
|
435
494
|
const lines = context.liveSummary
|
|
436
495
|
? row.summary_additions == null && row.summary_deletions == null
|
|
437
496
|
? null
|
|
@@ -443,13 +502,13 @@ function legacySessions(db, tables, context) {
|
|
|
443
502
|
.all(row.id)
|
|
444
503
|
.map((t) => ({ content: text(t.content), status: text(t.status) }))
|
|
445
504
|
: null
|
|
446
|
-
out.push(session(context, row, turn, {
|
|
505
|
+
out.push(session(context, row, turn, { working, lines, plan }))
|
|
447
506
|
}
|
|
448
507
|
return out
|
|
449
508
|
}
|
|
450
509
|
|
|
451
510
|
function legacyTurn(db, row, context) {
|
|
452
|
-
const turn = { busy: false, failed: false, prompts: 0, firstPrompt: '', model: '', provider: '', effort: '', tokens: null, cost: null, context: 0, lines: null, tool: null }
|
|
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 }
|
|
453
512
|
const typed = new Set(
|
|
454
513
|
db
|
|
455
514
|
.prepare(`SELECT message_id FROM part WHERE session_id = ? AND data LIKE '%"type":"text"%' AND data NOT LIKE '%"synthetic":true%'`)
|
|
@@ -458,10 +517,11 @@ function legacyTurn(db, row, context) {
|
|
|
458
517
|
)
|
|
459
518
|
let last = null
|
|
460
519
|
let lastAssistant = ''
|
|
461
|
-
for (const message of db.prepare('SELECT id, data FROM message WHERE session_id = ? ORDER BY time_created, rowid').all(row.id)) {
|
|
520
|
+
for (const message of db.prepare('SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created, rowid').all(row.id)) {
|
|
462
521
|
const data = parse(message.data)
|
|
463
522
|
last = data
|
|
464
523
|
if (data.role === 'user') {
|
|
524
|
+
turn.turnStartedAt = number(data.time?.created) || number(message.time_created)
|
|
465
525
|
if (typed.has(message.id)) {
|
|
466
526
|
turn.prompts++
|
|
467
527
|
if (!turn.firstPrompt) turn.firstPrompt = firstText(db, message.id)
|
|
@@ -481,7 +541,10 @@ function legacyTurn(db, row, context) {
|
|
|
481
541
|
if (data.providerID) turn.provider = text(data.providerID)
|
|
482
542
|
if (data.variant) turn.effort = text(data.variant)
|
|
483
543
|
if (data.cost != null) turn.cost = (turn.cost || 0) + number(data.cost)
|
|
484
|
-
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
|
+
}
|
|
485
548
|
}
|
|
486
549
|
const usage = usageFromColumns(row)
|
|
487
550
|
if (usage) {
|
|
@@ -496,8 +559,10 @@ function legacyTurn(db, row, context) {
|
|
|
496
559
|
const finish = text(last?.finish)
|
|
497
560
|
const ended = Boolean(last?.time?.completed) || Boolean(finish)
|
|
498
561
|
turn.busy = last?.role === 'user' || (last?.role === 'assistant' && !error && (!ended || context.continuing.has(finish)))
|
|
499
|
-
|
|
500
|
-
|
|
562
|
+
const failedTool = last?.role === 'assistant' && !error && ended ? toolFailure(db, lastAssistant) : ''
|
|
563
|
+
turn.failed = last?.role === 'assistant' && (error ? !ABORTED.test(error) : Boolean(failedTool))
|
|
564
|
+
turn.errorText = turn.failed ? text(last?.error?.data?.message) || error || failedTool : ''
|
|
565
|
+
if (turn.busy) turn.tool = runningTool(db, lastAssistant, text(row.directory))
|
|
501
566
|
return turn
|
|
502
567
|
}
|
|
503
568
|
|
|
@@ -509,21 +574,21 @@ function firstText(db, messageId) {
|
|
|
509
574
|
return ''
|
|
510
575
|
}
|
|
511
576
|
|
|
512
|
-
function
|
|
577
|
+
function toolFailure(db, messageId) {
|
|
513
578
|
for (const part of db.prepare(`SELECT data FROM part WHERE message_id = ? AND data LIKE '%"status":"error"%'`).all(messageId)) {
|
|
514
579
|
const state = parse(part.data).state || {}
|
|
515
|
-
if (!STOPPED.test(`${text(state.error)}\n${text(state.output)}`)) return
|
|
580
|
+
if (!STOPPED.test(`${text(state.error)}\n${text(state.output)}`)) return text(state.error) || text(state.output) || 'A tool failed'
|
|
516
581
|
}
|
|
517
|
-
return
|
|
582
|
+
return ''
|
|
518
583
|
}
|
|
519
584
|
|
|
520
|
-
function runningTool(db, messageId) {
|
|
585
|
+
function runningTool(db, messageId, cwd) {
|
|
521
586
|
if (!messageId) return null
|
|
522
587
|
let tool = null
|
|
523
588
|
for (const part of db.prepare('SELECT data FROM part WHERE message_id = ? ORDER BY rowid').all(messageId)) {
|
|
524
589
|
const data = parse(part.data)
|
|
525
590
|
if (data.type !== 'tool' || !OPEN_TOOL.has(data.state?.status)) continue
|
|
526
|
-
tool =
|
|
591
|
+
tool = openTool(text(data.tool), number(data.state?.time?.start), data.state?.input, cwd)
|
|
527
592
|
}
|
|
528
593
|
return tool
|
|
529
594
|
}
|
|
@@ -542,15 +607,36 @@ function currentSessions(db, tables, context) {
|
|
|
542
607
|
for (const row of rows) {
|
|
543
608
|
if (row.parent_id || row.time_archived != null) continue
|
|
544
609
|
const turn = turnOf(row)
|
|
545
|
-
|
|
546
|
-
out.push(session(context, row, turn, { errands, lines: null, plan: turn.plan }))
|
|
610
|
+
out.push(session(context, row, turn, { working: workingChildren(rows, row, turnOf, context), lines: null, plan: turn.plan }))
|
|
547
611
|
}
|
|
548
612
|
return out
|
|
549
613
|
}
|
|
550
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
|
+
|
|
551
634
|
function currentTurn(db, row, parts, context) {
|
|
552
|
-
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) }
|
|
553
637
|
turn.prompts = number(db.prepare(`SELECT COUNT(*) AS n FROM session_message WHERE session_id = ? AND type = 'user'`).get(row.id)?.n)
|
|
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)
|
|
639
|
+
turn.turnStartedAt = number(row.time_busy) || number(lastUser?.time_created)
|
|
554
640
|
turn.firstPrompt = text(parse(db.prepare(`SELECT data FROM session_message WHERE session_id = ? AND type = 'user' ORDER BY seq ASC LIMIT 1`).get(row.id)?.data).text).trim()
|
|
555
641
|
const assistant = db.prepare(`SELECT id, data FROM session_message WHERE session_id = ? AND type = 'assistant' ORDER BY seq DESC LIMIT 1`).get(row.id)
|
|
556
642
|
const latest = parse(assistant?.data)
|
|
@@ -573,13 +659,14 @@ function currentTurn(db, row, parts, context) {
|
|
|
573
659
|
turn.busy = busy
|
|
574
660
|
const outcome = text(row.idle_outcome) || (last?.type === 'idle' ? text(parse(last.data).outcome) : '')
|
|
575
661
|
turn.failed = !busy && outcome === 'failed'
|
|
576
|
-
|
|
662
|
+
turn.errorText = turn.failed && last?.type === 'idle' ? text(parse(last.data).error?.message) : ''
|
|
663
|
+
const tools = currentTools(db, row.id, assistant, parts, text(row.directory))
|
|
577
664
|
if (busy) turn.tool = tools.running
|
|
578
665
|
turn.plan = tools.todos
|
|
579
666
|
return turn
|
|
580
667
|
}
|
|
581
668
|
|
|
582
|
-
function currentTools(db, sessionId, assistant, parts) {
|
|
669
|
+
function currentTools(db, sessionId, assistant, parts, cwd) {
|
|
583
670
|
const result = { running: null, todos: null }
|
|
584
671
|
let latest = []
|
|
585
672
|
let recent = []
|
|
@@ -593,8 +680,14 @@ function currentTools(db, sessionId, assistant, parts) {
|
|
|
593
680
|
.map((p) => parse(p.data))
|
|
594
681
|
}
|
|
595
682
|
if (!latest.length && assistant) latest = list(parse(assistant.data).content).filter((c) => c?.type === 'tool')
|
|
683
|
+
if (!recent.length) {
|
|
684
|
+
recent = db
|
|
685
|
+
.prepare(`SELECT data FROM session_message WHERE session_id = ? AND type = 'assistant' AND data LIKE ? ORDER BY seq DESC LIMIT 20`)
|
|
686
|
+
.all(sessionId, `%"${TODO_TOOL}"%`)
|
|
687
|
+
.flatMap((m) => list(parse(m.data).content).filter((c) => c?.type === 'tool').reverse())
|
|
688
|
+
}
|
|
596
689
|
for (const part of latest) {
|
|
597
|
-
if (OPEN_TOOL.has(part.state?.status)) result.running =
|
|
690
|
+
if (OPEN_TOOL.has(part.state?.status)) result.running = openTool(text(part.name), number(part.time?.ran) || number(part.time?.created), part.state?.input, cwd)
|
|
598
691
|
}
|
|
599
692
|
const todos = recent.find((p) => p.name === TODO_TOOL && p.state?.status === 'completed' && Array.isArray(p.state?.input?.todos))
|
|
600
693
|
if (todos) result.todos = todos.state.input.todos.map((t) => ({ content: text(t?.content), status: text(t?.status) }))
|
package/lib/telemetry-fields.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { nameKind, toolKind } from './tool-kinds.mjs'
|
|
2
|
+
|
|
1
3
|
export const RECENT_MS = 24 * 60 * 60 * 1000
|
|
2
4
|
|
|
3
5
|
export const DETAIL_LIMITS = Object.freeze({
|
|
@@ -5,6 +7,10 @@ export const DETAIL_LIMITS = Object.freeze({
|
|
|
5
7
|
activity: 100,
|
|
6
8
|
prompts: 50,
|
|
7
9
|
promptText: 2000,
|
|
10
|
+
thoughts: 20,
|
|
11
|
+
thoughtText: 2000,
|
|
12
|
+
replies: 50,
|
|
13
|
+
replyText: 2000,
|
|
8
14
|
errors: 20,
|
|
9
15
|
errorText: 500,
|
|
10
16
|
input: 2000,
|
|
@@ -73,6 +79,8 @@ export function toolTarget(input, cwd = '', max = TARGET_MAX) {
|
|
|
73
79
|
return ''
|
|
74
80
|
}
|
|
75
81
|
|
|
82
|
+
export const inputKind = (name, input) => toolKind(name, typeof input === 'string' && input.endsWith('…') ? '' : input)
|
|
83
|
+
|
|
76
84
|
export function toolInput(input) {
|
|
77
85
|
if (typeof input === 'string') return clipBlock(input, DETAIL_LIMITS.input) || null
|
|
78
86
|
if (!input || typeof input !== 'object') return null
|
|
@@ -92,12 +100,76 @@ export function toolFile(input, cwd = '') {
|
|
|
92
100
|
return { path: null, line: null }
|
|
93
101
|
}
|
|
94
102
|
|
|
103
|
+
const ASK_QUESTIONS = 4
|
|
104
|
+
const ASK_OPTIONS = 8
|
|
105
|
+
const askText = (value) => clipBlock(text(value), DETAIL_LIMITS.errorText)
|
|
106
|
+
|
|
107
|
+
export function askQuestions(input) {
|
|
108
|
+
if (!Array.isArray(input?.questions) || !input.questions.length) return null
|
|
109
|
+
return input.questions.slice(0, ASK_QUESTIONS).map((q) => ({
|
|
110
|
+
header: clip(q?.header),
|
|
111
|
+
text: askText(q?.question ?? q?.text),
|
|
112
|
+
options: (Array.isArray(q?.options) ? q.options : []).slice(0, ASK_OPTIONS).map((o) => ({
|
|
113
|
+
label: clip(typeof o === 'string' ? o : o?.label),
|
|
114
|
+
description: askText(o?.description),
|
|
115
|
+
})),
|
|
116
|
+
multiple: q?.multiSelect === true || q?.multiple === true,
|
|
117
|
+
}))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function openTool(name, startedAt, input = null, cwd = '') {
|
|
121
|
+
const command = typeof input === 'string' ? input : commandText(input?.command ?? input?.cmd)
|
|
122
|
+
const tool = { name, startedAt, target: toolTarget(input, cwd), kind: toolKind(name, command || toolInput(input)).kind }
|
|
123
|
+
const questions = tool.kind === 'communicate' ? askQuestions(input) : null
|
|
124
|
+
if (questions) tool.questions = questions
|
|
125
|
+
return tool
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function askOf(status, { calls = [], errorText = '' } = {}) {
|
|
129
|
+
if (status === 'error') return { type: 'error', text: askText(errorText) }
|
|
130
|
+
if (status !== 'asking') return null
|
|
131
|
+
const asked = calls.findLast((c) => c?.questions)
|
|
132
|
+
if (asked) return { type: 'question', questions: asked.questions.map((q) => ({ ...q, options: q.options.map((o) => ({ ...o })) })) }
|
|
133
|
+
const call = calls.findLast((c) => (c.kind ?? nameKind(c.name)) !== 'agent')
|
|
134
|
+
if (call) return { type: 'approval', tool: call.name, target: call.target ?? '', kind: call.kind ?? nameKind(call.name) }
|
|
135
|
+
return { type: 'unknown' }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export const toolNow = (tool) => (tool ? { name: tool.name, startedAt: tool.startedAt, target: tool.target ?? '', kind: tool.kind ?? nameKind(tool.name) } : null)
|
|
139
|
+
|
|
95
140
|
export function keepNewest(list, item, max) {
|
|
96
141
|
list.push(item)
|
|
97
142
|
if (list.length > max) list.splice(0, list.length - max)
|
|
98
143
|
return item
|
|
99
144
|
}
|
|
100
145
|
|
|
146
|
+
const TEXT_LISTS = {
|
|
147
|
+
thoughts: [DETAIL_LIMITS.thoughts, DETAIL_LIMITS.thoughtText],
|
|
148
|
+
replies: [DETAIL_LIMITS.replies, DETAIL_LIMITS.replyText],
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function keepText(list, kind, { id = null, at, text: value }) {
|
|
152
|
+
const [max, maxText] = TEXT_LISTS[kind]
|
|
153
|
+
const body = clipBlock(value, maxText)
|
|
154
|
+
if (!body) return null
|
|
155
|
+
const last = list.at(-1)
|
|
156
|
+
if (id != null && last?.id === id) {
|
|
157
|
+
if (last.text.length < maxText) last.text = clipBlock(`${last.text}\n\n${body}`, maxText)
|
|
158
|
+
return last
|
|
159
|
+
}
|
|
160
|
+
return keepNewest(list, { id, at, text: body }, max)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const textList = (list) => (list ? list.map(({ at, text: value }) => ({ at, text: value })) : null)
|
|
164
|
+
|
|
165
|
+
export function joinedText(blocks, type, field = type) {
|
|
166
|
+
if (!Array.isArray(blocks)) return ''
|
|
167
|
+
return blocks
|
|
168
|
+
.filter((block) => block?.type === type && typeof block[field] === 'string' && block[field].trim())
|
|
169
|
+
.map((block) => block[field].trim())
|
|
170
|
+
.join('\n\n')
|
|
171
|
+
}
|
|
172
|
+
|
|
101
173
|
export function patchLines(lines) {
|
|
102
174
|
let added = 0
|
|
103
175
|
let removed = 0
|
|
@@ -151,12 +223,13 @@ export const emptyActivity = () => ({ calls: [], tools: new Map() })
|
|
|
151
223
|
|
|
152
224
|
export function startCall(activity, { id = '', name, target = '', input = null, path = null, line = null, startedAt, running = true }) {
|
|
153
225
|
if (id && activity.calls.some((c) => c.id === id)) return null
|
|
226
|
+
const { kind, server } = inputKind(name, input)
|
|
154
227
|
const call = keepNewest(
|
|
155
228
|
activity.calls,
|
|
156
|
-
{ id, name, target, input, path, line, startedAt, endedAt: null, running, error: null, errorText: null },
|
|
229
|
+
{ id, name, kind, server, target, input, path, line, startedAt, endedAt: null, running, error: null, errorText: null },
|
|
157
230
|
DETAIL_LIMITS.activity,
|
|
158
231
|
)
|
|
159
|
-
const tool = activity.tools.get(name) || { name, calls: 0, errors: 0, ms: 0 }
|
|
232
|
+
const tool = activity.tools.get(name) || { name, kind: nameKind(name), server, calls: 0, errors: 0, ms: 0 }
|
|
160
233
|
tool.calls++
|
|
161
234
|
activity.tools.set(name, tool)
|
|
162
235
|
return call
|
|
@@ -243,3 +316,37 @@ export function nextPlan(plan, tool, input) {
|
|
|
243
316
|
}
|
|
244
317
|
return next
|
|
245
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
|
+
}
|