@shipfox/api-logs 10.0.0 → 10.2.0
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/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +23 -0
- package/dist/core/append-logs.d.ts.map +1 -1
- package/dist/core/append-logs.js +13 -5
- package/dist/core/append-logs.js.map +1 -1
- package/dist/core/close-stream.d.ts.map +1 -1
- package/dist/core/close-stream.js +16 -1
- package/dist/core/close-stream.js.map +1 -1
- package/dist/core/entities/attempt-stream.d.ts +2 -1
- package/dist/core/entities/attempt-stream.d.ts.map +1 -1
- package/dist/core/entities/attempt-stream.js.map +1 -1
- package/dist/core/session/claude/rows.d.ts +15 -3
- package/dist/core/session/claude/rows.d.ts.map +1 -1
- package/dist/core/session/claude/rows.js +221 -15
- package/dist/core/session/claude/rows.js.map +1 -1
- package/dist/core/session/claude-parser.d.ts +4 -2
- package/dist/core/session/claude-parser.d.ts.map +1 -1
- package/dist/core/session/claude-parser.js +56 -10
- package/dist/core/session/claude-parser.js.map +1 -1
- package/dist/db/db.d.ts +113 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/schema/attempt-streams.d.ts +113 -0
- package/dist/db/schema/attempt-streams.d.ts.map +1 -1
- package/dist/db/schema/attempt-streams.js +2 -0
- package/dist/db/schema/attempt-streams.js.map +1 -1
- package/dist/db/streams.d.ts +3 -1
- package/dist/db/streams.d.ts.map +1 -1
- package/dist/db/streams.js +6 -1
- package/dist/db/streams.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0002_wise_dorian_gray.sql +1 -0
- package/drizzle/meta/0002_snapshot.json +561 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +4 -4
- package/src/core/append-logs.test.ts +57 -0
- package/src/core/append-logs.ts +15 -3
- package/src/core/close-stream.ts +17 -1
- package/src/core/entities/attempt-stream.ts +2 -1
- package/src/core/session/claude/rows.ts +295 -13
- package/src/core/session/claude-parser.test.ts +620 -11
- package/src/core/session/claude-parser.ts +59 -10
- package/src/db/schema/attempt-streams.ts +3 -1
- package/src/db/streams.ts +10 -2
- package/test/fixtures/claude-session/agent-step.jsonl +12 -0
- package/test/fixtures/claude-session/sdk-system-subtypes.json +32 -0
- package/test/fixtures/claude-session.ts +45 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/src/core/append-logs.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import {allowedBudget} from './budget.js';
|
|
31
31
|
import {closeStream, controlTombstone} from './close-stream.js';
|
|
32
32
|
import {MalformedLogChunkError, OffsetGapError} from './errors.js';
|
|
33
|
+
import {flushPendingToolRows} from './session/claude/rows.js';
|
|
33
34
|
import {
|
|
34
35
|
type ClaudeParseContext,
|
|
35
36
|
claudeInitSessionId,
|
|
@@ -180,6 +181,7 @@ interface StoredBody {
|
|
|
180
181
|
recordCounts: Partial<Record<LogRecord['type'], number>>;
|
|
181
182
|
claudeParseContext: ClaudeParseContext | undefined;
|
|
182
183
|
claudePendingResult: SessionViewLifecycleRow | null | undefined;
|
|
184
|
+
claudePendingToolRows: readonly SessionViewRow[] | undefined;
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
/**
|
|
@@ -303,6 +305,12 @@ function buildStoredBody(
|
|
|
303
305
|
}
|
|
304
306
|
}
|
|
305
307
|
|
|
308
|
+
if (isStreamFinal && parseContext?.claude !== undefined) {
|
|
309
|
+
for (const row of flushPendingToolRows(parseContext.claude)) {
|
|
310
|
+
storedRecords.push(storedAgentSessionRow(row));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
306
314
|
const body = Buffer.from(storedRecords.map((record) => `${JSON.stringify(record)}\n`).join(''));
|
|
307
315
|
const recordCounts: Partial<Record<LogRecord['type'], number>> = {};
|
|
308
316
|
for (const record of storedRecords) {
|
|
@@ -314,6 +322,7 @@ function buildStoredBody(
|
|
|
314
322
|
recordCounts,
|
|
315
323
|
claudeParseContext: parseContext?.claude,
|
|
316
324
|
claudePendingResult: parseContext?.claude === undefined ? undefined : pendingResult,
|
|
325
|
+
claudePendingToolRows: parseContext?.claude?.pendingToolRows,
|
|
317
326
|
};
|
|
318
327
|
}
|
|
319
328
|
|
|
@@ -451,18 +460,20 @@ export async function appendLogs(
|
|
|
451
460
|
}
|
|
452
461
|
|
|
453
462
|
const parseHarness =
|
|
454
|
-
sessionHarness === 'claude' ||
|
|
463
|
+
sessionHarness === 'claude' ||
|
|
464
|
+
stream.claudePendingResult !== null ||
|
|
465
|
+
stream.claudePendingToolRows.length > 0
|
|
455
466
|
? 'claude'
|
|
456
467
|
: sessionHarness;
|
|
457
468
|
const stored = buildStoredBody(
|
|
458
469
|
parsed.records,
|
|
459
470
|
parseHarness,
|
|
460
471
|
parseHarness === 'claude'
|
|
461
|
-
? {
|
|
472
|
+
? createClaudeParseContext(stream.claudePendingToolRows, {
|
|
462
473
|
hasInit: stream.claudeHasInit,
|
|
463
474
|
sessionId: stream.claudeSessionId,
|
|
464
475
|
turn: stream.claudeTurn,
|
|
465
|
-
}
|
|
476
|
+
})
|
|
466
477
|
: undefined,
|
|
467
478
|
parseHarness === 'claude' ? stream.claudePendingResult : undefined,
|
|
468
479
|
declaredTotalBytes !== undefined,
|
|
@@ -488,6 +499,7 @@ export async function appendLogs(
|
|
|
488
499
|
sessionId: stored.claudeParseContext.sessionId,
|
|
489
500
|
turn: stored.claudeParseContext.turn,
|
|
490
501
|
pendingResult: stored.claudePendingResult ?? null,
|
|
502
|
+
pendingToolRows: stored.claudePendingToolRows ?? [],
|
|
491
503
|
});
|
|
492
504
|
}
|
|
493
505
|
}
|
package/src/core/close-stream.ts
CHANGED
|
@@ -47,7 +47,23 @@ export async function closeStream(
|
|
|
47
47
|
});
|
|
48
48
|
if (!closedResult) return null;
|
|
49
49
|
|
|
50
|
-
const {stream: closed, pendingClaudeResult} = closedResult;
|
|
50
|
+
const {stream: closed, pendingClaudeResult, pendingClaudeToolRows} = closedResult;
|
|
51
|
+
if (pendingClaudeToolRows.length > 0) {
|
|
52
|
+
const data = Buffer.from(
|
|
53
|
+
`${pendingClaudeToolRows
|
|
54
|
+
.map((row) =>
|
|
55
|
+
JSON.stringify({v: 1, ts: row.timestamp, type: 'agent_session', row} satisfies LogRecord),
|
|
56
|
+
)
|
|
57
|
+
.join('\n')}\n`,
|
|
58
|
+
);
|
|
59
|
+
await insertChunk(tx, {
|
|
60
|
+
streamId: closed.id,
|
|
61
|
+
streamOffset: closed.committedLength,
|
|
62
|
+
byteLen: data.length,
|
|
63
|
+
data,
|
|
64
|
+
origin: 'control',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
51
67
|
if (pendingClaudeResult !== null) {
|
|
52
68
|
const record: LogRecord = {
|
|
53
69
|
v: 1,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {SessionViewLifecycleRow} from '@shipfox/api-logs-dto';
|
|
1
|
+
import type {SessionViewLifecycleRow, SessionViewRow} from '@shipfox/api-logs-dto';
|
|
2
2
|
|
|
3
3
|
/** Lifecycle state of an attempt's log stream. */
|
|
4
4
|
export type StreamState = 'open' | 'closed';
|
|
@@ -35,6 +35,7 @@ export interface AttemptStream {
|
|
|
35
35
|
claudeSessionId: string | null;
|
|
36
36
|
claudeTurn: number;
|
|
37
37
|
claudePendingResult: SessionViewLifecycleRow | null;
|
|
38
|
+
claudePendingToolRows: SessionViewRow[];
|
|
38
39
|
truncated: boolean;
|
|
39
40
|
objectKey: string | null;
|
|
40
41
|
createdAt: Date;
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
formatNumber,
|
|
14
14
|
isMeta,
|
|
15
15
|
metaItem,
|
|
16
|
+
numberField,
|
|
16
17
|
stringField,
|
|
17
18
|
stringifyValue,
|
|
18
19
|
toJson,
|
|
@@ -34,6 +35,127 @@ export const PURE_PROGRESS_CLAUDE_SYSTEM_SUBTYPES = new Set<string>([
|
|
|
34
35
|
]);
|
|
35
36
|
const OUTPUT_REPROMPT_PREFIX = 'The previous turn ended without setting required workflow outputs:';
|
|
36
37
|
|
|
38
|
+
type SystemEventMapping = {
|
|
39
|
+
label: string | ((message: Record<string, unknown>) => string);
|
|
40
|
+
tone:
|
|
41
|
+
| SessionViewLifecycleRow['tone']
|
|
42
|
+
| ((message: Record<string, unknown>) => SessionViewLifecycleRow['tone']);
|
|
43
|
+
detail?: (message: Record<string, unknown>) => string | null;
|
|
44
|
+
meta?: (message: Record<string, unknown>) => readonly SessionViewRowMeta[];
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export const SYSTEM_EVENT_MAPPINGS: Record<string, SystemEventMapping> = {
|
|
48
|
+
permission_denied: {
|
|
49
|
+
label: 'Permission denied',
|
|
50
|
+
tone: 'error',
|
|
51
|
+
detail: (message) => stringField(message, 'tool_name') ?? null,
|
|
52
|
+
meta: (message) =>
|
|
53
|
+
[
|
|
54
|
+
metaItem(
|
|
55
|
+
'reason',
|
|
56
|
+
stringField(message, 'decision_reason') ?? stringField(message, 'message'),
|
|
57
|
+
),
|
|
58
|
+
].filter(isMeta),
|
|
59
|
+
},
|
|
60
|
+
api_retry: {
|
|
61
|
+
label: 'API retry',
|
|
62
|
+
tone: 'warning',
|
|
63
|
+
detail: (message) => {
|
|
64
|
+
const attempt = numberField(message, 'attempt');
|
|
65
|
+
const maxRetries = numberField(message, 'max_retries');
|
|
66
|
+
return attempt == null || maxRetries == null
|
|
67
|
+
? null
|
|
68
|
+
: `attempt ${formatNumber(attempt)}/${formatNumber(maxRetries)}`;
|
|
69
|
+
},
|
|
70
|
+
meta: (message) =>
|
|
71
|
+
[
|
|
72
|
+
numberMeta(message, 'error_status', 'status'),
|
|
73
|
+
numberMeta(message, 'retry_delay_ms', 'retry delay', 'ms'),
|
|
74
|
+
].filter(isMeta),
|
|
75
|
+
},
|
|
76
|
+
informational: {
|
|
77
|
+
label: (message) => titleCase(stringField(message, 'level') ?? 'informational'),
|
|
78
|
+
tone: (message) => (stringField(message, 'level') === 'warning' ? 'warning' : 'default'),
|
|
79
|
+
detail: (message) => stringField(message, 'content') ?? null,
|
|
80
|
+
},
|
|
81
|
+
compact_boundary: {
|
|
82
|
+
label: 'Context compacted',
|
|
83
|
+
tone: 'default',
|
|
84
|
+
meta: (message) => {
|
|
85
|
+
const compactMetadata = asLooseObject(field(message, 'compact_metadata')) ?? {};
|
|
86
|
+
return [
|
|
87
|
+
metaItem('trigger', stringField(compactMetadata, 'trigger')),
|
|
88
|
+
numberMeta(compactMetadata, 'pre_tokens', 'tokens before', 'tokens'),
|
|
89
|
+
numberMeta(compactMetadata, 'post_tokens', 'tokens after', 'tokens'),
|
|
90
|
+
numberMeta(compactMetadata, 'duration_ms', 'duration', 'ms'),
|
|
91
|
+
].filter(isMeta);
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
hook_response: {
|
|
95
|
+
label: (message) => {
|
|
96
|
+
const outcome = stringField(message, 'outcome');
|
|
97
|
+
return outcome === 'error'
|
|
98
|
+
? 'Hook failed'
|
|
99
|
+
: outcome === 'cancelled'
|
|
100
|
+
? 'Hook cancelled'
|
|
101
|
+
: outcome === 'success'
|
|
102
|
+
? 'Hook completed'
|
|
103
|
+
: 'Hook response';
|
|
104
|
+
},
|
|
105
|
+
tone: (message) => {
|
|
106
|
+
const outcome = stringField(message, 'outcome');
|
|
107
|
+
return outcome === 'error' ? 'error' : outcome === 'cancelled' ? 'warning' : 'default';
|
|
108
|
+
},
|
|
109
|
+
detail: (message) =>
|
|
110
|
+
stringField(message, 'output') ??
|
|
111
|
+
stringField(message, 'stderr') ??
|
|
112
|
+
stringField(message, 'stdout') ??
|
|
113
|
+
null,
|
|
114
|
+
meta: (message) =>
|
|
115
|
+
[
|
|
116
|
+
metaItem('hook', stringField(message, 'hook_name')),
|
|
117
|
+
numberMeta(message, 'exit_code', 'exit code'),
|
|
118
|
+
].filter(isMeta),
|
|
119
|
+
},
|
|
120
|
+
model_refusal_fallback: {
|
|
121
|
+
label: 'Model refused, fell back',
|
|
122
|
+
tone: 'warning',
|
|
123
|
+
},
|
|
124
|
+
model_refusal_no_fallback: {
|
|
125
|
+
label: 'Model refused',
|
|
126
|
+
tone: 'error',
|
|
127
|
+
},
|
|
128
|
+
mirror_error: {
|
|
129
|
+
label: 'Session mirror failed',
|
|
130
|
+
tone: 'error',
|
|
131
|
+
detail: (message) => stringField(message, 'error') ?? null,
|
|
132
|
+
},
|
|
133
|
+
worker_shutting_down: {
|
|
134
|
+
label: 'Worker shutting down',
|
|
135
|
+
tone: 'warning',
|
|
136
|
+
},
|
|
137
|
+
elicitation_complete: {
|
|
138
|
+
label: 'Elicitation complete',
|
|
139
|
+
tone: 'default',
|
|
140
|
+
},
|
|
141
|
+
notification: {
|
|
142
|
+
label: 'Notification',
|
|
143
|
+
tone: 'default',
|
|
144
|
+
},
|
|
145
|
+
task_started: {
|
|
146
|
+
label: 'Task started',
|
|
147
|
+
tone: 'default',
|
|
148
|
+
},
|
|
149
|
+
task_updated: {
|
|
150
|
+
label: 'Task updated',
|
|
151
|
+
tone: 'default',
|
|
152
|
+
},
|
|
153
|
+
task_notification: {
|
|
154
|
+
label: 'Task notification',
|
|
155
|
+
tone: 'default',
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
37
159
|
export function systemRow(
|
|
38
160
|
timestamp: number,
|
|
39
161
|
message: Record<string, unknown>,
|
|
@@ -51,40 +173,177 @@ export function systemRow(
|
|
|
51
173
|
),
|
|
52
174
|
].filter(isMeta);
|
|
53
175
|
|
|
54
|
-
if (!isInit)
|
|
55
|
-
|
|
176
|
+
if (!isInit) {
|
|
177
|
+
const mapping = subtype === undefined ? undefined : SYSTEM_EVENT_MAPPINGS[subtype];
|
|
178
|
+
const label =
|
|
179
|
+
mapping === undefined
|
|
180
|
+
? `Session event (${subtype ?? 'unknown'})`
|
|
181
|
+
: typeof mapping.label === 'function'
|
|
182
|
+
? mapping.label(message)
|
|
183
|
+
: mapping.label;
|
|
184
|
+
const tone =
|
|
185
|
+
mapping === undefined
|
|
186
|
+
? 'default'
|
|
187
|
+
: typeof mapping.tone === 'function'
|
|
188
|
+
? mapping.tone(message)
|
|
189
|
+
: mapping.tone;
|
|
190
|
+
const detail = mapping?.detail?.(message) ?? null;
|
|
191
|
+
const meta = [...baseMeta, ...(mapping?.meta?.(message) ?? [])];
|
|
192
|
+
|
|
193
|
+
return lifecycleRow(timestamp, label, detail, tone, false, meta);
|
|
194
|
+
}
|
|
56
195
|
|
|
57
196
|
const isNewSession =
|
|
58
197
|
!context.hasInit || sessionId === undefined || sessionId !== context.sessionId;
|
|
198
|
+
if (isNewSession) {
|
|
199
|
+
context.pendingToolRows.length = 0;
|
|
200
|
+
context.toolCallRows.clear();
|
|
201
|
+
}
|
|
59
202
|
context.hasInit = true;
|
|
60
203
|
context.sessionId = sessionId ?? null;
|
|
61
204
|
context.turn = isNewSession ? 1 : context.turn + 1;
|
|
62
205
|
|
|
63
206
|
const label = isNewSession ? 'Session started' : `Turn ${context.turn} started`;
|
|
64
|
-
const meta = [
|
|
65
|
-
|
|
207
|
+
const meta = [
|
|
208
|
+
metaItem('session', isNewSession ? sessionId : null),
|
|
209
|
+
metaItem('turn', isNewSession ? null : String(context.turn)),
|
|
210
|
+
...baseMeta,
|
|
211
|
+
].filter(isMeta);
|
|
212
|
+
|
|
213
|
+
return lifecycleRow(timestamp, label, null, 'default', false, meta);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function authStatusRow(
|
|
217
|
+
timestamp: number,
|
|
218
|
+
message: Record<string, unknown>,
|
|
219
|
+
): SessionViewLifecycleRow {
|
|
220
|
+
const error = stringField(message, 'error');
|
|
221
|
+
const output = stringList(field(message, 'output')).join('\n');
|
|
222
|
+
const isAuthenticating = booleanField(message, 'isAuthenticating');
|
|
223
|
+
|
|
224
|
+
return lifecycleRow(
|
|
225
|
+
timestamp,
|
|
226
|
+
error
|
|
227
|
+
? 'Authentication failed'
|
|
228
|
+
: isAuthenticating
|
|
229
|
+
? 'Authenticating'
|
|
230
|
+
: 'Authentication complete',
|
|
231
|
+
(error ?? output) || null,
|
|
232
|
+
error ? 'error' : 'default',
|
|
233
|
+
false,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const RATE_LIMIT_STATUS_MAPPINGS: Record<
|
|
238
|
+
string,
|
|
239
|
+
{label: string; tone: SessionViewLifecycleRow['tone']}
|
|
240
|
+
> = {
|
|
241
|
+
rejected: {label: 'Rate limit exceeded', tone: 'error'},
|
|
242
|
+
allowed_warning: {label: 'Rate limit warning', tone: 'warning'},
|
|
243
|
+
allowed: {label: 'Rate limit available', tone: 'default'},
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
export function rateLimitRow(
|
|
247
|
+
timestamp: number,
|
|
248
|
+
message: Record<string, unknown>,
|
|
249
|
+
): SessionViewLifecycleRow {
|
|
250
|
+
const rateLimitInfo = asLooseObject(field(message, 'rate_limit_info')) ?? {};
|
|
251
|
+
const status = stringField(rateLimitInfo, 'status');
|
|
252
|
+
const rateLimitType = stringField(rateLimitInfo, 'rateLimitType');
|
|
253
|
+
const utilization = numberField(rateLimitInfo, 'utilization');
|
|
254
|
+
const mapping = status === undefined ? undefined : RATE_LIMIT_STATUS_MAPPINGS[status];
|
|
255
|
+
const meta = [
|
|
256
|
+
metaItem('utilization', utilization == null ? null : `${formatNumber(utilization * 100)}%`),
|
|
257
|
+
mapping === undefined ? metaItem('status', status ?? null) : null,
|
|
258
|
+
].filter(isMeta);
|
|
259
|
+
|
|
260
|
+
return lifecycleRow(
|
|
261
|
+
timestamp,
|
|
262
|
+
mapping?.label ?? 'Rate limit updated',
|
|
263
|
+
rateLimitType == null ? null : humanizeEnumValue(rateLimitType),
|
|
264
|
+
mapping?.tone ?? 'warning',
|
|
265
|
+
false,
|
|
266
|
+
meta,
|
|
66
267
|
);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function toolUseSummary(
|
|
271
|
+
message: Record<string, unknown>,
|
|
272
|
+
context: ClaudeParseContext,
|
|
273
|
+
): boolean {
|
|
274
|
+
const summary = stringField(message, 'summary');
|
|
275
|
+
if (summary === undefined) return false;
|
|
276
|
+
|
|
277
|
+
const precedingToolUseIds = stringList(field(message, 'preceding_tool_use_ids'));
|
|
278
|
+
const toolUseId = stringField(message, 'tool_use_id');
|
|
279
|
+
const candidateIds =
|
|
280
|
+
precedingToolUseIds.length > 0
|
|
281
|
+
? precedingToolUseIds
|
|
282
|
+
: toolUseId === undefined
|
|
283
|
+
? []
|
|
284
|
+
: [toolUseId];
|
|
285
|
+
|
|
286
|
+
for (const id of [...candidateIds].reverse()) {
|
|
287
|
+
const row = context.toolCallRows.get(id);
|
|
288
|
+
if (row === undefined) continue;
|
|
289
|
+
|
|
290
|
+
row.summary = row.summary === undefined ? summary : `${row.summary}\n\n${summary}`;
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
67
293
|
|
|
68
|
-
return
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function flushPendingToolRows(context: ClaudeParseContext): readonly SessionViewRow[] {
|
|
298
|
+
if (context.pendingToolRows.length === 0) return [];
|
|
299
|
+
|
|
300
|
+
const rows = context.pendingToolRows;
|
|
301
|
+
context.pendingToolRows = [];
|
|
302
|
+
context.toolCallRows.clear();
|
|
303
|
+
return rows;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function titleCase(value: string): string {
|
|
307
|
+
return value.length === 0 ? value : `${value[0]?.toUpperCase()}${value.slice(1)}`;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function humanizeEnumValue(value: string): string {
|
|
311
|
+
return titleCase(value.replaceAll('_', ' '));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function stringList(value: unknown): string[] {
|
|
315
|
+
if (!Array.isArray(value)) return [];
|
|
316
|
+
|
|
317
|
+
return value.filter((item): item is string => typeof item === 'string' && item.length > 0);
|
|
69
318
|
}
|
|
70
319
|
|
|
71
320
|
export function assistantRows(
|
|
72
321
|
timestamp: number,
|
|
73
322
|
message: Record<string, unknown>,
|
|
323
|
+
context: ClaudeParseContext,
|
|
74
324
|
): readonly SessionViewRow[] {
|
|
75
325
|
const sdkMessage = asLooseObject(message.message) ?? message;
|
|
76
326
|
const rows: SessionViewRow[] = [];
|
|
77
327
|
const textParts: string[] = [];
|
|
78
328
|
const thinkingParts: string[] = [];
|
|
329
|
+
let queueRows = context.toolCallRows.size > 0;
|
|
330
|
+
|
|
331
|
+
const pushRow = (row: SessionViewRow) => {
|
|
332
|
+
if (queueRows) {
|
|
333
|
+
context.pendingToolRows.push(row);
|
|
334
|
+
} else {
|
|
335
|
+
rows.push(row);
|
|
336
|
+
}
|
|
337
|
+
};
|
|
79
338
|
|
|
80
339
|
const pushText = () => {
|
|
81
340
|
if (textParts.length === 0) return;
|
|
82
|
-
|
|
341
|
+
pushRow(messageRow(timestamp, 'assistant', 'assistant', textParts.join('\n\n'), false));
|
|
83
342
|
textParts.length = 0;
|
|
84
343
|
};
|
|
85
344
|
const pushThinking = () => {
|
|
86
345
|
if (thinkingParts.length === 0) return;
|
|
87
|
-
|
|
346
|
+
pushRow(thinkingRow(timestamp, thinkingParts.join('\n\n')));
|
|
88
347
|
thinkingParts.length = 0;
|
|
89
348
|
};
|
|
90
349
|
|
|
@@ -93,7 +352,14 @@ export function assistantRows(
|
|
|
93
352
|
if (type === 'tool_use' || type === 'tool-use' || type === 'toolCall') {
|
|
94
353
|
pushText();
|
|
95
354
|
pushThinking();
|
|
96
|
-
|
|
355
|
+
const row = toolCallRow(timestamp, block);
|
|
356
|
+
if (row.id === null) {
|
|
357
|
+
pushRow(row);
|
|
358
|
+
} else {
|
|
359
|
+
queueRows = true;
|
|
360
|
+
context.pendingToolRows.push(row);
|
|
361
|
+
context.toolCallRows.set(row.id, row);
|
|
362
|
+
}
|
|
97
363
|
continue;
|
|
98
364
|
}
|
|
99
365
|
|
|
@@ -112,7 +378,7 @@ export function assistantRows(
|
|
|
112
378
|
pushText();
|
|
113
379
|
pushThinking();
|
|
114
380
|
|
|
115
|
-
if (rows.length > 0) return rows;
|
|
381
|
+
if (rows.length > 0 || queueRows) return rows;
|
|
116
382
|
|
|
117
383
|
const text = stringField(sdkMessage, 'content') ?? stringField(message, 'result');
|
|
118
384
|
return [messageRow(timestamp, 'assistant', 'assistant', text ?? toJson(message), false)];
|
|
@@ -121,11 +387,14 @@ export function assistantRows(
|
|
|
121
387
|
export function userRows(
|
|
122
388
|
timestamp: number,
|
|
123
389
|
message: Record<string, unknown>,
|
|
390
|
+
context: ClaudeParseContext,
|
|
124
391
|
): readonly SessionViewRow[] {
|
|
125
392
|
const sdkMessage = asLooseObject(message.message) ?? message;
|
|
126
393
|
const role = isPlatformMessage(sdkMessage) ? 'platform' : 'user';
|
|
127
394
|
const rows: SessionViewRow[] = [];
|
|
128
395
|
const textParts: string[] = [];
|
|
396
|
+
const hadPendingToolCall = context.toolCallRows.size > 0;
|
|
397
|
+
let matchedToolResult = false;
|
|
129
398
|
const pushText = () => {
|
|
130
399
|
if (textParts.length === 0) return;
|
|
131
400
|
rows.push(messageRow(timestamp, role, role, textParts.join('\n\n'), false));
|
|
@@ -136,7 +405,11 @@ export function userRows(
|
|
|
136
405
|
const type = stringField(block, 'type');
|
|
137
406
|
if (type === 'tool_result' || type === 'tool-result') {
|
|
138
407
|
pushText();
|
|
139
|
-
|
|
408
|
+
const row = toolResultRow(timestamp, block);
|
|
409
|
+
if (row.toolCallId !== null && context.toolCallRows.has(row.toolCallId)) {
|
|
410
|
+
matchedToolResult = true;
|
|
411
|
+
}
|
|
412
|
+
rows.push(row);
|
|
140
413
|
continue;
|
|
141
414
|
}
|
|
142
415
|
|
|
@@ -146,10 +419,19 @@ export function userRows(
|
|
|
146
419
|
|
|
147
420
|
pushText();
|
|
148
421
|
|
|
149
|
-
if (rows.length
|
|
422
|
+
if (rows.length === 0) {
|
|
423
|
+
const content = stringField(sdkMessage, 'content');
|
|
424
|
+
rows.push(messageRow(timestamp, role, role, content ?? toJson(message), false));
|
|
425
|
+
}
|
|
150
426
|
|
|
151
|
-
|
|
152
|
-
|
|
427
|
+
if (hadPendingToolCall) {
|
|
428
|
+
if (matchedToolResult) {
|
|
429
|
+
context.pendingToolRows.push(...rows);
|
|
430
|
+
return [];
|
|
431
|
+
}
|
|
432
|
+
return [...flushPendingToolRows(context), ...rows];
|
|
433
|
+
}
|
|
434
|
+
return rows;
|
|
153
435
|
}
|
|
154
436
|
|
|
155
437
|
export function resultRow(
|