sidekick-shared 0.21.1 → 0.21.3
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 -45
- package/dist/accountManager.js +9 -7
- package/dist/accountRegistry.js +18 -14
- package/dist/accountStatus.js +4 -1
- package/dist/accounts.js +60 -25
- package/dist/aggregation/EventAggregator.d.ts +14 -11
- package/dist/aggregation/EventAggregator.js +68 -31
- package/dist/aggregation/PatternExtractor.js +5 -4
- package/dist/autoSwitch.js +6 -12
- package/dist/claudeProfiles.js +3 -3
- package/dist/codexProfiles.js +45 -22
- package/dist/codexQuota.js +44 -16
- package/dist/codexQuotaWatcher.js +10 -3
- package/dist/context/composer.js +8 -6
- package/dist/context/sessionContext.js +12 -9
- package/dist/credentialIO.js +10 -8
- package/dist/extractors/sessionAssets.js +9 -1
- package/dist/extractors/sources/claudeAssets.js +7 -1
- package/dist/extractors/sources/codexAssets.js +14 -3
- package/dist/extractors/toolCall.js +1 -4
- package/dist/formatters/eventHighlighter.js +148 -28
- package/dist/formatters/noiseClassifier.js +3 -11
- package/dist/formatters/sessionDump.js +22 -13
- package/dist/formatters/toolSummary.js +1 -3
- package/dist/index.d.ts +25 -23
- package/dist/index.js +12 -3
- package/dist/modelContext.js +10 -8
- package/dist/modelInfo.js +11 -6
- package/dist/multiProviderQuotaService.js +10 -7
- package/dist/parsers/codexParser.js +170 -103
- package/dist/parsers/debugLogParser.js +5 -4
- package/dist/parsers/openCodeParser.js +66 -48
- package/dist/parsers/planExtractor.js +3 -3
- package/dist/parsers/sessionActivityDetector.js +10 -6
- package/dist/parsers/sessionPathResolver.js +29 -34
- package/dist/parsers/subagentScanner.js +21 -20
- package/dist/parsers/subagentTraceParser.js +7 -4
- package/dist/phrases.js +11 -11
- package/dist/providerStatus.js +3 -4
- package/dist/providers/claudeCode.js +3 -1
- package/dist/providers/codex.js +35 -14
- package/dist/providers/codexDatabase.js +14 -5
- package/dist/providers/detect.js +10 -6
- package/dist/providers/openCode.d.ts +4 -4
- package/dist/providers/openCode.js +96 -68
- package/dist/providers/openCodeDatabase.js +44 -17
- package/dist/quota.d.ts +19 -0
- package/dist/quota.js +50 -8
- package/dist/quotaHistory.js +10 -3
- package/dist/quotaSnapshots.d.ts +1 -1
- package/dist/quotaSnapshots.js +2 -2
- package/dist/readers/decisions.js +1 -1
- package/dist/readers/notes.js +5 -3
- package/dist/readers/plans.js +43 -23
- package/dist/readers/tasks.js +1 -1
- package/dist/report/htmlHelpers.js +135 -16
- package/dist/report/htmlReportGenerator.js +38 -12
- package/dist/report/transcriptParser.js +8 -2
- package/dist/schemas/index.d.ts +4 -4
- package/dist/schemas/quota.js +2 -10
- package/dist/schemas/sessionEvent.js +20 -10
- package/dist/search/advancedFilter.js +3 -3
- package/dist/search/sessionSearch.js +10 -4
- package/dist/terminalSync.js +4 -2
- package/dist/turns/assistantTurn.js +5 -1
- package/dist/watchers/eventBridge.js +56 -26
- package/dist/watchers/factory.js +5 -2
- package/dist/watchers/jsonlTail.js +3 -1
- package/dist/watchers/jsonlWatcher.js +128 -52
- package/dist/watchers/providerReaderWatcher.js +3 -1
- package/dist/watchers/sqliteWatcher.js +52 -22
- package/dist/zaiQuota.d.ts +6 -6
- package/dist/zaiQuota.js +6 -10
- package/dist/zaiQuotaApi.d.ts +33 -0
- package/dist/zaiQuotaApi.js +279 -0
- package/dist/zaiQuotaWatcher.js +10 -9
- package/package.json +5 -2
|
@@ -156,7 +156,10 @@ function convertUserMessage(message, parts) {
|
|
|
156
156
|
content.push({ type: 'text', text: part.text });
|
|
157
157
|
}
|
|
158
158
|
else if (part.type === 'file') {
|
|
159
|
-
content.push({
|
|
159
|
+
content.push({
|
|
160
|
+
type: 'text',
|
|
161
|
+
text: `[File: ${part.filename || 'unknown'} (${part.mime || 'unknown'})]`,
|
|
162
|
+
});
|
|
160
163
|
}
|
|
161
164
|
else if (part.type === 'subtask') {
|
|
162
165
|
content.push({ type: 'text', text: `[Subtask: ${part.description || 'unknown'}]` });
|
|
@@ -166,7 +169,7 @@ function convertUserMessage(message, parts) {
|
|
|
166
169
|
events.push({
|
|
167
170
|
type: 'user',
|
|
168
171
|
message: { role: 'user', id: message.id, content },
|
|
169
|
-
timestamp: toISOString(message.time.created)
|
|
172
|
+
timestamp: toISOString(message.time.created),
|
|
170
173
|
});
|
|
171
174
|
}
|
|
172
175
|
return events;
|
|
@@ -195,7 +198,7 @@ function convertAssistantMessage(message, parts) {
|
|
|
195
198
|
type: 'tool_use',
|
|
196
199
|
id: part.callID,
|
|
197
200
|
name: normalizeToolName(part.tool),
|
|
198
|
-
input: normalizeToolInput(part.state.input || {})
|
|
201
|
+
input: normalizeToolInput(part.state.input || {}),
|
|
199
202
|
});
|
|
200
203
|
break;
|
|
201
204
|
case 'tool':
|
|
@@ -203,7 +206,7 @@ function convertAssistantMessage(message, parts) {
|
|
|
203
206
|
type: 'tool_use',
|
|
204
207
|
id: part.callID,
|
|
205
208
|
name: normalizeToolName(part.tool),
|
|
206
|
-
input: normalizeToolInput(part.state.input || {})
|
|
209
|
+
input: normalizeToolInput(part.state.input || {}),
|
|
207
210
|
});
|
|
208
211
|
break;
|
|
209
212
|
case 'patch':
|
|
@@ -216,7 +219,7 @@ function convertAssistantMessage(message, parts) {
|
|
|
216
219
|
type: 'tool_use',
|
|
217
220
|
id: `patch-${part.id}-${i}`,
|
|
218
221
|
name: 'Write',
|
|
219
|
-
input: { file_path: part.files[i] }
|
|
222
|
+
input: { file_path: part.files[i] },
|
|
220
223
|
});
|
|
221
224
|
}
|
|
222
225
|
}
|
|
@@ -225,7 +228,7 @@ function convertAssistantMessage(message, parts) {
|
|
|
225
228
|
type: 'tool_use',
|
|
226
229
|
id: `patch-${part.id}`,
|
|
227
230
|
name: 'Patch',
|
|
228
|
-
input: { hash: part.hash, files: part.files }
|
|
231
|
+
input: { hash: part.hash, files: part.files },
|
|
229
232
|
});
|
|
230
233
|
}
|
|
231
234
|
break;
|
|
@@ -244,19 +247,19 @@ function convertAssistantMessage(message, parts) {
|
|
|
244
247
|
model: part.model,
|
|
245
248
|
prompt: part.prompt,
|
|
246
249
|
command: part.command,
|
|
247
|
-
}
|
|
250
|
+
},
|
|
248
251
|
});
|
|
249
252
|
break;
|
|
250
253
|
case 'file':
|
|
251
254
|
content.push({
|
|
252
255
|
type: 'text',
|
|
253
|
-
text: `[File: ${part.filename || 'unknown'} (${part.mime || 'unknown'})]
|
|
256
|
+
text: `[File: ${part.filename || 'unknown'} (${part.mime || 'unknown'})]`,
|
|
254
257
|
});
|
|
255
258
|
break;
|
|
256
259
|
case 'retry':
|
|
257
260
|
content.push({
|
|
258
261
|
type: 'text',
|
|
259
|
-
text: `[Retry attempt ${part.attempt ?? '?'}: ${part.error?.message || 'unknown error'}]
|
|
262
|
+
text: `[Retry attempt ${part.attempt ?? '?'}: ${part.error?.message || 'unknown error'}]`,
|
|
260
263
|
});
|
|
261
264
|
break;
|
|
262
265
|
case 'agent':
|
|
@@ -276,15 +279,14 @@ function convertAssistantMessage(message, parts) {
|
|
|
276
279
|
const errCode = message.error.code ? ` (code: ${message.error.code})` : '';
|
|
277
280
|
content.push({
|
|
278
281
|
type: 'text',
|
|
279
|
-
text: `[${errType}]${errCode} ${errMsg}
|
|
282
|
+
text: `[${errType}]${errCode} ${errMsg}`,
|
|
280
283
|
});
|
|
281
284
|
}
|
|
282
285
|
// Detect plan mode mentions in text/thinking blocks and inject synthetic
|
|
283
286
|
// tool_use blocks (OpenCode doesn't emit plan_enter/plan_exit tools).
|
|
284
287
|
const hasExistingPlanMode = content.some((b) => {
|
|
285
288
|
const block = b;
|
|
286
|
-
return block.type === 'tool_use' &&
|
|
287
|
-
(block.name === 'EnterPlanMode' || block.name === 'ExitPlanMode');
|
|
289
|
+
return (block.type === 'tool_use' && (block.name === 'EnterPlanMode' || block.name === 'ExitPlanMode'));
|
|
288
290
|
});
|
|
289
291
|
if (!hasExistingPlanMode) {
|
|
290
292
|
for (const block of content) {
|
|
@@ -304,7 +306,7 @@ function convertAssistantMessage(message, parts) {
|
|
|
304
306
|
type: 'tool_use',
|
|
305
307
|
id: `plan-${direction}-${message.id}`,
|
|
306
308
|
name: toolName,
|
|
307
|
-
input: { source: 'opencode_text_heuristic', _sidekickRawToolName: rawName }
|
|
309
|
+
input: { source: 'opencode_text_heuristic', _sidekickRawToolName: rawName },
|
|
308
310
|
});
|
|
309
311
|
break; // Only inject one plan mode block per message
|
|
310
312
|
}
|
|
@@ -320,9 +322,11 @@ function convertAssistantMessage(message, parts) {
|
|
|
320
322
|
}
|
|
321
323
|
}
|
|
322
324
|
// Build usage data from message tokens
|
|
323
|
-
const reportedCost =
|
|
325
|
+
const reportedCost = message.cost && message.cost > 0
|
|
324
326
|
? message.cost
|
|
325
|
-
:
|
|
327
|
+
: stepFinishCost > 0
|
|
328
|
+
? stepFinishCost
|
|
329
|
+
: undefined;
|
|
326
330
|
const usage = {
|
|
327
331
|
input_tokens: message.tokens.input || 0,
|
|
328
332
|
output_tokens: message.tokens.output || 0,
|
|
@@ -341,9 +345,9 @@ function convertAssistantMessage(message, parts) {
|
|
|
341
345
|
id: message.id,
|
|
342
346
|
model: message.modelID,
|
|
343
347
|
usage,
|
|
344
|
-
content
|
|
348
|
+
content,
|
|
345
349
|
},
|
|
346
|
-
timestamp
|
|
350
|
+
timestamp,
|
|
347
351
|
});
|
|
348
352
|
}
|
|
349
353
|
// Emit synthetic tool_result events for completed tool invocations
|
|
@@ -365,20 +369,24 @@ function convertAssistantMessage(message, parts) {
|
|
|
365
369
|
duration = endMs - startMs;
|
|
366
370
|
}
|
|
367
371
|
}
|
|
368
|
-
const resultContent = [
|
|
372
|
+
const resultContent = [
|
|
373
|
+
{
|
|
369
374
|
type: 'tool_result',
|
|
370
375
|
tool_use_id: part.callID,
|
|
371
376
|
content: part.state.status === 'error' ? part.state.error : part.state.output,
|
|
372
377
|
is_error: part.state.status === 'error',
|
|
373
378
|
...(duration !== undefined && { duration }),
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
: timestamp;
|
|
379
|
+
},
|
|
380
|
+
];
|
|
381
|
+
const resultTimestamp = part.state.time?.end ? toISOString(part.state.time.end) : timestamp;
|
|
378
382
|
events.push({
|
|
379
383
|
type: 'user',
|
|
380
|
-
message: {
|
|
381
|
-
|
|
384
|
+
message: {
|
|
385
|
+
role: 'user',
|
|
386
|
+
id: `${message.id}:${part.callID}:result`,
|
|
387
|
+
content: resultContent,
|
|
388
|
+
},
|
|
389
|
+
timestamp: resultTimestamp,
|
|
382
390
|
});
|
|
383
391
|
}
|
|
384
392
|
// Subtask parts get a synthetic tool_result so they appear in the timeline
|
|
@@ -388,24 +396,26 @@ function convertAssistantMessage(message, parts) {
|
|
|
388
396
|
message: {
|
|
389
397
|
role: 'user',
|
|
390
398
|
id: `${message.id}:subtask-${part.id}:result`,
|
|
391
|
-
content: [
|
|
399
|
+
content: [
|
|
400
|
+
{
|
|
392
401
|
type: 'tool_result',
|
|
393
402
|
tool_use_id: `subtask-${part.id}`,
|
|
394
403
|
content: part.description || 'Subtask completed',
|
|
395
|
-
is_error: false
|
|
396
|
-
}
|
|
404
|
+
is_error: false,
|
|
405
|
+
},
|
|
406
|
+
],
|
|
397
407
|
},
|
|
398
|
-
timestamp
|
|
408
|
+
timestamp,
|
|
399
409
|
});
|
|
400
410
|
}
|
|
401
411
|
}
|
|
402
412
|
// Detect compaction via summary flag or compaction part
|
|
403
|
-
const hasCompaction = message.summary || parts.some(p => p.type === 'compaction');
|
|
413
|
+
const hasCompaction = message.summary || parts.some((p) => p.type === 'compaction');
|
|
404
414
|
if (hasCompaction) {
|
|
405
415
|
events.push({
|
|
406
416
|
type: 'summary',
|
|
407
417
|
message: { role: 'assistant', id: `${message.id}:summary`, content: 'Context compacted' },
|
|
408
|
-
timestamp
|
|
418
|
+
timestamp,
|
|
409
419
|
});
|
|
410
420
|
}
|
|
411
421
|
return events;
|
|
@@ -426,11 +436,13 @@ function parseDbMessageData(row) {
|
|
|
426
436
|
const summary = data.summary;
|
|
427
437
|
// Parse error field if present (AuthError, APIError, OutputLengthError, etc.)
|
|
428
438
|
const errorData = data.error;
|
|
429
|
-
const error = errorData
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
439
|
+
const error = errorData
|
|
440
|
+
? {
|
|
441
|
+
type: errorData.type || undefined,
|
|
442
|
+
message: errorData.message || undefined,
|
|
443
|
+
code: errorData.code,
|
|
444
|
+
}
|
|
445
|
+
: undefined;
|
|
434
446
|
return {
|
|
435
447
|
id: row.id,
|
|
436
448
|
sessionID: row.session_id,
|
|
@@ -512,15 +524,19 @@ function parseDbPartData(row) {
|
|
|
512
524
|
reason: data.reason,
|
|
513
525
|
snapshot: data.snapshot,
|
|
514
526
|
cost: data.cost,
|
|
515
|
-
tokens: tokensData
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
527
|
+
tokens: tokensData
|
|
528
|
+
? {
|
|
529
|
+
input: tokensData.input,
|
|
530
|
+
output: tokensData.output,
|
|
531
|
+
reasoning: tokensData.reasoning,
|
|
532
|
+
cache: cacheData
|
|
533
|
+
? {
|
|
534
|
+
read: cacheData.read,
|
|
535
|
+
write: cacheData.write,
|
|
536
|
+
}
|
|
537
|
+
: undefined,
|
|
538
|
+
}
|
|
539
|
+
: undefined,
|
|
524
540
|
};
|
|
525
541
|
}
|
|
526
542
|
case 'patch':
|
|
@@ -561,10 +577,12 @@ function parseDbPartData(row) {
|
|
|
561
577
|
...base,
|
|
562
578
|
type: 'retry',
|
|
563
579
|
attempt: data.attempt,
|
|
564
|
-
error: errorData
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
580
|
+
error: errorData
|
|
581
|
+
? {
|
|
582
|
+
message: errorData.message,
|
|
583
|
+
code: errorData.code,
|
|
584
|
+
}
|
|
585
|
+
: undefined,
|
|
568
586
|
time: data.time,
|
|
569
587
|
};
|
|
570
588
|
}
|
|
@@ -306,9 +306,9 @@ class PlanExtractor {
|
|
|
306
306
|
finalizePlanMode() {
|
|
307
307
|
this._planModeActive = false;
|
|
308
308
|
// Prefer plan file content (from Write tool) → accumulated assistant text → disk read fallback
|
|
309
|
-
const markdown = this._planFileContent
|
|
310
|
-
|
|
311
|
-
|
|
309
|
+
const markdown = this._planFileContent ||
|
|
310
|
+
(this._planTexts.length > 0 ? this._planTexts.join('\n') : null) ||
|
|
311
|
+
(this._planFilePath && this._readFile ? this._readFile(this._planFilePath) : null);
|
|
312
312
|
this._planFileContent = null;
|
|
313
313
|
this._planFilePath = null;
|
|
314
314
|
this._planTexts = [];
|
|
@@ -68,9 +68,7 @@ const ENDING_PATTERNS = [
|
|
|
68
68
|
'"type":"user"', // User typing means AI is done with its turn
|
|
69
69
|
];
|
|
70
70
|
/** Patterns that definitely mean the session is done */
|
|
71
|
-
const TERMINAL_PATTERNS = [
|
|
72
|
-
'"type":"result"',
|
|
73
|
-
];
|
|
71
|
+
const TERMINAL_PATTERNS = ['"type":"result"'];
|
|
74
72
|
// ── Public API ──
|
|
75
73
|
/**
|
|
76
74
|
* Determines whether a session is ongoing, ended, or stale.
|
|
@@ -103,7 +101,7 @@ function detectSessionActivity(sessionPath) {
|
|
|
103
101
|
return { state: 'ended', lastActivityTime: new Date(stat.mtimeMs), reason: 'empty-file' };
|
|
104
102
|
}
|
|
105
103
|
// Parse the last few JSONL lines
|
|
106
|
-
const lines = tail.split('\n').filter(l => l.trim().length > 0);
|
|
104
|
+
const lines = tail.split('\n').filter((l) => l.trim().length > 0);
|
|
107
105
|
if (lines.length === 0) {
|
|
108
106
|
return { state: 'ended', lastActivityTime: new Date(stat.mtimeMs), reason: 'no-events' };
|
|
109
107
|
}
|
|
@@ -141,7 +139,11 @@ function detectSessionActivity(sessionPath) {
|
|
|
141
139
|
}
|
|
142
140
|
// AI activity after the last ending event → ongoing
|
|
143
141
|
if (lastAiActivityIndex > lastEndingIndex) {
|
|
144
|
-
return {
|
|
142
|
+
return {
|
|
143
|
+
state: 'ongoing',
|
|
144
|
+
lastActivityTime: new Date(stat.mtimeMs),
|
|
145
|
+
reason: 'ai-activity-after-ending',
|
|
146
|
+
};
|
|
145
147
|
}
|
|
146
148
|
// Ending event with no subsequent AI activity
|
|
147
149
|
if (lastEndingIndex >= 0) {
|
|
@@ -177,7 +179,9 @@ function readTail(filePath, fileSize) {
|
|
|
177
179
|
try {
|
|
178
180
|
fs.closeSync(fd);
|
|
179
181
|
}
|
|
180
|
-
catch {
|
|
182
|
+
catch {
|
|
183
|
+
/* ignore */
|
|
184
|
+
}
|
|
181
185
|
}
|
|
182
186
|
return null;
|
|
183
187
|
}
|
|
@@ -135,7 +135,7 @@ function findSubdirectorySessionDirs(workspacePath) {
|
|
|
135
135
|
// Encode workspace path to get the prefix to match
|
|
136
136
|
const encodedPrefix = encodeWorkspacePath(workspacePath).toLowerCase();
|
|
137
137
|
// Get all directories in projects folder
|
|
138
|
-
const allDirs = fs.readdirSync(projectsDir).filter(name => {
|
|
138
|
+
const allDirs = fs.readdirSync(projectsDir).filter((name) => {
|
|
139
139
|
const fullPath = path.join(projectsDir, name);
|
|
140
140
|
try {
|
|
141
141
|
return fs.statSync(fullPath).isDirectory();
|
|
@@ -184,8 +184,7 @@ function getMostRecentlyActiveSessionDir(sessionDirs) {
|
|
|
184
184
|
for (const dir of sessionDirs) {
|
|
185
185
|
try {
|
|
186
186
|
// Find most recent .jsonl file in this directory
|
|
187
|
-
const files = fs.readdirSync(dir)
|
|
188
|
-
.filter(file => file.endsWith('.jsonl'));
|
|
187
|
+
const files = fs.readdirSync(dir).filter((file) => file.endsWith('.jsonl'));
|
|
189
188
|
for (const file of files) {
|
|
190
189
|
try {
|
|
191
190
|
const fullPath = path.join(dir, file);
|
|
@@ -239,7 +238,7 @@ function discoverSessionDirectory(workspacePath) {
|
|
|
239
238
|
// Strategy 2: Scan ~/.claude/projects/ for matching directories
|
|
240
239
|
try {
|
|
241
240
|
if (fs.existsSync(projectsDir)) {
|
|
242
|
-
const existingDirs = fs.readdirSync(projectsDir).filter(name => {
|
|
241
|
+
const existingDirs = fs.readdirSync(projectsDir).filter((name) => {
|
|
243
242
|
const fullPath = path.join(projectsDir, name);
|
|
244
243
|
try {
|
|
245
244
|
return fs.statSync(fullPath).isDirectory();
|
|
@@ -263,9 +262,7 @@ function discoverSessionDirectory(workspacePath) {
|
|
|
263
262
|
}
|
|
264
263
|
}
|
|
265
264
|
// Fallback: match by final path component (project name)
|
|
266
|
-
const workspaceBasename = path.basename(workspacePath)
|
|
267
|
-
.replace(/_/g, '-')
|
|
268
|
-
.toLowerCase();
|
|
265
|
+
const workspaceBasename = path.basename(workspacePath).replace(/_/g, '-').toLowerCase();
|
|
269
266
|
for (const dir of existingDirs) {
|
|
270
267
|
const dirLower = dir.toLowerCase();
|
|
271
268
|
// Check if dir ends with the project name
|
|
@@ -283,7 +280,7 @@ function discoverSessionDirectory(workspacePath) {
|
|
|
283
280
|
try {
|
|
284
281
|
const claudeTempDir = path.join(os.tmpdir(), 'claude');
|
|
285
282
|
if (fs.existsSync(claudeTempDir)) {
|
|
286
|
-
const tempDirs = fs.readdirSync(claudeTempDir).filter(name => {
|
|
283
|
+
const tempDirs = fs.readdirSync(claudeTempDir).filter((name) => {
|
|
287
284
|
const fullPath = path.join(claudeTempDir, name);
|
|
288
285
|
try {
|
|
289
286
|
return fs.statSync(fullPath).isDirectory();
|
|
@@ -293,9 +290,7 @@ function discoverSessionDirectory(workspacePath) {
|
|
|
293
290
|
}
|
|
294
291
|
});
|
|
295
292
|
// Match by workspace basename
|
|
296
|
-
const workspaceBasename = path.basename(workspacePath)
|
|
297
|
-
.replace(/_/g, '-')
|
|
298
|
-
.toLowerCase();
|
|
293
|
+
const workspaceBasename = path.basename(workspacePath).replace(/_/g, '-').toLowerCase();
|
|
299
294
|
for (const encodedDir of tempDirs) {
|
|
300
295
|
const encodedLower = encodedDir.toLowerCase();
|
|
301
296
|
if (encodedLower.endsWith('-' + workspaceBasename) || encodedLower === workspaceBasename) {
|
|
@@ -342,9 +337,10 @@ function findActiveSession(workspacePath) {
|
|
|
342
337
|
}
|
|
343
338
|
const now = Date.now();
|
|
344
339
|
// Find all .jsonl files with stats
|
|
345
|
-
const files = fs
|
|
346
|
-
.
|
|
347
|
-
.
|
|
340
|
+
const files = fs
|
|
341
|
+
.readdirSync(sessionDir)
|
|
342
|
+
.filter((file) => file.endsWith('.jsonl'))
|
|
343
|
+
.map((file) => {
|
|
348
344
|
const fullPath = path.join(sessionDir, file);
|
|
349
345
|
const stats = fs.statSync(fullPath);
|
|
350
346
|
const mtime = stats.mtime.getTime();
|
|
@@ -352,11 +348,11 @@ function findActiveSession(workspacePath) {
|
|
|
352
348
|
path: fullPath,
|
|
353
349
|
mtime,
|
|
354
350
|
size: stats.size,
|
|
355
|
-
isActive:
|
|
351
|
+
isActive: now - mtime < ACTIVE_SESSION_THRESHOLD_MS,
|
|
356
352
|
};
|
|
357
353
|
})
|
|
358
354
|
// Filter out empty files
|
|
359
|
-
.filter(file => file.size > 0);
|
|
355
|
+
.filter((file) => file.size > 0);
|
|
360
356
|
// Return null if no session files
|
|
361
357
|
if (files.length === 0) {
|
|
362
358
|
return null;
|
|
@@ -405,18 +401,19 @@ function findAllSessions(workspacePath) {
|
|
|
405
401
|
return [];
|
|
406
402
|
}
|
|
407
403
|
// Find and sort all .jsonl files
|
|
408
|
-
const files = fs
|
|
409
|
-
.
|
|
410
|
-
.
|
|
404
|
+
const files = fs
|
|
405
|
+
.readdirSync(sessionDir)
|
|
406
|
+
.filter((file) => file.endsWith('.jsonl'))
|
|
407
|
+
.map((file) => {
|
|
411
408
|
const fullPath = path.join(sessionDir, file);
|
|
412
409
|
const stats = fs.statSync(fullPath);
|
|
413
410
|
return {
|
|
414
411
|
path: fullPath,
|
|
415
|
-
mtime: stats.mtime.getTime()
|
|
412
|
+
mtime: stats.mtime.getTime(),
|
|
416
413
|
};
|
|
417
414
|
})
|
|
418
415
|
.sort((a, b) => b.mtime - a.mtime)
|
|
419
|
-
.map(file => file.path);
|
|
416
|
+
.map((file) => file.path);
|
|
420
417
|
return files;
|
|
421
418
|
}
|
|
422
419
|
catch (error) {
|
|
@@ -505,8 +502,7 @@ function getAllProjectFolders(workspacePath) {
|
|
|
505
502
|
continue;
|
|
506
503
|
}
|
|
507
504
|
// Count session files and find most recent modification
|
|
508
|
-
const sessionFiles = fs.readdirSync(fullPath)
|
|
509
|
-
.filter(f => f.endsWith('.jsonl'));
|
|
505
|
+
const sessionFiles = fs.readdirSync(fullPath).filter((f) => f.endsWith('.jsonl'));
|
|
510
506
|
let lastModified = stats.mtime;
|
|
511
507
|
let sessionCount = 0;
|
|
512
508
|
for (const sessionFile of sessionFiles) {
|
|
@@ -529,7 +525,7 @@ function getAllProjectFolders(workspacePath) {
|
|
|
529
525
|
encodedName: entry,
|
|
530
526
|
name: decodeEncodedPath(entry),
|
|
531
527
|
sessionCount,
|
|
532
|
-
lastModified
|
|
528
|
+
lastModified,
|
|
533
529
|
});
|
|
534
530
|
}
|
|
535
531
|
catch {
|
|
@@ -588,16 +584,17 @@ function findSessionsInDirectory(sessionDir) {
|
|
|
588
584
|
if (!fs.existsSync(sessionDir)) {
|
|
589
585
|
return [];
|
|
590
586
|
}
|
|
591
|
-
const files = fs
|
|
592
|
-
.
|
|
593
|
-
.
|
|
587
|
+
const files = fs
|
|
588
|
+
.readdirSync(sessionDir)
|
|
589
|
+
.filter((file) => file.endsWith('.jsonl'))
|
|
590
|
+
.map((file) => {
|
|
594
591
|
const fullPath = path.join(sessionDir, file);
|
|
595
592
|
try {
|
|
596
593
|
const stats = fs.statSync(fullPath);
|
|
597
594
|
return {
|
|
598
595
|
path: fullPath,
|
|
599
596
|
mtime: stats.mtime.getTime(),
|
|
600
|
-
size: stats.size
|
|
597
|
+
size: stats.size,
|
|
601
598
|
};
|
|
602
599
|
}
|
|
603
600
|
catch {
|
|
@@ -606,7 +603,7 @@ function findSessionsInDirectory(sessionDir) {
|
|
|
606
603
|
})
|
|
607
604
|
.filter((f) => f !== null && f.size > 0)
|
|
608
605
|
.sort((a, b) => b.mtime - a.mtime)
|
|
609
|
-
.map(f => f.path);
|
|
606
|
+
.map((f) => f.path);
|
|
610
607
|
return files;
|
|
611
608
|
}
|
|
612
609
|
catch {
|
|
@@ -641,9 +638,7 @@ function resolveWorktreeMainRepo(workspacePath) {
|
|
|
641
638
|
const gitdir = match[1].trim();
|
|
642
639
|
// gitdir points to something like /main-repo/.git/worktrees/<name>
|
|
643
640
|
// Resolve to absolute path
|
|
644
|
-
const resolvedGitdir = path.isAbsolute(gitdir)
|
|
645
|
-
? gitdir
|
|
646
|
-
: path.resolve(workspacePath, gitdir);
|
|
641
|
+
const resolvedGitdir = path.isAbsolute(gitdir) ? gitdir : path.resolve(workspacePath, gitdir);
|
|
647
642
|
// Navigate up from .git/worktrees/<name> to the main repo
|
|
648
643
|
const worktreesDir = path.dirname(resolvedGitdir);
|
|
649
644
|
if (path.basename(worktreesDir) !== 'worktrees')
|
|
@@ -739,7 +734,7 @@ function findAllSessionsWithWorktrees(workspacePath) {
|
|
|
739
734
|
}
|
|
740
735
|
// Sort by mtime (most recent first)
|
|
741
736
|
return Array.from(allPaths)
|
|
742
|
-
.map(sessionPath => {
|
|
737
|
+
.map((sessionPath) => {
|
|
743
738
|
try {
|
|
744
739
|
const stat = fs.statSync(sessionPath);
|
|
745
740
|
return { path: sessionPath, mtime: stat.mtime.getTime() };
|
|
@@ -749,5 +744,5 @@ function findAllSessionsWithWorktrees(workspacePath) {
|
|
|
749
744
|
}
|
|
750
745
|
})
|
|
751
746
|
.sort((a, b) => b.mtime - a.mtime)
|
|
752
|
-
.map(f => f.path);
|
|
747
|
+
.map((f) => f.path);
|
|
753
748
|
}
|
|
@@ -60,9 +60,7 @@ const TASK_TOOLS = ['TaskCreate', 'TaskUpdate', 'TaskGet', 'TaskList'];
|
|
|
60
60
|
* Looks for "Task #N" or JSON taskId patterns in the result string.
|
|
61
61
|
*/
|
|
62
62
|
function extractTaskIdFromResult(resultContent) {
|
|
63
|
-
const resultStr = typeof resultContent === 'string'
|
|
64
|
-
? resultContent
|
|
65
|
-
: JSON.stringify(resultContent || '');
|
|
63
|
+
const resultStr = typeof resultContent === 'string' ? resultContent : JSON.stringify(resultContent || '');
|
|
66
64
|
// Try to match "Task #N" pattern
|
|
67
65
|
const taskIdMatch = resultStr.match(/Task #(\d+)/i);
|
|
68
66
|
if (taskIdMatch) {
|
|
@@ -146,7 +144,7 @@ function scanSubagentDir(sessionDir, sessionId, logger) {
|
|
|
146
144
|
function parseAgentFile(filePath, agentId, log) {
|
|
147
145
|
try {
|
|
148
146
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
149
|
-
const lines = content.split('\n').filter(line => line.trim());
|
|
147
|
+
const lines = content.split('\n').filter((line) => line.trim());
|
|
150
148
|
const toolCalls = [];
|
|
151
149
|
let agentType;
|
|
152
150
|
let description;
|
|
@@ -157,7 +155,7 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
157
155
|
// Task tracking state
|
|
158
156
|
const taskState = {
|
|
159
157
|
tasks: new Map(),
|
|
160
|
-
activeTaskId: null
|
|
158
|
+
activeTaskId: null,
|
|
161
159
|
};
|
|
162
160
|
const pendingTaskCreates = new Map();
|
|
163
161
|
for (const line of lines) {
|
|
@@ -171,7 +169,10 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
171
169
|
// Extract token usage from assistant messages
|
|
172
170
|
if (event.type === 'assistant' && event.message?.usage) {
|
|
173
171
|
const usage = event.message.usage;
|
|
174
|
-
inputTokens +=
|
|
172
|
+
inputTokens +=
|
|
173
|
+
(usage.input_tokens || 0) +
|
|
174
|
+
(usage.cache_creation_input_tokens || 0) +
|
|
175
|
+
(usage.cache_read_input_tokens || 0);
|
|
175
176
|
outputTokens += usage.output_tokens || 0;
|
|
176
177
|
}
|
|
177
178
|
// Extract agent type and description from Task tool invocation
|
|
@@ -189,24 +190,26 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
189
190
|
}
|
|
190
191
|
// Look for tool_use events in assistant messages
|
|
191
192
|
if (event.type === 'assistant' && event.message?.content) {
|
|
192
|
-
const contentArray = Array.isArray(event.message.content)
|
|
193
|
-
? event.message.content
|
|
194
|
-
: [];
|
|
193
|
+
const contentArray = Array.isArray(event.message.content) ? event.message.content : [];
|
|
195
194
|
for (const block of contentArray) {
|
|
196
195
|
if (block && typeof block === 'object' && block.type === 'tool_use') {
|
|
197
196
|
const toolUse = block;
|
|
198
197
|
const toolCall = {
|
|
199
198
|
name: toolUse.name,
|
|
200
199
|
input: toolUse.input || {},
|
|
201
|
-
timestamp: eventTimestamp
|
|
200
|
+
timestamp: eventTimestamp,
|
|
202
201
|
};
|
|
203
202
|
// Handle task tools
|
|
204
203
|
if (toolUse.name === 'TaskCreate') {
|
|
205
204
|
pendingTaskCreates.set(toolUse.id, {
|
|
206
205
|
subject: String(toolUse.input.subject || ''),
|
|
207
|
-
description: toolUse.input.description
|
|
208
|
-
|
|
209
|
-
|
|
206
|
+
description: toolUse.input.description
|
|
207
|
+
? String(toolUse.input.description)
|
|
208
|
+
: undefined,
|
|
209
|
+
activeForm: toolUse.input.activeForm
|
|
210
|
+
? String(toolUse.input.activeForm)
|
|
211
|
+
: undefined,
|
|
212
|
+
timestamp: eventTimestamp,
|
|
210
213
|
});
|
|
211
214
|
}
|
|
212
215
|
else if (toolUse.name === 'TaskUpdate') {
|
|
@@ -234,9 +237,7 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
234
237
|
}
|
|
235
238
|
// Look for tool_result events in user messages
|
|
236
239
|
if (event.type === 'user' && event.message?.content) {
|
|
237
|
-
const contentArray = Array.isArray(event.message.content)
|
|
238
|
-
? event.message.content
|
|
239
|
-
: [];
|
|
240
|
+
const contentArray = Array.isArray(event.message.content) ? event.message.content : [];
|
|
240
241
|
for (const block of contentArray) {
|
|
241
242
|
if (block && typeof block === 'object' && block.type === 'tool_result') {
|
|
242
243
|
const toolResult = block;
|
|
@@ -255,7 +256,7 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
255
256
|
activeForm: pendingCreate.activeForm,
|
|
256
257
|
blockedBy: [],
|
|
257
258
|
blocks: [],
|
|
258
|
-
associatedToolCalls: []
|
|
259
|
+
associatedToolCalls: [],
|
|
259
260
|
};
|
|
260
261
|
taskState.tasks.set(taskId, task);
|
|
261
262
|
}
|
|
@@ -282,7 +283,7 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
282
283
|
outputTokens,
|
|
283
284
|
startTime,
|
|
284
285
|
endTime,
|
|
285
|
-
durationMs
|
|
286
|
+
durationMs,
|
|
286
287
|
};
|
|
287
288
|
}
|
|
288
289
|
return null;
|
|
@@ -309,7 +310,7 @@ function handleTaskUpdate(taskState, input, timestamp) {
|
|
|
309
310
|
updatedAt: timestamp,
|
|
310
311
|
blockedBy: [],
|
|
311
312
|
blocks: [],
|
|
312
|
-
associatedToolCalls: []
|
|
313
|
+
associatedToolCalls: [],
|
|
313
314
|
};
|
|
314
315
|
taskState.tasks.set(taskId, task);
|
|
315
316
|
}
|
|
@@ -361,6 +362,6 @@ function handleTaskUpdate(taskState, input, timestamp) {
|
|
|
361
362
|
function extractTaskInfo(input) {
|
|
362
363
|
return {
|
|
363
364
|
agentType: input.subagent_type ? String(input.subagent_type) : undefined,
|
|
364
|
-
description: input.description ? String(input.description) : undefined
|
|
365
|
+
description: input.description ? String(input.description) : undefined,
|
|
365
366
|
};
|
|
366
367
|
}
|
|
@@ -86,7 +86,7 @@ function scanSubagentTraces(sessionDir, sessionId) {
|
|
|
86
86
|
// Link parent-child relationships
|
|
87
87
|
linkTraces(traces);
|
|
88
88
|
// Return only top-level traces (those without parents)
|
|
89
|
-
return traces.filter(t => !t.parentToolUseId);
|
|
89
|
+
return traces.filter((t) => !t.parentToolUseId);
|
|
90
90
|
}
|
|
91
91
|
catch {
|
|
92
92
|
return [];
|
|
@@ -96,7 +96,7 @@ function scanSubagentTraces(sessionDir, sessionId) {
|
|
|
96
96
|
function parseAgentTrace(filePath, agentId) {
|
|
97
97
|
try {
|
|
98
98
|
const content = fs.readFileSync(filePath, 'utf-8');
|
|
99
|
-
const lines = content.split('\n').filter(l => l.trim());
|
|
99
|
+
const lines = content.split('\n').filter((l) => l.trim());
|
|
100
100
|
const events = [];
|
|
101
101
|
const toolCalls = [];
|
|
102
102
|
let agentType;
|
|
@@ -155,7 +155,10 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
155
155
|
// Extract token usage
|
|
156
156
|
if (raw.type === 'assistant' && raw.message?.usage) {
|
|
157
157
|
const usage = raw.message.usage;
|
|
158
|
-
inputTokens +=
|
|
158
|
+
inputTokens +=
|
|
159
|
+
(usage.input_tokens || 0) +
|
|
160
|
+
(usage.cache_creation_input_tokens || 0) +
|
|
161
|
+
(usage.cache_read_input_tokens || 0);
|
|
159
162
|
outputTokens += usage.output_tokens || 0;
|
|
160
163
|
}
|
|
161
164
|
// Extract tool calls
|
|
@@ -258,7 +261,7 @@ function linkTraces(traces) {
|
|
|
258
261
|
}
|
|
259
262
|
// Phase 3: Positional fallback — timestamp proximity
|
|
260
263
|
// Traces without parents that fall within another trace's time window
|
|
261
|
-
const parentless = traces.filter(t => !t.parentToolUseId);
|
|
264
|
+
const parentless = traces.filter((t) => !t.parentToolUseId);
|
|
262
265
|
for (const child of parentless) {
|
|
263
266
|
if (!child.stats.startTime)
|
|
264
267
|
continue;
|