Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Commit bf7c8bd

Browse files
authored
Merge pull request #271 from closedloop-ai/feat/fea-1554
FEA-1554: Close session data collection gaps across all 5 harness parsers
2 parents 09cf797 + 1740b9c commit bf7c8bd

18 files changed

Lines changed: 2191 additions & 103 deletions

apps/desktop/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "desktop",
3-
"version": "0.15.114",
3+
"version": "0.15.115",
44
"description": "ClosedLoop Desktop",
55
"author": "ClosedLoop AI <support@closedloop.ai>",
66
"private": true,

apps/desktop/src/main/agent-session-sync-service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,11 @@ export function sanitizeSessionForSync(
833833
};
834834
}
835835

836-
const STRIPPED_LEAF_KEYS = new Set(["prompt", "content", "stdout", "stderr"]);
836+
const STRIPPED_LEAF_KEYS = new Set([
837+
"prompt", "content", "stdout", "stderr",
838+
"text", "output", "reasoning",
839+
"old_string", "new_string", "patch", "command", "arguments",
840+
]);
837841

838842
function stripDataContent(data: SyncJsonValue | undefined): SyncJsonValue | undefined {
839843
if (data === undefined || data === null) {

apps/desktop/src/main/collectors/claude/claude-parser.ts

Lines changed: 171 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,16 @@ import path from "node:path";
1212
import readline from "node:readline";
1313
import type {
1414
NormalizedApiError,
15+
NormalizedDiffStats,
16+
NormalizedMessage,
1517
NormalizedSession,
1618
NormalizedTokenCounts,
19+
NormalizedTokenRecord,
1720
NormalizedToolResultError,
1821
NormalizedToolUse,
1922
NormalizedTurnDuration,
2023
} from "../types.js";
24+
import { truncateText, computeLineDelta, collectArtifacts } from "../parser-utils.js";
2125

2226
/** Mirror the vendor's lenient timestamp handling: epoch number → ISO, string as-is. */
2327
function isoTs(ts: unknown): string | null {
@@ -72,6 +76,19 @@ export async function parseSessionFile(filePath: string): Promise<NormalizedSess
7276
const speeds = new Set<string>();
7377
const inferenceGeos = new Set<string>();
7478

79+
// CR-1: ordered messages
80+
const messages: NormalizedMessage[] = [];
81+
// CR-2: per-turn token time-series
82+
const tokenSeries: NormalizedTokenRecord[] = [];
83+
// CR-4: aggregate diff stats
84+
let totalAdded = 0;
85+
let totalRemoved = 0;
86+
const diffFiles = new Set<string>();
87+
// CR-7: slash commands
88+
const slashCommands: Array<{ name: string; timestamp: string }> = [];
89+
// CR-3: map tool_use_id → index in toolUses for back-linking tool results
90+
const toolUseIdIndex = new Map<string, number>();
91+
7592
try {
7693
for await (const line of rl) {
7794
if (!line.trim()) continue;
@@ -142,6 +159,52 @@ export async function parseSessionFile(filePath: string): Promise<NormalizedSess
142159

143160
if (entry.type === "user") {
144161
userMessageCount++;
162+
163+
// CR-1: Build NormalizedMessage for user messages.
164+
const userMsg = asRecord(entry.message);
165+
const userContent = Array.isArray(userMsg.content) ? userMsg.content : [];
166+
const userTextParts: string[] = [];
167+
for (const raw of userContent) {
168+
const block = asRecord(raw);
169+
if (block.type === "text" && typeof block.text === "string") {
170+
userTextParts.push(block.text);
171+
}
172+
// CR-3: Capture tool_result content and back-link to the originating tool_use.
173+
if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
174+
const resultContent = Array.isArray(block.content) ? block.content : [];
175+
const resultTextParts: string[] = [];
176+
for (const rc of resultContent) {
177+
const rcBlock = asRecord(rc);
178+
if (typeof rcBlock.text === "string") resultTextParts.push(rcBlock.text);
179+
}
180+
// Also handle string content directly
181+
if (typeof block.content === "string") resultTextParts.push(block.content);
182+
const resultText = resultTextParts.join("\n");
183+
const tuIdx = toolUseIdIndex.get(block.tool_use_id as string);
184+
if (tuIdx !== undefined && toolUses[tuIdx]) {
185+
toolUses[tuIdx].output = truncateText(resultText);
186+
if (block.is_error) toolUses[tuIdx].isError = true;
187+
}
188+
}
189+
}
190+
const userTextJoined = userTextParts.join("\n");
191+
messages.push({
192+
role: "human",
193+
timestamp: isoTs(entry.timestamp),
194+
text: truncateText(userTextJoined) || null,
195+
});
196+
197+
// CR-7: Scan user message text for <command-name> XML tags (slash commands).
198+
const cmdRe = /<command-name>([^<]+)<\/command-name>/g;
199+
let cmdMatch: RegExpExecArray | null;
200+
const entryIso = isoTs(entry.timestamp);
201+
while ((cmdMatch = cmdRe.exec(userTextJoined)) !== null) {
202+
if (entryIso) {
203+
slashCommands.push({ name: cmdMatch[1].trim(), timestamp: entryIso });
204+
}
205+
}
206+
207+
// Existing: toolUseResult error tracking (top-level shorthand).
145208
const toolUseResult = entry.toolUseResult;
146209
if (toolUseResult && typeof toolUseResult === "object") {
147210
const tur = toolUseResult as Record<string, unknown>;
@@ -171,6 +234,18 @@ export async function parseSessionFile(filePath: string): Promise<NormalizedSess
171234
tokensByModel[msgModel].output += num(usage.output_tokens);
172235
tokensByModel[msgModel].cacheRead += num(usage.cache_read_input_tokens);
173236
tokensByModel[msgModel].cacheWrite += num(usage.cache_creation_input_tokens);
237+
238+
// CR-2: Push per-turn token record for time-series.
239+
if (iso) {
240+
tokenSeries.push({
241+
timestamp: iso,
242+
model: msgModel,
243+
input: num(usage.input_tokens),
244+
output: num(usage.output_tokens),
245+
cacheRead: num(usage.cache_read_input_tokens),
246+
cacheWrite: num(usage.cache_creation_input_tokens),
247+
});
248+
}
174249
}
175250
if (msg.usage) {
176251
if (typeof usage.service_tier === "string") serviceTiers.add(usage.service_tier);
@@ -183,17 +258,90 @@ export async function parseSessionFile(filePath: string): Promise<NormalizedSess
183258
}
184259
}
185260
const content = msg.content;
261+
// CR-1: Collect text blocks for the assistant NormalizedMessage.
262+
const assistantTextParts: string[] = [];
186263
if (Array.isArray(content)) {
187264
for (const raw of content) {
188265
const block = asRecord(raw);
266+
if (block.type === "text" && typeof block.text === "string") {
267+
assistantTextParts.push(block.text);
268+
}
189269
if (block.type === "tool_use" && typeof block.name === "string") {
190-
toolUses.push({
191-
name: block.name,
270+
const toolName = block.name as string;
271+
const toolInput = block.input ?? null;
272+
const tu: NormalizedToolUse = {
273+
name: toolName,
192274
timestamp: iso || firstTimestamp,
193-
input: block.input ?? null,
275+
input: toolInput,
276+
};
277+
278+
// CR-8: Extract skill name from Skill tool.
279+
if (toolName === "Skill") {
280+
const inp = asRecord(toolInput);
281+
if (typeof inp.skill === "string") tu.skillName = inp.skill;
282+
}
283+
284+
// CR-4: Compute diffDelta for Edit and Write tool uses.
285+
if (toolName === "Edit") {
286+
const inp = asRecord(toolInput);
287+
const oldStr = typeof inp.old_string === "string" ? inp.old_string : null;
288+
const newStr = typeof inp.new_string === "string" ? inp.new_string : null;
289+
tu.diffDelta = computeLineDelta(oldStr, newStr);
290+
totalAdded += tu.diffDelta.add;
291+
totalRemoved += tu.diffDelta.del;
292+
if (typeof inp.file_path === "string") diffFiles.add(inp.file_path);
293+
}
294+
if (toolName === "Write") {
295+
const inp = asRecord(toolInput);
296+
const fileContent = typeof inp.content === "string" ? inp.content : "";
297+
const addLines = fileContent.split("\n").length;
298+
tu.diffDelta = { add: addLines, del: 0 };
299+
totalAdded += addLines;
300+
if (typeof inp.file_path === "string") diffFiles.add(inp.file_path);
301+
}
302+
303+
// CR-3: Track tool_use_id for back-linking tool results.
304+
if (typeof block.id === "string") {
305+
toolUseIdIndex.set(block.id as string, toolUses.length);
306+
}
307+
toolUses.push(tu);
308+
}
309+
if (block.type === "thinking") {
310+
thinkingBlockCount++;
311+
// CR-1: Emit a NormalizedMessage for thinking blocks (text redacted).
312+
messages.push({
313+
role: "assistant",
314+
timestamp: iso,
315+
text: null,
316+
model: msgModel,
317+
isThinking: true,
194318
});
195319
}
196-
if (block.type === "thinking") thinkingBlockCount++;
320+
}
321+
}
322+
// CR-1: Build main assistant NormalizedMessage.
323+
const assistantText = assistantTextParts.join("\n");
324+
messages.push({
325+
role: "assistant",
326+
timestamp: iso,
327+
text: truncateText(assistantText) || null,
328+
model: msgModel,
329+
tokens: msg.usage
330+
? {
331+
input: num(usage.input_tokens),
332+
output: num(usage.output_tokens),
333+
cacheRead: num(usage.cache_read_input_tokens),
334+
cacheWrite: num(usage.cache_creation_input_tokens),
335+
}
336+
: undefined,
337+
});
338+
339+
// CR-7: Scan assistant text for <command-name> tags too.
340+
const cmdRe = /<command-name>([^<]+)<\/command-name>/g;
341+
let cmdMatch: RegExpExecArray | null;
342+
while ((cmdMatch = cmdRe.exec(assistantText)) !== null) {
343+
if (iso) {
344+
slashCommands.push({ name: cmdMatch[1].trim(), timestamp: iso });
197345
}
198346
}
199347
}
@@ -218,6 +366,15 @@ export async function parseSessionFile(filePath: string): Promise<NormalizedSess
218366
/* non-fatal */
219367
}
220368

369+
// CR-4: Build aggregate diffStats (null when no edits were made).
370+
const diffStats: NormalizedDiffStats | null =
371+
diffFiles.size > 0
372+
? { filesChanged: diffFiles.size, linesAdded: totalAdded, linesRemoved: totalRemoved }
373+
: null;
374+
375+
// CR-13: Collect artifact references from tool uses.
376+
const artifacts = collectArtifacts(toolUses, cwd);
377+
221378
return {
222379
sessionId,
223380
name: sessionName,
@@ -247,5 +404,15 @@ export async function parseSessionFile(filePath: string): Promise<NormalizedSess
247404
speeds: [...speeds],
248405
inference_geos: [...inferenceGeos],
249406
},
407+
// CR-1: Ordered messages with text content.
408+
messages,
409+
// CR-2: Per-turn token time-series.
410+
tokenSeries,
411+
// CR-4: Aggregate diff stats.
412+
diffStats,
413+
// CR-7: Extracted slash commands.
414+
slashCommands,
415+
// CR-13: Structured artifact references.
416+
artifacts,
250417
};
251418
}

0 commit comments

Comments
 (0)