/* * Durable AgentRun Kafka projector storage operations. * Kafka I/O is intentionally outside every transaction in this module. */ import { createHash } from "node:crypto"; import { indexWorkbenchAggregateEvents, inputFactWithAggregateSeq, nonNegativeInteger, normalizeWorkbenchAggregateEventsForFacts, normalizeWorkbenchFacts, parseJsonColumn, stableJson, textOr } from "./runtime-store-core.ts"; export async function commitAgentRunKafkaProjection(store, params = {}) { const transport = normalizeTransport(params.transport); const canonicalEvent = objectValue(params.canonicalEvent); const projectedEvent = objectValue(params.projectedEvent); const sourceEventId = requiredText(canonicalEvent.eventId, "canonical AgentRun eventId"); const traceId = requiredText(projectedEvent.traceId ?? canonicalEvent.traceId, "canonical AgentRun traceId"); const sessionId = text(projectedEvent.sessionId ?? canonicalEvent.hwlabSessionId); const sourceSeq = nonNegativeInteger(canonicalEvent.sourceSeq ?? canonicalEvent.event?.seq); const inputSha256 = requiredText(transport.inputSha256, "Kafka input sha256"); const now = store.now(); const result = await store.withDurableTransaction("workbench.kafka-projection.commit", async (client) => { await lockKafkaInboxIdentity(client, transport, sourceEventId); await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [`workbench-projection:${traceId}`]); const existing = await client.query( "SELECT source_topic, source_partition, source_offset, source_event_id, input_sha256, status, projected_seq FROM workbench_kafka_inbox WHERE (source_topic = $1 AND source_partition = $2 AND source_offset = $3) OR (source_topic = $1 AND source_event_id = $4) ORDER BY created_at ASC LIMIT 1", [transport.sourceTopic, transport.sourcePartition, transport.sourceOffset, sourceEventId] ); const prior = existing.rows?.[0] ?? null; if (prior) { if (prior.input_sha256 !== inputSha256) { const diagnostic = { code: "workbench_kafka_inbox_hash_conflict", message: "Kafka transport or source event identity was reused with a different payload hash.", valuesRedacted: true }; if (prior.status !== "projected") { await client.query( "UPDATE workbench_kafka_inbox SET status = 'failed', error_json = $1, updated_at = $2 WHERE source_topic = $3 AND source_partition = $4 AND source_offset = $5", [stableJson(diagnostic), now, prior.source_topic, prior.source_partition, prior.source_offset] ); } await insertDlq(client, { ...transport, sourceEventId, inputSha256, error: diagnostic, envelope: redactedEnvelope(canonicalEvent), createdAt: now }); return { conflict: true, diagnostic, duplicate: false, projectedSeq: nonNegativeInteger(prior.projected_seq) }; } return { duplicate: true, conflict: false, status: prior.status, projectedSeq: nonNegativeInteger(prior.projected_seq) }; } await client.query( "INSERT INTO workbench_kafka_inbox (source_topic, source_partition, source_offset, source_key, source_event_id, input_sha256, trace_id, session_id, run_id, command_id, source_seq, status, projected_seq, envelope_json, error_json, created_at, updated_at, projected_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,'processing',NULL,$12,'{}',$13,$13,NULL)", [transport.sourceTopic, transport.sourcePartition, transport.sourceOffset, transport.sourceKey, sourceEventId, inputSha256, traceId, sessionId, text(canonicalEvent.runId), text(canonicalEvent.commandId), sourceSeq, stableJson(redactedEnvelope(canonicalEvent)), now] ); const checkpointResult = await client.query("SELECT checkpoint_json FROM workbench_projection_checkpoints WHERE trace_id = $1 FOR UPDATE", [traceId]); const previousCheckpoint = parseJsonColumn(checkpointResult.rows?.[0]?.checkpoint_json, null); const sessionResult = sessionId ? await client.query("SELECT owner_user_id, project_id, conversation_id, thread_id, session_json FROM workbench_sessions WHERE session_id = $1 FOR UPDATE", [sessionId]) : { rows: [] }; const existingSession = sessionResult.rows?.[0] ?? null; const maxResult = await client.query( "SELECT GREATEST(COALESCE((SELECT MAX(projected_seq) FROM workbench_trace_events WHERE trace_id = $1), 0), COALESCE((SELECT projected_seq FROM workbench_projection_checkpoints WHERE trace_id = $1), 0))::int AS max_projected_seq", [traceId] ); const projectedSeq = nonNegativeInteger(maxResult.rows?.[0]?.max_projected_seq) + 1; if (typeof params.factsFactory !== "function") throw codedError("workbench_kafka_facts_factory_required", "Kafka projection requires a pure facts factory."); const built = await params.factsFactory({ projectedSeq, previousCheckpoint, projectedAt: now }); let facts = null; let persistedEvents = []; if (built?.written !== false) { facts = normalizeWorkbenchFacts(mergeKafkaSessionIdentity(built?.facts ?? {}, existingSession), params.requestMeta ?? {}, now); const persisted = await persistFacts(store, client, facts, params.requestMeta ?? {}); persistedEvents = persisted.events; } const committedProjectedSeq = built?.written === false ? nonNegativeInteger(built?.projectedSeq) || nonNegativeInteger(previousCheckpoint?.projectedSeq) || projectedSeq : projectedSeq; const projectionWritten = built?.written !== false; if (projectionWritten) { const hwlabEvent = objectValue(params.hwlabEvent); const hwlabEventId = requiredText(hwlabEvent.eventId, "HWLAB Kafka eventId"); await client.query( "INSERT INTO hwlab_kafka_outbox (event_id, topic, partition_key, payload_json, headers_json, attempt_count, next_attempt_at, lease_owner, lease_expires_at, published_at, last_error, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,0,$6,NULL,NULL,NULL,NULL,$6,$6) ON CONFLICT (event_id) DO NOTHING", [hwlabEventId, requiredText(params.hwlabTopic, "HWLAB Kafka topic"), requiredText(params.partitionKey, "HWLAB Kafka partition key"), stableJson(hwlabEvent), stableJson(objectValue(params.headers)), now] ); } await client.query( "UPDATE workbench_kafka_inbox SET status = 'projected', projected_seq = $1, updated_at = $2, projected_at = $2 WHERE source_topic = $3 AND source_partition = $4 AND source_offset = $5", [committedProjectedSeq, now, transport.sourceTopic, transport.sourcePartition, transport.sourceOffset] ); return { duplicate: false, conflict: false, projectedSeq: committedProjectedSeq, facts, events: persistedEvents, projectionWritten, suppressedAfterSeal: built?.suppressedAfterSeal === true }; }); if (result.facts) store.memory.writeWorkbenchFacts({ facts: result.facts }, params.requestMeta ?? {}); return { ...result, transport, sourceEventId, traceId, sessionId, sourceSeq, valuesPrinted: false }; } export async function recordFailedAgentRunKafkaMessage(store, params = {}) { const transport = normalizeTransport(params.transport); const now = store.now(); const error = redactedError(params.error); const sourceEventId = text(params.sourceEventId); const inputSha256 = requiredText(transport.inputSha256, "Kafka input sha256"); const result = await store.withDurableTransaction("workbench.kafka-projection.dlq", async (client) => { await lockKafkaInboxIdentity(client, transport, sourceEventId); const existing = await client.query("SELECT source_topic, source_partition, source_offset, input_sha256, status, projected_seq FROM workbench_kafka_inbox WHERE (source_topic = $1 AND source_partition = $2 AND source_offset = $3) OR ($4::text IS NOT NULL AND source_topic = $1 AND source_event_id = $4) ORDER BY created_at ASC LIMIT 1 FOR UPDATE", [transport.sourceTopic, transport.sourcePartition, transport.sourceOffset, sourceEventId]); const prior = existing.rows?.[0] ?? null; if (prior) { const sameTransport = prior.source_partition === transport.sourcePartition && String(prior.source_offset) === transport.sourceOffset; const diagnostic = prior.input_sha256 !== inputSha256 ? { ...error, code: "workbench_kafka_inbox_hash_conflict" } : sameTransport ? error : { ...error, code: "workbench_kafka_inbox_source_event_duplicate" }; if (prior.status !== "projected") { await client.query("UPDATE workbench_kafka_inbox SET status = 'failed', error_json = $1, updated_at = $2 WHERE source_topic = $3 AND source_partition = $4 AND source_offset = $5", [stableJson(diagnostic), now, prior.source_topic, prior.source_partition, prior.source_offset]); } await insertDlq(client, { ...transport, sourceEventId, inputSha256, error: diagnostic, envelope: redactedEnvelope(params.envelope), createdAt: now }); return { duplicate: sameTransport, conflict: !sameTransport || prior.input_sha256 !== inputSha256, priorStatus: prior.status, projectedSeq: nonNegativeInteger(prior.projected_seq), diagnostic }; } await client.query( "INSERT INTO workbench_kafka_inbox (source_topic, source_partition, source_offset, source_key, source_event_id, input_sha256, trace_id, session_id, run_id, command_id, source_seq, status, projected_seq, envelope_json, error_json, created_at, updated_at, projected_at) VALUES ($1,$2,$3,$4,$5,$6,NULL,NULL,NULL,NULL,0,'failed',NULL,$7,$8,$9,$9,NULL) ON CONFLICT (source_topic, source_partition, source_offset) DO UPDATE SET status = 'failed', error_json = EXCLUDED.error_json, updated_at = EXCLUDED.updated_at", [transport.sourceTopic, transport.sourcePartition, transport.sourceOffset, transport.sourceKey, sourceEventId, inputSha256, stableJson(redactedEnvelope(params.envelope)), stableJson(error), now] ); await insertDlq(client, { ...transport, sourceEventId, inputSha256, error, envelope: redactedEnvelope(params.envelope), createdAt: now }); return { duplicate: false, conflict: false, priorStatus: null, diagnostic: error }; }); return { recorded: true, transport, sourceEventId, error: result.diagnostic, ...result, valuesPrinted: false }; } export async function recordIgnoredAgentRunKafkaMessage(store, params = {}) { const transport = normalizeTransport(params.transport); const envelope = objectValue(params.envelope); const sourceEventId = requiredText(envelope.eventId, "canonical AgentRun eventId"); const inputSha256 = requiredText(transport.inputSha256, "Kafka input sha256"); const now = store.now(); return store.withDurableTransaction("workbench.kafka-projection.ignore", async (client) => { await lockKafkaInboxIdentity(client, transport, sourceEventId); const existing = await client.query("SELECT input_sha256, status FROM workbench_kafka_inbox WHERE (source_topic = $1 AND source_partition = $2 AND source_offset = $3) OR (source_topic = $1 AND source_event_id = $4) ORDER BY created_at ASC LIMIT 1", [transport.sourceTopic, transport.sourcePartition, transport.sourceOffset, sourceEventId]); const prior = existing.rows?.[0] ?? null; if (prior) { if (prior.input_sha256 !== inputSha256) { const diagnostic = { code: "workbench_kafka_inbox_hash_conflict", message: "Ignored Kafka identity was reused with a different payload hash.", valuesRedacted: true }; await insertDlq(client, { ...transport, sourceEventId, inputSha256, error: diagnostic, envelope: redactedEnvelope(envelope), createdAt: now }); return { ignored: true, duplicate: false, conflict: true, status: prior.status, diagnostic, valuesPrinted: false }; } return { ignored: true, duplicate: true, status: prior.status, valuesPrinted: false }; } await client.query( "INSERT INTO workbench_kafka_inbox (source_topic, source_partition, source_offset, source_key, source_event_id, input_sha256, trace_id, session_id, run_id, command_id, source_seq, status, projected_seq, envelope_json, error_json, created_at, updated_at, projected_at) VALUES ($1,$2,$3,$4,$5,$6,NULL,NULL,$7,$8,$9,'ignored',NULL,$10,'{}',$11,$11,NULL)", [transport.sourceTopic, transport.sourcePartition, transport.sourceOffset, transport.sourceKey, sourceEventId, inputSha256, text(envelope.runId), text(envelope.commandId), nonNegativeInteger(envelope.sourceSeq), stableJson(redactedEnvelope(envelope)), now] ); return { ignored: true, duplicate: false, status: "ignored", valuesPrinted: false }; }); } export async function claimHwlabKafkaOutbox(store, { owner, leaseMs, limit } = {}) { const leaseOwner = requiredText(owner, "HWLAB Kafka relay owner"); const boundedLimit = boundedInteger(limit, 100, 1, 500); const now = store.now(); const leaseExpiresAt = new Date(Date.parse(now) + boundedInteger(leaseMs, 60_000, 1_000, 3_600_000)).toISOString(); const result = await store.withDurableTransaction("hwlab.kafka-outbox.claim", (client) => client.query( `WITH candidates AS ( SELECT candidate.outbox_seq FROM hwlab_kafka_outbox candidate WHERE candidate.published_at IS NULL AND candidate.next_attempt_at <= $1 AND (candidate.lease_expires_at IS NULL OR candidate.lease_expires_at <= $1) AND NOT EXISTS ( SELECT 1 FROM hwlab_kafka_outbox earlier WHERE earlier.published_at IS NULL AND earlier.partition_key = candidate.partition_key AND earlier.outbox_seq < candidate.outbox_seq ) ORDER BY candidate.outbox_seq ASC FOR UPDATE SKIP LOCKED LIMIT $2 ) UPDATE hwlab_kafka_outbox o SET attempt_count = o.attempt_count + 1, lease_owner = $3, lease_expires_at = $4, updated_at = $1 FROM candidates WHERE o.outbox_seq = candidates.outbox_seq RETURNING o.*`, [now, boundedLimit, leaseOwner, leaseExpiresAt] )); return (result.rows ?? []).map(outboxRow); } export async function completeHwlabKafkaOutbox(store, item = {}) { const result = await store.query( "UPDATE hwlab_kafka_outbox SET published_at = $1, lease_owner = NULL, lease_expires_at = NULL, last_error = NULL, updated_at = $1 WHERE outbox_seq = $2 AND lease_owner = $3 AND attempt_count = $4 AND lease_expires_at = $5 AND lease_expires_at > $1 AND published_at IS NULL RETURNING outbox_seq", [store.now(), item.outboxSeq, item.leaseOwner, item.attemptCount, item.leaseExpiresAt] ); if (!result.rows?.[0]) throw codedError("hwlab_kafka_outbox_lease_conflict", "HWLAB Kafka outbox claim is stale."); return { completed: true, outboxSeq: Number(result.rows[0].outbox_seq), valuesPrinted: false }; } export async function retryHwlabKafkaOutbox(store, item = {}, error = null, retryAt = null) { const now = store.now(); const nextAttemptAt = text(retryAt) ?? now; const result = await store.query( "UPDATE hwlab_kafka_outbox SET next_attempt_at = $1, lease_owner = NULL, lease_expires_at = NULL, last_error = $2, updated_at = $3 WHERE outbox_seq = $4 AND lease_owner = $5 AND attempt_count = $6 AND lease_expires_at = $7 AND lease_expires_at > $3 AND published_at IS NULL RETURNING outbox_seq", [nextAttemptAt, stableJson(redactedError(error)), now, item.outboxSeq, item.leaseOwner, item.attemptCount, item.leaseExpiresAt] ); if (!result.rows?.[0]) throw codedError("hwlab_kafka_outbox_lease_conflict", "HWLAB Kafka outbox claim is stale."); return { retried: true, outboxSeq: Number(result.rows[0].outbox_seq), nextAttemptAt, valuesPrinted: false }; } export async function hwlabKafkaProjectorStatus(store) { const result = await store.query( `SELECT COUNT(*) FILTER (WHERE published_at IS NULL)::int AS backlog_count, COUNT(*) FILTER (WHERE published_at IS NULL AND attempt_count > 0)::int AS retry_count, MIN(created_at) FILTER (WHERE published_at IS NULL) AS oldest_pending_at, (SELECT COUNT(*)::int FROM workbench_kafka_inbox WHERE status = 'failed') AS failed_inbox_count, (SELECT COUNT(*)::int FROM workbench_kafka_dlq) AS dlq_count FROM hwlab_kafka_outbox`, [] ); const row = result.rows?.[0] ?? {}; return { backlogCount: Number(row.backlog_count ?? 0), retryCount: Number(row.retry_count ?? 0), oldestPendingAt: row.oldest_pending_at ?? null, failedInboxCount: Number(row.failed_inbox_count ?? 0), dlqCount: Number(row.dlq_count ?? 0), valuesPrinted: false }; } export async function readAtomicWorkbenchProjectionSync(store, params = {}) { const since = nonNegativeInteger(params.afterOutboxSeq ?? params.afterSeq ?? params.since); const limit = boundedInteger(params.limit, 100, 1, 500); const traceId = text(params.traceId); const sessionId = text(params.sessionId); const snapshotOnly = params.snapshotOnly === true; const deltaOnly = params.deltaOnly === true; if (!traceId && !sessionId) throw codedError("workbench_sync_scope_required", "Atomic Workbench sync requires sessionId or traceId."); if (snapshotOnly && deltaOnly) throw codedError("workbench_sync_mode_invalid", "snapshotOnly and deltaOnly are mutually exclusive."); return store.withDurableTransaction("workbench.projection-sync.read", async (client) => { await client.query("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY"); const filter = traceId ? { column: "trace_id", value: traceId } : { column: "session_id", value: sessionId }; const cutoffResult = await client.query(`SELECT COALESCE(MAX(outbox_seq), 0)::bigint AS cutoff FROM workbench_projection_outbox WHERE ${filter.column} = $1`, [filter.value]); const cutoff = Number(cutoffResult.rows?.[0]?.cutoff ?? 0); const outboxResult = snapshotOnly ? { rows: [] } : await client.query( `SELECT outbox_seq, outbox_event_id, entity_family, entity_id, event_seq, aggregate_id, aggregate_seq, projection_revision, trace_id, session_id, turn_id, message_id, projected_seq, source_seq, source_event_id, commit_type, terminal, sealed, payload_json, created_at FROM workbench_projection_outbox WHERE outbox_seq > $1 AND outbox_seq <= $2 AND ${filter.column} = $3 ORDER BY outbox_seq ASC LIMIT $4`, [since, cutoff, filter.value, limit + 1] ); const allRows = (outboxResult.rows ?? []).map(projectionOutboxRow); const events = allRows.slice(0, limit); const facts = deltaOnly ? {} : await readSyncFacts(client, { traceId, sessionId, limit: 500, families: snapshotOnly ? ["sessions", "messages", "turns"] : null }); const hasMore = allRows.length > limit; return { facts, events, cutoffOutboxSeq: cutoff, cursorOutboxSeq: snapshotOnly ? cutoff : hasMore ? (events.at(-1)?.outboxSeq ?? since) : cutoff, hasMore, valuesPrinted: false }; }); } async function persistFacts(store, client, facts, requestMeta) { const aggregateEvents = normalizeWorkbenchAggregateEventsForFacts(facts, requestMeta, store.now()); const events = []; for (const event of aggregateEvents) events.push(await store.persistWorkbenchAggregateEvent(event, client)); const eventIndex = indexWorkbenchAggregateEvents(events); const inputFacts = facts.inputs.map((fact) => inputFactWithAggregateSeq(fact, eventIndex)); for (const fact of inputFacts) await store.persistWorkbenchSessionInputFact(fact, client); for (const fact of facts.sessions) await store.persistWorkbenchSessionFact(fact, client); for (const fact of facts.messages) await store.persistWorkbenchMessageFact(fact, client); for (const fact of facts.parts) await store.persistWorkbenchPartFact(fact, client); for (const fact of facts.turns) await store.persistWorkbenchTurnFact(fact, client); for (const fact of facts.traceEvents) await store.persistWorkbenchTraceEventFact(fact, client); for (const fact of facts.checkpoints) await store.persistWorkbenchProjectionCheckpoint(fact, client); for (const fact of facts.messages) { const event = eventIndex.get(`message:${fact.messageId}`) ?? eventIndex.get(`messages:${fact.messageId}`) ?? eventIndex.get(`sourceEvent:${fact.sourceEventId}`) ?? null; await store.persistWorkbenchProjectionOutbox(outboxRecord(fact, event, "messages", "message", { role: fact.role, status: fact.status }), client); } for (const fact of facts.traceEvents) { const event = eventIndex.get(`traceEvent:${fact.id}`) ?? eventIndex.get(`sourceEvent:${fact.sourceEventId}`) ?? null; await store.persistWorkbenchProjectionOutbox(outboxRecord(fact, event, "traceEvents", fact.terminal ? "terminal" : "event", { type: fact.eventType }), client); } for (const fact of facts.turns.filter((item) => item.terminal || item.sealed)) { const event = eventIndex.get(`turn:${fact.turnId}`) ?? eventIndex.get(`sourceEvent:${fact.sourceEventId}`) ?? null; await store.persistWorkbenchProjectionOutbox(outboxRecord(fact, event, "turns", "terminal", { status: fact.status, failureKind: fact.failureKind }), client); } return { events, inputFacts }; } function outboxRecord(fact, event, family, commitType, metadata) { const entityId = text(family === "messages" ? fact.messageId : family === "turns" ? fact.turnId : fact.id) ?? text(fact.traceId) ?? "projection"; return { outboxEventId: projectionOutboxEventId({ fact, event, family, entityId, commitType }), entityFamily: family, entityId, eventSeq: event?.eventSeq ?? null, aggregateId: event?.aggregateId ?? null, aggregateSeq: event?.aggregateSeq ?? 0, projectionRevision: event?.projectionRevision ?? fact.projectedSeq, traceId: fact.traceId, sessionId: fact.sessionId, turnId: fact.turnId, messageId: fact.messageId, projectedSeq: fact.projectedSeq, sourceSeq: fact.sourceSeq, sourceEventId: fact.sourceEventId, commitType, terminal: Boolean(fact.terminal), sealed: Boolean(fact.sealed), payload: { family, fact, metadata: { ...metadata, eventSeq: event?.eventSeq ?? null, aggregateSeq: event?.aggregateSeq ?? null, projectedSeq: fact.projectedSeq, valuesRedacted: true }, valuesRedacted: true }, createdAt: fact.occurredAt ?? fact.updatedAt ?? fact.createdAt }; } export function projectionOutboxEventId({ fact = {}, event = null, family, entityId, commitType }) { const sourceEventId = text(fact.sourceEventId); if (sourceEventId) return `${sourceEventId}:${family}:${entityId}:${commitType}`; const identity = stableJson({ family, entityId, commitType, eventSeq: event?.eventSeq ?? null, aggregateId: event?.aggregateId ?? null, aggregateSeq: event?.aggregateSeq ?? 0, projectionRevision: event?.projectionRevision ?? fact.projectedSeq ?? 0, projectedSeq: fact.projectedSeq ?? 0, sourceSeq: fact.sourceSeq ?? 0 }); return `projection:${createHash("sha256").update(identity).digest("hex")}`; } function mergeKafkaSessionIdentity(facts, row) { if (!row) return facts; const existing = parseJsonColumn(row.session_json, {}); const sessions = Array.isArray(facts?.sessions) ? facts.sessions.map((fact) => ({ ...existing, ...fact, ownerUserId: fact.ownerUserId ?? row.owner_user_id ?? existing.ownerUserId ?? null, projectId: fact.projectId ?? row.project_id ?? existing.projectId ?? null, conversationId: fact.conversationId ?? row.conversation_id ?? existing.conversationId ?? null, threadId: fact.threadId ?? row.thread_id ?? existing.threadId ?? null, ownerRole: fact.ownerRole ?? existing.ownerRole ?? null })) : []; return { ...facts, sessions }; } async function readSyncFacts(client, { traceId, sessionId, limit, families = null }) { const familySet = Array.isArray(families) ? new Set(families) : null; const specs = [ ["sessions", "workbench_sessions", "session_json", traceId ? "last_trace_id" : "session_id"], ["messages", "workbench_messages", "message_json", traceId ? "trace_id" : "session_id"], ["parts", "workbench_parts", "part_json", traceId ? "trace_id" : "session_id"], ["turns", "workbench_turns", "turn_json", traceId ? "trace_id" : "session_id"], ["traceEvents", "workbench_trace_events", "event_json", traceId ? "trace_id" : "session_id"], ["checkpoints", "workbench_projection_checkpoints", "checkpoint_json", traceId ? "trace_id" : "session_id"] ].filter(([family]) => !familySet || familySet.has(family)); const value = traceId ?? sessionId; const facts = {}; for (const [family, table, jsonColumn, column] of specs) { const result = await client.query(`SELECT ${jsonColumn} FROM ${table} WHERE ${column} = $1 ORDER BY updated_at ASC LIMIT $2`, [value, limit]); facts[family] = (result.rows ?? []).map((row) => parseJsonColumn(row[jsonColumn], null)).filter(Boolean); } return facts; } async function insertDlq(client, record) { await client.query( "INSERT INTO workbench_kafka_dlq (source_topic, source_partition, source_offset, source_event_id, input_sha256, error_code, error_message, envelope_json, created_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9) ON CONFLICT (source_topic, source_partition, source_offset) DO UPDATE SET error_code = EXCLUDED.error_code, error_message = EXCLUDED.error_message, envelope_json = EXCLUDED.envelope_json", [record.sourceTopic, record.sourcePartition, record.sourceOffset, record.sourceEventId, record.inputSha256, record.error.code, record.error.message, stableJson(record.envelope), record.createdAt] ); } async function lockKafkaInboxIdentity(client, transport, sourceEventId) { const transportIdentity = `${transport.sourceTopic}:transport:${transport.sourcePartition}:${transport.sourceOffset}`; await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [`workbench-kafka-inbox:${transportIdentity}`]); if (sourceEventId) { const eventIdentity = `${transport.sourceTopic}:event:${sourceEventId}`; await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [`workbench-kafka-inbox:${eventIdentity}`]); } } function normalizeTransport(value = {}) { const input = objectValue(value); const sourceTopic = requiredText(input.sourceTopic ?? input.topic, "Kafka source topic"); const sourcePartition = Number(input.sourcePartition ?? input.partition); const sourceOffset = text(input.sourceOffset ?? input.offset); if (!Number.isInteger(sourcePartition) || sourcePartition < 0) throw codedError("workbench_kafka_partition_invalid", "Kafka source partition must be a non-negative integer."); if (!/^\d+$/u.test(sourceOffset ?? "")) throw codedError("workbench_kafka_offset_invalid", "Kafka source offset must be a non-negative integer string."); return { sourceTopic, sourcePartition, sourceOffset, inputSha256: text(input.inputSha256), sourceKey: text(input.sourceKey), valuesPrinted: false }; } function projectionOutboxRow(row) { return { outboxSeq: Number(row.outbox_seq), outboxEventId: row.outbox_event_id, entityFamily: row.entity_family, entityId: row.entity_id, eventSeq: row.event_seq == null ? null : Number(row.event_seq), aggregateId: row.aggregate_id, aggregateSeq: Number(row.aggregate_seq ?? 0), projectionRevision: Number(row.projection_revision ?? 0), traceId: row.trace_id, sessionId: row.session_id, turnId: row.turn_id, messageId: row.message_id, projectedSeq: Number(row.projected_seq ?? 0), sourceSeq: Number(row.source_seq ?? 0), sourceEventId: row.source_event_id, commitType: row.commit_type, terminal: Boolean(row.terminal), sealed: Boolean(row.sealed), payload: parseJsonColumn(row.payload_json, {}), createdAt: row.created_at, valuesPrinted: false }; } function outboxRow(row) { return { outboxSeq: Number(row.outbox_seq), eventId: row.event_id, topic: row.topic, partitionKey: row.partition_key, payload: parseJsonColumn(row.payload_json, {}), headers: parseJsonColumn(row.headers_json, {}), attemptCount: Number(row.attempt_count ?? 0), nextAttemptAt: row.next_attempt_at, leaseOwner: row.lease_owner, leaseExpiresAt: row.lease_expires_at, createdAt: row.created_at, valuesPrinted: false }; } function redactedEnvelope(value) { const input = objectValue(value); return { schema: text(input.schema), eventType: text(input.eventType), eventId: text(input.eventId), outboxSeq: nonNegativeInteger(input.outboxSeq), sourceSeq: nonNegativeInteger(input.sourceSeq), traceId: text(input.traceId), sessionId: text(input.sessionId), hwlabSessionId: text(input.hwlabSessionId), runId: text(input.runId), commandId: text(input.commandId), valuesRedacted: true }; } function redactedError(error) { return { code: text(error?.code) ?? "workbench_kafka_message_invalid", message: String(error?.message ?? error ?? "Kafka message is invalid.").slice(0, 500), valuesRedacted: true }; } function objectValue(value) { return value && typeof value === "object" && !Array.isArray(value) ? value : {}; } function text(value) { const normalized = textOr(value, ""); return normalized || null; } function requiredText(value, label) { const normalized = text(value); if (!normalized) throw codedError("workbench_kafka_contract_invalid", `${label} is required.`); return normalized; } function boundedInteger(value, fallback, min, max) { const parsed = Number(value); return Number.isInteger(parsed) && parsed >= min && parsed <= max ? parsed : fallback; } function codedError(code, message) { const error = new Error(message); error.code = code; return error; }