@shipfox/api-logs 12.7.0 → 13.1.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.
@@ -1,6 +1,6 @@
1
1
  $ shipfox-swc && shipfox-temporal-bundle dist/temporal/workflows/index.js
2
- Successfully compiled: 59 files with swc (684.55ms)
3
- 2026-08-10T13:04:48.675Z [INFO] asset workflow-bundle-17839eee3b21fbae5b8c.js 3.18 MiB [emitted] [immutable] (name: main)
2
+ Successfully compiled: 59 files with swc (648.6ms)
3
+ 2026-08-23T10:00:37.358Z [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 12506 ms
20
- 2026-08-10T13:04:48.714Z [INFO] Workflow bundle created { size: '3.18MB' }
19
+ webpack 5.107.2 compiled successfully in 8670 ms
20
+ 2026-08-23T10:00:37.392Z [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,19 @@
1
1
  # @shipfox/api-logs
2
2
 
3
+ ## 13.1.0
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [0d3c2e3]
8
+ - @shipfox/api-workflows-dto@13.1.0
9
+
10
+ ## 13.0.0
11
+
12
+ ### Patch Changes
13
+
14
+ - Updated dependencies [af6b31e]
15
+ - @shipfox/api-workflows-dto@13.0.0
16
+
3
17
  ## 12.7.0
4
18
 
5
19
  ### Patch 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;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"}
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;AAmCnF,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,CA6I3B"}
@@ -7,7 +7,7 @@ import { accrueStoredBytes, claimCap, ensureJobAccounting, isJobCapped } from '#
7
7
  import { insertChunk } from '#db/chunks.js';
8
8
  import { db } from '#db/db.js';
9
9
  import { casExtendCommittedLength, getAttemptStream, getOrCreateAttemptStreamWithStatus, setClaudeParseContext, setDeclaredTotalBytes } from '#db/streams.js';
10
- import { recordAppendedCount, streamClosedCount, streamOpenedCount } from '#metrics/instance.js';
10
+ import { bytesIngestedCount, bytesStoredCount, recordAppendedCount, streamClosedCount, streamOpenedCount } from '#metrics/instance.js';
11
11
  import { allowedBudget } from './budget.js';
12
12
  import { closeStream, controlTombstone } from './close-stream.js';
13
13
  import { MalformedLogChunkError, OffsetGapError } from './errors.js';
@@ -307,7 +307,13 @@ function agentSessionRecord(record) {
307
307
  const metrics = {
308
308
  recordCounts: {},
309
309
  streamClosedReason: undefined,
310
- streamOpened: false
310
+ streamOpened: false,
311
+ // Raw runner body bytes accepted by the in-order CAS; normalized durable bytes written
312
+ // to chunk rows. Both are accumulated inside the transaction and recorded only after it
313
+ // commits, so a rolled-back append never counts. See the semantics on the metric
314
+ // definitions in `#metrics/instance.js`.
315
+ ingestedBytes: 0,
316
+ storedBytes: 0
311
317
  };
312
318
  const result = await db().transaction(async (tx)=>{
313
319
  if (commitByteLen === 0) return readHeartbeat(tx, params);
@@ -341,6 +347,9 @@ function agentSessionRecord(record) {
341
347
  capped: await isJobCapped(tx, params.jobId)
342
348
  };
343
349
  }
350
+ // In-order CAS extension: the raw body is accepted. Retries and gaps returned above, and
351
+ // closed streams / empty heartbeats never reach the CAS, so each body is counted once.
352
+ metrics.ingestedBytes += commitByteLen;
344
353
  const parseHarness = sessionHarness === 'claude' || stream.claudePendingResult !== null || stream.claudePendingToolRows.length > 0 ? 'claude' : sessionHarness;
345
354
  const stored = buildStoredBody(parsed.records, parseHarness, parseHarness === 'claude' ? createClaudeParseContext(stream.claudePendingToolRows, {
346
355
  hasInit: stream.claudeHasInit,
@@ -355,6 +364,8 @@ function agentSessionRecord(record) {
355
364
  declaredTotalBytes
356
365
  });
357
366
  if (chunkStored) {
367
+ // Normalized durable bytes; a cap-dropped straggler never reaches this branch.
368
+ metrics.storedBytes += stored.body.length;
358
369
  addRecordCounts(metrics.recordCounts, stored.recordCounts);
359
370
  if (stored.claudeParseContext !== undefined) {
360
371
  await setClaudeParseContext(tx, {
@@ -384,6 +395,8 @@ function agentSessionRecord(record) {
384
395
  return result;
385
396
  });
386
397
  if (metrics.streamOpened) streamOpenedCount.add(1);
398
+ if (metrics.ingestedBytes > 0) bytesIngestedCount.add(metrics.ingestedBytes);
399
+ if (metrics.storedBytes > 0) bytesStoredCount.add(metrics.storedBytes);
387
400
  for (const [kind, count] of Object.entries(metrics.recordCounts)){
388
401
  if (count > 0) recordAppendedCount.add(count, {
389
402
  kind: kind
@@ -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 {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
+ {"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 bytesIngestedCount,\n bytesStoredCount,\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 // Raw runner body bytes accepted by the in-order CAS; normalized durable bytes written\n // to chunk rows. Both are accumulated inside the transaction and recorded only after it\n // commits, so a rolled-back append never counts. See the semantics on the metric\n // definitions in `#metrics/instance.js`.\n ingestedBytes: 0,\n storedBytes: 0,\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 // In-order CAS extension: the raw body is accepted. Retries and gaps returned above, and\n // closed streams / empty heartbeats never reach the CAS, so each body is counted once.\n metrics.ingestedBytes += commitByteLen;\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 // Normalized durable bytes; a cap-dropped straggler never reaches this branch.\n metrics.storedBytes += stored.body.length;\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 if (metrics.ingestedBytes > 0) bytesIngestedCount.add(metrics.ingestedBytes);\n if (metrics.storedBytes > 0) bytesStoredCount.add(metrics.storedBytes);\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","bytesIngestedCount","bytesStoredCount","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","ingestedBytes","storedBytes","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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"}
@@ -48,6 +48,13 @@ export declare function readChunkPageBySeq(params: {
48
48
  afterSeq: number;
49
49
  maxBytes: number;
50
50
  }): Promise<ChunkPage>;
51
+ /**
52
+ * Total bytes held in hot chunk rows across all streams, runner and server-injected
53
+ * control chunks alike. Chunks are only deleted by compaction, so this is exactly the
54
+ * un-compacted hot volume the service gauge reports (open streams plus closed streams
55
+ * still awaiting compaction).
56
+ */
57
+ export declare function getUncompactedChunkBytes(): Promise<bigint>;
51
58
  export interface ChunkStats {
52
59
  count: number;
53
60
  maxSeq: number;
@@ -1 +1 @@
1
- {"version":3,"file":"chunks.d.ts","sourceRoot":"","sources":["../../src/db/chunks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAEnC,OAAO,EAAK,KAAK,WAAW,EAAC,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAC,KAAK,WAAW,EAAY,MAAM,oBAAoB,CAAC;AAE/D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ3F;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAQ1B;AAED,MAAM,WAAW,SAAS;IACxB,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,OAAO,EAAE,OAAO,CAAC;CAClB;AAMD;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,SAAS,CAAC,CAsCrB;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAetE"}
1
+ {"version":3,"file":"chunks.d.ts","sourceRoot":"","sources":["../../src/db/chunks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,MAAM,EAAC,MAAM,aAAa,CAAC;AAEnC,OAAO,EAAK,KAAK,WAAW,EAAC,MAAM,SAAS,CAAC;AAC7C,OAAO,EAAC,KAAK,WAAW,EAAY,MAAM,oBAAoB,CAAC;AAE/D,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,WAAW,CAAC;CACrB;AAED,wBAAsB,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ3F;AAED,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;;GAKG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;CACf,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAQ1B;AAED,MAAM,WAAW,SAAS;IACxB,oFAAoF;IACpF,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;IAChB,6DAA6D;IAC7D,OAAO,EAAE,OAAO,CAAC;CAClB;AAMD;;;;;;;;;;GAUG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE;IAC/C,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,SAAS,CAAC,CAsCrB;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,IAAI,OAAO,CAAC,MAAM,CAAC,CAMhE;AAED,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAetE"}
package/dist/db/chunks.js CHANGED
@@ -68,6 +68,17 @@ const CHUNK_PAGE_SCAN_CAP = 4096;
68
68
  hasMore
69
69
  };
70
70
  }
71
+ /**
72
+ * Total bytes held in hot chunk rows across all streams, runner and server-injected
73
+ * control chunks alike. Chunks are only deleted by compaction, so this is exactly the
74
+ * un-compacted hot volume the service gauge reports (open streams plus closed streams
75
+ * still awaiting compaction).
76
+ */ export async function getUncompactedChunkBytes() {
77
+ const [row] = await db().select({
78
+ value: sql`coalesce(sum(${logChunks.byteLen}), 0)`.mapWith(BigInt)
79
+ }).from(logChunks);
80
+ return row?.value ?? 0n;
81
+ }
71
82
  /**
72
83
  * Aggregate of a stream's chunks: row count, highest `seq`, and total stored bytes.
73
84
  * Compaction reads this once before uploading, both to stamp object metadata and as the
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/chunks.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {and, asc, eq, gt, lte, sql} from 'drizzle-orm';\nimport {db, type Transaction} from './db.js';\nimport {type ChunkOrigin, logChunks} from './schema/chunks.js';\n\nexport interface InsertChunkParams {\n streamId: string;\n streamOffset: number;\n byteLen: number;\n data: Buffer;\n origin: ChunkOrigin;\n}\n\nexport async function insertChunk(tx: Transaction, params: InsertChunkParams): Promise<void> {\n await tx.insert(logChunks).values({\n streamId: params.streamId,\n streamOffset: params.streamOffset,\n byteLen: params.byteLen,\n data: params.data,\n origin: params.origin,\n });\n}\n\nexport interface ChunkPageRow {\n seq: number;\n data: Buffer;\n}\n\n/**\n * One keyset page of a stream's chunk bytes in `seq` order. Compaction walks pages\n * with `afterSeq = lastSeqOfPreviousPage`, so memory stays flat regardless of stream\n * size, and reads run outside any transaction (no tx is pinned for the long upload).\n * Safe against late writers because a closed stream never accepts another chunk.\n */\nexport async function readChunksKeyset(params: {\n streamId: string;\n afterSeq: number;\n limit: number;\n}): Promise<ChunkPageRow[]> {\n const rows = await db()\n .select({seq: logChunks.seq, data: logChunks.data})\n .from(logChunks)\n .where(and(eq(logChunks.streamId, params.streamId), gt(logChunks.seq, params.afterSeq)))\n .orderBy(asc(logChunks.seq))\n .limit(params.limit);\n return rows;\n}\n\nexport interface ChunkPage {\n /** Concatenated chunk bytes for the page, in `seq` order: ready-to-serve NDJSON. */\n data: Buffer;\n /** The last `seq` included; the next read passes this as `afterSeq`. */\n nextSeq: number;\n /** Whether at least one more chunk exists past `nextSeq`. */\n hasMore: boolean;\n}\n\n// Caps the per-read metadata scan so a stream with pathologically many tiny chunks\n// can't make one read walk the whole stream; the client re-polls (hasMore) past it.\nconst CHUNK_PAGE_SCAN_CAP = 4096;\n\n/**\n * A byte-bounded page of a stream's chunk bytes in `seq` order, for the inline read path.\n * Walking by `seq` (not the runner byte offset) is what makes server-injected control tombstones\n * interleave with normalized runner records exactly as compaction concatenates them, so the inline\n * NDJSON is byte-identical to the compacted object.\n *\n * Returns at most ~`maxBytes`, but always at least one whole chunk so the cursor advances\n * even when a single chunk exceeds `maxBytes`. The cheap `(seq, byte_len)` scan picks the\n * prefix first, so the heavy `data` column is only materialized for the chunks actually\n * returned.\n */\nexport async function readChunkPageBySeq(params: {\n streamId: string;\n afterSeq: number;\n maxBytes: number;\n}): Promise<ChunkPage> {\n const meta = await db()\n .select({seq: logChunks.seq, byteLen: logChunks.byteLen})\n .from(logChunks)\n .where(and(eq(logChunks.streamId, params.streamId), gt(logChunks.seq, params.afterSeq)))\n .orderBy(asc(logChunks.seq))\n .limit(CHUNK_PAGE_SCAN_CAP + 1);\n\n if (meta.length === 0) {\n return {data: Buffer.alloc(0), nextSeq: params.afterSeq, hasMore: false};\n }\n\n let accumulatedBytes = 0;\n let includedCount = 0;\n let nextSeq = params.afterSeq;\n for (const row of meta) {\n if (includedCount > 0 && accumulatedBytes + row.byteLen > params.maxBytes) break;\n accumulatedBytes += row.byteLen;\n nextSeq = row.seq;\n includedCount += 1;\n if (includedCount >= CHUNK_PAGE_SCAN_CAP) break;\n }\n\n const hasMore = includedCount < meta.length;\n\n const rows = await db()\n .select({data: logChunks.data})\n .from(logChunks)\n .where(\n and(\n eq(logChunks.streamId, params.streamId),\n gt(logChunks.seq, params.afterSeq),\n lte(logChunks.seq, nextSeq),\n ),\n )\n .orderBy(asc(logChunks.seq));\n\n return {data: Buffer.concat(rows.map((row) => row.data)), nextSeq, hasMore};\n}\n\nexport interface ChunkStats {\n count: number;\n maxSeq: number;\n uncompressedBytes: number;\n}\n\n/**\n * Aggregate of a stream's chunks: row count, highest `seq`, and total stored bytes.\n * Compaction reads this once before uploading, both to stamp object metadata and as the\n * baseline its streamed totals are checked against before the chunk rows are deleted.\n */\nexport async function chunkStats(streamId: string): Promise<ChunkStats> {\n const [row] = await db()\n .select({\n count: sql<number>`count(*)::int`,\n maxSeq: sql<number>`coalesce(max(${logChunks.seq}), 0)::int`,\n uncompressedBytes: sql<string>`coalesce(sum(${logChunks.byteLen}), 0)::text`,\n })\n .from(logChunks)\n .where(eq(logChunks.streamId, streamId));\n\n return {\n count: row?.count ?? 0,\n maxSeq: row?.maxSeq ?? 0,\n uncompressedBytes: Number(row?.uncompressedBytes ?? '0'),\n };\n}\n"],"names":["Buffer","and","asc","eq","gt","lte","sql","db","logChunks","insertChunk","tx","params","insert","values","streamId","streamOffset","byteLen","data","origin","readChunksKeyset","rows","select","seq","from","where","afterSeq","orderBy","limit","CHUNK_PAGE_SCAN_CAP","readChunkPageBySeq","meta","length","alloc","nextSeq","hasMore","accumulatedBytes","includedCount","row","maxBytes","concat","map","chunkStats","count","maxSeq","uncompressedBytes","Number"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/db/chunks.ts"],"sourcesContent":["import {Buffer} from 'node:buffer';\nimport {and, asc, eq, gt, lte, sql} from 'drizzle-orm';\nimport {db, type Transaction} from './db.js';\nimport {type ChunkOrigin, logChunks} from './schema/chunks.js';\n\nexport interface InsertChunkParams {\n streamId: string;\n streamOffset: number;\n byteLen: number;\n data: Buffer;\n origin: ChunkOrigin;\n}\n\nexport async function insertChunk(tx: Transaction, params: InsertChunkParams): Promise<void> {\n await tx.insert(logChunks).values({\n streamId: params.streamId,\n streamOffset: params.streamOffset,\n byteLen: params.byteLen,\n data: params.data,\n origin: params.origin,\n });\n}\n\nexport interface ChunkPageRow {\n seq: number;\n data: Buffer;\n}\n\n/**\n * One keyset page of a stream's chunk bytes in `seq` order. Compaction walks pages\n * with `afterSeq = lastSeqOfPreviousPage`, so memory stays flat regardless of stream\n * size, and reads run outside any transaction (no tx is pinned for the long upload).\n * Safe against late writers because a closed stream never accepts another chunk.\n */\nexport async function readChunksKeyset(params: {\n streamId: string;\n afterSeq: number;\n limit: number;\n}): Promise<ChunkPageRow[]> {\n const rows = await db()\n .select({seq: logChunks.seq, data: logChunks.data})\n .from(logChunks)\n .where(and(eq(logChunks.streamId, params.streamId), gt(logChunks.seq, params.afterSeq)))\n .orderBy(asc(logChunks.seq))\n .limit(params.limit);\n return rows;\n}\n\nexport interface ChunkPage {\n /** Concatenated chunk bytes for the page, in `seq` order: ready-to-serve NDJSON. */\n data: Buffer;\n /** The last `seq` included; the next read passes this as `afterSeq`. */\n nextSeq: number;\n /** Whether at least one more chunk exists past `nextSeq`. */\n hasMore: boolean;\n}\n\n// Caps the per-read metadata scan so a stream with pathologically many tiny chunks\n// can't make one read walk the whole stream; the client re-polls (hasMore) past it.\nconst CHUNK_PAGE_SCAN_CAP = 4096;\n\n/**\n * A byte-bounded page of a stream's chunk bytes in `seq` order, for the inline read path.\n * Walking by `seq` (not the runner byte offset) is what makes server-injected control tombstones\n * interleave with normalized runner records exactly as compaction concatenates them, so the inline\n * NDJSON is byte-identical to the compacted object.\n *\n * Returns at most ~`maxBytes`, but always at least one whole chunk so the cursor advances\n * even when a single chunk exceeds `maxBytes`. The cheap `(seq, byte_len)` scan picks the\n * prefix first, so the heavy `data` column is only materialized for the chunks actually\n * returned.\n */\nexport async function readChunkPageBySeq(params: {\n streamId: string;\n afterSeq: number;\n maxBytes: number;\n}): Promise<ChunkPage> {\n const meta = await db()\n .select({seq: logChunks.seq, byteLen: logChunks.byteLen})\n .from(logChunks)\n .where(and(eq(logChunks.streamId, params.streamId), gt(logChunks.seq, params.afterSeq)))\n .orderBy(asc(logChunks.seq))\n .limit(CHUNK_PAGE_SCAN_CAP + 1);\n\n if (meta.length === 0) {\n return {data: Buffer.alloc(0), nextSeq: params.afterSeq, hasMore: false};\n }\n\n let accumulatedBytes = 0;\n let includedCount = 0;\n let nextSeq = params.afterSeq;\n for (const row of meta) {\n if (includedCount > 0 && accumulatedBytes + row.byteLen > params.maxBytes) break;\n accumulatedBytes += row.byteLen;\n nextSeq = row.seq;\n includedCount += 1;\n if (includedCount >= CHUNK_PAGE_SCAN_CAP) break;\n }\n\n const hasMore = includedCount < meta.length;\n\n const rows = await db()\n .select({data: logChunks.data})\n .from(logChunks)\n .where(\n and(\n eq(logChunks.streamId, params.streamId),\n gt(logChunks.seq, params.afterSeq),\n lte(logChunks.seq, nextSeq),\n ),\n )\n .orderBy(asc(logChunks.seq));\n\n return {data: Buffer.concat(rows.map((row) => row.data)), nextSeq, hasMore};\n}\n\n/**\n * Total bytes held in hot chunk rows across all streams, runner and server-injected\n * control chunks alike. Chunks are only deleted by compaction, so this is exactly the\n * un-compacted hot volume the service gauge reports (open streams plus closed streams\n * still awaiting compaction).\n */\nexport async function getUncompactedChunkBytes(): Promise<bigint> {\n const [row] = await db()\n .select({value: sql<bigint>`coalesce(sum(${logChunks.byteLen}), 0)`.mapWith(BigInt)})\n .from(logChunks);\n\n return row?.value ?? 0n;\n}\n\nexport interface ChunkStats {\n count: number;\n maxSeq: number;\n uncompressedBytes: number;\n}\n\n/**\n * Aggregate of a stream's chunks: row count, highest `seq`, and total stored bytes.\n * Compaction reads this once before uploading, both to stamp object metadata and as the\n * baseline its streamed totals are checked against before the chunk rows are deleted.\n */\nexport async function chunkStats(streamId: string): Promise<ChunkStats> {\n const [row] = await db()\n .select({\n count: sql<number>`count(*)::int`,\n maxSeq: sql<number>`coalesce(max(${logChunks.seq}), 0)::int`,\n uncompressedBytes: sql<string>`coalesce(sum(${logChunks.byteLen}), 0)::text`,\n })\n .from(logChunks)\n .where(eq(logChunks.streamId, streamId));\n\n return {\n count: row?.count ?? 0,\n maxSeq: row?.maxSeq ?? 0,\n uncompressedBytes: Number(row?.uncompressedBytes ?? '0'),\n };\n}\n"],"names":["Buffer","and","asc","eq","gt","lte","sql","db","logChunks","insertChunk","tx","params","insert","values","streamId","streamOffset","byteLen","data","origin","readChunksKeyset","rows","select","seq","from","where","afterSeq","orderBy","limit","CHUNK_PAGE_SCAN_CAP","readChunkPageBySeq","meta","length","alloc","nextSeq","hasMore","accumulatedBytes","includedCount","row","maxBytes","concat","map","getUncompactedChunkBytes","value","mapWith","BigInt","chunkStats","count","maxSeq","uncompressedBytes","Number"],"mappings":"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"}
@@ -4,6 +4,8 @@ export declare const recordAppendedCount: import("@shipfox/node-opentelemetry").
4
4
  kind: LogRecordMetricKind;
5
5
  }>;
6
6
  export declare const streamOpenedCount: import("@shipfox/node-opentelemetry").Counter<Record<string, never>>;
7
+ export declare const bytesIngestedCount: import("@shipfox/node-opentelemetry").Counter<Record<string, never>>;
8
+ export declare const bytesStoredCount: import("@shipfox/node-opentelemetry").Counter<Record<string, never>>;
7
9
  export declare const streamClosedCount: import("@shipfox/node-opentelemetry").Counter<{
8
10
  reason: 'declared' | 'timeout';
9
11
  }>;
@@ -11,4 +13,5 @@ export type CompactionMetricOutcome = 'already-compacted' | 'compacted' | 'faile
11
13
  export declare const compactionCount: import("@shipfox/node-opentelemetry").Counter<{
12
14
  outcome: CompactionMetricOutcome;
13
15
  }>;
16
+ export declare const compactedBytesCount: import("@shipfox/node-opentelemetry").Counter<Record<string, never>>;
14
17
  //# sourceMappingURL=instance.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/metrics/instance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAKrD,MAAM,MAAM,mBAAmB,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;AAEpD,eAAO,MAAM,mBAAmB;UAA8B,mBAAmB;EAGhF,CAAC;AAEF,eAAO,MAAM,iBAAiB,sEAE5B,CAAC;AAEH,eAAO,MAAM,iBAAiB;YAAgC,UAAU,GAAG,SAAS;EAGnF,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAC/B,mBAAmB,GACnB,WAAW,GACX,QAAQ,GACR,MAAM,GACN,iBAAiB,GACjB,YAAY,CAAC;AAEjB,eAAO,MAAM,eAAe;aAAiC,uBAAuB;EAGnF,CAAC"}
1
+ {"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/metrics/instance.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,uBAAuB,CAAC;AAKrD,MAAM,MAAM,mBAAmB,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;AAEpD,eAAO,MAAM,mBAAmB;UAA8B,mBAAmB;EAGhF,CAAC;AAEF,eAAO,MAAM,iBAAiB,sEAE5B,CAAC;AAmBH,eAAO,MAAM,kBAAkB,sEAO9B,CAAC;AAEF,eAAO,MAAM,gBAAgB,sEAI3B,CAAC;AAEH,eAAO,MAAM,iBAAiB;YAAgC,UAAU,GAAG,SAAS;EAGnF,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAC/B,mBAAmB,GACnB,WAAW,GACX,QAAQ,GACR,MAAM,GACN,iBAAiB,GACjB,YAAY,CAAC;AAEjB,eAAO,MAAM,eAAe;aAAiC,uBAAuB;EAGnF,CAAC;AAMF,eAAO,MAAM,mBAAmB,sEAM/B,CAAC"}
@@ -6,11 +6,44 @@ export const recordAppendedCount = meter.createCounter('logs_record_appended', {
6
6
  export const streamOpenedCount = meter.createCounter('logs_stream_opened', {
7
7
  description: 'Log streams opened by first append'
8
8
  });
9
+ // Byte-volume counters use the OpenTelemetry byte unit so Prometheus appends `_bytes` to the
10
+ // metric name. The two ingest axes MUST stay distinct:
11
+ //
12
+ // raw ingested (CAS axis) normalized stored (read axis)
13
+ // runner body bytes accepted durable chunk bytes written after ingest
14
+ // by the offset-CAS, before normalization (agent_session records are parsed
15
+ // normalization into view rows), excluding server tombstones
16
+ //
17
+ // `logs_bytes_ingested` counts what the protocol accepted: every in-order CAS extension,
18
+ // including cap-crossing appends and post-cap accept-and-drop stragglers (their
19
+ // `committed_length` advances, so they are accepted even though nothing is stored).
20
+ // Retries, gaps, closed-stream appends, and empty heartbeats never extend the CAS and are
21
+ // never counted, so the same bytes are never double-counted.
22
+ //
23
+ // `logs_bytes_stored` counts only normalized bodies durably written as chunk rows, so a
24
+ // capped job's dropped straggler and server-injected `capped`/`runner_lost` tombstones do
25
+ // not inflate it. ingested - stored is the normalization delta plus cap/close drops.
26
+ export const bytesIngestedCount = meter.createCounter('logs_bytes_ingested', {
27
+ description: 'Raw runner bytes accepted after offset validation (in-order CAS extension; retries, gaps, closed-stream and cap-dropped bodies excluded)',
28
+ unit: 'By'
29
+ });
30
+ export const bytesStoredCount = meter.createCounter('logs_bytes_stored', {
31
+ description: 'Normalized durable bytes written to log chunks from runner appends (server tombstones and cap-dropped bodies excluded)',
32
+ unit: 'By'
33
+ });
9
34
  export const streamClosedCount = meter.createCounter('logs_stream_closed', {
10
35
  description: 'Log streams closed by reason'
11
36
  });
12
37
  export const compactionCount = meter.createCounter('logs_compaction', {
13
38
  description: 'Log stream compaction attempts by outcome'
14
39
  });
40
+ // Uncompressed NDJSON log bytes (not the gzip object size), matching the byte axis of the
41
+ // ingest/storage counters and the `uncompressed_bytes` object metadata. Recorded only once
42
+ // per stream on the single-winner publish, so an idempotent re-run (`already-compacted`) or
43
+ // a failed attempt never double-counts.
44
+ export const compactedBytesCount = meter.createCounter('logs_compacted_bytes', {
45
+ description: 'Uncompressed log bytes successfully compacted to object storage',
46
+ unit: 'By'
47
+ });
15
48
 
16
49
  //# sourceMappingURL=instance.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/metrics/instance.ts"],"sourcesContent":["import type {LogRecord} from '@shipfox/api-logs-dto';\nimport {instanceMetrics} from '@shipfox/node-opentelemetry';\n\nconst meter = instanceMetrics.getMeter('logs');\n\nexport type LogRecordMetricKind = LogRecord['type'];\n\nexport const recordAppendedCount = meter.createCounter<{kind: LogRecordMetricKind}>(\n 'logs_record_appended',\n {description: 'Log records appended to durable stream storage by record type'},\n);\n\nexport const streamOpenedCount = meter.createCounter<Record<string, never>>('logs_stream_opened', {\n description: 'Log streams opened by first append',\n});\n\nexport const streamClosedCount = meter.createCounter<{reason: 'declared' | 'timeout'}>(\n 'logs_stream_closed',\n {description: 'Log streams closed by reason'},\n);\n\nexport type CompactionMetricOutcome =\n | 'already-compacted'\n | 'compacted'\n | 'failed'\n | 'gone'\n | 'retention-raced'\n | 'superseded';\n\nexport const compactionCount = meter.createCounter<{outcome: CompactionMetricOutcome}>(\n 'logs_compaction',\n {description: 'Log stream compaction attempts by outcome'},\n);\n"],"names":["instanceMetrics","meter","getMeter","recordAppendedCount","createCounter","description","streamOpenedCount","streamClosedCount","compactionCount"],"mappings":"AACA,SAAQA,eAAe,QAAO,8BAA8B;AAE5D,MAAMC,QAAQD,gBAAgBE,QAAQ,CAAC;AAIvC,OAAO,MAAMC,sBAAsBF,MAAMG,aAAa,CACpD,wBACA;IAACC,aAAa;AAA+D,GAC7E;AAEF,OAAO,MAAMC,oBAAoBL,MAAMG,aAAa,CAAwB,sBAAsB;IAChGC,aAAa;AACf,GAAG;AAEH,OAAO,MAAME,oBAAoBN,MAAMG,aAAa,CAClD,sBACA;IAACC,aAAa;AAA8B,GAC5C;AAUF,OAAO,MAAMG,kBAAkBP,MAAMG,aAAa,CAChD,mBACA;IAACC,aAAa;AAA2C,GACzD"}
1
+ {"version":3,"sources":["../../src/metrics/instance.ts"],"sourcesContent":["import type {LogRecord} from '@shipfox/api-logs-dto';\nimport {instanceMetrics} from '@shipfox/node-opentelemetry';\n\nconst meter = instanceMetrics.getMeter('logs');\n\nexport type LogRecordMetricKind = LogRecord['type'];\n\nexport const recordAppendedCount = meter.createCounter<{kind: LogRecordMetricKind}>(\n 'logs_record_appended',\n {description: 'Log records appended to durable stream storage by record type'},\n);\n\nexport const streamOpenedCount = meter.createCounter<Record<string, never>>('logs_stream_opened', {\n description: 'Log streams opened by first append',\n});\n\n// Byte-volume counters use the OpenTelemetry byte unit so Prometheus appends `_bytes` to the\n// metric name. The two ingest axes MUST stay distinct:\n//\n// raw ingested (CAS axis) normalized stored (read axis)\n// runner body bytes accepted durable chunk bytes written after ingest\n// by the offset-CAS, before normalization (agent_session records are parsed\n// normalization into view rows), excluding server tombstones\n//\n// `logs_bytes_ingested` counts what the protocol accepted: every in-order CAS extension,\n// including cap-crossing appends and post-cap accept-and-drop stragglers (their\n// `committed_length` advances, so they are accepted even though nothing is stored).\n// Retries, gaps, closed-stream appends, and empty heartbeats never extend the CAS and are\n// never counted, so the same bytes are never double-counted.\n//\n// `logs_bytes_stored` counts only normalized bodies durably written as chunk rows, so a\n// capped job's dropped straggler and server-injected `capped`/`runner_lost` tombstones do\n// not inflate it. ingested - stored is the normalization delta plus cap/close drops.\nexport const bytesIngestedCount = meter.createCounter<Record<string, never>>(\n 'logs_bytes_ingested',\n {\n description:\n 'Raw runner bytes accepted after offset validation (in-order CAS extension; retries, gaps, closed-stream and cap-dropped bodies excluded)',\n unit: 'By',\n },\n);\n\nexport const bytesStoredCount = meter.createCounter<Record<string, never>>('logs_bytes_stored', {\n description:\n 'Normalized durable bytes written to log chunks from runner appends (server tombstones and cap-dropped bodies excluded)',\n unit: 'By',\n});\n\nexport const streamClosedCount = meter.createCounter<{reason: 'declared' | 'timeout'}>(\n 'logs_stream_closed',\n {description: 'Log streams closed by reason'},\n);\n\nexport type CompactionMetricOutcome =\n | 'already-compacted'\n | 'compacted'\n | 'failed'\n | 'gone'\n | 'retention-raced'\n | 'superseded';\n\nexport const compactionCount = meter.createCounter<{outcome: CompactionMetricOutcome}>(\n 'logs_compaction',\n {description: 'Log stream compaction attempts by outcome'},\n);\n\n// Uncompressed NDJSON log bytes (not the gzip object size), matching the byte axis of the\n// ingest/storage counters and the `uncompressed_bytes` object metadata. Recorded only once\n// per stream on the single-winner publish, so an idempotent re-run (`already-compacted`) or\n// a failed attempt never double-counts.\nexport const compactedBytesCount = meter.createCounter<Record<string, never>>(\n 'logs_compacted_bytes',\n {\n description: 'Uncompressed log bytes successfully compacted to object storage',\n unit: 'By',\n },\n);\n"],"names":["instanceMetrics","meter","getMeter","recordAppendedCount","createCounter","description","streamOpenedCount","bytesIngestedCount","unit","bytesStoredCount","streamClosedCount","compactionCount","compactedBytesCount"],"mappings":"AACA,SAAQA,eAAe,QAAO,8BAA8B;AAE5D,MAAMC,QAAQD,gBAAgBE,QAAQ,CAAC;AAIvC,OAAO,MAAMC,sBAAsBF,MAAMG,aAAa,CACpD,wBACA;IAACC,aAAa;AAA+D,GAC7E;AAEF,OAAO,MAAMC,oBAAoBL,MAAMG,aAAa,CAAwB,sBAAsB;IAChGC,aAAa;AACf,GAAG;AAEH,6FAA6F;AAC7F,uDAAuD;AACvD,EAAE;AACF,+DAA+D;AAC/D,0EAA0E;AAC1E,iFAAiF;AACjF,8EAA8E;AAC9E,EAAE;AACF,yFAAyF;AACzF,gFAAgF;AAChF,oFAAoF;AACpF,0FAA0F;AAC1F,6DAA6D;AAC7D,EAAE;AACF,wFAAwF;AACxF,0FAA0F;AAC1F,qFAAqF;AACrF,OAAO,MAAME,qBAAqBN,MAAMG,aAAa,CACnD,uBACA;IACEC,aACE;IACFG,MAAM;AACR,GACA;AAEF,OAAO,MAAMC,mBAAmBR,MAAMG,aAAa,CAAwB,qBAAqB;IAC9FC,aACE;IACFG,MAAM;AACR,GAAG;AAEH,OAAO,MAAME,oBAAoBT,MAAMG,aAAa,CAClD,sBACA;IAACC,aAAa;AAA8B,GAC5C;AAUF,OAAO,MAAMM,kBAAkBV,MAAMG,aAAa,CAChD,mBACA;IAACC,aAAa;AAA2C,GACzD;AAEF,0FAA0F;AAC1F,2FAA2F;AAC3F,4FAA4F;AAC5F,wCAAwC;AACxC,OAAO,MAAMO,sBAAsBX,MAAMG,aAAa,CACpD,wBACA;IACEC,aAAa;IACbG,MAAM;AACR,GACA"}
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/metrics/service.ts"],"names":[],"mappings":"AAGA,wBAAgB,0BAA0B,IAAI,IAAI,CAajD"}
1
+ {"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/metrics/service.ts"],"names":[],"mappings":"AAIA,wBAAgB,0BAA0B,IAAI,IAAI,CAsBjD"}
@@ -1,14 +1,23 @@
1
1
  import { getServiceMetricsProvider } from '@shipfox/node-opentelemetry';
2
+ import { getUncompactedChunkBytes } from '#db/chunks.js';
2
3
  import { getOpenStreamCount } from '#db/streams.js';
3
4
  export function registerLogsServiceMetrics() {
4
5
  const meter = getServiceMetricsProvider().getMeter('logs');
5
6
  const openStreams = meter.createObservableGauge('logs_open_streams', {
6
7
  description: 'Log streams currently open for appends'
7
8
  });
9
+ // Hot-storage volume on the service plane: the chunk rows live in shared Postgres, so every
10
+ // pod would report the same sum and Prometheus must not add them together.
11
+ const openChunkBytes = meter.createObservableGauge('logs_open_chunk_bytes', {
12
+ description: 'Bytes in un-compacted hot log chunks (open streams plus closed streams awaiting compaction)',
13
+ unit: 'By'
14
+ });
8
15
  meter.addBatchObservableCallback(async (observer)=>{
9
16
  observer.observe(openStreams, toSafeGaugeNumber(await getOpenStreamCount()));
17
+ observer.observe(openChunkBytes, toSafeGaugeNumber(await getUncompactedChunkBytes()));
10
18
  }, [
11
- openStreams
19
+ openStreams,
20
+ openChunkBytes
12
21
  ]);
13
22
  }
14
23
  function toSafeGaugeNumber(value) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/metrics/service.ts"],"sourcesContent":["import {getServiceMetricsProvider} from '@shipfox/node-opentelemetry';\nimport {getOpenStreamCount} from '#db/streams.js';\n\nexport function registerLogsServiceMetrics(): void {\n const meter = getServiceMetricsProvider().getMeter('logs');\n\n const openStreams = meter.createObservableGauge('logs_open_streams', {\n description: 'Log streams currently open for appends',\n });\n\n meter.addBatchObservableCallback(\n async (observer) => {\n observer.observe(openStreams, toSafeGaugeNumber(await getOpenStreamCount()));\n },\n [openStreams],\n );\n}\n\nfunction toSafeGaugeNumber(value: bigint): number {\n // OpenTelemetry gauges accept numbers; clamp unrepresentable DB counts rather than round them.\n if (value <= BigInt(Number.MAX_SAFE_INTEGER)) return Number(value);\n return Number.MAX_SAFE_INTEGER;\n}\n"],"names":["getServiceMetricsProvider","getOpenStreamCount","registerLogsServiceMetrics","meter","getMeter","openStreams","createObservableGauge","description","addBatchObservableCallback","observer","observe","toSafeGaugeNumber","value","BigInt","Number","MAX_SAFE_INTEGER"],"mappings":"AAAA,SAAQA,yBAAyB,QAAO,8BAA8B;AACtE,SAAQC,kBAAkB,QAAO,iBAAiB;AAElD,OAAO,SAASC;IACd,MAAMC,QAAQH,4BAA4BI,QAAQ,CAAC;IAEnD,MAAMC,cAAcF,MAAMG,qBAAqB,CAAC,qBAAqB;QACnEC,aAAa;IACf;IAEAJ,MAAMK,0BAA0B,CAC9B,OAAOC;QACLA,SAASC,OAAO,CAACL,aAAaM,kBAAkB,MAAMV;IACxD,GACA;QAACI;KAAY;AAEjB;AAEA,SAASM,kBAAkBC,KAAa;IACtC,+FAA+F;IAC/F,IAAIA,SAASC,OAAOC,OAAOC,gBAAgB,GAAG,OAAOD,OAAOF;IAC5D,OAAOE,OAAOC,gBAAgB;AAChC"}
1
+ {"version":3,"sources":["../../src/metrics/service.ts"],"sourcesContent":["import {getServiceMetricsProvider} from '@shipfox/node-opentelemetry';\nimport {getUncompactedChunkBytes} from '#db/chunks.js';\nimport {getOpenStreamCount} from '#db/streams.js';\n\nexport function registerLogsServiceMetrics(): void {\n const meter = getServiceMetricsProvider().getMeter('logs');\n\n const openStreams = meter.createObservableGauge('logs_open_streams', {\n description: 'Log streams currently open for appends',\n });\n\n // Hot-storage volume on the service plane: the chunk rows live in shared Postgres, so every\n // pod would report the same sum and Prometheus must not add them together.\n const openChunkBytes = meter.createObservableGauge('logs_open_chunk_bytes', {\n description:\n 'Bytes in un-compacted hot log chunks (open streams plus closed streams awaiting compaction)',\n unit: 'By',\n });\n\n meter.addBatchObservableCallback(\n async (observer) => {\n observer.observe(openStreams, toSafeGaugeNumber(await getOpenStreamCount()));\n observer.observe(openChunkBytes, toSafeGaugeNumber(await getUncompactedChunkBytes()));\n },\n [openStreams, openChunkBytes],\n );\n}\n\nfunction toSafeGaugeNumber(value: bigint): number {\n // OpenTelemetry gauges accept numbers; clamp unrepresentable DB counts rather than round them.\n if (value <= BigInt(Number.MAX_SAFE_INTEGER)) return Number(value);\n return Number.MAX_SAFE_INTEGER;\n}\n"],"names":["getServiceMetricsProvider","getUncompactedChunkBytes","getOpenStreamCount","registerLogsServiceMetrics","meter","getMeter","openStreams","createObservableGauge","description","openChunkBytes","unit","addBatchObservableCallback","observer","observe","toSafeGaugeNumber","value","BigInt","Number","MAX_SAFE_INTEGER"],"mappings":"AAAA,SAAQA,yBAAyB,QAAO,8BAA8B;AACtE,SAAQC,wBAAwB,QAAO,gBAAgB;AACvD,SAAQC,kBAAkB,QAAO,iBAAiB;AAElD,OAAO,SAASC;IACd,MAAMC,QAAQJ,4BAA4BK,QAAQ,CAAC;IAEnD,MAAMC,cAAcF,MAAMG,qBAAqB,CAAC,qBAAqB;QACnEC,aAAa;IACf;IAEA,4FAA4F;IAC5F,2EAA2E;IAC3E,MAAMC,iBAAiBL,MAAMG,qBAAqB,CAAC,yBAAyB;QAC1EC,aACE;QACFE,MAAM;IACR;IAEAN,MAAMO,0BAA0B,CAC9B,OAAOC;QACLA,SAASC,OAAO,CAACP,aAAaQ,kBAAkB,MAAMZ;QACtDU,SAASC,OAAO,CAACJ,gBAAgBK,kBAAkB,MAAMb;IAC3D,GACA;QAACK;QAAaG;KAAe;AAEjC;AAEA,SAASK,kBAAkBC,KAAa;IACtC,+FAA+F;IAC/F,IAAIA,SAASC,OAAOC,OAAOC,gBAAgB,GAAG,OAAOD,OAAOF;IAC5D,OAAOE,OAAOC,gBAAgB;AAChC"}
@@ -1 +1 @@
1
- {"version":3,"file":"compact-stream.d.ts","sourceRoot":"","sources":["../../../src/temporal/activities/compact-stream.ts"],"names":[],"mappings":"AAIA,OAAO,EAAC,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAExD,OAAO,EAAK,KAAK,WAAW,EAAC,MAAM,WAAW,CAAC;AAI/C,MAAM,MAAM,mBAAmB,GAC3B;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACjB;IAAC,OAAO,EAAE,mBAAmB,CAAA;CAAC,GAC9B;IAAC,OAAO,EAAE,YAAY,CAAA;CAAC,GACvB;IAAC,OAAO,EAAE,iBAAiB,CAAA;CAAC,GAC5B;IAAC,OAAO,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAC,CAAC;AAE7F,UAAU,yBAAyB;IACjC,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,2BAA2B,EAAE,CAC3B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,KAC1C,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAC,CAAC,CAAC;CAClC;AAgGD,wBAAgB,2BAA2B,CACzC,YAAY,GAAE,yBAA+C,GAC5D,CAAC,MAAM,EAAE;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAW9D;AAED,eAAO,MAAM,qBAAqB,WAbtB;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,KAAK,OAAO,CAAC,mBAAmB,CAaI,CAAC"}
1
+ {"version":3,"file":"compact-stream.d.ts","sourceRoot":"","sources":["../../../src/temporal/activities/compact-stream.ts"],"names":[],"mappings":"AAIA,OAAO,EAAC,mBAAmB,EAAC,MAAM,qBAAqB,CAAC;AAExD,OAAO,EAAK,KAAK,WAAW,EAAC,MAAM,WAAW,CAAC;AAQ/C,MAAM,MAAM,mBAAmB,GAC3B;IAAC,OAAO,EAAE,MAAM,CAAA;CAAC,GACjB;IAAC,OAAO,EAAE,mBAAmB,CAAA;CAAC,GAC9B;IAAC,OAAO,EAAE,YAAY,CAAA;CAAC,GACvB;IAAC,OAAO,EAAE,iBAAiB,CAAA;CAAC,GAC5B;IAAC,OAAO,EAAE,WAAW,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,iBAAiB,EAAE,MAAM,CAAA;CAAC,CAAC;AAE7F,UAAU,yBAAyB;IACjC,mBAAmB,EAAE,OAAO,mBAAmB,CAAC;IAChD,2BAA2B,EAAE,CAC3B,EAAE,EAAE,WAAW,EACf,MAAM,EAAE;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAA;KAAC,KAC1C,OAAO,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAC,CAAC,CAAC;CAClC;AAgGD,wBAAgB,2BAA2B,CACzC,YAAY,GAAE,yBAA+C,GAC5D,CAAC,MAAM,EAAE;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,KAAK,OAAO,CAAC,mBAAmB,CAAC,CAiB9D;AAED,eAAO,MAAM,qBAAqB,WAnBtB;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAC,KAAK,OAAO,CAAC,mBAAmB,CAmBI,CAAC"}
@@ -6,7 +6,7 @@ import { compactedGzipStream } from '#core/compaction.js';
6
6
  import { chunkStats } from '#db/chunks.js';
7
7
  import { db } from '#db/db.js';
8
8
  import { getAttemptStreamById, setObjectKeyAndDeleteChunks } from '#db/streams.js';
9
- import { compactionCount } from '#metrics/instance.js';
9
+ import { compactedBytesCount, compactionCount } from '#metrics/instance.js';
10
10
  const defaultDependencies = {
11
11
  compactedGzipStream,
12
12
  setObjectKeyAndDeleteChunks
@@ -98,6 +98,12 @@ export function createCompactStreamActivity(dependencies = defaultDependencies)
98
98
  try {
99
99
  const result = await compactStream(params, dependencies);
100
100
  outcome = result.outcome;
101
+ // Count uncompressed log bytes only on the single-winner publish: idempotent re-runs
102
+ // (`already-compacted`) and failed attempts never reach this branch, so the counter
103
+ // tracks exactly the bytes durably moved to object storage.
104
+ if (result.outcome === 'compacted') {
105
+ compactedBytesCount.add(result.uncompressedBytes);
106
+ }
101
107
  return result;
102
108
  } finally{
103
109
  compactionCount.add(1, {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../src/temporal/activities/compact-stream.ts"],"sourcesContent":["import {reportError} from '@shipfox/node-error-monitoring';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {Context} from '@temporalio/activity';\nimport {compactedObjectKey, deleteObject, putCompactedObject} from '#api/object-storage.js';\nimport {compactedGzipStream} from '#core/compaction.js';\nimport {chunkStats} from '#db/chunks.js';\nimport {db, type Transaction} from '#db/db.js';\nimport {getAttemptStreamById, setObjectKeyAndDeleteChunks} from '#db/streams.js';\nimport {type CompactionMetricOutcome, compactionCount} from '#metrics/instance.js';\n\nexport type CompactStreamResult =\n | {outcome: 'gone'}\n | {outcome: 'already-compacted'}\n | {outcome: 'superseded'}\n | {outcome: 'retention-raced'}\n | {outcome: 'compacted'; objectKey: string; chunkCount: number; uncompressedBytes: number};\n\ninterface CompactStreamDependencies {\n compactedGzipStream: typeof compactedGzipStream;\n setObjectKeyAndDeleteChunks: (\n tx: Transaction,\n params: {streamId: string; objectKey: string},\n ) => Promise<{updated: boolean}>;\n}\n\nconst defaultDependencies: CompactStreamDependencies = {\n compactedGzipStream,\n setObjectKeyAndDeleteChunks,\n};\n\n/**\n * Compacts one closed stream into a single gzip NDJSON object, then deletes its chunk rows.\n *\n * load stream ──┬─ gone ─────────────► no-op (retention raced)\n * ├─ object_key set ───► no-op (idempotent re-run)\n * └─ else: upload to a per-attempt key ─► gzip ─► multipart (abort-aware, heartbeat)\n * │\n * verify streamed count/maxSeq/bytes == table (mismatch ─► delete upload, throw, retry)\n * │\n * tx: set object_key (state='closed' AND object_key IS NULL) + delete chunks\n * └─ 0 rows ─► delete this attempt's upload, then re-read the row:\n * gone ─► retention raced · keyed ─► superseded by another attempt\n *\n * Each attempt uploads to its own `compactedObjectKey(stream, uuid)`, so a slow or zombie\n * attempt can never overwrite a published object. The single-winner publish (object_key set\n * + chunk delete, atomic) drops chunks only once a complete object is durable; the integrity\n * check (count, maxSeq, and byte total) guards a read bug from publishing a truncated object\n * before the only copy of the source is gone (S3 part checksums cover byte transfer).\n */\nasync function compactStream(\n params: {streamId: string},\n dependencies: CompactStreamDependencies,\n): Promise<CompactStreamResult> {\n const stream = await getAttemptStreamById(params.streamId);\n if (!stream) return {outcome: 'gone'};\n if (stream.objectKey) return {outcome: 'already-compacted'};\n\n const ctx = Context.current();\n const uploadKey = compactedObjectKey(stream, crypto.randomUUID());\n const expected = await chunkStats(stream.id);\n const {body, stats} = dependencies.compactedGzipStream({\n streamId: stream.id,\n onPage: () => ctx.heartbeat(),\n });\n\n await putCompactedObject({\n key: uploadKey,\n body,\n signal: ctx.cancellationSignal,\n onProgress: () => ctx.heartbeat(),\n metadata: {\n stream_id: stream.id,\n chunk_count: String(expected.count),\n uncompressed_bytes: String(expected.uncompressedBytes),\n last_seq: String(expected.maxSeq),\n },\n });\n\n if (\n stats.chunkCount !== expected.count ||\n stats.lastSeq !== expected.maxSeq ||\n stats.uncompressedBytes !== expected.uncompressedBytes\n ) {\n await deleteObject(uploadKey).catch((error) => {\n logger().error(\n {err: error, streamId: stream.id, objectKey: uploadKey},\n 'Failed to delete invalid compacted log object',\n );\n reportError(error, {\n boundary: 'logs.cleanup',\n operation: 'delete-invalid-compaction-object',\n extra: {streamId: stream.id, objectKey: uploadKey},\n });\n });\n throw new Error(\n `Compaction integrity check failed for stream ${stream.id}: streamed ${stats.chunkCount} chunks / ${stats.uncompressedBytes} bytes up to seq ${stats.lastSeq}, table holds ${expected.count} / ${expected.uncompressedBytes} bytes up to seq ${expected.maxSeq}`,\n );\n }\n\n const {updated} = await db().transaction((tx) =>\n dependencies.setObjectKeyAndDeleteChunks(tx, {\n streamId: stream.id,\n objectKey: uploadKey,\n }),\n );\n if (!updated) {\n await deleteObject(uploadKey);\n const current = await getAttemptStreamById(stream.id);\n return {outcome: current ? 'superseded' : 'retention-raced'};\n }\n\n return {\n outcome: 'compacted',\n objectKey: uploadKey,\n chunkCount: stats.chunkCount,\n uncompressedBytes: stats.uncompressedBytes,\n };\n}\n\nexport function createCompactStreamActivity(\n dependencies: CompactStreamDependencies = defaultDependencies,\n): (params: {streamId: string}) => Promise<CompactStreamResult> {\n return async (params) => {\n let outcome: CompactionMetricOutcome = 'failed';\n try {\n const result = await compactStream(params, dependencies);\n outcome = result.outcome;\n return result;\n } finally {\n compactionCount.add(1, {outcome});\n }\n };\n}\n\nexport const compactStreamActivity = createCompactStreamActivity();\n"],"names":["reportError","logger","Context","compactedObjectKey","deleteObject","putCompactedObject","compactedGzipStream","chunkStats","db","getAttemptStreamById","setObjectKeyAndDeleteChunks","compactionCount","defaultDependencies","compactStream","params","dependencies","stream","streamId","outcome","objectKey","ctx","current","uploadKey","crypto","randomUUID","expected","id","body","stats","onPage","heartbeat","key","signal","cancellationSignal","onProgress","metadata","stream_id","chunk_count","String","count","uncompressed_bytes","uncompressedBytes","last_seq","maxSeq","chunkCount","lastSeq","catch","error","err","boundary","operation","extra","Error","updated","transaction","tx","createCompactStreamActivity","result","add","compactStreamActivity"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../src/temporal/activities/compact-stream.ts"],"sourcesContent":["import {reportError} from '@shipfox/node-error-monitoring';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {Context} from '@temporalio/activity';\nimport {compactedObjectKey, deleteObject, putCompactedObject} from '#api/object-storage.js';\nimport {compactedGzipStream} from '#core/compaction.js';\nimport {chunkStats} from '#db/chunks.js';\nimport {db, type Transaction} from '#db/db.js';\nimport {getAttemptStreamById, setObjectKeyAndDeleteChunks} from '#db/streams.js';\nimport {\n type CompactionMetricOutcome,\n compactedBytesCount,\n compactionCount,\n} from '#metrics/instance.js';\n\nexport type CompactStreamResult =\n | {outcome: 'gone'}\n | {outcome: 'already-compacted'}\n | {outcome: 'superseded'}\n | {outcome: 'retention-raced'}\n | {outcome: 'compacted'; objectKey: string; chunkCount: number; uncompressedBytes: number};\n\ninterface CompactStreamDependencies {\n compactedGzipStream: typeof compactedGzipStream;\n setObjectKeyAndDeleteChunks: (\n tx: Transaction,\n params: {streamId: string; objectKey: string},\n ) => Promise<{updated: boolean}>;\n}\n\nconst defaultDependencies: CompactStreamDependencies = {\n compactedGzipStream,\n setObjectKeyAndDeleteChunks,\n};\n\n/**\n * Compacts one closed stream into a single gzip NDJSON object, then deletes its chunk rows.\n *\n * load stream ──┬─ gone ─────────────► no-op (retention raced)\n * ├─ object_key set ───► no-op (idempotent re-run)\n * └─ else: upload to a per-attempt key ─► gzip ─► multipart (abort-aware, heartbeat)\n * │\n * verify streamed count/maxSeq/bytes == table (mismatch ─► delete upload, throw, retry)\n * │\n * tx: set object_key (state='closed' AND object_key IS NULL) + delete chunks\n * └─ 0 rows ─► delete this attempt's upload, then re-read the row:\n * gone ─► retention raced · keyed ─► superseded by another attempt\n *\n * Each attempt uploads to its own `compactedObjectKey(stream, uuid)`, so a slow or zombie\n * attempt can never overwrite a published object. The single-winner publish (object_key set\n * + chunk delete, atomic) drops chunks only once a complete object is durable; the integrity\n * check (count, maxSeq, and byte total) guards a read bug from publishing a truncated object\n * before the only copy of the source is gone (S3 part checksums cover byte transfer).\n */\nasync function compactStream(\n params: {streamId: string},\n dependencies: CompactStreamDependencies,\n): Promise<CompactStreamResult> {\n const stream = await getAttemptStreamById(params.streamId);\n if (!stream) return {outcome: 'gone'};\n if (stream.objectKey) return {outcome: 'already-compacted'};\n\n const ctx = Context.current();\n const uploadKey = compactedObjectKey(stream, crypto.randomUUID());\n const expected = await chunkStats(stream.id);\n const {body, stats} = dependencies.compactedGzipStream({\n streamId: stream.id,\n onPage: () => ctx.heartbeat(),\n });\n\n await putCompactedObject({\n key: uploadKey,\n body,\n signal: ctx.cancellationSignal,\n onProgress: () => ctx.heartbeat(),\n metadata: {\n stream_id: stream.id,\n chunk_count: String(expected.count),\n uncompressed_bytes: String(expected.uncompressedBytes),\n last_seq: String(expected.maxSeq),\n },\n });\n\n if (\n stats.chunkCount !== expected.count ||\n stats.lastSeq !== expected.maxSeq ||\n stats.uncompressedBytes !== expected.uncompressedBytes\n ) {\n await deleteObject(uploadKey).catch((error) => {\n logger().error(\n {err: error, streamId: stream.id, objectKey: uploadKey},\n 'Failed to delete invalid compacted log object',\n );\n reportError(error, {\n boundary: 'logs.cleanup',\n operation: 'delete-invalid-compaction-object',\n extra: {streamId: stream.id, objectKey: uploadKey},\n });\n });\n throw new Error(\n `Compaction integrity check failed for stream ${stream.id}: streamed ${stats.chunkCount} chunks / ${stats.uncompressedBytes} bytes up to seq ${stats.lastSeq}, table holds ${expected.count} / ${expected.uncompressedBytes} bytes up to seq ${expected.maxSeq}`,\n );\n }\n\n const {updated} = await db().transaction((tx) =>\n dependencies.setObjectKeyAndDeleteChunks(tx, {\n streamId: stream.id,\n objectKey: uploadKey,\n }),\n );\n if (!updated) {\n await deleteObject(uploadKey);\n const current = await getAttemptStreamById(stream.id);\n return {outcome: current ? 'superseded' : 'retention-raced'};\n }\n\n return {\n outcome: 'compacted',\n objectKey: uploadKey,\n chunkCount: stats.chunkCount,\n uncompressedBytes: stats.uncompressedBytes,\n };\n}\n\nexport function createCompactStreamActivity(\n dependencies: CompactStreamDependencies = defaultDependencies,\n): (params: {streamId: string}) => Promise<CompactStreamResult> {\n return async (params) => {\n let outcome: CompactionMetricOutcome = 'failed';\n try {\n const result = await compactStream(params, dependencies);\n outcome = result.outcome;\n // Count uncompressed log bytes only on the single-winner publish: idempotent re-runs\n // (`already-compacted`) and failed attempts never reach this branch, so the counter\n // tracks exactly the bytes durably moved to object storage.\n if (result.outcome === 'compacted') {\n compactedBytesCount.add(result.uncompressedBytes);\n }\n return result;\n } finally {\n compactionCount.add(1, {outcome});\n }\n };\n}\n\nexport const compactStreamActivity = createCompactStreamActivity();\n"],"names":["reportError","logger","Context","compactedObjectKey","deleteObject","putCompactedObject","compactedGzipStream","chunkStats","db","getAttemptStreamById","setObjectKeyAndDeleteChunks","compactedBytesCount","compactionCount","defaultDependencies","compactStream","params","dependencies","stream","streamId","outcome","objectKey","ctx","current","uploadKey","crypto","randomUUID","expected","id","body","stats","onPage","heartbeat","key","signal","cancellationSignal","onProgress","metadata","stream_id","chunk_count","String","count","uncompressed_bytes","uncompressedBytes","last_seq","maxSeq","chunkCount","lastSeq","catch","error","err","boundary","operation","extra","Error","updated","transaction","tx","createCompactStreamActivity","result","add","compactStreamActivity"],"mappings":"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"}