@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/telemetry/codex.mjs
CHANGED
|
@@ -2,35 +2,49 @@ import fsp from 'node:fs/promises'
|
|
|
2
2
|
import os from 'node:os'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { exists, jsonLines, listDirs, listFiles } from '../lib/fsutil.mjs'
|
|
5
|
-
import {
|
|
5
|
+
import { contextBreakdown, emptyFixed, emptySizes, setFixed, sizeCall, sizeCompaction, sizePrompt, sizeReply, sizeRequest, sizeResult } from '../lib/context-breakdown.mjs'
|
|
6
|
+
import { jsonlFollower, recentReaders } from '../lib/jsonl-follower.mjs'
|
|
6
7
|
import { columnsOf, tablesOf, withReadOnly } from '../lib/sqlite.mjs'
|
|
7
8
|
import {
|
|
8
9
|
DETAIL_LIMITS,
|
|
9
10
|
RECENT_MS,
|
|
10
11
|
activityDetail,
|
|
11
12
|
addFileChange,
|
|
13
|
+
askOf,
|
|
14
|
+
askQuestions,
|
|
12
15
|
clip,
|
|
13
16
|
clipBlock,
|
|
14
17
|
diffHunk,
|
|
15
18
|
emptyActivity,
|
|
19
|
+
emptySample,
|
|
16
20
|
endCall,
|
|
17
21
|
fileList,
|
|
22
|
+
inputKind,
|
|
23
|
+
joinedText,
|
|
18
24
|
keepNewest,
|
|
25
|
+
keepText,
|
|
19
26
|
nextPlan,
|
|
20
27
|
number,
|
|
21
28
|
planItems,
|
|
22
29
|
planSummary,
|
|
23
30
|
relativePath,
|
|
31
|
+
sampleList,
|
|
32
|
+
samplePoint,
|
|
24
33
|
settleCalls,
|
|
25
34
|
startCall,
|
|
26
35
|
text,
|
|
36
|
+
textList,
|
|
27
37
|
titleFromPrompt,
|
|
28
38
|
toolInput,
|
|
39
|
+
toolNow,
|
|
29
40
|
toolTarget,
|
|
30
41
|
} from '../lib/telemetry-fields.mjs'
|
|
31
42
|
|
|
32
43
|
const ACTIVE_WINDOW_MS = 30 * 60 * 1000
|
|
33
44
|
const SUBAGENT_WINDOW_MS = 10 * 60 * 1000
|
|
45
|
+
const OPENED_KEPT = 3
|
|
46
|
+
const COMPACT_SHARE = 0.9
|
|
47
|
+
const REPORTED_SHARE = 0.95
|
|
34
48
|
const ROLLOUT_FILE = /^rollout-.{19}-([0-9a-f-]{36})(?:_[0-9a-f-]{36})?\.jsonl$/i
|
|
35
49
|
const STATE_DB = /^state_(\d+)\.sqlite$/
|
|
36
50
|
const CALLS = new Set(['function_call', 'custom_tool_call', 'local_shell_call', 'tool_search_call'])
|
|
@@ -105,9 +119,9 @@ function callInput(item, args, patch) {
|
|
|
105
119
|
function latestCall(calls) {
|
|
106
120
|
let latest = null
|
|
107
121
|
for (const call of calls.values()) {
|
|
108
|
-
if (!latest || call.startedAt >= latest.startedAt) latest =
|
|
122
|
+
if (!latest || call.startedAt >= latest.startedAt) latest = call
|
|
109
123
|
}
|
|
110
|
-
return latest
|
|
124
|
+
return toolNow(latest)
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
function emptyRollout() {
|
|
@@ -128,12 +142,15 @@ function emptyRollout() {
|
|
|
128
142
|
fallbackPrompt: '',
|
|
129
143
|
fallbackPrompts: 0,
|
|
130
144
|
open: false,
|
|
145
|
+
turnStartedAt: 0,
|
|
146
|
+
usageLimits: null,
|
|
131
147
|
failed: false,
|
|
132
148
|
calls: new Map(),
|
|
133
149
|
patches: new Map(),
|
|
134
150
|
lines: { added: 0, removed: 0 },
|
|
135
151
|
tokens: null,
|
|
136
152
|
context: null,
|
|
153
|
+
contextSample: emptySample(),
|
|
137
154
|
plan: null,
|
|
138
155
|
history: [],
|
|
139
156
|
contextParts: null,
|
|
@@ -147,6 +164,87 @@ function emptyRollout() {
|
|
|
147
164
|
}
|
|
148
165
|
}
|
|
149
166
|
|
|
167
|
+
const AGENTS_FILE = /^# AGENTS\.md instructions for ([^\n]+)/
|
|
168
|
+
|
|
169
|
+
const emptyOpened = () => ({
|
|
170
|
+
lastAt: 0,
|
|
171
|
+
thoughts: [],
|
|
172
|
+
replies: [],
|
|
173
|
+
sizes: emptySizes(),
|
|
174
|
+
sent: { base: 0, developer: [], other: 0, memory: new Map(), tools: new Map() },
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
function callName(item) {
|
|
178
|
+
if (item.type === 'local_shell_call') return 'shell'
|
|
179
|
+
if (item.type === 'tool_search_call') return 'tool_search'
|
|
180
|
+
return `${text(item.namespace)}${text(item.name)}`
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function codexFixed(sent) {
|
|
184
|
+
const fixed = emptyFixed()
|
|
185
|
+
const developer = sent.developer.reduce((n, chars) => n + chars, 0)
|
|
186
|
+
fixed.system = developer + (sent.developer.includes(sent.base) ? 0 : sent.base) + sent.other
|
|
187
|
+
fixed.mcp = new Map(sent.tools)
|
|
188
|
+
fixed.memory = new Map(sent.memory)
|
|
189
|
+
return fixed
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function applyOpened(state, record) {
|
|
193
|
+
const at = Date.parse(record.timestamp) || state.lastAt
|
|
194
|
+
state.lastAt = at
|
|
195
|
+
const item = record.payload
|
|
196
|
+
if (!item || typeof item !== 'object') return
|
|
197
|
+
const { sizes, sent } = state
|
|
198
|
+
const before = sizes.first == null
|
|
199
|
+
if (record.type === 'session_meta') {
|
|
200
|
+
sent.base = text(item.base_instructions?.text).length
|
|
201
|
+
} else if (record.type === 'compacted') {
|
|
202
|
+
sizeCompaction(sizes)
|
|
203
|
+
return
|
|
204
|
+
} else if (record.type === 'event_msg') {
|
|
205
|
+
if (item.type === 'token_count' && item.info?.last_token_usage) sizeRequest(sizes, number(item.info.last_token_usage.input_tokens), at)
|
|
206
|
+
return
|
|
207
|
+
} else if (record.type !== 'response_item') {
|
|
208
|
+
return
|
|
209
|
+
} else if (item.type === 'reasoning') {
|
|
210
|
+
const said = joinedText(item.summary, 'summary_text', 'text')
|
|
211
|
+
keepText(state.thoughts, 'thoughts', { id: text(item.id) || null, at, text: said })
|
|
212
|
+
sizeReply(sizes, said.length)
|
|
213
|
+
return
|
|
214
|
+
} else if (item.type === 'message' && item.role === 'assistant') {
|
|
215
|
+
const said = joinedText(item.content, 'output_text', 'text')
|
|
216
|
+
keepText(state.replies, 'replies', { id: text(item.id) || null, at, text: said })
|
|
217
|
+
sizeReply(sizes, said.length)
|
|
218
|
+
return
|
|
219
|
+
} else if (CALLS.has(item.type)) {
|
|
220
|
+
sizeCall(sizes, text(item.call_id), callName(item), (text(item.arguments) || text(item.input) || JSON.stringify(item.action ?? '')).length)
|
|
221
|
+
return
|
|
222
|
+
} else if (OUTPUTS.has(item.type)) {
|
|
223
|
+
sizeResult(sizes, text(item.call_id), outputText(item.output).length)
|
|
224
|
+
return
|
|
225
|
+
} else if (item.type === 'additional_tools') {
|
|
226
|
+
for (const group of Array.isArray(item.tools) ? item.tools : []) {
|
|
227
|
+
const tools = Array.isArray(group?.tools) ? group.tools.length : 0
|
|
228
|
+
sent.tools.set(text(group?.name) || 'tools', { tools: tools || null, chars: JSON.stringify(group ?? null).length })
|
|
229
|
+
}
|
|
230
|
+
} else if (item.type === 'message') {
|
|
231
|
+
const raw = contentText(item.content)
|
|
232
|
+
const agents = item.role === 'user' ? AGENTS_FILE.exec(raw) : null
|
|
233
|
+
if (agents) sent.memory.set(`${agents[1].trim().replace(/[\/]+$/, '')}/AGENTS.md`, raw.length)
|
|
234
|
+
else if (item.role === 'user' && promptText(raw)) {
|
|
235
|
+
sizePrompt(sizes, raw.length)
|
|
236
|
+
return
|
|
237
|
+
} else if (!before) {
|
|
238
|
+
sizePrompt(sizes, raw.length)
|
|
239
|
+
return
|
|
240
|
+
} else if (item.role === 'developer' || item.role === 'system') sent.developer.push(raw.length)
|
|
241
|
+
else sent.other += raw.length
|
|
242
|
+
} else {
|
|
243
|
+
return
|
|
244
|
+
}
|
|
245
|
+
setFixed(sizes, codexFixed(sent))
|
|
246
|
+
}
|
|
247
|
+
|
|
150
248
|
function applyRecord(state, record) {
|
|
151
249
|
const at = Date.parse(record.timestamp)
|
|
152
250
|
if (at) {
|
|
@@ -199,23 +297,19 @@ function applyItem(state, item, at) {
|
|
|
199
297
|
const id = text(item.call_id)
|
|
200
298
|
if (!id) return
|
|
201
299
|
if (CALLS.has(item.type)) {
|
|
202
|
-
|
|
203
|
-
if (item.type === 'local_shell_call') name = 'shell'
|
|
204
|
-
if (item.type === 'tool_search_call') name = 'tool_search'
|
|
205
|
-
state.calls.set(id, { name, startedAt: at })
|
|
300
|
+
const name = callName(item)
|
|
206
301
|
const args = item.type === 'function_call' ? parseArgs(item.arguments) : null
|
|
207
302
|
if (item.type === 'function_call' && !item.namespace && item.name === 'update_plan') state.plan = nextPlan(state.plan, 'update_plan', args)
|
|
208
303
|
const patch = item.name === 'apply_patch' ? (item.type === 'custom_tool_call' ? text(item.input) : text(args?.input)) : ''
|
|
209
304
|
if (patch) state.patches.set(id, patch)
|
|
210
305
|
const file = patch ? patchFile(patch) : ''
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
})
|
|
306
|
+
const target = callTarget(item, args, patch, state.cwd)
|
|
307
|
+
const input = callInput(item, args, patch)
|
|
308
|
+
const entry = { name, startedAt: at, target, kind: inputKind(name, input).kind }
|
|
309
|
+
const questions = QUESTION_TOOLS.has(name) ? askQuestions(args) : null
|
|
310
|
+
if (questions) entry.questions = questions
|
|
311
|
+
state.calls.set(id, entry)
|
|
312
|
+
startCall(state.activity, { id, name, target, input, path: file ? relativePath(file, state.cwd) : null, startedAt: at })
|
|
219
313
|
return
|
|
220
314
|
}
|
|
221
315
|
if (!OUTPUTS.has(item.type)) return
|
|
@@ -274,6 +368,7 @@ function applyEvent(state, event, at) {
|
|
|
274
368
|
switch (event.type) {
|
|
275
369
|
case 'task_started':
|
|
276
370
|
case 'turn_started':
|
|
371
|
+
if (!state.open) state.turnStartedAt = at
|
|
277
372
|
state.open = true
|
|
278
373
|
state.failed = false
|
|
279
374
|
state.calls.clear()
|
|
@@ -298,6 +393,7 @@ function applyEvent(state, event, at) {
|
|
|
298
393
|
break
|
|
299
394
|
case 'token_count':
|
|
300
395
|
applyTokens(state, event.info, at)
|
|
396
|
+
applyLimits(state, event.rate_limits, at)
|
|
301
397
|
break
|
|
302
398
|
case 'user_message':
|
|
303
399
|
state.typed = true
|
|
@@ -316,6 +412,21 @@ function applyEvent(state, event, at) {
|
|
|
316
412
|
}
|
|
317
413
|
}
|
|
318
414
|
|
|
415
|
+
const LIMIT_WINDOWS = ['primary', 'secondary']
|
|
416
|
+
|
|
417
|
+
function applyLimits(state, limits, at) {
|
|
418
|
+
if (!limits || typeof limits !== 'object') return
|
|
419
|
+
const windows = []
|
|
420
|
+
for (const id of LIMIT_WINDOWS) {
|
|
421
|
+
const w = limits[id]
|
|
422
|
+
if (!w || typeof w !== 'object' || w.used_percent == null) continue
|
|
423
|
+
const resetsAt = w.resets_at != null ? number(w.resets_at) * 1000 : w.resets_in_seconds != null ? at + number(w.resets_in_seconds) * 1000 : null
|
|
424
|
+
windows.push({ id, usedPercent: number(w.used_percent), windowMinutes: w.window_minutes == null ? null : number(w.window_minutes), resetsAt: resetsAt || null })
|
|
425
|
+
}
|
|
426
|
+
if (!windows.length) return
|
|
427
|
+
state.usageLimits = { at, subscription: text(limits.plan_type) || null, windows }
|
|
428
|
+
}
|
|
429
|
+
|
|
319
430
|
function applyTokens(state, info, at) {
|
|
320
431
|
if (!info) return
|
|
321
432
|
const total = info.total_token_usage
|
|
@@ -344,6 +455,7 @@ function applyTokens(state, info, at) {
|
|
|
344
455
|
const sum = number(total?.total_tokens)
|
|
345
456
|
if (sum === state.lastTotal) return
|
|
346
457
|
state.lastTotal = sum
|
|
458
|
+
samplePoint(state.contextSample, last.total_tokens)
|
|
347
459
|
keepNewest(state.history, { at, context: number(last.total_tokens), output: number(last.output_tokens) }, DETAIL_LIMITS.history)
|
|
348
460
|
}
|
|
349
461
|
|
|
@@ -356,6 +468,7 @@ export function codex({
|
|
|
356
468
|
const locksDir = path.join(home, 'thread-writer-locks')
|
|
357
469
|
const indexFile = path.join(home, 'session_index.jsonl')
|
|
358
470
|
const rollouts = jsonlFollower({ empty: emptyRollout, apply: applyRecord })
|
|
471
|
+
const openedFor = recentReaders(() => jsonlFollower({ empty: emptyOpened, apply: applyOpened }), OPENED_KEPT)
|
|
359
472
|
let index = { mtime: -1, names: new Map() }
|
|
360
473
|
let known = new Map()
|
|
361
474
|
let childrenOf = new Map()
|
|
@@ -440,8 +553,9 @@ export function codex({
|
|
|
440
553
|
for (const { id, stat, state } of read) {
|
|
441
554
|
if (!state.subagent || !state.parentId || !state.open) continue
|
|
442
555
|
if (!locks.has(id) && at - stat.mtimeMs > SUBAGENT_WINDOW_MS) continue
|
|
443
|
-
running.set(state.parentId, (running.get(state.parentId) ||
|
|
556
|
+
running.set(state.parentId, [...(running.get(state.parentId) || []), { id, name: childName(state), startedAt: state.startedAt }])
|
|
444
557
|
}
|
|
558
|
+
const workingOf = (id) => (running.get(id) || []).sort((a, b) => a.startedAt - b.startedAt).map(({ id: childId, name }) => ({ id: childId, name }))
|
|
445
559
|
|
|
446
560
|
return read
|
|
447
561
|
.filter(({ id, state }) => !state.subagent && !state.internal && !archived.has(id))
|
|
@@ -449,7 +563,8 @@ export function codex({
|
|
|
449
563
|
const lastActivityAt = Math.max(state.lastAt, Math.floor(stat.mtimeMs))
|
|
450
564
|
const fresh = at - lastActivityAt < ACTIVE_WINDOW_MS
|
|
451
565
|
const live = locking ? locks.has(id) : state.open && fresh
|
|
452
|
-
|
|
566
|
+
const workingSubagents = workingOf(id)
|
|
567
|
+
return toSession({ id, state, names, live, turnLive: state.open && (locking ? live : fresh), fresh, locking, unread: unread.get(id), subagents: workingSubagents.length, workingSubagents, lastActivityAt })
|
|
453
568
|
})
|
|
454
569
|
}
|
|
455
570
|
|
|
@@ -465,7 +580,9 @@ export function codex({
|
|
|
465
580
|
const id = String(key).replace(/^codex:/, '').toLowerCase()
|
|
466
581
|
const found = await readKnown(id)
|
|
467
582
|
if (!found || found.state.subagent || found.state.internal) return null
|
|
468
|
-
const { file, state } = found
|
|
583
|
+
const { file, stat, state } = found
|
|
584
|
+
const said = await openedFor(file).read(file, stat).catch(() => null)
|
|
585
|
+
const limit = state.context?.limit || null
|
|
469
586
|
const subagents = []
|
|
470
587
|
for (const childId of childrenOf.get(id) || []) {
|
|
471
588
|
const child = await readKnown(childId)
|
|
@@ -475,7 +592,7 @@ export function codex({
|
|
|
475
592
|
const status = c.open && now() - lastActivityAt < ACTIVE_WINDOW_MS ? 'working' : c.failed ? 'error' : 'done'
|
|
476
593
|
subagents.push({
|
|
477
594
|
id: childId,
|
|
478
|
-
name:
|
|
595
|
+
name: childName(c),
|
|
479
596
|
type: '',
|
|
480
597
|
status,
|
|
481
598
|
model: c.model,
|
|
@@ -498,6 +615,13 @@ export function codex({
|
|
|
498
615
|
subagents: subagents.sort((a, b) => a.startedAt - b.startedAt),
|
|
499
616
|
files: fileList(state.files, state.cwd),
|
|
500
617
|
prompts: (state.typed ? state.promptList : state.fallbackPromptList).map((p) => ({ ...p })),
|
|
618
|
+
thoughts: textList(said?.thoughts),
|
|
619
|
+
replies: textList(said?.replies),
|
|
620
|
+
contextBreakdown: contextBreakdown(said?.sizes, {
|
|
621
|
+
total: state.context?.used,
|
|
622
|
+
limit,
|
|
623
|
+
reserved: limit ? Math.round(limit * (1 - COMPACT_SHARE / REPORTED_SHARE)) : null,
|
|
624
|
+
}),
|
|
501
625
|
errors: state.errors.map((e) => ({ ...e })),
|
|
502
626
|
}
|
|
503
627
|
}
|
|
@@ -512,17 +636,21 @@ export function codex({
|
|
|
512
636
|
}
|
|
513
637
|
}
|
|
514
638
|
|
|
515
|
-
|
|
516
|
-
|
|
639
|
+
const childName = (state) => titleFromPrompt(state.typed ? state.firstPrompt : state.fallbackPrompt) || 'Subagent'
|
|
640
|
+
|
|
641
|
+
function toSession({ id, state, names, live, turnLive, fresh, locking, unread, subagents, workingSubagents, lastActivityAt }) {
|
|
642
|
+
const calls = [...state.calls.values()].sort((a, b) => a.startedAt - b.startedAt)
|
|
643
|
+
const asked = calls.filter((c) => QUESTION_TOOLS.has(c.name))
|
|
517
644
|
let status
|
|
645
|
+
let statusReason
|
|
518
646
|
if (turnLive) {
|
|
519
|
-
if (
|
|
520
|
-
else if (calls.length ? calls.every((c) => AGENT_TOOLS.has(c.name)) : subagents > 0) status = 'delegating'
|
|
521
|
-
else status = 'working'
|
|
522
|
-
} else if (subagents) status = 'delegating'
|
|
523
|
-
else if (state.failed) status = 'error'
|
|
524
|
-
else if (unread !== undefined) status = unread ? 'done' : 'idle'
|
|
525
|
-
else status = (locking ? live : fresh) ? 'done' : 'idle'
|
|
647
|
+
if (asked.length) [status, statusReason] = ['asking', asked.some((c) => c.name === 'request_user_input') ? 'question' : 'approval']
|
|
648
|
+
else if (calls.length ? calls.every((c) => AGENT_TOOLS.has(c.name)) : subagents > 0) [status, statusReason] = ['delegating', 'subagents']
|
|
649
|
+
else [status, statusReason] = ['working', 'turn-open']
|
|
650
|
+
} else if (subagents) [status, statusReason] = ['delegating', 'subagents']
|
|
651
|
+
else if (state.failed) [status, statusReason] = ['error', 'turn-failed']
|
|
652
|
+
else if (unread !== undefined) [status, statusReason] = unread ? ['done', 'unread'] : ['idle', 'read']
|
|
653
|
+
else [status, statusReason] = (locking ? live : fresh) ? ['done', 'recent'] : ['idle', 'quiet']
|
|
526
654
|
|
|
527
655
|
const tool = turnLive && (status === 'working' || status === 'delegating') ? latestCall(state.calls) : null
|
|
528
656
|
const prompt = state.typed ? state.firstPrompt : state.fallbackPrompt
|
|
@@ -545,11 +673,17 @@ function toSession({ id, state, names, live, turnLive, fresh, locking, unread, s
|
|
|
545
673
|
tool,
|
|
546
674
|
tokens: state.tokens ? { ...state.tokens } : null,
|
|
547
675
|
context: state.context && state.context.used ? { ...state.context } : null,
|
|
676
|
+
contextHistory: sampleList(state.contextSample, state.context?.used),
|
|
548
677
|
costUsd: null,
|
|
549
678
|
lines: { ...state.lines },
|
|
550
679
|
plan: planSummary(state.plan),
|
|
551
680
|
subagents,
|
|
681
|
+
workingSubagents,
|
|
552
682
|
prompts: state.typed ? state.prompts : state.fallbackPrompts,
|
|
683
|
+
turnStartedAt: turnLive ? state.turnStartedAt || null : null,
|
|
684
|
+
statusReason,
|
|
685
|
+
ask: askOf(status, { calls: asked, errorText: state.errors.at(-1)?.message }),
|
|
686
|
+
usageLimits: state.usageLimits ? { ...state.usageLimits, windows: state.usageLimits.windows.map((w) => ({ ...w })) } : null,
|
|
553
687
|
}
|
|
554
688
|
}
|
|
555
689
|
|
package/telemetry/cursor.mjs
CHANGED
|
@@ -3,24 +3,30 @@ import os from 'node:os'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { decodeProjectDir } from '../lib/cursor-folder.mjs'
|
|
5
5
|
import { exists, listDirs, listFiles } from '../lib/fsutil.mjs'
|
|
6
|
-
import { jsonlFollower } from '../lib/jsonl-follower.mjs'
|
|
6
|
+
import { jsonlFollower, recentReaders } from '../lib/jsonl-follower.mjs'
|
|
7
7
|
import {
|
|
8
8
|
DETAIL_LIMITS,
|
|
9
9
|
RECENT_MS,
|
|
10
10
|
activityDetail,
|
|
11
|
+
askOf,
|
|
11
12
|
clip,
|
|
12
13
|
clipBlock,
|
|
13
14
|
emptyActivity,
|
|
15
|
+
joinedText,
|
|
14
16
|
keepNewest,
|
|
17
|
+
keepText,
|
|
15
18
|
nextPlan,
|
|
19
|
+
openTool,
|
|
16
20
|
planItems,
|
|
17
21
|
planSummary,
|
|
18
22
|
relativePath,
|
|
19
23
|
startCall,
|
|
20
24
|
text,
|
|
25
|
+
textList,
|
|
21
26
|
titleFromPrompt,
|
|
22
27
|
toolFile,
|
|
23
28
|
toolInput,
|
|
29
|
+
toolNow,
|
|
24
30
|
toolTarget,
|
|
25
31
|
} from '../lib/telemetry-fields.mjs'
|
|
26
32
|
|
|
@@ -57,6 +63,7 @@ function emptyTranscript() {
|
|
|
57
63
|
closed: false,
|
|
58
64
|
failed: false,
|
|
59
65
|
modelsTurn: false,
|
|
66
|
+
turnStartedAt: 0,
|
|
60
67
|
tool: null,
|
|
61
68
|
plan: null,
|
|
62
69
|
activity: emptyActivity(),
|
|
@@ -80,6 +87,7 @@ function applyRecord(state, record) {
|
|
|
80
87
|
state.closed = false
|
|
81
88
|
if (record.role === 'user') {
|
|
82
89
|
const prompt = userText(record)
|
|
90
|
+
if (!state.modelsTurn) state.turnStartedAt = stamp(record)
|
|
83
91
|
if (prompt) {
|
|
84
92
|
state.prompts++
|
|
85
93
|
state.firstPrompt ||= prompt
|
|
@@ -98,7 +106,16 @@ function applyRecord(state, record) {
|
|
|
98
106
|
startCall(state.activity, { name: text(call.name), target: toolTarget(call.input, '', Infinity), input: toolInput(call.input), ...toolFile(call.input), startedAt: 0, running: false })
|
|
99
107
|
}
|
|
100
108
|
state.modelsTurn = calls.length > 0
|
|
101
|
-
state.tool = calls.length ?
|
|
109
|
+
state.tool = calls.length ? openTool(text(calls.at(-1).name), 0, calls.at(-1).input) : null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const WORDS_KEPT = 3
|
|
113
|
+
|
|
114
|
+
const emptyWords = () => ({ turnAt: 0, replies: [] })
|
|
115
|
+
|
|
116
|
+
function applyWords(state, record) {
|
|
117
|
+
if (record.role === 'user') state.turnAt = stamp(record) || state.turnAt
|
|
118
|
+
else if (record.role === 'assistant') keepText(state.replies, 'replies', { at: state.turnAt, text: joinedText(record.message?.content, 'text') })
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
export function cursor({
|
|
@@ -107,6 +124,7 @@ export function cursor({
|
|
|
107
124
|
} = {}) {
|
|
108
125
|
const transcripts = jsonlFollower({ empty: emptyTranscript, apply: applyRecord })
|
|
109
126
|
const subagentFiles = jsonlFollower({ empty: emptyTranscript, apply: applyRecord })
|
|
127
|
+
const wordsFor = recentReaders(() => jsonlFollower({ empty: emptyWords, apply: applyWords }), WORDS_KEPT)
|
|
110
128
|
let subagentsSweptAt = 0
|
|
111
129
|
let known = new Map()
|
|
112
130
|
|
|
@@ -142,15 +160,17 @@ export function cursor({
|
|
|
142
160
|
}
|
|
143
161
|
|
|
144
162
|
async function runningSubagents(sessionDir) {
|
|
145
|
-
if (!sessionDir) return
|
|
146
|
-
|
|
163
|
+
if (!sessionDir) return []
|
|
164
|
+
const working = []
|
|
147
165
|
for (const file of await listFiles(path.join(sessionDir, 'subagents'), (n) => n.endsWith('.jsonl'))) {
|
|
148
166
|
const stat = await fsp.stat(file).catch(() => null)
|
|
149
167
|
if (!stat || now() - stat.mtimeMs > SUBAGENT_WINDOW_MS) continue
|
|
150
168
|
const state = await follow(file, stat, subagentFiles).catch(() => null)
|
|
151
|
-
if (state
|
|
169
|
+
if (!state || !isOpen(state, true)) continue
|
|
170
|
+
const id = path.basename(file, '.jsonl')
|
|
171
|
+
working.push({ id, name: titleFromPrompt(state.firstPrompt) || id, startedAt: state.startedAt || Math.floor(stat.birthtimeMs) || Math.floor(stat.mtimeMs) })
|
|
152
172
|
}
|
|
153
|
-
return
|
|
173
|
+
return working.sort((a, b) => a.startedAt - b.startedAt).map(({ id, name }) => ({ id, name }))
|
|
154
174
|
}
|
|
155
175
|
|
|
156
176
|
async function subagentList(sessionDir) {
|
|
@@ -170,7 +190,7 @@ export function cursor({
|
|
|
170
190
|
status: open ? 'working' : state.failed ? 'error' : 'done',
|
|
171
191
|
model: '',
|
|
172
192
|
tokens: null,
|
|
173
|
-
tool: open && state.tool ?
|
|
193
|
+
tool: open && state.tool ? toolNow(state.tool) : null,
|
|
174
194
|
startedAt: state.startedAt || Math.floor(stat.birthtimeMs) || Math.floor(stat.mtimeMs),
|
|
175
195
|
lastActivityAt: Math.floor(stat.mtimeMs),
|
|
176
196
|
})
|
|
@@ -184,6 +204,7 @@ export function cursor({
|
|
|
184
204
|
const stat = entry ? await fsp.stat(entry.file).catch(() => null) : null
|
|
185
205
|
if (!stat) return null
|
|
186
206
|
const state = await follow(entry.file, stat)
|
|
207
|
+
const said = await wordsFor(entry.file).read(entry.file, stat).catch(() => null)
|
|
187
208
|
const open = isOpen(state, now() - stat.mtimeMs < ACTIVE_WINDOW_MS)
|
|
188
209
|
const { activity, tools } = activityDetail(state.activity)
|
|
189
210
|
for (const call of activity) {
|
|
@@ -205,6 +226,9 @@ export function cursor({
|
|
|
205
226
|
subagents: await subagentList(entry.sessionDir),
|
|
206
227
|
files: null,
|
|
207
228
|
prompts: state.promptList.map((p) => ({ ...p })),
|
|
229
|
+
thoughts: null,
|
|
230
|
+
replies: textList(said?.replies),
|
|
231
|
+
contextBreakdown: null,
|
|
208
232
|
errors: state.errors.map((e) => ({ ...e })),
|
|
209
233
|
}
|
|
210
234
|
}
|
|
@@ -221,14 +245,16 @@ export function cursor({
|
|
|
221
245
|
}
|
|
222
246
|
const fresh = now() - entry.stat.mtimeMs < ACTIVE_WINDOW_MS
|
|
223
247
|
const open = isOpen(state, fresh)
|
|
224
|
-
const
|
|
248
|
+
const workingSubagents = await runningSubagents(entry.sessionDir)
|
|
249
|
+
const subagents = workingSubagents.length
|
|
225
250
|
const projectPath = await decodeProjectDir(path.basename(entry.projectDir))
|
|
226
251
|
found.set(entry.id, { file: entry.file, sessionDir: entry.sessionDir, projectPath })
|
|
227
252
|
let status
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
else if (
|
|
231
|
-
else status =
|
|
253
|
+
let statusReason
|
|
254
|
+
if (subagents) [status, statusReason] = ['delegating', 'subagents']
|
|
255
|
+
else if (open) [status, statusReason] = ['working', 'turn-open']
|
|
256
|
+
else if (state.failed) [status, statusReason] = ['error', 'turn-failed']
|
|
257
|
+
else [status, statusReason] = fresh ? ['done', 'recent'] : ['idle', 'quiet']
|
|
232
258
|
sessions.push({
|
|
233
259
|
id: `cursor:${entry.id}`,
|
|
234
260
|
harness: 'cursor',
|
|
@@ -244,14 +270,20 @@ export function cursor({
|
|
|
244
270
|
live: open,
|
|
245
271
|
startedAt: state.startedAt || Math.floor(entry.stat.birthtimeMs) || Math.floor(entry.stat.mtimeMs),
|
|
246
272
|
lastActivityAt: Math.floor(entry.stat.mtimeMs),
|
|
247
|
-
tool: open && state.tool ? { ...state.tool } : null,
|
|
273
|
+
tool: open && state.tool ? { ...toolNow(state.tool), target: clip(relativePath(state.tool.target, projectPath)) } : null,
|
|
248
274
|
tokens: null,
|
|
249
275
|
context: null,
|
|
276
|
+
contextHistory: null,
|
|
250
277
|
costUsd: null,
|
|
251
278
|
lines: null,
|
|
252
279
|
plan: planSummary(state.plan),
|
|
253
280
|
subagents,
|
|
281
|
+
workingSubagents,
|
|
254
282
|
prompts: state.prompts,
|
|
283
|
+
turnStartedAt: open ? state.turnStartedAt || null : null,
|
|
284
|
+
statusReason,
|
|
285
|
+
ask: askOf(status, { errorText: state.errors.at(-1)?.message }),
|
|
286
|
+
usageLimits: null,
|
|
255
287
|
})
|
|
256
288
|
}
|
|
257
289
|
transcripts.sweep()
|