fix: make Kafka projection the sole Workbench authority
This commit is contained in:
@@ -17,7 +17,7 @@ import { createCodeAgentErrorPayload, handleCodeAgentChat } from "./code-agent-c
|
||||
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
|
||||
import { codeAgentSessionLifecycleSummary } from "./code-agent-session-lifecycle.ts";
|
||||
import { messageAuthorityTextValue } from "./code-agent-agentrun-prompt.ts";
|
||||
import { writeWorkbenchProjectionSession, writeWorkbenchSessionAdmissionFact } from "./workbench-projection-writer.ts";
|
||||
import { writeWorkbenchSessionAdmissionFact } from "./workbench-projection-writer.ts";
|
||||
import { createWorkbenchReadModel } from "./workbench-read-model.ts";
|
||||
import { codeAgentOtelTraceFields, emitCodeAgentOtelSpan } from "./otel-trace.ts";
|
||||
import {
|
||||
@@ -1194,6 +1194,16 @@ async function recordCodeAgentTurnAdmission({ payload = {}, params = {}, options
|
||||
error.valuesRedacted = true;
|
||||
throw error;
|
||||
}
|
||||
await writeWorkbenchSessionAdmissionFact({
|
||||
runtimeStore: options.runtimeStore,
|
||||
session: ownerRecord,
|
||||
ownerUserId: options.actor?.id ?? params.ownerUserId,
|
||||
ownerRole: options.actor?.role ?? params.ownerRole,
|
||||
projectId: params.projectId ?? ownerRecord.projectId ?? null,
|
||||
conversationId: params.conversationId ?? ownerRecord.conversationId ?? null,
|
||||
threadId: params.threadId ?? ownerRecord.threadId ?? null,
|
||||
status: "running"
|
||||
});
|
||||
await recordCodeAgentSessionInputFact({
|
||||
params,
|
||||
options,
|
||||
|
||||
@@ -17,7 +17,7 @@ import { createCodeAgentErrorPayload, handleCodeAgentChat } from "./code-agent-c
|
||||
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
|
||||
import { codeAgentSessionLifecycleSummary } from "./code-agent-session-lifecycle.ts";
|
||||
import { messageAuthorityTextValue } from "./code-agent-agentrun-prompt.ts";
|
||||
import { writeWorkbenchProjectionSession, writeWorkbenchSessionAdmissionFact } from "./workbench-projection-writer.ts";
|
||||
import { recordWorkbenchSessionOwner } from "./workbench-projection-writer.ts";
|
||||
import { createWorkbenchReadModel } from "./workbench-read-model.ts";
|
||||
import { codeAgentOtelTraceFields, emitCodeAgentOtelSpan } from "./otel-trace.ts";
|
||||
import {
|
||||
@@ -450,10 +450,8 @@ async function recordCodeAgentSessionOwner({ payload = {}, params = {}, options
|
||||
const sessionId = safeSessionId(payload.sessionId ?? session?.sessionId ?? sessionReuse?.sessionId ?? params.sessionId) || null;
|
||||
const conversationId = safeConversationId(payload.conversationId ?? session?.conversationId ?? sessionReuse?.conversationId ?? params.conversationId) || null;
|
||||
const threadId = safeOpaqueId(session?.threadId ?? sessionReuse?.threadId ?? payload.threadId ?? params.threadId) || null;
|
||||
return writeWorkbenchProjectionSession({
|
||||
return recordWorkbenchSessionOwner({
|
||||
accessController: options.accessController,
|
||||
runtimeStore: options.runtimeStore,
|
||||
traceStore: options.traceStore ?? defaultCodeAgentTraceStore,
|
||||
traceId,
|
||||
ownerUserId,
|
||||
ownerRole: options.actor?.role ?? params.ownerRole ?? null,
|
||||
@@ -463,9 +461,7 @@ async function recordCodeAgentSessionOwner({ payload = {}, params = {}, options
|
||||
threadId,
|
||||
status,
|
||||
preserveLastTraceId,
|
||||
session: codeAgentSessionOwnerEvidence(payload, params),
|
||||
payload,
|
||||
params
|
||||
session: codeAgentSessionOwnerEvidence(payload, params)
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ import { createCodeAgentErrorPayload, handleCodeAgentChat } from "./code-agent-c
|
||||
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
|
||||
import { codeAgentSessionLifecycleSummary } from "./code-agent-session-lifecycle.ts";
|
||||
import { messageAuthorityTextValue } from "./code-agent-agentrun-prompt.ts";
|
||||
import { writeWorkbenchProjectionSession, writeWorkbenchSessionAdmissionFact } from "./workbench-projection-writer.ts";
|
||||
import { createWorkbenchReadModel } from "./workbench-read-model.ts";
|
||||
import { codeAgentOtelTraceFields, emitCodeAgentOtelSpan } from "./otel-trace.ts";
|
||||
import {
|
||||
|
||||
@@ -17,7 +17,6 @@ import { createCodeAgentErrorPayload, handleCodeAgentChat } from "./code-agent-c
|
||||
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
|
||||
import { codeAgentSessionLifecycleSummary } from "./code-agent-session-lifecycle.ts";
|
||||
import { messageAuthorityTextValue } from "./code-agent-agentrun-prompt.ts";
|
||||
import { writeWorkbenchProjectionSession, writeWorkbenchSessionAdmissionFact } from "./workbench-projection-writer.ts";
|
||||
import { createWorkbenchReadModel } from "./workbench-read-model.ts";
|
||||
import { codeAgentOtelTraceFields, emitCodeAgentOtelSpan } from "./otel-trace.ts";
|
||||
import {
|
||||
|
||||
@@ -52,9 +52,9 @@ test("workbench launch records OTel stage for Project Management link write 404"
|
||||
}
|
||||
},
|
||||
runtimeStore: {
|
||||
async writeWorkbenchFacts(params, requestMeta) {
|
||||
async writeWorkbenchSessionAdmissionFact(params, requestMeta) {
|
||||
factWrites.push({ params, requestMeta });
|
||||
return { ok: true, facts: params.facts };
|
||||
return { ok: true, fact: params.fact, admissionOnly: true };
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -86,7 +86,7 @@ test("workbench launch records OTel stage for Project Management link write 404"
|
||||
assert.match(context.otelAttributes["workbench.launch.task_ref_hash"], /^[0-9a-f]{24}$/u);
|
||||
assert.equal(context.otelAttributes["workbench.launch.values_redacted"], true);
|
||||
assert.equal(factWrites.length, 1);
|
||||
const sessionJson = factWrites[0].params.facts.sessions[0].sessionJson;
|
||||
const sessionJson = factWrites[0].params.fact.sessionJson;
|
||||
assert.equal(sessionJson.launchContext.sourceId, "hwlab-v03-mdtodo");
|
||||
assert.equal(sessionJson.launchContext.hwpodId, "constart-71freq-c");
|
||||
assert.equal(sessionJson.launchContext.contextFingerprint, "ctx_launch_otel");
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,70 +1,39 @@
|
||||
/*
|
||||
* SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-18-p0-unique-projection; draft-2026-06-20-p1-zero-split-durable-realtime; PJ2026-010205 HWLAB接入 draft-2026-06-17-r0.
|
||||
* 职责: WorkbenchProjectionWriter 组件入口。唯一封装 Code Agent/AgentRun facts 到 Workbench session projection facts 的持久化写入。
|
||||
* SPEC: PJ2026-0104010803 Workbench唯一投影; HWLAB#2464 transactional Kafka projector.
|
||||
* Responsibility: persist admission-only session ownership and build pure Kafka projection facts.
|
||||
* Message/turn/checkpoint facts are committed only by commitAgentRunKafkaProjection.
|
||||
*/
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
import { defaultCodeAgentTraceStore } from "./code-agent-trace-store.ts";
|
||||
import { emitCodeAgentOtelSpan } from "./otel-trace.ts";
|
||||
import { safeTraceId } from "./server-http-utils.ts";
|
||||
import { createWorkbenchTurnProjection, normalizeWorkbenchStatus, projectionDiagnostics, terminalFinalResponse, TERMINAL_STATUSES } from "./workbench-turn-projection.ts";
|
||||
import { normalizeWorkbenchStatus, terminalFinalResponse, TERMINAL_STATUSES } from "./workbench-turn-projection.ts";
|
||||
|
||||
export async function writeWorkbenchProjectionSession({ accessController, runtimeStore = null, traceStore = defaultCodeAgentTraceStore, traceId, ownerUserId, ownerRole = null, sessionId = null, projectId = null, conversationId = null, threadId = null, status = "active", session = {}, payload = null, params = {}, preserveLastTraceId = false } = {}) {
|
||||
export async function recordWorkbenchSessionOwner({ accessController, traceId, ownerUserId, ownerRole = null, sessionId = null, projectId = null, conversationId = null, threadId = null, status = "active", session = {}, preserveLastTraceId = false } = {}) {
|
||||
if (!ownerUserId || typeof accessController?.recordAgentSessionOwner !== "function") return null;
|
||||
const safeId = safeTraceId(traceId);
|
||||
try {
|
||||
const ownerRecord = await accessController.recordAgentSessionOwner({
|
||||
ownerUserId,
|
||||
ownerRole,
|
||||
sessionId,
|
||||
projectId,
|
||||
conversationId,
|
||||
threadId,
|
||||
traceId: preserveLastTraceId ? undefined : safeId,
|
||||
status,
|
||||
session
|
||||
});
|
||||
const factsWrite = await writeWorkbenchProjectionFacts({
|
||||
runtimeStore,
|
||||
traceStore,
|
||||
traceId: safeId,
|
||||
ownerUserId,
|
||||
ownerRole,
|
||||
sessionId: ownerRecord?.id ?? sessionId,
|
||||
projectId: ownerRecord?.projectId ?? projectId,
|
||||
conversationId: ownerRecord?.conversationId ?? conversationId,
|
||||
threadId: ownerRecord?.threadId ?? threadId,
|
||||
status: ownerRecord?.status ?? status,
|
||||
session,
|
||||
payload,
|
||||
params
|
||||
});
|
||||
if (!factsWrite) {
|
||||
throw workbenchProjectionError("workbench_projection_facts_not_written", "Workbench projection facts were not durably written.");
|
||||
}
|
||||
return ownerRecord;
|
||||
} catch (error) {
|
||||
emitWorkbenchProjectionOtel("workbench.projection.session.persist", safeId, "error", {
|
||||
"workbench.projection.phase": "session-owner",
|
||||
"workbench.projection.has_owner_user_id": Boolean(ownerUserId),
|
||||
"workbench.projection.has_session_id": Boolean(sessionId)
|
||||
}, error);
|
||||
if (safeId) appendProjectionDiagnostic(traceStore, safeId, {
|
||||
type: "session-owner",
|
||||
status: "degraded",
|
||||
label: "projection-writer:session-owner:persist-failed",
|
||||
errorCode: "workbench_projection_session_persist_failed",
|
||||
message: error?.message ?? "Workbench projection session persistence failed.",
|
||||
valuesPrinted: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
return accessController.recordAgentSessionOwner({
|
||||
ownerUserId,
|
||||
ownerRole,
|
||||
sessionId,
|
||||
projectId,
|
||||
conversationId,
|
||||
threadId,
|
||||
traceId: preserveLastTraceId ? undefined : safeTraceId(traceId),
|
||||
status,
|
||||
session
|
||||
});
|
||||
}
|
||||
|
||||
export async function writeWorkbenchSessionAdmissionFact({ runtimeStore = null, session = null, ownerUserId = null, ownerRole = null, projectId = null, conversationId = null, threadId = null, status = "idle", now = new Date().toISOString() } = {}) {
|
||||
if (typeof runtimeStore?.writeWorkbenchFacts !== "function" || !session || typeof session !== "object") return null;
|
||||
if (typeof runtimeStore?.writeWorkbenchSessionAdmissionFact !== "function") {
|
||||
throw workbenchProjectionError("workbench_admission_store_unconfigured", "Workbench admission requires the durable session admission store.");
|
||||
}
|
||||
if (!session || typeof session !== "object") {
|
||||
throw workbenchProjectionError("workbench_admission_session_required", "Workbench admission requires a session record.");
|
||||
}
|
||||
const sessionId = textValue(session.id ?? session.sessionId);
|
||||
if (!sessionId) return null;
|
||||
if (!sessionId) {
|
||||
throw workbenchProjectionError("workbench_admission_session_required", "Workbench admission requires sessionId.");
|
||||
}
|
||||
const createdAt = timestampValue(session.startedAt ?? session.createdAt ?? session.session?.createdAt ?? now);
|
||||
const updatedAt = timestampValue(session.updatedAt ?? createdAt);
|
||||
const normalizedStatus = normalizeWorkbenchStatus(session.status ?? session.session?.sessionStatus ?? status) || "idle";
|
||||
@@ -87,7 +56,7 @@ export async function writeWorkbenchSessionAdmissionFact({ runtimeStore = null,
|
||||
sessionJson: {
|
||||
...sessionJson,
|
||||
sessionStatus: normalizedStatus,
|
||||
source: textValue(sessionJson.source ?? session.session?.source) || "manual-session-create",
|
||||
source: textValue(sessionJson.source ?? session.session?.source) || "session-admission",
|
||||
providerProfile: textValue(sessionJson.providerProfile ?? session.session?.providerProfile) || null,
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
@@ -96,7 +65,7 @@ export async function writeWorkbenchSessionAdmissionFact({ runtimeStore = null,
|
||||
updatedAt,
|
||||
valuesRedacted: true
|
||||
};
|
||||
return runtimeStore.writeWorkbenchFacts({ facts: { sessions: [fact] } }, {
|
||||
return runtimeStore.writeWorkbenchSessionAdmissionFact({ fact }, {
|
||||
sessionId,
|
||||
ownerUserId: fact.ownerUserId,
|
||||
projectId: fact.projectId,
|
||||
@@ -104,257 +73,6 @@ export async function writeWorkbenchSessionAdmissionFact({ runtimeStore = null,
|
||||
});
|
||||
}
|
||||
|
||||
export async function writeWorkbenchProjectionEvent({ runtimeStore = null, event = {}, requestMeta = {}, previousCheckpoint = null } = {}) {
|
||||
if (typeof runtimeStore?.writeWorkbenchFacts !== "function") return null;
|
||||
if (typeof runtimeStore?.allocateWorkbenchProjectedSeq !== "function") throw new Error("Workbench projection event writer requires a durable projectedSeq allocator.");
|
||||
const traceId = safeTraceId(event.traceId ?? requestMeta.traceId);
|
||||
if (!traceId) return null;
|
||||
const projectedAt = new Date().toISOString();
|
||||
const sourceSeq = nonNegativeInteger(event.sourceSeq ?? event.seq);
|
||||
const sessionId = textValue(event.sessionId ?? requestMeta.sessionId) || null;
|
||||
const turnId = textValue(event.turnId) || traceId;
|
||||
const eventType = textValue(event.eventType ?? event.type ?? event.label) || "event";
|
||||
const terminal = event.terminal === true || TERMINAL_STATUSES.has(normalizeWorkbenchStatus(event.status));
|
||||
const status = terminal ? normalizeWorkbenchStatus(event.status) : "running";
|
||||
const checkpointHint = previousCheckpoint && typeof previousCheckpoint === "object" ? previousCheckpoint : null;
|
||||
const resolvedPreviousCheckpoint = checkpointHint ?? await latestWorkbenchCheckpoint(runtimeStore, traceId, { traceStore: defaultCodeAgentTraceStore, phase: "event-checkpoint" });
|
||||
const previousTiming = normalizeTimingProjection(resolvedPreviousCheckpoint?.timing) ?? normalizeTimingProjection(resolvedPreviousCheckpoint);
|
||||
const sourceOccurredAt = timestampValue(event.createdAt ?? event.occurredAt ?? event.updatedAt ?? projectedAt);
|
||||
const occurredAt = latestTimestamp(previousTiming?.lastEventAt, sourceOccurredAt) ?? sourceOccurredAt;
|
||||
const previousTerminal = checkpointIsTerminal(resolvedPreviousCheckpoint);
|
||||
const suppressedAfterSeal = previousTerminal && !terminal;
|
||||
if (suppressedAfterSeal) {
|
||||
emitWorkbenchProjectionOtel("workbench.projection.event.write", traceId, "ok", {
|
||||
"workbench.projection.phase": "event-suppressed-after-seal",
|
||||
"workbench.projection.terminal": false,
|
||||
"workbench.projection.suppressed_after_seal": true,
|
||||
"workbench.projection.session_id": sessionId,
|
||||
"workbench.projection.turn_id": turnId,
|
||||
"workbench.projection.source_seq": sourceSeq,
|
||||
"workbench.projection.event_type": eventType
|
||||
});
|
||||
return { written: false, suppressedAfterSeal: true, valuesPrinted: false };
|
||||
}
|
||||
const explicitEventTiming = normalizeTimingProjection(event.timing) ?? normalizeTimingProjection(event);
|
||||
const explicitDurationMs = durationValue(event.durationMs ?? explicitEventTiming?.durationMs);
|
||||
const explicitStartedAt = optionalTimestampValue(event.startedAt ?? event.traceStartedAt ?? event.runnerStartedAt ?? explicitEventTiming?.startedAt);
|
||||
// Once a trace has a durable checkpoint, startedAt is sealed as the card
|
||||
// elapsed-time source. Later trace/result hydration can carry older or newer
|
||||
// startedAt values, but it must not make the visible Code Agent card jump by
|
||||
// switching to a different clock origin.
|
||||
const provisionalLastEventAt = latestTimestamp(previousTiming?.lastEventAt, explicitEventTiming?.lastEventAt, occurredAt);
|
||||
const provisionalFinishedAt = terminal ? latestTimestamp(explicitEventTiming?.finishedAt, optionalTimestampValue(event.finishedAt), provisionalLastEventAt, occurredAt) : null;
|
||||
const startedAt = previousTiming?.startedAt ?? explicitStartedAt ?? startedAtFromDuration(provisionalFinishedAt, explicitDurationMs) ?? (sourceSeq <= 1 ? occurredAt : null);
|
||||
const lastEventAt = provisionalLastEventAt;
|
||||
const finishedAt = terminal ? latestTimestamp(provisionalFinishedAt, lastEventAt) : null;
|
||||
const timing = eventTimingProjection({ startedAt, lastEventAt, finishedAt, terminal, durationMs: explicitDurationMs ?? previousTiming?.durationMs });
|
||||
const timingAuthorityIssue = terminalTimingAuthorityIssue(timing, { terminal, traceId, source: "event", status, label: event.label, sourceSeq });
|
||||
if (timingAuthorityIssue) emitTerminalTimingAuthorityDiagnostic(defaultCodeAgentTraceStore, traceId, timingAuthorityIssue, {
|
||||
"workbench.projection.phase": "event-timing",
|
||||
"workbench.projection.session_id": sessionId,
|
||||
"workbench.projection.turn_id": turnId,
|
||||
"workbench.projection.source_seq": sourceSeq
|
||||
});
|
||||
const eventTiming = suppressedAfterSeal ? previousTiming ?? timing : timing;
|
||||
const sourceEventId = workbenchSourceEventId(event, { traceId, sourceSeq, occurredAt });
|
||||
const allocation = await runtimeStore.allocateWorkbenchProjectedSeq({
|
||||
traceId,
|
||||
sessionId,
|
||||
turnId,
|
||||
runId: textValue(event.runId) || null,
|
||||
commandId: textValue(event.commandId) || null,
|
||||
sourceSeq,
|
||||
sourceEventId,
|
||||
eventType,
|
||||
occurredAt,
|
||||
updatedAt: projectedAt
|
||||
}, {
|
||||
traceId,
|
||||
sessionId,
|
||||
agentSessionId: requestMeta.agentSessionId ?? sessionId,
|
||||
valuesPrinted: false
|
||||
});
|
||||
const projectedSeq = nonNegativeInteger(allocation?.projectedSeq);
|
||||
if (projectedSeq <= 0) throw new Error("Workbench projection allocator returned an invalid projectedSeq.");
|
||||
const eventId = isAssistantMessageTraceEvent(event) ? stableFactId("wte", { traceId, sourceEventId }) : textValue(event.id) || stableFactId("wte", { traceId, sourceEventId });
|
||||
const previousFinalText = finalResponseTextValue(resolvedPreviousCheckpoint?.finalResponse, resolvedPreviousCheckpoint?.assistantText, resolvedPreviousCheckpoint?.finalText);
|
||||
const liveAssistantText = isAssistantMessageTraceEvent(event) ? finalResponseTextValue(event.assistantText, event.text, event.reply, event.message) : finalResponseTextValue(event.assistantText, event.reply);
|
||||
const eventTerminalFinalResponse = terminal ? terminalFinalResponse(status, event, { evidence: event, finalResponse: event.finalResponse }) : null;
|
||||
const messageFinalText = terminal ? finalResponseTextValue(event.finalResponse, liveAssistantText, previousFinalText, eventTerminalFinalResponse) : null;
|
||||
const checkpointAssistantText = messageFinalText ?? liveAssistantText ?? previousFinalText ?? null;
|
||||
const checkpointFinalResponse = terminal && messageFinalText
|
||||
? { ...(eventTerminalFinalResponse ?? {}), text: messageFinalText, status, traceId, source: "workbench-event-projection", valuesPrinted: false }
|
||||
: resolvedPreviousCheckpoint?.finalResponse ?? null;
|
||||
const terminalSealBlocked = terminal && status === "completed" && !messageFinalText;
|
||||
const factTerminal = terminal && !terminalSealBlocked;
|
||||
const userMessageFact = sessionId && !suppressedAfterSeal && resolvedPreviousCheckpoint?.userMessage ? normalizeMessageFact(resolvedPreviousCheckpoint.userMessage, 0, { traceId, sessionId, turnId, terminal: false, terminalStatus: "sent", finalText: null, timestamp: projectedAt, timing }) : null;
|
||||
const messageFact = sessionId && !suppressedAfterSeal ? normalizeMessageFact({
|
||||
messageId: eventProjectionAssistantMessageId(traceId, event),
|
||||
role: "agent",
|
||||
status,
|
||||
traceId,
|
||||
turnId,
|
||||
text: messageFinalText ?? liveAssistantText ?? previousFinalText ?? "",
|
||||
projectedSeq,
|
||||
sourceSeq,
|
||||
sourceEventId,
|
||||
terminal: factTerminal,
|
||||
sealed: factTerminal,
|
||||
timing,
|
||||
startedAt: timing.startedAt,
|
||||
lastEventAt: timing.lastEventAt,
|
||||
finishedAt: timing.finishedAt,
|
||||
durationMs: timing.durationMs,
|
||||
createdAt: occurredAt,
|
||||
updatedAt: projectedAt,
|
||||
source: "workbench-event-projection"
|
||||
}, userMessageFact ? 1 : 0, { traceId, sessionId, turnId, terminal, terminalStatus: status, finalText: messageFinalText, terminalSealBlocked, timestamp: projectedAt, timing }) : null;
|
||||
const facts = {
|
||||
sessions: sessionId && !suppressedAfterSeal ? [{
|
||||
sessionId,
|
||||
status,
|
||||
lastTraceId: traceId,
|
||||
projectedSeq,
|
||||
sourceSeq,
|
||||
sourceEventId,
|
||||
terminal,
|
||||
sealed: terminal,
|
||||
timing,
|
||||
startedAt: timing.startedAt,
|
||||
lastEventAt: timing.lastEventAt,
|
||||
finishedAt: timing.finishedAt,
|
||||
durationMs: timing.durationMs,
|
||||
updatedAt: projectedAt
|
||||
}] : [],
|
||||
traceEvents: [{
|
||||
...event,
|
||||
id: eventId,
|
||||
traceId,
|
||||
sessionId,
|
||||
turnId,
|
||||
messageId: textValue(event.messageId) || null,
|
||||
sourceSeq,
|
||||
sourceEventId,
|
||||
projectedSeq,
|
||||
eventType,
|
||||
terminal,
|
||||
sealed: terminal,
|
||||
timing: eventTiming,
|
||||
startedAt: eventTiming?.startedAt ?? null,
|
||||
lastEventAt: eventTiming?.lastEventAt ?? null,
|
||||
finishedAt: eventTiming?.finishedAt ?? null,
|
||||
durationMs: eventTiming?.durationMs ?? null,
|
||||
suppressedAfterSeal,
|
||||
sourceOccurredAt: sourceOccurredAt !== occurredAt ? sourceOccurredAt : null,
|
||||
createdAt: occurredAt,
|
||||
occurredAt,
|
||||
updatedAt: projectedAt
|
||||
}],
|
||||
messages: [userMessageFact, messageFact].filter(Boolean),
|
||||
parts: [userMessageFact, messageFact].filter(Boolean).flatMap((message) => messagePartFacts(message, { finalText: message === messageFact ? messageFinalText : null })),
|
||||
turns: sessionId && !suppressedAfterSeal ? [{
|
||||
turnId,
|
||||
sessionId,
|
||||
traceId,
|
||||
messageId: messageFact?.messageId ?? null,
|
||||
status,
|
||||
projectedSeq,
|
||||
sourceSeq,
|
||||
sourceEventId,
|
||||
terminal: factTerminal,
|
||||
sealed: factTerminal,
|
||||
finalResponse: checkpointFinalResponse,
|
||||
assistantText: checkpointAssistantText,
|
||||
failureKind: textValue(event.failureKind ?? event.errorCode) || null,
|
||||
diagnostic: { timingAuthorityIssue, terminalSealBlocked, authority: "workbench-event-projection", valuesRedacted: true },
|
||||
timing,
|
||||
startedAt: timing.startedAt,
|
||||
lastEventAt: timing.lastEventAt,
|
||||
finishedAt: timing.finishedAt,
|
||||
durationMs: timing.durationMs,
|
||||
createdAt: occurredAt,
|
||||
updatedAt: projectedAt
|
||||
}] : [],
|
||||
checkpoints: !suppressedAfterSeal ? [{
|
||||
traceId,
|
||||
sessionId,
|
||||
turnId,
|
||||
runId: textValue(event.runId) || null,
|
||||
commandId: textValue(event.commandId) || null,
|
||||
projectedSeq,
|
||||
sourceSeq,
|
||||
sourceEventId,
|
||||
projectionStatus: terminal ? "caught_up" : "projecting",
|
||||
projectionHealth: terminalSealBlocked ? "degraded" : "healthy",
|
||||
terminal: factTerminal,
|
||||
sealed: factTerminal,
|
||||
assistantText: checkpointAssistantText,
|
||||
finalResponse: checkpointFinalResponse,
|
||||
timing,
|
||||
startedAt: timing.startedAt,
|
||||
lastEventAt: timing.lastEventAt,
|
||||
finishedAt: timing.finishedAt,
|
||||
durationMs: timing.durationMs,
|
||||
diagnostic: {
|
||||
lastEventLabel: textValue(event.label) || null,
|
||||
lastEventStatus: textValue(event.status) || null,
|
||||
timingAuthorityIssue,
|
||||
terminalSealBlocked,
|
||||
valuesRedacted: true
|
||||
},
|
||||
updatedAt: projectedAt
|
||||
}] : []
|
||||
};
|
||||
try {
|
||||
const result = await runtimeStore.writeWorkbenchFacts({ facts }, {
|
||||
traceId,
|
||||
sessionId,
|
||||
agentSessionId: requestMeta.agentSessionId ?? sessionId,
|
||||
valuesPrinted: false
|
||||
});
|
||||
emitWorkbenchProjectionOtel("workbench.projection.event.write", traceId, "ok", {
|
||||
...workbenchProjectionFactCountAttributes(facts),
|
||||
"workbench.projection.phase": terminal ? "event-terminal" : "event-running",
|
||||
"workbench.projection.terminal": terminal,
|
||||
"workbench.projection.suppressed_after_seal": suppressedAfterSeal,
|
||||
"workbench.projection.session_id": sessionId,
|
||||
"workbench.projection.turn_id": turnId,
|
||||
"workbench.projection.projected_seq": projectedSeq,
|
||||
"workbench.projection.source_seq": sourceSeq,
|
||||
"workbench.projection.source_event_id": sourceEventId,
|
||||
"workbench.projection.event_type": eventType,
|
||||
"workbench.projection.event_id": eventId,
|
||||
"workbench.projection.message_id": messageFact?.messageId ?? null
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
emitWorkbenchProjectionOtel("workbench.projection.event.write", traceId, "error", {
|
||||
...workbenchProjectionFactCountAttributes(facts),
|
||||
"workbench.projection.phase": terminal ? "event-terminal" : "event-running",
|
||||
"workbench.projection.terminal": terminal,
|
||||
"workbench.projection.suppressed_after_seal": suppressedAfterSeal,
|
||||
"workbench.projection.session_id": sessionId,
|
||||
"workbench.projection.turn_id": turnId,
|
||||
"workbench.projection.projected_seq": projectedSeq,
|
||||
"workbench.projection.source_seq": sourceSeq,
|
||||
"workbench.projection.source_event_id": sourceEventId,
|
||||
"workbench.projection.event_type": eventType,
|
||||
"workbench.projection.event_id": eventId,
|
||||
"workbench.projection.message_id": messageFact?.messageId ?? null
|
||||
}, error);
|
||||
appendProjectionDiagnostic(defaultCodeAgentTraceStore, traceId, {
|
||||
type: "facts",
|
||||
status: "degraded",
|
||||
label: "projection-writer:event-facts:persist-failed",
|
||||
errorCode: error?.code ?? "workbench_event_facts_persist_failed",
|
||||
message: error?.message ?? "Workbench event facts persistence failed.",
|
||||
terminal: false,
|
||||
valuesPrinted: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildWorkbenchProjectionEventFacts({ event = {}, requestMeta = {}, previousCheckpoint = null, projectedSeq, projectedAt = new Date().toISOString() } = {}) {
|
||||
const traceId = safeTraceId(event.traceId ?? requestMeta.traceId);
|
||||
if (!traceId) throw workbenchProjectionError("workbench_projection_trace_required", "Kafka projection event requires traceId.");
|
||||
@@ -370,7 +88,16 @@ export function buildWorkbenchProjectionEventFacts({ event = {}, requestMeta = {
|
||||
const sourceOccurredAt = timestampValue(event.createdAt ?? event.occurredAt ?? event.updatedAt ?? projectedAt);
|
||||
const occurredAt = latestTimestamp(previousTiming?.lastEventAt, sourceOccurredAt) ?? sourceOccurredAt;
|
||||
if (checkpointIsTerminal(previousCheckpoint) && !terminal) {
|
||||
return { written: false, suppressedAfterSeal: true, traceId, sessionId, sourceSeq, valuesPrinted: false };
|
||||
return {
|
||||
written: false,
|
||||
suppressedAfterSeal: true,
|
||||
traceId,
|
||||
sessionId,
|
||||
sourceSeq,
|
||||
sourceEventId: workbenchSourceEventId(event, { traceId, sourceSeq, occurredAt }),
|
||||
projectedSeq: nonNegativeInteger(previousCheckpoint?.projectedSeq) || durableProjectedSeq,
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
const explicitEventTiming = normalizeTimingProjection(event.timing) ?? normalizeTimingProjection(event);
|
||||
const explicitDurationMs = durationValue(event.durationMs ?? explicitEventTiming?.durationMs);
|
||||
@@ -491,293 +218,6 @@ function eventProjectionMessageId(traceId, role, event = {}) {
|
||||
return `msg_${traceSuffix}_${role === "user" ? "user" : "agent"}`;
|
||||
}
|
||||
|
||||
async function writeWorkbenchProjectionFacts({ runtimeStore = null, traceStore = defaultCodeAgentTraceStore, traceId = null, ownerUserId = null, ownerRole = null, sessionId = null, projectId = null, conversationId = null, threadId = null, status = "active", session = {}, payload = null, params = {} } = {}) {
|
||||
if (typeof runtimeStore?.writeWorkbenchFacts !== "function") return null;
|
||||
const safeId = safeTraceId(traceId ?? session?.traceId ?? payload?.traceId ?? params?.traceId);
|
||||
const previousCheckpoint = safeId ? await latestWorkbenchCheckpoint(runtimeStore, safeId, { traceStore, phase: "facts-checkpoint" }) : null;
|
||||
const facts = buildWorkbenchProjectionFacts({ traceId, ownerUserId, ownerRole, sessionId, projectId, conversationId, threadId, status, session, payload, params, previousCheckpoint });
|
||||
if (isEmptyFacts(facts)) return null;
|
||||
const timingAuthorityIssue = facts.checkpoints?.[0]?.diagnostic?.timingAuthorityIssue ?? null;
|
||||
if (timingAuthorityIssue) emitTerminalTimingAuthorityDiagnostic(traceStore, safeId, timingAuthorityIssue, {
|
||||
"workbench.projection.phase": "facts-timing",
|
||||
"workbench.projection.session_id": facts.sessions[0]?.sessionId ?? sessionId ?? null,
|
||||
"workbench.projection.turn_id": facts.turns[0]?.turnId ?? null
|
||||
});
|
||||
try {
|
||||
const result = await runtimeStore.writeWorkbenchFacts({ facts }, {
|
||||
traceId: facts.checkpoints[0]?.traceId ?? traceId,
|
||||
sessionId: facts.sessions[0]?.sessionId ?? sessionId,
|
||||
ownerUserId,
|
||||
projectId,
|
||||
valuesPrinted: false
|
||||
});
|
||||
emitWorkbenchProjectionOtel("workbench.projection.facts.write", safeId, "ok", {
|
||||
...workbenchProjectionFactCountAttributes(facts),
|
||||
"workbench.projection.terminal_trace_backfill_written": 0,
|
||||
"workbench.projection.phase": "session-facts",
|
||||
"workbench.projection.terminal": facts.turns.some((turn) => turn.terminal === true),
|
||||
"workbench.projection.session_id": facts.sessions[0]?.sessionId ?? sessionId ?? null,
|
||||
"workbench.projection.turn_id": facts.turns[0]?.turnId ?? null,
|
||||
"workbench.projection.projected_seq": facts.checkpoints[0]?.projectedSeq ?? facts.turns[0]?.projectedSeq ?? facts.messages[0]?.projectedSeq ?? null,
|
||||
"workbench.projection.source_seq": facts.checkpoints[0]?.sourceSeq ?? facts.turns[0]?.sourceSeq ?? facts.messages[0]?.sourceSeq ?? null,
|
||||
"workbench.projection.source_event_id": facts.checkpoints[0]?.sourceEventId ?? facts.turns[0]?.sourceEventId ?? facts.messages[0]?.sourceEventId ?? null,
|
||||
"workbench.projection.message_id": facts.messages[0]?.messageId ?? null
|
||||
});
|
||||
return result;
|
||||
} catch (error) {
|
||||
const safeId = safeTraceId(traceId);
|
||||
emitWorkbenchProjectionOtel("workbench.projection.facts.write", safeId, "error", {
|
||||
...workbenchProjectionFactCountAttributes(facts),
|
||||
"workbench.projection.phase": "session-facts",
|
||||
"workbench.projection.terminal": facts.turns.some((turn) => turn.terminal === true),
|
||||
"workbench.projection.session_id": facts.sessions[0]?.sessionId ?? sessionId ?? null,
|
||||
"workbench.projection.turn_id": facts.turns[0]?.turnId ?? null,
|
||||
"workbench.projection.projected_seq": facts.checkpoints[0]?.projectedSeq ?? facts.turns[0]?.projectedSeq ?? facts.messages[0]?.projectedSeq ?? null,
|
||||
"workbench.projection.source_seq": facts.checkpoints[0]?.sourceSeq ?? facts.turns[0]?.sourceSeq ?? facts.messages[0]?.sourceSeq ?? null,
|
||||
"workbench.projection.source_event_id": facts.checkpoints[0]?.sourceEventId ?? facts.turns[0]?.sourceEventId ?? facts.messages[0]?.sourceEventId ?? null,
|
||||
"workbench.projection.message_id": facts.messages[0]?.messageId ?? null
|
||||
}, error);
|
||||
if (safeId) appendProjectionDiagnostic(traceStore, safeId, {
|
||||
type: "facts",
|
||||
status: "degraded",
|
||||
label: "projection-writer:facts:persist-failed",
|
||||
errorCode: error?.code ?? "workbench_facts_persist_failed",
|
||||
message: error?.message ?? "Workbench facts persistence failed.",
|
||||
terminal: false
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function backfillTerminalTraceEvents({ runtimeStore = null, traceStore = defaultCodeAgentTraceStore, traceId = null, facts = {}, payload = null, status = null } = {}) {
|
||||
if (typeof runtimeStore?.writeWorkbenchFacts !== "function" || typeof runtimeStore?.allocateWorkbenchProjectedSeq !== "function") return null;
|
||||
const safeId = safeTraceId(traceId ?? facts?.checkpoints?.[0]?.traceId ?? facts?.turns?.[0]?.traceId);
|
||||
const checkpoint = facts?.checkpoints?.[0] ?? null;
|
||||
const turn = facts?.turns?.[0] ?? null;
|
||||
if (!safeId || checkpoint?.terminal !== true || turn?.terminal !== true) return null;
|
||||
const terminalStatus = normalizeWorkbenchStatus(turn?.status ?? checkpoint?.status ?? status);
|
||||
if (terminalStatus !== "completed") return null;
|
||||
const finalText = finalResponseTextValue(turn?.finalResponse, checkpoint?.finalResponse, turn?.assistantText, checkpoint?.assistantText, payload?.finalResponse, payload?.assistantText);
|
||||
if (!finalText) return null;
|
||||
const sessionId = textValue(checkpoint?.sessionId ?? facts?.sessions?.[0]?.sessionId) || null;
|
||||
const turnId = textValue(turn?.turnId ?? checkpoint?.turnId) || safeId;
|
||||
const runId = textValue(checkpoint?.runId ?? payload?.agentRun?.runId) || null;
|
||||
const commandId = textValue(checkpoint?.commandId ?? payload?.agentRun?.commandId) || null;
|
||||
const assistantMessageId = textValue(turn?.messageId ?? facts?.messages?.find((message) => message.role !== "user" && message.traceId === safeId)?.messageId) || null;
|
||||
try {
|
||||
const existingEvents = await existingWorkbenchTraceEvents(runtimeStore, safeId, { traceStore });
|
||||
if (existingEvents === null) return { written: 0, skipped: true, errorCode: "workbench_terminal_trace_backfill_probe_failed", valuesPrinted: false };
|
||||
const writeAssistantFinal = !hasTerminalAssistantFinalTraceEvent(existingEvents, finalText);
|
||||
const writeCompletion = !hasTerminalResultTraceEvent(existingEvents);
|
||||
if (!writeAssistantFinal && !writeCompletion) return { written: 0, idempotent: true, skippedExisting: true, valuesPrinted: false };
|
||||
const timestamp = latestTimestamp(turn?.finishedAt, checkpoint?.finishedAt, turn?.lastEventAt, checkpoint?.lastEventAt, turn?.updatedAt, checkpoint?.updatedAt, payload?.updatedAt) ?? new Date().toISOString();
|
||||
const baseSourceSeq = Math.max(nonNegativeInteger(checkpoint?.sourceSeq), nonNegativeInteger(turn?.sourceSeq));
|
||||
let offset = 1;
|
||||
let written = 0;
|
||||
if (writeAssistantFinal) {
|
||||
await writeWorkbenchProjectionEvent({
|
||||
runtimeStore,
|
||||
previousCheckpoint: checkpoint,
|
||||
event: {
|
||||
traceId: safeId,
|
||||
sessionId,
|
||||
turnId,
|
||||
messageId: assistantMessageId,
|
||||
source: "workbench-terminal-projection",
|
||||
sourceSeq: baseSourceSeq + offset,
|
||||
sourceEventId: `${safeId}:workbench-terminal-projection:assistant-final`,
|
||||
type: "assistant",
|
||||
eventType: "assistant",
|
||||
status: "completed",
|
||||
label: "agentrun:assistant:message",
|
||||
message: finalText,
|
||||
text: finalText,
|
||||
finalResponse: { text: finalText, status: "completed", traceId: safeId, valuesPrinted: false },
|
||||
final: true,
|
||||
replyAuthority: true,
|
||||
terminal: true,
|
||||
runId,
|
||||
commandId,
|
||||
timing: checkpoint?.timing,
|
||||
startedAt: checkpoint?.startedAt,
|
||||
lastEventAt: checkpoint?.lastEventAt,
|
||||
finishedAt: checkpoint?.finishedAt,
|
||||
durationMs: checkpoint?.durationMs,
|
||||
createdAt: timestamp,
|
||||
occurredAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
valuesPrinted: false
|
||||
}
|
||||
});
|
||||
written += 1;
|
||||
offset += 1;
|
||||
}
|
||||
if (writeCompletion) {
|
||||
await writeWorkbenchProjectionEvent({
|
||||
runtimeStore,
|
||||
previousCheckpoint: checkpoint,
|
||||
event: {
|
||||
traceId: safeId,
|
||||
sessionId,
|
||||
turnId,
|
||||
messageId: assistantMessageId,
|
||||
source: "workbench-terminal-projection",
|
||||
sourceSeq: baseSourceSeq + offset,
|
||||
sourceEventId: `${safeId}:workbench-terminal-projection:completion`,
|
||||
type: "result",
|
||||
eventType: "terminal",
|
||||
status: "completed",
|
||||
label: "agentrun:result:completed",
|
||||
message: "AgentRun result is committed to the Workbench projection event stream.",
|
||||
finalResponse: { text: finalText, status: "completed", traceId: safeId, valuesPrinted: false },
|
||||
terminal: true,
|
||||
sealed: true,
|
||||
runId,
|
||||
commandId,
|
||||
timing: checkpoint?.timing,
|
||||
startedAt: checkpoint?.startedAt,
|
||||
lastEventAt: checkpoint?.lastEventAt,
|
||||
finishedAt: checkpoint?.finishedAt,
|
||||
durationMs: checkpoint?.durationMs,
|
||||
createdAt: timestamp,
|
||||
occurredAt: timestamp,
|
||||
updatedAt: timestamp,
|
||||
valuesPrinted: false
|
||||
}
|
||||
});
|
||||
written += 1;
|
||||
}
|
||||
return { written, idempotent: true, valuesPrinted: false };
|
||||
} catch (error) {
|
||||
emitWorkbenchProjectionOtel("workbench.projection.terminal_trace_backfill", safeId, "error", {
|
||||
"workbench.projection.phase": "terminal-trace-backfill",
|
||||
"workbench.projection.session_id": sessionId,
|
||||
"workbench.projection.turn_id": turnId
|
||||
}, error);
|
||||
appendProjectionDiagnostic(traceStore, safeId, {
|
||||
type: "facts",
|
||||
status: "degraded",
|
||||
label: "projection-writer:terminal-trace-backfill-failed",
|
||||
errorCode: error?.code ?? "workbench_terminal_trace_backfill_failed",
|
||||
message: error?.message ?? "Workbench terminal trace event backfill failed.",
|
||||
terminal: false,
|
||||
valuesPrinted: false
|
||||
});
|
||||
return { written: 0, errorCode: error?.code ?? "workbench_terminal_trace_backfill_failed", valuesPrinted: false };
|
||||
}
|
||||
}
|
||||
|
||||
function buildWorkbenchProjectionFacts({ traceId = null, ownerUserId = null, ownerRole = null, sessionId = null, projectId = null, conversationId = null, threadId = null, status = "active", session = {}, payload = null, params = {}, previousCheckpoint = null } = {}) {
|
||||
const safeId = safeTraceId(traceId ?? session?.traceId ?? payload?.traceId ?? params?.traceId);
|
||||
const resolvedSessionId = textValue(sessionId ?? session?.sessionId ?? payload?.sessionId ?? params?.sessionId) || null;
|
||||
const resolvedConversationId = textValue(conversationId ?? session?.conversationId ?? payload?.conversationId ?? params?.conversationId) || null;
|
||||
const resolvedThreadId = textValue(threadId ?? session?.threadId ?? payload?.threadId ?? params?.threadId) || null;
|
||||
const agentRun = payload?.agentRun ?? session?.agentRun ?? null;
|
||||
const timestamp = timestampValue(payload?.updatedAt ?? session?.updatedAt ?? payload?.createdAt ?? session?.createdAt);
|
||||
const normalizedStatus = normalizeWorkbenchStatus(payload?.status ?? session?.sessionStatus ?? status);
|
||||
const projection = createWorkbenchTurnProjection({ traceId: safeId, result: payload, session: { id: resolvedSessionId, status: normalizedStatus, session }, trace: payload?.runnerTrace ?? null });
|
||||
const projectedStatus = normalizeWorkbenchStatus(projection.status ?? normalizedStatus);
|
||||
const terminal = projection.terminal === true;
|
||||
const terminalSealBlocked = projection.terminalSealBlocked === true;
|
||||
const terminalStatus = terminal ? (TERMINAL_STATUSES.has(projectedStatus) ? projectedStatus : normalizedStatus) : projectedStatus;
|
||||
const timing = terminalTimingAtLeastProjectedAt(projectionTimingForStatus(projection.timing, terminal), terminal);
|
||||
const timingAuthorityIssue = terminalTimingAuthorityIssue(timing, { terminal, traceId: safeId, source: "facts", status: terminalStatus, label: payload?.lastEventLabel, sourceSeq: projection.lastProjectedSeq });
|
||||
const baseDiagnostic = projectionDiagnostics({ traceId: safeId, result: payload, trace: payload?.runnerTrace ?? null, projection });
|
||||
const diagnostic = timingAuthorityIssue
|
||||
? { ...baseDiagnostic, projectionHealth: baseDiagnostic.projectionHealth === "unavailable" ? "unavailable" : "degraded", blocker: baseDiagnostic.blocker ?? timingAuthorityIssue, timingAuthorityIssue, valuesRedacted: true }
|
||||
: baseDiagnostic;
|
||||
const factSeq = workbenchProjectionFactSequence({ projection, previousCheckpoint, agentRun });
|
||||
const previousFinalText = finalResponseTextValue(previousCheckpoint?.finalResponse, previousCheckpoint?.assistantText, previousCheckpoint?.finalText);
|
||||
const finalText = terminal ? finalResponseTextValue(projection.finalResponse, payload?.finalResponse, session?.finalResponse, payload?.assistantText, previousFinalText) : null;
|
||||
const projectedFinalResponse = terminal && finalText ? { text: finalText, status: terminalStatus, traceId: safeId, valuesPrinted: false } : null;
|
||||
const messages = normalizeMessages(workbenchProjectionInputMessages({ session, payload, params, traceId: safeId, timestamp, terminal, terminalStatus, finalText, previousCheckpoint }), {
|
||||
traceId: safeId,
|
||||
sessionId: resolvedSessionId,
|
||||
conversationId: resolvedConversationId,
|
||||
threadId: resolvedThreadId,
|
||||
terminal,
|
||||
terminalStatus,
|
||||
finalText,
|
||||
terminalSealBlocked,
|
||||
timestamp,
|
||||
timing
|
||||
});
|
||||
const sessionJson = workbenchSessionJsonWithLaunchContext(session, { payload, params });
|
||||
return {
|
||||
sessions: resolvedSessionId ? [{
|
||||
sessionId: resolvedSessionId,
|
||||
ownerUserId,
|
||||
ownerRole,
|
||||
projectId: projectId ?? session?.projectId ?? payload?.projectId ?? params?.projectId ?? null,
|
||||
conversationId: resolvedConversationId,
|
||||
threadId: resolvedThreadId,
|
||||
status: terminal ? terminalStatus : "running",
|
||||
lastTraceId: safeId,
|
||||
projectedSeq: factSeq.projectedSeq,
|
||||
sourceSeq: factSeq.sourceSeq,
|
||||
sourceEventId: safeId,
|
||||
terminal,
|
||||
sealed: terminal,
|
||||
sessionJson,
|
||||
timing,
|
||||
startedAt: timing.startedAt,
|
||||
lastEventAt: timing.lastEventAt,
|
||||
finishedAt: timing.finishedAt,
|
||||
durationMs: timing.durationMs,
|
||||
createdAt: timestampValue(session?.createdAt ?? payload?.createdAt ?? timestamp),
|
||||
updatedAt: timestamp
|
||||
}] : [],
|
||||
messages,
|
||||
parts: messages.flatMap((message) => messagePartFacts(message, { finalText: message.role !== "user" ? finalText : null })),
|
||||
turns: safeId && resolvedSessionId ? [{
|
||||
turnId: projection.turnId ?? safeId,
|
||||
sessionId: resolvedSessionId,
|
||||
traceId: safeId,
|
||||
messageId: messages.find((message) => message.role !== "user")?.messageId ?? null,
|
||||
status: projection.status ?? normalizedStatus,
|
||||
projectedSeq: factSeq.projectedSeq,
|
||||
sourceSeq: factSeq.sourceSeq,
|
||||
sourceEventId: safeId,
|
||||
terminal: projection.terminal,
|
||||
sealed: projection.terminal,
|
||||
finalResponse: projectedFinalResponse,
|
||||
diagnostic,
|
||||
assistantText: finalText,
|
||||
userMessage: previousCheckpoint?.userMessage ?? null,
|
||||
timing,
|
||||
startedAt: timing.startedAt,
|
||||
lastEventAt: timing.lastEventAt,
|
||||
finishedAt: timing.finishedAt,
|
||||
durationMs: timing.durationMs,
|
||||
updatedAt: timestamp
|
||||
}] : [],
|
||||
checkpoints: safeId ? [{
|
||||
traceId: safeId,
|
||||
sessionId: resolvedSessionId,
|
||||
turnId: projection.turnId ?? safeId,
|
||||
runId: textValue(agentRun?.runId ?? projection.sourceRunId) || null,
|
||||
commandId: textValue(agentRun?.commandId ?? projection.sourceCommandId) || null,
|
||||
projectedSeq: factSeq.projectedSeq,
|
||||
sourceSeq: factSeq.sourceSeq,
|
||||
sourceEventId: safeId,
|
||||
projectionStatus: projection.terminal ? "caught_up" : "projecting",
|
||||
projectionHealth: diagnostic.projectionHealth === "unavailable" ? "unavailable" : diagnostic.projectionHealth === "stalled" ? "stalled" : "healthy",
|
||||
terminal: projection.terminal,
|
||||
sealed: projection.terminal,
|
||||
diagnostic,
|
||||
finalResponse: projectedFinalResponse,
|
||||
assistantText: finalText,
|
||||
userMessage: messages.find((message) => message.role === "user") ?? null,
|
||||
timing,
|
||||
startedAt: timing.startedAt,
|
||||
lastEventAt: timing.lastEventAt,
|
||||
finishedAt: timing.finishedAt,
|
||||
durationMs: timing.durationMs,
|
||||
updatedAt: timestamp
|
||||
}] : []
|
||||
};
|
||||
}
|
||||
|
||||
function workbenchSessionJsonWithLaunchContext(session = {}, { payload = null, params = null } = {}) {
|
||||
const base = plainObjectValue(session) ?? {};
|
||||
const launchContext = firstPlainObjectValue(
|
||||
@@ -792,14 +232,7 @@ function workbenchSessionJsonWithLaunchContext(session = {}, { payload = null, p
|
||||
params?.session?.launchContext
|
||||
);
|
||||
if (!launchContext) return base;
|
||||
const next = {
|
||||
...base,
|
||||
launchContext: {
|
||||
...launchContext,
|
||||
valuesRedacted: true
|
||||
},
|
||||
valuesRedacted: true
|
||||
};
|
||||
const next = { ...base, launchContext: { ...launchContext, valuesRedacted: true }, valuesRedacted: true };
|
||||
if (!Object.prototype.hasOwnProperty.call(next, "secretMaterialStored")) next.secretMaterialStored = false;
|
||||
return next;
|
||||
}
|
||||
@@ -816,16 +249,6 @@ function plainObjectValue(value) {
|
||||
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
||||
}
|
||||
|
||||
function workbenchProjectionFactSequence({ projection = {}, previousCheckpoint = null, agentRun = null } = {}) {
|
||||
const projectionSeq = nonNegativeInteger(projection?.lastProjectedSeq);
|
||||
const previousSeq = nonNegativeInteger(previousCheckpoint?.projectedSeq ?? previousCheckpoint?.lastProjectedSeq);
|
||||
const sourceSeq = nonNegativeInteger(agentRun?.lastSeq ?? projection?.sourceSeq ?? projectionSeq);
|
||||
return {
|
||||
projectedSeq: previousSeq > 0 ? Math.min(projectionSeq || previousSeq, previousSeq) : projectionSeq,
|
||||
sourceSeq
|
||||
};
|
||||
}
|
||||
|
||||
function workbenchProjectionError(code, message) {
|
||||
const error = new Error(message);
|
||||
error.code = code;
|
||||
@@ -833,121 +256,6 @@ function workbenchProjectionError(code, message) {
|
||||
return error;
|
||||
}
|
||||
|
||||
function emitWorkbenchProjectionOtel(name, traceId, status = "ok", attributes = {}, error = null) {
|
||||
void emitCodeAgentOtelSpan(name, safeTraceId(traceId) ?? "trc_unassigned", process.env, {
|
||||
status,
|
||||
error,
|
||||
attributes: {
|
||||
...attributes,
|
||||
"workbench.projection.zero_implicit_fallback": true,
|
||||
"workbench.projection.error_visible": status === "error"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function workbenchProjectionFactCountAttributes(facts = {}) {
|
||||
return {
|
||||
"workbench.facts.sessions": Array.isArray(facts.sessions) ? facts.sessions.length : 0,
|
||||
"workbench.facts.messages": Array.isArray(facts.messages) ? facts.messages.length : 0,
|
||||
"workbench.facts.parts": Array.isArray(facts.parts) ? facts.parts.length : 0,
|
||||
"workbench.facts.turns": Array.isArray(facts.turns) ? facts.turns.length : 0,
|
||||
"workbench.facts.trace_events": Array.isArray(facts.traceEvents) ? facts.traceEvents.length : 0,
|
||||
"workbench.facts.checkpoints": Array.isArray(facts.checkpoints) ? facts.checkpoints.length : 0
|
||||
};
|
||||
}
|
||||
|
||||
function workbenchProjectionInputMessages({ session = {}, payload = {}, params = {}, traceId = null, timestamp = null, terminal = false, terminalStatus = "completed", finalText = null, previousCheckpoint = null } = {}) {
|
||||
if (Array.isArray(session?.messages) && session.messages.length > 0) return preserveTerminalUserMessage(session.messages, { terminal, traceId, previousCheckpoint });
|
||||
if (Array.isArray(payload?.messages) && payload.messages.length > 0) return preserveTerminalUserMessage(payload.messages, { terminal, traceId, previousCheckpoint });
|
||||
if (!traceId) return [];
|
||||
const turnId = textValue(payload?.turnId) || traceId;
|
||||
const assistantMessageId = eventProjectionAssistantMessageId(traceId, payload);
|
||||
const createdAt = timestampValue(timestamp ?? payload?.createdAt ?? payload?.updatedAt);
|
||||
const messages = [];
|
||||
const userMessage = workbenchProjectionUserMessage({ payload, params, traceId, turnId, timestamp: createdAt, terminal });
|
||||
if (userMessage) messages.push(userMessage);
|
||||
messages.push({
|
||||
messageId: assistantMessageId,
|
||||
role: "agent",
|
||||
status: terminal ? terminalStatus : "running",
|
||||
text: terminal && finalText ? finalText : "",
|
||||
traceId,
|
||||
turnId,
|
||||
timing: payload?.timing,
|
||||
startedAt: payload?.startedAt,
|
||||
lastEventAt: payload?.lastEventAt,
|
||||
finishedAt: payload?.finishedAt,
|
||||
durationMs: payload?.durationMs,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
valuesPrinted: false
|
||||
});
|
||||
return messages;
|
||||
}
|
||||
|
||||
function workbenchProjectionUserMessage({ payload = {}, params = {}, traceId = null, turnId = null, timestamp = null, terminal = false } = {}) {
|
||||
const userText = textValue(params?.message ?? params?.prompt ?? payload?.prompt ?? payload?.userText ?? payload?.userMessage?.text ?? payload?.userMessage?.content ?? (!terminal ? payload?.message : null));
|
||||
if (!traceId || !userText) return null;
|
||||
const createdAt = timestampValue(timestamp ?? payload?.createdAt ?? payload?.updatedAt);
|
||||
return {
|
||||
messageId: eventProjectionUserMessageId(traceId, payload),
|
||||
role: "user",
|
||||
status: "sent",
|
||||
text: userText,
|
||||
traceId,
|
||||
turnId: textValue(turnId) || traceId,
|
||||
createdAt,
|
||||
updatedAt: createdAt,
|
||||
valuesPrinted: false
|
||||
};
|
||||
}
|
||||
|
||||
function preserveTerminalUserMessage(messages = [], { terminal = false, traceId = null, previousCheckpoint = null } = {}) {
|
||||
if (!terminal || !traceId) return messages;
|
||||
const previousUserMessage = previousCheckpoint?.userMessage ?? null;
|
||||
const previousText = textValue(previousUserMessage?.text ?? previousUserMessage?.content ?? previousUserMessage?.message);
|
||||
let handledUser = false;
|
||||
const normalizedTraceId = safeTraceId(traceId) ?? traceId;
|
||||
const preserved = messages.map((message) => {
|
||||
const role = textValue(message?.role) || "agent";
|
||||
const messageTraceId = safeTraceId(message?.traceId ?? message?.turnId ?? normalizedTraceId) ?? normalizedTraceId;
|
||||
if (role !== "user" || messageTraceId !== normalizedTraceId) return message;
|
||||
handledUser = true;
|
||||
const messageText = textValue(message?.text ?? message?.content ?? message?.message);
|
||||
if (!previousText && isSyntheticTerminalUserNotice(messageText)) return null;
|
||||
if (!previousText) return message;
|
||||
return {
|
||||
...message,
|
||||
...previousUserMessage,
|
||||
messageId: textValue(previousUserMessage.messageId ?? previousUserMessage.id ?? message?.messageId ?? message?.id) || eventProjectionUserMessageId(normalizedTraceId, message),
|
||||
role: "user",
|
||||
status: "sent",
|
||||
text: previousText,
|
||||
traceId: normalizedTraceId,
|
||||
turnId: textValue(previousUserMessage.turnId ?? message?.turnId) || normalizedTraceId,
|
||||
terminal: false,
|
||||
sealed: false
|
||||
};
|
||||
}).filter(Boolean);
|
||||
if (handledUser) return preserved;
|
||||
if (!previousText) return preserved;
|
||||
return [previousUserMessage, ...messages];
|
||||
}
|
||||
|
||||
function isSyntheticTerminalUserNotice(text) {
|
||||
const value = textValue(text);
|
||||
return /当前\s*AgentRun\s*请求已取消|请求已取消|hwlab-user-cancel/u.test(value);
|
||||
}
|
||||
|
||||
function normalizeMessages(messages, context) {
|
||||
const normalized = Array.isArray(messages) ? messages.map((message, index) => normalizeMessageFact(message, index, context)).filter(Boolean) : [];
|
||||
if (context?.terminal && context?.finalText && context?.traceId && !normalized.some((message) => message.role !== "user" && message.traceId === context.traceId)) {
|
||||
const projected = normalizeMessageFact({ messageId: eventProjectionAssistantMessageId(context.traceId), role: "agent", status: context.terminalStatus ?? "completed", text: context.finalText, traceId: context.traceId, source: "workbench-terminal-projection" }, normalized.length, context);
|
||||
if (projected) normalized.push(projected);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function messagePartFacts(message = {}, { finalText = null } = {}) {
|
||||
const messageId = textValue(message.messageId);
|
||||
if (!messageId || !message.sessionId) return [];
|
||||
@@ -978,50 +286,6 @@ function messagePartFacts(message = {}, { finalText = null } = {}) {
|
||||
}];
|
||||
}
|
||||
|
||||
async function existingWorkbenchTraceEvents(runtimeStore, traceId, { traceStore = defaultCodeAgentTraceStore } = {}) {
|
||||
if (typeof runtimeStore?.queryWorkbenchFacts !== "function") return [];
|
||||
const safeId = safeTraceId(traceId);
|
||||
try {
|
||||
const result = await runtimeStore.queryWorkbenchFacts({ traceId: safeId ?? traceId, families: ["traceEvents"], limit: 500 });
|
||||
return Array.isArray(result?.facts?.traceEvents) ? result.facts.traceEvents : [];
|
||||
} catch (error) {
|
||||
emitWorkbenchProjectionOtel("workbench.projection.terminal_trace_backfill.probe", safeId, "error", {
|
||||
"workbench.projection.phase": "terminal-trace-backfill-probe"
|
||||
}, error);
|
||||
if (safeId) appendProjectionDiagnostic(traceStore, safeId, {
|
||||
type: "facts",
|
||||
status: "degraded",
|
||||
label: "projection-writer:terminal-trace-backfill-probe-failed",
|
||||
errorCode: error?.code ?? "workbench_terminal_trace_backfill_probe_failed",
|
||||
message: error?.message ?? "Workbench terminal trace backfill probe failed.",
|
||||
terminal: false,
|
||||
valuesPrinted: false
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function hasTerminalAssistantFinalTraceEvent(events = [], finalText = null) {
|
||||
const expectedText = finalResponseTextValue(finalText);
|
||||
return events.some((event) => {
|
||||
const type = textValue(event?.eventType ?? event?.type).toLowerCase();
|
||||
const label = textValue(event?.label).toLowerCase();
|
||||
if (type !== "assistant" && !/assistant:message|assistant_message/u.test(label)) return false;
|
||||
if (event?.terminal !== true && normalizeWorkbenchStatus(event?.status) !== "completed") return false;
|
||||
const text = finalResponseTextValue(event?.finalResponse, event?.text, event?.message, event?.content);
|
||||
return !expectedText || text === expectedText;
|
||||
});
|
||||
}
|
||||
|
||||
function hasTerminalResultTraceEvent(events = []) {
|
||||
return events.some((event) => {
|
||||
const type = textValue(event?.eventType ?? event?.type).toLowerCase();
|
||||
const label = textValue(event?.label).toLowerCase();
|
||||
const status = normalizeWorkbenchStatus(event?.status);
|
||||
return (type === "terminal" || type === "result" || /result:completed|result_completed/u.test(label)) && event?.terminal === true && status === "completed";
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeMessageFact(message = {}, index, { traceId, sessionId, conversationId, threadId, terminal, terminalStatus = "completed", finalText = null, terminalSealBlocked = false, timestamp, timing: contextTiming }) {
|
||||
const role = textValue(message.role) || "agent";
|
||||
const messageId = textValue(message.messageId ?? message.id) || (role === "user" ? eventProjectionUserMessageId(traceId, message) : eventProjectionAssistantMessageId(traceId, message));
|
||||
@@ -1077,48 +341,6 @@ function finalResponseTextValue(...values) {
|
||||
return null;
|
||||
}
|
||||
|
||||
async function latestWorkbenchCheckpoint(runtimeStore, traceId, { traceStore = defaultCodeAgentTraceStore, phase = "checkpoint-read" } = {}) {
|
||||
if (typeof runtimeStore?.queryWorkbenchFacts !== "function") return null;
|
||||
const safeId = safeTraceId(traceId);
|
||||
try {
|
||||
const result = await runtimeStore.queryWorkbenchFacts({ traceId: safeId ?? traceId, families: ["checkpoints"], limit: 1 });
|
||||
return Array.isArray(result?.facts?.checkpoints) ? result.facts.checkpoints[0] ?? null : null;
|
||||
} catch (error) {
|
||||
emitWorkbenchProjectionOtel("workbench.projection.checkpoint.read", safeId, "error", {
|
||||
"workbench.projection.phase": phase
|
||||
}, error);
|
||||
if (safeId) appendProjectionDiagnostic(traceStore, safeId, {
|
||||
type: "facts",
|
||||
status: "degraded",
|
||||
label: "projection-writer:checkpoint-read-failed",
|
||||
errorCode: error?.code ?? "workbench_checkpoint_read_failed",
|
||||
message: error?.message ?? "Workbench checkpoint read failed.",
|
||||
terminal: false,
|
||||
valuesPrinted: false
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function projectionTimingForStatus(value, terminal) {
|
||||
const timing = normalizeTimingProjection(value) ?? emptyTimingProjection();
|
||||
if (terminal) return timing;
|
||||
return { ...timing, finishedAt: null, durationMs: null, valuesRedacted: timing.valuesRedacted !== false };
|
||||
}
|
||||
|
||||
function terminalTimingAtLeastProjectedAt(value, terminal) {
|
||||
const timing = normalizeTimingProjection(value) ?? emptyTimingProjection();
|
||||
if (!terminal) return timing;
|
||||
const finishedAt = timing.finishedAt ?? null;
|
||||
const durationMs = durationValue(timing.durationMs) ?? elapsedBetween(timing.startedAt, finishedAt);
|
||||
return {
|
||||
...timing,
|
||||
finishedAt,
|
||||
durationMs,
|
||||
valuesRedacted: timing.valuesRedacted !== false
|
||||
};
|
||||
}
|
||||
|
||||
function checkpointIsTerminal(value) {
|
||||
const source = value && typeof value === "object" ? value : null;
|
||||
if (!source) return false;
|
||||
@@ -1200,27 +422,6 @@ function terminalTimingAuthorityIssue(timing, { terminal = false, traceId = null
|
||||
};
|
||||
}
|
||||
|
||||
function emitTerminalTimingAuthorityDiagnostic(traceStore, traceId, issue, attributes = {}) {
|
||||
if (!issue) return null;
|
||||
emitWorkbenchProjectionOtel("workbench.projection.terminal_timing_authority", traceId, "error", {
|
||||
...attributes,
|
||||
"workbench.projection.timing_authority_missing": true,
|
||||
"workbench.projection.missing_started_at": issue.missingStartedAt === true,
|
||||
"workbench.projection.missing_finished_at": issue.missingFinishedAt === true,
|
||||
"workbench.projection.missing_duration_ms": issue.missingDurationMs === true
|
||||
}, workbenchProjectionError(issue.code, issue.message));
|
||||
return appendProjectionDiagnostic(traceStore, traceId, {
|
||||
type: "projection-timing",
|
||||
status: "degraded",
|
||||
label: "projection-writer:terminal-timing:missing-authority",
|
||||
errorCode: issue.code,
|
||||
message: issue.message,
|
||||
terminal: false,
|
||||
timingAuthorityIssue: issue,
|
||||
valuesPrinted: false
|
||||
});
|
||||
}
|
||||
|
||||
function latestTimestamp(...values) {
|
||||
let latest = null;
|
||||
let latestMs = Number.NEGATIVE_INFINITY;
|
||||
@@ -1234,10 +435,6 @@ function latestTimestamp(...values) {
|
||||
return latest;
|
||||
}
|
||||
|
||||
function isEmptyFacts(facts) {
|
||||
return Object.values(facts).every((items) => !Array.isArray(items) || items.length === 0);
|
||||
}
|
||||
|
||||
function stableFactId(prefix, value) {
|
||||
return `${prefix}_${createHash("sha256").update(JSON.stringify(sortJson(value))).digest("hex").slice(0, 32)}`;
|
||||
}
|
||||
@@ -1296,15 +493,3 @@ function optionalTimestampValue(value) {
|
||||
function textValue(value) {
|
||||
return String(value ?? "").trim();
|
||||
}
|
||||
|
||||
export function appendProjectionDiagnostic(traceStore = defaultCodeAgentTraceStore, traceId, event = {}) {
|
||||
const safeId = safeTraceId(traceId);
|
||||
if (!safeId || typeof traceStore?.append !== "function") return null;
|
||||
return traceStore.append(safeId, {
|
||||
type: "projection-diagnostic",
|
||||
status: "degraded",
|
||||
label: "projection:diagnostic",
|
||||
...event,
|
||||
valuesPrinted: false
|
||||
});
|
||||
}
|
||||
|
||||
@@ -681,6 +681,26 @@ export class CloudRuntimeStore {
|
||||
};
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
queryWorkbenchFacts(params = {}) {
|
||||
const families = workbenchFactFamilySet(params);
|
||||
const sessions = [...this.workbenchSessions.values()].filter((record) => matchesWorkbenchSessionFactQuery(record, params));
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { test } from "bun:test";
|
||||
import pg from "pg";
|
||||
|
||||
import { buildWorkbenchProjectionEventFacts } from "../cloud/workbench-projection-writer.ts";
|
||||
import { PostgresCloudRuntimeStore } from "./runtime-store-postgres.ts";
|
||||
import { commitAgentRunKafkaProjection as commitProjection } from "./runtime-store-postgres-kafka.ts";
|
||||
|
||||
const dbUrl = String(process.env.HWLAB_KAFKA_PROJECTOR_INTEGRATION_DB_URL ?? "").trim();
|
||||
const enabled = Boolean(dbUrl) && process.env.HWLAB_KAFKA_PROJECTOR_INTEGRATION_CONFIRM_NON_PRODUCTION === "1";
|
||||
|
||||
if (!enabled) {
|
||||
test.skip("Kafka projector real PostgreSQL transaction and fault-injection contract", () => {});
|
||||
} else {
|
||||
test("Kafka projector real PostgreSQL transaction and fault-injection contract", async () => {
|
||||
const { Pool } = pg;
|
||||
const pool = new Pool({ connectionString: dbUrl, ssl: false, max: 4 });
|
||||
const suffix = randomUUID().replaceAll("-", "");
|
||||
const traceId = `trc_it_${suffix}`;
|
||||
const sessionId = `ses_it_${suffix}`;
|
||||
const ownerUserId = `usr_it_${suffix}`;
|
||||
const now = "2026-07-10T12:00:00.000Z";
|
||||
const store = new PostgresCloudRuntimeStore({
|
||||
dbUrl,
|
||||
sslMode: "disable",
|
||||
env: { ...process.env, HWLAB_CLOUD_DB_SSL_MODE: "disable" },
|
||||
queryClient: pool,
|
||||
now: () => now,
|
||||
logger: { error() {}, warn() {}, info() {} }
|
||||
});
|
||||
|
||||
try {
|
||||
const readiness = await store.readiness();
|
||||
assert.equal(readiness.ready, true);
|
||||
assert.equal(readiness.durable, true);
|
||||
assert.equal(readiness.migration?.ready, true);
|
||||
|
||||
await pool.query(
|
||||
"INSERT INTO users (id, username, display_name, role, status, created_at, updated_at) VALUES ($1,$2,$2,'user','active',$3,$3)",
|
||||
[ownerUserId, `integration-${suffix}`, now]
|
||||
);
|
||||
|
||||
await store.writeWorkbenchSessionAdmissionFact({
|
||||
fact: {
|
||||
sessionId,
|
||||
ownerUserId,
|
||||
projectId: `prj_it_${suffix}`,
|
||||
conversationId: `cnv_it_${suffix}`,
|
||||
threadId: `thread_it_${suffix}`,
|
||||
status: "queued",
|
||||
lastTraceId: traceId,
|
||||
projectedSeq: 0,
|
||||
sourceSeq: 0,
|
||||
terminal: false,
|
||||
sealed: false,
|
||||
sessionJson: { sessionId, launchContext: { source: "integration-test" }, valuesRedacted: true },
|
||||
createdAt: now,
|
||||
updatedAt: now
|
||||
}
|
||||
});
|
||||
|
||||
const running = await project(store, {
|
||||
traceId,
|
||||
sessionId,
|
||||
sourceEventId: `evt_running_${suffix}`,
|
||||
sourceSeq: 1,
|
||||
sourceOffset: "100",
|
||||
inputSha256: "1".repeat(64),
|
||||
event: { eventType: "assistant_message", status: "running", assistantText: "partial", createdAt: now }
|
||||
});
|
||||
assert.equal(running.duplicate, false);
|
||||
assert.equal(running.projectedSeq, 1);
|
||||
assert.equal(await count(pool, "workbench_kafka_inbox", "trace_id = $1", [traceId]), 1);
|
||||
assert.equal(await count(pool, "workbench_trace_events", "trace_id = $1", [traceId]), 1);
|
||||
assert.equal(await count(pool, "workbench_projection_outbox", "trace_id = $1", [traceId]), 2);
|
||||
assert.equal(await count(pool, "hwlab_kafka_outbox", "partition_key = $1", [traceId]), 1);
|
||||
|
||||
const sameTransport = await project(store, {
|
||||
traceId,
|
||||
sessionId,
|
||||
sourceEventId: `evt_running_${suffix}`,
|
||||
sourceSeq: 1,
|
||||
sourceOffset: "100",
|
||||
inputSha256: "1".repeat(64),
|
||||
event: { eventType: "assistant_message", status: "running", assistantText: "partial", createdAt: now }
|
||||
});
|
||||
const newOffsetSameEvent = await project(store, {
|
||||
traceId,
|
||||
sessionId,
|
||||
sourceEventId: `evt_running_${suffix}`,
|
||||
sourceSeq: 1,
|
||||
sourceOffset: "101",
|
||||
inputSha256: "1".repeat(64),
|
||||
event: { eventType: "assistant_message", status: "running", assistantText: "partial", createdAt: now }
|
||||
});
|
||||
assert.equal(sameTransport.duplicate, true);
|
||||
assert.equal(newOffsetSameEvent.duplicate, true);
|
||||
assert.equal(await count(pool, "workbench_kafka_inbox", "trace_id = $1", [traceId]), 1);
|
||||
assert.equal(await count(pool, "workbench_projection_outbox", "trace_id = $1", [traceId]), 2);
|
||||
|
||||
const conflict = await project(store, {
|
||||
traceId,
|
||||
sessionId,
|
||||
sourceEventId: `evt_running_${suffix}`,
|
||||
sourceSeq: 1,
|
||||
sourceOffset: "102",
|
||||
inputSha256: "2".repeat(64),
|
||||
event: { eventType: "assistant_message", status: "running", assistantText: "mutated", createdAt: now }
|
||||
});
|
||||
assert.equal(conflict.conflict, true);
|
||||
assert.equal(await count(pool, "workbench_kafka_dlq", "source_event_id = $1", [`evt_running_${suffix}`]), 1);
|
||||
const projectedInbox = await pool.query(
|
||||
"SELECT status, projected_seq FROM workbench_kafka_inbox WHERE source_topic = $1 AND source_event_id = $2",
|
||||
["agentrun.event.v1", `evt_running_${suffix}`]
|
||||
);
|
||||
assert.deepEqual(projectedInbox.rows[0], { status: "projected", projected_seq: 1 });
|
||||
|
||||
await pool.query(`
|
||||
CREATE OR REPLACE FUNCTION issue_2464_delay_inbox_insert() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF NEW.source_topic = 'agentrun.event.v1' AND NEW.source_partition = 0 AND NEW.source_offset = 300 THEN
|
||||
PERFORM pg_sleep(0.25);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
DROP TRIGGER IF EXISTS issue_2464_delay_inbox_insert ON workbench_kafka_inbox;
|
||||
CREATE TRIGGER issue_2464_delay_inbox_insert BEFORE INSERT ON workbench_kafka_inbox
|
||||
FOR EACH ROW EXECUTE FUNCTION issue_2464_delay_inbox_insert();
|
||||
`);
|
||||
try {
|
||||
const concurrent = await Promise.allSettled([
|
||||
project(store, {
|
||||
traceId: `trc_transport_a_${suffix}`,
|
||||
sessionId: `ses_transport_a_${suffix}`,
|
||||
sourceEventId: `evt_transport_a_${suffix}`,
|
||||
sourceSeq: 1,
|
||||
sourceOffset: "300",
|
||||
inputSha256: "6".repeat(64),
|
||||
event: { eventType: "backend_status", status: "running", createdAt: now }
|
||||
}),
|
||||
project(store, {
|
||||
traceId: `trc_transport_b_${suffix}`,
|
||||
sessionId: `ses_transport_b_${suffix}`,
|
||||
sourceEventId: `evt_transport_b_${suffix}`,
|
||||
sourceSeq: 1,
|
||||
sourceOffset: "300",
|
||||
inputSha256: "7".repeat(64),
|
||||
event: { eventType: "backend_status", status: "running", createdAt: now }
|
||||
})
|
||||
]);
|
||||
assert.deepEqual(concurrent.map((result) => result.status), ["fulfilled", "fulfilled"]);
|
||||
const values = concurrent.map((result) => {
|
||||
if (result.status !== "fulfilled") throw result.reason;
|
||||
return result.value;
|
||||
});
|
||||
assert.equal(values.filter((result) => result.conflict === true).length, 1);
|
||||
assert.equal(values.filter((result) => result.conflict !== true && result.duplicate !== true).length, 1);
|
||||
assert.equal(await count(pool, "workbench_kafka_inbox", "source_topic = $1 AND source_partition = 0 AND source_offset = 300", ["agentrun.event.v1"]), 1);
|
||||
assert.equal(await count(pool, "workbench_kafka_dlq", "source_topic = $1 AND source_partition = 0 AND source_offset = 300", ["agentrun.event.v1"]), 1);
|
||||
} finally {
|
||||
await pool.query("DROP TRIGGER IF EXISTS issue_2464_delay_inbox_insert ON workbench_kafka_inbox; DROP FUNCTION IF EXISTS issue_2464_delay_inbox_insert()");
|
||||
}
|
||||
|
||||
const faultTraceId = `trc_fault_${suffix}`;
|
||||
const faultSessionId = `ses_fault_${suffix}`;
|
||||
const faultStore = new PostgresCloudRuntimeStore({
|
||||
dbUrl,
|
||||
sslMode: "disable",
|
||||
env: { ...process.env, HWLAB_CLOUD_DB_SSL_MODE: "disable" },
|
||||
queryClient: faultInjectingPool(pool, /^INSERT INTO workbench_projection_outbox/u),
|
||||
now: () => now,
|
||||
logger: { error() {}, warn() {}, info() {} }
|
||||
});
|
||||
await assert.rejects(project(faultStore, {
|
||||
traceId: faultTraceId,
|
||||
sessionId: faultSessionId,
|
||||
sourceEventId: `evt_fault_${suffix}`,
|
||||
sourceSeq: 1,
|
||||
sourceOffset: "200",
|
||||
inputSha256: "3".repeat(64),
|
||||
event: { eventType: "assistant_message", status: "running", assistantText: "must roll back", createdAt: now }
|
||||
}), /injected projection outbox failure/u);
|
||||
assert.equal(await count(pool, "workbench_kafka_inbox", "trace_id = $1", [faultTraceId]), 0);
|
||||
assert.equal(await count(pool, "workbench_trace_events", "trace_id = $1", [faultTraceId]), 0);
|
||||
assert.equal(await count(pool, "workbench_projection_outbox", "trace_id = $1", [faultTraceId]), 0);
|
||||
assert.equal(await count(pool, "hwlab_kafka_outbox", "partition_key = $1", [faultTraceId]), 0);
|
||||
|
||||
const terminal = await project(store, {
|
||||
traceId,
|
||||
sessionId,
|
||||
sourceEventId: `evt_terminal_${suffix}`,
|
||||
sourceSeq: 2,
|
||||
sourceOffset: "103",
|
||||
inputSha256: "4".repeat(64),
|
||||
event: {
|
||||
eventType: "terminal_status",
|
||||
status: "completed",
|
||||
terminal: true,
|
||||
assistantText: "authoritative final",
|
||||
finalResponse: { text: "authoritative final", status: "completed" },
|
||||
startedAt: now,
|
||||
finishedAt: "2026-07-10T12:00:01.000Z",
|
||||
durationMs: 1000,
|
||||
createdAt: "2026-07-10T12:00:01.000Z"
|
||||
}
|
||||
});
|
||||
assert.equal(terminal.projectedSeq, 2);
|
||||
const sealed = await pool.query(
|
||||
"SELECT projected_seq, terminal, sealed, checkpoint_json FROM workbench_projection_checkpoints WHERE trace_id = $1",
|
||||
[traceId]
|
||||
);
|
||||
assert.equal(sealed.rows[0].projected_seq, 2);
|
||||
assert.equal(sealed.rows[0].terminal, true);
|
||||
assert.equal(sealed.rows[0].sealed, true);
|
||||
assert.equal(JSON.parse(sealed.rows[0].checkpoint_json).finalResponse.text, "authoritative final");
|
||||
assert.equal(await count(pool, "workbench_projection_outbox", "trace_id = $1", [traceId]), 5);
|
||||
|
||||
const late = await project(store, {
|
||||
traceId,
|
||||
sessionId,
|
||||
sourceEventId: `evt_late_${suffix}`,
|
||||
sourceSeq: 3,
|
||||
sourceOffset: "104",
|
||||
inputSha256: "5".repeat(64),
|
||||
event: { eventType: "backend_status", status: "running", terminal: false, createdAt: "2026-07-10T12:00:02.000Z" }
|
||||
});
|
||||
assert.equal(late.suppressedAfterSeal, true);
|
||||
assert.equal(late.projectedSeq, 2);
|
||||
assert.equal(await count(pool, "workbench_trace_events", "trace_id = $1", [traceId]), 2);
|
||||
assert.equal(await count(pool, "workbench_projection_outbox", "trace_id = $1", [traceId]), 5);
|
||||
const lateInbox = await pool.query(
|
||||
"SELECT status, projected_seq FROM workbench_kafka_inbox WHERE source_topic = $1 AND source_event_id = $2",
|
||||
["agentrun.event.v1", `evt_late_${suffix}`]
|
||||
);
|
||||
assert.deepEqual(lateInbox.rows[0], { status: "projected", projected_seq: 2 });
|
||||
|
||||
const firstSync = await store.readAtomicWorkbenchProjectionSync({ traceId, afterOutboxSeq: 0, limit: 100 });
|
||||
assert.equal(firstSync.events.length, 5);
|
||||
assert.equal(firstSync.hasMore, false);
|
||||
assert.equal(firstSync.cursorOutboxSeq, firstSync.cutoffOutboxSeq);
|
||||
assert.equal(new Set(firstSync.events.map((event) => event.outboxSeq)).size, firstSync.events.length);
|
||||
assert.equal(firstSync.facts.checkpoints[0].projectedSeq, 2);
|
||||
assert.equal(firstSync.facts.turns[0].finalResponse.text, "authoritative final");
|
||||
const caughtUp = await store.readAtomicWorkbenchProjectionSync({ traceId, afterOutboxSeq: firstSync.cursorOutboxSeq, limit: 100, deltaOnly: true });
|
||||
assert.equal(caughtUp.events.length, 0);
|
||||
assert.equal(caughtUp.cursorOutboxSeq, firstSync.cursorOutboxSeq);
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
}, 30_000);
|
||||
}
|
||||
|
||||
async function project(store, { traceId, sessionId, sourceEventId, sourceSeq, sourceOffset, inputSha256, event }) {
|
||||
const canonicalEvent = {
|
||||
schema: "agentrun.event.v1",
|
||||
eventId: sourceEventId,
|
||||
sourceSeq,
|
||||
traceId,
|
||||
hwlabSessionId: sessionId,
|
||||
runId: `run_${traceId}`,
|
||||
commandId: `cmd_${traceId}`,
|
||||
event
|
||||
};
|
||||
const projectedEvent = { ...event, traceId, sessionId, sourceEventId, sourceSeq, runId: canonicalEvent.runId, commandId: canonicalEvent.commandId };
|
||||
return commitProjection(store, {
|
||||
transport: { sourceTopic: "agentrun.event.v1", sourcePartition: 0, sourceOffset, sourceKey: canonicalEvent.runId, inputSha256 },
|
||||
canonicalEvent,
|
||||
projectedEvent,
|
||||
requestMeta: { traceId, sessionId, valuesPrinted: false },
|
||||
factsFactory: ({ projectedSeq, previousCheckpoint, projectedAt }) => buildWorkbenchProjectionEventFacts({ projectedSeq, previousCheckpoint, projectedAt, event: projectedEvent }),
|
||||
hwlabEvent: { schema: "hwlab.event.v1", eventId: `hwlab:${sourceEventId}`, traceId, sessionId, sourceEventId, sourceSeq, valuesPrinted: false },
|
||||
hwlabTopic: "hwlab.event.v1",
|
||||
partitionKey: traceId,
|
||||
headers: { sourceEventId }
|
||||
});
|
||||
}
|
||||
|
||||
function faultInjectingPool(pool, pattern) {
|
||||
return {
|
||||
async connect() {
|
||||
const client = await pool.connect();
|
||||
let injected = false;
|
||||
return {
|
||||
async query(sql, params) {
|
||||
if (!injected && pattern.test(String(sql))) {
|
||||
injected = true;
|
||||
const error = new Error("injected projection outbox failure");
|
||||
error.code = "XX2464";
|
||||
throw error;
|
||||
}
|
||||
return client.query(sql, params);
|
||||
},
|
||||
release() {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async function count(pool, table, where, params) {
|
||||
if (!/^[a-z_]+$/u.test(table)) throw new Error("unsafe test table");
|
||||
const result = await pool.query(`SELECT COUNT(*)::int AS count FROM ${table} WHERE ${where}`, params);
|
||||
return Number(result.rows[0].count);
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
completeHwlabKafkaOutbox,
|
||||
projectionOutboxEventId,
|
||||
recordFailedAgentRunKafkaMessage,
|
||||
recordIgnoredAgentRunKafkaMessage,
|
||||
retryHwlabKafkaOutbox
|
||||
} from "./runtime-store-postgres-kafka.ts";
|
||||
|
||||
@@ -34,11 +35,35 @@ test("malformed duplicate source event at a new offset is durably DLQed without
|
||||
assert.equal(result.recorded, true);
|
||||
assert.equal(result.conflict, true);
|
||||
assert.equal(result.priorStatus, "projected");
|
||||
assert.equal(queries[0].params[0], "workbench-kafka-inbox:agentrun.event.v1:event:evt-duplicate");
|
||||
assert.equal(queries[0].params[0], "workbench-kafka-inbox:agentrun.event.v1:transport:0:11");
|
||||
assert.equal(queries[1].params[0], "workbench-kafka-inbox:agentrun.event.v1:event:evt-duplicate");
|
||||
assert.equal(queries.some(({ sql }) => sql.startsWith("UPDATE workbench_kafka_inbox")), false);
|
||||
assert.equal(queries.some(({ sql }) => sql.startsWith("INSERT INTO workbench_kafka_dlq")), true);
|
||||
});
|
||||
|
||||
test("ignored Kafka messages acquire transport then event identity locks", async () => {
|
||||
const queries = [];
|
||||
const client = {
|
||||
async query(sql, params) {
|
||||
queries.push({ sql, params });
|
||||
if (sql.startsWith("SELECT pg_advisory_xact_lock")) return { rows: [] };
|
||||
if (sql.startsWith("SELECT input_sha256")) return { rows: [] };
|
||||
if (sql.startsWith("INSERT INTO workbench_kafka_inbox")) return { rows: [] };
|
||||
throw new Error(`unexpected SQL: ${sql}`);
|
||||
}
|
||||
};
|
||||
const result = await recordIgnoredAgentRunKafkaMessage(transactionStore(client), {
|
||||
transport: { sourceTopic: "agentrun.event.v1", sourcePartition: 2, sourceOffset: "31", sourceKey: "run-ignored", inputSha256: "hash-ignored" },
|
||||
envelope: { eventId: "evt-ignored", sourceSeq: 1, runId: "run-ignored" },
|
||||
reason: "hwlab-correlation-absent"
|
||||
});
|
||||
|
||||
assert.equal(result.ignored, true);
|
||||
assert.equal(result.duplicate, false);
|
||||
assert.equal(queries[0].params[0], "workbench-kafka-inbox:agentrun.event.v1:transport:2:31");
|
||||
assert.equal(queries[1].params[0], "workbench-kafka-inbox:agentrun.event.v1:event:evt-ignored");
|
||||
});
|
||||
|
||||
test("projection outbox derives a stable non-null identity when a fact has no source event", () => {
|
||||
const input = {
|
||||
fact: { traceId: "trc-derived", projectedSeq: 3, sourceSeq: 2 },
|
||||
@@ -147,6 +172,44 @@ test("terminal Kafka transaction persists sealed turn and immutable SSE snapshot
|
||||
assert.equal(sse.payload.turn.finalResponse.text, "final body");
|
||||
});
|
||||
|
||||
test("late nonterminal Kafka event is acknowledged without advancing sealed projectedSeq", async () => {
|
||||
const queries = [];
|
||||
const client = {
|
||||
async query(sql, params) {
|
||||
queries.push({ sql, params });
|
||||
if (sql.startsWith("SELECT pg_advisory_xact_lock")) return { rows: [] };
|
||||
if (sql.startsWith("SELECT source_topic")) return { rows: [] };
|
||||
if (sql.startsWith("INSERT INTO workbench_kafka_inbox")) return { rows: [] };
|
||||
if (sql.startsWith("SELECT checkpoint_json")) return { rows: [{ checkpoint_json: { traceId: "trc_sealed", projectedSeq: 9, terminal: true, sealed: true } }] };
|
||||
if (sql.startsWith("SELECT owner_user_id")) return { rows: [] };
|
||||
if (sql.startsWith("SELECT GREATEST")) return { rows: [{ max_projected_seq: 9 }] };
|
||||
if (sql.startsWith("INSERT INTO hwlab_kafka_outbox") || sql.startsWith("UPDATE workbench_kafka_inbox")) return { rows: [] };
|
||||
throw new Error(`unexpected SQL: ${sql}`);
|
||||
}
|
||||
};
|
||||
const store = { ...transactionStore(client), memory: { writeWorkbenchFacts() { throw new Error("suppressed facts must not reach memory"); } } };
|
||||
const result = await commitAgentRunKafkaProjection(store, {
|
||||
transport: { sourceTopic: "agentrun.event.v1", sourcePartition: 0, sourceOffset: "21", inputSha256: "hash-late" },
|
||||
canonicalEvent: { eventId: "evt-late", sourceSeq: 10, traceId: "trc_sealed", hwlabSessionId: "ses_sealed" },
|
||||
projectedEvent: { traceId: "trc_sealed", sessionId: "ses_sealed" },
|
||||
factsFactory: ({ projectedSeq, previousCheckpoint, projectedAt }) => buildWorkbenchProjectionEventFacts({
|
||||
projectedSeq,
|
||||
previousCheckpoint,
|
||||
projectedAt,
|
||||
event: { traceId: "trc_sealed", sessionId: "ses_sealed", sourceEventId: "evt-late", sourceSeq: 10, status: "running", terminal: false }
|
||||
}),
|
||||
hwlabEvent: { eventId: "hwlab:evt-late" },
|
||||
hwlabTopic: "hwlab.event.v1",
|
||||
partitionKey: "trc_sealed",
|
||||
headers: {}
|
||||
});
|
||||
|
||||
assert.equal(result.suppressedAfterSeal, true);
|
||||
assert.equal(result.projectedSeq, 9);
|
||||
const inboxUpdate = queries.find(({ sql }) => sql.startsWith("UPDATE workbench_kafka_inbox"));
|
||||
assert.equal(inboxUpdate.params[0], 9);
|
||||
});
|
||||
|
||||
test("HWLAB outbox claim and completion use attempt and lease fencing", async () => {
|
||||
const calls = [];
|
||||
const row = { outbox_seq: 3, event_id: "evt-3", topic: "hwlab.event.v1", partition_key: "trc-3", payload_json: {}, headers_json: {}, attempt_count: 4, lease_owner: "relay-a", lease_expires_at: "2026-07-10T10:05:00.000Z", next_attempt_at: "2026-07-10T10:00:00.000Z", created_at: "2026-07-10T10:00:00.000Z" };
|
||||
|
||||
@@ -71,6 +71,9 @@ export async function commitAgentRunKafkaProjection(store, params = {}) {
|
||||
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 hwlabEvent = objectValue(params.hwlabEvent);
|
||||
const hwlabEventId = requiredText(hwlabEvent.eventId, "HWLAB Kafka eventId");
|
||||
await client.query(
|
||||
@@ -79,9 +82,9 @@ export async function commitAgentRunKafkaProjection(store, params = {}) {
|
||||
);
|
||||
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",
|
||||
[projectedSeq, now, transport.sourceTopic, transport.sourcePartition, transport.sourceOffset]
|
||||
[committedProjectedSeq, now, transport.sourceTopic, transport.sourcePartition, transport.sourceOffset]
|
||||
);
|
||||
return { duplicate: false, conflict: false, projectedSeq, facts, events: persistedEvents, suppressedAfterSeal: built?.suppressedAfterSeal === true };
|
||||
return { duplicate: false, conflict: false, projectedSeq: committedProjectedSeq, facts, events: persistedEvents, suppressedAfterSeal: built?.suppressedAfterSeal === true };
|
||||
});
|
||||
if (result.facts) store.memory.writeWorkbenchFacts({ facts: result.facts }, params.requestMeta ?? {});
|
||||
return { ...result, transport, sourceEventId, traceId, sessionId, sourceSeq, valuesPrinted: false };
|
||||
@@ -350,10 +353,12 @@ async function insertDlq(client, record) {
|
||||
}
|
||||
|
||||
async function lockKafkaInboxIdentity(client, transport, sourceEventId) {
|
||||
const identity = sourceEventId
|
||||
? `${transport.sourceTopic}:event:${sourceEventId}`
|
||||
: `${transport.sourceTopic}:transport:${transport.sourcePartition}:${transport.sourceOffset}`;
|
||||
await client.query("SELECT pg_advisory_xact_lock(hashtext($1))", [`workbench-kafka-inbox:${identity}`]);
|
||||
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 = {}) {
|
||||
|
||||
@@ -763,6 +763,19 @@ export class PostgresCloudRuntimeStore {
|
||||
return withPersistence({ written: true, facts: persistedFacts, events: txResult.events }, this.summary());
|
||||
}
|
||||
|
||||
async writeWorkbenchSessionAdmissionFact(params = {}, requestMeta = {}) {
|
||||
const readiness = this.summary();
|
||||
if (readiness.ready !== true || readiness.durable !== true || readiness.liveRuntimeEvidence !== true) {
|
||||
await this.assertReadyForWrites();
|
||||
}
|
||||
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.");
|
||||
await this.withDurableTransaction("workbench.session-admission.write", (client) => this.persistWorkbenchSessionAdmissionFact(fact, client));
|
||||
const memoryResult = this.memory.writeWorkbenchSessionAdmissionFact({ fact }, requestMeta);
|
||||
return withPersistence({ written: true, admissionOnly: true, fact: memoryResult.fact, valuesPrinted: false }, this.summary());
|
||||
}
|
||||
|
||||
async commitAgentRunKafkaProjection(params = {}) {
|
||||
await this.assertReadyForWrites();
|
||||
return commitAgentRunKafkaProjectionOperation(this, params);
|
||||
@@ -1181,6 +1194,13 @@ export class PostgresCloudRuntimeStore {
|
||||
);
|
||||
}
|
||||
|
||||
async persistWorkbenchSessionAdmissionFact(record, client = this) {
|
||||
await client.query(
|
||||
"INSERT INTO workbench_sessions (session_id, owner_user_id, project_id, conversation_id, thread_id, status, last_trace_id, projected_seq, source_seq, source_event_id, terminal, sealed, session_json, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT (session_id) DO UPDATE SET owner_user_id = COALESCE(workbench_sessions.owner_user_id, EXCLUDED.owner_user_id), project_id = COALESCE(workbench_sessions.project_id, EXCLUDED.project_id), conversation_id = COALESCE(workbench_sessions.conversation_id, EXCLUDED.conversation_id), thread_id = COALESCE(workbench_sessions.thread_id, EXCLUDED.thread_id), status = CASE WHEN workbench_sessions.projected_seq <= 0 AND NOT workbench_sessions.sealed THEN EXCLUDED.status ELSE workbench_sessions.status END, last_trace_id = CASE WHEN workbench_sessions.projected_seq <= 0 AND NOT workbench_sessions.sealed THEN COALESCE(EXCLUDED.last_trace_id, workbench_sessions.last_trace_id) ELSE workbench_sessions.last_trace_id END, session_json = CASE WHEN workbench_sessions.projected_seq <= 0 AND NOT workbench_sessions.sealed THEN EXCLUDED.session_json ELSE workbench_sessions.session_json END, updated_at = CASE WHEN workbench_sessions.projected_seq <= 0 AND NOT workbench_sessions.sealed THEN EXCLUDED.updated_at ELSE workbench_sessions.updated_at END",
|
||||
[record.sessionId, record.ownerUserId, record.projectId, record.conversationId, record.threadId, record.status, record.lastTraceId, record.projectedSeq, record.sourceSeq, record.sourceEventId, record.terminal, record.sealed, stableJson(record), record.createdAt, record.updatedAt]
|
||||
);
|
||||
}
|
||||
|
||||
async persistWorkbenchSessionInputFact(record, client = this) {
|
||||
await client.query(
|
||||
"INSERT INTO workbench_session_inputs (input_id, session_id, turn_id, trace_id, message_id, command_id, delivery, admitted_seq, promoted_seq, status, error_code, source_event_id, input_json, created_at, updated_at) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15) ON CONFLICT (input_id) DO UPDATE SET session_id = EXCLUDED.session_id, turn_id = EXCLUDED.turn_id, trace_id = EXCLUDED.trace_id, message_id = EXCLUDED.message_id, command_id = COALESCE(EXCLUDED.command_id, workbench_session_inputs.command_id), delivery = EXCLUDED.delivery, admitted_seq = CASE WHEN workbench_session_inputs.admitted_seq > 0 THEN workbench_session_inputs.admitted_seq ELSE EXCLUDED.admitted_seq END, promoted_seq = COALESCE(EXCLUDED.promoted_seq, workbench_session_inputs.promoted_seq), status = EXCLUDED.status, error_code = EXCLUDED.error_code, source_event_id = COALESCE(EXCLUDED.source_event_id, workbench_session_inputs.source_event_id), input_json = EXCLUDED.input_json, updated_at = EXCLUDED.updated_at",
|
||||
|
||||
Reference in New Issue
Block a user