@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.
Files changed (47) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +23 -0
  3. package/dist/core/append-logs.d.ts.map +1 -1
  4. package/dist/core/append-logs.js +13 -5
  5. package/dist/core/append-logs.js.map +1 -1
  6. package/dist/core/close-stream.d.ts.map +1 -1
  7. package/dist/core/close-stream.js +16 -1
  8. package/dist/core/close-stream.js.map +1 -1
  9. package/dist/core/entities/attempt-stream.d.ts +2 -1
  10. package/dist/core/entities/attempt-stream.d.ts.map +1 -1
  11. package/dist/core/entities/attempt-stream.js.map +1 -1
  12. package/dist/core/session/claude/rows.d.ts +15 -3
  13. package/dist/core/session/claude/rows.d.ts.map +1 -1
  14. package/dist/core/session/claude/rows.js +221 -15
  15. package/dist/core/session/claude/rows.js.map +1 -1
  16. package/dist/core/session/claude-parser.d.ts +4 -2
  17. package/dist/core/session/claude-parser.d.ts.map +1 -1
  18. package/dist/core/session/claude-parser.js +56 -10
  19. package/dist/core/session/claude-parser.js.map +1 -1
  20. package/dist/db/db.d.ts +113 -0
  21. package/dist/db/db.d.ts.map +1 -1
  22. package/dist/db/schema/attempt-streams.d.ts +113 -0
  23. package/dist/db/schema/attempt-streams.d.ts.map +1 -1
  24. package/dist/db/schema/attempt-streams.js +2 -0
  25. package/dist/db/schema/attempt-streams.js.map +1 -1
  26. package/dist/db/streams.d.ts +3 -1
  27. package/dist/db/streams.d.ts.map +1 -1
  28. package/dist/db/streams.js +6 -1
  29. package/dist/db/streams.js.map +1 -1
  30. package/dist/tsconfig.test.tsbuildinfo +1 -1
  31. package/drizzle/0002_wise_dorian_gray.sql +1 -0
  32. package/drizzle/meta/0002_snapshot.json +561 -0
  33. package/drizzle/meta/_journal.json +7 -0
  34. package/package.json +4 -4
  35. package/src/core/append-logs.test.ts +57 -0
  36. package/src/core/append-logs.ts +15 -3
  37. package/src/core/close-stream.ts +17 -1
  38. package/src/core/entities/attempt-stream.ts +2 -1
  39. package/src/core/session/claude/rows.ts +295 -13
  40. package/src/core/session/claude-parser.test.ts +620 -11
  41. package/src/core/session/claude-parser.ts +59 -10
  42. package/src/db/schema/attempt-streams.ts +3 -1
  43. package/src/db/streams.ts +10 -2
  44. package/test/fixtures/claude-session/agent-step.jsonl +12 -0
  45. package/test/fixtures/claude-session/sdk-system-subtypes.json +32 -0
  46. package/test/fixtures/claude-session.ts +45 -0
  47. package/tsconfig.build.tsbuildinfo +1 -1
@@ -1,10 +1,14 @@
1
- import type {SessionViewRow} from '@shipfox/api-logs-dto';
1
+ import type {SessionViewRow, SessionViewToolCallRow} from '@shipfox/api-logs-dto';
2
2
  import {z} from 'zod';
3
3
  import {
4
4
  assistantRows,
5
+ authStatusRow,
6
+ flushPendingToolRows,
5
7
  PURE_PROGRESS_CLAUDE_SYSTEM_SUBTYPES,
8
+ rateLimitRow,
6
9
  resultRow,
7
10
  systemRow,
11
+ toolUseSummary,
8
12
  userRows,
9
13
  } from './claude/rows.js';
10
14
  import {stringField} from './object.js';
@@ -21,10 +25,25 @@ export interface ClaudeParseContext {
21
25
  hasInit: boolean;
22
26
  sessionId: string | null;
23
27
  turn: number;
28
+ pendingToolRows: SessionViewRow[];
29
+ toolCallRows: Map<string, SessionViewToolCallRow>;
24
30
  }
25
31
 
26
- export function createClaudeParseContext(): ClaudeParseContext {
27
- return {hasInit: false, sessionId: null, turn: 0};
32
+ export function createClaudeParseContext(
33
+ pendingToolRows: readonly SessionViewRow[] = [],
34
+ state: Pick<ClaudeParseContext, 'hasInit' | 'sessionId' | 'turn'> = {
35
+ hasInit: false,
36
+ sessionId: null,
37
+ turn: 0,
38
+ },
39
+ ): ClaudeParseContext {
40
+ const rows = [...pendingToolRows];
41
+ const toolCallRows = new Map<string, SessionViewToolCallRow>();
42
+ for (const row of rows) {
43
+ if (row.kind === 'tool-call' && row.id !== null) toolCallRows.set(row.id, row);
44
+ }
45
+
46
+ return {...state, pendingToolRows: rows, toolCallRows};
28
47
  }
29
48
 
30
49
  export function claudeInitSessionId(record: AgentSessionRecord): string | undefined {
@@ -58,11 +77,13 @@ export function parseClaudeSessionRecord(
58
77
  try {
59
78
  json = JSON.parse(record.data);
60
79
  } catch {
61
- return [rawRecordRow(record, 'Malformed session entry')];
80
+ return [...flushPendingToolRows(context), rawRecordRow(record, 'Malformed session entry')];
62
81
  }
63
82
 
64
83
  const parsed = claudeMessageSchema.safeParse(json);
65
- if (!parsed.success) return [rawRecordRow(record, 'Unsupported Claude message')];
84
+ if (!parsed.success) {
85
+ return [...flushPendingToolRows(context), rawRecordRow(record, 'Unsupported Claude message')];
86
+ }
66
87
 
67
88
  const message = parsed.data;
68
89
  if (
@@ -75,15 +96,43 @@ export function parseClaudeSessionRecord(
75
96
 
76
97
  switch (message.type) {
77
98
  case 'system':
99
+ if (stringField(message, 'subtype') === 'init') {
100
+ return [...flushPendingToolRows(context), systemRow(record.ts, message, context)];
101
+ }
102
+ return deferRow(context, systemRow(record.ts, message, context));
78
103
  case 'init':
79
- return [systemRow(record.ts, message, context)];
104
+ return [...flushPendingToolRows(context), systemRow(record.ts, message, context)];
105
+ case 'tool_progress':
106
+ case 'prompt_suggestion':
107
+ // These messages describe an already-emitted tool call or an interactive client state.
108
+ // They have no standalone row representation and must not look like parser failures.
109
+ return [];
110
+ case 'tool_use_summary':
111
+ return toolUseSummary(message, context) ? flushPendingToolRows(context) : [];
112
+ case 'auth_status':
113
+ return deferRow(context, authStatusRow(record.ts, message));
114
+ case 'rate_limit_event':
115
+ return deferRow(context, rateLimitRow(record.ts, message));
80
116
  case 'assistant':
81
- return assistantRows(record.ts, message);
117
+ return [...flushPendingToolRows(context), ...assistantRows(record.ts, message, context)];
82
118
  case 'user':
83
- return userRows(record.ts, message);
119
+ return userRows(record.ts, message, context);
84
120
  case 'result':
85
- return [resultRow(record.ts, message, context.turn, isFinalResult)];
121
+ return [
122
+ ...flushPendingToolRows(context),
123
+ resultRow(record.ts, message, context.turn, isFinalResult),
124
+ ];
86
125
  default:
87
- return [rawRecordRow(record, `Unknown Claude message: ${message.type}`)];
126
+ return [
127
+ ...flushPendingToolRows(context),
128
+ rawRecordRow(record, `Unknown Claude message: ${message.type}`),
129
+ ];
88
130
  }
89
131
  }
132
+
133
+ function deferRow(context: ClaudeParseContext, row: SessionViewRow): readonly SessionViewRow[] {
134
+ if (context.toolCallRows.size === 0) return [row];
135
+
136
+ context.pendingToolRows.push(row);
137
+ return [];
138
+ }
@@ -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
  import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
3
3
  import {sql} from 'drizzle-orm';
4
4
  import {
@@ -55,6 +55,7 @@ export const attemptStreams = pgTable(
55
55
  claudeSessionId: text('claude_session_id'),
56
56
  claudeTurn: integer('claude_turn').notNull().default(0),
57
57
  claudePendingResult: jsonb('claude_pending_result').$type<SessionViewLifecycleRow>(),
58
+ claudePendingToolRows: jsonb('claude_pending_tool_rows').$type<SessionViewRow[]>(),
58
59
  truncated: boolean('truncated').notNull().default(false),
59
60
  objectKey: text('object_key'),
60
61
  createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
@@ -107,6 +108,7 @@ export function toAttemptStream(row: AttemptStreamDb): AttemptStream {
107
108
  claudeSessionId: row.claudeSessionId,
108
109
  claudeTurn: row.claudeTurn,
109
110
  claudePendingResult: row.claudePendingResult ?? null,
111
+ claudePendingToolRows: row.claudePendingToolRows ?? [],
110
112
  truncated: row.truncated,
111
113
  objectKey: row.objectKey,
112
114
  createdAt: row.createdAt,
package/src/db/streams.ts CHANGED
@@ -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
  import {and, asc, eq, getTableColumns, isNull, lt, notInArray, sql} from 'drizzle-orm';
3
3
  import type {AttemptStream, StreamCloseReason} from '#core/entities/attempt-stream.js';
4
4
  import {LeaseStreamMismatchError} from '#core/errors.js';
@@ -169,6 +169,7 @@ export async function setClaudeParseContext(
169
169
  sessionId: string | null;
170
170
  turn: number;
171
171
  pendingResult: SessionViewLifecycleRow | null;
172
+ pendingToolRows: readonly SessionViewRow[];
172
173
  },
173
174
  ): Promise<void> {
174
175
  await tx
@@ -178,6 +179,7 @@ export async function setClaudeParseContext(
178
179
  claudeSessionId: params.sessionId,
179
180
  claudeTurn: params.turn,
180
181
  claudePendingResult: params.pendingResult,
182
+ claudePendingToolRows: [...params.pendingToolRows],
181
183
  updatedAt: sql`now()`,
182
184
  })
183
185
  .where(eq(attemptStreams.id, params.streamId));
@@ -193,7 +195,11 @@ export async function setClaudeParseContext(
193
195
  export async function markStreamClosed(
194
196
  tx: Transaction,
195
197
  params: {streamId: string; reason: StreamCloseReason; markTruncated: boolean},
196
- ): Promise<{stream: AttemptStream; pendingClaudeResult: SessionViewLifecycleRow | null} | null> {
198
+ ): Promise<{
199
+ stream: AttemptStream;
200
+ pendingClaudeResult: SessionViewLifecycleRow | null;
201
+ pendingClaudeToolRows: SessionViewRow[];
202
+ } | null> {
197
203
  const [open] = await tx
198
204
  .select()
199
205
  .from(attemptStreams)
@@ -209,6 +215,7 @@ export async function markStreamClosed(
209
215
  closedAt: sql`now()`,
210
216
  updatedAt: sql`now()`,
211
217
  claudePendingResult: null,
218
+ claudePendingToolRows: null,
212
219
  ...(params.markTruncated ? {truncated: true} : {}),
213
220
  })
214
221
  .where(and(eq(attemptStreams.id, params.streamId), eq(attemptStreams.state, 'open')))
@@ -218,6 +225,7 @@ export async function markStreamClosed(
218
225
  ? {
219
226
  stream: toAttemptStream(row),
220
227
  pendingClaudeResult: open.claudePendingResult ?? null,
228
+ pendingClaudeToolRows: open.claudePendingToolRows ?? [],
221
229
  }
222
230
  : null;
223
231
  }
@@ -0,0 +1,12 @@
1
+ {"type":"system","subtype":"init","apiKeySource":"user","claude_code_version":"1.0.0","cwd":"/workspace","tools":["Read","Bash"],"mcp_servers":[],"model":"claude-sonnet-4-20250514","permissionMode":"default","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"uuid":"init-1","session_id":"session-1"}
2
+ {"type":"assistant","message":{"id":"msg-1","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"I will inspect the repository."},{"type":"tool_use","id":"toolu-1","name":"Read","input":{"file_path":"README.md"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":100,"output_tokens":20}},"parent_tool_use_id":null,"uuid":"assistant-1","session_id":"session-1"}
3
+ {"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu-1","content":[{"type":"text","text":"# Shipfox\n"}],"is_error":false}]},"parent_tool_use_id":null,"uuid":"user-1","session_id":"session-1"}
4
+ {"type":"system","subtype":"permission_denied","tool_name":"mcp__shipfox_outputs__set_output","tool_use_id":"toolu-2","decision_reason_type":"mode","decision_reason":"Permission mode","message":"Permission denied","uuid":"permission-1","session_id":"session-1"}
5
+ {"type":"system","subtype":"api_retry","attempt":2,"max_retries":3,"retry_delay_ms":1500,"error_status":429,"error":"rate_limit","uuid":"retry-1","session_id":"session-1"}
6
+ {"type":"system","subtype":"hook_response","hook_id":"hook-1","hook_name":"PreToolUse","hook_event":"PreToolUse","output":"Hook completed","stdout":"","stderr":"","exit_code":0,"outcome":"success","uuid":"hook-1","session_id":"session-1"}
7
+ {"type":"system","subtype":"compact_boundary","compact_metadata":{"trigger":"auto","pre_tokens":12000,"post_tokens":4000,"duration_ms":250},"uuid":"compact-1","session_id":"session-1"}
8
+ {"type":"result","subtype":"success","is_error":false,"result":"First turn response","duration_ms":1200,"duration_api_ms":900,"num_turns":1,"total_cost_usd":0.0042,"uuid":"result-1","session_id":"session-1"}
9
+ {"type":"system","subtype":"init","apiKeySource":"user","claude_code_version":"1.0.0","cwd":"/workspace","tools":["Read","Bash"],"mcp_servers":[],"model":"claude-sonnet-4-20250514","permissionMode":"default","slash_commands":[],"output_style":"default","skills":[],"plugins":[],"uuid":"init-2","session_id":"session-1"}
10
+ {"type":"user","message":{"role":"user","content":"The previous turn ended without setting required workflow outputs: answer. Call set_output for each missing key, then provide your final response."},"parent_tool_use_id":null,"isSynthetic":true,"uuid":"user-2","session_id":"session-1"}
11
+ {"type":"assistant","message":{"id":"msg-2","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"The answer is ready."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":120,"output_tokens":8}},"parent_tool_use_id":null,"uuid":"assistant-2","session_id":"session-1"}
12
+ {"type":"result","subtype":"success","is_error":false,"result":"Final response","duration_ms":800,"duration_api_ms":600,"num_turns":1,"total_cost_usd":0.0021,"uuid":"result-2","session_id":"session-1"}
@@ -0,0 +1,32 @@
1
+ {
2
+ "package": "@anthropic-ai/claude-agent-sdk",
3
+ "version": "0.3.200",
4
+ "subtypes": [
5
+ "api_retry",
6
+ "commands_changed",
7
+ "compact_boundary",
8
+ "elicitation_complete",
9
+ "files_persisted",
10
+ "hook_progress",
11
+ "hook_response",
12
+ "hook_started",
13
+ "informational",
14
+ "init",
15
+ "local_command_output",
16
+ "memory_recall",
17
+ "mirror_error",
18
+ "model_refusal_fallback",
19
+ "model_refusal_no_fallback",
20
+ "notification",
21
+ "permission_denied",
22
+ "plugin_install",
23
+ "session_state_changed",
24
+ "status",
25
+ "task_notification",
26
+ "task_progress",
27
+ "task_started",
28
+ "task_updated",
29
+ "thinking_tokens",
30
+ "worker_shutting_down"
31
+ ]
32
+ }
@@ -0,0 +1,45 @@
1
+ import {readFileSync} from 'node:fs';
2
+ import type {AgentSessionRecord} from '#core/session/session-record.js';
3
+
4
+ export interface ClaudeSystemSubtypeFixture {
5
+ package: string;
6
+ version: string;
7
+ subtypes: string[];
8
+ }
9
+
10
+ const SDK_ENTRY_URL = import.meta.resolve('@anthropic-ai/claude-agent-sdk');
11
+ const SDK_TYPES_URL = new URL('./sdk.d.ts', SDK_ENTRY_URL);
12
+
13
+ export function readClaudeSessionFixture(name: string): AgentSessionRecord[] {
14
+ const contents = readFileSync(
15
+ new URL(`./claude-session/${name}`, import.meta.url),
16
+ 'utf8',
17
+ ).trim();
18
+
19
+ return contents.split('\n').map((data, index) => ({data, ts: index + 1}));
20
+ }
21
+
22
+ export function readClaudeSystemSubtypeFixture(): ClaudeSystemSubtypeFixture {
23
+ const contents = readFileSync(
24
+ new URL('./claude-session/sdk-system-subtypes.json', import.meta.url),
25
+ 'utf8',
26
+ );
27
+ return JSON.parse(contents) as ClaudeSystemSubtypeFixture;
28
+ }
29
+
30
+ export function readInstalledClaudeSdkVersion(): string {
31
+ const contents = readFileSync(new URL('./package.json', SDK_TYPES_URL), 'utf8');
32
+ const packageJson = JSON.parse(contents) as {version?: unknown};
33
+ if (typeof packageJson.version !== 'string') {
34
+ throw new Error('The installed Claude SDK package has no version');
35
+ }
36
+ return packageJson.version;
37
+ }
38
+
39
+ export function readInstalledClaudeSystemSubtypes(): string[] {
40
+ const declaration = readFileSync(SDK_TYPES_URL, 'utf8');
41
+ return [...declaration.matchAll(/type: 'system';\s+subtype: '([^']+)'/g)]
42
+ .map((match) => match[1])
43
+ .filter((subtype): subtype is string => subtype !== undefined)
44
+ .sort();
45
+ }