@shipfox/api-logs 9.3.0 → 10.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 +25 -0
- package/dist/core/append-logs.d.ts.map +1 -1
- package/dist/core/append-logs.js +100 -12
- package/dist/core/append-logs.js.map +1 -1
- package/dist/core/close-stream.d.ts +6 -5
- package/dist/core/close-stream.d.ts.map +1 -1
- package/dist/core/close-stream.js +25 -7
- package/dist/core/close-stream.js.map +1 -1
- package/dist/core/entities/attempt-stream.d.ts +5 -0
- package/dist/core/entities/attempt-stream.d.ts.map +1 -1
- package/dist/core/entities/attempt-stream.js +1 -1
- package/dist/core/entities/attempt-stream.js.map +1 -1
- package/dist/core/session/claude/rows.d.ts +4 -2
- package/dist/core/session/claude/rows.d.ts.map +1 -1
- package/dist/core/session/claude/rows.js +44 -9
- package/dist/core/session/claude/rows.js.map +1 -1
- package/dist/core/session/claude-parser.d.ts +8 -1
- package/dist/core/session/claude-parser.d.ts.map +1 -1
- package/dist/core/session/claude-parser.js +30 -4
- package/dist/core/session/claude-parser.js.map +1 -1
- package/dist/core/session/parse-session.d.ts +6 -1
- package/dist/core/session/parse-session.d.ts.map +1 -1
- package/dist/core/session/parse-session.js +2 -2
- package/dist/core/session/parse-session.js.map +1 -1
- package/dist/db/db.d.ts +94 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/schema/attempt-streams.d.ts +94 -0
- package/dist/db/schema/attempt-streams.d.ts.map +1 -1
- package/dist/db/schema/attempt-streams.js +9 -1
- package/dist/db/schema/attempt-streams.js.map +1 -1
- package/dist/db/streams.d.ts +17 -6
- package/dist/db/streams.d.ts.map +1 -1
- package/dist/db/streams.js +21 -6
- package/dist/db/streams.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0001_strong_major_mapleleaf.sql +4 -0
- package/drizzle/meta/0001_snapshot.json +555 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +5 -5
- package/src/core/append-logs.test.ts +144 -4
- package/src/core/append-logs.ts +158 -6
- package/src/core/close-stream.ts +26 -7
- package/src/core/entities/attempt-stream.ts +6 -0
- package/src/core/finalize-attempt-stream.test.ts +57 -0
- package/src/core/session/claude/rows.ts +57 -7
- package/src/core/session/claude-parser.test.ts +118 -1
- package/src/core/session/claude-parser.ts +55 -4
- package/src/core/session/parse-session.ts +10 -1
- package/src/db/schema/attempt-streams.ts +10 -0
- package/src/db/streams.ts +43 -7
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {PURE_PROGRESS_CLAUDE_SYSTEM_SUBTYPES} from './claude/rows.js';
|
|
2
|
+
import {createClaudeParseContext, parseClaudeSessionRecord} from './claude-parser.js';
|
|
2
3
|
|
|
3
4
|
const record = (data: unknown, ts = 1) => ({
|
|
4
5
|
ts,
|
|
@@ -54,6 +55,122 @@ describe('parseClaudeSessionRecord', () => {
|
|
|
54
55
|
]);
|
|
55
56
|
});
|
|
56
57
|
|
|
58
|
+
it.each([
|
|
59
|
+
...PURE_PROGRESS_CLAUDE_SYSTEM_SUBTYPES,
|
|
60
|
+
])('drops pure-progress system subtype %s', (subtype) => {
|
|
61
|
+
const rows = parseClaudeSessionRecord(record({type: 'system', subtype}));
|
|
62
|
+
|
|
63
|
+
expect(rows).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('reports repeated init and result records as turns within one session', () => {
|
|
67
|
+
const context = createClaudeParseContext();
|
|
68
|
+
const rows = [
|
|
69
|
+
...parseClaudeSessionRecord(
|
|
70
|
+
record({type: 'system', subtype: 'init', session_id: 'session-1'}),
|
|
71
|
+
context,
|
|
72
|
+
),
|
|
73
|
+
...parseClaudeSessionRecord(
|
|
74
|
+
record({type: 'result', subtype: 'success', result: 'first response'}),
|
|
75
|
+
context,
|
|
76
|
+
false,
|
|
77
|
+
),
|
|
78
|
+
...parseClaudeSessionRecord(
|
|
79
|
+
record({type: 'system', subtype: 'init', session_id: 'session-1'}),
|
|
80
|
+
context,
|
|
81
|
+
),
|
|
82
|
+
...parseClaudeSessionRecord(
|
|
83
|
+
record({type: 'result', subtype: 'success', result: 'final response'}),
|
|
84
|
+
context,
|
|
85
|
+
),
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
expect(rows).toEqual([
|
|
89
|
+
{
|
|
90
|
+
kind: 'lifecycle',
|
|
91
|
+
timestamp: 1,
|
|
92
|
+
label: 'Session started',
|
|
93
|
+
detail: 'session-1',
|
|
94
|
+
meta: [],
|
|
95
|
+
tone: 'default',
|
|
96
|
+
terminalFailure: false,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
kind: 'lifecycle',
|
|
100
|
+
timestamp: 1,
|
|
101
|
+
label: 'Turn 1 completed',
|
|
102
|
+
detail: 'first response',
|
|
103
|
+
meta: [{label: 'turn', value: '1'}],
|
|
104
|
+
tone: 'default',
|
|
105
|
+
terminalFailure: false,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
kind: 'lifecycle',
|
|
109
|
+
timestamp: 1,
|
|
110
|
+
label: 'Turn 2 started',
|
|
111
|
+
detail: 'session-1',
|
|
112
|
+
meta: [{label: 'turn', value: '2'}],
|
|
113
|
+
tone: 'default',
|
|
114
|
+
terminalFailure: false,
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
kind: 'lifecycle',
|
|
118
|
+
timestamp: 1,
|
|
119
|
+
label: 'Session completed',
|
|
120
|
+
detail: 'final response',
|
|
121
|
+
meta: [],
|
|
122
|
+
tone: 'default',
|
|
123
|
+
terminalFailure: false,
|
|
124
|
+
},
|
|
125
|
+
]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('keeps non-init system messages as session events', () => {
|
|
129
|
+
const rows = parseClaudeSessionRecord(
|
|
130
|
+
record({type: 'system', subtype: 'custom_event', session_id: 'session-1'}),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(rows).toEqual([
|
|
134
|
+
{
|
|
135
|
+
kind: 'lifecycle',
|
|
136
|
+
timestamp: 1,
|
|
137
|
+
label: 'Session event',
|
|
138
|
+
detail: 'session-1',
|
|
139
|
+
meta: [],
|
|
140
|
+
tone: 'default',
|
|
141
|
+
terminalFailure: false,
|
|
142
|
+
},
|
|
143
|
+
]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('labels workflow-output re-prompts as platform messages', () => {
|
|
147
|
+
const rows = parseClaudeSessionRecord(
|
|
148
|
+
record({
|
|
149
|
+
type: 'user',
|
|
150
|
+
message: {
|
|
151
|
+
role: 'user',
|
|
152
|
+
content:
|
|
153
|
+
'The previous turn ended without setting required workflow outputs: answer. ' +
|
|
154
|
+
'Call set_output for each missing key, then provide your final response.',
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
expect(rows).toEqual([
|
|
160
|
+
{
|
|
161
|
+
kind: 'message',
|
|
162
|
+
timestamp: 1,
|
|
163
|
+
role: 'platform',
|
|
164
|
+
label: 'platform',
|
|
165
|
+
meta: [],
|
|
166
|
+
text:
|
|
167
|
+
'The previous turn ended without setting required workflow outputs: answer. ' +
|
|
168
|
+
'Call set_output for each missing key, then provide your final response.',
|
|
169
|
+
terminalFailure: false,
|
|
170
|
+
},
|
|
171
|
+
]);
|
|
172
|
+
});
|
|
173
|
+
|
|
57
174
|
it('expands assistant text, thinking, and tool-use blocks in order', () => {
|
|
58
175
|
const rows = parseClaudeSessionRecord(
|
|
59
176
|
record({
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type {SessionViewRow} from '@shipfox/api-logs-dto';
|
|
2
2
|
import {z} from 'zod';
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
assistantRows,
|
|
5
|
+
PURE_PROGRESS_CLAUDE_SYSTEM_SUBTYPES,
|
|
6
|
+
resultRow,
|
|
7
|
+
systemRow,
|
|
8
|
+
userRows,
|
|
9
|
+
} from './claude/rows.js';
|
|
10
|
+
import {stringField} from './object.js';
|
|
4
11
|
import {rawRecordRow} from './rows.js';
|
|
5
12
|
import type {AgentSessionRecord} from './session-record.js';
|
|
6
13
|
|
|
@@ -10,7 +17,43 @@ const claudeMessageSchema = z
|
|
|
10
17
|
})
|
|
11
18
|
.catchall(z.unknown());
|
|
12
19
|
|
|
13
|
-
export
|
|
20
|
+
export interface ClaudeParseContext {
|
|
21
|
+
hasInit: boolean;
|
|
22
|
+
sessionId: string | null;
|
|
23
|
+
turn: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createClaudeParseContext(): ClaudeParseContext {
|
|
27
|
+
return {hasInit: false, sessionId: null, turn: 0};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function claudeInitSessionId(record: AgentSessionRecord): string | undefined {
|
|
31
|
+
let json: unknown;
|
|
32
|
+
try {
|
|
33
|
+
json = JSON.parse(record.data);
|
|
34
|
+
} catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parsed = claudeMessageSchema.safeParse(json);
|
|
39
|
+
if (!parsed.success) return undefined;
|
|
40
|
+
|
|
41
|
+
const message = parsed.data;
|
|
42
|
+
if (
|
|
43
|
+
message.type !== 'init' &&
|
|
44
|
+
!(message.type === 'system' && stringField(message, 'subtype') === 'init')
|
|
45
|
+
) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return stringField(message, 'session_id') ?? stringField(message, 'sessionId');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function parseClaudeSessionRecord(
|
|
53
|
+
record: AgentSessionRecord,
|
|
54
|
+
context: ClaudeParseContext = createClaudeParseContext(),
|
|
55
|
+
isFinalResult = true,
|
|
56
|
+
): readonly SessionViewRow[] {
|
|
14
57
|
let json: unknown;
|
|
15
58
|
try {
|
|
16
59
|
json = JSON.parse(record.data);
|
|
@@ -22,16 +65,24 @@ export function parseClaudeSessionRecord(record: AgentSessionRecord): readonly S
|
|
|
22
65
|
if (!parsed.success) return [rawRecordRow(record, 'Unsupported Claude message')];
|
|
23
66
|
|
|
24
67
|
const message = parsed.data;
|
|
68
|
+
if (
|
|
69
|
+
message.type === 'system' &&
|
|
70
|
+
typeof message.subtype === 'string' &&
|
|
71
|
+
PURE_PROGRESS_CLAUDE_SYSTEM_SUBTYPES.has(message.subtype)
|
|
72
|
+
) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
25
76
|
switch (message.type) {
|
|
26
77
|
case 'system':
|
|
27
78
|
case 'init':
|
|
28
|
-
return [systemRow(record.ts, message)];
|
|
79
|
+
return [systemRow(record.ts, message, context)];
|
|
29
80
|
case 'assistant':
|
|
30
81
|
return assistantRows(record.ts, message);
|
|
31
82
|
case 'user':
|
|
32
83
|
return userRows(record.ts, message);
|
|
33
84
|
case 'result':
|
|
34
|
-
return [resultRow(record.ts, message)];
|
|
85
|
+
return [resultRow(record.ts, message, context.turn, isFinalResult)];
|
|
35
86
|
default:
|
|
36
87
|
return [rawRecordRow(record, `Unknown Claude message: ${message.type}`)];
|
|
37
88
|
}
|
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import type {SessionViewRow} from '@shipfox/api-logs-dto';
|
|
2
2
|
import type {Harness} from '@shipfox/workflow-document';
|
|
3
|
+
import type {ClaudeParseContext} from './claude-parser.js';
|
|
3
4
|
import {parseClaudeSessionRecord} from './claude-parser.js';
|
|
4
5
|
import {parsePiSessionRecord} from './pi-parser.js';
|
|
5
6
|
import type {AgentSessionRecord} from './session-record.js';
|
|
6
7
|
|
|
8
|
+
export interface SessionParseContext {
|
|
9
|
+
claude?: ClaudeParseContext;
|
|
10
|
+
isFinalResult?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
export function parseSessionRecord(
|
|
8
14
|
record: AgentSessionRecord,
|
|
9
15
|
harness: Harness,
|
|
16
|
+
context?: SessionParseContext,
|
|
10
17
|
): readonly SessionViewRow[] {
|
|
11
18
|
try {
|
|
12
|
-
return harness === 'claude'
|
|
19
|
+
return harness === 'claude'
|
|
20
|
+
? parseClaudeSessionRecord(record, context?.claude, context?.isFinalResult ?? true)
|
|
21
|
+
: parsePiSessionRecord(record);
|
|
13
22
|
} catch {
|
|
14
23
|
return [
|
|
15
24
|
{
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type {SessionViewLifecycleRow} from '@shipfox/api-logs-dto';
|
|
1
2
|
import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
|
|
2
3
|
import {sql} from 'drizzle-orm';
|
|
3
4
|
import {
|
|
@@ -5,6 +6,7 @@ import {
|
|
|
5
6
|
boolean,
|
|
6
7
|
index,
|
|
7
8
|
integer,
|
|
9
|
+
jsonb,
|
|
8
10
|
text,
|
|
9
11
|
timestamp,
|
|
10
12
|
uniqueIndex,
|
|
@@ -49,6 +51,10 @@ export const attemptStreams = pgTable(
|
|
|
49
51
|
.default('open'),
|
|
50
52
|
closeReason: text('close_reason', {enum: ['declared', 'timeout']}),
|
|
51
53
|
declaredTotalBytes: bigint('declared_total_bytes', {mode: 'number'}),
|
|
54
|
+
claudeHasInit: boolean('claude_has_init').notNull().default(false),
|
|
55
|
+
claudeSessionId: text('claude_session_id'),
|
|
56
|
+
claudeTurn: integer('claude_turn').notNull().default(0),
|
|
57
|
+
claudePendingResult: jsonb('claude_pending_result').$type<SessionViewLifecycleRow>(),
|
|
52
58
|
truncated: boolean('truncated').notNull().default(false),
|
|
53
59
|
objectKey: text('object_key'),
|
|
54
60
|
createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
|
|
@@ -97,6 +103,10 @@ export function toAttemptStream(row: AttemptStreamDb): AttemptStream {
|
|
|
97
103
|
state: row.state,
|
|
98
104
|
closeReason: row.closeReason,
|
|
99
105
|
declaredTotalBytes: row.declaredTotalBytes,
|
|
106
|
+
claudeHasInit: row.claudeHasInit,
|
|
107
|
+
claudeSessionId: row.claudeSessionId,
|
|
108
|
+
claudeTurn: row.claudeTurn,
|
|
109
|
+
claudePendingResult: row.claudePendingResult ?? null,
|
|
100
110
|
truncated: row.truncated,
|
|
101
111
|
objectKey: row.objectKey,
|
|
102
112
|
createdAt: row.createdAt,
|
package/src/db/streams.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type {SessionViewLifecycleRow} from '@shipfox/api-logs-dto';
|
|
1
2
|
import {and, asc, eq, getTableColumns, isNull, lt, notInArray, sql} from 'drizzle-orm';
|
|
2
3
|
import type {AttemptStream, StreamCloseReason} from '#core/entities/attempt-stream.js';
|
|
3
4
|
import {LeaseStreamMismatchError} from '#core/errors.js';
|
|
@@ -160,17 +161,46 @@ export async function setDeclaredTotalBytes(
|
|
|
160
161
|
.where(eq(attemptStreams.id, params.streamId));
|
|
161
162
|
}
|
|
162
163
|
|
|
164
|
+
export async function setClaudeParseContext(
|
|
165
|
+
tx: Transaction,
|
|
166
|
+
params: {
|
|
167
|
+
streamId: string;
|
|
168
|
+
hasInit: boolean;
|
|
169
|
+
sessionId: string | null;
|
|
170
|
+
turn: number;
|
|
171
|
+
pendingResult: SessionViewLifecycleRow | null;
|
|
172
|
+
},
|
|
173
|
+
): Promise<void> {
|
|
174
|
+
await tx
|
|
175
|
+
.update(attemptStreams)
|
|
176
|
+
.set({
|
|
177
|
+
claudeHasInit: params.hasInit,
|
|
178
|
+
claudeSessionId: params.sessionId,
|
|
179
|
+
claudeTurn: params.turn,
|
|
180
|
+
claudePendingResult: params.pendingResult,
|
|
181
|
+
updatedAt: sql`now()`,
|
|
182
|
+
})
|
|
183
|
+
.where(eq(attemptStreams.id, params.streamId));
|
|
184
|
+
}
|
|
185
|
+
|
|
163
186
|
/**
|
|
164
|
-
* Guarded close: flips `open → closed
|
|
165
|
-
*
|
|
166
|
-
* it
|
|
167
|
-
*
|
|
168
|
-
*
|
|
187
|
+
* Guarded close: locks the open row, flips `open → closed`, clears any pending Claude result,
|
|
188
|
+
* and stamps `closed_at`. The pending result is returned separately so the caller can
|
|
189
|
+
* materialize it before publishing the close event. Returns null when another path already
|
|
190
|
+
* closed the stream. `committed_length` is left untouched; it stays equal to the runner spool
|
|
191
|
+
* bytes. `truncated` is set only on the timeout path.
|
|
169
192
|
*/
|
|
170
193
|
export async function markStreamClosed(
|
|
171
194
|
tx: Transaction,
|
|
172
195
|
params: {streamId: string; reason: StreamCloseReason; markTruncated: boolean},
|
|
173
|
-
): Promise<AttemptStream | null> {
|
|
196
|
+
): Promise<{stream: AttemptStream; pendingClaudeResult: SessionViewLifecycleRow | null} | null> {
|
|
197
|
+
const [open] = await tx
|
|
198
|
+
.select()
|
|
199
|
+
.from(attemptStreams)
|
|
200
|
+
.where(and(eq(attemptStreams.id, params.streamId), eq(attemptStreams.state, 'open')))
|
|
201
|
+
.for('update');
|
|
202
|
+
if (!open) return null;
|
|
203
|
+
|
|
174
204
|
const [row] = await tx
|
|
175
205
|
.update(attemptStreams)
|
|
176
206
|
.set({
|
|
@@ -178,12 +208,18 @@ export async function markStreamClosed(
|
|
|
178
208
|
closeReason: params.reason,
|
|
179
209
|
closedAt: sql`now()`,
|
|
180
210
|
updatedAt: sql`now()`,
|
|
211
|
+
claudePendingResult: null,
|
|
181
212
|
...(params.markTruncated ? {truncated: true} : {}),
|
|
182
213
|
})
|
|
183
214
|
.where(and(eq(attemptStreams.id, params.streamId), eq(attemptStreams.state, 'open')))
|
|
184
215
|
.returning();
|
|
185
216
|
|
|
186
|
-
return row
|
|
217
|
+
return row
|
|
218
|
+
? {
|
|
219
|
+
stream: toAttemptStream(row),
|
|
220
|
+
pendingClaudeResult: open.claudePendingResult ?? null,
|
|
221
|
+
}
|
|
222
|
+
: null;
|
|
187
223
|
}
|
|
188
224
|
|
|
189
225
|
/** Open streams of a job, for the timeout sweep to force-close after the grace period. */
|