Files
pikasTech-HWLAB/internal/db/runtime-store-core.ts
T

1559 lines
63 KiB
TypeScript

/*
* SPEC: PJ2026-010403 API契约 draft-2026-06-17-r0; PJ2026-0102 Agent编排 draft-2026-06-17-r0; PJ2026-0104010803 Workbench唯一投影 draft-2026-06-19-p1-agentrun-incremental-cursor; draft-2026-06-20-p1-zero-split-durable-realtime; draft-2026-06-24-p0-aggregate-event-stream; PJ2026-010205 HWLAB接入 draft-2026-06-25-p0-session-warm-runner-contract.
* 职责: Cloud runtime durable store。Code Agent trace/session/projection cursor 投影事实必须从同一持久化适配器写入和恢复。
*/
import { createHash, randomUUID } from "node:crypto";
import {
ENVIRONMENT_DEV,
ERROR_CODES,
HwlabProtocolError,
assertProtocolRecord
} from "../protocol/index.mjs";
import {
CLOUD_API_SERVICE_ID,
createProtocolAuditEvent,
deriveProtocolActorFromMeta
} from "../audit/index.mjs";
import {
CLOUD_CORE_MIGRATION_ID,
CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS
} from "./schema.ts";
export const RUNTIME_STORE_KIND = "memory";
export const RUNTIME_STORE_KIND_POSTGRES = "postgres";
export const RUNTIME_DURABLE_ADAPTER_MISSING = "runtime_durable_adapter_missing";
export const RUNTIME_DURABLE_ADAPTER_UNCONFIGURED = "runtime_durable_adapter_unconfigured";
export const RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING = "runtime_durable_adapter_driver_missing";
export const RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED = "runtime_durable_adapter_ssl_blocked";
export const RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED = "runtime_durable_adapter_auth_blocked";
export const RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED = "runtime_durable_adapter_schema_blocked";
export const RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED = "runtime_durable_adapter_migration_blocked";
export const RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED = "runtime_durable_adapter_query_blocked";
export const RUNTIME_DURABILITY_REQUIRED_EVIDENCE = "runtime_adapter_schema_migration_read_query";
export const RUNTIME_ADAPTER_ENV = "HWLAB_CLOUD_RUNTIME_ADAPTER";
export const RUNTIME_DURABLE_ENV = "HWLAB_CLOUD_RUNTIME_DURABLE";
export const RUNTIME_DB_POOL_MAX_ENV = "HWLAB_CLOUD_DB_POOL_MAX";
export const requiredPostgresSchema = CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS;
export const postgresCountTables = Object.freeze([
"users",
"user_sessions",
"gateway_sessions",
"box_resources",
"box_capabilities",
"hardware_operations",
"audit_events",
"evidence_records",
"agent_sessions",
"account_workspaces",
"worker_sessions",
"agent_trace_events",
"workbench_projection_state",
"workbench_sessions",
"workbench_messages",
"workbench_parts",
"workbench_turns",
"workbench_trace_events",
"workbench_event_sequences",
"workbench_events",
"workbench_session_inputs",
"workbench_projection_checkpoints"
]);
export const postgresRuntimeReadIndexes = Object.freeze([
"CREATE INDEX IF NOT EXISTS idx_agent_trace_events_trace_order ON agent_trace_events(trace_id, occurred_at, id)",
"CREATE INDEX IF NOT EXISTS idx_agent_trace_events_session_order ON agent_trace_events(agent_session_id, occurred_at, id)",
"CREATE INDEX IF NOT EXISTS idx_agent_trace_events_worker_order ON agent_trace_events(worker_session_id, occurred_at, id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_projection_state_status_retry_updated ON workbench_projection_state(projection_status, next_retry_at, updated_at)",
"CREATE INDEX IF NOT EXISTS idx_workbench_projection_state_run_command ON workbench_projection_state(run_id, command_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_projection_state_session_updated ON workbench_projection_state(session_id, updated_at DESC)",
"CREATE INDEX IF NOT EXISTS idx_workbench_sessions_updated ON workbench_sessions(updated_at DESC, session_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_sessions_owner_updated ON workbench_sessions(owner_user_id, updated_at DESC, session_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_sessions_status_updated ON workbench_sessions(status, updated_at DESC, session_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_messages_session_updated ON workbench_messages(session_id, updated_at ASC, message_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_messages_trace ON workbench_messages(trace_id, message_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_messages_turn ON workbench_messages(turn_id, message_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_parts_message_index ON workbench_parts(message_id, part_index, part_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_parts_session_trace ON workbench_parts(session_id, trace_id, part_index)",
"CREATE INDEX IF NOT EXISTS idx_workbench_turns_session_updated ON workbench_turns(session_id, updated_at DESC, turn_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_turns_trace ON workbench_turns(trace_id, turn_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_trace_events_trace_projected_seq ON workbench_trace_events(trace_id, projected_seq)",
"CREATE INDEX IF NOT EXISTS idx_workbench_trace_events_session_projected_seq ON workbench_trace_events(session_id, projected_seq)",
"CREATE INDEX IF NOT EXISTS idx_workbench_event_sequences_session ON workbench_event_sequences(session_id, updated_at DESC, aggregate_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_event_sequences_trace ON workbench_event_sequences(trace_id, updated_at DESC, aggregate_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_events_trace_seq ON workbench_events(trace_id, event_seq)",
"CREATE INDEX IF NOT EXISTS idx_workbench_events_session_seq ON workbench_events(session_id, event_seq)",
"CREATE INDEX IF NOT EXISTS idx_workbench_events_type_seq ON workbench_events(event_type, event_seq)",
"CREATE INDEX IF NOT EXISTS idx_workbench_session_inputs_session_seq ON workbench_session_inputs(session_id, admitted_seq, input_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_session_inputs_trace ON workbench_session_inputs(trace_id, input_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_session_inputs_command ON workbench_session_inputs(command_id, input_id) WHERE command_id IS NOT NULL",
"CREATE INDEX IF NOT EXISTS idx_workbench_projection_checkpoints_status_updated ON workbench_projection_checkpoints(projection_status, updated_at DESC, trace_id)",
"CREATE INDEX IF NOT EXISTS idx_workbench_projection_checkpoints_session_updated ON workbench_projection_checkpoints(session_id, updated_at DESC, trace_id)"
]);
export function buildPostgresPoolConfig({ dbUrl, sslMode, timeoutMs, poolMax, queryTimeoutMs } = {}) {
const normalizedSslMode = normalizePostgresSslMode(sslMode ?? postgresSslModeFromConnectionString(dbUrl));
const normalizedPoolMax = normalizeRuntimePoolMax(poolMax);
const normalizedQueryTimeoutMs = queryTimeoutMs === undefined || queryTimeoutMs === null || String(queryTimeoutMs).trim() === ""
? null
: normalizeRuntimeTimeoutMs(queryTimeoutMs);
return {
connectionString: normalizePostgresConnectionStringForSslMode(dbUrl, normalizedSslMode),
ssl: postgresSslOption(normalizedSslMode),
connectionTimeoutMillis: normalizeRuntimeTimeoutMs(timeoutMs),
...(normalizedQueryTimeoutMs !== null ? { query_timeout: normalizedQueryTimeoutMs, statement_timeout: normalizedQueryTimeoutMs } : {}),
...(normalizedPoolMax !== null ? { max: normalizedPoolMax } : {})
};
}
export const RUNTIME_DB_QUERY_TIMEOUT_MS_ENV = "HWLAB_CLOUD_DB_QUERY_TIMEOUT_MS";
export const RUNTIME_DB_QUERY_RETRY_MAX_ATTEMPTS_ENV = "HWLAB_CLOUD_DB_QUERY_RETRY_MAX_ATTEMPTS";
export const RUNTIME_DB_QUERY_RETRY_INITIAL_DELAY_MS_ENV = "HWLAB_CLOUD_DB_QUERY_RETRY_INITIAL_DELAY_MS";
export const RUNTIME_DB_QUERY_RETRY_MAX_DELAY_MS_ENV = "HWLAB_CLOUD_DB_QUERY_RETRY_MAX_DELAY_MS";
export const RUNTIME_DB_POOL_RESET_COOLDOWN_MS_ENV = "HWLAB_CLOUD_DB_POOL_RESET_COOLDOWN_MS";
export const DEFAULT_RUNTIME_DB_QUERY_RETRY_MAX_ATTEMPTS = 5;
export const DEFAULT_RUNTIME_DB_QUERY_RETRY_INITIAL_DELAY_MS = 250;
export const DEFAULT_RUNTIME_DB_QUERY_RETRY_MAX_DELAY_MS = 5_000;
export const DEFAULT_RUNTIME_DB_POOL_RESET_COOLDOWN_MS = 10_000;
export function normalizePositiveInteger(value, fallback) {
const number = Number(value);
return Number.isFinite(number) && number > 0 ? Math.trunc(number) : fallback;
}
export function delayRuntimeDbQueryRetry(ms) {
return new Promise((resolve) => setTimeout(resolve, Math.max(0, Math.trunc(Number(ms) || 0))));
}
export function isReadyForDurableRuntimeRead(readiness = {}) {
return readiness.ready === true && readiness.durable === true && readiness.liveRuntimeEvidence === true;
}
export function normalizeCapabilityInputs(params) {
const value = params.capabilities ?? params.capability ?? params;
if (Array.isArray(value)) {
if (value.length === 0) {
throw new HwlabProtocolError("capabilities must not be empty", {
code: ERROR_CODES.invalidParams
});
}
return value.map((item) => asObject(item, "capability"));
}
return [asObject(value, "capability")];
}
export function normalizeShellInput(params) {
const input = normalizeJsonObject(params.input);
const command = params.command ?? input.command;
if (command === undefined || command === null || command === "") {
throw new HwlabProtocolError("hardware.invoke.shell requires input.command or command", {
code: ERROR_CODES.invalidParams,
data: {
required: ["projectId", "gatewaySessionId", "resourceId", "capabilityId", "input.command"]
}
});
}
if (Array.isArray(command)) {
return {
command: command.map((item) => String(item)),
cwd: input.cwd,
timeoutMs: input.timeoutMs
};
}
return {
command: String(command),
cwd: input.cwd,
timeoutMs: input.timeoutMs
};
}
export function normalizeAgentTraceEvent(value, requestMeta = {}, now) {
const input = normalizeJsonObject(value);
const traceId = textOr(input.traceId ?? requestMeta.traceId, "");
if (!traceId) {
throw new HwlabProtocolError("agent trace event requires traceId", {
code: ERROR_CODES.invalidParams,
data: { required: ["traceId"] }
});
}
const seq = positiveIntegerOrNull(input.seq);
const occurredAt = timestampOr(input.occurredAt ?? input.createdAt ?? input.timestamp, now);
const status = textOr(input.status, "observed");
const type = textOr(input.type ?? input.kind, "event");
const level = traceEventLevel(input.level ?? status ?? type);
const message = textOr(input.message ?? input.label ?? type, "");
const agentSessionId = textOr(input.agentSessionId ?? input.sessionId ?? requestMeta.agentSessionId ?? requestMeta.sessionId, "");
const workerSessionId = textOr(input.workerSessionId ?? input.workerSession?.id ?? input.workerId, "");
const id = textOr(input.id ?? input.eventId, "") || `tev_${sha256Hex({
traceId,
seq,
source: input.source ?? null,
sourceSeq: input.sourceSeq ?? null,
type,
status,
label: input.label ?? null,
message,
occurredAt
}).slice(0, 32)}`;
const event = pruneUndefined({
...input,
traceId,
seq: seq ?? undefined,
agentSessionId: agentSessionId || undefined,
workerSessionId: workerSessionId || undefined,
level,
createdAt: timestampOr(input.createdAt ?? occurredAt, occurredAt),
valuesPrinted: false
});
return {
id,
traceId,
agentSessionId: agentSessionId || null,
workerSessionId: workerSessionId || null,
level,
message,
occurredAt,
event
};
}
export function normalizeWorkbenchProjectionState(value, requestMeta = {}, now) {
const input = normalizeJsonObject(value);
const traceId = textOr(input.traceId ?? requestMeta.traceId, "");
const runId = textOr(input.runId ?? input.sourceRunId, "");
const commandId = textOr(input.commandId ?? input.sourceCommandId, "");
if (!traceId || !runId || !commandId) {
throw new HwlabProtocolError("workbench projection state requires traceId, runId, and commandId", {
code: ERROR_CODES.invalidParams,
data: { required: ["traceId", "runId", "commandId"] }
});
}
const timestamp = timestampOr(input.updatedAt, now);
const lastAgentRunSeq = nonNegativeInteger(input.lastAgentRunSeq ?? input.lastSourceSeq);
const lastProjectedSeq = nonNegativeInteger(input.lastProjectedSeq ?? lastAgentRunSeq);
const upstreamLatestSeq = nonNegativeIntegerOrNull(input.upstreamLatestSeq ?? input.sourceLatestSeq);
return pruneUndefined({
...input,
traceId,
sessionId: textOr(input.sessionId ?? requestMeta.sessionId, "") || null,
conversationId: textOr(input.conversationId, "") || null,
threadId: textOr(input.threadId, "") || null,
ownerUserId: textOr(input.ownerUserId, "") || null,
ownerRole: textOr(input.ownerRole, "") || null,
sourceRunId: runId,
sourceCommandId: commandId,
runId,
commandId,
lastSourceSeq: lastAgentRunSeq,
lastAgentRunSeq,
lastProjectedSeq,
sourceLatestSeq: upstreamLatestSeq ?? lastAgentRunSeq,
upstreamLatestSeq: upstreamLatestSeq ?? lastAgentRunSeq,
projectionStatus: projectionStatusOr(input.projectionStatus, "projecting"),
projectionHealth: projectionHealthOr(input.projectionHealth, "healthy"),
resultSyncState: resultSyncStateOr(input.resultSyncState, "not_started"),
lastProjectedAt: timestampOr(input.lastProjectedAt, timestamp),
lastResultSyncAt: input.lastResultSyncAt ? timestampOr(input.lastResultSyncAt, timestamp) : null,
lastErrorCode: textOr(input.lastErrorCode, "") || null,
lastErrorMessage: textOr(input.lastErrorMessage, "") || null,
failureCount: nonNegativeInteger(input.failureCount),
nextRetryAt: input.nextRetryAt ? timestampOr(input.nextRetryAt, timestamp) : null,
createdAt: timestampOr(input.createdAt, timestamp),
updatedAt: timestamp,
valuesPrinted: false
});
}
export function normalizeWorkbenchProjectionAllocation(value, requestMeta = {}, now) {
const input = normalizeJsonObject(value);
const traceId = textOr(input.traceId ?? requestMeta.traceId, "");
const sourceEventId = textOr(input.sourceEventId ?? input.eventId, "");
if (!traceId || !sourceEventId) {
throw new HwlabProtocolError("workbench projectedSeq allocation requires traceId and sourceEventId", {
code: ERROR_CODES.invalidParams,
data: { required: ["traceId", "sourceEventId"] }
});
}
const timestamp = timestampOr(input.updatedAt ?? input.occurredAt ?? input.createdAt, now);
return pruneUndefined({
...input,
traceId,
sessionId: textOr(input.sessionId ?? requestMeta.sessionId, "") || null,
turnId: textOr(input.turnId ?? requestMeta.turnId ?? traceId, "") || null,
runId: textOr(input.runId ?? input.sourceRunId, "") || null,
commandId: textOr(input.commandId ?? input.sourceCommandId, "") || null,
sourceSeq: nonNegativeInteger(input.sourceSeq ?? input.lastSourceSeq ?? input.lastAgentRunSeq),
sourceEventId,
eventType: textOr(input.eventType ?? input.type, "event"),
createdAt: timestampOr(input.createdAt, timestamp),
updatedAt: timestamp,
valuesPrinted: false
});
}
export function normalizeWorkbenchFacts(value, requestMeta = {}, now) {
const input = normalizeJsonObject(value);
return {
inputs: arrayInput(input.inputs ?? input.input).map((item) => normalizeWorkbenchSessionInputFact(item, requestMeta, now)),
sessions: arrayInput(input.sessions ?? input.session).map((item) => normalizeWorkbenchSessionFact(item, requestMeta, now)),
messages: arrayInput(input.messages ?? input.message).map((item) => normalizeWorkbenchMessageFact(item, requestMeta, now)),
parts: arrayInput(input.parts ?? input.part).map((item) => normalizeWorkbenchPartFact(item, requestMeta, now)),
turns: arrayInput(input.turns ?? input.turn).map((item) => normalizeWorkbenchTurnFact(item, requestMeta, now)),
traceEvents: arrayInput(input.traceEvents ?? input.traceEvent).map((item) => normalizeWorkbenchTraceEventFact(item, requestMeta, now)),
checkpoints: arrayInput(input.checkpoints ?? input.checkpoint).map((item) => normalizeWorkbenchProjectionCheckpoint(item, requestMeta, now))
};
}
export function normalizeWorkbenchAggregateEventsForFacts(facts = {}, requestMeta = {}, now) {
const events = [];
for (const fact of arrayInput(facts.inputs)) appendWorkbenchFactEvent(events, "inputs", fact, requestMeta, now);
for (const fact of arrayInput(facts.sessions)) appendWorkbenchFactEvent(events, "sessions", fact, requestMeta, now);
for (const fact of arrayInput(facts.messages)) appendWorkbenchFactEvent(events, "messages", fact, requestMeta, now);
for (const fact of arrayInput(facts.parts)) appendWorkbenchFactEvent(events, "parts", fact, requestMeta, now);
for (const fact of arrayInput(facts.traceEvents)) appendWorkbenchFactEvent(events, "traceEvents", fact, requestMeta, now);
for (const fact of arrayInput(facts.checkpoints)) appendWorkbenchFactEvent(events, "checkpoints", fact, requestMeta, now);
for (const fact of arrayInput(facts.turns)) appendWorkbenchFactEvent(events, "turns", fact, requestMeta, now);
return events;
}
export function appendWorkbenchFactEvent(events, factFamily, fact, requestMeta, now) {
const aggregate = workbenchAggregateForFact(fact, requestMeta);
const factId = textOr(fact.id ?? fact.inputId ?? fact.sessionId ?? fact.messageId ?? fact.partId ?? fact.turnId ?? fact.traceId, "");
const rawSourceEventId = textOr(fact.sourceEventId ?? fact.eventId, "") || factId;
const sourceEventId = rawSourceEventId ? `${factFamily}:${rawSourceEventId}` : null;
const eventType = workbenchAggregateEventType(factFamily, fact);
const projectedSeq = nonNegativeInteger(fact.projectedSeq);
const sourceSeq = nonNegativeInteger(fact.sourceSeq);
const occurredAt = timestampOr(fact.occurredAt ?? fact.updatedAt ?? fact.createdAt, now);
events.push(normalizeWorkbenchAggregateEvent({
...aggregate,
factFamily,
factId,
factKey: `${factFamily.slice(0, -1) || factFamily}:${factId}`,
eventId: stableWorkbenchFactId("wbe", { aggregateId: aggregate.aggregateId, factFamily, sourceEventId, eventType, factId }),
messageId: textOr(fact.messageId, "") || null,
sourceRunId: textOr(fact.sourceRunId ?? fact.runId, "") || null,
sourceCommandId: textOr(fact.sourceCommandId ?? fact.commandId, "") || null,
sourceSeq,
sourceEventId,
eventType,
projectionRevision: projectedSeq || sourceSeq,
terminal: Boolean(fact.terminal),
sealed: Boolean(fact.sealed),
payload: {
factFamily,
factId,
status: textOr(fact.status ?? fact.projectionStatus, "") || null,
eventType,
projectedSeq,
sourceSeq,
originalSourceEventId: rawSourceEventId || null,
valuesRedacted: true
},
occurredAt,
committedAt: timestampOr(fact.updatedAt ?? occurredAt, now)
}, requestMeta, now));
}
export function workbenchAggregateForFact(fact = {}, requestMeta = {}) {
const sessionId = textOr(fact.sessionId ?? requestMeta.sessionId, "") || null;
const turnId = textOr(fact.turnId ?? requestMeta.turnId, "") || null;
const traceId = textOr(fact.traceId ?? requestMeta.traceId, "") || null;
if (sessionId) return { aggregateId: `session:${sessionId}`, aggregateType: "session", sessionId, turnId, traceId };
if (turnId) return { aggregateId: `turn:${turnId}`, aggregateType: "turn", sessionId, turnId, traceId };
if (traceId) return { aggregateId: `trace:${traceId}`, aggregateType: "trace", sessionId, turnId, traceId };
throw requiredWorkbenchFactError("workbench aggregate event", ["sessionId", "turnId", "traceId"]);
}
export function normalizeWorkbenchAggregateEvent(value, requestMeta = {}, now) {
const input = normalizeJsonObject(value);
const aggregateId = textOr(input.aggregateId, "");
if (!aggregateId) throw requiredWorkbenchFactError("workbench aggregate event", ["aggregateId"]);
const timestamp = timestampOr(input.committedAt ?? input.occurredAt ?? input.updatedAt ?? input.createdAt, now);
const sourceSeq = nonNegativeInteger(input.sourceSeq);
const projectionRevision = nonNegativeInteger(input.projectionRevision ?? input.projectedSeq ?? sourceSeq);
return pruneUndefined({
...input,
eventSeq: nonNegativeInteger(input.eventSeq),
eventId: textOr(input.eventId ?? input.id, "") || stableWorkbenchFactId("wbe", { aggregateId, eventType: input.eventType, sourceEventId: input.sourceEventId, sourceSeq, projectionRevision }),
aggregateId,
aggregateType: textOr(input.aggregateType, "trace"),
aggregateSeq: nonNegativeInteger(input.aggregateSeq),
sessionId: textOr(input.sessionId ?? requestMeta.sessionId, "") || null,
turnId: textOr(input.turnId ?? requestMeta.turnId, "") || null,
traceId: textOr(input.traceId ?? requestMeta.traceId, "") || null,
messageId: textOr(input.messageId ?? requestMeta.messageId, "") || null,
sourceRunId: textOr(input.sourceRunId ?? input.runId, "") || null,
sourceCommandId: textOr(input.sourceCommandId ?? input.commandId, "") || null,
sourceSeq,
sourceEventId: textOr(input.sourceEventId, "") || null,
eventType: textOr(input.eventType ?? input.type, "event"),
projectionRevision,
terminal: Boolean(input.terminal),
sealed: Boolean(input.sealed),
payload: normalizeJsonObject(input.payload),
occurredAt: timestampOr(input.occurredAt, timestamp),
committedAt: timestamp,
valuesPrinted: false
});
}
export function workbenchAggregateEventType(factFamily, fact = {}) {
if (factFamily === "inputs") return `input_${textOr(fact.delivery, "queue")}_${textOr(fact.status, "admitted")}`;
if (factFamily === "sessions") return "session_upsert";
if (factFamily === "messages") return `${textOr(fact.role, "agent")}_message`;
if (factFamily === "parts") return `part_${textOr(fact.partType ?? fact.type, "text")}`;
if (factFamily === "turns") return fact.terminal || fact.sealed ? "turn_terminal" : "turn_update";
if (factFamily === "traceEvents") return `trace_${textOr(fact.eventType ?? fact.type, "event")}`;
if (factFamily === "checkpoints") return `checkpoint_${textOr(fact.projectionStatus, "projecting")}`;
return "event";
}
export function workbenchInputDelivery(value) {
const text = textOr(value, "queue").toLowerCase();
return ["queue", "steer", "cancel"].includes(text) ? text : "queue";
}
export function workbenchInputStatus(value) {
const text = textOr(value, "admitted").toLowerCase().replace(/-/gu, "_");
return ["admitting", "admitted", "promoted", "blocked", "failed", "canceled", "completed"].includes(text) ? text : "admitted";
}
export function indexWorkbenchAggregateEvents(events = []) {
const index = new Map();
for (const event of events) {
if (event?.factKey) index.set(event.factKey, event);
if (event?.factFamily && event?.factId) index.set(`${event.factFamily}:${event.factId}`, event);
if (event?.factFamily === "inputs" && event?.factId) index.set(`input:${event.factId}`, event);
if (event?.factFamily === "turns" && event?.turnId) index.set(`turn:${event.turnId}`, event);
if (event?.factFamily === "traceEvents" && event?.factId) index.set(`traceEvent:${event.factId}`, event);
if (event?.sourceEventId) index.set(`sourceEvent:${event.sourceEventId}`, event);
}
return index;
}
export function normalizeWorkbenchSessionInputFact(value, requestMeta = {}, now) {
const input = normalizeJsonObject(value);
const sessionId = textOr(input.sessionId ?? requestMeta.sessionId, "");
if (!sessionId) throw requiredWorkbenchFactError("workbench session input fact", ["sessionId"]);
const timestamp = timestampOr(input.updatedAt ?? input.createdAt, now);
const turnId = textOr(input.turnId ?? requestMeta.turnId ?? input.traceId ?? requestMeta.traceId, "") || null;
const traceId = textOr(input.traceId ?? requestMeta.traceId, "") || null;
const messageId = textOr(input.messageId ?? requestMeta.messageId, "") || null;
const commandId = textOr(input.commandId ?? input.sourceCommandId, "") || null;
const delivery = workbenchInputDelivery(input.delivery);
const status = workbenchInputStatus(input.status);
const sourceEventId = textOr(input.sourceEventId ?? input.eventId, "") || `${sessionId}:${turnId ?? "turn-unassigned"}:${delivery}:${traceId ?? commandId ?? messageId ?? "input"}`;
const inputId = textOr(input.inputId ?? input.id, "") || stableWorkbenchFactId("wsi", { sessionId, turnId, traceId, messageId, commandId, delivery, sourceEventId });
const admittedSeq = nonNegativeInteger(input.admittedSeq ?? input.aggregateSeq ?? input.sourceSeq);
const promotedSeq = workbenchSessionInputPromotedSeq({ ...input, status }, admittedSeq);
return pruneUndefined({
...input,
id: inputId,
inputId,
sessionId,
turnId,
traceId,
messageId,
commandId,
delivery,
admittedSeq,
promotedSeq,
sourceSeq: admittedSeq,
projectedSeq: admittedSeq,
status,
errorCode: textOr(input.errorCode ?? input.error?.code, "") || null,
sourceEventId,
createdAt: timestampOr(input.createdAt, timestamp),
updatedAt: timestamp,
valuesPrinted: false,
valuesRedacted: true
});
}
export function memoryWorkbenchSessionInputWithSeq(fact, records) {
const existingFactSeq = nonNegativeInteger(fact.admittedSeq);
if (existingFactSeq > 0) return workbenchSessionInputWithSeq(fact, existingFactSeq);
const existing = records.get(fact.inputId) ?? null;
const existingSeq = nonNegativeInteger(existing?.admittedSeq);
if (existingSeq > 0) return workbenchSessionInputWithSeq(fact, existingSeq);
const nextSeq = [...records.values()]
.filter((record) => record.sessionId === fact.sessionId)
.reduce((max, record) => Math.max(max, nonNegativeInteger(record.admittedSeq)), 0) + 1;
return workbenchSessionInputWithSeq(fact, nextSeq);
}
export function inputFactWithAggregateSeq(fact, eventIndex) {
const existingFactSeq = nonNegativeInteger(fact.admittedSeq);
if (existingFactSeq > 0) return workbenchSessionInputWithSeq(fact, existingFactSeq);
const event = eventIndex.get(`input:${fact.inputId}`) ?? eventIndex.get(`sourceEvent:${fact.sourceEventId}`) ?? null;
const admittedSeq = nonNegativeInteger(event?.aggregateSeq);
return admittedSeq > 0 ? workbenchSessionInputWithSeq(fact, admittedSeq) : fact;
}
export function workbenchSessionInputWithSeq(fact, admittedSeq) {
return {
...fact,
admittedSeq,
promotedSeq: workbenchSessionInputPromotedSeq(fact, admittedSeq),
sourceSeq: admittedSeq,
projectedSeq: admittedSeq
};
}
export function workbenchSessionInputPromotedSeq(fact, admittedSeq) {
const existing = nonNegativeIntegerOrNull(fact.promotedSeq);
if (existing !== null && existing > 0) return existing;
const seq = nonNegativeInteger(admittedSeq);
return textOr(fact.status, "") === "promoted" && seq > 0 ? seq : existing;
}
export function normalizeWorkbenchSessionFact(value, requestMeta = {}, now) {
const input = normalizeJsonObject(value);
const sessionId = textOr(input.sessionId ?? input.id ?? requestMeta.sessionId, "");
if (!sessionId) throw requiredWorkbenchFactError("workbench session fact", ["sessionId"]);
const sourceSeq = nonNegativeInteger(input.sourceSeq);
const projectedSeq = nonNegativeInteger(input.projectedSeq ?? sourceSeq);
const timestamp = timestampOr(input.updatedAt ?? input.createdAt, now);
return pruneUndefined({
...input,
id: sessionId,
sessionId,
ownerUserId: textOr(input.ownerUserId ?? requestMeta.ownerUserId, "") || null,
projectId: textOr(input.projectId ?? requestMeta.projectId, "") || null,
conversationId: textOr(input.conversationId ?? requestMeta.conversationId, "") || null,
threadId: textOr(input.threadId ?? requestMeta.threadId, "") || null,
status: textOr(input.status, "unknown"),
lastTraceId: textOr(input.lastTraceId ?? input.traceId ?? requestMeta.traceId, "") || null,
projectedSeq,
sourceSeq,
sourceEventId: textOr(input.sourceEventId ?? input.eventId, "") || null,
terminal: Boolean(input.terminal),
sealed: Boolean(input.sealed),
createdAt: timestampOr(input.createdAt, timestamp),
updatedAt: timestamp,
valuesPrinted: false
});
}
export function normalizeWorkbenchMessageFact(value, requestMeta = {}, now) {
const input = normalizeJsonObject(value);
const sessionId = textOr(input.sessionId ?? requestMeta.sessionId, "");
if (!sessionId) throw requiredWorkbenchFactError("workbench message fact", ["sessionId"]);
const traceId = textOr(input.traceId ?? requestMeta.traceId, "") || null;
const turnId = textOr(input.turnId ?? requestMeta.turnId ?? traceId, "") || null;
const role = textOr(input.role, "agent");
const sourceSeq = nonNegativeInteger(input.sourceSeq);
const projectedSeq = nonNegativeInteger(input.projectedSeq ?? sourceSeq);
const timestamp = timestampOr(input.updatedAt ?? input.createdAt, now);
const messageId = textOr(input.messageId ?? input.id, "") || stableWorkbenchFactId("msg", { sessionId, turnId, traceId, role, sourceSeq, projectedSeq });
return pruneUndefined({
...input,
id: messageId,
messageId,
sessionId,
turnId,
traceId,
role,
status: textOr(input.status, "unknown"),
projectedSeq,
sourceSeq,
sourceEventId: textOr(input.sourceEventId ?? input.eventId, "") || null,
terminal: Boolean(input.terminal),
sealed: Boolean(input.sealed),
createdAt: timestampOr(input.createdAt, timestamp),
updatedAt: timestamp,
valuesPrinted: false
});
}
export function normalizeWorkbenchPartFact(value, requestMeta = {}, now) {
const input = normalizeJsonObject(value);
const sessionId = textOr(input.sessionId ?? requestMeta.sessionId, "");
const messageId = textOr(input.messageId ?? requestMeta.messageId, "");
if (!sessionId || !messageId) throw requiredWorkbenchFactError("workbench part fact", ["sessionId", "messageId"]);
const traceId = textOr(input.traceId ?? requestMeta.traceId, "") || null;
const turnId = textOr(input.turnId ?? requestMeta.turnId ?? traceId, "") || null;
const partIndex = nonNegativeInteger(input.partIndex ?? input.index);
const sourceSeq = nonNegativeInteger(input.sourceSeq);
const projectedSeq = nonNegativeInteger(input.projectedSeq ?? sourceSeq);
const timestamp = timestampOr(input.updatedAt ?? input.createdAt, now);
const partType = textOr(input.partType ?? input.type, "text");
const partId = textOr(input.partId ?? input.id, "") || stableWorkbenchFactId("wpt", { sessionId, messageId, turnId, traceId, partIndex, partType });
return pruneUndefined({
...input,
id: partId,
partId,
messageId,
sessionId,
turnId,
traceId,
partIndex,
partType,
status: textOr(input.status, "unknown"),
projectedSeq,
sourceSeq,
sourceEventId: textOr(input.sourceEventId ?? input.eventId, "") || null,
terminal: Boolean(input.terminal),
sealed: Boolean(input.sealed),
createdAt: timestampOr(input.createdAt, timestamp),
updatedAt: timestamp,
valuesPrinted: false
});
}
export function normalizeWorkbenchTurnFact(value, requestMeta = {}, now) {
const input = normalizeJsonObject(value);
const sessionId = textOr(input.sessionId ?? requestMeta.sessionId, "");
const traceId = textOr(input.traceId ?? requestMeta.traceId, "") || null;
const turnId = textOr(input.turnId ?? input.id ?? requestMeta.turnId ?? traceId, "");
if (!sessionId || !turnId) throw requiredWorkbenchFactError("workbench turn fact", ["sessionId", "turnId"]);
const sourceSeq = nonNegativeInteger(input.sourceSeq);
const projectedSeq = nonNegativeInteger(input.projectedSeq ?? sourceSeq);
const timestamp = timestampOr(input.updatedAt ?? input.createdAt, now);
const status = textOr(input.status, "unknown");
const terminal = Boolean(input.terminal);
const sealed = Boolean(input.sealed);
const finalResponse = input.finalResponse ?? null;
assertWorkbenchTurnFinalResponseInvariant({ status, terminal, sealed, finalResponse });
return pruneUndefined({
...input,
id: turnId,
turnId,
sessionId,
traceId,
messageId: textOr(input.messageId ?? requestMeta.messageId, "") || null,
status,
projectedSeq,
sourceSeq,
sourceEventId: textOr(input.sourceEventId ?? input.eventId, "") || null,
terminal,
sealed,
finalResponse,
diagnostic: normalizeJsonObject(input.diagnostic ?? input.projection ?? input.blocker),
createdAt: timestampOr(input.createdAt, timestamp),
updatedAt: timestamp,
valuesPrinted: false
});
}
export function assertWorkbenchTurnFinalResponseInvariant({ status, terminal, sealed, finalResponse } = {}) {
if (normalizeWorkbenchFactStatus(status) !== "completed") return;
if (!terminal && !sealed) return;
if (workbenchFactFinalResponseText(finalResponse)) return;
const error = new Error("completed Workbench turn facts must include an authoritative final response before sealing");
error.code = "workbench_terminal_final_response_missing";
error.layer = "workbench-read-model";
error.category = "projection-invariant";
throw error;
}
export function normalizeWorkbenchFactStatus(value) {
return textOr(value, "").toLowerCase().replace(/_/gu, "-");
}
export function workbenchFactFinalResponseText(value) {
if (value && typeof value === "object" && !Array.isArray(value)) {
return workbenchFactFinalResponseText(value.text ?? value.content ?? value.message ?? value.summary ?? value.preview);
}
const text = textOr(value, "");
return text && text !== "[object Object]" ? text : null;
}
export function normalizeWorkbenchTraceEventFact(value, requestMeta = {}, now) {
const input = normalizeJsonObject(value);
const traceId = textOr(input.traceId ?? requestMeta.traceId, "");
if (!traceId) throw requiredWorkbenchFactError("workbench trace event fact", ["traceId"]);
const sessionId = textOr(input.sessionId ?? requestMeta.sessionId, "") || null;
const turnId = textOr(input.turnId ?? requestMeta.turnId ?? traceId, "") || null;
const sourceSeq = nonNegativeInteger(input.sourceSeq);
const projectedSeq = nonNegativeInteger(input.projectedSeq ?? input.seq);
const occurredAt = timestampOr(input.occurredAt ?? input.createdAt ?? input.timestamp, now);
const eventType = textOr(input.eventType ?? input.type ?? input.kind, "event");
const id = textOr(input.id ?? input.eventId, "") || stableWorkbenchFactId("wte", { traceId, sourceSeq, projectedSeq, eventType, sourceEventId: input.sourceEventId ?? null, occurredAt });
return pruneUndefined({
...input,
id,
traceId,
sessionId,
turnId,
messageId: textOr(input.messageId ?? requestMeta.messageId, "") || null,
sourceSeq,
sourceEventId: textOr(input.sourceEventId ?? input.eventId, "") || null,
projectedSeq,
eventType,
terminal: Boolean(input.terminal),
sealed: Boolean(input.sealed),
occurredAt,
updatedAt: timestampOr(input.updatedAt, occurredAt),
valuesPrinted: false
});
}
export function normalizeWorkbenchProjectionCheckpoint(value, requestMeta = {}, now) {
const input = normalizeJsonObject(value);
const traceId = textOr(input.traceId ?? requestMeta.traceId, "");
if (!traceId) throw requiredWorkbenchFactError("workbench projection checkpoint", ["traceId"]);
const sourceSeq = nonNegativeInteger(input.sourceSeq ?? input.lastSourceSeq ?? input.lastAgentRunSeq);
const projectedSeq = nonNegativeInteger(input.projectedSeq ?? input.lastProjectedSeq ?? sourceSeq);
const timestamp = timestampOr(input.updatedAt ?? input.lastProjectedAt, now);
return pruneUndefined({
...input,
id: traceId,
traceId,
sessionId: textOr(input.sessionId ?? requestMeta.sessionId, "") || null,
turnId: textOr(input.turnId ?? requestMeta.turnId ?? traceId, "") || null,
runId: textOr(input.runId ?? input.sourceRunId, "") || null,
commandId: textOr(input.commandId ?? input.sourceCommandId, "") || null,
projectedSeq,
sourceSeq,
sourceEventId: textOr(input.sourceEventId ?? input.eventId, "") || null,
projectionStatus: projectionStatusOr(input.projectionStatus, "projecting"),
projectionHealth: projectionHealthOr(input.projectionHealth, "healthy"),
terminal: Boolean(input.terminal),
sealed: Boolean(input.sealed),
diagnostic: normalizeJsonObject(input.diagnostic ?? input.blocker),
createdAt: timestampOr(input.createdAt, timestamp),
updatedAt: timestamp,
valuesPrinted: false
});
}
export function arrayInput(value) {
if (Array.isArray(value)) return value;
if (value && typeof value === "object") return [value];
return [];
}
export function stableWorkbenchFactId(prefix, value) {
return `${prefix}_${sha256Hex(value).slice(0, 32)}`;
}
export function requiredWorkbenchFactError(label, fields) {
return new HwlabProtocolError(`${label} requires ${fields.join(", ")}`, {
code: ERROR_CODES.invalidParams,
data: { required: fields }
});
}
export function compareTraceEventRecords(left, right) {
const leftSeq = positiveIntegerOrNull(left?.event?.seq);
const rightSeq = positiveIntegerOrNull(right?.event?.seq);
if (leftSeq && rightSeq && leftSeq !== rightSeq) return leftSeq - rightSeq;
return String(left?.occurredAt ?? "").localeCompare(String(right?.occurredAt ?? "")) ||
String(left?.id ?? "").localeCompare(String(right?.id ?? ""));
}
export function compareProjectionStateRecords(left, right) {
return String(left?.updatedAt ?? "").localeCompare(String(right?.updatedAt ?? "")) ||
String(left?.traceId ?? "").localeCompare(String(right?.traceId ?? ""));
}
export function compareWorkbenchFactRecords(left, right) {
return String(left?.updatedAt ?? left?.createdAt ?? "").localeCompare(String(right?.updatedAt ?? right?.createdAt ?? "")) ||
String(left?.id ?? left?.sessionId ?? left?.messageId ?? left?.turnId ?? left?.traceId ?? "").localeCompare(String(right?.id ?? right?.sessionId ?? right?.messageId ?? right?.turnId ?? right?.traceId ?? ""));
}
export function compareWorkbenchInputFactRecords(left, right) {
return nonNegativeInteger(left?.admittedSeq) - nonNegativeInteger(right?.admittedSeq) ||
String(left?.inputId ?? left?.id ?? "").localeCompare(String(right?.inputId ?? right?.id ?? ""));
}
export function compareWorkbenchPartFactRecords(left, right) {
return String(left?.messageId ?? "").localeCompare(String(right?.messageId ?? "")) ||
nonNegativeInteger(left?.partIndex) - nonNegativeInteger(right?.partIndex) ||
compareWorkbenchFactRecords(left, right);
}
export function compareWorkbenchTraceEventFacts(left, right) {
return nonNegativeInteger(left?.projectedSeq) - nonNegativeInteger(right?.projectedSeq);
}
export function sortWorkbenchFactRows(records, params = {}, family, fallbackCompare) {
const sorted = [...records];
if (workbenchFactOrder(params, family) === "updated_desc") {
return sorted.sort((left, right) => String(right?.updatedAt ?? right?.createdAt ?? "").localeCompare(String(left?.updatedAt ?? left?.createdAt ?? "")) || fallbackCompare(left, right));
}
if (family === "inputs") return sorted.sort(compareWorkbenchInputFactRecords);
return sorted.sort(fallbackCompare);
}
export const WORKBENCH_FACT_FAMILIES = Object.freeze(["inputs", "sessions", "messages", "parts", "turns", "traceEvents", "checkpoints"]);
export function workbenchFactFamilySet(params = {}) {
const raw = params.families ?? params.factFamilies;
if (raw === undefined || raw === null || raw === "") return new Set(WORKBENCH_FACT_FAMILIES);
const values = Array.isArray(raw) ? raw : String(raw).split(",");
const selected = values.map((value) => String(value ?? "").trim()).filter((value) => WORKBENCH_FACT_FAMILIES.includes(value));
return new Set(selected.length > 0 ? selected : WORKBENCH_FACT_FAMILIES);
}
export function workbenchFactFamilyForTable(table) {
if (table === "workbench_session_inputs") return "inputs";
if (table === "workbench_sessions") return "sessions";
if (table === "workbench_messages") return "messages";
if (table === "workbench_parts") return "parts";
if (table === "workbench_turns") return "turns";
if (table === "workbench_trace_events") return "traceEvents";
if (table === "workbench_projection_checkpoints") return "checkpoints";
return "unknown";
}
export function workbenchSessionSummarySelectClause() {
return [
"session_id",
"owner_user_id",
"project_id",
"conversation_id",
"thread_id",
"status",
"last_trace_id",
"projected_seq",
"source_seq",
"source_event_id",
"terminal",
"sealed",
"created_at",
"updated_at",
"COALESCE(NULLIF(session_json::jsonb ->> 'providerProfile', ''), NULLIF(session_json::jsonb #>> '{sessionJson,providerProfile}', '')) AS provider_profile"
].join(", ");
}
export function workbenchSessionSummaryFactFromRow(row = {}) {
const sessionId = textOr(row.session_id, "");
if (!sessionId) return null;
const providerProfile = textOr(row.provider_profile, "");
return {
id: sessionId,
sessionId,
ownerUserId: textOr(row.owner_user_id, null),
projectId: textOr(row.project_id, null),
conversationId: textOr(row.conversation_id, null),
threadId: textOr(row.thread_id, null),
status: textOr(row.status, "unknown"),
lastTraceId: textOr(row.last_trace_id, null),
projectedSeq: nonNegativeInteger(row.projected_seq),
sourceSeq: nonNegativeInteger(row.source_seq),
sourceEventId: textOr(row.source_event_id, null),
terminal: row.terminal === true || row.terminal === "t",
sealed: row.sealed === true || row.sealed === "t",
...(providerProfile ? {
providerProfile,
sessionJson: {
providerProfile,
valuesRedacted: true,
secretMaterialStored: false
}
} : {}),
createdAt: textOr(row.created_at, null),
updatedAt: textOr(row.updated_at, row.created_at ?? null),
valuesPrinted: false,
valuesRedacted: true
};
}
export function workbenchFactOrder(params = {}, family = "") {
const value = params[`${family}Order`] ?? params.order;
return value === "updated_desc" ? "updated_desc" : "updated_asc";
}
export function traceEventLevel(value) {
const text = textOr(value, "info").toLowerCase();
if (["failed", "failure", "error", "timeout"].includes(text)) return "error";
if (["warn", "warning", "blocked", "canceled", "cancelled"].includes(text)) return "warn";
if (["debug", "trace"].includes(text)) return "debug";
return "info";
}
export function positiveIntegerOrNull(value) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
}
export function nonNegativeInteger(value) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed >= 0 ? parsed : 0;
}
export function nonNegativeIntegerOrNull(value) {
const parsed = Number.parseInt(value ?? "", 10);
return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
}
export function projectionStatusOr(value, fallback) {
const text = textOr(value, fallback).toLowerCase();
return ["projecting", "caught_up", "terminal", "degraded", "blocked", "stalled"].includes(text) ? text : fallback;
}
export function projectionHealthOr(value, fallback) {
const text = textOr(value, fallback).toLowerCase();
return ["healthy", "degraded", "unavailable", "stalled"].includes(text) ? text : fallback;
}
export function resultSyncStateOr(value, fallback) {
const text = textOr(value, fallback).toLowerCase();
return ["not_started", "pending", "synced", "failed", "timed_out"].includes(text) ? text : fallback;
}
export function timestampOr(value, fallback) {
const text = textOr(value, "");
if (text) {
const ms = Date.parse(text);
if (Number.isFinite(ms)) return new Date(ms).toISOString();
}
return textOr(fallback, "") || new Date().toISOString();
}
export function textOr(value, fallback) {
const text = String(value ?? "").trim();
return text || fallback;
}
export function textList(value) {
const raw = Array.isArray(value) ? value : [];
return [...new Set(raw.map((item) => textOr(item, "")).filter(Boolean))];
}
export function asObject(value, label) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new HwlabProtocolError(`${label} must be a JSON object`, {
code: ERROR_CODES.invalidParams
});
}
return value;
}
export function requireString(input, field) {
if (typeof input[field] !== "string" || input[field].trim() === "") {
throw new HwlabProtocolError(`${field} is required`, {
code: ERROR_CODES.invalidParams,
data: { field }
});
}
return input[field];
}
export function requireProtocolId(input, field) {
const value = requireString(input, field);
return value;
}
export function normalizeJsonObject(value) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return {};
}
return { ...value };
}
export function pruneUndefined(input) {
return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined));
}
export function makeId(prefix) {
return `${prefix}_${randomUUID()}`;
}
export function stableJson(value) {
return JSON.stringify(sortJson(value));
}
export function sha256Hex(value) {
return createHash("sha256").update(stableJson(value)).digest("hex");
}
export function sortJson(value) {
if (Array.isArray(value)) {
return value.map(sortJson);
}
if (value && typeof value === "object") {
return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortJson(value[key])]));
}
return value;
}
export function matchesQuery(record, params, fields) {
for (const field of fields) {
if (params[field] !== undefined && params[field] !== null && record[field] !== params[field]) {
return false;
}
const values = textList(params[`${field}s`]);
if (values.length > 0 && !values.includes(textOr(record[field], ""))) {
return false;
}
}
return true;
}
export function matchesWorkbenchSessionFactQuery(record, params) {
if (!matchesQuery(record, params, ["sessionId", "ownerUserId", "projectId", "conversationId", "threadId", "status"])) return false;
const traceId = textOr(params.traceId ?? params.lastTraceId, "");
if (traceId && record.lastTraceId !== traceId && record.traceId !== traceId) return false;
return true;
}
export function limitResults(records, limit) {
const parsed = Number.parseInt(limit ?? "", 10);
if (!Number.isInteger(parsed) || parsed <= 0) {
return records;
}
return records.slice(0, parsed);
}
export function normalizeRuntimeAdapter(value) {
if (typeof value !== "string" || value.trim() === "") {
return RUNTIME_STORE_KIND;
}
const normalized = value.trim().toLowerCase();
if (["postgres", "postgresql", "pg", "db"].includes(normalized)) {
return RUNTIME_STORE_KIND_POSTGRES;
}
return RUNTIME_STORE_KIND;
}
export function normalizePostgresSslMode(value) {
const normalized = String(value ?? "").trim().toLowerCase();
if (["disable", "false", "0", "off", "no"].includes(normalized)) {
return "disable";
}
return "require";
}
export function postgresSslModeFromConnectionString(dbUrl) {
if (typeof dbUrl !== "string" || dbUrl.trim() === "") return undefined;
try {
const url = new URL(dbUrl);
if (!["postgres:", "postgresql:"].includes(url.protocol)) return undefined;
return url.searchParams.get("sslmode") ?? url.searchParams.get("ssl") ?? undefined;
} catch {
return undefined;
}
}
export function postgresSslOption(sslMode) {
return sslMode === "disable" ? false : { rejectUnauthorized: false };
}
export function normalizePostgresConnectionStringForSslMode(dbUrl, sslMode) {
if (typeof dbUrl !== "string" || dbUrl.trim() === "") {
return dbUrl;
}
let url;
try {
url = new URL(dbUrl);
} catch {
return dbUrl;
}
if (!["postgres:", "postgresql:"].includes(url.protocol)) {
return dbUrl;
}
for (const key of ["ssl", "sslmode", "sslcert", "sslkey", "sslrootcert"]) {
url.searchParams.delete(key);
}
if (sslMode !== "disable") {
url.searchParams.set("sslmode", "require");
url.searchParams.set("uselibpqcompat", "true");
}
return url.toString();
}
export function postgresAdapterContract() {
return {
required: "Postgres-backed durable runtime adapter using HWLAB_CLOUD_DB_URL",
adapterEnv: RUNTIME_ADAPTER_ENV,
adapterEnvValue: RUNTIME_STORE_KIND_POSTGRES,
sourceIssue: "pikasTech/HWLAB#164",
secretMaterialRequiredInSource: false,
fixtureEvidenceAllowed: false
};
}
export function addRuntimeDurabilityContract(summary) {
return {
...summary,
durabilityContract: {
ready: summary.durable === true && summary.ready === true && summary.liveRuntimeEvidence === true,
status: summary.durable === true && summary.ready === true && summary.liveRuntimeEvidence === true ? "ready" : "blocked",
requiredEvidence: RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
dbLiveEvidenceIsDurabilityEvidence: false,
liveRuntimeEvidence: Boolean(summary.liveRuntimeEvidence),
adapterQueryRequired: true,
blockedLayer: summary.durable === true && summary.ready === true && summary.liveRuntimeEvidence === true
? null
: durabilityBlockedLayer(summary),
blocker: summary.blocker ?? null,
secretMaterialRead: false
}
};
}
export function durabilityBlockedLayer(summary = {}) {
const blocker = summary.blocker;
if (
blocker === RUNTIME_DURABLE_ADAPTER_MISSING ||
blocker === RUNTIME_DURABLE_ADAPTER_UNCONFIGURED ||
blocker === RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING
) {
return "adapter";
}
if (blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED) {
return "ssl";
}
if (blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED) {
return "auth";
}
if (blocker === RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED) {
return "schema";
}
if (blocker === RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED) {
return "migration";
}
if (summary.connection?.queryAttempted || blocker === RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED) {
return "durability_query";
}
return "adapter";
}
export function runtimeSafety() {
return {
devOnly: true,
secretsRead: false,
secretMaterialRead: false,
valuesRedacted: true,
endpointRedacted: true
};
}
export function classifyRuntimeDbError(error) {
const code = typeof error?.code === "string" ? error.code : "UNKNOWN";
if (isRuntimeDbConnectTimeout(error)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
reason: "Postgres runtime adapter connection attempt timed out before completing the runtime query",
retryable: true,
transient: true,
retryAfterMs: 5000,
connection: {
queryAttempted: true,
queryResult: "connect_timeout",
errorCode: code
}
};
}
if (code === "HWLAB_PG_DRIVER_MISSING") {
return {
blocker: RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING,
reason: "Postgres runtime adapter is selected, but the pg package is not installed in the runtime image",
connection: {
queryAttempted: false,
queryResult: "driver_missing",
errorCode: code
}
};
}
if (isRuntimeDbSslError(error)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
reason: "Postgres runtime adapter reached the DB driver but SSL negotiation or SSL mode compatibility blocked the schema query",
connection: {
queryAttempted: true,
queryResult: "ssl_negotiation_blocked",
errorCode: code
}
};
}
if (["28P01", "28000", "42501"].includes(code)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
reason: "Postgres runtime adapter reached the DB driver but authentication or authorization blocked the schema query",
connection: {
queryAttempted: true,
queryResult: "auth_blocked",
errorCode: code
}
};
}
if (["42P01", "42703", "3F000"].includes(code)) {
return {
blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
reason: "Postgres runtime adapter reached the database, but the runtime schema is missing or incompatible",
connection: {
queryAttempted: true,
queryResult: "schema_blocked",
errorCode: code
}
};
}
if (code === "53300") {
return {
blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
reason: "Postgres runtime adapter reached the database, but the server has exhausted available connection slots",
retryable: true,
transient: true,
retryAfterMs: 2000,
connection: {
queryAttempted: true,
queryResult: "too_many_connections",
errorCode: code
}
};
}
return {
blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
reason: "Postgres runtime adapter could not complete a live runtime schema query",
connection: {
queryAttempted: true,
queryResult: "query_blocked",
errorCode: code
}
};
}
export function isRuntimeDbConnectTimeout(error) {
const message = String(error?.message ?? error ?? "").toLowerCase();
const pgConnectTimeout = message.includes("timeout exceeded when trying to connect");
const pgConnectionTimeoutTermination = message.includes("connection terminated due to connection timeout");
if (!pgConnectTimeout && !pgConnectionTimeoutTermination) return false;
if (pgConnectionTimeoutTermination) return true;
const stack = String(error?.stack ?? "").toLowerCase();
return !stack || stack.includes("pg-pool") || stack.includes("node_modules/pg");
}
export function durableRuntimeReadBlockedError(method, readiness = {}) {
return new HwlabProtocolError("Postgres durable runtime adapter is blocked for runtime evidence queries", {
code: ERROR_CODES.internalError,
data: durableRuntimeReadBlockedData(method, readiness),
context: {
result: "failed"
}
});
}
export function durableRuntimeReadBlockedData(method, readiness = {}) {
const durability = readiness.durabilityContract ?? {};
const connection = readiness.connection ?? {};
const blockedLayer = durability.blockedLayer ?? durabilityBlockedLayer(readiness);
const queryResult = connection.queryResult ?? (blockedLayer === "durability_query" ? "query_blocked" : "not_ready");
return pruneUndefined({
method,
adapter: readiness.adapter ?? RUNTIME_STORE_KIND_POSTGRES,
status: "blocked",
blocker: readiness.blocker ?? RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
durable: false,
durableRequested: true,
liveRuntimeEvidence: false,
queryResult,
blockedLayer,
requiredEvidence: durability.requiredEvidence ?? RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
dbLiveEvidenceIsDurabilityEvidence: false,
retryable: readiness.retryable === true || connection.queryResult === "connect_timeout",
transient: readiness.transient === true || connection.queryResult === "connect_timeout",
retryAfterMs: Number.isFinite(Number(readiness.retryAfterMs)) ? Math.max(0, Math.trunc(Number(readiness.retryAfterMs))) : undefined,
secretMaterialRead: false,
valueRedacted: true,
endpointRedacted: true,
errorCode: connection.errorCode ?? undefined,
redaction: {
valueRedacted: true,
valuesRedacted: true,
endpointRedacted: true,
secretMaterialRead: false
}
});
}
export function isRuntimeDbSslError(error) {
const code = typeof error?.code === "string" ? error.code : "";
if (code.startsWith("ERR_SSL") || code.startsWith("ERR_TLS")) {
return true;
}
if ([
"DEPTH_ZERO_SELF_SIGNED_CERT",
"SELF_SIGNED_CERT_IN_CHAIN",
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
"CERT_HAS_EXPIRED"
].includes(code)) {
return true;
}
const message = typeof error?.message === "string" ? error.message.toLowerCase() : "";
return [
"does not support ssl",
"ssl is not enabled",
"ssl negotiation",
"ssl off",
"ssl required",
"requires ssl",
"no ssl encryption",
"no encryption",
"hostssl",
"server requires ssl",
"before secure tls connection",
"tls handshake"
].some((pattern) => message.includes(pattern));
}
export function summarizeRuntimeSchema(rows, requiredSchema = requiredPostgresSchema) {
const columnsByTable = new Map();
for (const row of rows) {
const table = row.table_name;
const column = row.column_name;
if (!table || !column) continue;
if (!columnsByTable.has(table)) {
columnsByTable.set(table, new Set());
}
columnsByTable.get(table).add(column);
}
const missingTables = [];
const missingColumns = [];
for (const [table, columns] of Object.entries(requiredSchema)) {
const observed = columnsByTable.get(table);
if (!observed) {
missingTables.push(table);
missingColumns.push(...columns.map((column) => `${table}.${column}`));
continue;
}
for (const column of columns) {
if (!observed.has(column)) {
missingColumns.push(`${table}.${column}`);
}
}
}
return {
ready: missingTables.length === 0 && missingColumns.length === 0,
checked: true,
requiredTables: Object.keys(requiredSchema),
missingTables,
missingColumns
};
}
export function notCheckedRuntimeMigration() {
return {
checked: false,
ready: false,
table: CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
requiredMigrationId: CLOUD_CORE_MIGRATION_ID,
requiredSchemaVersion: CLOUD_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
appliedMigrationId: null,
appliedSchemaVersion: null,
missing: true
};
}
export function blockedRuntimeMigration({ errorCode = null } = {}) {
return {
...notCheckedRuntimeMigration(),
checked: true,
errorCode
};
}
export function runtimeGates({
ssl = notCheckedGate(),
auth = notCheckedGate(),
schema = notCheckedGate(),
migration = notCheckedGate(),
durability = notCheckedGate()
} = {}) {
return {
ssl,
auth,
schema,
migration,
durability
};
}
export function defaultRuntimeGates({ blocker, connection, schema, migration } = {}) {
if (blocker === RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED) {
return {
ssl: blockedGate({ checked: true, blocker }),
auth: notCheckedGate()
};
}
if (blocker === RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED) {
return {
ssl: readyGate(),
auth: blockedGate({ checked: true, blocker })
};
}
if (blocker === RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED) {
return {
ssl: connection?.queryAttempted ? readyGate() : notCheckedGate(),
auth: connection?.queryAttempted ? readyGate() : notCheckedGate()
};
}
if (blocker === RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED && schema?.checked && migration?.checked) {
return {
ssl: readyGate(),
auth: readyGate()
};
}
return {
ssl: notCheckedGate(),
auth: notCheckedGate()
};
}
export function defaultSchemaGate({ blocker, schema } = {}) {
if (schema?.checked) {
return gateFromReadiness(schema.ready, { blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED });
}
if (blocker === RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED) {
return blockedGate({ checked: true, blocker });
}
return notCheckedGate();
}
export function defaultMigrationGate({ blocker, migration } = {}) {
if (migration?.checked) {
return gateFromReadiness(migration.ready, { blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED });
}
if (blocker === RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED) {
return blockedGate({ checked: true, blocker });
}
return notCheckedGate();
}
export function readyGate() {
return {
checked: true,
ready: true,
status: "ready",
blocker: null
};
}
export function blockedGate({ checked = true, blocker = "runtime_durable_adapter_blocked" } = {}) {
return {
checked,
ready: false,
status: checked ? "blocked" : "not_checked",
blocker
};
}
export function notCheckedGate() {
return {
checked: false,
ready: false,
status: "not_checked",
blocker: null
};
}
export function gateFromReadiness(ready, { blocker } = {}) {
return ready ? readyGate() : blockedGate({ checked: true, blocker });
}
export function snapshotMemory(memory) {
return {
gatewaySessions: new Map(memory.gatewaySessions),
boxResources: new Map(memory.boxResources),
boxCapabilities: new Map(memory.boxCapabilities),
hardwareOperations: new Map(memory.hardwareOperations),
auditEvents: new Map(memory.auditEvents),
evidenceRecords: new Map(memory.evidenceRecords),
agentTraceEvents: new Map(memory.agentTraceEvents),
workbenchProjectionStates: new Map(memory.workbenchProjectionStates),
workbenchSessions: new Map(memory.workbenchSessions),
workbenchMessages: new Map(memory.workbenchMessages),
workbenchParts: new Map(memory.workbenchParts),
workbenchTurns: new Map(memory.workbenchTurns),
workbenchTraceEvents: new Map(memory.workbenchTraceEvents),
workbenchProjectionCheckpoints: new Map(memory.workbenchProjectionCheckpoints)
};
}
export function newRecords(current, before) {
return [...current.entries()]
.filter(([id]) => !before.has(id))
.map(([, record]) => record);
}
export function changedRecords(current, before) {
return [...current.entries()]
.filter(([id, record]) => stableJson(before.get(id)) !== stableJson(record))
.map(([, record]) => record);
}
export function withPersistence(result, persistence) {
return {
...result,
persistence
};
}
export function parseJsonColumn(value, fallback) {
if (!value) return fallback;
if (typeof value === "object") return value;
try {
return JSON.parse(String(value));
} catch {
return fallback;
}
}
export function postgresPoolStats(pool) {
if (!pool || typeof pool !== "object") return null;
return {
totalCount: Number.isFinite(Number(pool.totalCount)) ? Number(pool.totalCount) : null,
idleCount: Number.isFinite(Number(pool.idleCount)) ? Number(pool.idleCount) : null,
waitingCount: Number.isFinite(Number(pool.waitingCount)) ? Number(pool.waitingCount) : null,
valuesRedacted: true
};
}
export function toCountKey(table) {
if (table === "users") return "users";
if (table === "user_sessions") return "userSessions";
if (table === "gateway_sessions") return "gatewaySessions";
if (table === "box_resources") return "boxResources";
if (table === "box_capabilities") return "boxCapabilities";
if (table === "hardware_operations") return "hardwareOperations";
if (table === "audit_events") return "auditEvents";
if (table === "evidence_records") return "evidenceRecords";
if (table === "agent_sessions") return "agentSessions";
if (table === "account_workspaces") return "accountWorkspaces";
if (table === "worker_sessions") return "workerSessions";
if (table === "agent_trace_events") return "agentTraceEvents";
if (table === "workbench_projection_state") return "workbenchProjectionStates";
if (table === "workbench_sessions") return "workbenchSessions";
if (table === "workbench_messages") return "workbenchMessages";
if (table === "workbench_parts") return "workbenchParts";
if (table === "workbench_turns") return "workbenchTurns";
if (table === "workbench_trace_events") return "workbenchTraceEvents";
if (table === "workbench_projection_checkpoints") return "workbenchProjectionCheckpoints";
return table;
}
export function normalizeRuntimeTimeoutMs(value) {
const parsed = Number.parseInt(String(value ?? ""), 10);
if (!Number.isInteger(parsed) || parsed <= 0) {
return 1200;
}
return Math.min(parsed, 5000);
}
export function normalizeRuntimePoolMax(value) {
if (value === undefined || value === null || String(value).trim() === "") return null;
const parsed = Number.parseInt(String(value), 10);
if (!Number.isInteger(parsed) || parsed <= 0 || String(parsed) !== String(value).trim()) {
const error = new Error(`${RUNTIME_DB_POOL_MAX_ENV} must be a positive integer`);
error.code = "HWLAB_CLOUD_DB_POOL_MAX_INVALID";
throw error;
}
return parsed;
}