wave-agent-sdk 0.18.7 → 0.19.1
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/builtin/skills/settings/ENV.md +0 -1
- package/builtin/skills/settings/HOOKS.md +10 -0
- package/builtin/skills/settings/MODELS.md +55 -10
- package/builtin/skills/settings/SKILL.md +8 -4
- package/builtin/skills/settings/SKILLS.md +6 -4
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +12 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/managers/aiManager.d.ts.map +1 -1
- package/dist/managers/aiManager.js +20 -4
- package/dist/managers/mcpManager.d.ts.map +1 -1
- package/dist/managers/mcpManager.js +7 -2
- package/dist/managers/messageManager.d.ts +3 -5
- package/dist/managers/messageManager.d.ts.map +1 -1
- package/dist/managers/messageManager.js +17 -77
- package/dist/managers/skillManager.d.ts +1 -0
- package/dist/managers/skillManager.d.ts.map +1 -1
- package/dist/managers/skillManager.js +11 -2
- package/dist/prompts/index.d.ts +3 -2
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +5 -1
- package/dist/services/aiService.d.ts.map +1 -1
- package/dist/services/aiService.js +16 -47
- package/dist/services/configurationService.d.ts.map +1 -1
- package/dist/services/configurationService.js +25 -19
- package/dist/services/hook.d.ts.map +1 -1
- package/dist/services/hook.js +6 -0
- package/dist/services/initializationService.d.ts.map +1 -1
- package/dist/services/initializationService.js +1 -1
- package/dist/services/interactionService.js +1 -1
- package/dist/services/reversionService.d.ts +5 -1
- package/dist/services/reversionService.d.ts.map +1 -1
- package/dist/services/reversionService.js +50 -22
- package/dist/services/session.d.ts +4 -13
- package/dist/services/session.d.ts.map +1 -1
- package/dist/services/session.js +59 -215
- package/dist/telemetry/sessionTracing.d.ts +14 -4
- package/dist/telemetry/sessionTracing.d.ts.map +1 -1
- package/dist/telemetry/sessionTracing.js +134 -8
- package/dist/tools/bashTool.d.ts.map +1 -1
- package/dist/tools/bashTool.js +16 -1
- package/dist/types/config.d.ts +11 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/hooks.d.ts +1 -0
- package/dist/types/hooks.d.ts.map +1 -1
- package/dist/types/messaging.d.ts +0 -1
- package/dist/types/messaging.d.ts.map +1 -1
- package/dist/types/skills.d.ts +1 -0
- package/dist/types/skills.d.ts.map +1 -1
- package/dist/types/telemetry.d.ts +14 -1
- package/dist/types/telemetry.d.ts.map +1 -1
- package/dist/utils/atomicWrite.d.ts +15 -0
- package/dist/utils/atomicWrite.d.ts.map +1 -0
- package/dist/utils/atomicWrite.js +59 -0
- package/dist/utils/cacheControlUtils.d.ts +3 -15
- package/dist/utils/cacheControlUtils.d.ts.map +1 -1
- package/dist/utils/cacheControlUtils.js +4 -35
- package/dist/utils/containerSetup.d.ts.map +1 -1
- package/dist/utils/containerSetup.js +5 -1
- package/dist/utils/convertMessagesForAPI.d.ts +9 -1
- package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
- package/dist/utils/convertMessagesForAPI.js +74 -55
- package/dist/utils/mcpUtils.d.ts.map +1 -1
- package/dist/utils/mcpUtils.js +2 -1
- package/dist/utils/modelCapabilities.d.ts +22 -0
- package/dist/utils/modelCapabilities.d.ts.map +1 -0
- package/dist/utils/modelCapabilities.js +24 -0
- package/dist/utils/skillParser.d.ts.map +1 -1
- package/dist/utils/skillParser.js +3 -1
- package/package.json +1 -1
- package/src/agent.ts +11 -5
- package/src/index.ts +1 -0
- package/src/managers/aiManager.ts +22 -2
- package/src/managers/mcpManager.ts +8 -2
- package/src/managers/messageManager.ts +22 -104
- package/src/managers/skillManager.ts +16 -2
- package/src/prompts/index.ts +6 -1
- package/src/services/aiService.ts +15 -85
- package/src/services/configurationService.ts +27 -41
- package/src/services/hook.ts +7 -0
- package/src/services/initializationService.ts +1 -3
- package/src/services/interactionService.ts +1 -1
- package/src/services/reversionService.ts +50 -27
- package/src/services/session.ts +63 -262
- package/src/telemetry/sessionTracing.ts +167 -12
- package/src/tools/bashTool.ts +23 -1
- package/src/types/config.ts +12 -2
- package/src/types/hooks.ts +1 -0
- package/src/types/messaging.ts +0 -1
- package/src/types/skills.ts +1 -0
- package/src/types/telemetry.ts +17 -1
- package/src/utils/atomicWrite.ts +61 -0
- package/src/utils/cacheControlUtils.ts +5 -38
- package/src/utils/containerSetup.ts +5 -1
- package/src/utils/convertMessagesForAPI.ts +87 -57
- package/src/utils/mcpUtils.ts +2 -1
- package/src/utils/modelCapabilities.ts +30 -0
- package/src/utils/skillParser.ts +3 -1
package/src/services/session.ts
CHANGED
|
@@ -28,8 +28,6 @@ import { getMessageContent } from "../utils/messageOperations.js";
|
|
|
28
28
|
|
|
29
29
|
export interface SessionData {
|
|
30
30
|
id: string;
|
|
31
|
-
rootSessionId?: string;
|
|
32
|
-
parentSessionId?: string;
|
|
33
31
|
messages: Message[];
|
|
34
32
|
metadata: {
|
|
35
33
|
workdir: string;
|
|
@@ -40,8 +38,6 @@ export interface SessionData {
|
|
|
40
38
|
|
|
41
39
|
export interface SessionMetadata {
|
|
42
40
|
id: string;
|
|
43
|
-
rootSessionId?: string;
|
|
44
|
-
parentSessionId?: string;
|
|
45
41
|
sessionType: "main" | "subagent";
|
|
46
42
|
subagentType?: string;
|
|
47
43
|
workdir: string;
|
|
@@ -51,17 +47,6 @@ export interface SessionMetadata {
|
|
|
51
47
|
firstMessage?: string;
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
export interface SessionIndex {
|
|
55
|
-
sessions: Record<
|
|
56
|
-
string,
|
|
57
|
-
Omit<SessionMetadata, "id" | "lastActiveAt" | "createdAt"> & {
|
|
58
|
-
lastActiveAt: string;
|
|
59
|
-
createdAt: string;
|
|
60
|
-
}
|
|
61
|
-
>;
|
|
62
|
-
lastUpdated: string;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
50
|
/**
|
|
66
51
|
* Generate a new session ID using Node.js native crypto.randomUUID()
|
|
67
52
|
* @returns UUID string for session identification
|
|
@@ -82,40 +67,6 @@ export function generateSubagentFilename(sessionId: string): string {
|
|
|
82
67
|
// Constants
|
|
83
68
|
export const SESSION_DIR = join(homedir(), ".wave", "projects");
|
|
84
69
|
const MAX_SESSION_AGE_DAYS = 14;
|
|
85
|
-
const SESSION_INDEX_FILENAME = "sessions-index.json";
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Update the session index for a project directory
|
|
89
|
-
*/
|
|
90
|
-
async function updateSessionIndex(
|
|
91
|
-
projectDirPath: string,
|
|
92
|
-
metadata: SessionMetadata,
|
|
93
|
-
): Promise<void> {
|
|
94
|
-
const indexPath = join(projectDirPath, SESSION_INDEX_FILENAME);
|
|
95
|
-
let index: SessionIndex = {
|
|
96
|
-
sessions: {},
|
|
97
|
-
lastUpdated: new Date().toISOString(),
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const content = await fs.readFile(indexPath, "utf8");
|
|
102
|
-
index = JSON.parse(content);
|
|
103
|
-
} catch {
|
|
104
|
-
// Index doesn't exist or is invalid, start fresh
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const { id, ...rest } = metadata;
|
|
108
|
-
index.sessions[id] = {
|
|
109
|
-
...rest,
|
|
110
|
-
createdAt: metadata.createdAt.toISOString(),
|
|
111
|
-
lastActiveAt: metadata.lastActiveAt.toISOString(),
|
|
112
|
-
firstMessage: metadata.firstMessage || index.sessions[id]?.firstMessage,
|
|
113
|
-
parentSessionId: metadata.parentSessionId,
|
|
114
|
-
};
|
|
115
|
-
index.lastUpdated = new Date().toISOString();
|
|
116
|
-
|
|
117
|
-
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf8");
|
|
118
|
-
}
|
|
119
70
|
|
|
120
71
|
/**
|
|
121
72
|
* Ensure session directory exists
|
|
@@ -201,8 +152,6 @@ export async function appendMessages(
|
|
|
201
152
|
newMessages: Message[],
|
|
202
153
|
workdir: string,
|
|
203
154
|
sessionType: "main" | "subagent" = "main",
|
|
204
|
-
rootSessionId?: string,
|
|
205
|
-
parentSessionId?: string,
|
|
206
155
|
): Promise<void> {
|
|
207
156
|
// Do not save session files in test environment
|
|
208
157
|
if (process.env.NODE_ENV === "test") {
|
|
@@ -235,50 +184,6 @@ export async function appendMessages(
|
|
|
235
184
|
await jsonlHandler.append(filePath, newMessages, {
|
|
236
185
|
atomic: false,
|
|
237
186
|
});
|
|
238
|
-
|
|
239
|
-
// Update index
|
|
240
|
-
const encoder = new PathEncoder();
|
|
241
|
-
const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
|
|
242
|
-
const lastMessage = newMessages[newMessages.length - 1];
|
|
243
|
-
|
|
244
|
-
// Get first message content and createdAt from existing index
|
|
245
|
-
let firstMessage: string | undefined;
|
|
246
|
-
let createdAt: Date | undefined;
|
|
247
|
-
try {
|
|
248
|
-
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
249
|
-
const content = await fs.readFile(indexPath, "utf8");
|
|
250
|
-
const index = JSON.parse(content) as SessionIndex;
|
|
251
|
-
if (!index.sessions[sessionId]?.firstMessage) {
|
|
252
|
-
firstMessage =
|
|
253
|
-
(await getFirstMessageContent(sessionId, workdir)) || undefined;
|
|
254
|
-
}
|
|
255
|
-
if (index.sessions[sessionId]?.createdAt) {
|
|
256
|
-
createdAt = new Date(index.sessions[sessionId].createdAt);
|
|
257
|
-
}
|
|
258
|
-
} catch {
|
|
259
|
-
// If index doesn't exist, this might be the first message
|
|
260
|
-
firstMessage =
|
|
261
|
-
(await getFirstMessageContent(sessionId, workdir)) || undefined;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
// Derive createdAt from session ID if not in index
|
|
265
|
-
if (!createdAt) {
|
|
266
|
-
createdAt = new Date();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
await updateSessionIndex(projectDir.encodedPath, {
|
|
270
|
-
id: sessionId,
|
|
271
|
-
rootSessionId,
|
|
272
|
-
parentSessionId,
|
|
273
|
-
sessionType,
|
|
274
|
-
workdir,
|
|
275
|
-
createdAt,
|
|
276
|
-
lastActiveAt: new Date(lastMessage.timestamp),
|
|
277
|
-
latestTotalTokens: lastMessage.usage
|
|
278
|
-
? extractLatestTotalTokens([lastMessage])
|
|
279
|
-
: 0,
|
|
280
|
-
firstMessage,
|
|
281
|
-
});
|
|
282
187
|
}
|
|
283
188
|
|
|
284
189
|
/**
|
|
@@ -314,34 +219,25 @@ export async function loadSessionFromJsonl(
|
|
|
314
219
|
throw error;
|
|
315
220
|
}
|
|
316
221
|
|
|
317
|
-
const
|
|
222
|
+
const allMessages = await jsonlHandler.read(filePath);
|
|
223
|
+
|
|
224
|
+
// Find the last compact boundary — only return messages from there forward
|
|
225
|
+
let lastCompactIndex = -1;
|
|
226
|
+
for (let i = allMessages.length - 1; i >= 0; i--) {
|
|
227
|
+
if (allMessages[i].blocks?.some((b) => b.type === "compact")) {
|
|
228
|
+
lastCompactIndex = i;
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const messages =
|
|
233
|
+
lastCompactIndex >= 0 ? allMessages.slice(lastCompactIndex) : allMessages;
|
|
318
234
|
|
|
319
235
|
// Extract metadata from messages
|
|
320
236
|
const lastMessage =
|
|
321
237
|
messages.length > 0 ? messages[messages.length - 1] : null;
|
|
322
238
|
|
|
323
|
-
// Try to get rootSessionId and parentSessionId from index
|
|
324
|
-
let rootSessionId: string | undefined;
|
|
325
|
-
let parentSessionId: string | undefined;
|
|
326
|
-
try {
|
|
327
|
-
const encoder = new PathEncoder();
|
|
328
|
-
const projectDir = await encoder.getProjectDirectory(
|
|
329
|
-
workdir,
|
|
330
|
-
SESSION_DIR,
|
|
331
|
-
);
|
|
332
|
-
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
333
|
-
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
334
|
-
const index = JSON.parse(indexContent) as SessionIndex;
|
|
335
|
-
rootSessionId = index.sessions[sessionId]?.rootSessionId;
|
|
336
|
-
parentSessionId = index.sessions[sessionId]?.parentSessionId;
|
|
337
|
-
} catch {
|
|
338
|
-
// Ignore index errors
|
|
339
|
-
}
|
|
340
|
-
|
|
341
239
|
const sessionData: SessionData = {
|
|
342
240
|
id: sessionId,
|
|
343
|
-
rootSessionId: rootSessionId || sessionId,
|
|
344
|
-
parentSessionId,
|
|
345
241
|
messages,
|
|
346
242
|
metadata: {
|
|
347
243
|
workdir,
|
|
@@ -435,33 +331,9 @@ export async function listSessionsFromJsonl(
|
|
|
435
331
|
|
|
436
332
|
const projectDir = await encoder.getProjectDirectory(workdir, baseDir);
|
|
437
333
|
|
|
438
|
-
// Try to read from index first
|
|
439
|
-
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
440
334
|
const sevenDaysAgo = new Date();
|
|
441
335
|
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
|
|
442
336
|
|
|
443
|
-
try {
|
|
444
|
-
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
445
|
-
const index = JSON.parse(indexContent) as SessionIndex;
|
|
446
|
-
const sessions: SessionMetadata[] = Object.entries(index.sessions)
|
|
447
|
-
.filter(([, meta]) => {
|
|
448
|
-
const lastActiveAt = new Date(meta.lastActiveAt);
|
|
449
|
-
return meta.sessionType === "main" && lastActiveAt >= sevenDaysAgo;
|
|
450
|
-
})
|
|
451
|
-
.map(([id, meta]) => ({
|
|
452
|
-
id,
|
|
453
|
-
...meta,
|
|
454
|
-
createdAt: new Date(meta.createdAt),
|
|
455
|
-
lastActiveAt: new Date(meta.lastActiveAt),
|
|
456
|
-
}));
|
|
457
|
-
|
|
458
|
-
return sessions.sort(
|
|
459
|
-
(a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
|
|
460
|
-
);
|
|
461
|
-
} catch {
|
|
462
|
-
// Fallback to manual listing if index fails
|
|
463
|
-
}
|
|
464
|
-
|
|
465
337
|
let files: string[];
|
|
466
338
|
try {
|
|
467
339
|
files = await fs.readdir(projectDir.encodedPath);
|
|
@@ -521,7 +393,7 @@ export async function listSessionsFromJsonl(
|
|
|
521
393
|
const sessionMeta: SessionMetadata = {
|
|
522
394
|
id: sessionId,
|
|
523
395
|
sessionType: "main",
|
|
524
|
-
subagentType: undefined,
|
|
396
|
+
subagentType: undefined,
|
|
525
397
|
workdir: projectDir.originalPath,
|
|
526
398
|
createdAt: new Date(),
|
|
527
399
|
lastActiveAt,
|
|
@@ -530,7 +402,7 @@ export async function listSessionsFromJsonl(
|
|
|
530
402
|
: 0,
|
|
531
403
|
};
|
|
532
404
|
|
|
533
|
-
// Try to get first message content for
|
|
405
|
+
// Try to get first message content for display
|
|
534
406
|
try {
|
|
535
407
|
const firstContent = await getFirstMessageContent(sessionId, workdir);
|
|
536
408
|
if (firstContent) {
|
|
@@ -548,30 +420,9 @@ export async function listSessionsFromJsonl(
|
|
|
548
420
|
}
|
|
549
421
|
|
|
550
422
|
// Sort by last active time (most recently active first)
|
|
551
|
-
|
|
423
|
+
return sessions.sort(
|
|
552
424
|
(a, b) => b.lastActiveAt.getTime() - a.lastActiveAt.getTime(),
|
|
553
425
|
);
|
|
554
|
-
|
|
555
|
-
// Rebuild index if we had to fall back
|
|
556
|
-
try {
|
|
557
|
-
const index: SessionIndex = {
|
|
558
|
-
sessions: {},
|
|
559
|
-
lastUpdated: new Date().toISOString(),
|
|
560
|
-
};
|
|
561
|
-
for (const session of sessions) {
|
|
562
|
-
const { id, ...rest } = session;
|
|
563
|
-
index.sessions[id] = {
|
|
564
|
-
...rest,
|
|
565
|
-
createdAt: session.createdAt.toISOString(),
|
|
566
|
-
lastActiveAt: session.lastActiveAt.toISOString(),
|
|
567
|
-
};
|
|
568
|
-
}
|
|
569
|
-
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf8");
|
|
570
|
-
} catch (error) {
|
|
571
|
-
logger.warn(`Failed to rebuild session index for ${workdir}:`, error);
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
return sortedSessions;
|
|
575
426
|
} catch (error) {
|
|
576
427
|
throw new Error(`Failed to list sessions: ${error}`);
|
|
577
428
|
}
|
|
@@ -607,28 +458,52 @@ export async function listAllSessions(): Promise<SessionMetadata[]> {
|
|
|
607
458
|
continue;
|
|
608
459
|
}
|
|
609
460
|
|
|
610
|
-
//
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
461
|
+
// Scan .jsonl files in the project directory
|
|
462
|
+
const files = await fs.readdir(projectPath);
|
|
463
|
+
|
|
464
|
+
for (const file of files) {
|
|
465
|
+
if (!file.endsWith(".jsonl") || file.startsWith("subagent-")) {
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
try {
|
|
470
|
+
const filePath = join(projectPath, file);
|
|
471
|
+
const uuidMatch = file.match(
|
|
472
|
+
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
473
|
+
);
|
|
474
|
+
if (!uuidMatch) continue;
|
|
475
|
+
|
|
476
|
+
const sessionId = uuidMatch[1];
|
|
477
|
+
const jsonlHandler = new JsonlHandler();
|
|
478
|
+
const lastMessage = await jsonlHandler.getLastMessage(filePath);
|
|
479
|
+
|
|
480
|
+
let lastActiveAt: Date;
|
|
481
|
+
if (lastMessage) {
|
|
482
|
+
lastActiveAt = new Date(lastMessage.timestamp);
|
|
483
|
+
} else {
|
|
484
|
+
const stats = await fs.stat(filePath);
|
|
485
|
+
lastActiveAt = stats.mtime;
|
|
624
486
|
}
|
|
487
|
+
|
|
488
|
+
if (lastActiveAt < sevenDaysAgo) continue;
|
|
489
|
+
|
|
490
|
+
allSessions.push({
|
|
491
|
+
id: sessionId,
|
|
492
|
+
sessionType: "main" as const,
|
|
493
|
+
subagentType: undefined,
|
|
494
|
+
workdir: projectDirName,
|
|
495
|
+
createdAt: new Date(),
|
|
496
|
+
lastActiveAt,
|
|
497
|
+
latestTotalTokens: lastMessage?.usage
|
|
498
|
+
? extractLatestTotalTokens([lastMessage])
|
|
499
|
+
: 0,
|
|
500
|
+
});
|
|
501
|
+
} catch {
|
|
502
|
+
continue;
|
|
625
503
|
}
|
|
626
|
-
} catch {
|
|
627
|
-
// If index fails, we skip this project directory
|
|
628
|
-
// In the future, we could scan for .jsonl files here
|
|
629
504
|
}
|
|
630
505
|
} catch {
|
|
631
|
-
// Skip if stat fails
|
|
506
|
+
// Skip if stat/readdir fails
|
|
632
507
|
}
|
|
633
508
|
}
|
|
634
509
|
|
|
@@ -677,38 +552,6 @@ export async function cleanupExpiredSessionsFromJsonl(
|
|
|
677
552
|
if (fileAge > maxAge) {
|
|
678
553
|
await fs.unlink(filePath);
|
|
679
554
|
deletedCount++;
|
|
680
|
-
|
|
681
|
-
// Remove from index if it exists
|
|
682
|
-
try {
|
|
683
|
-
const indexPath = join(
|
|
684
|
-
projectDir.encodedPath,
|
|
685
|
-
SESSION_INDEX_FILENAME,
|
|
686
|
-
);
|
|
687
|
-
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
688
|
-
const index = JSON.parse(indexContent) as SessionIndex;
|
|
689
|
-
const uuidMatch = file.match(
|
|
690
|
-
/^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
691
|
-
);
|
|
692
|
-
const subagentMatch = file.match(
|
|
693
|
-
/^subagent-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/,
|
|
694
|
-
);
|
|
695
|
-
const sessionId = uuidMatch
|
|
696
|
-
? uuidMatch[1]
|
|
697
|
-
: subagentMatch
|
|
698
|
-
? subagentMatch[1]
|
|
699
|
-
: null;
|
|
700
|
-
|
|
701
|
-
if (sessionId && index.sessions[sessionId]) {
|
|
702
|
-
delete index.sessions[sessionId];
|
|
703
|
-
await fs.writeFile(
|
|
704
|
-
indexPath,
|
|
705
|
-
JSON.stringify(index, null, 2),
|
|
706
|
-
"utf8",
|
|
707
|
-
);
|
|
708
|
-
}
|
|
709
|
-
} catch {
|
|
710
|
-
// Ignore index update errors during cleanup
|
|
711
|
-
}
|
|
712
555
|
}
|
|
713
556
|
} catch {
|
|
714
557
|
// Skip failed operations and continue processing other files
|
|
@@ -897,21 +740,6 @@ export async function deleteSession(
|
|
|
897
740
|
throw error;
|
|
898
741
|
}
|
|
899
742
|
}
|
|
900
|
-
|
|
901
|
-
// Also remove from index
|
|
902
|
-
try {
|
|
903
|
-
const encoder = new PathEncoder();
|
|
904
|
-
const projectDir = await encoder.getProjectDirectory(workdir, SESSION_DIR);
|
|
905
|
-
const indexPath = join(projectDir.encodedPath, SESSION_INDEX_FILENAME);
|
|
906
|
-
const indexContent = await fs.readFile(indexPath, "utf8");
|
|
907
|
-
const index = JSON.parse(indexContent) as SessionIndex;
|
|
908
|
-
if (index.sessions[sessionId]) {
|
|
909
|
-
delete index.sessions[sessionId];
|
|
910
|
-
await fs.writeFile(indexPath, JSON.stringify(index, null, 2), "utf8");
|
|
911
|
-
}
|
|
912
|
-
} catch {
|
|
913
|
-
// Ignore index errors
|
|
914
|
-
}
|
|
915
743
|
}
|
|
916
744
|
|
|
917
745
|
/**
|
|
@@ -995,7 +823,9 @@ export async function handleSessionRestoration(
|
|
|
995
823
|
}
|
|
996
824
|
|
|
997
825
|
/**
|
|
998
|
-
* Load the full message thread
|
|
826
|
+
* Load the full message thread for a session.
|
|
827
|
+
* With append-only compaction, all messages are in a single file.
|
|
828
|
+
* Returns the active messages (post-compact boundary).
|
|
999
829
|
* @param currentSessionId - The ID of the current session
|
|
1000
830
|
* @param workdir - Working directory for the session
|
|
1001
831
|
* @returns Promise that resolves to an array of all messages in the thread
|
|
@@ -1004,36 +834,7 @@ export async function loadFullMessageThread(
|
|
|
1004
834
|
currentSessionId: string,
|
|
1005
835
|
workdir: string,
|
|
1006
836
|
): Promise<{ messages: Message[]; sessionIds: string[] }> {
|
|
1007
|
-
const
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
while (currentId) {
|
|
1012
|
-
const sessionData = await loadSessionFromJsonl(currentId, workdir);
|
|
1013
|
-
if (!sessionData) break;
|
|
1014
|
-
|
|
1015
|
-
sessionIds.unshift(currentId);
|
|
1016
|
-
// Add messages from this session to the beginning of the list
|
|
1017
|
-
// But skip the "compact" block if it's not the first session in our traversal (which is the latest)
|
|
1018
|
-
// Actually, we should probably keep all messages and let the UI/logic handle it.
|
|
1019
|
-
// But wait, if we are concatenating, the "compact" block in session N summarizes session N-1.
|
|
1020
|
-
// So if we have session N-1 and session N, we should probably skip the compact block in session N.
|
|
1021
|
-
|
|
1022
|
-
const messages = sessionData.messages;
|
|
1023
|
-
if (allMessages.length > 0) {
|
|
1024
|
-
// If we already have messages (from "later" sessions),
|
|
1025
|
-
// we are now adding messages from an "earlier" session.
|
|
1026
|
-
// The later session's first message might be a "compact" block.
|
|
1027
|
-
if (allMessages[0].blocks.some((b) => b.type === "compact")) {
|
|
1028
|
-
// Remove the compact block from the later session's messages
|
|
1029
|
-
// because we are now providing the actual messages it summarized.
|
|
1030
|
-
allMessages.shift();
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
|
|
1034
|
-
allMessages.unshift(...messages);
|
|
1035
|
-
currentId = sessionData.parentSessionId;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
return { messages: allMessages, sessionIds };
|
|
837
|
+
const sessionData = await loadSessionFromJsonl(currentSessionId, workdir);
|
|
838
|
+
if (!sessionData) return { messages: [], sessionIds: [] };
|
|
839
|
+
return { messages: sessionData.messages, sessionIds: [currentSessionId] };
|
|
1039
840
|
}
|
|
@@ -11,19 +11,55 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
14
15
|
import type { Span } from "@opentelemetry/api";
|
|
15
16
|
import {
|
|
16
17
|
getOTELApi,
|
|
17
18
|
isInitialized,
|
|
18
19
|
getCurrentConfig,
|
|
19
20
|
} from "./instrumentation.js";
|
|
20
|
-
import
|
|
21
|
+
import { logOTelEvent } from "./events.js";
|
|
22
|
+
import type {
|
|
23
|
+
LLMRequestMetadata,
|
|
24
|
+
LLMRequestInput,
|
|
25
|
+
ToolMetadata,
|
|
26
|
+
} from "../types/telemetry.js";
|
|
27
|
+
|
|
28
|
+
// -- Constants --
|
|
29
|
+
|
|
30
|
+
/** Max content length for span attributes (60KB, aligning with Claude Code) */
|
|
31
|
+
const MAX_CONTENT_LENGTH = 60000;
|
|
21
32
|
|
|
22
33
|
// -- AsyncLocalStorage for context propagation --
|
|
23
34
|
|
|
24
35
|
const interactionContext = new AsyncLocalStorage<Span | undefined>();
|
|
25
36
|
const toolContext = new AsyncLocalStorage<Span | undefined>();
|
|
26
37
|
|
|
38
|
+
// -- Incremental tracking state (module-level, per session) --
|
|
39
|
+
|
|
40
|
+
/** Number of messages already reported in new_context; only deltas are sent */
|
|
41
|
+
let lastReportedMessageCount = 0;
|
|
42
|
+
/** Set of system prompt hashes already emitted via logOTelEvent */
|
|
43
|
+
const seenSystemPromptHashes = new Set<string>();
|
|
44
|
+
/** Set of tool schema hashes already emitted via logOTelEvent */
|
|
45
|
+
const seenToolSchemaHashes = new Set<string>();
|
|
46
|
+
|
|
47
|
+
// -- Helpers --
|
|
48
|
+
|
|
49
|
+
/** Computes SHA-256 hash of content, returns first 12 hex characters */
|
|
50
|
+
function hashContent(content: string): string {
|
|
51
|
+
return createHash("sha256").update(content).digest("hex").substring(0, 12);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Truncates content to MAX_CONTENT_LENGTH, returns [truncated, originalLength] */
|
|
55
|
+
function truncateContent(content: string): [string, number] {
|
|
56
|
+
const originalLength = content.length;
|
|
57
|
+
if (originalLength > MAX_CONTENT_LENGTH) {
|
|
58
|
+
return [content.substring(0, MAX_CONTENT_LENGTH), originalLength];
|
|
59
|
+
}
|
|
60
|
+
return [content, originalLength];
|
|
61
|
+
}
|
|
62
|
+
|
|
27
63
|
// -- Tracer accessor --
|
|
28
64
|
|
|
29
65
|
function getTracer() {
|
|
@@ -71,18 +107,33 @@ export function endInteractionSpan(): void {
|
|
|
71
107
|
interactionContext.enterWith(undefined);
|
|
72
108
|
}
|
|
73
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Resets incremental tracing state. Should be called after compaction to
|
|
112
|
+
* prevent incorrect delta calculations.
|
|
113
|
+
*/
|
|
114
|
+
export function resetTracingState(): void {
|
|
115
|
+
lastReportedMessageCount = 0;
|
|
116
|
+
seenSystemPromptHashes.clear();
|
|
117
|
+
seenToolSchemaHashes.clear();
|
|
118
|
+
}
|
|
119
|
+
|
|
74
120
|
/**
|
|
75
121
|
* Creates an LLM request span as a child of the interaction span.
|
|
76
122
|
* Does NOT enter any ALS — the span must be passed explicitly to endLLMRequestSpan.
|
|
123
|
+
*
|
|
124
|
+
* When logToolContent is enabled, captures:
|
|
125
|
+
* - system_prompt_hash / system_prompt_preview / system_prompt_length
|
|
126
|
+
* - new_context (incremental messages delta)
|
|
127
|
+
* - tools (JSON array of {name, hash}) / tools_count
|
|
77
128
|
*/
|
|
78
129
|
export function startLLMRequestSpan(
|
|
79
130
|
model: string,
|
|
80
|
-
options?:
|
|
131
|
+
options?: LLMRequestInput,
|
|
81
132
|
): Span | undefined {
|
|
82
133
|
const tracer = getTracer();
|
|
83
134
|
if (!tracer) return undefined;
|
|
84
135
|
|
|
85
|
-
const attributes: Record<string, string> = {
|
|
136
|
+
const attributes: Record<string, string | number | boolean> = {
|
|
86
137
|
"span.type": "llm_request",
|
|
87
138
|
model,
|
|
88
139
|
};
|
|
@@ -90,6 +141,91 @@ export function startLLMRequestSpan(
|
|
|
90
141
|
attributes["llm_request.context"] = options.context;
|
|
91
142
|
}
|
|
92
143
|
|
|
144
|
+
const config = getCurrentConfig();
|
|
145
|
+
|
|
146
|
+
// Content capture gated by logToolContent
|
|
147
|
+
if (config?.logToolContent) {
|
|
148
|
+
// System prompt: hash + preview + length
|
|
149
|
+
if (options?.systemPrompt) {
|
|
150
|
+
const hash = hashContent(options.systemPrompt);
|
|
151
|
+
attributes.system_prompt_hash = hash;
|
|
152
|
+
attributes.system_prompt_length = options.systemPrompt.length;
|
|
153
|
+
const preview =
|
|
154
|
+
options.systemPrompt.length > 500
|
|
155
|
+
? options.systemPrompt.substring(0, 500)
|
|
156
|
+
: options.systemPrompt;
|
|
157
|
+
attributes.system_prompt_preview = preview;
|
|
158
|
+
|
|
159
|
+
// Emit full system prompt via event only once per hash
|
|
160
|
+
if (!seenSystemPromptHashes.has(hash)) {
|
|
161
|
+
seenSystemPromptHashes.add(hash);
|
|
162
|
+
logOTelEvent("system_prompt", {
|
|
163
|
+
system_prompt_hash: hash,
|
|
164
|
+
system_prompt: options.systemPrompt,
|
|
165
|
+
}).catch(() => {});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Incremental input messages: only send new messages since last report
|
|
170
|
+
if (options?.inputMessages && options.inputMessages.length > 0) {
|
|
171
|
+
const totalCount = options.inputMessages.length;
|
|
172
|
+
const deltaCount = totalCount - lastReportedMessageCount;
|
|
173
|
+
if (deltaCount > 0) {
|
|
174
|
+
const newMessages = options.inputMessages.slice(
|
|
175
|
+
lastReportedMessageCount,
|
|
176
|
+
);
|
|
177
|
+
const newContextStr = JSON.stringify(newMessages);
|
|
178
|
+
const [truncated, originalLength] = truncateContent(newContextStr);
|
|
179
|
+
attributes.new_context = truncated;
|
|
180
|
+
attributes.new_context_message_count = deltaCount;
|
|
181
|
+
if (originalLength > MAX_CONTENT_LENGTH) {
|
|
182
|
+
attributes.new_context_truncated = true;
|
|
183
|
+
attributes.new_context_original_length = originalLength;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
lastReportedMessageCount = totalCount;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Tool schemas: hash per tool + count
|
|
190
|
+
if (options?.toolsSchema) {
|
|
191
|
+
try {
|
|
192
|
+
const tools = JSON.parse(options.toolsSchema);
|
|
193
|
+
if (Array.isArray(tools)) {
|
|
194
|
+
const toolHashes = tools.map(
|
|
195
|
+
(tool: { function?: { name?: string } }) => {
|
|
196
|
+
const name = tool.function?.name || "unknown";
|
|
197
|
+
const toolStr = JSON.stringify(tool);
|
|
198
|
+
const hash = hashContent(toolStr);
|
|
199
|
+
return { name, hash };
|
|
200
|
+
},
|
|
201
|
+
);
|
|
202
|
+
attributes.tools = JSON.stringify(toolHashes);
|
|
203
|
+
attributes.tools_count = toolHashes.length;
|
|
204
|
+
|
|
205
|
+
// Emit full tool schema via event only once per hash
|
|
206
|
+
for (const { name, hash } of toolHashes) {
|
|
207
|
+
if (!seenToolSchemaHashes.has(hash)) {
|
|
208
|
+
seenToolSchemaHashes.add(hash);
|
|
209
|
+
const fullTool = tools.find(
|
|
210
|
+
(t: { function?: { name?: string } }) =>
|
|
211
|
+
t.function?.name === name,
|
|
212
|
+
);
|
|
213
|
+
if (fullTool) {
|
|
214
|
+
logOTelEvent("tool_schema", {
|
|
215
|
+
tool_name: name,
|
|
216
|
+
tool_hash: hash,
|
|
217
|
+
tool_schema: JSON.stringify(fullTool),
|
|
218
|
+
}).catch(() => {});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} catch {
|
|
224
|
+
// toolsSchema is not valid JSON — skip
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
93
229
|
const parent = interactionContext.getStore();
|
|
94
230
|
let span: Span;
|
|
95
231
|
if (parent) {
|
|
@@ -105,6 +241,8 @@ export function startLLMRequestSpan(
|
|
|
105
241
|
/**
|
|
106
242
|
* Ends an LLM request span with response metadata.
|
|
107
243
|
* The span is passed explicitly — no ALS is read or modified.
|
|
244
|
+
*
|
|
245
|
+
* When logToolContent is enabled, captures response.model_output.
|
|
108
246
|
*/
|
|
109
247
|
export function endLLMRequestSpan(
|
|
110
248
|
span: Span | undefined,
|
|
@@ -127,6 +265,20 @@ export function endLLMRequestSpan(
|
|
|
127
265
|
if (metadata.hasToolCall != null)
|
|
128
266
|
span.setAttribute("has_tool_call", metadata.hasToolCall);
|
|
129
267
|
|
|
268
|
+
// Content capture gated by logToolContent
|
|
269
|
+
const config = getCurrentConfig();
|
|
270
|
+
if (config?.logToolContent && metadata.modelOutput) {
|
|
271
|
+
const [truncated, originalLength] = truncateContent(metadata.modelOutput);
|
|
272
|
+
span.setAttribute("response.model_output", truncated);
|
|
273
|
+
if (originalLength > MAX_CONTENT_LENGTH) {
|
|
274
|
+
span.setAttribute("response.model_output_truncated", true);
|
|
275
|
+
span.setAttribute(
|
|
276
|
+
"response.model_output_original_length",
|
|
277
|
+
originalLength,
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
130
282
|
span.end();
|
|
131
283
|
}
|
|
132
284
|
|
|
@@ -142,16 +294,18 @@ export function startToolSpan(
|
|
|
142
294
|
if (!tracer) return undefined;
|
|
143
295
|
|
|
144
296
|
const config = getCurrentConfig();
|
|
145
|
-
const attributes: Record<string, string | number> = {
|
|
297
|
+
const attributes: Record<string, string | number | boolean> = {
|
|
146
298
|
"span.type": "tool",
|
|
147
299
|
tool_name: toolName,
|
|
148
300
|
};
|
|
149
301
|
if (config?.logToolContent && input !== undefined) {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
302
|
+
const inputStr = typeof input === "string" ? input : JSON.stringify(input);
|
|
303
|
+
const [truncated, originalLength] = truncateContent(inputStr);
|
|
304
|
+
attributes.tool_input = truncated;
|
|
305
|
+
if (originalLength > MAX_CONTENT_LENGTH) {
|
|
306
|
+
attributes.tool_input_truncated = true;
|
|
307
|
+
attributes.tool_input_original_length = originalLength;
|
|
153
308
|
}
|
|
154
|
-
attributes.tool_input = inputStr;
|
|
155
309
|
}
|
|
156
310
|
|
|
157
311
|
const parent = interactionContext.getStore();
|
|
@@ -181,11 +335,12 @@ export function endToolSpan(metadata: ToolMetadata): void {
|
|
|
181
335
|
|
|
182
336
|
const config = getCurrentConfig();
|
|
183
337
|
if (config?.logToolContent && metadata.output) {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
338
|
+
const [truncated, originalLength] = truncateContent(metadata.output);
|
|
339
|
+
span.setAttribute("tool_output", truncated);
|
|
340
|
+
if (originalLength > MAX_CONTENT_LENGTH) {
|
|
341
|
+
span.setAttribute("tool_output_truncated", true);
|
|
342
|
+
span.setAttribute("tool_output_original_length", originalLength);
|
|
187
343
|
}
|
|
188
|
-
span.setAttribute("tool_output", outputStr);
|
|
189
344
|
}
|
|
190
345
|
|
|
191
346
|
span.end();
|