@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/.turbo/turbo-build.log
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
$ shipfox-swc && shipfox-temporal-bundle dist/temporal/workflows/index.js
|
|
2
|
-
Successfully compiled: 59 files with swc (
|
|
3
|
-
2026-07-
|
|
2
|
+
Successfully compiled: 59 files with swc (607.05ms)
|
|
3
|
+
2026-07-28T22:45:25.101Z [INFO] asset workflow-bundle-17839eee3b21fbae5b8c.js 3.18 MiB [emitted] [immutable] (name: main)
|
|
4
4
|
orphan modules 33.5 KiB [orphan] 21 modules
|
|
5
5
|
runtime modules 1.13 KiB 5 modules
|
|
6
6
|
modules by path ../../../node_modules/.pnpm/ 970 KiB 196 modules
|
|
@@ -16,6 +16,6 @@ optional modules 30 bytes [optional]
|
|
|
16
16
|
__temporal_custom_payload_converter (ignored) 15 bytes [optional] [built] [code generated]
|
|
17
17
|
__temporal_custom_failure_converter (ignored) 15 bytes [optional] [built] [code generated]
|
|
18
18
|
../../shared/node/temporal/dist/workflow-error-interceptor.js 1.21 KiB [built] [code generated]
|
|
19
|
-
webpack 5.107.2 compiled successfully in
|
|
20
|
-
2026-07-
|
|
19
|
+
webpack 5.107.2 compiled successfully in 7341 ms
|
|
20
|
+
2026-07-28T22:45:25.133Z [INFO] Workflow bundle created { size: '3.18MB' }
|
|
21
21
|
/home/runner/work/shipfox/shipfox/libs/api/logs/dist/temporal/workflows/index.js: 3335222 bytes
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# @shipfox/api-logs
|
|
2
2
|
|
|
3
|
+
## 11.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- 25158c8: Carry workspace lifecycle status in JWT membership claims and enforce suspended or inactive access at the stateless workspace gate while keeping access-token verification stateless.
|
|
8
|
+
|
|
9
|
+
`getAuthenticatedSessionContext()` now reads refresh-session metadata from verified access-token claims without checking active refresh-session state; revoking a refresh session does not invalidate an already-issued access token.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Updated dependencies [25158c8]
|
|
14
|
+
- @shipfox/api-auth-context@11.0.0
|
|
15
|
+
|
|
16
|
+
## 10.2.0
|
|
17
|
+
|
|
18
|
+
### Minor Changes
|
|
19
|
+
|
|
20
|
+
- 57e69d8: Stop treating benign Claude SDK message types as unknown session-parser failures.
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- aaa0962: Cover Claude hook responses while adding drift detection against the installed SDK message union.
|
|
25
|
+
- Updated dependencies [57e69d8]
|
|
26
|
+
- @shipfox/api-logs-dto@10.2.0
|
|
27
|
+
- @shipfox/api-auth-context@10.2.0
|
|
28
|
+
|
|
3
29
|
## 10.1.0
|
|
4
30
|
|
|
5
31
|
### Minor Changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"append-logs.d.ts","sourceRoot":"","sources":["../../src/core/append-logs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AASnC,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"append-logs.d.ts","sourceRoot":"","sources":["../../src/core/append-logs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AASnC,OAAO,KAAK,EAAC,qBAAqB,EAAC,MAAM,yCAAyC,CAAC;AAiCnF,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,gBAAgB;IAC/B,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,OAAO,CAAC;CACjB;AAiVD;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,MAAM,EAAE,gBAAgB,EACxB,SAAS,CAAC,EAAE,qBAAqB,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAgI3B"}
|
package/dist/core/append-logs.js
CHANGED
|
@@ -11,6 +11,7 @@ import { recordAppendedCount, streamClosedCount, streamOpenedCount } from '#metr
|
|
|
11
11
|
import { allowedBudget } from './budget.js';
|
|
12
12
|
import { closeStream, controlTombstone } from './close-stream.js';
|
|
13
13
|
import { MalformedLogChunkError, OffsetGapError } from './errors.js';
|
|
14
|
+
import { flushPendingToolRows } from './session/claude/rows.js';
|
|
14
15
|
import { claudeInitSessionId, createClaudeParseContext } from './session/claude-parser.js';
|
|
15
16
|
import { parseSessionRecord } from './session/parse-session.js';
|
|
16
17
|
/**
|
|
@@ -203,6 +204,11 @@ function buildStoredBody(records, harness, initialClaudeParseContext, initialCla
|
|
|
203
204
|
storedRecords.push(storedAgentSessionRow(row));
|
|
204
205
|
}
|
|
205
206
|
}
|
|
207
|
+
if (isStreamFinal && parseContext?.claude !== undefined) {
|
|
208
|
+
for (const row of flushPendingToolRows(parseContext.claude)){
|
|
209
|
+
storedRecords.push(storedAgentSessionRow(row));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
206
212
|
const body = Buffer.from(storedRecords.map((record)=>`${JSON.stringify(record)}\n`).join(''));
|
|
207
213
|
const recordCounts = {};
|
|
208
214
|
for (const record of storedRecords){
|
|
@@ -212,7 +218,8 @@ function buildStoredBody(records, harness, initialClaudeParseContext, initialCla
|
|
|
212
218
|
body,
|
|
213
219
|
recordCounts,
|
|
214
220
|
claudeParseContext: parseContext?.claude,
|
|
215
|
-
claudePendingResult: parseContext?.claude === undefined ? undefined : pendingResult
|
|
221
|
+
claudePendingResult: parseContext?.claude === undefined ? undefined : pendingResult,
|
|
222
|
+
claudePendingToolRows: parseContext?.claude?.pendingToolRows
|
|
216
223
|
};
|
|
217
224
|
}
|
|
218
225
|
function storedAgentSessionRow(row) {
|
|
@@ -334,12 +341,12 @@ function agentSessionRecord(record) {
|
|
|
334
341
|
capped: await isJobCapped(tx, params.jobId)
|
|
335
342
|
};
|
|
336
343
|
}
|
|
337
|
-
const parseHarness = sessionHarness === 'claude' || stream.claudePendingResult !== null ? 'claude' : sessionHarness;
|
|
338
|
-
const stored = buildStoredBody(parsed.records, parseHarness, parseHarness === 'claude' ? {
|
|
344
|
+
const parseHarness = sessionHarness === 'claude' || stream.claudePendingResult !== null || stream.claudePendingToolRows.length > 0 ? 'claude' : sessionHarness;
|
|
345
|
+
const stored = buildStoredBody(parsed.records, parseHarness, parseHarness === 'claude' ? createClaudeParseContext(stream.claudePendingToolRows, {
|
|
339
346
|
hasInit: stream.claudeHasInit,
|
|
340
347
|
sessionId: stream.claudeSessionId,
|
|
341
348
|
turn: stream.claudeTurn
|
|
342
|
-
} : undefined, parseHarness === 'claude' ? stream.claudePendingResult : undefined, declaredTotalBytes !== undefined);
|
|
349
|
+
}) : undefined, parseHarness === 'claude' ? stream.claudePendingResult : undefined, declaredTotalBytes !== undefined);
|
|
343
350
|
const { recordCounts, stored: chunkStored, ...result } = await storeChunk(tx, {
|
|
344
351
|
params,
|
|
345
352
|
streamId: stream.id,
|
|
@@ -355,7 +362,8 @@ function agentSessionRecord(record) {
|
|
|
355
362
|
hasInit: stored.claudeParseContext.hasInit,
|
|
356
363
|
sessionId: stored.claudeParseContext.sessionId,
|
|
357
364
|
turn: stored.claudeParseContext.turn,
|
|
358
|
-
pendingResult: stored.claudePendingResult ?? null
|
|
365
|
+
pendingResult: stored.claudePendingResult ?? null,
|
|
366
|
+
pendingToolRows: stored.claudePendingToolRows ?? []
|
|
359
367
|
});
|
|
360
368
|
}
|
|
361
369
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/append-logs.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {\n type LogRecord,\n parseLogRecordLine,\n parseRawLogRecordLine,\n type RawLogRecord,\n type SessionViewLifecycleRow,\n type SessionViewRow,\n} from '@shipfox/api-logs-dto';\nimport type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {DEFAULT_HARNESS, type Harness} from '@shipfox/workflow-document';\nimport {config} from '#config.js';\nimport {accrueStoredBytes, claimCap, ensureJobAccounting, isJobCapped} from '#db/accounting.js';\nimport {insertChunk} from '#db/chunks.js';\nimport {db, type Transaction} from '#db/db.js';\nimport {\n casExtendCommittedLength,\n getAttemptStream,\n getOrCreateAttemptStreamWithStatus,\n setClaudeParseContext,\n setDeclaredTotalBytes,\n} from '#db/streams.js';\nimport {\n type LogRecordMetricKind,\n recordAppendedCount,\n streamClosedCount,\n streamOpenedCount,\n} from '#metrics/instance.js';\nimport {allowedBudget} from './budget.js';\nimport {closeStream, controlTombstone} from './close-stream.js';\nimport {MalformedLogChunkError, OffsetGapError} from './errors.js';\nimport {\n type ClaudeParseContext,\n claudeInitSessionId,\n createClaudeParseContext,\n} from './session/claude-parser.js';\nimport type {SessionParseContext} from './session/parse-session.js';\nimport {parseSessionRecord} from './session/parse-session.js';\nimport type {AgentSessionRecord} from './session/session-record.js';\n\nexport interface AppendLogsParams {\n jobId: string;\n workspaceId: string;\n projectId: string;\n workflowRunAttemptId: string;\n stepId: string;\n attempt: number;\n offset: number;\n body: Buffer;\n}\n\nexport interface AppendLogsResult {\n committedLength: number;\n capped: boolean;\n}\n\ninterface ParsedBody {\n declaredTotalBytes?: number;\n records: RawLogRecord[];\n hasAgentSessionRecord: boolean;\n}\n\n/**\n * Pure pre-transaction parse. Requires whole, newline-terminated lines so\n * `committed_length` always lands on a line boundary (one body = one chunk = whole\n * lines; no line ever spans two chunks). Runs before any lock is taken, so a\n * malformed or large body never holds a row. The offset CAS uses the raw append byte length; the\n * budget charges the normalized body built from these parsed records.\n *\n * Each line is validated against the raw record union (a forged server-only\n * `capped`/`runner_lost` fails here); the declared total is pulled from an `end`\n * record. A line that is a valid server-only record under the read union surfaces\n * its type via `forgedType` for the narrowed audit warn.\n */\nfunction parseAppendBody(body: Buffer): ParsedBody {\n if (body.length === 0) return {records: [], hasAgentSessionRecord: false};\n\n const text = body.toString('utf8');\n if (!text.endsWith('\\n')) {\n throw new MalformedLogChunkError('append body must end with a newline (whole records only)');\n }\n const lines = text.split('\\n');\n lines.pop();\n\n let declaredTotalBytes: number | undefined;\n const records: RawLogRecord[] = [];\n let hasAgentSessionRecord = false;\n for (const line of lines) {\n let record: ReturnType<typeof parseRawLogRecordLine>;\n try {\n record = parseRawLogRecordLine(line);\n } catch {\n throw new MalformedLogChunkError(\n 'append body contains an invalid NDJSON record',\n detectForgedType(line),\n );\n }\n records.push(record);\n if (record.type === 'end') {\n declaredTotalBytes = record.total_bytes;\n }\n // An agent_session line is one whole entry in one record; bound its size here (the DTO\n // schema leaves `data` uncapped because it cannot read this runtime config). An over-cap\n // line is rejected so a single entry can never blow the request body or the spool window.\n if (\n record.type === 'agent_session' &&\n Buffer.byteLength(line, 'utf8') > config.LOG_MAX_SESSION_LINE_BYTES\n ) {\n throw new MalformedLogChunkError(\n `agent_session line exceeds ${config.LOG_MAX_SESSION_LINE_BYTES} bytes`,\n );\n }\n if (record.type === 'agent_session') {\n hasAgentSessionRecord = true;\n }\n }\n\n return declaredTotalBytes === undefined\n ? {records, hasAgentSessionRecord}\n : {declaredTotalBytes, records, hasAgentSessionRecord};\n}\n\n/**\n * A line that fails the raw write union but is a valid record under the read union\n * can only be a server-only `capped`/`runner_lost` tombstone — i.e. a forgery\n * attempt. Returns its type for the audit warn, or undefined for plain garbage.\n */\nfunction detectForgedType(line: string): string | undefined {\n try {\n return parseLogRecordLine(line).type;\n } catch {\n return undefined;\n }\n}\n\nasync function getSessionHarness(\n workflows: WorkflowsModuleClient | undefined,\n stepId: string,\n): Promise<Harness> {\n return workflows ? (await workflows.getStepLogContext({stepId})).harness : DEFAULT_HARNESS;\n}\n\n/**\n * Empty-body heartbeat: report the current committed length without materializing\n * a stream, so a runner cannot mint unbounded rows with empty appends.\n */\nasync function readHeartbeat(tx: Transaction, params: AppendLogsParams): Promise<AppendLogsResult> {\n const existing = await getAttemptStream(tx, {\n jobId: params.jobId,\n stepId: params.stepId,\n attempt: params.attempt,\n });\n return {\n committedLength: existing?.committedLength ?? 0,\n capped: await isJobCapped(tx, params.jobId),\n };\n}\n\ninterface StoreChunkParams {\n params: AppendLogsParams;\n streamId: string;\n body: Buffer;\n committedLength: number;\n declaredTotalBytes: number | undefined;\n}\n\ninterface StoreChunkResult extends AppendLogsResult {\n /**\n * Whether the runner chunk was persisted. False only when the job was already\n * capped and the body (including any `end` record) was dropped, so the stream is\n * not whole and must not be declared-closed.\n */\n stored: boolean;\n recordCounts: Partial<Record<LogRecord['type'], number>>;\n}\n\ninterface StoredBody {\n body: Buffer;\n recordCounts: Partial<Record<LogRecord['type'], number>>;\n claudeParseContext: ClaudeParseContext | undefined;\n claudePendingResult: SessionViewLifecycleRow | null | undefined;\n}\n\n/**\n * Accrues the stored bytes, persists the chunk, and trips the per-job cap when\n * this append crosses the budget. Runs only after the offset-CAS extended\n * `committed_length`, so the committed length already reflects the accepted bytes.\n */\nasync function storeChunk(\n tx: Transaction,\n {params, streamId, body, committedLength, declaredTotalBytes}: StoreChunkParams,\n): Promise<StoreChunkResult> {\n await ensureJobAccounting(tx, {jobId: params.jobId, workspaceId: params.workspaceId});\n const storedByteLen = body.length;\n const accrued = await accrueStoredBytes(tx, {jobId: params.jobId, delta: storedByteLen});\n\n // Already capped: accept-and-drop. committed_length has advanced so the runner\n // drains its spool cleanly instead of retry-looping; nothing is stored.\n if (!accrued) return {committedLength, capped: true, stored: false, recordCounts: {}};\n\n await insertChunk(tx, {\n streamId,\n streamOffset: params.offset,\n byteLen: storedByteLen,\n data: body,\n origin: 'runner',\n });\n if (declaredTotalBytes !== undefined) {\n await setDeclaredTotalBytes(tx, {streamId, declaredTotalBytes});\n }\n\n const allowed = allowedBudget({\n baseBytes: config.LOG_BUDGET_BASE_BYTES,\n ratePerMinuteBytes: config.LOG_BUDGET_RATE_BYTES_PER_MINUTE,\n elapsedMs: Date.now() - accrued.startedAt.getTime(),\n });\n if (accrued.used <= allowed) {\n return {committedLength, capped: false, stored: true, recordCounts: {}};\n }\n\n // Over budget. No hard ceiling: this crossing append is stored in full (overshoot\n // bounded by one body). Claim the cap once and inject an in-band `capped` tombstone\n // for the winner.\n const won = await claimCap(tx, params.jobId);\n if (won) {\n const tombstone = controlTombstone('capped');\n await insertChunk(tx, {\n streamId,\n streamOffset: committedLength,\n byteLen: tombstone.length,\n data: tombstone,\n origin: 'control',\n });\n }\n return {\n committedLength,\n capped: true,\n stored: true,\n recordCounts: won ? {capped: 1} : {},\n };\n}\n\nfunction buildStoredBody(\n records: readonly RawLogRecord[],\n harness: Harness,\n initialClaudeParseContext?: ClaudeParseContext,\n initialClaudePendingResult?: SessionViewLifecycleRow | null,\n isStreamFinal = false,\n): StoredBody {\n const storedRecords: LogRecord[] = [];\n const parseContext: SessionParseContext | undefined =\n harness === 'claude'\n ? {\n claude: initialClaudeParseContext ?? createClaudeParseContext(),\n isFinalResult: true,\n }\n : undefined;\n const latestClaudeInitIndicesBySessionId =\n harness === 'claude' ? latestClaudeInitIndices(records) : undefined;\n let pendingResult = initialClaudePendingResult ?? null;\n\n if (parseContext?.claude !== undefined && pendingResult !== null) {\n const firstInit = firstClaudeInit(records);\n if (isStreamFinal || firstInit.hasInit) {\n storedRecords.push(\n storedAgentSessionRow(\n finalizePendingResult(\n pendingResult,\n firstInit.sessionId !== undefined &&\n firstInit.sessionId === parseContext.claude.sessionId,\n parseContext.claude.turn,\n ),\n ),\n );\n pendingResult = null;\n }\n }\n\n for (const [index, record] of records.entries()) {\n if (record.type !== 'agent_session') {\n storedRecords.push(record);\n continue;\n }\n\n if (parseContext?.claude !== undefined && latestClaudeInitIndicesBySessionId !== undefined) {\n parseContext.isFinalResult = !hasFutureClaudeInit(\n latestClaudeInitIndicesBySessionId,\n index,\n parseContext.claude.sessionId,\n );\n }\n\n for (const row of parseSessionRecord(agentSessionRecord(record), harness, parseContext)) {\n if (parseContext?.claude !== undefined && !isStreamFinal && isClaudeFinalResultRow(row)) {\n if (pendingResult !== null) {\n storedRecords.push(storedAgentSessionRow(pendingResult));\n }\n pendingResult = row;\n continue;\n }\n storedRecords.push(storedAgentSessionRow(row));\n }\n }\n\n const body = Buffer.from(storedRecords.map((record) => `${JSON.stringify(record)}\\n`).join(''));\n const recordCounts: Partial<Record<LogRecord['type'], number>> = {};\n for (const record of storedRecords) {\n recordCounts[record.type] = (recordCounts[record.type] ?? 0) + 1;\n }\n\n return {\n body,\n recordCounts,\n claudeParseContext: parseContext?.claude,\n claudePendingResult: parseContext?.claude === undefined ? undefined : pendingResult,\n };\n}\n\nfunction storedAgentSessionRow(row: SessionViewRow): LogRecord {\n return {v: 1, ts: row.timestamp, type: 'agent_session', row};\n}\n\nfunction isClaudeFinalResultRow(row: SessionViewRow): row is SessionViewLifecycleRow {\n return row.kind === 'lifecycle' && row.label === 'Session completed';\n}\n\nfunction finalizePendingResult(\n row: SessionViewLifecycleRow,\n sameSession: boolean,\n turn: number,\n): SessionViewLifecycleRow {\n if (!sameSession) return row;\n\n return {\n ...row,\n label: `Turn ${turn} completed`,\n meta: [{label: 'turn', value: String(turn)}, ...row.meta],\n };\n}\n\nfunction firstClaudeInit(records: readonly RawLogRecord[]): {\n hasInit: boolean;\n sessionId: string | undefined;\n} {\n for (const record of records) {\n if (record.type !== 'agent_session') continue;\n const sessionRecord = agentSessionRecord(record);\n const sessionId = claudeInitSessionId(sessionRecord);\n if (sessionId !== undefined) return {hasInit: true, sessionId};\n }\n\n return {hasInit: false, sessionId: undefined};\n}\n\nfunction latestClaudeInitIndices(records: readonly RawLogRecord[]): Map<string, number> {\n const latestIndices = new Map<string, number>();\n\n for (const [index, record] of records.entries()) {\n if (record.type !== 'agent_session') continue;\n\n const sessionId = claudeInitSessionId(agentSessionRecord(record));\n if (sessionId !== undefined) latestIndices.set(sessionId, index);\n }\n\n return latestIndices;\n}\n\nfunction hasFutureClaudeInit(\n latestClaudeInitIndicesBySessionId: ReadonlyMap<string, number>,\n currentIndex: number,\n sessionId: string | null,\n): boolean {\n if (sessionId === null) return false;\n\n return (latestClaudeInitIndicesBySessionId.get(sessionId) ?? -1) > currentIndex;\n}\n\nfunction agentSessionRecord(\n record: Extract<RawLogRecord, {type: 'agent_session'}>,\n): AgentSessionRecord {\n return {data: record.data, ts: record.ts};\n}\n\n/**\n * Concurrency is serialized through Postgres row locks taken implicitly by the\n * conditional UPDATEs, not an explicit `SELECT ... FOR UPDATE`. Appends for one\n * job contend on its single accounting row, so the path is multi-instance safe\n * but not lock-free.\n */\nexport async function appendLogs(\n params: AppendLogsParams,\n workflows?: WorkflowsModuleClient,\n): Promise<AppendLogsResult> {\n let parsed: ParsedBody;\n try {\n parsed = parseAppendBody(params.body);\n } catch (error) {\n // Narrowed audit: only the detectable forgery case, never the payload or a token.\n if (error instanceof MalformedLogChunkError && error.forgedType !== undefined) {\n logger().warn(\n {\n jobId: params.jobId,\n stepId: params.stepId,\n offendingType: error.forgedType,\n },\n 'Rejected forged server-only log record on append',\n );\n }\n throw error;\n }\n const {declaredTotalBytes} = parsed;\n const sessionHarness = parsed.hasAgentSessionRecord\n ? await getSessionHarness(workflows, params.stepId)\n : DEFAULT_HARNESS;\n const commitByteLen = params.body.length;\n const metrics = {\n recordCounts: {} as Partial<Record<LogRecordMetricKind, number>>,\n streamClosedReason: undefined as 'declared' | undefined,\n streamOpened: false,\n };\n\n const result = await db().transaction(async (tx) => {\n if (commitByteLen === 0) return readHeartbeat(tx, params);\n\n const {created, stream} = await getOrCreateAttemptStreamWithStatus(tx, {\n jobId: params.jobId,\n stepId: params.stepId,\n attempt: params.attempt,\n workspaceId: params.workspaceId,\n projectId: params.projectId,\n workflowRunAttemptId: params.workflowRunAttemptId,\n });\n metrics.streamOpened = created;\n\n // Closed stream (the runner's end already landed, or the job-terminated sweep ran):\n // accept-and-drop so a late chunk can never race compaction. committed_length is\n // frozen at close, so this reports the final offset and the runner stops cleanly.\n if (stream.state === 'closed') {\n return {committedLength: stream.committedLength, capped: await isJobCapped(tx, params.jobId)};\n }\n\n const cas = await casExtendCommittedLength(tx, {\n streamId: stream.id,\n offset: params.offset,\n byteLen: commitByteLen,\n });\n if (cas.outcome === 'gap') throw new OffsetGapError(cas.committedLength);\n if (cas.outcome === 'retry') {\n return {committedLength: cas.committedLength, capped: await isJobCapped(tx, params.jobId)};\n }\n\n const parseHarness =\n sessionHarness === 'claude' || stream.claudePendingResult !== null\n ? 'claude'\n : sessionHarness;\n const stored = buildStoredBody(\n parsed.records,\n parseHarness,\n parseHarness === 'claude'\n ? {\n hasInit: stream.claudeHasInit,\n sessionId: stream.claudeSessionId,\n turn: stream.claudeTurn,\n }\n : undefined,\n parseHarness === 'claude' ? stream.claudePendingResult : undefined,\n declaredTotalBytes !== undefined,\n );\n\n const {\n recordCounts,\n stored: chunkStored,\n ...result\n } = await storeChunk(tx, {\n params,\n streamId: stream.id,\n body: stored.body,\n committedLength: cas.committedLength,\n declaredTotalBytes,\n });\n if (chunkStored) {\n addRecordCounts(metrics.recordCounts, stored.recordCounts);\n if (stored.claudeParseContext !== undefined) {\n await setClaudeParseContext(tx, {\n streamId: stream.id,\n hasInit: stored.claudeParseContext.hasInit,\n sessionId: stored.claudeParseContext.sessionId,\n turn: stored.claudeParseContext.turn,\n pendingResult: stored.claudePendingResult ?? null,\n });\n }\n }\n addRecordCounts(metrics.recordCounts, recordCounts);\n\n // The runner's end record was committed in this append (offset-CAS guarantees\n // everything before it is already committed), so the stream is whole. Declared-close\n // it in-band so compaction starts at once instead of waiting for the timeout sweep.\n // Only when the chunk was actually stored: an end body dropped because the job was\n // already capped persists nothing, so the stream is not whole and stays open for the\n // timeout sweep to close it as truncated.\n if (declaredTotalBytes !== undefined && chunkStored) {\n const closed = await closeStream(tx, {streamId: stream.id, reason: 'declared'});\n if (closed) metrics.streamClosedReason = 'declared';\n }\n\n return result;\n });\n\n if (metrics.streamOpened) streamOpenedCount.add(1);\n for (const [kind, count] of Object.entries(metrics.recordCounts)) {\n if (count > 0) recordAppendedCount.add(count, {kind: kind as LogRecordMetricKind});\n }\n if (metrics.streamClosedReason) {\n streamClosedCount.add(1, {reason: metrics.streamClosedReason});\n }\n\n return result;\n}\n\nfunction addRecordCounts(\n target: Partial<Record<LogRecordMetricKind, number>>,\n source: Partial<Record<LogRecordMetricKind, number>>,\n): void {\n for (const [kind, count] of Object.entries(source)) {\n target[kind as LogRecordMetricKind] = (target[kind as LogRecordMetricKind] ?? 0) + (count ?? 0);\n }\n}\n"],"names":["Buffer","parseLogRecordLine","parseRawLogRecordLine","logger","DEFAULT_HARNESS","config","accrueStoredBytes","claimCap","ensureJobAccounting","isJobCapped","insertChunk","db","casExtendCommittedLength","getAttemptStream","getOrCreateAttemptStreamWithStatus","setClaudeParseContext","setDeclaredTotalBytes","recordAppendedCount","streamClosedCount","streamOpenedCount","allowedBudget","closeStream","controlTombstone","MalformedLogChunkError","OffsetGapError","claudeInitSessionId","createClaudeParseContext","parseSessionRecord","parseAppendBody","body","length","records","hasAgentSessionRecord","text","toString","endsWith","lines","split","pop","declaredTotalBytes","line","record","detectForgedType","push","type","total_bytes","byteLength","LOG_MAX_SESSION_LINE_BYTES","undefined","getSessionHarness","workflows","stepId","getStepLogContext","harness","readHeartbeat","tx","params","existing","jobId","attempt","committedLength","capped","storeChunk","streamId","workspaceId","storedByteLen","accrued","delta","stored","recordCounts","streamOffset","offset","byteLen","data","origin","allowed","baseBytes","LOG_BUDGET_BASE_BYTES","ratePerMinuteBytes","LOG_BUDGET_RATE_BYTES_PER_MINUTE","elapsedMs","Date","now","startedAt","getTime","used","won","tombstone","buildStoredBody","initialClaudeParseContext","initialClaudePendingResult","isStreamFinal","storedRecords","parseContext","claude","isFinalResult","latestClaudeInitIndicesBySessionId","latestClaudeInitIndices","pendingResult","firstInit","firstClaudeInit","hasInit","storedAgentSessionRow","finalizePendingResult","sessionId","turn","index","entries","hasFutureClaudeInit","row","agentSessionRecord","isClaudeFinalResultRow","from","map","JSON","stringify","join","claudeParseContext","claudePendingResult","v","ts","timestamp","kind","label","sameSession","meta","value","String","sessionRecord","latestIndices","Map","set","currentIndex","get","appendLogs","parsed","error","forgedType","warn","offendingType","sessionHarness","commitByteLen","metrics","streamClosedReason","streamOpened","result","transaction","created","stream","projectId","workflowRunAttemptId","state","cas","id","outcome","parseHarness","claudeHasInit","claudeSessionId","claudeTurn","chunkStored","addRecordCounts","closed","reason","add","count","Object","target","source"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/core/append-logs.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {\n type LogRecord,\n parseLogRecordLine,\n parseRawLogRecordLine,\n type RawLogRecord,\n type SessionViewLifecycleRow,\n type SessionViewRow,\n} from '@shipfox/api-logs-dto';\nimport type {WorkflowsModuleClient} from '@shipfox/api-workflows-dto/inter-module';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {DEFAULT_HARNESS, type Harness} from '@shipfox/workflow-document';\nimport {config} from '#config.js';\nimport {accrueStoredBytes, claimCap, ensureJobAccounting, isJobCapped} from '#db/accounting.js';\nimport {insertChunk} from '#db/chunks.js';\nimport {db, type Transaction} from '#db/db.js';\nimport {\n casExtendCommittedLength,\n getAttemptStream,\n getOrCreateAttemptStreamWithStatus,\n setClaudeParseContext,\n setDeclaredTotalBytes,\n} from '#db/streams.js';\nimport {\n type LogRecordMetricKind,\n recordAppendedCount,\n streamClosedCount,\n streamOpenedCount,\n} from '#metrics/instance.js';\nimport {allowedBudget} from './budget.js';\nimport {closeStream, controlTombstone} from './close-stream.js';\nimport {MalformedLogChunkError, OffsetGapError} from './errors.js';\nimport {flushPendingToolRows} from './session/claude/rows.js';\nimport {\n type ClaudeParseContext,\n claudeInitSessionId,\n createClaudeParseContext,\n} from './session/claude-parser.js';\nimport type {SessionParseContext} from './session/parse-session.js';\nimport {parseSessionRecord} from './session/parse-session.js';\nimport type {AgentSessionRecord} from './session/session-record.js';\n\nexport interface AppendLogsParams {\n jobId: string;\n workspaceId: string;\n projectId: string;\n workflowRunAttemptId: string;\n stepId: string;\n attempt: number;\n offset: number;\n body: Buffer;\n}\n\nexport interface AppendLogsResult {\n committedLength: number;\n capped: boolean;\n}\n\ninterface ParsedBody {\n declaredTotalBytes?: number;\n records: RawLogRecord[];\n hasAgentSessionRecord: boolean;\n}\n\n/**\n * Pure pre-transaction parse. Requires whole, newline-terminated lines so\n * `committed_length` always lands on a line boundary (one body = one chunk = whole\n * lines; no line ever spans two chunks). Runs before any lock is taken, so a\n * malformed or large body never holds a row. The offset CAS uses the raw append byte length; the\n * budget charges the normalized body built from these parsed records.\n *\n * Each line is validated against the raw record union (a forged server-only\n * `capped`/`runner_lost` fails here); the declared total is pulled from an `end`\n * record. A line that is a valid server-only record under the read union surfaces\n * its type via `forgedType` for the narrowed audit warn.\n */\nfunction parseAppendBody(body: Buffer): ParsedBody {\n if (body.length === 0) return {records: [], hasAgentSessionRecord: false};\n\n const text = body.toString('utf8');\n if (!text.endsWith('\\n')) {\n throw new MalformedLogChunkError('append body must end with a newline (whole records only)');\n }\n const lines = text.split('\\n');\n lines.pop();\n\n let declaredTotalBytes: number | undefined;\n const records: RawLogRecord[] = [];\n let hasAgentSessionRecord = false;\n for (const line of lines) {\n let record: ReturnType<typeof parseRawLogRecordLine>;\n try {\n record = parseRawLogRecordLine(line);\n } catch {\n throw new MalformedLogChunkError(\n 'append body contains an invalid NDJSON record',\n detectForgedType(line),\n );\n }\n records.push(record);\n if (record.type === 'end') {\n declaredTotalBytes = record.total_bytes;\n }\n // An agent_session line is one whole entry in one record; bound its size here (the DTO\n // schema leaves `data` uncapped because it cannot read this runtime config). An over-cap\n // line is rejected so a single entry can never blow the request body or the spool window.\n if (\n record.type === 'agent_session' &&\n Buffer.byteLength(line, 'utf8') > config.LOG_MAX_SESSION_LINE_BYTES\n ) {\n throw new MalformedLogChunkError(\n `agent_session line exceeds ${config.LOG_MAX_SESSION_LINE_BYTES} bytes`,\n );\n }\n if (record.type === 'agent_session') {\n hasAgentSessionRecord = true;\n }\n }\n\n return declaredTotalBytes === undefined\n ? {records, hasAgentSessionRecord}\n : {declaredTotalBytes, records, hasAgentSessionRecord};\n}\n\n/**\n * A line that fails the raw write union but is a valid record under the read union\n * can only be a server-only `capped`/`runner_lost` tombstone — i.e. a forgery\n * attempt. Returns its type for the audit warn, or undefined for plain garbage.\n */\nfunction detectForgedType(line: string): string | undefined {\n try {\n return parseLogRecordLine(line).type;\n } catch {\n return undefined;\n }\n}\n\nasync function getSessionHarness(\n workflows: WorkflowsModuleClient | undefined,\n stepId: string,\n): Promise<Harness> {\n return workflows ? (await workflows.getStepLogContext({stepId})).harness : DEFAULT_HARNESS;\n}\n\n/**\n * Empty-body heartbeat: report the current committed length without materializing\n * a stream, so a runner cannot mint unbounded rows with empty appends.\n */\nasync function readHeartbeat(tx: Transaction, params: AppendLogsParams): Promise<AppendLogsResult> {\n const existing = await getAttemptStream(tx, {\n jobId: params.jobId,\n stepId: params.stepId,\n attempt: params.attempt,\n });\n return {\n committedLength: existing?.committedLength ?? 0,\n capped: await isJobCapped(tx, params.jobId),\n };\n}\n\ninterface StoreChunkParams {\n params: AppendLogsParams;\n streamId: string;\n body: Buffer;\n committedLength: number;\n declaredTotalBytes: number | undefined;\n}\n\ninterface StoreChunkResult extends AppendLogsResult {\n /**\n * Whether the runner chunk was persisted. False only when the job was already\n * capped and the body (including any `end` record) was dropped, so the stream is\n * not whole and must not be declared-closed.\n */\n stored: boolean;\n recordCounts: Partial<Record<LogRecord['type'], number>>;\n}\n\ninterface StoredBody {\n body: Buffer;\n recordCounts: Partial<Record<LogRecord['type'], number>>;\n claudeParseContext: ClaudeParseContext | undefined;\n claudePendingResult: SessionViewLifecycleRow | null | undefined;\n claudePendingToolRows: readonly SessionViewRow[] | undefined;\n}\n\n/**\n * Accrues the stored bytes, persists the chunk, and trips the per-job cap when\n * this append crosses the budget. Runs only after the offset-CAS extended\n * `committed_length`, so the committed length already reflects the accepted bytes.\n */\nasync function storeChunk(\n tx: Transaction,\n {params, streamId, body, committedLength, declaredTotalBytes}: StoreChunkParams,\n): Promise<StoreChunkResult> {\n await ensureJobAccounting(tx, {jobId: params.jobId, workspaceId: params.workspaceId});\n const storedByteLen = body.length;\n const accrued = await accrueStoredBytes(tx, {jobId: params.jobId, delta: storedByteLen});\n\n // Already capped: accept-and-drop. committed_length has advanced so the runner\n // drains its spool cleanly instead of retry-looping; nothing is stored.\n if (!accrued) return {committedLength, capped: true, stored: false, recordCounts: {}};\n\n await insertChunk(tx, {\n streamId,\n streamOffset: params.offset,\n byteLen: storedByteLen,\n data: body,\n origin: 'runner',\n });\n if (declaredTotalBytes !== undefined) {\n await setDeclaredTotalBytes(tx, {streamId, declaredTotalBytes});\n }\n\n const allowed = allowedBudget({\n baseBytes: config.LOG_BUDGET_BASE_BYTES,\n ratePerMinuteBytes: config.LOG_BUDGET_RATE_BYTES_PER_MINUTE,\n elapsedMs: Date.now() - accrued.startedAt.getTime(),\n });\n if (accrued.used <= allowed) {\n return {committedLength, capped: false, stored: true, recordCounts: {}};\n }\n\n // Over budget. No hard ceiling: this crossing append is stored in full (overshoot\n // bounded by one body). Claim the cap once and inject an in-band `capped` tombstone\n // for the winner.\n const won = await claimCap(tx, params.jobId);\n if (won) {\n const tombstone = controlTombstone('capped');\n await insertChunk(tx, {\n streamId,\n streamOffset: committedLength,\n byteLen: tombstone.length,\n data: tombstone,\n origin: 'control',\n });\n }\n return {\n committedLength,\n capped: true,\n stored: true,\n recordCounts: won ? {capped: 1} : {},\n };\n}\n\nfunction buildStoredBody(\n records: readonly RawLogRecord[],\n harness: Harness,\n initialClaudeParseContext?: ClaudeParseContext,\n initialClaudePendingResult?: SessionViewLifecycleRow | null,\n isStreamFinal = false,\n): StoredBody {\n const storedRecords: LogRecord[] = [];\n const parseContext: SessionParseContext | undefined =\n harness === 'claude'\n ? {\n claude: initialClaudeParseContext ?? createClaudeParseContext(),\n isFinalResult: true,\n }\n : undefined;\n const latestClaudeInitIndicesBySessionId =\n harness === 'claude' ? latestClaudeInitIndices(records) : undefined;\n let pendingResult = initialClaudePendingResult ?? null;\n\n if (parseContext?.claude !== undefined && pendingResult !== null) {\n const firstInit = firstClaudeInit(records);\n if (isStreamFinal || firstInit.hasInit) {\n storedRecords.push(\n storedAgentSessionRow(\n finalizePendingResult(\n pendingResult,\n firstInit.sessionId !== undefined &&\n firstInit.sessionId === parseContext.claude.sessionId,\n parseContext.claude.turn,\n ),\n ),\n );\n pendingResult = null;\n }\n }\n\n for (const [index, record] of records.entries()) {\n if (record.type !== 'agent_session') {\n storedRecords.push(record);\n continue;\n }\n\n if (parseContext?.claude !== undefined && latestClaudeInitIndicesBySessionId !== undefined) {\n parseContext.isFinalResult = !hasFutureClaudeInit(\n latestClaudeInitIndicesBySessionId,\n index,\n parseContext.claude.sessionId,\n );\n }\n\n for (const row of parseSessionRecord(agentSessionRecord(record), harness, parseContext)) {\n if (parseContext?.claude !== undefined && !isStreamFinal && isClaudeFinalResultRow(row)) {\n if (pendingResult !== null) {\n storedRecords.push(storedAgentSessionRow(pendingResult));\n }\n pendingResult = row;\n continue;\n }\n storedRecords.push(storedAgentSessionRow(row));\n }\n }\n\n if (isStreamFinal && parseContext?.claude !== undefined) {\n for (const row of flushPendingToolRows(parseContext.claude)) {\n storedRecords.push(storedAgentSessionRow(row));\n }\n }\n\n const body = Buffer.from(storedRecords.map((record) => `${JSON.stringify(record)}\\n`).join(''));\n const recordCounts: Partial<Record<LogRecord['type'], number>> = {};\n for (const record of storedRecords) {\n recordCounts[record.type] = (recordCounts[record.type] ?? 0) + 1;\n }\n\n return {\n body,\n recordCounts,\n claudeParseContext: parseContext?.claude,\n claudePendingResult: parseContext?.claude === undefined ? undefined : pendingResult,\n claudePendingToolRows: parseContext?.claude?.pendingToolRows,\n };\n}\n\nfunction storedAgentSessionRow(row: SessionViewRow): LogRecord {\n return {v: 1, ts: row.timestamp, type: 'agent_session', row};\n}\n\nfunction isClaudeFinalResultRow(row: SessionViewRow): row is SessionViewLifecycleRow {\n return row.kind === 'lifecycle' && row.label === 'Session completed';\n}\n\nfunction finalizePendingResult(\n row: SessionViewLifecycleRow,\n sameSession: boolean,\n turn: number,\n): SessionViewLifecycleRow {\n if (!sameSession) return row;\n\n return {\n ...row,\n label: `Turn ${turn} completed`,\n meta: [{label: 'turn', value: String(turn)}, ...row.meta],\n };\n}\n\nfunction firstClaudeInit(records: readonly RawLogRecord[]): {\n hasInit: boolean;\n sessionId: string | undefined;\n} {\n for (const record of records) {\n if (record.type !== 'agent_session') continue;\n const sessionRecord = agentSessionRecord(record);\n const sessionId = claudeInitSessionId(sessionRecord);\n if (sessionId !== undefined) return {hasInit: true, sessionId};\n }\n\n return {hasInit: false, sessionId: undefined};\n}\n\nfunction latestClaudeInitIndices(records: readonly RawLogRecord[]): Map<string, number> {\n const latestIndices = new Map<string, number>();\n\n for (const [index, record] of records.entries()) {\n if (record.type !== 'agent_session') continue;\n\n const sessionId = claudeInitSessionId(agentSessionRecord(record));\n if (sessionId !== undefined) latestIndices.set(sessionId, index);\n }\n\n return latestIndices;\n}\n\nfunction hasFutureClaudeInit(\n latestClaudeInitIndicesBySessionId: ReadonlyMap<string, number>,\n currentIndex: number,\n sessionId: string | null,\n): boolean {\n if (sessionId === null) return false;\n\n return (latestClaudeInitIndicesBySessionId.get(sessionId) ?? -1) > currentIndex;\n}\n\nfunction agentSessionRecord(\n record: Extract<RawLogRecord, {type: 'agent_session'}>,\n): AgentSessionRecord {\n return {data: record.data, ts: record.ts};\n}\n\n/**\n * Concurrency is serialized through Postgres row locks taken implicitly by the\n * conditional UPDATEs, not an explicit `SELECT ... FOR UPDATE`. Appends for one\n * job contend on its single accounting row, so the path is multi-instance safe\n * but not lock-free.\n */\nexport async function appendLogs(\n params: AppendLogsParams,\n workflows?: WorkflowsModuleClient,\n): Promise<AppendLogsResult> {\n let parsed: ParsedBody;\n try {\n parsed = parseAppendBody(params.body);\n } catch (error) {\n // Narrowed audit: only the detectable forgery case, never the payload or a token.\n if (error instanceof MalformedLogChunkError && error.forgedType !== undefined) {\n logger().warn(\n {\n jobId: params.jobId,\n stepId: params.stepId,\n offendingType: error.forgedType,\n },\n 'Rejected forged server-only log record on append',\n );\n }\n throw error;\n }\n const {declaredTotalBytes} = parsed;\n const sessionHarness = parsed.hasAgentSessionRecord\n ? await getSessionHarness(workflows, params.stepId)\n : DEFAULT_HARNESS;\n const commitByteLen = params.body.length;\n const metrics = {\n recordCounts: {} as Partial<Record<LogRecordMetricKind, number>>,\n streamClosedReason: undefined as 'declared' | undefined,\n streamOpened: false,\n };\n\n const result = await db().transaction(async (tx) => {\n if (commitByteLen === 0) return readHeartbeat(tx, params);\n\n const {created, stream} = await getOrCreateAttemptStreamWithStatus(tx, {\n jobId: params.jobId,\n stepId: params.stepId,\n attempt: params.attempt,\n workspaceId: params.workspaceId,\n projectId: params.projectId,\n workflowRunAttemptId: params.workflowRunAttemptId,\n });\n metrics.streamOpened = created;\n\n // Closed stream (the runner's end already landed, or the job-terminated sweep ran):\n // accept-and-drop so a late chunk can never race compaction. committed_length is\n // frozen at close, so this reports the final offset and the runner stops cleanly.\n if (stream.state === 'closed') {\n return {committedLength: stream.committedLength, capped: await isJobCapped(tx, params.jobId)};\n }\n\n const cas = await casExtendCommittedLength(tx, {\n streamId: stream.id,\n offset: params.offset,\n byteLen: commitByteLen,\n });\n if (cas.outcome === 'gap') throw new OffsetGapError(cas.committedLength);\n if (cas.outcome === 'retry') {\n return {committedLength: cas.committedLength, capped: await isJobCapped(tx, params.jobId)};\n }\n\n const parseHarness =\n sessionHarness === 'claude' ||\n stream.claudePendingResult !== null ||\n stream.claudePendingToolRows.length > 0\n ? 'claude'\n : sessionHarness;\n const stored = buildStoredBody(\n parsed.records,\n parseHarness,\n parseHarness === 'claude'\n ? createClaudeParseContext(stream.claudePendingToolRows, {\n hasInit: stream.claudeHasInit,\n sessionId: stream.claudeSessionId,\n turn: stream.claudeTurn,\n })\n : undefined,\n parseHarness === 'claude' ? stream.claudePendingResult : undefined,\n declaredTotalBytes !== undefined,\n );\n\n const {\n recordCounts,\n stored: chunkStored,\n ...result\n } = await storeChunk(tx, {\n params,\n streamId: stream.id,\n body: stored.body,\n committedLength: cas.committedLength,\n declaredTotalBytes,\n });\n if (chunkStored) {\n addRecordCounts(metrics.recordCounts, stored.recordCounts);\n if (stored.claudeParseContext !== undefined) {\n await setClaudeParseContext(tx, {\n streamId: stream.id,\n hasInit: stored.claudeParseContext.hasInit,\n sessionId: stored.claudeParseContext.sessionId,\n turn: stored.claudeParseContext.turn,\n pendingResult: stored.claudePendingResult ?? null,\n pendingToolRows: stored.claudePendingToolRows ?? [],\n });\n }\n }\n addRecordCounts(metrics.recordCounts, recordCounts);\n\n // The runner's end record was committed in this append (offset-CAS guarantees\n // everything before it is already committed), so the stream is whole. Declared-close\n // it in-band so compaction starts at once instead of waiting for the timeout sweep.\n // Only when the chunk was actually stored: an end body dropped because the job was\n // already capped persists nothing, so the stream is not whole and stays open for the\n // timeout sweep to close it as truncated.\n if (declaredTotalBytes !== undefined && chunkStored) {\n const closed = await closeStream(tx, {streamId: stream.id, reason: 'declared'});\n if (closed) metrics.streamClosedReason = 'declared';\n }\n\n return result;\n });\n\n if (metrics.streamOpened) streamOpenedCount.add(1);\n for (const [kind, count] of Object.entries(metrics.recordCounts)) {\n if (count > 0) recordAppendedCount.add(count, {kind: kind as LogRecordMetricKind});\n }\n if (metrics.streamClosedReason) {\n streamClosedCount.add(1, {reason: metrics.streamClosedReason});\n }\n\n return result;\n}\n\nfunction addRecordCounts(\n target: Partial<Record<LogRecordMetricKind, number>>,\n source: Partial<Record<LogRecordMetricKind, number>>,\n): void {\n for (const [kind, count] of Object.entries(source)) {\n target[kind as LogRecordMetricKind] = (target[kind as LogRecordMetricKind] ?? 0) + (count ?? 0);\n }\n}\n"],"names":["Buffer","parseLogRecordLine","parseRawLogRecordLine","logger","DEFAULT_HARNESS","config","accrueStoredBytes","claimCap","ensureJobAccounting","isJobCapped","insertChunk","db","casExtendCommittedLength","getAttemptStream","getOrCreateAttemptStreamWithStatus","setClaudeParseContext","setDeclaredTotalBytes","recordAppendedCount","streamClosedCount","streamOpenedCount","allowedBudget","closeStream","controlTombstone","MalformedLogChunkError","OffsetGapError","flushPendingToolRows","claudeInitSessionId","createClaudeParseContext","parseSessionRecord","parseAppendBody","body","length","records","hasAgentSessionRecord","text","toString","endsWith","lines","split","pop","declaredTotalBytes","line","record","detectForgedType","push","type","total_bytes","byteLength","LOG_MAX_SESSION_LINE_BYTES","undefined","getSessionHarness","workflows","stepId","getStepLogContext","harness","readHeartbeat","tx","params","existing","jobId","attempt","committedLength","capped","storeChunk","streamId","workspaceId","storedByteLen","accrued","delta","stored","recordCounts","streamOffset","offset","byteLen","data","origin","allowed","baseBytes","LOG_BUDGET_BASE_BYTES","ratePerMinuteBytes","LOG_BUDGET_RATE_BYTES_PER_MINUTE","elapsedMs","Date","now","startedAt","getTime","used","won","tombstone","buildStoredBody","initialClaudeParseContext","initialClaudePendingResult","isStreamFinal","storedRecords","parseContext","claude","isFinalResult","latestClaudeInitIndicesBySessionId","latestClaudeInitIndices","pendingResult","firstInit","firstClaudeInit","hasInit","storedAgentSessionRow","finalizePendingResult","sessionId","turn","index","entries","hasFutureClaudeInit","row","agentSessionRecord","isClaudeFinalResultRow","from","map","JSON","stringify","join","claudeParseContext","claudePendingResult","claudePendingToolRows","pendingToolRows","v","ts","timestamp","kind","label","sameSession","meta","value","String","sessionRecord","latestIndices","Map","set","currentIndex","get","appendLogs","parsed","error","forgedType","warn","offendingType","sessionHarness","commitByteLen","metrics","streamClosedReason","streamOpened","result","transaction","created","stream","projectId","workflowRunAttemptId","state","cas","id","outcome","parseHarness","claudeHasInit","claudeSessionId","claudeTurn","chunkStored","addRecordCounts","closed","reason","add","count","Object","target","source"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"close-stream.d.ts","sourceRoot":"","sources":["../../src/core/close-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAGnC,OAAO,KAAK,EAAC,aAAa,EAAE,iBAAiB,EAAC,MAAM,kCAAkC,CAAC;AAEvF,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,WAAW,CAAC;AAI3C,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,aAAa,CAAC;AAErD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAG5D;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"close-stream.d.ts","sourceRoot":"","sources":["../../src/core/close-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAGnC,OAAO,KAAK,EAAC,aAAa,EAAE,iBAAiB,EAAC,MAAM,kCAAkC,CAAC;AAEvF,OAAO,KAAK,EAAC,WAAW,EAAC,MAAM,WAAW,CAAC;AAI3C,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,aAAa,CAAC;AAErD;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAG5D;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,iBAAiB,CAAC;CAC3B;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,WAAW,CAC/B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE,iBAAiB,GACxB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAiE/B"}
|
|
@@ -34,7 +34,22 @@ import { markStreamClosed } from '#db/streams.js';
|
|
|
34
34
|
markTruncated: params.reason === 'timeout'
|
|
35
35
|
});
|
|
36
36
|
if (!closedResult) return null;
|
|
37
|
-
const { stream: closed, pendingClaudeResult } = closedResult;
|
|
37
|
+
const { stream: closed, pendingClaudeResult, pendingClaudeToolRows } = closedResult;
|
|
38
|
+
if (pendingClaudeToolRows.length > 0) {
|
|
39
|
+
const data = Buffer.from(`${pendingClaudeToolRows.map((row)=>JSON.stringify({
|
|
40
|
+
v: 1,
|
|
41
|
+
ts: row.timestamp,
|
|
42
|
+
type: 'agent_session',
|
|
43
|
+
row
|
|
44
|
+
})).join('\n')}\n`);
|
|
45
|
+
await insertChunk(tx, {
|
|
46
|
+
streamId: closed.id,
|
|
47
|
+
streamOffset: closed.committedLength,
|
|
48
|
+
byteLen: data.length,
|
|
49
|
+
data,
|
|
50
|
+
origin: 'control'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
38
53
|
if (pendingClaudeResult !== null) {
|
|
39
54
|
const record = {
|
|
40
55
|
v: 1,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/core/close-stream.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {LOG_STREAM_CLOSED, type LogRecord, type LogsEventMap} from '@shipfox/api-logs-dto';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport type {AttemptStream, StreamCloseReason} from '#core/entities/attempt-stream.js';\nimport {insertChunk} from '#db/chunks.js';\nimport type {Transaction} from '#db/db.js';\nimport {logsOutbox} from '#db/schema/outbox.js';\nimport {markStreamClosed} from '#db/streams.js';\n\nexport type TombstoneKind = 'capped' | 'runner_lost';\n\n/**\n * Frames a server tombstone as one newline-terminated record. Typed against the\n * shared contract so an envelope change breaks this at compile time. Stored as a\n * `control`-origin chunk; like every server record it does NOT advance\n * `committed_length`, so the CAS axis stays equal to runner spool bytes.\n */\nexport function controlTombstone(kind: TombstoneKind): Buffer {\n const record: LogRecord = {v: 1, ts: Date.now(), type: kind};\n return Buffer.from(`${JSON.stringify(record)}\\n`, 'utf8');\n}\n\nexport interface CloseStreamParams {\n streamId: string;\n reason: StreamCloseReason;\n}\n\n/**\n * The row lock plus guarded UPDATE (`WHERE state='open'`) in `markStreamClosed` provide\n * the lock and idempotency gate: a stream already closed by the other path (append-time\n * declared close vs the job-terminated timeout sweep) returns null, so no duplicate\n * `LOG_STREAM_CLOSED` is written and no second tombstone lands. A pending Claude result\n * is materialized before the close event, then a timeout close sets `truncated` and injects\n * a `runner_lost` tombstone in-band (a `capped` tombstone, if any, was injected earlier at\n * the append that tripped the cap).\n *\n * The event drives compaction; it is written in the same transaction as the flip.\n */\nexport async function closeStream(\n tx: Transaction,\n params: CloseStreamParams,\n): Promise<AttemptStream | null> {\n const closedResult = await markStreamClosed(tx, {\n streamId: params.streamId,\n reason: params.reason,\n markTruncated: params.reason === 'timeout',\n });\n if (!closedResult) return null;\n\n const {stream: closed, pendingClaudeResult} = closedResult;\n if (pendingClaudeResult !== null) {\n const record: LogRecord = {\n v: 1,\n ts: pendingClaudeResult.timestamp,\n type: 'agent_session',\n row: pendingClaudeResult,\n };\n const data = Buffer.from(`${JSON.stringify(record)}\\n`, 'utf8');\n await insertChunk(tx, {\n streamId: closed.id,\n streamOffset: closed.committedLength,\n byteLen: data.length,\n data,\n origin: 'control',\n });\n }\n\n if (params.reason === 'timeout') {\n const tombstone = controlTombstone('runner_lost');\n await insertChunk(tx, {\n streamId: closed.id,\n streamOffset: closed.committedLength,\n byteLen: tombstone.length,\n data: tombstone,\n origin: 'control',\n });\n }\n\n await writeOutboxEvent<LogsEventMap>(tx, logsOutbox, {\n type: LOG_STREAM_CLOSED,\n payload: {\n workspaceId: closed.workspaceId,\n jobId: closed.jobId,\n stepId: closed.stepId,\n attempt: closed.attempt,\n streamId: closed.id,\n },\n });\n\n return closed;\n}\n"],"names":["Buffer","LOG_STREAM_CLOSED","writeOutboxEvent","insertChunk","logsOutbox","markStreamClosed","controlTombstone","kind","record","v","ts","Date","now","type","from","JSON","stringify","closeStream","tx","params","closedResult","streamId","reason","markTruncated","stream","closed","pendingClaudeResult","
|
|
1
|
+
{"version":3,"sources":["../../src/core/close-stream.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {LOG_STREAM_CLOSED, type LogRecord, type LogsEventMap} from '@shipfox/api-logs-dto';\nimport {writeOutboxEvent} from '@shipfox/node-outbox';\nimport type {AttemptStream, StreamCloseReason} from '#core/entities/attempt-stream.js';\nimport {insertChunk} from '#db/chunks.js';\nimport type {Transaction} from '#db/db.js';\nimport {logsOutbox} from '#db/schema/outbox.js';\nimport {markStreamClosed} from '#db/streams.js';\n\nexport type TombstoneKind = 'capped' | 'runner_lost';\n\n/**\n * Frames a server tombstone as one newline-terminated record. Typed against the\n * shared contract so an envelope change breaks this at compile time. Stored as a\n * `control`-origin chunk; like every server record it does NOT advance\n * `committed_length`, so the CAS axis stays equal to runner spool bytes.\n */\nexport function controlTombstone(kind: TombstoneKind): Buffer {\n const record: LogRecord = {v: 1, ts: Date.now(), type: kind};\n return Buffer.from(`${JSON.stringify(record)}\\n`, 'utf8');\n}\n\nexport interface CloseStreamParams {\n streamId: string;\n reason: StreamCloseReason;\n}\n\n/**\n * The row lock plus guarded UPDATE (`WHERE state='open'`) in `markStreamClosed` provide\n * the lock and idempotency gate: a stream already closed by the other path (append-time\n * declared close vs the job-terminated timeout sweep) returns null, so no duplicate\n * `LOG_STREAM_CLOSED` is written and no second tombstone lands. A pending Claude result\n * is materialized before the close event, then a timeout close sets `truncated` and injects\n * a `runner_lost` tombstone in-band (a `capped` tombstone, if any, was injected earlier at\n * the append that tripped the cap).\n *\n * The event drives compaction; it is written in the same transaction as the flip.\n */\nexport async function closeStream(\n tx: Transaction,\n params: CloseStreamParams,\n): Promise<AttemptStream | null> {\n const closedResult = await markStreamClosed(tx, {\n streamId: params.streamId,\n reason: params.reason,\n markTruncated: params.reason === 'timeout',\n });\n if (!closedResult) return null;\n\n const {stream: closed, pendingClaudeResult, pendingClaudeToolRows} = closedResult;\n if (pendingClaudeToolRows.length > 0) {\n const data = Buffer.from(\n `${pendingClaudeToolRows\n .map((row) =>\n JSON.stringify({v: 1, ts: row.timestamp, type: 'agent_session', row} satisfies LogRecord),\n )\n .join('\\n')}\\n`,\n );\n await insertChunk(tx, {\n streamId: closed.id,\n streamOffset: closed.committedLength,\n byteLen: data.length,\n data,\n origin: 'control',\n });\n }\n if (pendingClaudeResult !== null) {\n const record: LogRecord = {\n v: 1,\n ts: pendingClaudeResult.timestamp,\n type: 'agent_session',\n row: pendingClaudeResult,\n };\n const data = Buffer.from(`${JSON.stringify(record)}\\n`, 'utf8');\n await insertChunk(tx, {\n streamId: closed.id,\n streamOffset: closed.committedLength,\n byteLen: data.length,\n data,\n origin: 'control',\n });\n }\n\n if (params.reason === 'timeout') {\n const tombstone = controlTombstone('runner_lost');\n await insertChunk(tx, {\n streamId: closed.id,\n streamOffset: closed.committedLength,\n byteLen: tombstone.length,\n data: tombstone,\n origin: 'control',\n });\n }\n\n await writeOutboxEvent<LogsEventMap>(tx, logsOutbox, {\n type: LOG_STREAM_CLOSED,\n payload: {\n workspaceId: closed.workspaceId,\n jobId: closed.jobId,\n stepId: closed.stepId,\n attempt: closed.attempt,\n streamId: closed.id,\n },\n });\n\n return closed;\n}\n"],"names":["Buffer","LOG_STREAM_CLOSED","writeOutboxEvent","insertChunk","logsOutbox","markStreamClosed","controlTombstone","kind","record","v","ts","Date","now","type","from","JSON","stringify","closeStream","tx","params","closedResult","streamId","reason","markTruncated","stream","closed","pendingClaudeResult","pendingClaudeToolRows","length","data","map","row","timestamp","join","id","streamOffset","committedLength","byteLen","origin","tombstone","payload","workspaceId","jobId","stepId","attempt"],"mappings":"AAAA,SAAQA,MAAM,QAAO,cAAc;AACnC,SAAQC,iBAAiB,QAA0C,wBAAwB;AAC3F,SAAQC,gBAAgB,QAAO,uBAAuB;AAEtD,SAAQC,WAAW,QAAO,gBAAgB;AAE1C,SAAQC,UAAU,QAAO,uBAAuB;AAChD,SAAQC,gBAAgB,QAAO,iBAAiB;AAIhD;;;;;CAKC,GACD,OAAO,SAASC,iBAAiBC,IAAmB;IAClD,MAAMC,SAAoB;QAACC,GAAG;QAAGC,IAAIC,KAAKC,GAAG;QAAIC,MAAMN;IAAI;IAC3D,OAAOP,OAAOc,IAAI,CAAC,GAAGC,KAAKC,SAAS,CAACR,QAAQ,EAAE,CAAC,EAAE;AACpD;AAOA;;;;;;;;;;CAUC,GACD,OAAO,eAAeS,YACpBC,EAAe,EACfC,MAAyB;IAEzB,MAAMC,eAAe,MAAMf,iBAAiBa,IAAI;QAC9CG,UAAUF,OAAOE,QAAQ;QACzBC,QAAQH,OAAOG,MAAM;QACrBC,eAAeJ,OAAOG,MAAM,KAAK;IACnC;IACA,IAAI,CAACF,cAAc,OAAO;IAE1B,MAAM,EAACI,QAAQC,MAAM,EAAEC,mBAAmB,EAAEC,qBAAqB,EAAC,GAAGP;IACrE,IAAIO,sBAAsBC,MAAM,GAAG,GAAG;QACpC,MAAMC,OAAO7B,OAAOc,IAAI,CACtB,GAAGa,sBACAG,GAAG,CAAC,CAACC,MACJhB,KAAKC,SAAS,CAAC;gBAACP,GAAG;gBAAGC,IAAIqB,IAAIC,SAAS;gBAAEnB,MAAM;gBAAiBkB;YAAG,IAEpEE,IAAI,CAAC,MAAM,EAAE,CAAC;QAEnB,MAAM9B,YAAYe,IAAI;YACpBG,UAAUI,OAAOS,EAAE;YACnBC,cAAcV,OAAOW,eAAe;YACpCC,SAASR,KAAKD,MAAM;YACpBC;YACAS,QAAQ;QACV;IACF;IACA,IAAIZ,wBAAwB,MAAM;QAChC,MAAMlB,SAAoB;YACxBC,GAAG;YACHC,IAAIgB,oBAAoBM,SAAS;YACjCnB,MAAM;YACNkB,KAAKL;QACP;QACA,MAAMG,OAAO7B,OAAOc,IAAI,CAAC,GAAGC,KAAKC,SAAS,CAACR,QAAQ,EAAE,CAAC,EAAE;QACxD,MAAML,YAAYe,IAAI;YACpBG,UAAUI,OAAOS,EAAE;YACnBC,cAAcV,OAAOW,eAAe;YACpCC,SAASR,KAAKD,MAAM;YACpBC;YACAS,QAAQ;QACV;IACF;IAEA,IAAInB,OAAOG,MAAM,KAAK,WAAW;QAC/B,MAAMiB,YAAYjC,iBAAiB;QACnC,MAAMH,YAAYe,IAAI;YACpBG,UAAUI,OAAOS,EAAE;YACnBC,cAAcV,OAAOW,eAAe;YACpCC,SAASE,UAAUX,MAAM;YACzBC,MAAMU;YACND,QAAQ;QACV;IACF;IAEA,MAAMpC,iBAA+BgB,IAAId,YAAY;QACnDS,MAAMZ;QACNuC,SAAS;YACPC,aAAahB,OAAOgB,WAAW;YAC/BC,OAAOjB,OAAOiB,KAAK;YACnBC,QAAQlB,OAAOkB,MAAM;YACrBC,SAASnB,OAAOmB,OAAO;YACvBvB,UAAUI,OAAOS,EAAE;QACrB;IACF;IAEA,OAAOT;AACT"}
|
|
@@ -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
|
/** Lifecycle state of an attempt's log stream. */
|
|
3
3
|
export type StreamState = 'open' | 'closed';
|
|
4
4
|
/** Why a stream was closed, once it is. */
|
|
@@ -32,6 +32,7 @@ export interface AttemptStream {
|
|
|
32
32
|
claudeSessionId: string | null;
|
|
33
33
|
claudeTurn: number;
|
|
34
34
|
claudePendingResult: SessionViewLifecycleRow | null;
|
|
35
|
+
claudePendingToolRows: SessionViewRow[];
|
|
35
36
|
truncated: boolean;
|
|
36
37
|
objectKey: string | null;
|
|
37
38
|
createdAt: Date;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"attempt-stream.d.ts","sourceRoot":"","sources":["../../../src/core/entities/attempt-stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,uBAAuB,EAAC,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"attempt-stream.d.ts","sourceRoot":"","sources":["../../../src/core/entities/attempt-stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,uBAAuB,EAAE,cAAc,EAAC,MAAM,uBAAuB,CAAC;AAEnF,kDAAkD;AAClD,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,QAAQ,CAAC;AAE5C,2CAA2C;AAC3C,MAAM,MAAM,iBAAiB,GAAG,UAAU,GAAG,SAAS,CAAC;AAEvD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,aAAa;IAC5B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,MAAM,CAAC;IAClB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,eAAe,EAAE,MAAM,CAAC;IACxB,KAAK,EAAE,WAAW,CAAC;IACnB,WAAW,EAAE,iBAAiB,GAAG,IAAI,CAAC;IACtC,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,aAAa,EAAE,OAAO,CAAC;IACvB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,UAAU,EAAE,MAAM,CAAC;IACnB,mBAAmB,EAAE,uBAAuB,GAAG,IAAI,CAAC;IACpD,qBAAqB,EAAE,cAAc,EAAE,CAAC;IACxC,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,SAAS,EAAE,IAAI,CAAC;IAChB,SAAS,EAAE,IAAI,CAAC;IAChB,yFAAyF;IACzF,QAAQ,EAAE,IAAI,GAAG,IAAI,CAAC;CACvB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/core/entities/attempt-stream.ts"],"sourcesContent":["import type {SessionViewLifecycleRow} from '@shipfox/api-logs-dto';\n\n/** Lifecycle state of an attempt's log stream. */\nexport type StreamState = 'open' | 'closed';\n\n/** Why a stream was closed, once it is. */\nexport type StreamCloseReason = 'declared' | 'timeout';\n\n/**\n * A single `(job, step, attempt)` log stream. Identity is scoped to the lease's\n * job. `workspaceId`, `projectId`, and `workflowRunAttemptId` are stamped from the lease on\n * stream creation and asserted on subsequent appends, so reads can authorize\n * per-project without joining back to workflows. `committedLength` is the\n * offset-CAS axis: raw NDJSON spool bytes the server has durably accepted from\n * the runner.\n *\n * `truncated` is an out-of-band terminal flag set when the timeout sweep\n * force-closes a stream the runner never ended. Budget exhaustion (\"capped\") is a\n * job-level signal recorded in `job_accounting` and surfaced in-band by a `capped`\n * tombstone record, not on the stream row.\n */\nexport interface AttemptStream {\n id: string;\n jobId: string;\n stepId: string;\n attempt: number;\n workspaceId: string;\n projectId: string;\n workflowRunAttemptId: string;\n committedLength: number;\n state: StreamState;\n closeReason: StreamCloseReason | null;\n declaredTotalBytes: number | null;\n claudeHasInit: boolean;\n claudeSessionId: string | null;\n claudeTurn: number;\n claudePendingResult: SessionViewLifecycleRow | null;\n truncated: boolean;\n objectKey: string | null;\n createdAt: Date;\n updatedAt: Date;\n /** When the stream was closed (either close path); null while open. Retention anchor. */\n closedAt: Date | null;\n}\n"],"names":[],"mappings":"AAQA;;;;;;;;;;;;CAYC,GACD,
|
|
1
|
+
{"version":3,"sources":["../../../src/core/entities/attempt-stream.ts"],"sourcesContent":["import type {SessionViewLifecycleRow, SessionViewRow} from '@shipfox/api-logs-dto';\n\n/** Lifecycle state of an attempt's log stream. */\nexport type StreamState = 'open' | 'closed';\n\n/** Why a stream was closed, once it is. */\nexport type StreamCloseReason = 'declared' | 'timeout';\n\n/**\n * A single `(job, step, attempt)` log stream. Identity is scoped to the lease's\n * job. `workspaceId`, `projectId`, and `workflowRunAttemptId` are stamped from the lease on\n * stream creation and asserted on subsequent appends, so reads can authorize\n * per-project without joining back to workflows. `committedLength` is the\n * offset-CAS axis: raw NDJSON spool bytes the server has durably accepted from\n * the runner.\n *\n * `truncated` is an out-of-band terminal flag set when the timeout sweep\n * force-closes a stream the runner never ended. Budget exhaustion (\"capped\") is a\n * job-level signal recorded in `job_accounting` and surfaced in-band by a `capped`\n * tombstone record, not on the stream row.\n */\nexport interface AttemptStream {\n id: string;\n jobId: string;\n stepId: string;\n attempt: number;\n workspaceId: string;\n projectId: string;\n workflowRunAttemptId: string;\n committedLength: number;\n state: StreamState;\n closeReason: StreamCloseReason | null;\n declaredTotalBytes: number | null;\n claudeHasInit: boolean;\n claudeSessionId: string | null;\n claudeTurn: number;\n claudePendingResult: SessionViewLifecycleRow | null;\n claudePendingToolRows: SessionViewRow[];\n truncated: boolean;\n objectKey: string | null;\n createdAt: Date;\n updatedAt: Date;\n /** When the stream was closed (either close path); null while open. Retention anchor. */\n closedAt: Date | null;\n}\n"],"names":[],"mappings":"AAQA;;;;;;;;;;;;CAYC,GACD,WAuBC"}
|
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
import type { SessionViewLifecycleRow, SessionViewRow } from '@shipfox/api-logs-dto';
|
|
1
|
+
import type { SessionViewLifecycleRow, SessionViewRow, SessionViewRowMeta } from '@shipfox/api-logs-dto';
|
|
2
2
|
import type { ClaudeParseContext } from '../claude-parser.js';
|
|
3
3
|
export declare const PURE_PROGRESS_CLAUDE_SYSTEM_SUBTYPES: Set<string>;
|
|
4
|
+
type SystemEventMapping = {
|
|
5
|
+
label: string | ((message: Record<string, unknown>) => string);
|
|
6
|
+
tone: SessionViewLifecycleRow['tone'] | ((message: Record<string, unknown>) => SessionViewLifecycleRow['tone']);
|
|
7
|
+
detail?: (message: Record<string, unknown>) => string | null;
|
|
8
|
+
meta?: (message: Record<string, unknown>) => readonly SessionViewRowMeta[];
|
|
9
|
+
};
|
|
10
|
+
export declare const SYSTEM_EVENT_MAPPINGS: Record<string, SystemEventMapping>;
|
|
4
11
|
export declare function systemRow(timestamp: number, message: Record<string, unknown>, context: ClaudeParseContext): SessionViewLifecycleRow;
|
|
5
|
-
export declare function
|
|
6
|
-
export declare function
|
|
12
|
+
export declare function authStatusRow(timestamp: number, message: Record<string, unknown>): SessionViewLifecycleRow;
|
|
13
|
+
export declare function rateLimitRow(timestamp: number, message: Record<string, unknown>): SessionViewLifecycleRow;
|
|
14
|
+
export declare function toolUseSummary(message: Record<string, unknown>, context: ClaudeParseContext): boolean;
|
|
15
|
+
export declare function flushPendingToolRows(context: ClaudeParseContext): readonly SessionViewRow[];
|
|
16
|
+
export declare function assistantRows(timestamp: number, message: Record<string, unknown>, context: ClaudeParseContext): readonly SessionViewRow[];
|
|
17
|
+
export declare function userRows(timestamp: number, message: Record<string, unknown>, context: ClaudeParseContext): readonly SessionViewRow[];
|
|
7
18
|
export declare function resultRow(timestamp: number, message: Record<string, unknown>, turn: number, isFinalResult: boolean): SessionViewLifecycleRow;
|
|
19
|
+
export {};
|
|
8
20
|
//# sourceMappingURL=rows.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../../../../src/core/session/claude/rows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,cAAc,
|
|
1
|
+
{"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../../../../src/core/session/claude/rows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACvB,cAAc,EACd,kBAAkB,EAGnB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,qBAAqB,CAAC;AAe5D,eAAO,MAAM,oCAAoC,aAY/C,CAAC;AAGH,KAAK,kBAAkB,GAAG;IACxB,KAAK,EAAE,MAAM,GAAG,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,CAAC,CAAC;IAC/D,IAAI,EACA,uBAAuB,CAAC,MAAM,CAAC,GAC/B,CAAC,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,uBAAuB,CAAC,MAAM,CAAC,CAAC,CAAC;IAC5E,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,MAAM,GAAG,IAAI,CAAC;IAC7D,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,SAAS,kBAAkB,EAAE,CAAC;CAC5E,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CA8GpE,CAAC;AAEF,wBAAgB,SAAS,CACvB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,EAAE,kBAAkB,GAC1B,uBAAuB,CAmDzB;AAED,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,uBAAuB,CAgBzB;AAWD,wBAAgB,YAAY,CAC1B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC/B,uBAAuB,CAmBzB;AAED,wBAAgB,cAAc,CAC5B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAsBT;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,kBAAkB,GAAG,SAAS,cAAc,EAAE,CAO3F;AAgBD,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,EAAE,kBAAkB,GAC1B,SAAS,cAAc,EAAE,CA6D3B;AAED,wBAAgB,QAAQ,CACtB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,OAAO,EAAE,kBAAkB,GAC1B,SAAS,cAAc,EAAE,CA4C3B;AAED,wBAAgB,SAAS,CACvB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAChC,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,OAAO,GACrB,uBAAuB,CA6BzB"}
|
|
@@ -15,7 +15,7 @@ export const PURE_PROGRESS_CLAUDE_SYSTEM_SUBTYPES = new Set([
|
|
|
15
15
|
'plugin_install'
|
|
16
16
|
]);
|
|
17
17
|
const OUTPUT_REPROMPT_PREFIX = 'The previous turn ended without setting required workflow outputs:';
|
|
18
|
-
const SYSTEM_EVENT_MAPPINGS = {
|
|
18
|
+
export const SYSTEM_EVENT_MAPPINGS = {
|
|
19
19
|
permission_denied: {
|
|
20
20
|
label: 'Permission denied',
|
|
21
21
|
tone: 'error',
|
|
@@ -55,6 +55,21 @@ const SYSTEM_EVENT_MAPPINGS = {
|
|
|
55
55
|
].filter(isMeta);
|
|
56
56
|
}
|
|
57
57
|
},
|
|
58
|
+
hook_response: {
|
|
59
|
+
label: (message)=>{
|
|
60
|
+
const outcome = stringField(message, 'outcome');
|
|
61
|
+
return outcome === 'error' ? 'Hook failed' : outcome === 'cancelled' ? 'Hook cancelled' : outcome === 'success' ? 'Hook completed' : 'Hook response';
|
|
62
|
+
},
|
|
63
|
+
tone: (message)=>{
|
|
64
|
+
const outcome = stringField(message, 'outcome');
|
|
65
|
+
return outcome === 'error' ? 'error' : outcome === 'cancelled' ? 'warning' : 'default';
|
|
66
|
+
},
|
|
67
|
+
detail: (message)=>stringField(message, 'output') ?? stringField(message, 'stderr') ?? stringField(message, 'stdout') ?? null,
|
|
68
|
+
meta: (message)=>[
|
|
69
|
+
metaItem('hook', stringField(message, 'hook_name')),
|
|
70
|
+
numberMeta(message, 'exit_code', 'exit code')
|
|
71
|
+
].filter(isMeta)
|
|
72
|
+
},
|
|
58
73
|
model_refusal_fallback: {
|
|
59
74
|
label: 'Model refused, fell back',
|
|
60
75
|
tone: 'warning'
|
|
@@ -114,6 +129,10 @@ export function systemRow(timestamp, message, context) {
|
|
|
114
129
|
return lifecycleRow(timestamp, label, detail, tone, false, meta);
|
|
115
130
|
}
|
|
116
131
|
const isNewSession = !context.hasInit || sessionId === undefined || sessionId !== context.sessionId;
|
|
132
|
+
if (isNewSession) {
|
|
133
|
+
context.pendingToolRows.length = 0;
|
|
134
|
+
context.toolCallRows.clear();
|
|
135
|
+
}
|
|
117
136
|
context.hasInit = true;
|
|
118
137
|
context.sessionId = sessionId ?? null;
|
|
119
138
|
context.turn = isNewSession ? 1 : context.turn + 1;
|
|
@@ -125,22 +144,94 @@ export function systemRow(timestamp, message, context) {
|
|
|
125
144
|
].filter(isMeta);
|
|
126
145
|
return lifecycleRow(timestamp, label, null, 'default', false, meta);
|
|
127
146
|
}
|
|
147
|
+
export function authStatusRow(timestamp, message) {
|
|
148
|
+
const error = stringField(message, 'error');
|
|
149
|
+
const output = stringList(field(message, 'output')).join('\n');
|
|
150
|
+
const isAuthenticating = booleanField(message, 'isAuthenticating');
|
|
151
|
+
return lifecycleRow(timestamp, error ? 'Authentication failed' : isAuthenticating ? 'Authenticating' : 'Authentication complete', (error ?? output) || null, error ? 'error' : 'default', false);
|
|
152
|
+
}
|
|
153
|
+
const RATE_LIMIT_STATUS_MAPPINGS = {
|
|
154
|
+
rejected: {
|
|
155
|
+
label: 'Rate limit exceeded',
|
|
156
|
+
tone: 'error'
|
|
157
|
+
},
|
|
158
|
+
allowed_warning: {
|
|
159
|
+
label: 'Rate limit warning',
|
|
160
|
+
tone: 'warning'
|
|
161
|
+
},
|
|
162
|
+
allowed: {
|
|
163
|
+
label: 'Rate limit available',
|
|
164
|
+
tone: 'default'
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
export function rateLimitRow(timestamp, message) {
|
|
168
|
+
const rateLimitInfo = asLooseObject(field(message, 'rate_limit_info')) ?? {};
|
|
169
|
+
const status = stringField(rateLimitInfo, 'status');
|
|
170
|
+
const rateLimitType = stringField(rateLimitInfo, 'rateLimitType');
|
|
171
|
+
const utilization = numberField(rateLimitInfo, 'utilization');
|
|
172
|
+
const mapping = status === undefined ? undefined : RATE_LIMIT_STATUS_MAPPINGS[status];
|
|
173
|
+
const meta = [
|
|
174
|
+
metaItem('utilization', utilization == null ? null : `${formatNumber(utilization * 100)}%`),
|
|
175
|
+
mapping === undefined ? metaItem('status', status ?? null) : null
|
|
176
|
+
].filter(isMeta);
|
|
177
|
+
return lifecycleRow(timestamp, mapping?.label ?? 'Rate limit updated', rateLimitType == null ? null : humanizeEnumValue(rateLimitType), mapping?.tone ?? 'warning', false, meta);
|
|
178
|
+
}
|
|
179
|
+
export function toolUseSummary(message, context) {
|
|
180
|
+
const summary = stringField(message, 'summary');
|
|
181
|
+
if (summary === undefined) return false;
|
|
182
|
+
const precedingToolUseIds = stringList(field(message, 'preceding_tool_use_ids'));
|
|
183
|
+
const toolUseId = stringField(message, 'tool_use_id');
|
|
184
|
+
const candidateIds = precedingToolUseIds.length > 0 ? precedingToolUseIds : toolUseId === undefined ? [] : [
|
|
185
|
+
toolUseId
|
|
186
|
+
];
|
|
187
|
+
for (const id of [
|
|
188
|
+
...candidateIds
|
|
189
|
+
].reverse()){
|
|
190
|
+
const row = context.toolCallRows.get(id);
|
|
191
|
+
if (row === undefined) continue;
|
|
192
|
+
row.summary = row.summary === undefined ? summary : `${row.summary}\n\n${summary}`;
|
|
193
|
+
return true;
|
|
194
|
+
}
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
export function flushPendingToolRows(context) {
|
|
198
|
+
if (context.pendingToolRows.length === 0) return [];
|
|
199
|
+
const rows = context.pendingToolRows;
|
|
200
|
+
context.pendingToolRows = [];
|
|
201
|
+
context.toolCallRows.clear();
|
|
202
|
+
return rows;
|
|
203
|
+
}
|
|
128
204
|
function titleCase(value) {
|
|
129
205
|
return value.length === 0 ? value : `${value[0]?.toUpperCase()}${value.slice(1)}`;
|
|
130
206
|
}
|
|
131
|
-
|
|
207
|
+
function humanizeEnumValue(value) {
|
|
208
|
+
return titleCase(value.replaceAll('_', ' '));
|
|
209
|
+
}
|
|
210
|
+
function stringList(value) {
|
|
211
|
+
if (!Array.isArray(value)) return [];
|
|
212
|
+
return value.filter((item)=>typeof item === 'string' && item.length > 0);
|
|
213
|
+
}
|
|
214
|
+
export function assistantRows(timestamp, message, context) {
|
|
132
215
|
const sdkMessage = asLooseObject(message.message) ?? message;
|
|
133
216
|
const rows = [];
|
|
134
217
|
const textParts = [];
|
|
135
218
|
const thinkingParts = [];
|
|
219
|
+
let queueRows = context.toolCallRows.size > 0;
|
|
220
|
+
const pushRow = (row)=>{
|
|
221
|
+
if (queueRows) {
|
|
222
|
+
context.pendingToolRows.push(row);
|
|
223
|
+
} else {
|
|
224
|
+
rows.push(row);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
136
227
|
const pushText = ()=>{
|
|
137
228
|
if (textParts.length === 0) return;
|
|
138
|
-
|
|
229
|
+
pushRow(messageRow(timestamp, 'assistant', 'assistant', textParts.join('\n\n'), false));
|
|
139
230
|
textParts.length = 0;
|
|
140
231
|
};
|
|
141
232
|
const pushThinking = ()=>{
|
|
142
233
|
if (thinkingParts.length === 0) return;
|
|
143
|
-
|
|
234
|
+
pushRow(thinkingRow(timestamp, thinkingParts.join('\n\n')));
|
|
144
235
|
thinkingParts.length = 0;
|
|
145
236
|
};
|
|
146
237
|
for (const block of contentBlocks(sdkMessage)){
|
|
@@ -148,7 +239,14 @@ export function assistantRows(timestamp, message) {
|
|
|
148
239
|
if (type === 'tool_use' || type === 'tool-use' || type === 'toolCall') {
|
|
149
240
|
pushText();
|
|
150
241
|
pushThinking();
|
|
151
|
-
|
|
242
|
+
const row = toolCallRow(timestamp, block);
|
|
243
|
+
if (row.id === null) {
|
|
244
|
+
pushRow(row);
|
|
245
|
+
} else {
|
|
246
|
+
queueRows = true;
|
|
247
|
+
context.pendingToolRows.push(row);
|
|
248
|
+
context.toolCallRows.set(row.id, row);
|
|
249
|
+
}
|
|
152
250
|
continue;
|
|
153
251
|
}
|
|
154
252
|
if (type === 'thinking' || type === 'reasoning') {
|
|
@@ -163,17 +261,19 @@ export function assistantRows(timestamp, message) {
|
|
|
163
261
|
}
|
|
164
262
|
pushText();
|
|
165
263
|
pushThinking();
|
|
166
|
-
if (rows.length > 0) return rows;
|
|
264
|
+
if (rows.length > 0 || queueRows) return rows;
|
|
167
265
|
const text = stringField(sdkMessage, 'content') ?? stringField(message, 'result');
|
|
168
266
|
return [
|
|
169
267
|
messageRow(timestamp, 'assistant', 'assistant', text ?? toJson(message), false)
|
|
170
268
|
];
|
|
171
269
|
}
|
|
172
|
-
export function userRows(timestamp, message) {
|
|
270
|
+
export function userRows(timestamp, message, context) {
|
|
173
271
|
const sdkMessage = asLooseObject(message.message) ?? message;
|
|
174
272
|
const role = isPlatformMessage(sdkMessage) ? 'platform' : 'user';
|
|
175
273
|
const rows = [];
|
|
176
274
|
const textParts = [];
|
|
275
|
+
const hadPendingToolCall = context.toolCallRows.size > 0;
|
|
276
|
+
let matchedToolResult = false;
|
|
177
277
|
const pushText = ()=>{
|
|
178
278
|
if (textParts.length === 0) return;
|
|
179
279
|
rows.push(messageRow(timestamp, role, role, textParts.join('\n\n'), false));
|
|
@@ -183,18 +283,32 @@ export function userRows(timestamp, message) {
|
|
|
183
283
|
const type = stringField(block, 'type');
|
|
184
284
|
if (type === 'tool_result' || type === 'tool-result') {
|
|
185
285
|
pushText();
|
|
186
|
-
|
|
286
|
+
const row = toolResultRow(timestamp, block);
|
|
287
|
+
if (row.toolCallId !== null && context.toolCallRows.has(row.toolCallId)) {
|
|
288
|
+
matchedToolResult = true;
|
|
289
|
+
}
|
|
290
|
+
rows.push(row);
|
|
187
291
|
continue;
|
|
188
292
|
}
|
|
189
293
|
const text = blockText(block);
|
|
190
294
|
if (text) textParts.push(text);
|
|
191
295
|
}
|
|
192
296
|
pushText();
|
|
193
|
-
if (rows.length
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
297
|
+
if (rows.length === 0) {
|
|
298
|
+
const content = stringField(sdkMessage, 'content');
|
|
299
|
+
rows.push(messageRow(timestamp, role, role, content ?? toJson(message), false));
|
|
300
|
+
}
|
|
301
|
+
if (hadPendingToolCall) {
|
|
302
|
+
if (matchedToolResult) {
|
|
303
|
+
context.pendingToolRows.push(...rows);
|
|
304
|
+
return [];
|
|
305
|
+
}
|
|
306
|
+
return [
|
|
307
|
+
...flushPendingToolRows(context),
|
|
308
|
+
...rows
|
|
309
|
+
];
|
|
310
|
+
}
|
|
311
|
+
return rows;
|
|
198
312
|
}
|
|
199
313
|
export function resultRow(timestamp, message, turn, isFinalResult) {
|
|
200
314
|
const isError = booleanField(message, 'is_error') || booleanField(message, 'isError');
|