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

886 lines
32 KiB
TypeScript

/*
* In-memory Cloud runtime store implementation.
* Public construction remains in runtime-store.ts.
*/
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_RUNTIME_DURABLE_ADAPTER_SCHEMA_VERSION,
CLOUD_RUNTIME_DURABLE_MIGRATION_TABLE,
CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS
} from "./schema.ts";
import * as runtimeCore from "./runtime-store-core.ts";
const {
RUNTIME_STORE_KIND,
RUNTIME_STORE_KIND_POSTGRES,
RUNTIME_DURABLE_ADAPTER_MISSING,
RUNTIME_DURABLE_ADAPTER_UNCONFIGURED,
RUNTIME_DURABLE_ADAPTER_DRIVER_MISSING,
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
RUNTIME_DURABILITY_REQUIRED_EVIDENCE,
RUNTIME_ADAPTER_ENV,
RUNTIME_DURABLE_ENV,
RUNTIME_DB_POOL_MAX_ENV,
requiredPostgresSchema,
postgresCountTables,
postgresRuntimeReadIndexes,
buildPostgresPoolConfig,
RUNTIME_DB_QUERY_TIMEOUT_MS_ENV,
RUNTIME_DB_QUERY_RETRY_MAX_ATTEMPTS_ENV,
RUNTIME_DB_QUERY_RETRY_INITIAL_DELAY_MS_ENV,
RUNTIME_DB_QUERY_RETRY_MAX_DELAY_MS_ENV,
RUNTIME_DB_POOL_RESET_COOLDOWN_MS_ENV,
DEFAULT_RUNTIME_DB_QUERY_RETRY_MAX_ATTEMPTS,
DEFAULT_RUNTIME_DB_QUERY_RETRY_INITIAL_DELAY_MS,
DEFAULT_RUNTIME_DB_QUERY_RETRY_MAX_DELAY_MS,
DEFAULT_RUNTIME_DB_POOL_RESET_COOLDOWN_MS,
normalizePositiveInteger,
delayRuntimeDbQueryRetry,
isReadyForDurableRuntimeRead,
normalizeCapabilityInputs,
normalizeShellInput,
normalizeAgentTraceEvent,
normalizeWorkbenchProjectionState,
normalizeWorkbenchProjectionAllocation,
normalizeWorkbenchFacts,
normalizeWorkbenchAggregateEventsForFacts,
appendWorkbenchFactEvent,
workbenchAggregateForFact,
normalizeWorkbenchAggregateEvent,
workbenchAggregateEventType,
workbenchInputDelivery,
workbenchInputStatus,
indexWorkbenchAggregateEvents,
normalizeWorkbenchSessionInputFact,
memoryWorkbenchSessionInputWithSeq,
inputFactWithAggregateSeq,
workbenchSessionInputWithSeq,
workbenchSessionInputPromotedSeq,
normalizeWorkbenchSessionFact,
normalizeWorkbenchMessageFact,
normalizeWorkbenchPartFact,
normalizeWorkbenchTurnFact,
assertWorkbenchTurnFinalResponseInvariant,
normalizeWorkbenchFactStatus,
workbenchFactFinalResponseText,
normalizeWorkbenchTraceEventFact,
normalizeWorkbenchProjectionCheckpoint,
arrayInput,
stableWorkbenchFactId,
requiredWorkbenchFactError,
compareTraceEventRecords,
compareProjectionStateRecords,
compareWorkbenchFactRecords,
compareWorkbenchInputFactRecords,
compareWorkbenchPartFactRecords,
compareWorkbenchTraceEventFacts,
sortWorkbenchFactRows,
WORKBENCH_FACT_FAMILIES,
workbenchFactFamilySet,
workbenchFactFamilyForTable,
workbenchSessionSummarySelectClause,
workbenchSessionSummaryFactFromRow,
workbenchFactOrder,
traceEventLevel,
positiveIntegerOrNull,
nonNegativeInteger,
nonNegativeIntegerOrNull,
projectionStatusOr,
projectionHealthOr,
resultSyncStateOr,
timestampOr,
textOr,
textList,
asObject,
requireString,
requireProtocolId,
normalizeJsonObject,
pruneUndefined,
makeId,
stableJson,
sha256Hex,
sortJson,
matchesQuery,
matchesWorkbenchSessionFactQuery,
limitResults,
normalizeRuntimeAdapter,
normalizePostgresSslMode,
postgresSslModeFromConnectionString,
postgresSslOption,
normalizePostgresConnectionStringForSslMode,
postgresAdapterContract,
addRuntimeDurabilityContract,
durabilityBlockedLayer,
runtimeSafety,
classifyRuntimeDbError,
isRuntimeDbConnectTimeout,
durableRuntimeReadBlockedError,
durableRuntimeReadBlockedData,
isRuntimeDbSslError,
summarizeRuntimeSchema,
notCheckedRuntimeMigration,
blockedRuntimeMigration,
runtimeGates,
defaultRuntimeGates,
defaultSchemaGate,
defaultMigrationGate,
readyGate,
blockedGate,
notCheckedGate,
gateFromReadiness,
snapshotMemory,
newRecords,
changedRecords,
withPersistence,
parseJsonColumn,
postgresPoolStats,
toCountKey,
normalizeRuntimeTimeoutMs,
normalizeRuntimePoolMax
} = runtimeCore;
export class CloudRuntimeStore {
constructor({ now = () => new Date().toISOString() } = {}) {
this.now = now;
this.gatewaySessions = new Map();
this.boxResources = new Map();
this.boxCapabilities = new Map();
this.hardwareOperations = new Map();
this.auditEvents = new Map();
this.evidenceRecords = new Map();
this.agentTraceEvents = new Map();
this.workbenchProjectionStates = new Map();
this.workbenchSessions = new Map();
this.workbenchMessages = new Map();
this.workbenchParts = new Map();
this.workbenchTurns = new Map();
this.workbenchTraceEvents = new Map();
this.workbenchEventSequences = new Map();
this.workbenchEvents = new Map();
this.workbenchSessionInputs = new Map();
this.workbenchProjectionCheckpoints = new Map();
}
summary() {
return addRuntimeDurabilityContract({
adapter: RUNTIME_STORE_KIND,
durable: false,
status: "degraded",
blocker: RUNTIME_DURABLE_ADAPTER_MISSING,
reason: "L1 runtime writes are process-local only; no DB-backed durable runtime adapter is configured",
adapterContract: {
required: "DB-backed durable runtime adapter using HWLAB_CLOUD_DB_URL",
sourceIssue: "pikasTech/HWLAB#164",
secretMaterialRequiredInSource: false
},
counts: {
gatewaySessions: this.gatewaySessions.size,
boxResources: this.boxResources.size,
boxCapabilities: this.boxCapabilities.size,
hardwareOperations: this.hardwareOperations.size,
auditEvents: this.auditEvents.size,
evidenceRecords: this.evidenceRecords.size,
agentTraceEvents: this.agentTraceEvents.size,
workbenchProjectionStates: this.workbenchProjectionStates.size,
workbenchSessions: this.workbenchSessions.size,
workbenchMessages: this.workbenchMessages.size,
workbenchParts: this.workbenchParts.size,
workbenchTurns: this.workbenchTurns.size,
workbenchTraceEvents: this.workbenchTraceEvents.size,
workbenchEventSequences: this.workbenchEventSequences.size,
workbenchEvents: this.workbenchEvents.size,
workbenchSessionInputs: this.workbenchSessionInputs.size,
workbenchProjectionCheckpoints: this.workbenchProjectionCheckpoints.size
}
});
}
async workbenchTransactionalRealtimeReadiness() {
return {
ready: true,
adapter: RUNTIME_STORE_KIND,
durable: false,
testRuntime: true,
valuesRedacted: true
};
}
async assertWorkbenchTransactionalRealtimeReady() {
return this.workbenchTransactionalRealtimeReadiness();
}
registerGatewaySession(params = {}, requestMeta = {}) {
const now = this.now();
const input = asObject(params.gatewaySession ?? params, "gatewaySession");
const gatewaySession = pruneUndefined({
gatewaySessionId: input.gatewaySessionId || makeId("gws"),
projectId: requireProtocolId(input, "projectId"),
serviceId: input.serviceId || "hwlab-gateway",
gatewayId: input.gatewayId || makeId("gtw"),
endpoint: input.endpoint,
status: input.status || "connected",
environment: ENVIRONMENT_DEV,
startedAt: input.startedAt || now,
lastSeenAt: input.lastSeenAt || now,
stoppedAt: input.stoppedAt,
labels: normalizeJsonObject(input.labels)
});
assertProtocolRecord("gatewaySession", gatewaySession);
this.gatewaySessions.set(gatewaySession.gatewaySessionId, gatewaySession);
const auditEvent = this.recordAuditEvent({
requestMeta,
params,
action: "gateway.session.register",
targetType: "gateway_session",
targetId: gatewaySession.gatewaySessionId,
projectId: gatewaySession.projectId,
gatewaySessionId: gatewaySession.gatewaySessionId,
outcome: "accepted",
metadata: {
gatewayId: gatewaySession.gatewayId,
serviceId: gatewaySession.serviceId
}
});
return {
registered: true,
gatewaySession,
auditEvent,
persistence: this.summary()
};
}
registerBoxResource(params = {}, requestMeta = {}) {
const now = this.now();
const input = asObject(params.resource ?? params.boxResource ?? params, "boxResource");
const gatewaySessionId = requireProtocolId(input, "gatewaySessionId");
const gatewaySession = this.requireGatewaySession(gatewaySessionId);
const projectId = input.projectId || gatewaySession.projectId;
const resource = pruneUndefined({
resourceId: input.resourceId || makeId("res"),
projectId,
gatewaySessionId,
boxId: input.boxId || makeId("box"),
resourceType: input.resourceType || "simulator_endpoint",
name: input.name,
state: input.state || "available",
environment: ENVIRONMENT_DEV,
metadata: normalizeJsonObject(input.metadata),
createdAt: input.createdAt || now,
updatedAt: input.updatedAt || now
});
assertProtocolRecord("boxResource", resource);
this.boxResources.set(resource.resourceId, resource);
const auditEvent = this.recordAuditEvent({
requestMeta,
params,
action: "box.resource.register",
targetType: "box_resource",
targetId: resource.resourceId,
projectId: resource.projectId,
gatewaySessionId: resource.gatewaySessionId,
outcome: "accepted",
metadata: {
boxId: resource.boxId,
resourceType: resource.resourceType
}
});
return {
registered: true,
resource,
auditEvent,
persistence: this.summary()
};
}
reportBoxCapabilities(params = {}, requestMeta = {}) {
const now = this.now();
const inputs = normalizeCapabilityInputs(params);
const capabilities = [];
for (const input of inputs) {
const resourceId = requireProtocolId(input, "resourceId");
const resource = this.requireBoxResource(resourceId);
const capability = pruneUndefined({
capabilityId: input.capabilityId || makeId("cap"),
resourceId,
projectId: input.projectId || resource.projectId,
name: requireString(input, "name"),
description: input.description,
direction: input.direction || "bidirectional",
valueType: input.valueType || "object",
unit: input.unit,
constraints: normalizeJsonObject(input.constraints),
mutatesState: input.mutatesState ?? true,
createdAt: input.createdAt || now,
updatedAt: input.updatedAt || now
});
assertProtocolRecord("boxCapability", capability);
this.boxCapabilities.set(capability.capabilityId, capability);
capabilities.push(capability);
}
const first = capabilities[0];
const auditEvent = this.recordAuditEvent({
requestMeta,
params,
action: "box.capability.report",
targetType: "box_capability",
targetId: first.capabilityId,
projectId: first.projectId,
gatewaySessionId: this.boxResources.get(first.resourceId)?.gatewaySessionId,
outcome: "accepted",
metadata: {
capabilityIds: capabilities.map((capability) => capability.capabilityId)
}
});
return {
reported: true,
capabilities,
auditEvent,
persistence: this.summary()
};
}
requestHardwareOperation(params = {}, requestMeta = {}) {
const refs = this.requireOperationRefs(params, requestMeta);
const now = this.now();
const operation = pruneUndefined({
operationId: params.operationId || makeId("op"),
projectId: refs.projectId,
gatewaySessionId: refs.gatewaySession.gatewaySessionId,
agentSessionId: params.agentSessionId,
workerSessionId: params.workerSessionId,
resourceId: refs.resource.resourceId,
capabilityId: refs.capability.capabilityId,
requestedBy: refs.actor.actorId,
input: normalizeJsonObject(params.input),
status: "accepted",
environment: ENVIRONMENT_DEV,
requestedAt: params.requestedAt || now,
updatedAt: now
});
assertProtocolRecord("hardwareOperation", operation);
this.hardwareOperations.set(operation.operationId, operation);
const auditEvent = this.recordAuditEvent({
requestMeta,
params,
action: "hardware.operation.request",
targetType: "hardware_operation",
targetId: operation.operationId,
projectId: operation.projectId,
gatewaySessionId: operation.gatewaySessionId,
workerSessionId: operation.workerSessionId,
operationId: operation.operationId,
outcome: "accepted",
metadata: {
resourceId: operation.resourceId,
capabilityId: operation.capabilityId
}
});
return {
accepted: true,
status: operation.status,
operationId: operation.operationId,
operation,
auditEvent,
persistence: this.summary()
};
}
invokeHardwareShell(params = {}, requestMeta = {}) {
const shellInput = normalizeShellInput(params);
const operationResult = this.requestHardwareOperation(
{
...params,
input: {
...normalizeJsonObject(params.input),
shell: shellInput
}
},
requestMeta
);
const operation = {
...operationResult.operation,
output: {
shellExecuted: false,
dispatchStatus: "not_connected",
message: "cloud-api recorded the shell invoke request; no gateway shell adapter is connected in L1"
},
updatedAt: this.now()
};
assertProtocolRecord("hardwareOperation", operation);
this.hardwareOperations.set(operation.operationId, operation);
const evidenceRecord = this.writeEvidenceRecord(
{
projectId: operation.projectId,
operationId: operation.operationId,
kind: "trace",
mimeType: "application/json",
serviceId: CLOUD_API_SERVICE_ID,
metadata: {
traceId: requestMeta.traceId,
gatewaySessionId: operation.gatewaySessionId,
resourceId: operation.resourceId,
capabilityId: operation.capabilityId,
shellExecuted: false,
dispatchStatus: "not_connected"
},
content: {
operationId: operation.operationId,
shell: shellInput,
dispatchStatus: "not_connected"
}
},
requestMeta
).evidenceRecord;
const auditEvent = this.recordAuditEvent({
requestMeta,
params,
action: "hardware.invoke.shell",
targetType: "hardware_operation",
targetId: operation.operationId,
projectId: operation.projectId,
gatewaySessionId: operation.gatewaySessionId,
workerSessionId: operation.workerSessionId,
operationId: operation.operationId,
outcome: "accepted",
metadata: {
evidenceId: evidenceRecord.evidenceId,
shellExecuted: false,
dispatchStatus: "not_connected"
}
});
return {
accepted: true,
status: operation.status,
operationId: operation.operationId,
operation,
auditEvent,
evidenceRecord,
dispatch: operation.output,
persistence: this.summary()
};
}
writeAuditEvent(params = {}, requestMeta = {}) {
const eventInput = params.event ?? params.auditEvent ?? params;
const auditEvent = eventInput.auditId
? pruneUndefined({
...eventInput,
environment: ENVIRONMENT_DEV
})
: this.buildAuditEventFromInput(eventInput, requestMeta);
assertProtocolRecord("auditEvent", auditEvent);
this.auditEvents.set(auditEvent.auditId, auditEvent);
return {
written: true,
auditEvent,
persistence: this.summary()
};
}
queryAuditEvents(params = {}) {
const events = [...this.auditEvents.values()].filter((event) => matchesQuery(event, params, [
"auditId",
"traceId",
"projectId",
"gatewaySessionId",
"operationId",
"action",
"targetId"
]));
return {
events: limitResults(events, params.limit),
count: events.length,
persistence: this.summary()
};
}
writeEvidenceRecord(params = {}, requestMeta = {}) {
const now = this.now();
const input = asObject(params.record ?? params.evidenceRecord ?? params, "evidenceRecord");
const content = input.content ?? input.metadata ?? {};
const sha256 = input.sha256 || sha256Hex(content);
const evidenceId = input.evidenceId || makeId("evd");
const evidenceRecord = pruneUndefined({
evidenceId,
projectId: requireProtocolId(input, "projectId"),
operationId: requireProtocolId(input, "operationId"),
agentSessionId: input.agentSessionId,
workerSessionId: input.workerSessionId,
kind: input.kind || "trace",
uri: input.uri || `memory://hwlab/evidence/${evidenceId}`,
mimeType: input.mimeType,
sha256,
sizeBytes: input.sizeBytes ?? Buffer.byteLength(stableJson(content), "utf8"),
serviceId: input.serviceId || CLOUD_API_SERVICE_ID,
environment: ENVIRONMENT_DEV,
metadata: {
...normalizeJsonObject(input.metadata),
...(requestMeta.traceId ? { traceId: requestMeta.traceId } : {})
},
createdAt: input.createdAt || now
});
assertProtocolRecord("evidenceRecord", evidenceRecord);
this.evidenceRecords.set(evidenceRecord.evidenceId, evidenceRecord);
return {
written: true,
evidenceRecord,
persistence: this.summary()
};
}
queryEvidenceRecords(params = {}) {
const records = [...this.evidenceRecords.values()].filter((record) => matchesQuery(record, params, [
"evidenceId",
"projectId",
"operationId",
"agentSessionId",
"workerSessionId",
"kind",
"serviceId"
]));
return {
records: limitResults(records, params.limit),
count: records.length,
persistence: this.summary()
};
}
writeAgentTraceEvent(params = {}, requestMeta = {}) {
const traceEvent = normalizeAgentTraceEvent(params.traceEvent ?? params.event ?? params, requestMeta, this.now());
this.agentTraceEvents.set(traceEvent.id, traceEvent);
return {
written: true,
traceEvent,
persistence: this.summary()
};
}
queryAgentTraceEvents(params = {}) {
const records = [...this.agentTraceEvents.values()].filter((record) => matchesQuery(record, params, [
"id",
"traceId",
"agentSessionId",
"workerSessionId",
"level"
]));
records.sort(compareTraceEventRecords);
return {
events: limitResults(records.map((record) => record.event), params.limit),
count: records.length,
persistence: this.summary()
};
}
writeWorkbenchProjectionState(params = {}, requestMeta = {}) {
const state = normalizeWorkbenchProjectionState(params.projectionState ?? params.state ?? params, requestMeta, this.now());
this.workbenchProjectionStates.set(state.traceId, state);
return {
written: true,
projectionState: state,
persistence: this.summary()
};
}
getWorkbenchProjectionState(params = {}) {
const traceId = textOr(params.traceId, "");
const state = traceId ? this.workbenchProjectionStates.get(traceId) ?? null : null;
return {
projectionState: state,
found: Boolean(state),
persistence: this.summary()
};
}
allocateWorkbenchProjectedSeq(params = {}, requestMeta = {}) {
const allocation = normalizeWorkbenchProjectionAllocation(params, requestMeta, this.now());
const existing = [...this.workbenchTraceEvents.values()].find((record) =>
record.traceId === allocation.traceId && record.sourceEventId === allocation.sourceEventId
);
if (existing) {
return {
...allocation,
projectedSeq: nonNegativeInteger(existing.projectedSeq),
reused: true,
persistence: this.summary(),
valuesPrinted: false
};
}
const eventMax = [...this.workbenchTraceEvents.values()]
.filter((record) => record.traceId === allocation.traceId)
.reduce((max, record) => Math.max(max, nonNegativeInteger(record.projectedSeq)), 0);
const checkpointMax = nonNegativeInteger(this.workbenchProjectionCheckpoints.get(allocation.traceId)?.projectedSeq);
return {
...allocation,
projectedSeq: Math.max(eventMax, checkpointMax) + 1,
reused: false,
persistence: this.summary(),
valuesPrinted: false
};
}
queryWorkbenchProjectionStates(params = {}) {
const dueAt = textOr(params.dueAt, "");
const statuses = Array.isArray(params.projectionStatuses) ? new Set(params.projectionStatuses.map((item) => textOr(item, "")).filter(Boolean)) : null;
const records = [...this.workbenchProjectionStates.values()]
.filter((record) => matchesQuery(record, params, ["traceId", "sessionId", "runId", "commandId", "sourceRunId", "sourceCommandId"]))
.filter((record) => !statuses || statuses.has(record.projectionStatus))
.filter((record) => !dueAt || !record.nextRetryAt || String(record.nextRetryAt) <= dueAt)
.sort(compareProjectionStateRecords);
return {
states: limitResults(records, params.limit),
count: records.length,
persistence: this.summary()
};
}
writeWorkbenchFacts(params = {}, requestMeta = {}) {
const facts = normalizeWorkbenchFacts(params.facts ?? params, requestMeta, this.now());
const inputFacts = facts.inputs.map((fact) => memoryWorkbenchSessionInputWithSeq(fact, this.workbenchSessionInputs));
for (const fact of inputFacts) this.workbenchSessionInputs.set(fact.inputId, fact);
for (const fact of facts.sessions) this.workbenchSessions.set(fact.sessionId, fact);
for (const fact of facts.messages) {
const previous = this.workbenchMessages.get(fact.messageId) ?? null;
this.workbenchMessages.set(fact.messageId, previous && (nonNegativeInteger(previous.projectedSeq) > 0 || previous.sealed === true) ? previous : fact);
}
for (const fact of facts.parts) this.workbenchParts.set(fact.partId, fact);
for (const fact of facts.turns) {
const previous = this.workbenchTurns.get(fact.turnId) ?? null;
this.workbenchTurns.set(fact.turnId, previous && (nonNegativeInteger(previous.projectedSeq) > 0 || previous.sealed === true) ? previous : fact);
}
for (const fact of facts.traceEvents) this.workbenchTraceEvents.set(fact.id, fact);
for (const fact of facts.checkpoints) {
const previous = this.workbenchProjectionCheckpoints.get(fact.traceId) ?? null;
if (!previous || nonNegativeInteger(fact.projectedSeq) >= nonNegativeInteger(previous.projectedSeq)) {
this.workbenchProjectionCheckpoints.set(fact.traceId, fact);
}
}
return {
written: true,
facts: { ...facts, inputs: inputFacts },
persistence: this.summary()
};
}
writeWorkbenchSessionAdmissionFact(params = {}, requestMeta = {}) {
const facts = normalizeWorkbenchFacts({ sessions: [params.fact ?? params.session ?? params] }, requestMeta, this.now());
const fact = facts.sessions[0];
if (!fact?.sessionId) throw new Error("Workbench session admission fact requires sessionId.");
const previous = this.workbenchSessions.get(fact.sessionId) ?? null;
const projected = previous && (nonNegativeInteger(previous.projectedSeq) > 0 || previous.sealed === true);
const ownership = {
ownerUserId: previous?.ownerUserId ?? fact.ownerUserId ?? null,
ownerRole: previous?.ownerRole ?? fact.ownerRole ?? null,
projectId: previous?.projectId ?? fact.projectId ?? null,
conversationId: previous?.conversationId ?? fact.conversationId ?? null,
threadId: previous?.threadId ?? fact.threadId ?? null
};
const stored = projected
? { ...previous, ...ownership }
: { ...previous, ...fact, ...ownership, createdAt: previous?.createdAt ?? fact.createdAt };
this.workbenchSessions.set(fact.sessionId, stored);
return { written: true, admissionOnly: true, fact: stored, persistence: this.summary(), valuesPrinted: false };
}
promoteWorkbenchTurnAdmission(params = {}, requestMeta = {}) {
const facts = normalizeWorkbenchFacts(params.facts ?? params, requestMeta, this.now());
const sessionFact = facts.sessions[0];
const inputFact = facts.inputs[0];
const turnFact = facts.turns[0];
if (!sessionFact?.sessionId || !inputFact?.inputId || !turnFact?.turnId) throw new Error("Workbench turn admission promotion requires session, input, and turn facts.");
const storedSession = this.writeWorkbenchSessionAdmissionFact({ fact: sessionFact }, requestMeta).fact;
const storedInput = memoryWorkbenchSessionInputWithSeq(inputFact, this.workbenchSessionInputs);
this.workbenchSessionInputs.set(storedInput.inputId, storedInput);
for (const fact of facts.messages) {
const previous = this.workbenchMessages.get(fact.messageId) ?? null;
if (!previous || (nonNegativeInteger(previous.projectedSeq) <= 0 && previous.sealed !== true)) this.workbenchMessages.set(fact.messageId, fact);
}
for (const fact of facts.parts) {
const previous = this.workbenchParts.get(fact.partId) ?? null;
if (!previous || (nonNegativeInteger(previous.projectedSeq) <= 0 && previous.sealed !== true)) this.workbenchParts.set(fact.partId, fact);
}
for (const fact of facts.turns) {
const previous = this.workbenchTurns.get(fact.turnId) ?? null;
if (!previous || (nonNegativeInteger(previous.projectedSeq) <= 0 && previous.sealed !== true)) this.workbenchTurns.set(fact.turnId, fact);
}
return {
written: true,
promoted: true,
outboxCommitted: true,
facts: { ...facts, sessions: [storedSession], inputs: [storedInput] },
persistence: this.summary(),
valuesPrinted: false
};
}
queryWorkbenchFacts(params = {}) {
const families = workbenchFactFamilySet(params);
const sessions = [...this.workbenchSessions.values()].filter((record) => matchesWorkbenchSessionFactQuery(record, params));
const inputs = [...this.workbenchSessionInputs.values()].filter((record) => matchesQuery(record, params, ["inputId", "sessionId", "turnId", "traceId", "messageId", "commandId", "delivery", "status"]));
const messages = [...this.workbenchMessages.values()].filter((record) => matchesQuery(record, params, ["messageId", "sessionId", "turnId", "traceId", "role", "status"]));
const parts = [...this.workbenchParts.values()].filter((record) => matchesQuery(record, params, ["partId", "messageId", "sessionId", "turnId", "traceId", "partType", "status"]));
const turns = [...this.workbenchTurns.values()].filter((record) => matchesQuery(record, params, ["turnId", "sessionId", "traceId", "messageId", "status"]));
const afterProjectedSeq = nonNegativeInteger(params.afterProjectedSeq);
const traceEvents = [...this.workbenchTraceEvents.values()].filter((record) => matchesQuery(record, params, ["id", "traceId", "sessionId", "turnId", "messageId", "eventType"]) && (afterProjectedSeq <= 0 || nonNegativeInteger(record.projectedSeq) > afterProjectedSeq));
const checkpoints = [...this.workbenchProjectionCheckpoints.values()].filter((record) => matchesQuery(record, params, ["traceId", "sessionId", "turnId", "runId", "commandId", "projectionStatus", "projectionHealth"]));
const facts = {
inputs: families.has("inputs") ? limitResults(sortWorkbenchFactRows(inputs, params, "inputs", compareWorkbenchFactRecords), params.limit) : [],
sessions: families.has("sessions") ? limitResults(sortWorkbenchFactRows(sessions, params, "sessions", compareWorkbenchFactRecords), params.limit) : [],
messages: families.has("messages") ? limitResults(sortWorkbenchFactRows(messages, params, "messages", compareWorkbenchFactRecords), params.limit) : [],
parts: families.has("parts") ? limitResults(sortWorkbenchFactRows(parts, params, "parts", compareWorkbenchPartFactRecords), params.limit) : [],
turns: families.has("turns") ? limitResults(sortWorkbenchFactRows(turns, params, "turns", compareWorkbenchFactRecords), params.limit) : [],
traceEvents: families.has("traceEvents") ? limitResults(sortWorkbenchFactRows(traceEvents, params, "traceEvents", compareWorkbenchTraceEventFacts), params.limit) : [],
checkpoints: families.has("checkpoints") ? limitResults(sortWorkbenchFactRows(checkpoints, params, "checkpoints", compareWorkbenchFactRecords), params.limit) : []
};
return {
facts,
count: Object.values(facts).reduce((sum, rows) => sum + rows.length, 0),
persistence: this.summary()
};
}
recordAuditEvent(input) {
return this.writeAuditEvent(this.buildAuditEventFromInput(input, input.requestMeta), input.requestMeta).auditEvent;
}
buildAuditEventFromInput(input = {}, requestMeta = {}) {
const actor = deriveProtocolActorFromMeta(requestMeta, input.params?.audit ?? input);
return createProtocolAuditEvent({
auditId: input.auditId,
traceId: requestMeta.traceId || input.traceId,
actorType: actor.actorType,
actorId: actor.actorId,
action: input.action,
targetType: input.targetType,
targetId: input.targetId,
projectId: input.projectId,
gatewaySessionId: input.gatewaySessionId,
workerSessionId: input.workerSessionId,
operationId: input.operationId,
serviceId: CLOUD_API_SERVICE_ID,
outcome: input.outcome,
reason: input.reason || input.params?.audit?.reason,
metadata: input.metadata,
occurredAt: input.occurredAt
});
}
requireOperationRefs(params, requestMeta = {}) {
const projectId = requireProtocolId(params, "projectId");
const gatewaySession = this.requireGatewaySession(requireProtocolId(params, "gatewaySessionId"));
const resource = this.requireBoxResource(requireProtocolId(params, "resourceId"));
const capability = this.requireBoxCapability(requireProtocolId(params, "capabilityId"));
if (gatewaySession.projectId !== projectId || resource.projectId !== projectId || capability.projectId !== projectId) {
throw new HwlabProtocolError("hardware operation references must belong to one project", {
code: ERROR_CODES.invalidParams,
data: {
projectId,
gatewaySessionId: gatewaySession.gatewaySessionId,
resourceId: resource.resourceId,
capabilityId: capability.capabilityId
}
});
}
if (resource.gatewaySessionId !== gatewaySession.gatewaySessionId) {
throw new HwlabProtocolError("resource is not registered under the requested gateway session", {
code: ERROR_CODES.resourceLocked,
data: {
gatewaySessionId: gatewaySession.gatewaySessionId,
resourceGatewaySessionId: resource.gatewaySessionId,
resourceId: resource.resourceId
}
});
}
if (capability.resourceId !== resource.resourceId) {
throw new HwlabProtocolError("capability is not registered on the requested resource", {
code: ERROR_CODES.capabilityUnavailable,
data: {
resourceId: resource.resourceId,
capabilityId: capability.capabilityId
}
});
}
return {
projectId,
gatewaySession,
resource,
capability,
actor: deriveProtocolActorFromMeta(requestMeta, params.audit ?? {})
};
}
requireGatewaySession(gatewaySessionId) {
const gatewaySession = this.gatewaySessions.get(gatewaySessionId);
if (!gatewaySession) {
throw new HwlabProtocolError(`gateway session ${gatewaySessionId} is not registered`, {
code: ERROR_CODES.sessionNotFound,
data: { gatewaySessionId }
});
}
return gatewaySession;
}
requireBoxResource(resourceId) {
const resource = this.boxResources.get(resourceId);
if (!resource) {
throw new HwlabProtocolError(`box resource ${resourceId} is not registered`, {
code: ERROR_CODES.capabilityUnavailable,
data: { resourceId }
});
}
return resource;
}
requireBoxCapability(capabilityId) {
const capability = this.boxCapabilities.get(capabilityId);
if (!capability) {
throw new HwlabProtocolError(`capability ${capabilityId} is not reported`, {
code: ERROR_CODES.capabilityUnavailable,
data: { capabilityId }
});
}
return capability;
}
}