Zion Boggan zionboggan.com ↗

Analysis v2 foundation: retain per-turn agent actions, model, and thinking in the lineage

9040f3f   Zion Boggan committed on Jun 12, 2026 (1 week ago)
src/extract.js +13 -0
@@ -51,6 +51,7 @@ export function classifyPrompts(sessions) {
prevNode.kind = prevNode.kind === KIND.ROOT ? KIND.ROOT : classifyOne(text, prompt, true);
prevNode.chars = text.length;
}
+ mergeActions(prevNode, prompt);
continue;
}
@@ -58,6 +59,7 @@ export function classifyPrompts(sessions) {
prevNode.reruns = (prevNode.reruns || 0) + 1;
prevNode.text = text;
prevNode.title = makeTitle(text);
+ mergeActions(prevNode, prompt);
continue;
}
@@ -67,6 +69,7 @@ export function classifyPrompts(sessions) {
CONTINUATION_RE.test(text)
) {
prevNode.nudges++;
+ mergeActions(prevNode, prompt);
continue;
}
@@ -85,6 +88,8 @@ export function classifyPrompts(sessions) {
status: 'accepted',
nudges: 0,
afterInterruption: prompt.afterInterruption,
+ actions: prompt.actions || [],
+ thinking: prompt.thinking || 0,
chars: text.length,
} : {
id: null,
@@ -98,6 +103,8 @@ export function classifyPrompts(sessions) {
status: 'accepted',
nudges: 0,
afterInterruption: prompt.afterInterruption,
+ actions: prompt.actions || [],
+ thinking: prompt.thinking || 0,
chars: text.length,
};
if (node.kind === KIND.ROOT) rootAssigned = true;
@@ -147,4 +154,10 @@ export function makeTitle(text) {
return truncate(sentence, 96);
}
+function mergeActions(node, prompt) {
+ node.actions = node.actions || [];
+ if (prompt.actions && prompt.actions.length) node.actions.push(...prompt.actions);
+ if (prompt.thinking) node.thinking = (node.thinking || 0) + prompt.thinking;
+}
+
export { KIND };
src/parse.js +21 -4
@@ -31,6 +31,7 @@ export async function parseSessionFile(path, sessionMeta = {}) {
isContinuation: false,
_usageByMsgId: new Map(),
_pendingInterruption: false,
+ _currentPrompt: null,
};
const stream = createReadStream(path, { encoding: 'utf8' });
@@ -165,7 +166,7 @@ function ingestUser(session, rec) {
if (!trimmed && hasImage) trimmed = '[image-only prompt: screenshot/annotated feedback]';
if (!trimmed) return;
- session.prompts.push({
+ const prompt = {
uuid: rec.uuid || null,
parentUuid: rec.parentUuid || null,
ts: rec.timestamp || null,
@@ -173,7 +174,11 @@ function ingestUser(session, rec) {
hasImage,
hadToolResultContext: hasToolResult,
afterInterruption: Boolean(session._pendingInterruption),
- });
+ actions: [],
+ thinking: 0,
+ };
+ session.prompts.push(prompt);
+ session._currentPrompt = prompt;
session._pendingInterruption = false;
}
@@ -191,13 +196,25 @@ function ingestAssistant(session, rec) {
session._usageByMsgId.set(msg.id || rec.uuid, msg.usage);
}
+ const current = session._currentPrompt;
const content = Array.isArray(msg.content) ? msg.content : [];
for (const block of content) {
- if (block && block.type === 'tool_use') {
+ if (!block) continue;
+ if (block.type === 'tool_use') {
session.stats.toolUses++;
const input = block.input || {};
const file = input.file_path || input.notebook_path || null;
if (typeof file === 'string') session.stats.filesTouched.add(file);
+ if (current) {
+ current.actions.push({
+ tool: block.name || null,
+ file: typeof file === 'string' ? file : null,
+ command: block.name === 'Bash' && typeof input.command === 'string' ? input.command : null,
+ model: synthetic ? null : msg.model || null,
+ });
+ }
+ } else if (block.type === 'thinking' || block.type === 'redacted_thinking') {
+ if (current) current.thinking++;
}
}
}
@@ -312,7 +329,7 @@ export function parsePlainTranscript(text, label = 'pasted-transcript') {
gitBranch: null,
firstTs: null,
lastTs: null,
- prompts: prompts.map((p) => ({ ...p, text: p.text.trim() })),
+ prompts: prompts.map((p) => ({ ...p, text: p.text.trim(), actions: [], thinking: 0 })),
index: new Map(),
leafUuid: null,
activeLeafUuid: null,