@shipfox/api-logs 2.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 +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +179 -0
- package/LICENSE +21 -0
- package/README.md +151 -0
- package/dist/api/object-storage.d.ts +69 -0
- package/dist/api/object-storage.d.ts.map +1 -0
- package/dist/api/object-storage.js +199 -0
- package/dist/api/object-storage.js.map +1 -0
- package/dist/config.d.ts +20 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +108 -0
- package/dist/config.js.map +1 -0
- package/dist/core/append-logs.d.ts +28 -0
- package/dist/core/append-logs.d.ts.map +1 -0
- package/dist/core/append-logs.js +318 -0
- package/dist/core/append-logs.js.map +1 -0
- package/dist/core/budget.d.ts +12 -0
- package/dist/core/budget.d.ts.map +1 -0
- package/dist/core/budget.js +9 -0
- package/dist/core/budget.js.map +1 -0
- package/dist/core/close-stream.d.ts +27 -0
- package/dist/core/close-stream.d.ts.map +1 -0
- package/dist/core/close-stream.js +59 -0
- package/dist/core/close-stream.js.map +1 -0
- package/dist/core/compaction.d.ts +28 -0
- package/dist/core/compaction.d.ts.map +1 -0
- package/dist/core/compaction.js +49 -0
- package/dist/core/compaction.js.map +1 -0
- package/dist/core/entities/attempt-stream.d.ts +37 -0
- package/dist/core/entities/attempt-stream.d.ts.map +1 -0
- package/dist/core/entities/attempt-stream.js +15 -0
- package/dist/core/entities/attempt-stream.js.map +1 -0
- package/dist/core/entities/job-accounting.d.ts +12 -0
- package/dist/core/entities/job-accounting.d.ts.map +1 -0
- package/dist/core/entities/job-accounting.js +6 -0
- package/dist/core/entities/job-accounting.js.map +1 -0
- package/dist/core/entities/log-object.d.ts +13 -0
- package/dist/core/entities/log-object.d.ts.map +1 -0
- package/dist/core/entities/log-object.js +9 -0
- package/dist/core/entities/log-object.js.map +1 -0
- package/dist/core/errors.d.ts +26 -0
- package/dist/core/errors.d.ts.map +1 -0
- package/dist/core/errors.js +31 -0
- package/dist/core/errors.js.map +1 -0
- package/dist/core/finalize-attempt-stream.d.ts +22 -0
- package/dist/core/finalize-attempt-stream.d.ts.map +1 -0
- package/dist/core/finalize-attempt-stream.js +48 -0
- package/dist/core/finalize-attempt-stream.js.map +1 -0
- package/dist/core/read-logs.d.ts +33 -0
- package/dist/core/read-logs.d.ts.map +1 -0
- package/dist/core/read-logs.js +49 -0
- package/dist/core/read-logs.js.map +1 -0
- package/dist/core/reap-stale-open-streams.d.ts +27 -0
- package/dist/core/reap-stale-open-streams.d.ts.map +1 -0
- package/dist/core/reap-stale-open-streams.js +50 -0
- package/dist/core/reap-stale-open-streams.js.map +1 -0
- package/dist/core/retention.d.ts +32 -0
- package/dist/core/retention.d.ts.map +1 -0
- package/dist/core/retention.js +148 -0
- package/dist/core/retention.js.map +1 -0
- package/dist/core/session/claude/rows.d.ts +6 -0
- package/dist/core/session/claude/rows.d.ts.map +1 -0
- package/dist/core/session/claude/rows.js +145 -0
- package/dist/core/session/claude/rows.js.map +1 -0
- package/dist/core/session/claude-parser.d.ts +4 -0
- package/dist/core/session/claude-parser.d.ts.map +1 -0
- package/dist/core/session/claude-parser.js +42 -0
- package/dist/core/session/claude-parser.js.map +1 -0
- package/dist/core/session/entry-schema.d.ts +51 -0
- package/dist/core/session/entry-schema.d.ts.map +1 -0
- package/dist/core/session/entry-schema.js +61 -0
- package/dist/core/session/entry-schema.js.map +1 -0
- package/dist/core/session/object.d.ts +12 -0
- package/dist/core/session/object.d.ts.map +1 -0
- package/dist/core/session/object.js +50 -0
- package/dist/core/session/object.js.map +1 -0
- package/dist/core/session/parse-session.d.ts +5 -0
- package/dist/core/session/parse-session.d.ts.map +1 -0
- package/dist/core/session/parse-session.js +18 -0
- package/dist/core/session/parse-session.js.map +1 -0
- package/dist/core/session/pi/message-rows.d.ts +4 -0
- package/dist/core/session/pi/message-rows.d.ts.map +1 -0
- package/dist/core/session/pi/message-rows.js +113 -0
- package/dist/core/session/pi/message-rows.js.map +1 -0
- package/dist/core/session/pi/metadata.d.ts +18 -0
- package/dist/core/session/pi/metadata.d.ts.map +1 -0
- package/dist/core/session/pi/metadata.js +120 -0
- package/dist/core/session/pi/metadata.js.map +1 -0
- package/dist/core/session/pi/text.d.ts +12 -0
- package/dist/core/session/pi/text.d.ts.map +1 -0
- package/dist/core/session/pi/text.js +59 -0
- package/dist/core/session/pi/text.js.map +1 -0
- package/dist/core/session/pi-parser.d.ts +5 -0
- package/dist/core/session/pi-parser.d.ts.map +1 -0
- package/dist/core/session/pi-parser.js +73 -0
- package/dist/core/session/pi-parser.js.map +1 -0
- package/dist/core/session/rows.d.ts +7 -0
- package/dist/core/session/rows.d.ts.map +1 -0
- package/dist/core/session/rows.js +39 -0
- package/dist/core/session/rows.js.map +1 -0
- package/dist/core/session/session-record.d.ts +5 -0
- package/dist/core/session/session-record.d.ts.map +1 -0
- package/dist/core/session/session-record.js +3 -0
- package/dist/core/session/session-record.js.map +1 -0
- package/dist/db/accounting.d.ts +33 -0
- package/dist/db/accounting.d.ts.map +1 -0
- package/dist/db/accounting.js +52 -0
- package/dist/db/accounting.js.map +1 -0
- package/dist/db/chunks.d.ts +62 -0
- package/dist/db/chunks.d.ts.map +1 -0
- package/dist/db/chunks.js +88 -0
- package/dist/db/chunks.js.map +1 -0
- package/dist/db/db.d.ts +752 -0
- package/dist/db/db.d.ts.map +1 -0
- package/dist/db/db.js +24 -0
- package/dist/db/db.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +5 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/schema/attempt-streams.d.ts +304 -0
- package/dist/db/schema/attempt-streams.d.ts.map +1 -0
- package/dist/db/schema/attempt-streams.js +100 -0
- package/dist/db/schema/attempt-streams.js.map +1 -0
- package/dist/db/schema/chunks.d.ts +166 -0
- package/dist/db/schema/chunks.d.ts.map +1 -0
- package/dist/db/schema/chunks.js +47 -0
- package/dist/db/schema/chunks.js.map +1 -0
- package/dist/db/schema/common.d.ts +29 -0
- package/dist/db/schema/common.d.ts.map +1 -0
- package/dist/db/schema/common.js +11 -0
- package/dist/db/schema/common.js.map +1 -0
- package/dist/db/schema/job-accounting.d.ts +143 -0
- package/dist/db/schema/job-accounting.d.ts.map +1 -0
- package/dist/db/schema/job-accounting.js +43 -0
- package/dist/db/schema/job-accounting.js.map +1 -0
- package/dist/db/schema/outbox.d.ts +195 -0
- package/dist/db/schema/outbox.d.ts.map +1 -0
- package/dist/db/schema/outbox.js +5 -0
- package/dist/db/schema/outbox.js.map +1 -0
- package/dist/db/streams.d.ts +141 -0
- package/dist/db/streams.d.ts.map +1 -0
- package/dist/db/streams.js +197 -0
- package/dist/db/streams.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +81 -0
- package/dist/index.js.map +1 -0
- package/dist/metrics/index.d.ts +2 -0
- package/dist/metrics/index.d.ts.map +1 -0
- package/dist/metrics/index.js +3 -0
- package/dist/metrics/index.js.map +1 -0
- package/dist/metrics/instance.d.ts +14 -0
- package/dist/metrics/instance.d.ts.map +1 -0
- package/dist/metrics/instance.js +16 -0
- package/dist/metrics/instance.js.map +1 -0
- package/dist/metrics/service.d.ts +2 -0
- package/dist/metrics/service.d.ts.map +1 -0
- package/dist/metrics/service.js +20 -0
- package/dist/metrics/service.js.map +1 -0
- package/dist/presentation/dto/read-logs.d.ts +4 -0
- package/dist/presentation/dto/read-logs.d.ts.map +1 -0
- package/dist/presentation/dto/read-logs.js +22 -0
- package/dist/presentation/dto/read-logs.js.map +1 -0
- package/dist/presentation/routes/append-logs.d.ts +2 -0
- package/dist/presentation/routes/append-logs.d.ts.map +1 -0
- package/dist/presentation/routes/append-logs.js +71 -0
- package/dist/presentation/routes/append-logs.js.map +1 -0
- package/dist/presentation/routes/index.d.ts +3 -0
- package/dist/presentation/routes/index.d.ts.map +1 -0
- package/dist/presentation/routes/index.js +32 -0
- package/dist/presentation/routes/index.js.map +1 -0
- package/dist/presentation/routes/read-logs.d.ts +2 -0
- package/dist/presentation/routes/read-logs.d.ts.map +1 -0
- package/dist/presentation/routes/read-logs.js +41 -0
- package/dist/presentation/routes/read-logs.js.map +1 -0
- package/dist/presentation/subscribers/on-job-terminated.d.ts +8 -0
- package/dist/presentation/subscribers/on-job-terminated.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-job-terminated.js +32 -0
- package/dist/presentation/subscribers/on-job-terminated.js.map +1 -0
- package/dist/presentation/subscribers/on-log-stream-closed.d.ts +9 -0
- package/dist/presentation/subscribers/on-log-stream-closed.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-log-stream-closed.js +31 -0
- package/dist/presentation/subscribers/on-log-stream-closed.js.map +1 -0
- package/dist/presentation/subscribers/on-step-attempt-terminated.d.ts +3 -0
- package/dist/presentation/subscribers/on-step-attempt-terminated.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-step-attempt-terminated.js +14 -0
- package/dist/presentation/subscribers/on-step-attempt-terminated.js.map +1 -0
- package/dist/temporal/activities/close-abandoned-streams.d.ts +12 -0
- package/dist/temporal/activities/close-abandoned-streams.d.ts.map +1 -0
- package/dist/temporal/activities/close-abandoned-streams.js +33 -0
- package/dist/temporal/activities/close-abandoned-streams.js.map +1 -0
- package/dist/temporal/activities/compact-stream.d.ts +33 -0
- package/dist/temporal/activities/compact-stream.d.ts.map +1 -0
- package/dist/temporal/activities/compact-stream.js +95 -0
- package/dist/temporal/activities/compact-stream.js.map +1 -0
- package/dist/temporal/activities/compaction-reconcile.d.ts +16 -0
- package/dist/temporal/activities/compaction-reconcile.d.ts.map +1 -0
- package/dist/temporal/activities/compaction-reconcile.js +52 -0
- package/dist/temporal/activities/compaction-reconcile.js.map +1 -0
- package/dist/temporal/activities/index.d.ts +14 -0
- package/dist/temporal/activities/index.d.ts.map +1 -0
- package/dist/temporal/activities/index.js +16 -0
- package/dist/temporal/activities/index.js.map +1 -0
- package/dist/temporal/activities/reap-stale-open-streams.d.ts +4 -0
- package/dist/temporal/activities/reap-stale-open-streams.d.ts.map +1 -0
- package/dist/temporal/activities/reap-stale-open-streams.js +11 -0
- package/dist/temporal/activities/reap-stale-open-streams.js.map +1 -0
- package/dist/temporal/activities/retention-sweep.d.ts +7 -0
- package/dist/temporal/activities/retention-sweep.d.ts.map +1 -0
- package/dist/temporal/activities/retention-sweep.js +19 -0
- package/dist/temporal/activities/retention-sweep.js.map +1 -0
- package/dist/temporal/constants.d.ts +7 -0
- package/dist/temporal/constants.d.ts.map +1 -0
- package/dist/temporal/constants.js +13 -0
- package/dist/temporal/constants.js.map +1 -0
- package/dist/temporal/workflows/close-abandoned-streams.d.ts +11 -0
- package/dist/temporal/workflows/close-abandoned-streams.d.ts.map +1 -0
- package/dist/temporal/workflows/close-abandoned-streams.js +22 -0
- package/dist/temporal/workflows/close-abandoned-streams.js.map +1 -0
- package/dist/temporal/workflows/compact-stream.d.ts +6 -0
- package/dist/temporal/workflows/compact-stream.d.ts.map +1 -0
- package/dist/temporal/workflows/compact-stream.js +25 -0
- package/dist/temporal/workflows/compact-stream.js.map +1 -0
- package/dist/temporal/workflows/compaction-reconcile-cron.d.ts +3 -0
- package/dist/temporal/workflows/compaction-reconcile-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/compaction-reconcile-cron.js +15 -0
- package/dist/temporal/workflows/compaction-reconcile-cron.js.map +1 -0
- package/dist/temporal/workflows/index.d.ts +6 -0
- package/dist/temporal/workflows/index.d.ts.map +1 -0
- package/dist/temporal/workflows/index.js +7 -0
- package/dist/temporal/workflows/index.js.map +1 -0
- package/dist/temporal/workflows/reap-stale-open-streams-cron.d.ts +2 -0
- package/dist/temporal/workflows/reap-stale-open-streams-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/reap-stale-open-streams-cron.js +15 -0
- package/dist/temporal/workflows/reap-stale-open-streams-cron.js.map +1 -0
- package/dist/temporal/workflows/retention-sweep-cron.d.ts +2 -0
- package/dist/temporal/workflows/retention-sweep-cron.d.ts.map +1 -0
- package/dist/temporal/workflows/retention-sweep-cron.js +23 -0
- package/dist/temporal/workflows/retention-sweep-cron.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/drizzle/0000_initial.sql +65 -0
- package/drizzle/meta/0000_snapshot.json +529 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +7 -0
- package/package.json +84 -0
- package/src/api/object-storage-credentials.test.ts +70 -0
- package/src/api/object-storage.test.ts +64 -0
- package/src/api/object-storage.ts +226 -0
- package/src/config.test.ts +118 -0
- package/src/config.ts +130 -0
- package/src/core/append-logs.test.ts +563 -0
- package/src/core/append-logs.ts +384 -0
- package/src/core/budget.test.ts +30 -0
- package/src/core/budget.ts +19 -0
- package/src/core/close-stream.ts +72 -0
- package/src/core/compaction.ts +68 -0
- package/src/core/entities/attempt-stream.ts +38 -0
- package/src/core/entities/job-accounting.ts +11 -0
- package/src/core/entities/log-object.test.ts +25 -0
- package/src/core/entities/log-object.ts +18 -0
- package/src/core/errors.ts +37 -0
- package/src/core/finalize-attempt-stream.test.ts +90 -0
- package/src/core/finalize-attempt-stream.ts +59 -0
- package/src/core/read-logs.ts +77 -0
- package/src/core/reap-stale-open-streams.test.ts +137 -0
- package/src/core/reap-stale-open-streams.ts +60 -0
- package/src/core/retention.test.ts +388 -0
- package/src/core/retention.ts +184 -0
- package/src/core/session/claude/rows.ts +223 -0
- package/src/core/session/claude-parser.test.ts +199 -0
- package/src/core/session/claude-parser.ts +38 -0
- package/src/core/session/entry-schema.ts +82 -0
- package/src/core/session/object.ts +57 -0
- package/src/core/session/parse-session.ts +23 -0
- package/src/core/session/pi/message-rows.ts +211 -0
- package/src/core/session/pi/metadata.ts +148 -0
- package/src/core/session/pi/text.ts +83 -0
- package/src/core/session/pi-parser.test.ts +451 -0
- package/src/core/session/pi-parser.ts +107 -0
- package/src/core/session/rows.ts +38 -0
- package/src/core/session/session-record.ts +4 -0
- package/src/db/accounting.ts +81 -0
- package/src/db/chunks.ts +143 -0
- package/src/db/db.ts +22 -0
- package/src/db/index.ts +4 -0
- package/src/db/schema/attempt-streams.ts +106 -0
- package/src/db/schema/chunks.ts +43 -0
- package/src/db/schema/common.ts +12 -0
- package/src/db/schema/job-accounting.ts +38 -0
- package/src/db/schema/outbox.ts +4 -0
- package/src/db/streams.test.ts +47 -0
- package/src/db/streams.ts +368 -0
- package/src/index.ts +77 -0
- package/src/metrics/index.ts +1 -0
- package/src/metrics/instance.ts +33 -0
- package/src/metrics/service.ts +23 -0
- package/src/presentation/dto/read-logs.ts +24 -0
- package/src/presentation/routes/append-logs.test.ts +279 -0
- package/src/presentation/routes/append-logs.ts +66 -0
- package/src/presentation/routes/index.ts +27 -0
- package/src/presentation/routes/read-logs.test.ts +504 -0
- package/src/presentation/routes/read-logs.ts +38 -0
- package/src/presentation/subscribers/on-job-terminated.test.ts +64 -0
- package/src/presentation/subscribers/on-job-terminated.ts +26 -0
- package/src/presentation/subscribers/on-log-stream-closed.test.ts +63 -0
- package/src/presentation/subscribers/on-log-stream-closed.ts +26 -0
- package/src/presentation/subscribers/on-step-attempt-terminated.ts +16 -0
- package/src/temporal/activities/close-abandoned-streams.test.ts +84 -0
- package/src/temporal/activities/close-abandoned-streams.ts +30 -0
- package/src/temporal/activities/compact-stream.test.ts +221 -0
- package/src/temporal/activities/compact-stream.ts +123 -0
- package/src/temporal/activities/compaction-reconcile.test.ts +143 -0
- package/src/temporal/activities/compaction-reconcile.ts +48 -0
- package/src/temporal/activities/index.ts +15 -0
- package/src/temporal/activities/reap-stale-open-streams.test.ts +48 -0
- package/src/temporal/activities/reap-stale-open-streams.ts +14 -0
- package/src/temporal/activities/retention-sweep.test.ts +38 -0
- package/src/temporal/activities/retention-sweep.ts +23 -0
- package/src/temporal/constants.ts +14 -0
- package/src/temporal/workflows/close-abandoned-streams.ts +25 -0
- package/src/temporal/workflows/compact-stream.ts +26 -0
- package/src/temporal/workflows/compaction-reconcile-cron.ts +14 -0
- package/src/temporal/workflows/index.ts +5 -0
- package/src/temporal/workflows/reap-stale-open-streams-cron.ts +13 -0
- package/src/temporal/workflows/retention-sweep-cron.ts +23 -0
- package/test/env.ts +37 -0
- package/test/factories/job-accounting.ts +38 -0
- package/test/fixtures/closed-stream.ts +53 -0
- package/test/fixtures/lease-token.ts +42 -0
- package/test/fixtures/ndjson.ts +34 -0
- package/test/globalSetup.ts +44 -0
- package/test/index.ts +9 -0
- package/test/queries.ts +54 -0
- package/test/setup.ts +17 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +3 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
import {parsePiSessionRecord} from './pi-parser.js';
|
|
2
|
+
|
|
3
|
+
const record = (data: unknown, ts = 1) => ({
|
|
4
|
+
ts,
|
|
5
|
+
data: typeof data === 'string' ? data : JSON.stringify(data),
|
|
6
|
+
});
|
|
7
|
+
const meta = (label: string, value: string, inline?: boolean) =>
|
|
8
|
+
inline == null ? {label, value} : {label, value, inline};
|
|
9
|
+
|
|
10
|
+
describe('parsePiSessionRecord', () => {
|
|
11
|
+
test('returns a raw row for malformed JSON', () => {
|
|
12
|
+
const rows = parsePiSessionRecord(record('{not json'));
|
|
13
|
+
|
|
14
|
+
expect(rows).toEqual([
|
|
15
|
+
{kind: 'raw', timestamp: 1, label: 'Malformed session entry', raw: '{not json'},
|
|
16
|
+
]);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('returns a raw row for an unknown entry type', () => {
|
|
20
|
+
const rows = parsePiSessionRecord(record({type: 'future_entry', payload: {x: 1}}));
|
|
21
|
+
|
|
22
|
+
expect(rows).toEqual([
|
|
23
|
+
{
|
|
24
|
+
kind: 'raw',
|
|
25
|
+
timestamp: 1,
|
|
26
|
+
label: 'Unknown session entry: future_entry',
|
|
27
|
+
raw: '{"type":"future_entry","payload":{"x":1}}',
|
|
28
|
+
},
|
|
29
|
+
]);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('expands assistant text, thinking, and tool calls into ordered rows', () => {
|
|
33
|
+
const rows = parsePiSessionRecord(
|
|
34
|
+
record({
|
|
35
|
+
type: 'message',
|
|
36
|
+
message: {
|
|
37
|
+
role: 'assistant',
|
|
38
|
+
model: 'claude-opus-4',
|
|
39
|
+
content: [
|
|
40
|
+
{type: 'text', text: 'I will edit the file.'},
|
|
41
|
+
{type: 'thinking', text: 'Need to inspect the module.'},
|
|
42
|
+
{type: 'toolCall', id: 'call-1', name: 'read_file', arguments: {path: 'src/a.ts'}},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
expect(rows).toEqual([
|
|
49
|
+
{
|
|
50
|
+
kind: 'message',
|
|
51
|
+
timestamp: 1,
|
|
52
|
+
role: 'assistant',
|
|
53
|
+
label: 'assistant',
|
|
54
|
+
meta: [meta('model', 'claude-opus-4')],
|
|
55
|
+
text: 'I will edit the file.',
|
|
56
|
+
terminalFailure: false,
|
|
57
|
+
},
|
|
58
|
+
{kind: 'thinking', timestamp: 1, text: 'Need to inspect the module.'},
|
|
59
|
+
{
|
|
60
|
+
kind: 'tool-call',
|
|
61
|
+
timestamp: 1,
|
|
62
|
+
id: 'call-1',
|
|
63
|
+
name: 'read_file',
|
|
64
|
+
input: '{\n "path": "src/a.ts"\n}',
|
|
65
|
+
},
|
|
66
|
+
]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('preserves interleaved assistant text and thinking block order', () => {
|
|
70
|
+
const rows = parsePiSessionRecord(
|
|
71
|
+
record({
|
|
72
|
+
type: 'message',
|
|
73
|
+
message: {
|
|
74
|
+
role: 'assistant',
|
|
75
|
+
content: [
|
|
76
|
+
{type: 'text', text: 'First visible update.'},
|
|
77
|
+
{type: 'thinking', text: 'First private thought.'},
|
|
78
|
+
{type: 'text', text: 'Second visible update.'},
|
|
79
|
+
{type: 'toolCall', id: 'call-1', name: 'read_file', arguments: {path: 'src/a.ts'}},
|
|
80
|
+
{type: 'thinking', text: 'Thought after tool call.'},
|
|
81
|
+
{type: 'text', text: 'Final visible update.'},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
expect(rows).toEqual([
|
|
88
|
+
{
|
|
89
|
+
kind: 'message',
|
|
90
|
+
timestamp: 1,
|
|
91
|
+
role: 'assistant',
|
|
92
|
+
label: 'assistant',
|
|
93
|
+
meta: [],
|
|
94
|
+
text: 'First visible update.',
|
|
95
|
+
terminalFailure: false,
|
|
96
|
+
},
|
|
97
|
+
{kind: 'thinking', timestamp: 1, text: 'First private thought.'},
|
|
98
|
+
{
|
|
99
|
+
kind: 'message',
|
|
100
|
+
timestamp: 1,
|
|
101
|
+
role: 'assistant',
|
|
102
|
+
label: 'assistant',
|
|
103
|
+
meta: [],
|
|
104
|
+
text: 'Second visible update.',
|
|
105
|
+
terminalFailure: false,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
kind: 'tool-call',
|
|
109
|
+
timestamp: 1,
|
|
110
|
+
id: 'call-1',
|
|
111
|
+
name: 'read_file',
|
|
112
|
+
input: '{\n "path": "src/a.ts"\n}',
|
|
113
|
+
},
|
|
114
|
+
{kind: 'thinking', timestamp: 1, text: 'Thought after tool call.'},
|
|
115
|
+
{
|
|
116
|
+
kind: 'message',
|
|
117
|
+
timestamp: 1,
|
|
118
|
+
role: 'assistant',
|
|
119
|
+
label: 'assistant',
|
|
120
|
+
meta: [],
|
|
121
|
+
text: 'Final visible update.',
|
|
122
|
+
terminalFailure: false,
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test.each([
|
|
128
|
+
['success', false],
|
|
129
|
+
['error', true],
|
|
130
|
+
])('expands a tool result with %s state', (_label, isError) => {
|
|
131
|
+
const rows = parsePiSessionRecord(
|
|
132
|
+
record({
|
|
133
|
+
type: 'message',
|
|
134
|
+
message: {
|
|
135
|
+
toolCallId: 'call-1',
|
|
136
|
+
toolName: 'read_file',
|
|
137
|
+
content: [{type: 'text', text: 'file contents'}],
|
|
138
|
+
isError,
|
|
139
|
+
},
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(rows).toEqual([
|
|
144
|
+
{
|
|
145
|
+
kind: 'tool-result',
|
|
146
|
+
timestamp: 1,
|
|
147
|
+
toolCallId: 'call-1',
|
|
148
|
+
toolName: 'read_file',
|
|
149
|
+
output: 'file contents',
|
|
150
|
+
isError,
|
|
151
|
+
},
|
|
152
|
+
]);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test.each([
|
|
156
|
+
[{type: 'session', id: 'session-1'}, 'Session started', 'session-1', []],
|
|
157
|
+
[
|
|
158
|
+
{type: 'session', version: 2, id: 'session-1', cwd: '/workspace'},
|
|
159
|
+
'Session started',
|
|
160
|
+
'v2 · session-1 · /workspace',
|
|
161
|
+
[],
|
|
162
|
+
],
|
|
163
|
+
[{type: 'thinking_level_change', thinkingLevel: 'high'}, 'Thinking level changed', 'high', []],
|
|
164
|
+
[
|
|
165
|
+
{type: 'model_change', modelId: 'gpt-5-codex', provider: 'openai'},
|
|
166
|
+
'Model changed',
|
|
167
|
+
'gpt-5-codex · openai',
|
|
168
|
+
[],
|
|
169
|
+
],
|
|
170
|
+
[
|
|
171
|
+
{type: 'compaction', summary: 'summarized old context', tokensBefore: 12_345},
|
|
172
|
+
'Context compacted',
|
|
173
|
+
'summarized old context',
|
|
174
|
+
[meta('tokens before', '12.3K tokens')],
|
|
175
|
+
],
|
|
176
|
+
[
|
|
177
|
+
{type: 'label', label: 'Implement tests', targetId: 'entry-1'},
|
|
178
|
+
'Label',
|
|
179
|
+
'Implement tests',
|
|
180
|
+
[meta('target', 'entry-1', false)],
|
|
181
|
+
],
|
|
182
|
+
])('expands lifecycle entry %#', (entry, label, detail, meta) => {
|
|
183
|
+
const rows = parsePiSessionRecord(record(entry));
|
|
184
|
+
|
|
185
|
+
expect(rows).toEqual([
|
|
186
|
+
{
|
|
187
|
+
kind: 'lifecycle',
|
|
188
|
+
timestamp: 1,
|
|
189
|
+
label,
|
|
190
|
+
detail,
|
|
191
|
+
meta,
|
|
192
|
+
tone: 'default',
|
|
193
|
+
terminalFailure: false,
|
|
194
|
+
},
|
|
195
|
+
]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('renders message content blocks without exposing image data', () => {
|
|
199
|
+
const rows = parsePiSessionRecord(
|
|
200
|
+
record({
|
|
201
|
+
type: 'message',
|
|
202
|
+
message: {
|
|
203
|
+
role: 'user',
|
|
204
|
+
content: [
|
|
205
|
+
{type: 'text', text: 'Inspect this screenshot.'},
|
|
206
|
+
{type: 'image', mimeType: 'image/png', data: 'base64-payload'},
|
|
207
|
+
],
|
|
208
|
+
},
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
expect(rows).toEqual([
|
|
213
|
+
{
|
|
214
|
+
kind: 'message',
|
|
215
|
+
timestamp: 1,
|
|
216
|
+
role: 'user',
|
|
217
|
+
label: 'user',
|
|
218
|
+
meta: [],
|
|
219
|
+
text: 'Inspect this screenshot.\n\n[image/png image]',
|
|
220
|
+
terminalFailure: false,
|
|
221
|
+
},
|
|
222
|
+
]);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test('renders bash execution messages without falling back to raw JSON', () => {
|
|
226
|
+
const rows = parsePiSessionRecord(
|
|
227
|
+
record({
|
|
228
|
+
type: 'message',
|
|
229
|
+
message: {
|
|
230
|
+
role: 'bashExecution',
|
|
231
|
+
command: 'pnpm test',
|
|
232
|
+
output: '1 failed',
|
|
233
|
+
exitCode: 1,
|
|
234
|
+
cancelled: false,
|
|
235
|
+
truncated: true,
|
|
236
|
+
fullOutputPath: '/tmp/output.log',
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
expect(rows).toEqual([
|
|
242
|
+
{
|
|
243
|
+
kind: 'message',
|
|
244
|
+
timestamp: 1,
|
|
245
|
+
role: 'bash-execution',
|
|
246
|
+
label: 'bash execution',
|
|
247
|
+
meta: [
|
|
248
|
+
meta('exit', '1'),
|
|
249
|
+
meta('truncated', 'yes'),
|
|
250
|
+
meta('full output', '/tmp/output.log', false),
|
|
251
|
+
],
|
|
252
|
+
text: '$ pnpm test\n1 failed',
|
|
253
|
+
terminalFailure: false,
|
|
254
|
+
},
|
|
255
|
+
]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test.each([
|
|
259
|
+
[
|
|
260
|
+
{role: 'branchSummary', summary: 'branched for fix', fromId: 'entry-1'},
|
|
261
|
+
'branch-summary',
|
|
262
|
+
'branch summary',
|
|
263
|
+
[meta('from', 'entry-1')],
|
|
264
|
+
'branched for fix',
|
|
265
|
+
],
|
|
266
|
+
[
|
|
267
|
+
{role: 'compactionSummary', summary: 'summarized history', tokensBefore: 42_000},
|
|
268
|
+
'compaction-summary',
|
|
269
|
+
'compaction summary',
|
|
270
|
+
[meta('tokens before', '42K tokens')],
|
|
271
|
+
'summarized history',
|
|
272
|
+
],
|
|
273
|
+
[
|
|
274
|
+
{role: 'custom', customType: 'extension', content: 'extension output', display: true},
|
|
275
|
+
'custom',
|
|
276
|
+
'custom',
|
|
277
|
+
[meta('type', 'extension'), meta('display', 'on')],
|
|
278
|
+
'extension output',
|
|
279
|
+
],
|
|
280
|
+
])('renders extended message role %#', (message, role, label, meta, text) => {
|
|
281
|
+
const rows = parsePiSessionRecord(record({type: 'message', message}));
|
|
282
|
+
|
|
283
|
+
expect(rows).toEqual([
|
|
284
|
+
{kind: 'message', timestamp: 1, role, label, meta, text, terminalFailure: false},
|
|
285
|
+
]);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test('marks assistant terminal errors as failure anchors', () => {
|
|
289
|
+
const rows = parsePiSessionRecord(
|
|
290
|
+
record({
|
|
291
|
+
type: 'message',
|
|
292
|
+
message: {
|
|
293
|
+
role: 'assistant',
|
|
294
|
+
content: [{type: 'text', text: 'I cannot continue.'}],
|
|
295
|
+
stopReason: 'error',
|
|
296
|
+
},
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(rows[0]).toMatchObject({kind: 'message', terminalFailure: true});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test('adds a failure anchor for thinking-only assistant terminal errors', () => {
|
|
304
|
+
const rows = parsePiSessionRecord(
|
|
305
|
+
record({
|
|
306
|
+
type: 'message',
|
|
307
|
+
message: {
|
|
308
|
+
role: 'assistant',
|
|
309
|
+
content: [{type: 'thinking', text: 'No visible response before failure.'}],
|
|
310
|
+
stopReason: 'error',
|
|
311
|
+
},
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
expect(rows).toEqual([
|
|
316
|
+
{kind: 'thinking', timestamp: 1, text: 'No visible response before failure.'},
|
|
317
|
+
{
|
|
318
|
+
kind: 'message',
|
|
319
|
+
timestamp: 1,
|
|
320
|
+
role: 'assistant',
|
|
321
|
+
label: 'assistant',
|
|
322
|
+
meta: [meta('stop', 'error')],
|
|
323
|
+
text: 'Assistant stopped with an error.',
|
|
324
|
+
terminalFailure: true,
|
|
325
|
+
},
|
|
326
|
+
]);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test('expands a tool result with no id', () => {
|
|
330
|
+
const rows = parsePiSessionRecord(
|
|
331
|
+
record({
|
|
332
|
+
type: 'message',
|
|
333
|
+
message: {role: 'tool', toolName: 'read_file', content: [{type: 'text', text: 'data'}]},
|
|
334
|
+
}),
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
expect(rows).toEqual([
|
|
338
|
+
{
|
|
339
|
+
kind: 'tool-result',
|
|
340
|
+
timestamp: 1,
|
|
341
|
+
toolCallId: null,
|
|
342
|
+
toolName: 'read_file',
|
|
343
|
+
output: 'data',
|
|
344
|
+
isError: false,
|
|
345
|
+
},
|
|
346
|
+
]);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test('expands a tool call with no id', () => {
|
|
350
|
+
const rows = parsePiSessionRecord(
|
|
351
|
+
record({
|
|
352
|
+
type: 'message',
|
|
353
|
+
message: {
|
|
354
|
+
role: 'assistant',
|
|
355
|
+
content: [{type: 'tool_call', name: 'run', arguments: {cmd: 'ls'}}],
|
|
356
|
+
},
|
|
357
|
+
}),
|
|
358
|
+
);
|
|
359
|
+
|
|
360
|
+
expect(rows).toEqual([
|
|
361
|
+
{kind: 'tool-call', timestamp: 1, id: null, name: 'run', input: '{\n "cmd": "ls"\n}'},
|
|
362
|
+
]);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
test('falls back to the raw message when assistant content has no renderable blocks', () => {
|
|
366
|
+
const rows = parsePiSessionRecord(
|
|
367
|
+
record({type: 'message', message: {role: 'assistant', content: [{type: 'image'}]}}),
|
|
368
|
+
);
|
|
369
|
+
|
|
370
|
+
expect(rows).toHaveLength(1);
|
|
371
|
+
expect(rows[0]).toMatchObject({kind: 'message', role: 'assistant'});
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test('renders a branch summary as a system message row', () => {
|
|
375
|
+
const rows = parsePiSessionRecord(
|
|
376
|
+
record({type: 'branch_summary', summary: 'merged main', fromId: 'entry-1'}),
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
expect(rows).toEqual([
|
|
380
|
+
{
|
|
381
|
+
kind: 'message',
|
|
382
|
+
timestamp: 1,
|
|
383
|
+
role: 'system',
|
|
384
|
+
label: 'branch summary',
|
|
385
|
+
meta: [meta('from', 'entry-1')],
|
|
386
|
+
text: 'merged main',
|
|
387
|
+
terminalFailure: false,
|
|
388
|
+
},
|
|
389
|
+
]);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
test('renders a custom entry as a message row', () => {
|
|
393
|
+
const rows = parsePiSessionRecord(
|
|
394
|
+
record({type: 'custom', customType: 'extension', data: {ok: true}}),
|
|
395
|
+
);
|
|
396
|
+
|
|
397
|
+
expect(rows).toEqual([
|
|
398
|
+
{
|
|
399
|
+
kind: 'message',
|
|
400
|
+
timestamp: 1,
|
|
401
|
+
role: 'custom',
|
|
402
|
+
label: 'custom',
|
|
403
|
+
meta: [meta('type', 'extension')],
|
|
404
|
+
text: '{\n "ok": true\n}',
|
|
405
|
+
terminalFailure: false,
|
|
406
|
+
},
|
|
407
|
+
]);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test('renders a custom message entry from content', () => {
|
|
411
|
+
const rows = parsePiSessionRecord(
|
|
412
|
+
record({type: 'custom_message', customType: 'extension', content: 'hello there'}),
|
|
413
|
+
);
|
|
414
|
+
|
|
415
|
+
expect(rows).toEqual([
|
|
416
|
+
{
|
|
417
|
+
kind: 'message',
|
|
418
|
+
timestamp: 1,
|
|
419
|
+
role: 'custom',
|
|
420
|
+
label: 'custom message',
|
|
421
|
+
meta: [meta('type', 'extension')],
|
|
422
|
+
text: 'hello there',
|
|
423
|
+
terminalFailure: false,
|
|
424
|
+
},
|
|
425
|
+
]);
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
test('renders a session_info entry as a lifecycle row', () => {
|
|
429
|
+
const rows = parsePiSessionRecord(record({type: 'session_info', message: 'resumed'}));
|
|
430
|
+
|
|
431
|
+
expect(rows).toEqual([
|
|
432
|
+
{
|
|
433
|
+
kind: 'lifecycle',
|
|
434
|
+
timestamp: 1,
|
|
435
|
+
label: 'Session info',
|
|
436
|
+
detail: 'resumed',
|
|
437
|
+
meta: [],
|
|
438
|
+
tone: 'default',
|
|
439
|
+
terminalFailure: false,
|
|
440
|
+
},
|
|
441
|
+
]);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
test('returns an unsupported raw row when the entry has no type', () => {
|
|
445
|
+
const rows = parsePiSessionRecord(record({payload: {x: 1}}));
|
|
446
|
+
|
|
447
|
+
expect(rows).toEqual([
|
|
448
|
+
{kind: 'raw', timestamp: 1, label: 'Unsupported entry', raw: '{"payload":{"x":1}}'},
|
|
449
|
+
]);
|
|
450
|
+
});
|
|
451
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import type {SessionViewRow} from '@shipfox/api-logs-dto';
|
|
2
|
+
import {parseSessionEntry, sessionMessageEntrySchema} from './entry-schema.js';
|
|
3
|
+
import {toJson} from './object.js';
|
|
4
|
+
import {expandMessageEntry} from './pi/message-rows.js';
|
|
5
|
+
import {
|
|
6
|
+
branchSummaryDetail,
|
|
7
|
+
branchSummaryMeta,
|
|
8
|
+
compactionDetail,
|
|
9
|
+
compactionMeta,
|
|
10
|
+
customEntryLabel,
|
|
11
|
+
customEntryMeta,
|
|
12
|
+
customEntryText,
|
|
13
|
+
entryDetail,
|
|
14
|
+
labelDetail,
|
|
15
|
+
labelMeta,
|
|
16
|
+
modelChangeDetail,
|
|
17
|
+
sessionDetail,
|
|
18
|
+
} from './pi/metadata.js';
|
|
19
|
+
import {lifecycleRow, messageRow, rawRecordRow} from './rows.js';
|
|
20
|
+
import type {AgentSessionRecord} from './session-record.js';
|
|
21
|
+
|
|
22
|
+
export type {AgentSessionRecord} from './session-record.js';
|
|
23
|
+
|
|
24
|
+
export function parsePiSessionRecord(record: AgentSessionRecord): readonly SessionViewRow[] {
|
|
25
|
+
const parsed = parseSessionEntry(record.data);
|
|
26
|
+
if (!parsed.ok) {
|
|
27
|
+
return [
|
|
28
|
+
rawRecordRow(
|
|
29
|
+
record,
|
|
30
|
+
parsed.reason === 'invalid-json' ? 'Malformed session entry' : 'Unsupported entry',
|
|
31
|
+
),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const entry = parsed.entry;
|
|
36
|
+
switch (entry.type) {
|
|
37
|
+
case 'message': {
|
|
38
|
+
const parsedEntry = sessionMessageEntrySchema.safeParse(entry);
|
|
39
|
+
if (!parsedEntry.success) {
|
|
40
|
+
return [
|
|
41
|
+
{kind: 'raw', timestamp: record.ts, label: 'Unsupported message entry', raw: record.data},
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
return expandMessageEntry(record.ts, parsedEntry.data.message);
|
|
45
|
+
}
|
|
46
|
+
case 'session':
|
|
47
|
+
return [lifecycleRow(record.ts, 'Session started', sessionDetail(entry), 'default', false)];
|
|
48
|
+
case 'session_info':
|
|
49
|
+
return [lifecycleRow(record.ts, 'Session info', entryDetail(entry), 'default', false)];
|
|
50
|
+
case 'thinking_level_change':
|
|
51
|
+
return [
|
|
52
|
+
lifecycleRow(record.ts, 'Thinking level changed', entryDetail(entry), 'default', false),
|
|
53
|
+
];
|
|
54
|
+
case 'model_change':
|
|
55
|
+
return [lifecycleRow(record.ts, 'Model changed', modelChangeDetail(entry), 'default', false)];
|
|
56
|
+
case 'compaction':
|
|
57
|
+
return [
|
|
58
|
+
lifecycleRow(
|
|
59
|
+
record.ts,
|
|
60
|
+
'Context compacted',
|
|
61
|
+
compactionDetail(entry),
|
|
62
|
+
'default',
|
|
63
|
+
false,
|
|
64
|
+
compactionMeta(entry),
|
|
65
|
+
),
|
|
66
|
+
];
|
|
67
|
+
case 'branch_summary':
|
|
68
|
+
return [
|
|
69
|
+
messageRow(
|
|
70
|
+
record.ts,
|
|
71
|
+
'system',
|
|
72
|
+
'branch summary',
|
|
73
|
+
branchSummaryDetail(entry) ?? toJson(entry),
|
|
74
|
+
false,
|
|
75
|
+
branchSummaryMeta(entry),
|
|
76
|
+
),
|
|
77
|
+
];
|
|
78
|
+
case 'custom':
|
|
79
|
+
return [
|
|
80
|
+
messageRow(
|
|
81
|
+
record.ts,
|
|
82
|
+
'custom',
|
|
83
|
+
customEntryLabel('custom'),
|
|
84
|
+
customEntryText(entry) ?? toJson(entry),
|
|
85
|
+
false,
|
|
86
|
+
customEntryMeta(entry),
|
|
87
|
+
),
|
|
88
|
+
];
|
|
89
|
+
case 'custom_message':
|
|
90
|
+
return [
|
|
91
|
+
messageRow(
|
|
92
|
+
record.ts,
|
|
93
|
+
'custom',
|
|
94
|
+
customEntryLabel('custom message'),
|
|
95
|
+
customEntryText(entry) ?? toJson(entry),
|
|
96
|
+
false,
|
|
97
|
+
customEntryMeta(entry),
|
|
98
|
+
),
|
|
99
|
+
];
|
|
100
|
+
case 'label':
|
|
101
|
+
return [
|
|
102
|
+
lifecycleRow(record.ts, 'Label', labelDetail(entry), 'default', false, labelMeta(entry)),
|
|
103
|
+
];
|
|
104
|
+
default:
|
|
105
|
+
return [rawRecordRow(record, `Unknown session entry: ${entry.type}`)];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SessionViewLifecycleRow,
|
|
3
|
+
SessionViewMessageRow,
|
|
4
|
+
SessionViewRow,
|
|
5
|
+
SessionViewRowMeta,
|
|
6
|
+
SessionViewThinkingRow,
|
|
7
|
+
} from '@shipfox/api-logs-dto';
|
|
8
|
+
import type {AgentSessionRecord} from './session-record.js';
|
|
9
|
+
|
|
10
|
+
export function messageRow(
|
|
11
|
+
timestamp: number,
|
|
12
|
+
role: string,
|
|
13
|
+
label: string,
|
|
14
|
+
text: string,
|
|
15
|
+
terminalFailure: boolean,
|
|
16
|
+
meta: readonly SessionViewRowMeta[] = [],
|
|
17
|
+
): SessionViewMessageRow {
|
|
18
|
+
return {kind: 'message', timestamp, role, label, meta, text, terminalFailure};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function thinkingRow(timestamp: number, text: string): SessionViewThinkingRow {
|
|
22
|
+
return {kind: 'thinking', timestamp, text};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function lifecycleRow(
|
|
26
|
+
timestamp: number,
|
|
27
|
+
label: string,
|
|
28
|
+
detail: string | null,
|
|
29
|
+
tone: SessionViewLifecycleRow['tone'],
|
|
30
|
+
terminalFailure: boolean,
|
|
31
|
+
meta: readonly SessionViewRowMeta[] = [],
|
|
32
|
+
): SessionViewLifecycleRow {
|
|
33
|
+
return {kind: 'lifecycle', timestamp, label, detail, meta, tone, terminalFailure};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function rawRecordRow(record: AgentSessionRecord, label: string): SessionViewRow {
|
|
37
|
+
return {kind: 'raw', timestamp: record.ts, label, raw: record.data};
|
|
38
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import {and, eq, isNull, lt, sql} from 'drizzle-orm';
|
|
2
|
+
import type {Transaction} from './db.js';
|
|
3
|
+
import {jobAccounting} from './schema/job-accounting.js';
|
|
4
|
+
|
|
5
|
+
/** Creates the per-job accounting row on first append; a no-op once it exists. */
|
|
6
|
+
export async function ensureJobAccounting(
|
|
7
|
+
tx: Transaction,
|
|
8
|
+
params: {jobId: string; workspaceId: string},
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
await tx
|
|
11
|
+
.insert(jobAccounting)
|
|
12
|
+
.values({jobId: params.jobId, workspaceId: params.workspaceId})
|
|
13
|
+
.onConflictDoNothing({target: jobAccounting.jobId});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AccrualResult {
|
|
17
|
+
used: number;
|
|
18
|
+
startedAt: Date;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Atomically adds `delta` stored bytes to the job's accrual, returning the new
|
|
23
|
+
* total and the budget clock origin. Returns null when the job is already capped
|
|
24
|
+
* (the `capped_at IS NULL` guard matched no row), so the caller drops the append.
|
|
25
|
+
*/
|
|
26
|
+
export async function accrueStoredBytes(
|
|
27
|
+
tx: Transaction,
|
|
28
|
+
params: {jobId: string; delta: number},
|
|
29
|
+
): Promise<AccrualResult | null> {
|
|
30
|
+
const [row] = await tx
|
|
31
|
+
.update(jobAccounting)
|
|
32
|
+
.set({
|
|
33
|
+
storedBytesUsed: sql`${jobAccounting.storedBytesUsed} + ${params.delta}`,
|
|
34
|
+
updatedAt: sql`now()`,
|
|
35
|
+
})
|
|
36
|
+
.where(and(eq(jobAccounting.jobId, params.jobId), isNull(jobAccounting.cappedAt)))
|
|
37
|
+
.returning({used: jobAccounting.storedBytesUsed, startedAt: jobAccounting.startedAt});
|
|
38
|
+
|
|
39
|
+
return row ?? null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Claims the cap for a job. Returns true for the single caller that wins the race. */
|
|
43
|
+
export async function claimCap(tx: Transaction, jobId: string): Promise<boolean> {
|
|
44
|
+
const [row] = await tx
|
|
45
|
+
.update(jobAccounting)
|
|
46
|
+
.set({cappedAt: sql`now()`, updatedAt: sql`now()`})
|
|
47
|
+
.where(and(eq(jobAccounting.jobId, jobId), isNull(jobAccounting.cappedAt)))
|
|
48
|
+
.returning({jobId: jobAccounting.jobId});
|
|
49
|
+
|
|
50
|
+
return Boolean(row);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function isJobCapped(tx: Transaction, jobId: string): Promise<boolean> {
|
|
54
|
+
const [row] = await tx
|
|
55
|
+
.select({cappedAt: jobAccounting.cappedAt})
|
|
56
|
+
.from(jobAccounting)
|
|
57
|
+
.where(eq(jobAccounting.jobId, jobId));
|
|
58
|
+
|
|
59
|
+
return Boolean(row?.cappedAt);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Prunes accounting only after the job has no recent budget activity. A live job re-touches
|
|
64
|
+
* `updated_at` on append, so this guard keeps retention from resetting its cap state.
|
|
65
|
+
*/
|
|
66
|
+
export async function deleteJobAccounting(
|
|
67
|
+
tx: Transaction,
|
|
68
|
+
params: {jobId: string; retentionDays: number},
|
|
69
|
+
): Promise<{deleted: boolean}> {
|
|
70
|
+
const [row] = await tx
|
|
71
|
+
.delete(jobAccounting)
|
|
72
|
+
.where(
|
|
73
|
+
and(
|
|
74
|
+
eq(jobAccounting.jobId, params.jobId),
|
|
75
|
+
lt(jobAccounting.updatedAt, sql`now() - make_interval(days => ${params.retentionDays})`),
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
.returning({jobId: jobAccounting.jobId});
|
|
79
|
+
|
|
80
|
+
return {deleted: Boolean(row)};
|
|
81
|
+
}
|