@shipfox/api-logs 10.1.0 → 11.0.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 +26 -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 +127 -13
- 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/presentation/routes/read-logs.d.ts.map +1 -1
- package/dist/presentation/routes/read-logs.js +11 -5
- package/dist/presentation/routes/read-logs.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 +3 -3
- 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 +172 -9
- package/src/core/session/claude-parser.test.ts +453 -4
- 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/src/presentation/routes/read-logs.test.ts +18 -4
- package/src/presentation/routes/read-logs.ts +9 -5
- 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;
|
|
@@ -44,7 +44,7 @@ type SystemEventMapping = {
|
|
|
44
44
|
meta?: (message: Record<string, unknown>) => readonly SessionViewRowMeta[];
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
-
const SYSTEM_EVENT_MAPPINGS: Record<string, SystemEventMapping> = {
|
|
47
|
+
export const SYSTEM_EVENT_MAPPINGS: Record<string, SystemEventMapping> = {
|
|
48
48
|
permission_denied: {
|
|
49
49
|
label: 'Permission denied',
|
|
50
50
|
tone: 'error',
|
|
@@ -91,6 +91,32 @@ const SYSTEM_EVENT_MAPPINGS: Record<string, SystemEventMapping> = {
|
|
|
91
91
|
].filter(isMeta);
|
|
92
92
|
},
|
|
93
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
|
+
},
|
|
94
120
|
model_refusal_fallback: {
|
|
95
121
|
label: 'Model refused, fell back',
|
|
96
122
|
tone: 'warning',
|
|
@@ -169,6 +195,10 @@ export function systemRow(
|
|
|
169
195
|
|
|
170
196
|
const isNewSession =
|
|
171
197
|
!context.hasInit || sessionId === undefined || sessionId !== context.sessionId;
|
|
198
|
+
if (isNewSession) {
|
|
199
|
+
context.pendingToolRows.length = 0;
|
|
200
|
+
context.toolCallRows.clear();
|
|
201
|
+
}
|
|
172
202
|
context.hasInit = true;
|
|
173
203
|
context.sessionId = sessionId ?? null;
|
|
174
204
|
context.turn = isNewSession ? 1 : context.turn + 1;
|
|
@@ -183,27 +213,137 @@ export function systemRow(
|
|
|
183
213
|
return lifecycleRow(timestamp, label, null, 'default', false, meta);
|
|
184
214
|
}
|
|
185
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,
|
|
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
|
+
}
|
|
293
|
+
|
|
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
|
+
|
|
186
306
|
function titleCase(value: string): string {
|
|
187
307
|
return value.length === 0 ? value : `${value[0]?.toUpperCase()}${value.slice(1)}`;
|
|
188
308
|
}
|
|
189
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);
|
|
318
|
+
}
|
|
319
|
+
|
|
190
320
|
export function assistantRows(
|
|
191
321
|
timestamp: number,
|
|
192
322
|
message: Record<string, unknown>,
|
|
323
|
+
context: ClaudeParseContext,
|
|
193
324
|
): readonly SessionViewRow[] {
|
|
194
325
|
const sdkMessage = asLooseObject(message.message) ?? message;
|
|
195
326
|
const rows: SessionViewRow[] = [];
|
|
196
327
|
const textParts: string[] = [];
|
|
197
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
|
+
};
|
|
198
338
|
|
|
199
339
|
const pushText = () => {
|
|
200
340
|
if (textParts.length === 0) return;
|
|
201
|
-
|
|
341
|
+
pushRow(messageRow(timestamp, 'assistant', 'assistant', textParts.join('\n\n'), false));
|
|
202
342
|
textParts.length = 0;
|
|
203
343
|
};
|
|
204
344
|
const pushThinking = () => {
|
|
205
345
|
if (thinkingParts.length === 0) return;
|
|
206
|
-
|
|
346
|
+
pushRow(thinkingRow(timestamp, thinkingParts.join('\n\n')));
|
|
207
347
|
thinkingParts.length = 0;
|
|
208
348
|
};
|
|
209
349
|
|
|
@@ -212,7 +352,14 @@ export function assistantRows(
|
|
|
212
352
|
if (type === 'tool_use' || type === 'tool-use' || type === 'toolCall') {
|
|
213
353
|
pushText();
|
|
214
354
|
pushThinking();
|
|
215
|
-
|
|
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
|
+
}
|
|
216
363
|
continue;
|
|
217
364
|
}
|
|
218
365
|
|
|
@@ -231,7 +378,7 @@ export function assistantRows(
|
|
|
231
378
|
pushText();
|
|
232
379
|
pushThinking();
|
|
233
380
|
|
|
234
|
-
if (rows.length > 0) return rows;
|
|
381
|
+
if (rows.length > 0 || queueRows) return rows;
|
|
235
382
|
|
|
236
383
|
const text = stringField(sdkMessage, 'content') ?? stringField(message, 'result');
|
|
237
384
|
return [messageRow(timestamp, 'assistant', 'assistant', text ?? toJson(message), false)];
|
|
@@ -240,11 +387,14 @@ export function assistantRows(
|
|
|
240
387
|
export function userRows(
|
|
241
388
|
timestamp: number,
|
|
242
389
|
message: Record<string, unknown>,
|
|
390
|
+
context: ClaudeParseContext,
|
|
243
391
|
): readonly SessionViewRow[] {
|
|
244
392
|
const sdkMessage = asLooseObject(message.message) ?? message;
|
|
245
393
|
const role = isPlatformMessage(sdkMessage) ? 'platform' : 'user';
|
|
246
394
|
const rows: SessionViewRow[] = [];
|
|
247
395
|
const textParts: string[] = [];
|
|
396
|
+
const hadPendingToolCall = context.toolCallRows.size > 0;
|
|
397
|
+
let matchedToolResult = false;
|
|
248
398
|
const pushText = () => {
|
|
249
399
|
if (textParts.length === 0) return;
|
|
250
400
|
rows.push(messageRow(timestamp, role, role, textParts.join('\n\n'), false));
|
|
@@ -255,7 +405,11 @@ export function userRows(
|
|
|
255
405
|
const type = stringField(block, 'type');
|
|
256
406
|
if (type === 'tool_result' || type === 'tool-result') {
|
|
257
407
|
pushText();
|
|
258
|
-
|
|
408
|
+
const row = toolResultRow(timestamp, block);
|
|
409
|
+
if (row.toolCallId !== null && context.toolCallRows.has(row.toolCallId)) {
|
|
410
|
+
matchedToolResult = true;
|
|
411
|
+
}
|
|
412
|
+
rows.push(row);
|
|
259
413
|
continue;
|
|
260
414
|
}
|
|
261
415
|
|
|
@@ -265,10 +419,19 @@ export function userRows(
|
|
|
265
419
|
|
|
266
420
|
pushText();
|
|
267
421
|
|
|
268
|
-
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
|
+
}
|
|
269
426
|
|
|
270
|
-
|
|
271
|
-
|
|
427
|
+
if (hadPendingToolCall) {
|
|
428
|
+
if (matchedToolResult) {
|
|
429
|
+
context.pendingToolRows.push(...rows);
|
|
430
|
+
return [];
|
|
431
|
+
}
|
|
432
|
+
return [...flushPendingToolRows(context), ...rows];
|
|
433
|
+
}
|
|
434
|
+
return rows;
|
|
272
435
|
}
|
|
273
436
|
|
|
274
437
|
export function resultRow(
|