Files
openclaw/extensions/qa-lab/src/runtime-parity.ts
Peter Steinberger 5a795f4dda fix(qa): require a fresh reply and real tool use after switching models (#119662)
* fix(qa): require a fresh reply after switching models

* fix(qa): prove successful tool use after model switching

* fix(qa): wait for the new persisted model-switch tool result

* fix(qa): authenticate canonical model-switch attempt evidence

* fix(ai): preserve effective response model evidence

Punchcard-Session: golden-valley-workshop-br

* fix(agent): publish run-owned terminal receipts

Punchcard-Session: golden-valley-workshop-br

* fix(qa): require run-owned model-switch evidence

Punchcard-Session: golden-valley-workshop-br

* fix(agent): record explicit tool completion outcomes

Punchcard-Session: golden-valley-workshop-br

* fix(agent): exclude unavailable approvals from receipts

Punchcard-Session: golden-valley-workshop-br

* fix(agent): derive receipt visibility from terminal reply

Punchcard-Session: golden-valley-workshop-br

* fix(qa): bind model-switch continuity to terminal reply

Punchcard-Session: golden-valley-workshop-br

* fix(qa): project Crabline Telegram visible text

Punchcard-Session: golden-valley-workshop-br

* fix(qa): record run-owned delivery evidence

Punchcard-Session: golden-valley-workshop-br

* fix(qa): bind primary model-switch delivery

Punchcard-Session: golden-valley-workshop-br

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
2026-08-08 03:05:32 +08:00

1563 lines
50 KiB
TypeScript

import {
loadTranscriptEventsSync,
resolveStorePath,
} from "openclaw/plugin-sdk/session-store-runtime";
import {
loadSqliteTrajectoryRuntimeEvents,
type SqliteTrajectoryRuntimeEventForTest,
} from "openclaw/plugin-sdk/sqlite-runtime-testing";
// Qa Lab plugin module implements runtime parity behavior.
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
import {
asFiniteNumber as readFiniteNumber,
isRecord as isMessageRecord,
normalizeOptionalString as readNonEmptyString,
} from "openclaw/plugin-sdk/string-coerce-runtime";
import {
scanDirectReplyTranscriptSentinels,
scanGatewayLogSentinels,
type GatewayLogSentinelFinding,
} from "./gateway-log-sentinel.js";
import { discardIgnoredResponseBody } from "./ignored-response-body.js";
import * as parity from "./parity-shared.js";
import {
buildRuntimeParityCacheDiagnostics,
type RuntimeParityCacheDiagnostics,
} from "./runtime-parity-cache-diagnostics.js";
import type { RuntimeParityUsage } from "./runtime-parity-usage.js";
import { readRawQaSessionStore } from "./suite-runtime-agent-session.js";
export type { RuntimeParityUsage } from "./runtime-parity-usage.js";
// These are the canonical QA comparison cells, not the extensible product
// AgentHarness registry. Broader harness coverage needs its own explicit lane.
export type RuntimeId = "openclaw" | "codex";
type RuntimeParityStatus = "pass" | "fail" | "skip";
const CANONICAL_RUNTIME_IDS = ["openclaw", "codex"] as const satisfies readonly RuntimeId[];
export type RuntimeParityToolCall = {
tool: string;
argsHash: string;
resultHash: string;
errorClass?: string;
};
export type RuntimeParityUsagePolicy =
| { expectation: "assistant-message-required" }
| { expectation: "not-applicable"; reason: string };
export type RuntimeParityCell = {
runtime: RuntimeId;
transcriptBytes: string;
toolCalls: RuntimeParityToolCall[];
providerPlanToolCalls?: RuntimeParityToolCall[];
finalText: string;
usage: RuntimeParityUsage;
cacheDiagnostics?: RuntimeParityCacheDiagnostics;
wallClockMs: number;
bootstrapWallClockMs?: number;
transportErrorClass?: string;
runtimeErrorClass?: string;
bootStateLines: string[];
sentinelFindings?: GatewayLogSentinelFinding[];
modelSwitchEvidence?: Record<string, unknown>;
};
type RuntimeParityResultCell = RuntimeParityCell & {
status: RuntimeParityStatus;
details?: string;
};
// Runtime-tool fixtures reserve this prefix for tracked harness limitations.
// Matching only that explicit skip keeps unexpected coverage loss blocking.
const KNOWN_HARNESS_GAP_DETAILS_PREFIX = "known-harness-gap ";
function isKnownHarnessGapSkip(
cell: Pick<RuntimeParityResultCell, "details" | "status"> | undefined,
) {
return (
cell?.status === "skip" &&
cell.details?.trimStart().startsWith(KNOWN_HARNESS_GAP_DETAILS_PREFIX) === true
);
}
export type RuntimeParityDrift =
| "none"
| "text-only"
| "tool-call-shape"
| "tool-result-shape"
| "structural"
| "failure-mode";
export type RuntimeParityResult = {
scenarioId: string;
runtimeParityUsage?: RuntimeParityUsagePolicy;
cells: Record<RuntimeId, RuntimeParityResultCell>;
drift: RuntimeParityDrift;
driftDetails?: string;
};
export function resolveRuntimeParityUsagePolicy(value: unknown): RuntimeParityUsagePolicy {
// Legacy or malformed summaries must not silently disable live-usage proof.
if (!value || typeof value !== "object") {
return { expectation: "assistant-message-required" };
}
const candidate = value as { expectation?: unknown; reason?: unknown };
if (
candidate.expectation === "not-applicable" &&
typeof candidate.reason === "string" &&
candidate.reason.trim()
) {
return { expectation: "not-applicable", reason: candidate.reason.trim() };
}
return { expectation: "assistant-message-required" };
}
export type RuntimeParityScenarioExecution = {
status: RuntimeParityStatus;
details?: string;
cell: RuntimeParityCell;
};
export function runtimeParityCellStatus(
cell: RuntimeParityCell | undefined,
): "pass" | "fail" | "missing" {
if (!cell) {
return "missing";
}
return cell.runtimeErrorClass || cell.transportErrorClass ? "fail" : "pass";
}
export function isRuntimeParityResultPass(result: RuntimeParityResult) {
if (result.drift === "failure-mode") {
return false;
}
const cells = CANONICAL_RUNTIME_IDS.map((runtime) => result.cells[runtime]);
const knownHarnessGapSkips = cells.filter((cell) => isKnownHarnessGapSkip(cell));
return (
knownHarnessGapSkips.length <= 1 &&
cells.every(
(cell) =>
isRuntimeParityCellPassable(cell) &&
(cell.status === "pass" || isKnownHarnessGapSkip(cell)),
)
);
}
type QaGatewayLike = {
logs?: () => string;
tempRoot: string;
};
type QaSuiteScenarioLike = {
details?: string;
status: "pass" | "fail" | "skip";
steps?: Array<{ details?: string; status?: "pass" | "fail" | "skip" }>;
modelSwitchEvidence?: Record<string, unknown>;
};
type RuntimeParityCaptureParams = {
runtime: RuntimeId;
gateway: QaGatewayLike;
scenarioResult: QaSuiteScenarioLike;
wallClockMs: number;
bootstrapWallClockMs?: number;
agentId?: string;
mockBaseUrl?: string;
};
type RuntimeParitySessionEntry = {
createdAt?: number;
sessionId?: string;
sessionFile?: string;
updatedAt?: number;
heartbeatIsolatedBaseSessionKey?: string;
spawnedBy?: string;
parentSessionKey?: string;
spawnDepth?: number;
subagentRole?: string;
};
type RuntimeParitySessionCandidate = {
entry: RuntimeParitySessionEntry;
sessionKey: string;
};
type RuntimeParityTranscriptRecord = {
message: Record<string, unknown>;
role: "user" | "assistant" | "tool" | "toolResult";
};
type RuntimeParityMockRequestSnapshot = {
prompt?: string;
allInputText?: string;
plannedToolName?: string;
plannedToolArgs?: unknown;
toolOutput?: string;
};
type RuntimeParityObservedToolCall = RuntimeParityToolCall & {
callId?: string;
hasArguments?: boolean;
hasResult?: boolean;
};
type RuntimeParityPendingToolCall = RuntimeParityObservedToolCall & {
_resolved: boolean;
};
type RuntimeParityCaptureSources = {
sessions: Array<{
transcriptBytes: string;
trajectoryToolCalls: RuntimeParityObservedToolCall[];
}>;
transcriptBytes: string;
};
const DEFAULT_AGENT_ID = "qa";
const HEARTBEAT_RESPONSE_TOOL_NAME = "heartbeat_respond";
const HEARTBEAT_TRANSCRIPT_PROMPT = "[OpenClaw heartbeat poll]";
const HEARTBEAT_TASK_PROMPT_PREFIX =
"Run the following periodic tasks (only those due based on their intervals):";
const TOOL_RESULT_MISSING_ERROR_CLASS = "tool-result-missing";
const RUNTIME_PARITY_SESSION_KEY_DETAIL_PREFIX = "RUNTIME_PARITY_SESSION_KEY=";
const BOOT_STATE_LINE_RE =
/\b(?:FailoverError|No API key found|Codex app-server|auth profile|runtime policy|restart mode:|plugin|doctor)\b/i;
const TOOL_RESULT_ERROR_RE = /\b(?:error|failed|failure|timeout|denied|enoent|not found)\b/i;
function normalizeTextForParity(text: string) {
return text.replace(/\s+/gu, " ").trim();
}
function readUsageTotals(raw: unknown): RuntimeParityUsage {
const usage = isMessageRecord(raw) ? raw : {};
const inputTokens =
readFiniteNumber(usage.input) ??
readFiniteNumber(usage.inputTokens) ??
readFiniteNumber(usage.input_tokens) ??
0;
const outputTokens =
readFiniteNumber(usage.output) ??
readFiniteNumber(usage.outputTokens) ??
readFiniteNumber(usage.output_tokens) ??
0;
const cacheTelemetryUnavailable =
isMessageRecord(usage.cacheTelemetry) && usage.cacheTelemetry.state === "unavailable";
const cacheRead = cacheTelemetryUnavailable
? undefined
: (readFiniteNumber(usage.cacheRead) ?? readFiniteNumber(usage.cache_read_tokens));
const cacheWrite = cacheTelemetryUnavailable
? undefined
: (readFiniteNumber(usage.cacheWrite) ?? readFiniteNumber(usage.cache_write_tokens));
const componentTotal = inputTokens + outputTokens + (cacheRead ?? 0) + (cacheWrite ?? 0);
const totalTokens =
readFiniteNumber(usage.total) ??
readFiniteNumber(usage.totalTokens) ??
readFiniteNumber(usage.total_tokens) ??
componentTotal;
return {
inputTokens,
outputTokens,
totalTokens,
...(cacheRead !== undefined ? { cacheRead } : {}),
...(cacheWrite !== undefined ? { cacheWrite } : {}),
};
}
function readAssistantUsage(message: Record<string, unknown>): RuntimeParityUsage {
const usage = readUsageTotals(message.usage ?? null);
if (!isMessageRecord(message.usage) || isMessageRecord(message.usage.cacheTelemetry)) {
return usage;
}
const provider = readNonEmptyString(message.provider)?.toLowerCase();
const api = readNonEmptyString(message.api)?.toLowerCase();
if (
(provider === "ollama" || api === "ollama") &&
usage.cacheRead === 0 &&
usage.cacheWrite === 0
) {
// Transcripts written before cacheTelemetry was added contain Ollama's
// required placeholder zeros. Explicit current provenance always wins.
delete usage.cacheRead;
delete usage.cacheWrite;
}
return usage;
}
function addUsage(target: RuntimeParityUsage, next: RuntimeParityUsage) {
target.inputTokens += next.inputTokens;
target.outputTokens += next.outputTokens;
target.totalTokens += next.totalTokens;
if (next.cacheRead !== undefined) {
target.cacheRead = (target.cacheRead ?? 0) + next.cacheRead;
}
if (next.cacheWrite !== undefined) {
target.cacheWrite = (target.cacheWrite ?? 0) + next.cacheWrite;
}
}
function extractAssistantText(message: Record<string, unknown>) {
const rawContent = message.content;
if (typeof rawContent === "string") {
return rawContent.trim();
}
if (!Array.isArray(rawContent)) {
return "";
}
const parts: string[] = [];
for (const block of rawContent) {
if (typeof block === "string") {
if (block.trim()) {
parts.push(block.trim());
}
continue;
}
if (!isMessageRecord(block)) {
continue;
}
const text = readNonEmptyString(block.text);
if (text) {
parts.push(text);
continue;
}
const nestedText = readNonEmptyString(block.content);
if (
nestedText &&
(block.type === "output_text" || block.type === "text" || block.type === "message")
) {
parts.push(nestedText);
}
}
return parts.join("\n").trim();
}
function normalizeToolCallId(value: unknown) {
return readNonEmptyString(value);
}
function parseJsonRecord(value: string): Record<string, unknown> | undefined {
if (!value.trim()) {
return undefined;
}
try {
const parsed = JSON.parse(value) as unknown;
return isMessageRecord(parsed) ? parsed : undefined;
} catch {
return undefined;
}
}
function extractToolCalls(message: Record<string, unknown>): Array<{
id?: string;
tool: string;
args: unknown;
}> {
const calls: Array<{ id?: string; tool: string; args: unknown }> = [];
const rawContent = message.content;
if (Array.isArray(rawContent)) {
for (const block of rawContent) {
if (!isMessageRecord(block)) {
continue;
}
const type = readNonEmptyString(block.type)?.toLowerCase();
if (type !== "tool_use" && type !== "toolcall" && type !== "tool_call") {
continue;
}
const tool = readNonEmptyString(block.name) ?? "unknown";
calls.push({
id:
normalizeToolCallId(block.id) ??
normalizeToolCallId(block.toolCallId) ??
normalizeToolCallId(block.toolUseId),
tool,
args: block.input ?? block.arguments ?? block.args ?? block.payload ?? null,
});
}
}
const rawToolCalls =
message.tool_calls ?? message.toolCalls ?? message.function_call ?? message.functionCall;
const toolCalls = Array.isArray(rawToolCalls) ? rawToolCalls : rawToolCalls ? [rawToolCalls] : [];
for (const call of toolCalls) {
if (!isMessageRecord(call)) {
continue;
}
const functionRecord = isMessageRecord(call.function) ? call.function : undefined;
const tool =
readNonEmptyString(call.name) ?? readNonEmptyString(functionRecord?.name) ?? "unknown";
calls.push({
id:
normalizeToolCallId(call.id) ??
normalizeToolCallId(call.toolCallId) ??
normalizeToolCallId(call.toolUseId),
tool,
args:
call.arguments ?? functionRecord?.arguments ?? call.input ?? functionRecord?.input ?? null,
});
}
return calls;
}
function extractToolResults(message: Record<string, unknown>): Array<{
id?: string;
tool?: string;
result: unknown;
errorClass?: string;
}> {
const results: Array<{ id?: string; tool?: string; result: unknown; errorClass?: string }> = [];
const toolName =
readNonEmptyString(message.toolName) ??
readNonEmptyString(message.tool_name) ??
readNonEmptyString(message.name) ??
readNonEmptyString(message.tool);
if ((message.role === "tool" || message.role === "toolResult") && message.content !== undefined) {
const contentText = extractAssistantText(message);
results.push({
tool: toolName,
result: message.content,
...(message.isError === true || TOOL_RESULT_ERROR_RE.test(contentText)
? { errorClass: "tool-result-error" }
: {}),
});
}
const rawContent = message.content;
if (!Array.isArray(rawContent)) {
return results;
}
for (const block of rawContent) {
if (!isMessageRecord(block)) {
continue;
}
const type = readNonEmptyString(block.type)?.toLowerCase();
if (type !== "tool_result" && type !== "tool_result_error") {
continue;
}
const content = block.content ?? block.result ?? block.output ?? block.text ?? null;
const contentText =
typeof content === "string"
? content
: Array.isArray(content)
? JSON.stringify(content)
: JSON.stringify(content ?? "");
results.push({
id:
normalizeToolCallId(block.tool_use_id) ??
normalizeToolCallId(block.toolUseId) ??
normalizeToolCallId(block.tool_call_id) ??
normalizeToolCallId(block.toolCallId),
tool: toolName,
result: content,
...(block.is_error === true ||
type === "tool_result_error" ||
TOOL_RESULT_ERROR_RE.test(contentText)
? { errorClass: "tool-result-error" }
: {}),
});
}
return results;
}
function classifyToolResultError(params: {
rawOutput: string;
parsedOutput: Record<string, unknown> | undefined;
}) {
const error = readNonEmptyString(params.parsedOutput?.error);
if (error) {
return "tool-result-error";
}
const status = readNonEmptyString(params.parsedOutput?.status);
if (status && /\b(?:error|failed|failure)\b/i.test(status)) {
return "tool-result-error";
}
if (!params.parsedOutput) {
const normalized = params.rawOutput.trim().toLowerCase();
if (
normalized.startsWith("error:") ||
normalized.startsWith("failed:") ||
normalized.includes("unsupported call:") ||
normalized.includes("permission denied") ||
normalized.includes("no such file") ||
normalized.includes("enoent")
) {
return "tool-result-error";
}
}
return undefined;
}
function finalizeToolCallOrder(
ordered: RuntimeParityPendingToolCall[],
): RuntimeParityObservedToolCall[] {
return ordered.map(({ _resolved, ...toolCall }) =>
_resolved
? toolCall
: {
...toolCall,
errorClass: toolCall.errorClass ?? TOOL_RESULT_MISSING_ERROR_CLASS,
},
);
}
function resolveToolCallOrder(
records: RuntimeParityTranscriptRecord[],
): RuntimeParityObservedToolCall[] {
const ordered: RuntimeParityPendingToolCall[] = [];
const byId = new Map<string, number>();
const unresolvedByTool = new Map<string, number[]>();
const unresolvedOrder: number[] = [];
const enqueueUnresolved = (tool: string, index: number) => {
const indices = unresolvedByTool.get(tool) ?? [];
indices.push(index);
unresolvedByTool.set(tool, indices);
unresolvedOrder.push(index);
};
const markResolved = (index: number) => {
const pending = ordered[index];
if (!pending) {
return;
}
ordered[index] = { ...pending, _resolved: true };
const unresolvedIndex = unresolvedOrder.indexOf(index);
if (unresolvedIndex >= 0) {
unresolvedOrder.splice(unresolvedIndex, 1);
}
const toolIndices = unresolvedByTool.get(pending.tool);
if (!toolIndices) {
return;
}
const nextIndices = toolIndices.filter((candidate) => candidate !== index);
if (nextIndices.length > 0) {
unresolvedByTool.set(pending.tool, nextIndices);
return;
}
unresolvedByTool.delete(pending.tool);
};
const matchPendingIndex = (result: { id?: string; tool?: string }) => {
if (result.id && byId.has(result.id)) {
return byId.get(result.id);
}
if (result.tool) {
const toolIndices = unresolvedByTool.get(result.tool);
if (toolIndices && toolIndices.length > 0) {
return toolIndices[0];
}
}
return unresolvedOrder[0];
};
for (const record of records) {
if (record.role === "assistant") {
for (const call of extractToolCalls(record.message)) {
const index =
ordered.push({
tool: call.tool,
argsHash: parity.stableHash(call.args),
resultHash: parity.stableHash(null),
callId: call.id,
hasArguments: true,
hasResult: false,
_resolved: false,
}) - 1;
if (call.id) {
byId.set(call.id, index);
}
enqueueUnresolved(call.tool, index);
}
}
if (record.role === "user" || record.role === "tool" || record.role === "toolResult") {
for (const result of extractToolResults(record.message)) {
const pendingIndex = matchPendingIndex(result);
const nextValue: RuntimeParityObservedToolCall = {
tool:
result.tool ??
(pendingIndex !== undefined ? ordered[pendingIndex]?.tool : undefined) ??
"unknown",
argsHash:
pendingIndex !== undefined
? (ordered[pendingIndex]?.argsHash ?? parity.stableHash(null))
: parity.stableHash(null),
resultHash: parity.stableHash(result.result),
callId: pendingIndex !== undefined ? ordered[pendingIndex]?.callId : result.id,
hasArguments:
pendingIndex !== undefined ? ordered[pendingIndex]?.hasArguments === true : false,
hasResult: true,
...(result.errorClass ? { errorClass: result.errorClass } : {}),
};
if (pendingIndex === undefined || !ordered[pendingIndex]) {
ordered.push({ ...nextValue, _resolved: true });
continue;
}
ordered[pendingIndex] = {
...nextValue,
_resolved: true,
};
markResolved(pendingIndex);
}
}
}
return finalizeToolCallOrder(ordered);
}
function resolveToolCallOrderFromMockRequests(
requests: RuntimeParityMockRequestSnapshot[],
): RuntimeParityToolCall[] {
const ordered: RuntimeParityPendingToolCall[] = [];
const unresolvedOrder: number[] = [];
const enqueueUnresolved = (index: number) => {
unresolvedOrder.push(index);
};
const markResolved = (index: number) => {
const pending = ordered[index];
if (!pending) {
return;
}
ordered[index] = { ...pending, _resolved: true };
const unresolvedIndex = unresolvedOrder.indexOf(index);
if (unresolvedIndex >= 0) {
unresolvedOrder.splice(unresolvedIndex, 1);
}
};
for (const request of requests) {
const rawToolOutput = readNonEmptyString(request.toolOutput) ?? "";
if (rawToolOutput) {
const pendingIndex = unresolvedOrder[0];
const parsedOutput = parseJsonRecord(rawToolOutput);
const resolvedCall: RuntimeParityToolCall = {
tool: pendingIndex !== undefined ? (ordered[pendingIndex]?.tool ?? "unknown") : "unknown",
argsHash:
pendingIndex !== undefined
? (ordered[pendingIndex]?.argsHash ?? parity.stableHash(null))
: parity.stableHash(null),
resultHash: parity.stableHash(parsedOutput ?? rawToolOutput),
...(classifyToolResultError({
rawOutput: rawToolOutput,
parsedOutput,
})
? { errorClass: "tool-result-error" }
: {}),
};
if (pendingIndex === undefined || !ordered[pendingIndex]) {
ordered.push({ ...resolvedCall, _resolved: true });
} else {
ordered[pendingIndex] = {
...resolvedCall,
_resolved: true,
};
markResolved(pendingIndex);
}
}
const plannedToolName = readNonEmptyString(request.plannedToolName);
if (!plannedToolName) {
continue;
}
ordered.push({
tool: plannedToolName,
argsHash: parity.stableHash(request.plannedToolArgs ?? null),
resultHash: parity.stableHash(null),
_resolved: false,
});
enqueueUnresolved(ordered.length - 1);
}
return finalizeToolCallOrder(ordered);
}
function trajectoryToolCallId(data: Record<string, unknown>) {
return (
normalizeToolCallId(data.toolCallId) ??
normalizeToolCallId(data.itemId) ??
normalizeToolCallId(data.id)
);
}
function trajectoryToolResultValue(data: Record<string, unknown>) {
if (Object.hasOwn(data, "result")) {
return data.result;
}
if (Object.hasOwn(data, "output")) {
return data.output;
}
if (Object.hasOwn(data, "contentItems")) {
return data.contentItems;
}
return {
...(Object.hasOwn(data, "status") ? { status: data.status } : {}),
...(Object.hasOwn(data, "success") ? { success: data.success } : {}),
};
}
function isTrajectoryToolResultError(data: Record<string, unknown>) {
if (data.isError === true || data.success === false) {
return true;
}
const status = readNonEmptyString(data.status)?.toLowerCase();
return (
status === "blocked" ||
status === "cancelled" ||
status === "declined" ||
status === "error" ||
status === "failed"
);
}
function resolveTrajectoryToolCallOrder(
events: readonly SqliteTrajectoryRuntimeEventForTest[],
): RuntimeParityObservedToolCall[] {
const ordered: Array<{
call: RuntimeParityObservedToolCall;
resolved: boolean;
}> = [];
const matchPendingIndex = (data: Record<string, unknown>, tool?: string) => {
const id = trajectoryToolCallId(data);
if (id) {
const idMatch = ordered.findIndex((pending) => pending.call.callId === id);
if (idMatch >= 0) {
return idMatch;
}
return undefined;
}
const toolMatch = ordered.findIndex(
(pending) => !pending.resolved && (!tool || pending.call.tool === tool),
);
if (toolMatch >= 0) {
return toolMatch;
}
return undefined;
};
for (const event of events) {
const data = event.data ?? {};
if (event.type === "tool.call") {
const tool = readNonEmptyString(data.name) ?? "unknown";
ordered.push({
call: {
tool,
argsHash: parity.stableHash(data.arguments ?? null),
resultHash: parity.stableHash(null),
callId: trajectoryToolCallId(data),
hasArguments: true,
hasResult: false,
},
resolved: false,
});
continue;
}
if (event.type !== "tool.result") {
continue;
}
const tool = readNonEmptyString(data.name);
const pendingIndex = matchPendingIndex(data, tool);
const pendingCall = pendingIndex !== undefined ? ordered[pendingIndex]?.call : undefined;
const nextValue: RuntimeParityObservedToolCall = {
tool: tool ?? pendingCall?.tool ?? "unknown",
argsHash: pendingCall?.argsHash ?? parity.stableHash(null),
resultHash: parity.stableHash(trajectoryToolResultValue(data)),
callId: trajectoryToolCallId(data) ?? pendingCall?.callId,
hasArguments: pendingCall?.hasArguments === true,
hasResult: true,
...(isTrajectoryToolResultError(data) ? { errorClass: "tool-result-error" } : {}),
};
if (pendingIndex === undefined) {
ordered.push({
call: nextValue,
resolved: true,
});
continue;
}
ordered[pendingIndex] = {
...ordered[pendingIndex],
call: nextValue,
resolved: true,
};
}
for (const pending of ordered) {
if (!pending.resolved) {
pending.call.errorClass ??= TOOL_RESULT_MISSING_ERROR_CLASS;
}
}
return ordered.map((pending) => pending.call);
}
function mergeRuntimeParityToolCalls(params: {
transcriptToolCalls: RuntimeParityObservedToolCall[];
trajectoryToolCalls: RuntimeParityObservedToolCall[];
}): RuntimeParityObservedToolCall[] {
if (params.trajectoryToolCalls.length === 0) {
return params.transcriptToolCalls;
}
if (params.transcriptToolCalls.length === 0) {
return params.trajectoryToolCalls;
}
const transcript = params.transcriptToolCalls;
const trajectory = params.trajectoryToolCalls;
const callsMatch = (
transcriptCall: RuntimeParityObservedToolCall,
trajectoryCall: RuntimeParityObservedToolCall,
) => {
if (transcriptCall.callId && trajectoryCall.callId) {
return transcriptCall.callId === trajectoryCall.callId;
}
if (transcriptCall.callId || trajectoryCall.callId) {
return false;
}
return (
transcriptCall.tool === trajectoryCall.tool &&
transcriptCall.argsHash === trajectoryCall.argsHash
);
};
const mergeMatchingCalls = (
transcriptCall: RuntimeParityObservedToolCall,
trajectoryCall: RuntimeParityObservedToolCall,
): RuntimeParityObservedToolCall => {
const invocation = transcriptCall.hasArguments ? transcriptCall : trajectoryCall;
const completion = transcriptCall.hasResult ? transcriptCall : trajectoryCall;
return {
tool: invocation.tool,
argsHash: invocation.argsHash,
resultHash: completion.resultHash,
callId: transcriptCall.callId ?? trajectoryCall.callId,
hasArguments: invocation.hasArguments,
hasResult: completion.hasResult,
...(completion.errorClass ? { errorClass: completion.errorClass } : {}),
};
};
const sharedSuffixLengths = Array.from({ length: transcript.length + 1 }, () =>
Array<number>(trajectory.length + 1).fill(0),
);
for (let transcriptIndex = transcript.length - 1; transcriptIndex >= 0; transcriptIndex -= 1) {
for (let trajectoryIndex = trajectory.length - 1; trajectoryIndex >= 0; trajectoryIndex -= 1) {
sharedSuffixLengths[transcriptIndex]![trajectoryIndex] = callsMatch(
transcript[transcriptIndex]!,
trajectory[trajectoryIndex]!,
)
? 1 + sharedSuffixLengths[transcriptIndex + 1]![trajectoryIndex + 1]!
: Math.max(
sharedSuffixLengths[transcriptIndex + 1]![trajectoryIndex]!,
sharedSuffixLengths[transcriptIndex]![trajectoryIndex + 1]!,
);
}
}
const merged: RuntimeParityObservedToolCall[] = [];
let transcriptIndex = 0;
let trajectoryIndex = 0;
while (transcriptIndex < transcript.length && trajectoryIndex < trajectory.length) {
const transcriptCall = transcript[transcriptIndex]!;
const trajectoryCall = trajectory[trajectoryIndex]!;
if (callsMatch(transcriptCall, trajectoryCall)) {
merged.push(mergeMatchingCalls(transcriptCall, trajectoryCall));
transcriptIndex += 1;
trajectoryIndex += 1;
continue;
}
if (
sharedSuffixLengths[transcriptIndex + 1]![trajectoryIndex]! >=
sharedSuffixLengths[transcriptIndex]![trajectoryIndex + 1]!
) {
merged.push(transcriptCall);
transcriptIndex += 1;
continue;
}
merged.push(trajectoryCall);
trajectoryIndex += 1;
}
return [...merged, ...transcript.slice(transcriptIndex), ...trajectory.slice(trajectoryIndex)];
}
function removeRuntimeParityToolCallIdentity(
toolCalls: RuntimeParityObservedToolCall[],
): RuntimeParityToolCall[] {
return toolCalls.map(
({ callId: _callId, hasArguments: _hasArguments, hasResult: _hasResult, ...toolCall }) =>
toolCall,
);
}
function classifyScenarioError(details: string | undefined): string | undefined {
const normalized = normalizeTextForParity(details ?? "").toLowerCase();
if (!normalized) {
return undefined;
}
if (normalized.includes("no api key found")) {
return "missing-api-key";
}
if (normalized.includes("failover")) {
return "failover";
}
if (normalized.includes("timeout") || normalized.includes("timed out")) {
return "timeout";
}
if (normalized.includes("codex app-server")) {
return "codex-app-server";
}
if (
normalized.includes("auth profile") ||
normalized.includes("oauth") ||
normalized.includes("api key")
) {
return "auth";
}
if (normalized.includes("tool")) {
return "tool-error";
}
return "scenario-failure";
}
function extractBootStateLines(logs: string | undefined): string[] {
if (!logs) {
return [];
}
return logs
.split(/\r?\n/u)
.map((line) => line.trim())
.filter((line) => line.length > 0 && BOOT_STATE_LINE_RE.test(line))
.slice(-30);
}
function buildTranscriptRecords(transcriptBytes: string): RuntimeParityTranscriptRecord[] {
const records: RuntimeParityTranscriptRecord[] = [];
for (const line of transcriptBytes.split(/\r?\n/u)) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
try {
const parsed = JSON.parse(trimmed) as Record<string, unknown>;
const message = isMessageRecord(parsed.message) ? parsed.message : undefined;
const role = readNonEmptyString(message?.role);
if (
!message ||
(role !== "user" && role !== "assistant" && role !== "tool" && role !== "toolResult")
) {
continue;
}
records.push({
message,
role,
});
} catch {
// Ignore malformed QA transcript rows and keep the classifier deterministic.
}
}
return records;
}
function isHeartbeatOnlyRuntimeTranscript(transcriptBytes: string) {
const records = buildTranscriptRecords(transcriptBytes);
if (records.length === 0) {
return false;
}
const userTexts = records
.filter((record) => record.role === "user" && !isToolResultLikeMessage(record.message))
.map((record) => extractAssistantText(record.message));
return userTexts.length > 0 && userTexts.every(isHeartbeatRuntimeUserText);
}
function isToolResultLikeMessage(message: Record<string, unknown>) {
if (message.role === "tool" || message.role === "toolResult") {
return true;
}
const rawContent = message.content;
if (!Array.isArray(rawContent)) {
return false;
}
return rawContent.some((block) => {
if (!isMessageRecord(block)) {
return false;
}
const type = readNonEmptyString(block.type)?.toLowerCase();
return type === "tool_result" || type === "toolresult" || type === "tool_result_error";
});
}
function isHeartbeatRuntimeUserText(text: string) {
const normalized = normalizeTextForParity(text).toLowerCase();
if (!normalized) {
return false;
}
if (normalized === HEARTBEAT_TRANSCRIPT_PROMPT.toLowerCase()) {
return true;
}
if (normalized.startsWith("read heartbeat.md") && normalized.includes("heartbeat_ok")) {
return true;
}
if (
normalized.startsWith("read heartbeat.md") &&
normalized.includes(HEARTBEAT_RESPONSE_TOOL_NAME)
) {
return true;
}
return (
normalized.startsWith(HEARTBEAT_TASK_PROMPT_PREFIX.toLowerCase()) &&
(normalized.includes("heartbeat_ok") || normalized.includes(HEARTBEAT_RESPONSE_TOOL_NAME))
);
}
function extractFinalAssistantText(records: RuntimeParityTranscriptRecord[]) {
let lastAssistantText = "";
for (const record of records) {
if (record.role !== "assistant") {
continue;
}
const text = extractAssistantText(record.message);
if (text) {
lastAssistantText = text;
}
}
return normalizeTextForParity(lastAssistantText);
}
function aggregateUsage(records: RuntimeParityTranscriptRecord[]): RuntimeParityUsage {
const totals: RuntimeParityUsage = {
inputTokens: 0,
outputTokens: 0,
totalTokens: 0,
};
for (const record of records) {
if (record.role !== "assistant") {
continue;
}
const usage = readAssistantUsage(record.message);
addUsage(totals, usage);
}
return totals;
}
function compareToolResultShape(
left: RuntimeParityToolCall[],
right: RuntimeParityToolCall[],
): string | undefined {
const total = Math.min(left.length, right.length);
for (let index = 0; index < total; index += 1) {
const leftCall = left[index];
const rightCall = right[index];
if (!leftCall || !rightCall) {
continue;
}
if (
leftCall.errorClass === "tool-result-error" &&
rightCall.errorClass === "tool-result-error"
) {
continue;
}
if (
leftCall.resultHash !== rightCall.resultHash ||
(leftCall.errorClass ?? "") !== (rightCall.errorClass ?? "")
) {
return `tool result ${index + 1} differs (${leftCall.tool})`;
}
}
return undefined;
}
function isHardFailureRuntimeError(errorClass: string | undefined) {
return (
errorClass === "missing-api-key" ||
errorClass === "failover" ||
errorClass === "codex-app-server" ||
errorClass === "auth" ||
errorClass === "capture-missing" ||
errorClass?.startsWith("sentinel:") === true
);
}
export function isRuntimeParityCellPassable(cell: RuntimeParityCell | undefined) {
if (!cell || cell.transportErrorClass || isHardFailureRuntimeError(cell.runtimeErrorClass)) {
return false;
}
return !cell.runtimeErrorClass || cell.runtimeErrorClass === "tool-error";
}
function hasMissingToolResult(toolCalls: readonly RuntimeParityToolCall[]) {
return toolCalls.some((toolCall) => toolCall.errorClass === TOOL_RESULT_MISSING_ERROR_CLASS);
}
function hasProvenTerminalImageResult(scenarioResult: QaSuiteScenarioLike) {
return (
scenarioResult.status === "pass" &&
(scenarioResult.steps ?? []).some(
(step) =>
step.status === "pass" &&
/(?:^|\n)image_generate=true\r?\nMEDIA:\S+/u.test(step.details ?? ""),
)
);
}
const PROVEN_TERMINAL_IMAGE_RESULT_HASH = parity.stableHash({ kind: "media", status: "success" });
function resolveRuntimeParityToolCalls(params: {
transcriptToolCalls: RuntimeParityToolCall[];
terminalImageResultProven?: boolean;
}): RuntimeParityToolCall[] {
let selected = params.transcriptToolCalls;
const imageCalls = selected.filter((toolCall) => toolCall.tool === "image_generate");
if (params.terminalImageResultProven && imageCalls.length === 1) {
selected = selected.map((toolCall) => {
if (
toolCall.tool !== "image_generate" ||
(toolCall.errorClass !== undefined &&
toolCall.errorClass !== TOOL_RESULT_MISSING_ERROR_CLASS)
) {
return toolCall;
}
return {
...toolCall,
resultHash: PROVEN_TERMINAL_IMAGE_RESULT_HASH,
errorClass: undefined,
};
});
}
return selected;
}
function filterMockRequestsForParentPrompt(
requests: RuntimeParityMockRequestSnapshot[],
parentPrompt: string,
parentPrompts: readonly string[] = [parentPrompt],
) {
const normalizedParentPrompts = parentPrompts
.map(normalizeTextForParity)
.filter((prompt) => prompt.length > 0);
if (normalizedParentPrompts.length === 0) {
return requests;
}
const matching = requests.filter((request) => {
const normalizedPrompt = normalizeTextForParity(request.prompt ?? "");
if (normalizedPrompt) {
return normalizedParentPrompts.some((prompt) => normalizedPrompt.includes(prompt));
}
const normalizedHistory = normalizeTextForParity(request.allInputText ?? "");
return normalizedParentPrompts.some((prompt) => normalizedHistory.includes(prompt));
});
return matching.length > 0 ? matching : requests;
}
function summarizeSentinelErrorClass(findings: readonly GatewayLogSentinelFinding[]) {
if (findings.length === 0) {
return undefined;
}
return `sentinel:${findings
.map((finding) => finding.kind)
.toSorted((left, right) => left.localeCompare(right))
.join(",")}`;
}
function classifyRuntimeParityCells(params: {
openclaw: RuntimeParityCell;
codex: RuntimeParityCell;
openclawStatus: RuntimeParityStatus;
codexStatus: RuntimeParityStatus;
openclawDetails?: string;
codexDetails?: string;
}): Pick<RuntimeParityResult, "drift" | "driftDetails"> {
if (
isHardFailureRuntimeError(params.openclaw.runtimeErrorClass) ||
isHardFailureRuntimeError(params.codex.runtimeErrorClass) ||
params.openclaw.transportErrorClass ||
params.codex.transportErrorClass
) {
return {
drift: "failure-mode",
driftDetails:
params.openclaw.transportErrorClass || params.codex.transportErrorClass
? "at least one runtime hit a transport failure"
: "at least one runtime hit a hard runtime failure",
};
}
if (
hasMissingToolResult(params.openclaw.toolCalls) ||
hasMissingToolResult(params.codex.toolCalls)
) {
return {
drift: "failure-mode",
driftDetails: "at least one runtime planned a tool call without a tool result",
};
}
const openclawKnownHarnessGap = isKnownHarnessGapSkip({
status: params.openclawStatus,
details: params.openclawDetails,
});
const codexKnownHarnessGap = isKnownHarnessGapSkip({
status: params.codexStatus,
details: params.codexDetails,
});
if (
openclawKnownHarnessGap !== codexKnownHarnessGap &&
(openclawKnownHarnessGap ? params.codexStatus : params.openclawStatus) === "pass" &&
isRuntimeParityCellPassable(params.openclaw) &&
isRuntimeParityCellPassable(params.codex)
) {
const skippedRuntime = openclawKnownHarnessGap ? "openclaw" : "codex";
return {
drift: "structural",
driftDetails: `known harness gap in ${skippedRuntime} runtime; paired runtime passed`,
};
}
if (
params.openclawStatus !== "pass" ||
params.codexStatus !== "pass" ||
!isRuntimeParityCellPassable(params.openclaw) ||
!isRuntimeParityCellPassable(params.codex)
) {
return {
drift: "failure-mode",
driftDetails:
params.openclawStatus === params.codexStatus
? params.openclawStatus === "skip"
? "both canonical runtime-pair cells skipped"
: params.openclawStatus === "fail"
? "both canonical runtime-pair cells failed"
: "at least one runtime failed"
: `runtime-pair cell status differs (${params.openclawStatus} vs ${params.codexStatus})`,
};
}
const toolCallShapeDetails = parity.compareToolCallShape(
params.openclaw.toolCalls,
params.codex.toolCalls,
);
if (toolCallShapeDetails) {
return { drift: "tool-call-shape", driftDetails: toolCallShapeDetails };
}
const toolResultShapeDetails = compareToolResultShape(
params.openclaw.toolCalls,
params.codex.toolCalls,
);
if (toolResultShapeDetails) {
return { drift: "tool-result-shape", driftDetails: toolResultShapeDetails };
}
const openclawTranscriptLines = params.openclaw.transcriptBytes.trim().length
? params.openclaw.transcriptBytes.trim().split(/\r?\n/u).length
: 0;
const codexTranscriptLines = params.codex.transcriptBytes.trim().length
? params.codex.transcriptBytes.trim().split(/\r?\n/u).length
: 0;
if (
openclawTranscriptLines !== codexTranscriptLines ||
(!params.openclaw.finalText && Boolean(params.codex.finalText)) ||
(Boolean(params.openclaw.finalText) && !params.codex.finalText)
) {
return {
drift: "structural",
driftDetails: `transcript/final-text structure differs (${openclawTranscriptLines} lines vs ${codexTranscriptLines})`,
};
}
if (
normalizeTextForParity(params.openclaw.finalText) ===
normalizeTextForParity(params.codex.finalText)
) {
return { drift: "none" };
}
return { drift: "text-only", driftDetails: "final text differs after whitespace normalization" };
}
function isRuntimeParityRootSession(entry: RuntimeParitySessionEntry) {
if (readNonEmptyString(entry.spawnedBy) || readNonEmptyString(entry.parentSessionKey)) {
return false;
}
if (typeof entry.spawnDepth === "number" && entry.spawnDepth > 0) {
return false;
}
if (readNonEmptyString(entry.subagentRole)) {
return false;
}
return true;
}
function runtimeParitySessionEnv(stateDir: string): NodeJS.ProcessEnv {
return { ...process.env, OPENCLAW_STATE_DIR: stateDir };
}
async function readRuntimeParitySessionEntries(params: {
gateway: QaGatewayLike;
agentId: string;
preferredSessionKeys?: ReadonlySet<string>;
}): Promise<RuntimeParitySessionCandidate[]> {
// This feeds release evidence: after bounded FTS-settle retries, a persistent
// store failure must fail capture instead of becoming an empty false green.
const store = await readRawQaSessionStore(
{ gateway: params.gateway },
{ agentId: params.agentId },
);
const entries = Object.entries(store)
.filter(([, entry]) => readNonEmptyString(entry.sessionId))
.map(([sessionKey, entry]) => ({
entry: entry as RuntimeParitySessionEntry,
sessionKey,
}))
.filter(({ entry }) => !readNonEmptyString(entry.heartbeatIsolatedBaseSessionKey));
const selectedEntries = params.preferredSessionKeys
? entries.filter(({ sessionKey }) => params.preferredSessionKeys?.has(sessionKey))
: entries;
const rootEntries = selectedEntries.filter(({ entry }) => isRuntimeParityRootSession(entry));
const candidates = rootEntries.length > 0 ? rootEntries : selectedEntries;
return candidates.toSorted((left, right) => {
const leftCreatedAt = left.entry.createdAt ?? left.entry.updatedAt ?? 0;
const rightCreatedAt = right.entry.createdAt ?? right.entry.updatedAt ?? 0;
return leftCreatedAt - rightCreatedAt || left.sessionKey.localeCompare(right.sessionKey);
});
}
async function loadRuntimeParityCaptureSources(params: {
gateway: QaGatewayLike;
agentId: string;
preferredSessionKeys?: readonly string[];
}): Promise<RuntimeParityCaptureSources> {
const stateDir = `${params.gateway.tempRoot}/state`;
const env = runtimeParitySessionEnv(stateDir);
const storePath = resolveStorePath(undefined, { agentId: params.agentId, env });
const sessionEntries = await readRuntimeParitySessionEntries({
gateway: params.gateway,
agentId: params.agentId,
...(params.preferredSessionKeys?.length
? { preferredSessionKeys: new Set(params.preferredSessionKeys) }
: {}),
});
const sessions: RuntimeParityCaptureSources["sessions"] = [];
for (const { entry, sessionKey } of sessionEntries) {
const sessionId = readNonEmptyString(entry.sessionId);
if (!sessionId) {
continue;
}
let transcriptBytes = "";
try {
const events = loadTranscriptEventsSync({
agentId: params.agentId,
env,
sessionId,
sessionKey,
});
transcriptBytes = events
.map((event) => JSON.stringify(event))
.join("\n")
.trimEnd();
if (transcriptBytes && isHeartbeatOnlyRuntimeTranscript(transcriptBytes)) {
continue;
}
} catch {
// Ignore missing transcript files so failed cells still render.
}
let trajectoryToolCalls: RuntimeParityObservedToolCall[] = [];
try {
const trajectoryEvents = await loadSqliteTrajectoryRuntimeEvents({
agentId: params.agentId,
env,
sessionId,
storePath,
});
trajectoryToolCalls = resolveTrajectoryToolCallOrder(trajectoryEvents);
} catch {
// Transcript evidence remains authoritative when diagnostics are unavailable.
}
if (transcriptBytes || trajectoryToolCalls.length > 0) {
sessions.push({ transcriptBytes, trajectoryToolCalls });
}
}
return {
sessions,
transcriptBytes: sessions
.map((session) => session.transcriptBytes)
.filter(Boolean)
.join("\n"),
};
}
function runtimeParitySessionKeysFromScenarioResult(result: QaSuiteScenarioLike) {
const sessionKeys = new Set<string>();
const detailBlocks = [result.details, ...(result.steps ?? []).map((step) => step.details)];
for (const detailBlock of detailBlocks) {
for (const line of detailBlock?.split(/\r?\n/u) ?? []) {
if (!line.startsWith(RUNTIME_PARITY_SESSION_KEY_DETAIL_PREFIX)) {
continue;
}
const sessionKey = readNonEmptyString(
line.slice(RUNTIME_PARITY_SESSION_KEY_DETAIL_PREFIX.length),
);
if (sessionKey) {
sessionKeys.add(sessionKey);
}
}
}
return [...sessionKeys];
}
async function loadRuntimeParityMockToolCalls(
mockBaseUrl: string | undefined,
parentPrompt: string,
parentPrompts: readonly string[] = [parentPrompt],
): Promise<RuntimeParityToolCall[] | null> {
const normalizedBaseUrl = mockBaseUrl?.trim().replace(/\/+$/u, "");
if (!normalizedBaseUrl) {
return null;
}
try {
const { response, release } = await fetchWithSsrFGuard({
url: `${normalizedBaseUrl}/debug/requests`,
policy: { allowPrivateNetwork: true },
auditContext: "qa-lab-runtime-parity-mock-tool-calls",
});
let payload: unknown;
try {
if (!response.ok) {
await discardIgnoredResponseBody(response);
return null;
}
payload = await response.json();
} finally {
await release();
}
if (!Array.isArray(payload)) {
return null;
}
const requests = payload.filter(isMessageRecord).map(
(entry): RuntimeParityMockRequestSnapshot => ({
prompt: readNonEmptyString(entry.prompt),
allInputText: readNonEmptyString(entry.allInputText),
plannedToolName: readNonEmptyString(entry.plannedToolName),
plannedToolArgs: entry.plannedToolArgs ?? null,
toolOutput: readNonEmptyString(entry.toolOutput) ?? "",
}),
);
return resolveToolCallOrderFromMockRequests(
filterMockRequestsForParentPrompt(requests, parentPrompt, parentPrompts),
);
} catch {
return null;
}
}
export async function captureRuntimeParityCell(
params: RuntimeParityCaptureParams,
): Promise<RuntimeParityCell> {
const agentId = params.agentId ?? DEFAULT_AGENT_ID;
const { sessions, transcriptBytes } = await loadRuntimeParityCaptureSources({
gateway: params.gateway,
agentId,
preferredSessionKeys: runtimeParitySessionKeysFromScenarioResult(params.scenarioResult),
});
const transcriptRecords = buildTranscriptRecords(transcriptBytes);
// Runtime-tool fixtures split happy and failure paths across root sessions.
// Resolve each session separately so repeated tool-call ids cannot cross-link.
const runtimeToolCalls = removeRuntimeParityToolCallIdentity(
sessions.flatMap((session) =>
mergeRuntimeParityToolCalls({
transcriptToolCalls: resolveToolCallOrder(buildTranscriptRecords(session.transcriptBytes)),
trajectoryToolCalls: session.trajectoryToolCalls,
}),
),
);
const parentPrompts = transcriptRecords
.filter((record) => record.role === "user")
.map((record) => extractAssistantText(record.message))
.filter((prompt) => prompt.length > 0);
const parentPrompt = parentPrompts[0] ?? "";
const mockToolCalls = await loadRuntimeParityMockToolCalls(
params.mockBaseUrl,
parentPrompt,
parentPrompts,
);
const gatewayLogs = params.gateway.logs?.();
const sentinelFindings = [
...scanGatewayLogSentinels(gatewayLogs),
...scanDirectReplyTranscriptSentinels(transcriptBytes),
];
// Retry passes retain first-attempt diagnostics; only terminal failures may
// classify that historical text as the cell's runtime error.
const scenarioErrorClass =
params.scenarioResult.status === "fail"
? classifyScenarioError(params.scenarioResult.details)
: undefined;
const sentinelErrorClass = summarizeSentinelErrorClass(sentinelFindings);
const terminalImageResultProven = hasProvenTerminalImageResult(params.scenarioResult);
return {
runtime: params.runtime,
transcriptBytes,
toolCalls: resolveRuntimeParityToolCalls({
transcriptToolCalls: runtimeToolCalls,
terminalImageResultProven,
}),
...(mockToolCalls ? { providerPlanToolCalls: mockToolCalls } : {}),
finalText: extractFinalAssistantText(transcriptRecords),
usage: aggregateUsage(transcriptRecords),
cacheDiagnostics: buildRuntimeParityCacheDiagnostics(
transcriptRecords
.filter((record) => record.role === "assistant")
.map((record) => readAssistantUsage(record.message)),
),
wallClockMs: params.wallClockMs,
...(params.bootstrapWallClockMs === undefined
? {}
: { bootstrapWallClockMs: params.bootstrapWallClockMs }),
...(scenarioErrorClass || sentinelErrorClass
? { runtimeErrorClass: scenarioErrorClass ?? sentinelErrorClass }
: {}),
bootStateLines: extractBootStateLines(gatewayLogs),
...(sentinelFindings.length > 0 ? { sentinelFindings } : {}),
...(params.scenarioResult.modelSwitchEvidence
? { modelSwitchEvidence: params.scenarioResult.modelSwitchEvidence }
: {}),
};
}
export async function runRuntimeParityScenario(params: {
scenarioId: string;
runtimeParityUsage?: RuntimeParityUsagePolicy;
runtimePair?: readonly [RuntimeId, RuntimeId];
runCell: (runtime: RuntimeId) => Promise<RuntimeParityScenarioExecution>;
}): Promise<RuntimeParityResult> {
const [firstRuntime, secondRuntime] = params.runtimePair ?? CANONICAL_RUNTIME_IDS;
if (firstRuntime === secondRuntime) {
throw new Error("Runtime parity must compare two different runtimes.");
}
const first = await params.runCell(firstRuntime);
const second = await params.runCell(secondRuntime);
const [openclaw, codex] = firstRuntime === "openclaw" ? [first, second] : [second, first];
const drift = classifyRuntimeParityCells({
openclaw: openclaw.cell,
codex: codex.cell,
openclawStatus: openclaw.status,
codexStatus: codex.status,
openclawDetails: openclaw.details,
codexDetails: codex.details,
});
return {
scenarioId: params.scenarioId,
runtimeParityUsage: resolveRuntimeParityUsagePolicy(params.runtimeParityUsage),
cells: {
openclaw: {
...openclaw.cell,
status: openclaw.status,
...(openclaw.details ? { details: openclaw.details } : {}),
},
codex: {
...codex.cell,
status: codex.status,
...(codex.details ? { details: codex.details } : {}),
},
},
drift: drift.drift,
...(drift.driftDetails ? { driftDetails: drift.driftDetails } : {}),
};
}
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */