Files
pikasTech-HWLAB/internal/db/runtime-store-postgres-kafka.ts

419 lines
28 KiB
TypeScript

/*
* 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;
}