Files
qwen-code/scripts/run-managed-agent-server-e2e.ts
T
jinye b1cd4ac0c6 refactor(managed-agent): align shared MySQL schema conventions (#16143)
* docs(managed-agent): document database tables and columns

* refactor(managed-agent): align shared MySQL schema conventions
2026-11-03 12:00:29 +00:00

6025 lines
240 KiB
TypeScript

import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
import { createHash, randomBytes, randomUUID } from 'node:crypto';
import {
existsSync,
mkdirSync,
mkdtempSync,
readFileSync,
realpathSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { createServer, request as httpRequest, type Server } from 'node:http';
import { homedir, tmpdir } from 'node:os';
import path from 'node:path';
import { inspect } from 'node:util';
import {
fakeToolCall,
startFakeOpenAIServer,
} from '../integration-tests/fake-openai-server.js';
import { SseObserver } from '../integration-tests/helpers/hosted-sse-observer.js';
import { isDeepStrictEqual } from 'node:util';
const root = process.cwd();
const databaseEpochMillisSql =
'CAST(UNIX_TIMESTAMP() AS DECIMAL(20, 0)) * 1000' +
' + FLOOR(EXTRACT(MICROSECOND FROM CURRENT_TIMESTAMP(6)) / 1000)';
const reliefMarker = 'G3_STEP3_RELIEF_TURN';
const reliefResponse = 'G3_STEP3_RELIEF_COMPLETED';
let originalApprovalId: string | undefined;
interface SavedApprovalOptions {
v: number;
inputRevision: number;
policyRevision: string;
createdAt: number;
expiresAt: number;
continuationRef: { resourceId: string };
}
interface SavedApprovalPlan {
calls: {
prepareKey: string;
requestDigest: string;
runtimeCallId?: string;
partIndex?: number;
actionId?: string | null;
}[];
actionId: string;
approvalOrdinal?: number;
stage: string;
batchId: string;
runtime: {
runtimeSessionId: string;
bindingId: string;
generation: string;
workspaceGeneration: string;
};
}
let originalApprovalOptions: SavedApprovalOptions | undefined;
let originalApprovalPlan: SavedApprovalPlan | undefined;
// A3: the pending second sibling's Action, captured at the crash barrier and
// re-decided by the recovery generation.
let siblingApprovalId: string | undefined;
let siblingApprovalOptions: SavedApprovalOptions | undefined;
// B3: tool B's new Action, detected after Allow A and decided by the runner's
// second Allow; both earlier Actions must end decided under their original IDs.
let secondApprovalId: string | undefined;
let verifiedOriginalTerminal: string | undefined;
function writeEvidenceFile(
directory: string,
name: string,
body: unknown,
): void {
mkdirSync(directory, { recursive: true });
writeFileSync(
path.join(directory, name),
JSON.stringify(body, null, 2) + '\n',
);
}
function saveEvidence(name: string, body: unknown): void {
if (!verificationMode) return;
writeEvidenceFile(verificationDirectory, name, body);
}
const argumentsList = process.argv.slice(2);
let model = 'moonshot/kimi-k3';
let runtimeDelayMs = 0;
let settingsPath = path.join(homedir(), '.qwen', 'settings.json');
let sessionFailover = false;
let inflightFailover = false;
let continuationFailover = false;
let bigOutput = false;
let harnessOnly = false;
let freeze = false;
let modelRoundFailover = false;
let frozenControlPlane = false;
let g3VerifyArgument: string | undefined;
// Step 3 A5/A5a barriers, the A4/C4 lost-reply arms, and the C1-C3
// cleanup/terminal arms layer one fault point on the Allow arm; every other
// mode answers a different question and would make the same crash prove two
// things at once.
const approvalFaults = [
'after-decision',
'after-prepare',
'after-result',
'finished-before-commit',
'sibling-approval',
'after-earlier-tools',
'cleanup-failed',
'decision-reply-lost',
'admission-reply-lost',
'load-reply-lost',
'terminal-reply-lost',
'terminal-write-failed',
] as const;
type ApprovalFault = (typeof approvalFaults)[number];
let g3FaultArgument: string | undefined;
for (let index = 0; index < argumentsList.length; index += 1) {
const argument = argumentsList[index];
const value = argumentsList[index + 1];
if (argument === '--model' && value) {
model = value;
index += 1;
} else if (argument === '--runtime-delay-ms' && value) {
runtimeDelayMs = Number(value);
index += 1;
} else if (argument === '--settings' && value) {
settingsPath = path.resolve(value);
index += 1;
} else if (argument === '--session-failover') {
sessionFailover = true;
} else if (argument === '--inflight-failover') {
inflightFailover = true;
} else if (argument === '--big-output') {
bigOutput = true;
} else if (argument === '--continuation-failover') {
continuationFailover = true;
} else if (argument === '--harness-only') {
harnessOnly = true;
} else if (argument === '--freeze') {
freeze = true;
} else if (argument === '--model-round-failover') {
modelRoundFailover = true;
} else if (argument === '--frozen-control-plane') {
frozenControlPlane = true;
} else if (argument === '--g3-verify' && value) {
g3VerifyArgument = value;
index += 1;
} else if (argument === '--g3-fault' && value) {
g3FaultArgument = value;
index += 1;
} else {
throw new Error(
'Usage: run-managed-agent-server-e2e.ts [--model ID] [--runtime-delay-ms N] [--settings PATH] [--session-failover|--inflight-failover|--continuation-failover|--model-round-failover|--big-output] [--harness-only] [--freeze] [--frozen-control-plane] [--g3-verify allow|deny|cancel|expiry|continuation] [--g3-fault after-decision|after-prepare|after-result|finished-before-commit|sibling-approval|after-earlier-tools|cleanup-failed|decision-reply-lost|admission-reply-lost|load-reply-lost|terminal-reply-lost|terminal-write-failed]',
);
}
}
// The flag is what CI and the npm scripts pass; the environment spelling is
// what the Step 3 report published, so it keeps working.
const verificationMode = g3VerifyArgument ?? process.env['G3_VERIFY_MODE'];
if (
verificationMode &&
!['allow', 'deny', 'cancel', 'expiry', 'continuation'].includes(
verificationMode,
)
) {
throw new Error(
'--g3-verify (or G3_VERIFY_MODE) must be allow, deny, cancel, expiry or continuation',
);
}
const approvalRecovery =
!!verificationMode && verificationMode !== 'continuation';
const approvalFault = (g3FaultArgument ?? process.env['G3_FAULT']) as
| ApprovalFault
| undefined;
if (approvalFault && !approvalFaults.includes(approvalFault)) {
throw new Error(
`--g3-fault (or G3_FAULT) must be ${approvalFaults.join(', ')}`,
);
}
if (approvalFault && verificationMode !== 'allow') {
throw new Error(
'--g3-fault requires --g3-verify allow: Deny, Cancel and expiry have no approved execution to crash into',
);
}
const verificationDirectory = path.resolve(
root,
process.env['G3_VERIFY_REPORT_DIR'] ??
`.qwen/investigations/g3-step3-implementation/packaged-${verificationMode}-${Date.now()}`,
);
// The model-round arm has no gate mode; name its evidence explicitly instead
// of borrowing a packaged-undefined-<epoch> directory (R1-10).
const modelRoundEvidenceDirectory = path.resolve(
root,
process.env['G3_VERIFY_REPORT_DIR'] ??
`.qwen/investigations/g3-step3-implementation/model-round-${Date.now()}`,
);
if (
[
sessionFailover,
inflightFailover,
continuationFailover,
modelRoundFailover,
bigOutput,
].filter(Boolean).length > 1
) {
throw new Error('The deterministic E2E modes are exclusive');
}
if (
harnessOnly &&
!(
sessionFailover ||
inflightFailover ||
continuationFailover ||
modelRoundFailover
)
) {
throw new Error('--harness-only requires one of the failover modes');
}
if (freeze && (!continuationFailover || harnessOnly)) {
throw new Error(
'--freeze requires --continuation-failover without --harness-only',
);
}
// The model-round arm replaces either the Harness alone (twice, under a live
// Spring) or a frozen, still-alive former control plane; a kill-both variant
// would only repeat --session-failover's owner swap without a parked round.
if (modelRoundFailover && harnessOnly === frozenControlPlane) {
throw new Error(
'--model-round-failover requires exactly one of --harness-only or --frozen-control-plane',
);
}
if (frozenControlPlane && !modelRoundFailover) {
throw new Error('--frozen-control-plane requires --model-round-failover');
}
if (verificationMode && (!continuationFailover || !harnessOnly || freeze)) {
throw new Error(
'This isolated Step 3 probe requires --continuation-failover --harness-only',
);
}
if (verificationMode) mkdirSync(verificationDirectory, { recursive: true });
const durableFailover =
sessionFailover ||
inflightFailover ||
continuationFailover ||
modelRoundFailover ||
bigOutput;
const runtimeTakeover = inflightFailover || continuationFailover;
const workspaceTurns = runtimeTakeover || bigOutput;
const registerStorage = process.env['QWEN_E2E_REGISTER_STORAGE'] === '1';
if (registerStorage && process.platform !== 'linux') {
throw new Error(
'QWEN_E2E_REGISTER_STORAGE=1 requires Linux native storage identity',
);
}
// The tool-driven modes take over a dead Runtime binding, which needs the
// durable local-Worker reclaim from the W0e line — Linux-only today. With
// --harness-only the Spring and its Broker stay alive, the worker is never
// orphaned, and no reclaim is needed.
if (runtimeTakeover && !harnessOnly && process.platform !== 'linux') {
throw new Error(
`${inflightFailover ? '--inflight-failover' : '--continuation-failover'} requires Linux: the replacement owner must retire the dead worker's Runtime binding through the durable local-Worker reclaim (#12380 W0e), which only runs on Linux. Run the mode in the Hosted failover E2E CI job or a Linux container.`,
);
}
// The Stage A acceptance criterion names a 15-second Runtime delay; the
// real-provider TTFT margin under it is unrecorded (tracked in #12941).
const modelBeforeRuntimeAssertionDelayMs = 15_000;
// The frozen-arm teardown wakes the former writer by this registry key, so
// the start name and the wake-up lookup must share it (a rename drift
// would leave a stopped child wedged on the runner).
const hostedHarnessLabel = 'Hosted Harness';
if (!Number.isSafeInteger(runtimeDelayMs) || runtimeDelayMs < 0) {
throw new Error('--runtime-delay-ms must be a non-negative integer');
}
const cliBundle = path.join(root, 'dist', 'cli.js');
const springModule = path.join(root, 'packages', 'managed-agent-server');
const springTarget = path.join(springModule, 'server', 'target');
// The pom attaches classified repackage executions alongside the
// unclassified server jar, so a wildcard over target/ matches several
// artifacts. Read the project version from the
// pom and name the unclassified jar exactly — version-independent without
// assuming it is the only packaged artifact. The <version> immediately
// following the module's own <artifactId> (not the <parent> block's). The
// capture itself is whitespace-tolerant, so a formatter wrapping or
// re-indenting the element cannot flow newlines into the jar path.
const pomXml = readFileSync(path.join(springModule, 'pom.xml'), 'utf8');
const pomVersion = pomXml.match(
/<artifactId>qwen-managed-agent-parent<\/artifactId>\s*<version>\s*([^<]*?\S)\s*<\/version>/,
)?.[1];
if (!pomVersion) {
throw new Error(
'Could not read the project <version> from managed-agent-server/pom.xml',
);
}
const springJar = path.join(
springTarget,
`qwen-managed-agent-server-${pomVersion}-exec.jar`,
);
for (const required of [
cliBundle,
springJar,
...(durableFailover ? [] : [settingsPath]),
]) {
if (!existsSync(required)) {
throw new Error(`Required file is missing: ${required}`);
}
}
function command(name: string): string {
const result = spawnSync('which', [name], { encoding: 'utf8' });
if (result.status !== 0) {
throw new Error(`Required command is missing: ${name}`);
}
return realpathSync(result.stdout.trim());
}
const java = command('java');
const mysqld = command('mysqld');
const mysql = command('mysql');
const mysqladmin = command('mysqladmin');
const mysqldUserArguments = process.getuid?.() === 0 ? ['--user=root'] : [];
let receivedSignal: NodeJS.Signals | undefined;
const handleSignal = (signal: NodeJS.Signals) => {
receivedSignal = signal;
};
process.on('SIGINT', handleSignal);
process.on('SIGTERM', handleSignal);
const temporary = realpathSync(
mkdtempSync(path.join(tmpdir(), 'managed-agent-server-e2e-')),
);
const workspace = path.join(temporary, 'workspace');
const harnessHome = path.join(temporary, 'harness-home');
const replacementHarnessHome = path.join(temporary, 'replacement-harness-home');
const runtimeHome = path.join(temporary, 'runtime-home');
const replacementRuntimeHome = path.join(temporary, 'replacement-runtime-home');
const runtimeState = path.join(temporary, 'runtime-state');
// The frozen control-plane arm runs two live Brokers side by side, so the
// replacement must not share the frozen one's local Runtime state.
const replacementRuntimeState = path.join(
temporary,
'replacement-runtime-state',
);
const mysqlData = path.join(temporary, 'mysql-data');
const mysqlSocket = path.join(temporary, 'mysql.sock');
const mysqlError = path.join(temporary, 'mysql-error.log');
const trustedFolders = path.join(temporary, 'trusted-folders.json');
const delayedNode = path.join(temporary, 'delayed-node');
const sideEffectName = 'managed-agent-real-e2e.txt';
const sideEffectContent = 'managed agent real model tool execution complete';
const workspaceMount = path.join(temporary, 'workspace-mount');
const sideEffect = path.join(workspaceMount, sideEffectName);
const boundWorkspaceId = 'e2e-workspace';
const boundStorageId = 'e2e-storage';
const trustedActorHeader = 'x-qwen-e2e-trusted-actor';
const trustedActor = 'e2e-actor';
const inflightSideEffectName = 'managed-inflight-side-effect.txt';
const inflightSideEffect = path.join(workspaceMount, inflightSideEffectName);
const inflightSideEffectContent = 'MANAGED_INFLIGHT_TOOL_EXECUTED\n';
// A3 mixed-sibling arm: the second sibling writes a distinct file so each
// sibling's single execution has its own physical effect.
const siblingSideEffectName = 'managed-sibling-side-effect.txt';
const siblingSideEffect = path.join(workspaceMount, siblingSideEffectName);
const siblingSideEffectContent = 'MANAGED_SIBLING_TOOL_EXECUTED\n';
// B3 after-earlier-tools arm: a second, later tool round writes its own file so
// each earlier round's single execution has its own physical effect. The +Nk
// budget directive makes the TurnBudget snapshot non-null so its restoration is
// numerically assertable (the default null budget hides a ledger reset). A round
// reset is caught by the exactly-once / deep-equal / round-continuity assertions.
const b3SecondSideEffectName = 'managed-b3-second-side-effect.txt';
const b3SecondSideEffect = path.join(workspaceMount, b3SecondSideEffectName);
const b3SecondSideEffectContent = 'MANAGED_B3_SECOND_TOOL_EXECUTED\n';
const b3BudgetDirective = '+500k';
const workspaceId = createHash('sha256')
.update(workspace)
.digest('hex')
.slice(0, 16);
try {
for (const directory of [
workspace,
path.join(harnessHome, '.qwen'),
...(durableFailover
? [
path.join(replacementHarnessHome, '.qwen'),
path.join(replacementRuntimeHome, '.qwen'),
]
: []),
workspaceMount,
path.join(runtimeHome, '.qwen'),
runtimeState,
...(frozenControlPlane ? [replacementRuntimeState] : []),
mysqlData,
]) {
// The durable local-Runtime store refuses a directory not private to its
// owner, so the state directory gets owner-only permissions.
mkdirSync(directory, {
recursive: true,
...(directory === runtimeState && workspaceTurns ? { mode: 0o700 } : {}),
});
}
if (durableFailover) {
for (const home of [harnessHome, replacementHarnessHome]) {
writeFileSync(
path.join(home, '.qwen', 'settings.json'),
JSON.stringify({ ui: { enableFollowupSuggestions: false } }),
{ mode: 0o600 },
);
}
} else {
const sourceSettings = JSON.parse(
readFileSync(settingsPath, 'utf8'),
) as Record<string, unknown>;
const providerGroups = sourceSettings['modelProviders'] as
| Record<string, unknown>
| undefined;
let selectedProviderGroup: string | undefined;
let selectedProvider: Record<string, unknown> | undefined;
for (const [group, entries] of Object.entries(providerGroups ?? {})) {
const providers = Array.isArray(entries)
? entries
: Object.values((entries ?? {}) as Record<string, unknown>);
const match = providers.find(
(entry) =>
typeof entry === 'object' &&
entry !== null &&
((entry as Record<string, unknown>)['id'] === model ||
(entry as Record<string, unknown>)['name'] === model),
);
if (match) {
selectedProviderGroup = group;
selectedProvider = match as Record<string, unknown>;
break;
}
}
if (!selectedProviderGroup || !selectedProvider) {
throw new Error(
`Model provider is missing from ${settingsPath}: ${model}`,
);
}
const environmentKey = selectedProvider['envKey'];
const sourceEnvironment = (sourceSettings['env'] ?? {}) as Record<
string,
unknown
>;
if (
typeof environmentKey !== 'string' ||
typeof (
sourceEnvironment[environmentKey] ?? process.env[environmentKey]
) !== 'string'
) {
throw new Error(`Model credential is missing for ${model}`);
}
const harnessSettings = {
...(sourceSettings['$version'] === undefined
? {}
: { $version: sourceSettings['$version'] }),
env: {
[environmentKey]:
sourceEnvironment[environmentKey] ?? process.env[environmentKey],
},
model: {
name: model,
...(typeof selectedProvider['baseUrl'] === 'string'
? { baseUrl: selectedProvider['baseUrl'] }
: {}),
},
modelProviders: { [selectedProviderGroup]: [selectedProvider] },
...(sourceSettings['security'] === undefined
? {}
: { security: sourceSettings['security'] }),
ui: { enableFollowupSuggestions: false },
};
writeFileSync(
path.join(harnessHome, '.qwen', 'settings.json'),
JSON.stringify(harnessSettings),
{ mode: 0o600 },
);
}
writeFileSync(
path.join(runtimeHome, '.qwen', 'settings.json'),
JSON.stringify({ ui: { enableFollowupSuggestions: false } }),
{ mode: 0o600 },
);
if (durableFailover) {
writeFileSync(
path.join(replacementRuntimeHome, '.qwen', 'settings.json'),
JSON.stringify({ ui: { enableFollowupSuggestions: false } }),
{ mode: 0o600 },
);
}
writeFileSync(
trustedFolders,
JSON.stringify({ [workspace]: 'TRUST_FOLDER' }),
{ mode: 0o600 },
);
writeFileSync(
delayedNode,
`#!/bin/sh\n/bin/sleep ${runtimeDelayMs / 1000}\nexec '${process.execPath.replaceAll("'", "'\\''")}' "$@"\n`,
{ mode: 0o700 },
);
} catch (error) {
rmSync(temporary, { recursive: true, force: true });
throw error;
}
const cleanEnvironment = Object.fromEntries(
Object.entries(process.env).filter(
([key]) =>
!/^(https?|all)_proxy$/i.test(key) &&
!/^(qwen|dashscope|openai|anthropic|google|gemini|azure|aws|vertex)_/i.test(
key,
) &&
!/(api_?key|token|secret|password|credentials?)$/i.test(key),
),
);
type Child = { child: ChildProcess; log: () => string; name: string };
const children: Child[] = [];
// Children the frozen control-plane arm stopped: teardown must wake them, or
// stopChild's SIGTERM stays pending on a stopped tree for its whole stall.
const frozenChildren: Child[] = [];
function start(
executable: string,
args: string[],
options: { cwd?: string; env?: NodeJS.ProcessEnv } = {},
name: string,
): Child {
let output = '';
const child = spawn(executable, args, {
cwd: options.cwd ?? root,
env: options.env ?? process.env,
detached: process.platform !== 'win32',
stdio: ['ignore', 'pipe', 'pipe'],
});
const append = (chunk: Buffer) => {
output += chunk.toString();
if (output.length > 32_768) output = output.slice(-32_768);
};
child.stdout?.on('data', append);
child.stderr?.on('data', append);
const registered = { child, log: () => output, name };
children.push(registered);
return registered;
}
// One spelling for "has this child exited": five independent phrasings
// drifted into the pre-fix ESRCH that process.kill raised on a reaped pid.
function childExited(child: ChildProcess): boolean {
return child.exitCode !== null || child.signalCode !== null;
}
function processTreeExists(child: ChildProcess): boolean {
if (child.pid === undefined) return false;
if (process.platform === 'win32') {
return !childExited(child);
}
try {
process.kill(-child.pid, 0);
return true;
} catch (error) {
return (error as NodeJS.ErrnoException).code !== 'ESRCH';
}
}
function signalProcessTree(child: ChildProcess, signal: NodeJS.Signals): void {
if (process.platform !== 'win32' && child.pid !== undefined) {
try {
process.kill(-child.pid, signal);
return;
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'ESRCH') return;
}
}
child.kill(signal);
}
async function stopChild(child: ChildProcess): Promise<void> {
if (!processTreeExists(child)) return;
signalProcessTree(child, 'SIGTERM');
const deadline = Date.now() + 10_000;
while (processTreeExists(child) && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
if (processTreeExists(child)) {
signalProcessTree(child, 'SIGKILL');
const killDeadline = Date.now() + 5_000;
while (processTreeExists(child) && Date.now() < killDeadline) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
}
}
async function crashChild(child: ChildProcess, name: string): Promise<void> {
if (!processTreeExists(child)) {
throw new Error(`${name} exited before the crash was injected`);
}
signalProcessTree(child, 'SIGKILL');
const deadline = Date.now() + 5_000;
while (processTreeExists(child) && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
if (processTreeExists(child)) {
throw new Error(`${name} process tree survived SIGKILL`);
}
}
async function crashProcess(child: ChildProcess, name: string): Promise<void> {
// A child killed BY signal has exitCode null but signalCode set; missing
// that branch kills a pid that no longer exists and throws raw ESRCH.
if (child.pid === undefined || childExited(child)) {
throw new Error(`${name} exited before the crash was injected`);
}
process.kill(child.pid, 'SIGKILL');
const deadline = Date.now() + 5_000;
while (!childExited(child) && Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 50));
}
if (!childExited(child)) {
throw new Error(`${name} survived SIGKILL`);
}
}
const allocatedPorts = new Set<number>();
async function freePort(): Promise<number> {
const server = createServer();
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => resolve());
});
const address = server.address();
if (!address || typeof address === 'string') {
server.close();
throw new Error('Could not allocate a loopback port');
}
await new Promise<void>((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
if (allocatedPorts.has(address.port)) return freePort();
allocatedPorts.add(address.port);
return address.port;
}
type HeldExecutionStartProxy = {
baseUrl: string;
close: () => Promise<void>;
heldPath: () => string | undefined;
observations: () => string[];
// C3: every Runtime cleanup release is refused until allowRelease(), so
// the doomed generation cannot discharge its own cleanup debt before the
// crash lands.
releaseFailureCount: () => number;
releaseSuccessCount: () => number;
allowRelease: () => void;
};
async function startHeldExecutionStartProxy(
targetOrigin: string,
holdExecutionStart = true,
failRelease = false,
): Promise<HeldExecutionStartProxy> {
let heldPath: string | undefined;
const observations: string[] = [];
let closed = false;
let releaseAllowed = !failRelease;
let releaseFailureCount = 0;
let releaseSuccessCount = 0;
const server: Server = createServer((request, response) => {
void (async () => {
const chunks: Buffer[] = [];
for await (const chunk of request) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const target = new URL(request.url ?? '/', targetOrigin);
const method = request.method ?? 'GET';
observations.push(`${method} ${target.pathname} received`);
if (
failRelease &&
method === 'POST' &&
target.pathname.endsWith(':release') &&
!target.pathname.endsWith(':release-mount')
) {
if (!releaseAllowed) {
releaseFailureCount += 1;
observations.push(`${method} ${target.pathname} refused`);
response.writeHead(500, { 'content-type': 'application/json' });
response.end('{"error":"e2e_cleanup_release_failed"}');
return;
}
releaseSuccessCount += 1;
}
if (
holdExecutionStart &&
method === 'POST' &&
target.pathname.includes('/executions/') &&
target.pathname.endsWith(':start')
) {
heldPath ??= target.pathname;
observations.push(`${method} ${target.pathname} held`);
request.socket.once('close', () => response.destroy());
return;
}
const headers = new Headers();
for (const [name, value] of Object.entries(request.headers)) {
if (
value === undefined ||
[
'connection',
'content-length',
'host',
'transfer-encoding',
].includes(name)
) {
continue;
}
headers.set(name, Array.isArray(value) ? value.join(', ') : value);
}
const body = Buffer.concat(chunks);
const upstream = await fetch(target, {
method,
headers,
...(['GET', 'HEAD'].includes(method) ? {} : { body }),
});
const upstreamBody = Buffer.from(await upstream.arrayBuffer());
observations.push(
`${method} ${target.pathname}${target.search} ${upstream.status}${
upstream.ok ? '' : ` ${upstreamBody.toString('utf8').slice(0, 500)}`
}`,
);
const responseHeaders: Record<string, string> = {};
upstream.headers.forEach((value, name) => {
if (
![
'connection',
'content-encoding',
'content-length',
'transfer-encoding',
].includes(name)
) {
responseHeaders[name] = value;
}
});
response.writeHead(upstream.status, responseHeaders);
response.end(upstreamBody);
})().catch((error: unknown) => {
if (response.destroyed) return;
if (!response.headersSent) response.writeHead(502);
response.end(String(error));
});
});
const port = await freePort();
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(port, '127.0.0.1', () => {
server.off('error', reject);
resolve();
});
});
const address = server.address();
if (!address || typeof address === 'string') {
server.close();
throw new Error('Held Runtime Broker proxy omitted its address');
}
return {
baseUrl: `http://127.0.0.1:${address.port}`,
heldPath: () => heldPath,
observations: () => [...observations],
releaseFailureCount: () => releaseFailureCount,
releaseSuccessCount: () => releaseSuccessCount,
allowRelease: () => {
releaseAllowed = true;
observations.push('release allowed');
},
close: async () => {
if (closed) return;
closed = true;
server.closeAllConnections();
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
},
};
}
// A 202 only admits the response operation (`PENDING` / `JAVA_DURABLE`);
// the Action row stays `requested` until the coordinator's delivery commits
// the decision. The after-decision barrier must therefore read the durable
// row, not the HTTP answer.
function approvalDecisionCommitted(row: string): boolean {
const [state, receipt, digest] = row.split('\t');
return state === 'decided' && receipt === '1' && digest === '1';
}
// The control plane answers 202 only after the decision or admission is
// committed, so dropping the reply here is a lost response, not a weaker
// request: the server keeps the committed row, and the caller retries the
// same Idempotency-Key and must land on that original command.
function postAndDropReply(
url: string,
body: string,
headers: Record<string, string>,
): Promise<void> {
const target = new URL(url);
return new Promise<void>((resolve, reject) => {
const request = httpRequest({
hostname: target.hostname,
port: target.port,
path: `${target.pathname}${target.search}`,
method: 'POST',
headers: { ...headers, 'content-length': Buffer.byteLength(body) },
});
request.once('response', (response) => {
// Destroy the reply unread: the caller must behave as if the answer
// never arrived, while the committed row stays on the server. A
// non-202 answer means nothing was committed, so the loss would prove
// nothing about the retry.
if (response.statusCode !== 202) {
response.destroy();
reject(
new Error(
`Lost-reply probe expected 202 from ${target.pathname}, got ${response.statusCode}`,
),
);
return;
}
response.once('error', () => {});
response.destroy();
resolve();
});
request.once('error', reject);
request.end(body);
});
}
type StoreTerminalFault = 'drop-reply' | 'reject';
type StoreFaultProxy = {
baseUrl: string;
observations: () => string[];
armTerminal: (sessionId: string, fault: StoreTerminalFault) => void;
terminalFaulted: () => boolean;
terminalRejectCount: () => number;
terminalAcceptCount: () => number;
allowTerminal: () => void;
close: () => Promise<void>;
};
// C1/C2: the Harness reaches the Session Store over the URL the control
// plane advertises. This proxy fronts that boundary so one matching
// `turn.settled` commit can lose its reply (committed, not delivered) or
// be refused before it lands (payable checkpoint, terminal missing).
function startStoreFaultProxy(targetOrigin: string): Promise<StoreFaultProxy> {
return (async () => {
let armed: { sessionId: string; fault: StoreTerminalFault } | undefined;
let terminalFaulted = false;
let terminalAllowed = false;
let terminalRejectCount = 0;
let terminalAcceptCount = 0;
let closed = false;
const observations: string[] = [];
const server: Server = createServer((request, response) => {
void (async () => {
const chunks: Buffer[] = [];
for await (const chunk of request) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const body = Buffer.concat(chunks);
const target = new URL(request.url ?? '/', targetOrigin);
const method = request.method ?? 'GET';
const commit = target.pathname.match(
/^\/internal\/managed-session-store\/v1\/sessions\/([^/]+)\/transactions:commit(?:-v2)?$/,
);
const sessionId = commit ? decodeURIComponent(commit[1]) : undefined;
const terminal =
armed !== undefined &&
sessionId === armed.sessionId &&
commitCarriesTurnSettled(body);
if (terminal && armed?.fault === 'reject' && !terminalAllowed) {
terminalRejectCount += 1;
terminalFaulted = true;
observations.push(`${method} ${target.pathname} rejected`);
response.writeHead(500, { 'content-type': 'application/json' });
response.end('{"error":"e2e_terminal_commit_failed"}');
return;
}
const headers = new Headers();
for (const [name, value] of Object.entries(request.headers)) {
if (
value === undefined ||
[
'connection',
'content-length',
'host',
'transfer-encoding',
].includes(name)
) {
continue;
}
headers.set(name, Array.isArray(value) ? value.join(', ') : value);
}
const upstream = await fetch(target, {
method,
headers,
...(['GET', 'HEAD'].includes(method) ? {} : { body }),
});
const upstreamBody = Buffer.from(await upstream.arrayBuffer());
if (terminal) {
if (armed?.fault === 'drop-reply' && !terminalAllowed) {
// Keep dropping every bounded retry until the crash. Arm the
// barrier only on a confirmed 200: a rejected first commit did
// not land, and letting it pass would turn C1 into C2 while the
// later compensation satisfies the final accept count.
if (upstream.status === 200) {
terminalFaulted = true;
terminalAcceptCount += 1;
observations.push(
`${method} ${target.pathname} forwarded 200 dropped reply`,
);
} else {
observations.push(
`${method} ${target.pathname} forwarded ${upstream.status} dropped reply (not committed)`,
);
}
response.destroy();
return;
}
if (upstream.status === 200) terminalAcceptCount += 1;
}
observations.push(
`${method} ${target.pathname} forwarded ${upstream.status}`,
);
const responseHeaders: Record<string, string> = {};
upstream.headers.forEach((value, name) => {
if (
![
'connection',
'content-encoding',
'content-length',
'transfer-encoding',
].includes(name)
) {
responseHeaders[name] = value;
}
});
response.writeHead(upstream.status, responseHeaders);
response.end(upstreamBody);
})().catch((error: unknown) => {
if (response.destroyed) return;
if (!response.headersSent) response.writeHead(502);
response.end(String(error));
});
});
const port = await freePort();
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(port, '127.0.0.1', () => {
server.off('error', reject);
resolve();
});
});
return {
baseUrl: `http://127.0.0.1:${port}`,
observations: () => [...observations],
armTerminal: (sessionId, fault) => {
armed = { sessionId, fault };
terminalFaulted = false;
terminalAllowed = false;
observations.push(`armed ${fault} for ${sessionId}`);
},
terminalFaulted: () => terminalFaulted,
terminalRejectCount: () => terminalRejectCount,
terminalAcceptCount: () => terminalAcceptCount,
allowTerminal: () => {
terminalAllowed = true;
observations.push('terminal commits allowed');
},
close: async () => {
if (closed) return;
closed = true;
server.closeAllConnections();
await new Promise<void>((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
},
};
})();
}
function commitCarriesTurnSettled(body: Buffer): boolean {
try {
const parsed = JSON.parse(body.toString('utf8')) as {
recordBytesBase64?: unknown;
};
if (typeof parsed.recordBytesBase64 !== 'string') return false;
const records = Buffer.from(parsed.recordBytesBase64, 'base64').toString(
'utf8',
);
return records.split('\n').some((line) => {
if (!line.trim()) return false;
const record = JSON.parse(line) as {
subtype?: unknown;
managedSession?: { kind?: unknown };
};
return (
record.subtype === 'managed_session_event_v1' &&
record.managedSession?.kind === 'turn.settled'
);
});
} catch {
return false;
}
}
async function waitUntil(
name: string,
predicate: (remainingMs: number) => Promise<boolean> | boolean,
timeoutMs: number,
child?: Child | Child[],
): Promise<void> {
const deadline = Date.now() + timeoutMs;
const watched =
child === undefined ? [] : Array.isArray(child) ? child : [child];
let lastError: unknown;
let lastErrorPredatesStall = false;
// Identity, not text: matching the rendered message would let a reworded
// sentinel silently invert the error precedence in the catch below.
const stall = new Error(`${name} predicate stalled`);
while (Date.now() < deadline) {
if (receivedSignal) throw new Error(`Interrupted by ${receivedSignal}`);
for (const each of watched) {
if (childExited(each.child)) {
throw new Error(`${name} exited early: ${each.name}\n${each.log()}`);
}
}
try {
// Bound one iteration against the remaining deadline: a hung
// predicate must not outlive timeoutMs, and its last error must
// surface instead of vanishing into "did not become ready". unref the
// stall timer: a quick success must not keep the event loop (and this
// runner) alive until the discarded timeout fires. The race bounds
// only an asynchronous predicate — a synchronous one blocks the event
// loop, so the remaining budget is handed to the predicate for its
// own probe timeout.
const ready = await Promise.race([
Promise.resolve().then(() =>
predicate(Math.max(1, deadline - Date.now())),
),
new Promise<boolean>((_, reject) => {
// Floor the bound: the final iteration can start with a sliver of
// budget left, and a poll that would answer just past the deadline
// must resolve the wait, not be discarded and misreported as a
// wedged predicate. The clamp re-admits up to ~500ms of overshoot
// on a genuinely wedged poll — the same trade runMysql's probe
// timeout floor makes for the synchronous path.
const stallTimer = setTimeout(
() => reject(stall),
Math.max(500, deadline - Date.now()),
);
stallTimer.unref();
}),
]);
if (ready) return;
// The predicate answered; an error from an earlier phase no longer
// describes the state the deadline found.
lastError = undefined;
} catch (error) {
// Prefer the cause an undici fetch rejection carries (its own message
// is "fetch failed"). A real predicate error wins over the synthetic
// stall; when the stall is what the deadline found, the older error
// is reported as predating it instead of posing as the final state.
if (error instanceof Error && error.cause instanceof Error) {
lastError = error.cause;
} else if (error !== stall) {
lastError = error;
} else if (lastError === undefined) {
lastError = error;
} else {
lastErrorPredatesStall = true;
}
}
await new Promise((resolve) => setTimeout(resolve, 100));
}
// A non-Error rejection (a status string, a library object) is still
// the last thing the predicate said: inspect it so the content survives —
// String() renders a plain object as "[object Object]".
let cause = '';
if (lastError instanceof Error) {
cause = `: ${lastError.message}`;
} else if (lastError !== undefined) {
try {
cause = `: ${inspect(lastError)}`;
} catch {
cause = `: ${String(lastError)}`;
}
}
if (lastErrorPredatesStall) {
cause = `: predicate stalled; last error before the stall${cause}`;
}
throw new Error(
`${name} did not become ready${cause}\n${watched.map((each) => each.log()).join('\n')}`,
);
}
// MySQL reads $HOME/.mylogin.cnf even when option files are disabled,
// so a mysql_config_editor credential on a developer's machine would
// silently auth-connect to the scratch empty-password server — and an
// exported MYSQL_PWD (read as the password) or MYSQL_TEST_LOGIN_FILE
// (relocates .mylogin.cnf ahead of $HOME, past --no-defaults) does the
// same. Client invocations run against an isolated environment instead of
// inheriting the real one: an empty HOME under the runner's one scratch
// root, so the finally reclaims it and QWEN_MANAGED_E2E_KEEP_TMP=1 keeps
// it for inspection, with both credential variables stripped.
const mysqlClientHome = path.join(temporary, 'mysql-client-home');
mkdirSync(mysqlClientHome, { recursive: true });
const mysqlClientEnv: NodeJS.ProcessEnv = {
...process.env,
HOME: mysqlClientHome,
};
delete mysqlClientEnv['MYSQL_PWD'];
delete mysqlClientEnv['MYSQL_TEST_LOGIN_FILE'];
function runMysql(port: number, sql: string, timeoutMs = 10_000): string {
const result = spawnSync(
mysql,
[
// First argument, honored only there: a developer's ~/.my.cnf client
// options must not auth-fail a scratch empty-password server.
'--no-defaults',
'--protocol=tcp',
'--host=127.0.0.1',
`--port=${port}`,
'--user=root',
'--batch',
'--skip-column-names',
'--execute',
sql,
],
{
encoding: 'utf8',
env: mysqlClientEnv,
// waitUntil hands the lease polls its remaining budget, and the final
// iteration can shrink it below one client round trip; floor the probe
// timeout so the last poll is never handed less than it can cost. The
// clamp re-admits up to ~2s of overshoot per wedged poll, against the
// ~10s the budget threading set out to remove.
timeout: Math.max(2_000, timeoutMs),
maxBuffer: 16 * 1024 * 1024,
},
);
// A timed-out spawn sets error and leaves status null with empty stderr:
// surface the spawn-level reason, or a wedge reads as a bare
// "MySQL command failed: ".
if (result.error) {
throw new Error(`MySQL command failed: ${result.error.message}`);
}
if (result.status !== 0) {
throw new Error(`MySQL command failed: ${result.stderr}`);
}
return result.stdout.trim();
}
function assertStoredAnswer(
port: number,
tenant: string,
sessionId: string,
expected: string,
): void {
const rows = runMysql(
port,
`SELECT resource_id, kind, byte_length, sha256, HEX(inline_bytes) FROM qwen_managed_agent.ma_resource WHERE tenant_id=${sqlString(tenant)} AND session_id=${sqlString(sessionId)} AND kind IN ('managed-message', 'managed-message-chunks', 'managed-message-part')`,
).split('\n');
const bodies = new Map<string, { kind: string; bytes: Buffer }>();
for (const row of rows) {
const [id, kind, length, digest, hex] = row.split('\t');
const bytes = Buffer.from(hex ?? '', 'hex');
const actualDigest = createHash('sha256').update(bytes).digest('hex');
let reason: string | undefined;
if (!id) reason = 'missing resource id';
else if (!kind) reason = 'missing resource kind';
else if (!hex || hex === 'NULL') reason = 'missing inline bytes';
else if (bytes.length !== Number(length))
reason = `byte_length=${length} actual=${bytes.length}`;
else if (actualDigest !== digest)
reason = `sha256=${digest} actual=${actualDigest}`;
else if (bytes.length > 65_536)
reason = `inline body ${bytes.length}B exceeds 65536B`;
if (reason) {
throw new Error(
`Stored message resource failed validation: kind=${kind ?? 'unknown'} id=${id ?? 'unknown'} ${reason}`,
);
}
bodies.set(id, { kind, bytes });
}
const records = [...bodies.entries()]
.filter(([, { kind }]) => kind !== 'managed-message-part')
.map(([id, { kind, bytes }]) => {
if (kind === 'managed-message-chunks') {
let manifest: { parts: Array<{ resourceId: string }> };
try {
manifest = JSON.parse(bytes.toString('utf8')) as typeof manifest;
} catch {
throw new Error(
`Stored message manifest is invalid JSON: kind=${kind} id=${id}`,
);
}
bytes = Buffer.concat(
manifest.parts.map((part) => {
const stored = bodies.get(part.resourceId);
if (stored?.kind !== 'managed-message-part')
throw new Error(
`Stored message part is missing: kind=managed-message-part id=${part.resourceId} manifest=${id}`,
);
return stored.bytes;
}),
);
}
try {
return JSON.parse(bytes.toString('utf8')) as {
type: string;
uuid: string;
parentUuid: string;
message?: { parts?: Array<{ text?: string }> };
};
} catch {
throw new Error(
`Stored message ${kind === 'managed-message-chunks' ? 'joined-chunk' : 'inline'} record is invalid JSON: kind=${kind} id=${id}`,
);
}
});
const answers = records.filter(
(record) =>
record.type === 'assistant' &&
record.message?.parts?.map((part) => part.text ?? '').join('') ===
expected,
);
if (answers.length !== 1 || !answers[0].uuid || !answers[0].parentUuid)
throw new Error('Stored answer is incomplete or duplicated');
const manifests = [...bodies.values()].filter(
({ kind }) => kind === 'managed-message-chunks',
);
if (manifests.length !== 1)
throw new Error(
`Expected exactly one managed-message-chunks manifest, found ${manifests.length}`,
);
}
// B3a: assert the crash window from the same durable journal the recovery
// reader reads, not from the hook's log line. The window's left boundary is
// the post-tool main attempt's usage-only Finished marker (state
// output_committed); its right boundary is the assistant message.committed
// that has not landed yet. This mirrors findHostedCommittedModelOutput's own
// discriminator — a latest main attempt that is not output_committed, or an
// assistant commit at/after that attempt, both fall outside the window — so a
// hold moved above the Finished commit reds here instead of passing vacuously
// (the recovery reader also accepts a saved request whose attempt is still
// started, so request count / parity / answer equality cannot tell that
// earlier crash apart from B3a). The journal is UTF-8 JSON Lines in
// ma_session_journal_tx.record_bytes; read it back as HEX like the stored
// answer probe so embedded newlines never split a row.
function finishedBeforeCommitWindow(
port: number,
sessionFilter: string,
promptId: string,
): { attemptId: string; attemptSequence: number } {
const events = readJournalManagedEvents(port, sessionFilter)
.filter((event) => typeof event.sequence === 'number')
.map((event) => ({
kind: event.kind,
sequence: event.sequence as number,
payload: event.payload ?? {},
}));
events.sort((a, b) => a.sequence - b.sequence);
const mainAttempts = events.filter(
(event) =>
event.kind === 'model.attempt' &&
typeof event.payload['attemptId'] === 'string' &&
(event.payload['attemptId'] as string).startsWith(`${promptId}:main:`),
);
const latestAttempt = mainAttempts[mainAttempts.length - 1];
if (!latestAttempt)
throw new Error(
`Finished-before-commit crash window has no main model attempt: promptId=${promptId}`,
);
if (latestAttempt.payload['state'] !== 'output_committed')
throw new Error(
`Finished-before-commit crash landed before the model Finished marker committed: attemptState=${String(latestAttempt.payload['state'])} attemptId=${String(latestAttempt.payload['attemptId'])}`,
);
const assistantCommit = events.find(
(event) =>
event.kind === 'message.committed' &&
event.payload['role'] === 'assistant' &&
event.sequence > latestAttempt.sequence,
);
if (assistantCommit)
throw new Error(
`Finished-before-commit crash landed after the assistant commit: commitSequence=${assistantCommit.sequence} attemptSequence=${latestAttempt.sequence}`,
);
return {
attemptId: latestAttempt.payload['attemptId'] as string,
attemptSequence: latestAttempt.sequence,
};
}
// A3: read the recovered final batch plan the way recovery does — the latest
// hosted.batch.planned link for the batch, then its continuation resource —
// so the sibling order/identity assertion runs on the same durable plan the
// next generation resumed from, not on the execution store's row order.
interface SiblingFinalPlan {
stage: string;
calls: Array<{
runtimeCallId: string;
partIndex: number;
prepareKey: string;
requestDigest: string;
actionId: string | null;
}>;
}
function siblingFinalPlan(
port: number,
sessionFilter: string,
batchId: string,
): SiblingFinalPlan {
let planRef: { resourceId?: string } | undefined;
let planSequence = -1;
for (const event of readJournalManagedEvents(port, sessionFilter)) {
if (
typeof event.sequence !== 'number' ||
event.kind !== 'hosted.batch.planned'
)
continue;
const payload = event.payload ?? {};
if (payload['batchId'] !== batchId) continue;
if (event.sequence > planSequence) {
planSequence = event.sequence;
planRef = payload['planRef'] as { resourceId?: string } | undefined;
}
}
if (!planRef?.resourceId)
throw new Error(
`Sibling recovery has no final batch plan link: batchId=${batchId}`,
);
const plan = JSON.parse(
runMysql(
port,
`SELECT CONVERT(inline_bytes USING utf8mb4) FROM qwen_managed_agent.ma_resource WHERE ${sessionFilter} AND resource_id=${sqlString(planRef.resourceId)}`,
),
);
if (plan.stage !== 'final' || !Array.isArray(plan.calls))
throw new Error(
`Sibling recovered plan is not a final batch plan: stage=${String(plan.stage)}`,
);
return plan;
}
// B3: read the durable round and cost ledger the way recovery restores them.
// The round comes from the latest hosted-model-request (model.attempt.recoveryRef);
// the budget snapshot and per-model token metrics are reverse-scanned from the
// route/usage resources exactly as bindBudget does on a cold load.
interface B3RoundBudget {
round: number;
budget: unknown;
models: unknown;
}
function b3RoundAndBudget(port: number, sessionFilter: string): B3RoundBudget {
const attempts = readJournalManagedEvents(port, sessionFilter)
.filter(
(event) =>
event.kind === 'model.attempt' && typeof event.sequence === 'number',
)
.map((event) => ({
sequence: event.sequence as number,
payload: event.payload ?? {},
}));
attempts.sort((a, b) => a.sequence - b.sequence);
if (attempts.length === 0) throw new Error('B3 window has no model attempt');
const readResource = (ref: unknown): Record<string, unknown> | undefined => {
const resourceId = (ref as { resourceId?: string } | null)?.resourceId;
if (!resourceId) return undefined;
// HEX + decode avoids the mysql --batch backslash escaping that would
// corrupt the JSON's \" sequences — the saved request carries the full
// system prompt, unlike the small approval-continuation plan.
const hex = runMysql(
port,
`SELECT HEX(inline_bytes) FROM qwen_managed_agent.ma_resource WHERE ${sessionFilter} AND resource_id=${sqlString(resourceId)}`,
);
if (!hex || hex === 'NULL') return undefined;
return JSON.parse(Buffer.from(hex, 'hex').toString('utf8')) as Record<
string,
unknown
>;
};
let round = -1;
for (let i = attempts.length - 1; i >= 0; i -= 1) {
const req = readResource(attempts[i].payload['recoveryRef']);
if (req && typeof req['round'] === 'number') {
round = req['round'];
break;
}
}
let budget: unknown;
let models: unknown;
for (let i = attempts.length - 1; i >= 0; i -= 1) {
for (const field of ['usageRef', 'routeRef']) {
const saved = readResource(attempts[i].payload[field]);
if (!saved) continue;
if (budget === undefined && 'budget' in saved) budget = saved['budget'];
if (models === undefined && saved['models']) models = saved['models'];
}
if (budget !== undefined && models !== undefined) break;
}
if (round < 0) throw new Error('B3 window has no saved model-request round');
return { round, budget: budget ?? null, models: models ?? null };
}
// B3: the turn cost snapshot and per-model token ledger must survive the
// replacement. The snapshot's NUMERIC budget must be present: a non-null
// snapshot with budget: null is a legal production state (no +Nk directive
// parsed), and an object-truthy guard would let null == null pass vacuously.
// The ledger may grow as the held round completes, but every crash-boundary
// model's output-token total must survive; an unrestored ledger is a truthy
// {}, so a null check alone would let a zeroed restore pass too.
function assertB3CostContinuity(
before: B3RoundBudget,
after: B3RoundBudget,
): void {
const b3Budget = (before.budget as { budget?: number | null } | null)?.budget;
if (b3Budget === null || b3Budget === undefined)
throw new Error(
`B3 crash-boundary turn budget was not exercised: ${JSON.stringify(before.budget)}`,
);
if (!isDeepStrictEqual(after.budget, before.budget))
throw new Error(
`B3 recovery reset the turn budget snapshot: before=${JSON.stringify(before.budget)} after=${JSON.stringify(after.budget)}`,
);
const b3ModelsBefore = (before.models ?? {}) as Record<
string,
{ tokens?: { candidates?: number } }
>;
const b3ModelsAfter = (after.models ?? {}) as Record<
string,
{ tokens?: { candidates?: number } }
>;
const b3LedgerEntries = Object.entries(b3ModelsBefore);
if (
b3LedgerEntries.length === 0 ||
b3LedgerEntries.every(
([, metric]) => (metric.tokens?.candidates ?? 0) === 0,
)
)
throw new Error(
'B3 crash-boundary per-model token ledger is empty: the fixture did not exercise the ledger',
);
for (const [model, metric] of b3LedgerEntries) {
const beforeTokens = metric.tokens?.candidates ?? 0;
const afterTokens = b3ModelsAfter[model]?.tokens?.candidates ?? 0;
if (afterTokens < beforeTokens)
throw new Error(
`B3 recovery wiped the per-model token ledger: model=${model} before=${beforeTokens} after=${afterTokens}`,
);
}
}
// The text-delta selector shared by deltaSequences (which throws on an empty
// match) and b2DeltaIdentity (which tolerates it): one filter, two policies.
function textDeltas(events: PublicEvent[], text: string): PublicEvent[] {
return events.filter(
(event) =>
event.type === 'item.output_text.delta' &&
eventText(event).includes(text),
);
}
// B2: one identity group per prefix text — the item/part ids the public deltas
// carried while that prefix was live. After retraction the reload view blanks
// those deltas, so the dead generations' identities survive only in the live
// SSE capture; that is what makes the live view load-bearing.
function b2DeltaIdentity(
events: PublicEvent[],
text: string,
): { itemIds: string[]; partIds: string[]; sequences: number[] } {
const deltas = textDeltas(events, text);
const distinct = (values: Array<string | null | undefined>): string[] => [
...new Set(values.filter((id): id is string => typeof id === 'string')),
];
return {
itemIds: distinct(deltas.map((event) => event.item_id)),
partIds: distinct(deltas.map((event) => event.content_part_id)),
sequences: deltas.map((event) => event.sequence),
};
}
// The journal HEX decode shared by every window/identity reader: --batch text
// would escape backslashes and corrupt the JSON, so rows are read as HEX and
// decoded here, once.
function readJournalManagedEvents(
port: number,
sessionFilter: string,
): Array<{
kind?: unknown;
sequence?: unknown;
payload?: Record<string, unknown>;
}> {
const rows = runMysql(
port,
`SELECT HEX(record_bytes) FROM qwen_managed_agent.ma_session_journal_tx WHERE ${sessionFilter} ORDER BY first_sequence`,
).split('\n');
const events: Array<{
kind?: unknown;
sequence?: unknown;
payload?: Record<string, unknown>;
}> = [];
for (const row of rows) {
if (!row || row === 'NULL') continue;
for (const line of Buffer.from(row, 'hex').toString('utf8').split('\n')) {
if (!line) continue;
const record = JSON.parse(line) as {
subtype?: unknown;
managedSession?: {
kind?: unknown;
sequence?: unknown;
payload?: Record<string, unknown>;
};
};
if (record.subtype !== 'managed_session_event_v1') continue;
if (record.managedSession) events.push(record.managedSession);
}
}
return events;
}
// B2: the original prompt's model attempts, oldest first. Each streaming
// generation must open a new attemptId.
function b2ModelAttempts(
port: number,
sessionFilter: string,
promptId: string,
): Array<{ attemptId: string; state: string; sequence: number }> {
const attempts: Array<{
attemptId: string;
state: string;
sequence: number;
}> = [];
for (const event of readJournalManagedEvents(port, sessionFilter)) {
if (event.kind !== 'model.attempt' || typeof event.sequence !== 'number')
continue;
const attemptId = event.payload?.['attemptId'];
if (
typeof attemptId !== 'string' ||
!attemptId.startsWith(`${promptId}:main:`)
)
continue;
attempts.push({
attemptId,
state: String(event.payload?.['state'] ?? ''),
sequence: event.sequence,
});
}
return attempts.sort((a, b) => a.sequence - b.sequence);
}
interface B2SseCapture {
events: PublicEvent[];
lastId: string;
}
// Open SSE observers must not hold the process open when the arm fails early:
// the main finally aborts whatever is still pending.
const b2SseObservers = new Set<SseObserver>();
// B2: read the public SSE view through the shared SseObserver (wire-format
// assertions included). One connection stays open across the Harness crashes
// (the stream is control-plane-owned and reconciles from after=0 on connect).
// `until` is a PROGRESS signal, not the exit: after it first holds, a bounded
// drain window keeps reading so a double-projected terminal is still observed
// (R1-2). SseObserver and waitUntil are free identifiers so behavioural tests
// inject stubs.
async function readPublicSse(
storeUrl: string,
sessionId: string,
options: {
tenantId: string;
after?: number;
lastEventId?: string;
until: (events: PublicEvent[]) => boolean;
timeoutMs: number;
drainMs?: number;
},
): Promise<B2SseCapture> {
const observer = new SseObserver({
surface: 'public',
sessionId,
storeUrl,
tenantId: options.tenantId,
timeoutMs: options.timeoutMs,
});
b2SseObservers.add(observer);
const snapshot = (): PublicEvent[] =>
observer.frames.map((frame) => frame.data as unknown as PublicEvent);
const fail = (): void => {
if (observer.failure)
throw new Error(`B2 live SSE failed: ${String(observer.failure)}`);
};
try {
await observer.start(
options.lastEventId !== undefined
? Number(options.lastEventId)
: options.after,
);
await waitUntil(
'B2 SSE capture progress',
() => {
fail();
return options.until(snapshot());
},
options.timeoutMs,
);
const drainMs = options.drainMs ?? 2_000;
const drainEnd = Date.now() + drainMs;
await waitUntil(
'B2 SSE capture drain',
() => {
fail();
return Date.now() >= drainEnd;
},
drainMs + 1_000,
);
fail();
const events = snapshot();
return { events, lastId: String(observer.frames.at(-1)?.id ?? '') };
} catch (error) {
if (observer.failure)
throw new Error(`B2 live SSE failed: ${String(observer.failure)}`);
if (observer.controller.signal.aborted)
throw new Error(
`B2 live SSE aborted (budget or teardown): ${String(error)}`,
);
throw new Error(`B2 live SSE capture incomplete: ${String(error)}`);
} finally {
observer.controller.abort();
await observer.task?.catch(() => undefined);
b2SseObservers.delete(observer);
}
}
// B2: the stream-view audit, lifted out of the arm's try block so behavioural
// tests execute it (R1-6). The live capture holds the dead generations'
// original identities; reload/replay hold the blanked prefixes and the
// surviving answer. item_id is the Turn's ONE assistant item (stable across
// the repair); the content Part is the stream identity — a delta continues the
// previous Part only while that delta keeps its identity, so every retraction
// must force a new Part (pairwise distinct Parts; every group must actually
// carry the assistant item, R1-3; the replay compares Parts too, R1-4).
function assertB2StreamViews(input: {
live: PublicEvent[];
reload: PublicEvent[];
replay: PublicEvent[];
partials: string[];
answer: string;
firstPrefixSequence: number;
secondPrefixSequence: number;
originalTurnId?: string;
}): {
livePartial1: ReturnType<typeof b2DeltaIdentity>;
livePartial2: ReturnType<typeof b2DeltaIdentity>;
liveAnswer: ReturnType<typeof b2DeltaIdentity>;
reloadAnswer: ReturnType<typeof b2DeltaIdentity>;
replayAnswer: ReturnType<typeof b2DeltaIdentity>;
} {
// The public stream.reconciled frame carries no payload: a client seeing one
// reloads. The converged view is the deltas after the LAST retraction.
const converged = (events: PublicEvent[]): string => {
const reconciled = events.filter(
(event) => event.type === 'stream.reconciled',
);
const last = reconciled.length
? Math.max(...reconciled.map((event) => event.sequence))
: 0;
return events
.filter(
(event) =>
event.type === 'item.output_text.delta' && event.sequence > last,
)
.map(eventText)
.join('');
};
const terminalsOf = (events: PublicEvent[]): PublicEvent[] =>
events.filter(
(event) =>
event.terminal &&
(input.originalTurnId === undefined ||
event.turn_id === input.originalTurnId),
);
const livePartial1 = b2DeltaIdentity(input.live, input.partials[0]);
const livePartial2 = b2DeltaIdentity(input.live, input.partials[1]);
const liveAnswer = b2DeltaIdentity(input.live, input.answer);
const liveTerminals = terminalsOf(input.live);
const liveRetractions = input.live.filter(
(event) => event.type === 'stream.reconciled',
);
if (
converged(input.live) !== input.answer ||
liveTerminals.length !== 1 ||
liveTerminals[0].type !== 'turn.completed' ||
livePartial1.sequences.length === 0 ||
livePartial2.sequences.length === 0 ||
liveAnswer.sequences.length === 0 ||
liveRetractions.length < 2 ||
!reconciledBetween(
input.live,
Math.min(...livePartial1.sequences),
Math.min(...livePartial2.sequences),
) ||
!reconciledBetween(
input.live,
Math.min(...livePartial2.sequences),
Math.min(...liveAnswer.sequences),
)
)
throw new Error(
`B2 live SSE: text=${JSON.stringify(converged(input.live))} terminals=${liveTerminals.length} partial1=${livePartial1.sequences.length} partial2=${livePartial2.sequences.length} answer=${liveAnswer.sequences.length} retractions=${liveRetractions.length} frames=${input.live.length}`,
);
const reloadAnswer = b2DeltaIdentity(input.reload, input.answer);
// F5: the live answer's identity is load-bearing — the persisted views being
// correct does not excuse a live stream that delivers the surviving answer
// under a dead Part, no identity, or a foreign item.
if (liveAnswer.itemIds.length === 0 || liveAnswer.partIds.length === 0)
throw new Error(
`B2 live SSE: the live answer carries no identity: ${JSON.stringify(liveAnswer)}`,
);
if (
liveAnswer.itemIds.join(',') !== reloadAnswer.itemIds.join(',') ||
liveAnswer.partIds.join(',') !== reloadAnswer.partIds.join(',')
)
throw new Error(
`B2 live SSE: the live answer identity differs from reload: live=${JSON.stringify(liveAnswer)} reload=${JSON.stringify(reloadAnswer)}`,
);
const groups = [livePartial1, livePartial2, reloadAnswer];
const itemIds = new Set(groups.flatMap((group) => group.itemIds));
if (
groups.some(
(group) => group.partIds.length === 0 || group.itemIds.length === 0,
) ||
itemIds.size !== 1
)
throw new Error(
`B2 stream identities: expected one shared assistant item and a Part per generation: ${JSON.stringify(groups)}`,
);
for (let i = 0; i < groups.length; i += 1) {
for (let j = i + 1; j < groups.length; j += 1) {
if (groups[i].partIds.some((id) => groups[j].partIds.includes(id)))
throw new Error(
`B2 stream identities: generations ${i + 1} and ${j + 1} share a content Part (retraction did not break the continuation): ${JSON.stringify(groups)}`,
);
}
}
const blanked = (events: PublicEvent[], sequence: number): boolean => {
const delta = events.find((event) => event.sequence === sequence);
return (
!!delta &&
delta.type === 'item.output_text.delta' &&
eventText(delta) === '' &&
delta.item_id == null &&
delta.content_part_id == null
);
};
if (
!blanked(input.reload, input.firstPrefixSequence) ||
!blanked(input.reload, input.secondPrefixSequence)
)
throw new Error(
`B2 stream identities: retracted prefixes not blanked in reload at ${input.firstPrefixSequence}/${input.secondPrefixSequence}`,
);
const replayAnswer = b2DeltaIdentity(input.replay, input.answer);
const replayTerminals = terminalsOf(input.replay);
if (
converged(input.replay) !== input.answer ||
replayTerminals.length !== 1 ||
// F6: a reconnecting client shown a failure terminal while live/reload
// show success is not a passing B2.
replayTerminals[0].type !== 'turn.completed' ||
!blanked(input.replay, input.firstPrefixSequence) ||
!blanked(input.replay, input.secondPrefixSequence) ||
replayAnswer.itemIds.join(',') !== reloadAnswer.itemIds.join(',') ||
replayAnswer.partIds.join(',') !== reloadAnswer.partIds.join(',')
)
throw new Error(
`B2 replay view: text=${JSON.stringify(converged(input.replay))} terminals=${replayTerminals.length} answerIds=${replayAnswer.itemIds.join(',')} answerParts=${replayAnswer.partIds.join(',')} reloadIds=${reloadAnswer.itemIds.join(',')} reloadParts=${reloadAnswer.partIds.join(',')} frames=${input.replay.length}`,
);
return { livePartial1, livePartial2, liveAnswer, reloadAnswer, replayAnswer };
}
function assertPublicAnswer(events: PublicEvent[], expected: string): void {
const terminal = events.filter((event) => event.terminal);
const text = events
.filter((event) => event.type === 'item.output_text.delta')
.map(eventText)
.join('');
if (
terminal.length !== 1 ||
terminal[0].type !== 'turn.completed' ||
text !== expected
) {
throw new Error(
`Long-answer integrity failed: terminal=${terminal.map((event) => event.type)} expectedChars=${expected.length} actualChars=${text.length}`,
);
}
}
function sqlString(value: string): string {
return `'${value.replaceAll("'", "''")}'`;
}
async function fetchJson<T>(url: string, init?: RequestInit): Promise<T> {
const response = await fetch(url, init);
if (!response.ok) {
throw new Error(`${response.status} ${await response.text()}`);
}
return (await response.json()) as T;
}
interface PublicSession {
id: string;
last_event_id: number;
status: string;
}
interface PublicEvent {
sequence: number;
terminal: boolean;
type: string;
turn_id?: string | null;
item_id?: string | null;
content_part_id?: string | null;
data?: unknown;
}
function eventText(event: PublicEvent): string {
const data = event.data;
if (
typeof data === 'object' &&
data !== null &&
'text' in data &&
typeof data.text === 'string'
) {
return data.text;
}
return '';
}
interface PublicList<T> {
data: T[];
}
// What a reader of the public feed sees right now: retracted prefixes are
// gone from this view, so it is the reload witness the recovery arms compare
// against (the live SSE view is not read).
async function publicTranscript(
springUrl: string,
tenant: string,
sessionId: string,
): Promise<{ events: PublicEvent[]; text: string; terminals: PublicEvent[] }> {
const page = await fetchJson<PublicList<PublicEvent>>(
`${springUrl}/v1/agents/sessions/${sessionId}/events?after=0&limit=100`,
{ headers: tenantHeaders(tenant) },
);
return {
events: page.data,
text: page.data
.filter((event) => event.type === 'item.output_text.delta')
.map(eventText)
.join(''),
terminals: page.data.filter((event) => event.terminal),
};
}
function deltaSequences(events: PublicEvent[], text: string): number[] {
const sequences = textDeltas(events, text).map((event) => event.sequence);
// An empty list would turn Math.min into Infinity and every "before"
// check into a vacuous pass.
if (sequences.length === 0) {
throw new Error(`No public text delta carries ${JSON.stringify(text)}`);
}
return sequences;
}
// A retraction blanks the dead owner's deltas in place and then appends
// stream.reconciled, so "retracted before the new text" is an ordering of
// sequences, not something a poll can catch in time.
function reconciledBetween(
events: PublicEvent[],
after: number,
before: number,
): boolean {
return events.some(
(event) =>
event.type === 'stream.reconciled' &&
event.sequence > after &&
event.sequence < before,
);
}
function tenantHeaders(tenant: string): Record<string, string> {
return {
'x-qwen-tenant-id': tenant,
[trustedActorHeader]: trustedActor,
};
}
async function waitForTerminal(
springUrl: string,
tenant: string,
sessionId: string,
after: number,
child: Child | Child[],
timeoutMs = 120_000,
): Promise<{ events: PublicEvent[]; lastSequence: number }> {
const events: PublicEvent[] = [];
let cursor = after;
await waitUntil(
'Managed Turn terminal event',
async () => {
const page = await fetchJson<PublicList<PublicEvent>>(
`${springUrl}/v1/agents/sessions/${sessionId}/events?after=${cursor}&limit=100`,
{ headers: tenantHeaders(tenant) },
);
for (const event of page.data) {
events.push(event);
cursor = Math.max(cursor, event.sequence);
}
return page.data.some((event) => event.terminal);
},
timeoutMs,
child,
);
return { events, lastSequence: cursor };
}
const failoverFirstMarker = 'MANAGED_SESSION_FAILOVER_FIRST_TURN';
const failoverSecondMarker = 'MANAGED_SESSION_FAILOVER_SECOND_TURN';
const bigOutputSource = 'abcdefghijklmnopqrstu长😀'.repeat(
bigOutput ? 8_000 : 0,
);
const failoverFirstResponse = bigOutput
? Array.from({ length: 48 }, (_, index) => {
// Distinct prefixes keep the provider's cumulative-stream detection out
// of this storage regression without changing its character/byte sizes.
return (
String(index).padStart(4, '0') +
bigOutputSource.slice(index * 4_000 + 4, (index + 1) * 4_000)
);
}).join('')
: 'FIRST_TURN_DURABLY_COMMITTED';
const failoverSecondResponse = bigOutput
? 'CONTROL_ANSWER__'.repeat(500)
: 'SECOND_TURN_RESTORED_CONTEXT';
const failoverMissingResponse = 'SECOND_TURN_CONTEXT_MISSING';
const inflightMarker = 'MANAGED_SESSION_INFLIGHT_FAILOVER';
const inflightResponse = 'INFLIGHT_TURN_RECOVERED';
const continuationMarker = 'MANAGED_SESSION_CONTINUATION_FAILOVER';
const continuationPartial = 'CONTINUATION_PARTIAL';
const continuationResponse = 'CONTINUATION_TURN_RECOVERED';
let acceptReplacementContinuation = false;
let releaseContinuationHold = () => {};
const continuationHold = new Promise<void>((resolve) => {
releaseContinuationHold = resolve;
});
// after-result arms a fresh hold after the decision: the continuation hold
// above is already released by the time the approved call runs.
let approvalResultHold: Promise<void> | undefined;
let releaseApprovalResultHold = () => {};
const modelRoundMarker = 'MANAGED_SESSION_MODEL_ROUND_FAILOVER';
const modelRoundResponse = 'MODEL_ROUND_TURN_RECOVERED';
const modelRoundNextMarker = 'MANAGED_SESSION_MODEL_ROUND_NEXT_TURN';
const modelRoundNextResponse = 'MODEL_ROUND_NEXT_TURN_COMPLETED';
// One published, held prefix per owner generation that dies inside the first
// model round: two Harness-only replacements, or one frozen control plane.
const modelRoundPartials = frozenControlPlane
? ['MODEL_ROUND_PARTIAL_ONE']
: ['MODEL_ROUND_PARTIAL_ONE', 'MODEL_ROUND_PARTIAL_TWO'];
const modelRoundHolds = modelRoundPartials.map(() => {
let release = () => {};
const promise = new Promise<void>((resolve) => {
release = resolve;
});
return { promise, release };
});
let modelRoundGeneration = 0;
// The runner advances the round only after the owner holding it is dead or
// frozen, so a reissue always lands on the next generation's response.
function advanceModelRound(): void {
modelRoundHolds[modelRoundGeneration]?.release();
modelRoundGeneration += 1;
}
let fake: Awaited<ReturnType<typeof startFakeOpenAIServer>> | undefined;
let heldStartProxy: HeldExecutionStartProxy | undefined;
let replacementBrokerProxy: HeldExecutionStartProxy | undefined;
let storeFaultProxy: StoreFaultProxy | undefined;
let failure: unknown;
let dumpPort: number | undefined;
// The success JSON, assembled inside the try and printed only after
// the post-finally throws, so an interrupt can never put a clean pass
// on stdout for a run the operator explicitly stopped.
let resultJson: string | undefined;
let approvalResultJson: string | undefined;
// Snapshot before the relief Turn so the summary cannot report the relief's
// store/broker traffic as part of the original fault arm.
let originalFaultObservations: Record<string, number> | undefined;
let g3SummaryJson: string | undefined;
// The freeze arm's wake record reads as a pass line too, so it defers the
// same way and prints after the main result, as in the base's ordering.
let fencedWriterJson: string | undefined;
try {
const harnessToken = randomBytes(24).toString('base64url');
const brokerToken = randomBytes(24).toString('base64url');
const credentialKey = randomBytes(32).toString('base64');
const capabilityDigest = `sha256:${randomBytes(32).toString('hex')}`;
const tenant = continuationFailover
? 'managed-continuation-failover-e2e'
: inflightFailover
? 'managed-inflight-failover-e2e'
: bigOutput
? 'managed-big-output-e2e'
: sessionFailover
? 'managed-session-failover-e2e'
: frozenControlPlane
? 'managed-frozen-control-plane-e2e'
: modelRoundFailover
? 'managed-model-round-failover-e2e'
: 'real-model-e2e';
const springArguments = ['-jar', springJar];
springArguments.push(
`--qwen.managed-agent.runtime-broker.workspace-mounts[0].tenant-id=${tenant}`,
`--qwen.managed-agent.runtime-broker.workspace-mounts[0].storage-id=${boundStorageId}`,
`--qwen.managed-agent.runtime-broker.workspace-mounts[0].root=${workspaceMount}`,
);
if (durableFailover) {
fake = await startFakeOpenAIServer(({ body }) => {
const messages = Array.isArray(body['messages']) ? body['messages'] : [];
const serialized = JSON.stringify(messages);
// Match the new current input before any marker from restored history.
if (serialized.includes(reliefMarker)) return { content: reliefResponse };
if (modelRoundFailover && serialized.includes(modelRoundNextMarker)) {
return { content: modelRoundNextResponse };
}
if (modelRoundFailover && serialized.includes(modelRoundMarker)) {
const generation = modelRoundGeneration;
const partial = modelRoundPartials[generation];
return partial === undefined
? { content: modelRoundResponse }
: {
contentChunks: [partial],
holdAfterChunks: 1,
holdUntil: modelRoundHolds[generation].promise,
};
}
if (continuationFailover && serialized.includes(continuationMarker)) {
// B3: two earlier tool rounds, then a held post-tool round. The fake
// model decides by the number of tool results already in the history:
// 0 -> tool A (round 0), 1 -> tool B (round 1), >=2 -> the held final
// answer (round 2). The crash lands on the round-2 request so recovery
// must resume the saved round and cost ledger, not restart them.
if (approvalFault === 'after-earlier-tools') {
const toolResultCount = (serialized.match(/"role":"tool"/g) ?? [])
.length;
// Non-zero usage makes the token ledger numerically observable (the
// fake's default usage is all zeros). The earlier rounds outweigh the
// final round so a zeroed restore totals less than the crash-boundary
// ledger, not just the same sum re-counted.
if (toolResultCount === 0) {
return {
toolCalls: [
fakeToolCall(
'write_file',
{
file_path: inflightSideEffectName,
content: inflightSideEffectContent,
},
'call_managed_b3_first',
),
],
usage: {
prompt_tokens: 0,
completion_tokens: 300,
total_tokens: 300,
},
};
}
if (toolResultCount === 1) {
return {
toolCalls: [
fakeToolCall(
'write_file',
{
file_path: b3SecondSideEffectName,
content: b3SecondSideEffectContent,
},
'call_managed_b3_second',
),
],
usage: {
prompt_tokens: 0,
completion_tokens: 300,
total_tokens: 300,
},
};
}
if (approvalResultHold) {
return {
contentChunks: [continuationPartial],
holdAfterChunks: 1,
holdUntil: approvalResultHold,
usage: {
prompt_tokens: 0,
completion_tokens: 100,
total_tokens: 100,
},
};
}
return {
content: continuationResponse,
usage: {
prompt_tokens: 0,
completion_tokens: 100,
total_tokens: 100,
},
};
}
if (!serialized.includes('"role":"tool"')) {
// A3: a two-call batch so the replacement can be crashed in the
// approval gap between a decided sibling and a pending one.
if (approvalFault === 'sibling-approval') {
return {
toolCalls: [
fakeToolCall(
'write_file',
{
file_path: inflightSideEffectName,
content: inflightSideEffectContent,
},
'call_managed_continuation_failover',
),
fakeToolCall(
'write_file',
{
file_path: siblingSideEffectName,
content: siblingSideEffectContent,
},
'call_managed_continuation_sibling',
),
],
};
}
return {
toolCalls: [
fakeToolCall(
'write_file',
{
file_path: inflightSideEffectName,
content: inflightSideEffectContent,
},
'call_managed_continuation_failover',
),
],
};
}
if (approvalRecovery) {
if (approvalFault === 'after-result' && approvalResultHold) {
// Publish a prefix and park the round so the crash lands after
// the execution result commits but before the assistant output
// commits; the reissue must reuse this exact saved request.
return {
contentChunks: [continuationPartial],
holdAfterChunks: 1,
holdUntil: approvalResultHold,
};
}
return { content: continuationResponse };
}
if (!acceptReplacementContinuation) {
return {
contentChunks: [continuationPartial],
holdAfterChunks: 1,
holdUntil: continuationHold,
};
}
return { content: continuationResponse };
}
if (inflightFailover && serialized.includes(inflightMarker)) {
if (!serialized.includes('"role":"tool"')) {
return {
toolCalls: [
fakeToolCall(
'write_file',
{
file_path: inflightSideEffectName,
content: inflightSideEffectContent,
},
'call_managed_inflight_failover',
),
],
};
}
return { content: inflightResponse };
}
if (serialized.includes(failoverSecondMarker)) {
const restored =
serialized.includes(failoverFirstMarker) &&
serialized.includes(failoverFirstResponse);
return {
content: restored ? failoverSecondResponse : failoverMissingResponse,
};
}
if (serialized.includes(failoverFirstMarker)) {
return bigOutput
? {
contentChunks: Array.from({ length: 48 }, (_, index) =>
failoverFirstResponse.slice(index * 4_000, (index + 1) * 4_000),
),
}
: { content: failoverFirstResponse };
}
return { content: 'UNEXPECTED_FAILOVER_PROMPT' };
});
}
const fakeBaseUrl = fake?.baseUrl;
if (durableFailover && fakeBaseUrl === undefined) {
throw new Error('Fake model server did not start');
}
const mysqlPort = await freePort();
dumpPort = mysqlPort;
const springPort = await freePort();
const harnessPort = await freePort();
const brokerPort = await freePort();
// C1/C2 replace the Harness's Session Store reply/write after the
// payable checkpoint, so the store URL the control plane advertises
// must point at this proxy from the first load onward. Other arms keep
// the direct URL and an unchanged path.
if (
approvalFault === 'terminal-reply-lost' ||
approvalFault === 'terminal-write-failed'
) {
storeFaultProxy = await startStoreFaultProxy(
`http://127.0.0.1:${springPort}`,
);
}
const sessionStoreUrl =
storeFaultProxy?.baseUrl ?? `http://127.0.0.1:${springPort}`;
const initialized = spawnSync(
mysqld,
[
'--no-defaults',
...mysqldUserArguments,
'--initialize-insecure',
`--datadir=${mysqlData}`,
],
{ encoding: 'utf8' },
);
if (initialized.status !== 0) {
throw new Error(`MySQL initialization failed: ${initialized.stderr}`);
}
const mysqlServer = start(
mysqld,
[
'--no-defaults',
...mysqldUserArguments,
`--datadir=${mysqlData}`,
`--socket=${mysqlSocket}`,
`--port=${mysqlPort}`,
'--bind-address=127.0.0.1',
'--mysqlx=0',
`--pid-file=${path.join(temporary, 'mysql.pid')}`,
`--log-error=${mysqlError}`,
],
{},
'MySQL',
);
await waitUntil(
'MySQL',
// spawnSync blocks the event loop, so the race deadline cannot fire
// inside one iteration: the probe carries its own timeout.
() =>
spawnSync(
mysqladmin,
[
'--no-defaults',
'--protocol=tcp',
'--host=127.0.0.1',
`--port=${mysqlPort}`,
'--user=root',
'ping',
],
{
stdio: 'ignore',
env: mysqlClientEnv,
timeout: 10_000,
},
).status === 0,
60_000,
mysqlServer,
);
runMysql(
mysqlPort,
'CREATE DATABASE qwen_managed_agent CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci',
);
const spring = start(
java,
springArguments,
{
env: {
...cleanEnvironment,
HOME: runtimeHome,
LANG: process.env['LANG'] ?? 'C',
LC_ALL: process.env['LC_ALL'] ?? 'C',
NO_PROXY: '127.0.0.1,localhost',
QWEN_HOME: path.join(runtimeHome, '.qwen'),
TMPDIR: temporary,
no_proxy: '127.0.0.1,localhost',
SERVER_PORT: String(springPort),
SPRING_DATASOURCE_PASSWORD: '',
SPRING_DATASOURCE_URL: `jdbc:mysql://127.0.0.1:${mysqlPort}/qwen_managed_agent?useSSL=false&allowPublicKeyRetrieval=true`,
SPRING_DATASOURCE_USERNAME: 'root',
QWEN_MANAGED_AGENT_APPROVAL_MODE: approvalRecovery ? 'default' : 'yolo',
QWEN_MANAGED_AGENT_APPROVAL_TIMEOUT:
verificationMode === 'expiry' ? '30s' : '5m',
QWEN_MANAGED_AGENT_CAPABILITY_DIGEST: capabilityDigest,
QWEN_MANAGED_AGENT_HARNESS_BASE_URL: `http://127.0.0.1:${harnessPort}`,
QWEN_MANAGED_AGENT_HARNESS_ENABLED: 'true',
QWEN_MANAGED_AGENT_HARNESS_REQUEST_TIMEOUT: '120s',
QWEN_MANAGED_AGENT_HARNESS_TOKEN: harnessToken,
// Trusted reboot recovery stays pinned off in every runner mode;
// durable local process follows runtimeTakeover. Both pins keep each
// mode's previously verified behavior and keep the runner starting
// off Linux.
QWEN_MANAGED_AGENT_RUNTIME_TRUSTED_LOCAL_REBOOT_RECOVERY: 'false',
...(registerStorage
? {
QWEN_MANAGED_AGENT_RUNTIME_VERIFIED_WORKSPACE_RECOVERY_ENABLED:
'true',
}
: {}),
QWEN_MANAGED_AGENT_TRUSTED_ACTOR_HEADER: trustedActorHeader,
QWEN_MANAGED_AGENT_WORKSPACE_FILES_ENABLED: 'true',
...(runtimeTakeover
? {
QWEN_MANAGED_AGENT_RUNTIME_DURABLE_LOCAL_PROCESS:
process.platform === 'linux' ? 'true' : 'false',
}
: {
QWEN_MANAGED_AGENT_RUNTIME_DURABLE_LOCAL_PROCESS: 'false',
}),
...(durableFailover
? {
QWEN_MANAGED_AGENT_DISPATCH_LEASE_DURATION: '2s',
QWEN_MANAGED_AGENT_DISPATCH_LEASE_RENEW_INTERVAL: '500ms',
QWEN_MANAGED_AGENT_DISPATCH_SCAN_DELAY: '200ms',
QWEN_MANAGED_AGENT_SESSION_STORE_BASE_URL: sessionStoreUrl,
QWEN_MANAGED_AGENT_SESSION_STORE_ENABLED: 'true',
QWEN_MANAGED_AGENT_SESSION_STORE_WRITER_LEASE_DURATION: '5s',
QWEN_MANAGED_AGENT_WORKSPACE_ID: workspaceId,
QWEN_MANAGED_AGENT_RUNTIME_BROKER_ENABLED: 'true',
QWEN_MANAGED_AGENT_RUNTIME_BROKER_PORT: String(brokerPort),
QWEN_MANAGED_AGENT_RUNTIME_BROKER_TOKEN: brokerToken,
QWEN_MANAGED_AGENT_RUNTIME_CREDENTIAL_KEY: credentialKey,
QWEN_MANAGED_AGENT_RUNTIME_CREDENTIAL_KEY_ID: 'e2e-local-v1',
QWEN_MANAGED_AGENT_RUNTIME_STATE_DIRECTORY: runtimeState,
QWEN_MANAGED_AGENT_RUNTIME_WORKER_ENTRY: cliBundle,
QWEN_MANAGED_AGENT_NODE_EXECUTABLE: process.execPath,
QWEN_MANAGED_AGENT_WORKSPACE_CWD: workspace,
}
: {
QWEN_MANAGED_AGENT_RUNTIME_BROKER_ENABLED: 'true',
QWEN_MANAGED_AGENT_RUNTIME_BROKER_PORT: String(brokerPort),
QWEN_MANAGED_AGENT_RUNTIME_BROKER_TOKEN: brokerToken,
QWEN_MANAGED_AGENT_RUNTIME_CREDENTIAL_KEY: credentialKey,
QWEN_MANAGED_AGENT_RUNTIME_CREDENTIAL_KEY_ID: 'e2e-local-v1',
QWEN_MANAGED_AGENT_RUNTIME_STATE_DIRECTORY: runtimeState,
QWEN_MANAGED_AGENT_RUNTIME_WORKER_ENTRY: cliBundle,
QWEN_MANAGED_AGENT_SESSION_STORE_BASE_URL: sessionStoreUrl,
QWEN_MANAGED_AGENT_SESSION_STORE_ENABLED: 'true',
QWEN_MANAGED_AGENT_SESSION_STORE_WRITER_LEASE_DURATION: '60s',
QWEN_MANAGED_AGENT_WORKSPACE_ID: workspaceId,
QWEN_MANAGED_AGENT_NODE_EXECUTABLE:
runtimeDelayMs === 0 ? process.execPath : delayedNode,
QWEN_MANAGED_AGENT_WORKSPACE_CWD: workspace,
}),
},
},
'Spring Managed Agent Server',
);
const springUrl = `http://127.0.0.1:${springPort}`;
await waitUntil(
'Spring Managed Agent Server',
async () => {
const response = await fetch(`${springUrl}/actuator/health`);
return response.ok;
},
60_000,
spring,
);
runMysql(
mysqlPort,
`INSERT INTO qwen_managed_agent.ma_workspace (tenant_id, workspace_id, workspace_generation, storage_id, display_name, config_ref, policy_ref, state) VALUES (${sqlString(tenant)}, ${sqlString(boundWorkspaceId)}, 1, ${sqlString(boundStorageId)}, 'E2E', 'managed-runtime-tools/1', 'preapproved-workspace-tools/1', 'ACTIVE')`,
);
runMysql(
mysqlPort,
`INSERT INTO qwen_managed_agent.ma_workspace_access (tenant_id, workspace_id, actor_id, role) VALUES (${sqlString(tenant)}, ${sqlString(boundWorkspaceId)}, ${sqlString(trustedActor)}, 'OPERATOR')`,
);
if (registerStorage) {
// The new root's mtime must differ from its birth time; no Harness or tool can write it yet.
const touch = path.join(workspaceMount, '.storage-registration-touch');
mkdirSync(touch);
rmSync(touch, { recursive: true });
const registration = spawnSync(
java,
[
'-cp',
springJar,
'-Dloader.main=com.alibaba.qwen.code.managedagent.maintenance.WorkspaceStorageRegistrationMain',
'org.springframework.boot.loader.launch.PropertiesLauncher',
'register',
tenant,
boundStorageId,
workspaceMount,
randomUUID(),
'--offline-confirmed',
],
{
encoding: 'utf8',
timeout: 60_000,
env: {
...cleanEnvironment,
W1_JDBC_URL: `jdbc:mysql://127.0.0.1:${mysqlPort}/qwen_managed_agent?useSSL=false&allowPublicKeyRetrieval=true`,
W1_JDBC_USER: 'root',
W1_JDBC_PASSWORD: '',
},
},
);
if (registration.error || registration.status !== 0) {
throw new Error(
`Native storage registration failed: ${registration.stderr || registration.error?.message || registration.status}`,
);
}
}
if (inflightFailover) {
heldStartProxy = await startHeldExecutionStartProxy(
`http://127.0.0.1:${brokerPort}`,
);
}
const harness = start(
process.execPath,
[
cliBundle,
'serve',
'--profile',
'hosted-harness',
'--port',
String(harnessPort),
'--hostname',
'127.0.0.1',
'--require-auth',
'--no-web',
'--workspace',
workspace,
'--managed-runtime-broker-url',
heldStartProxy?.baseUrl ?? `http://127.0.0.1:${brokerPort}`,
// Joined form: a base64url token can start with '-', which argv
// would otherwise parse as another flag.
`--managed-runtime-broker-token=${brokerToken}`,
],
{
env: {
...cleanEnvironment,
HOME: harnessHome,
LANG: process.env['LANG'] ?? 'C',
LC_ALL: process.env['LC_ALL'] ?? 'C',
QWEN_HOME: path.join(harnessHome, '.qwen'),
QWEN_CODE_TRUSTED_FOLDERS_PATH: trustedFolders,
QWEN_HOSTED_HARNESS_CAPABILITY_DIGEST: capabilityDigest,
QWEN_SERVER_TOKEN: harnessToken,
...(durableFailover
? {
OPENAI_API_KEY: 'fake-key',
OPENAI_BASE_URL: fakeBaseUrl,
OPENAI_MODEL: 'fake-model',
QWEN_MODEL: 'fake-model',
}
: {}),
QWEN_RUNTIME_BROKER_TOKEN: brokerToken,
QWEN_RUNTIME_BROKER_URL:
heldStartProxy?.baseUrl ?? `http://127.0.0.1:${brokerPort}`,
},
},
hostedHarnessLabel,
);
await waitUntil(
'Hosted Harness',
async () => {
const response = await fetch(
`http://127.0.0.1:${harnessPort}/health?deep=1`,
{
headers: { authorization: `Bearer ${harnessToken}` },
},
);
return response.ok;
},
60_000,
harness,
);
if (durableFailover) {
const createResponse = await fetch(`${springUrl}/v1/agents/sessions`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'idempotency-key': `failover-create-${Date.now()}`,
...tenantHeaders(tenant),
},
body: JSON.stringify({
agent_id: 'qwen-code',
input: [
{
type: 'text',
text: continuationFailover
? approvalFault === 'after-earlier-tools'
? `${continuationMarker}. ${b3BudgetDirective} Execute the requested tools and reply exactly ${continuationResponse}.`
: `${continuationMarker}. Execute the requested tool once and reply exactly ${continuationResponse}.`
: inflightFailover
? `${inflightMarker}. Execute the requested tool once and reply exactly ${inflightResponse}.`
: modelRoundFailover
? `${modelRoundMarker}. Use no tools and reply exactly ${modelRoundResponse}.`
: bigOutput
? `${failoverFirstMarker}. Produce the configured long answer.`
: `${failoverFirstMarker}. Reply exactly ${failoverFirstResponse}.`,
},
],
...(workspaceTurns
? { workspace: { workspace_id: boundWorkspaceId } }
: {}),
metadata: {
title: continuationFailover
? 'Managed continuation owner failover E2E'
: inflightFailover
? 'Managed in-flight owner failover E2E'
: frozenControlPlane
? 'Managed frozen control-plane fencing E2E'
: modelRoundFailover
? 'Managed model-round owner failover E2E'
: bigOutput
? 'Managed long-answer persistence E2E'
: 'Managed Session owner failover E2E',
},
}),
});
if (createResponse.status !== 202) {
throw new Error(
`Failover Session create returned ${createResponse.status}: ${await createResponse.text()}`,
);
}
const session = (await createResponse.json()) as PublicSession;
let firstTurnLastSequence = 0;
let formerDispatchOwner = '';
let firstPrefixSequence = 0;
// B2: the live SSE capture stays open across both Harness crashes; the
// second prefix's sequence is hoisted so the audit can check its blanking.
let b2LiveCapture: Promise<B2SseCapture> | undefined;
let b2SecondPrefixSequence = 0;
let b2AttemptIds: string[] = [];
let heldExecutionStartPath: string | undefined;
let originalExecutionCallId: string | undefined;
let originalRuntimeSessionId: string | undefined;
// The runtime_session_id column is written once at INSERT and never
// rewritten, so an equality pin cannot fail. The harness-only arms
// therefore also compare the row's own liveness metric before and
// after the crash: a surviving worker's re-attach renews it.
function runtimeSessionHeartbeat(
runtimeSessionId: string,
): [string, number] {
const row = runMysql(
mysqlPort,
`SELECT last_active_at, record_version FROM qwen_managed_agent.ma_runtime_session WHERE runtime_session_id = ${sqlString(runtimeSessionId)}`,
).split('\t');
if (row.length !== 2 || row[1].length === 0)
throw new Error(`Runtime session row missing (${runtimeSessionId})`);
return [row[0], Number(row[1])];
}
let firstRuntimeHeartbeat: [string, number] | undefined;
if (inflightFailover) {
if (heldStartProxy === undefined) {
throw new Error('In-flight failover did not start its Broker proxy');
}
try {
await waitUntil(
'Runtime execution start boundary',
() => {
const observations = heldStartProxy?.observations() ?? [];
return (
heldStartProxy?.heldPath() !== undefined ||
observations.some((observation) =>
/ [45]\d\d(?: |$)/.test(observation),
)
);
},
120_000,
harness,
);
} catch (error) {
throw new Error(
`Runtime execution start boundary failed; proxy=${heldStartProxy.observations().join(' | ') || 'no requests'}`,
{ cause: error },
);
}
heldExecutionStartPath = heldStartProxy.heldPath();
if (heldExecutionStartPath === undefined) {
throw new Error(
`Runtime execution start boundary failed; proxy=${heldStartProxy.observations().join(' | ') || 'no requests'}`,
);
}
} else if (approvalRecovery) {
await waitUntil(
'Original requested approval',
async () => {
const page = await fetchJson<PublicList<Record<string, unknown>>>(
`${springUrl}/v1/agents/sessions/${session.id}/actions?limit=100`,
{ headers: tenantHeaders(tenant) },
);
const requested = page.data.filter(
(action) => action.state === 'requested',
);
if (requested.length !== 1) return false;
originalApprovalId = String(requested[0].id);
return originalApprovalId.startsWith('tool_approval_');
},
120_000,
harness,
);
const actionFilter = `tenant_id=${sqlString(tenant)} AND session_id=${sqlString(session.id)} AND record_kind='ACTION' AND action_id=${sqlString(originalApprovalId!)}`;
originalApprovalOptions = JSON.parse(
runMysql(
mysqlPort,
`SELECT options_json FROM qwen_managed_agent.ma_session_record WHERE ${actionFilter}`,
),
);
if (
originalApprovalOptions?.v !== 3 ||
!originalApprovalOptions.continuationRef ||
!Number.isSafeInteger(originalApprovalOptions.createdAt) ||
!Number.isSafeInteger(originalApprovalOptions.expiresAt) ||
originalApprovalOptions.expiresAt <= originalApprovalOptions.createdAt
) {
throw new Error(
`New approval omitted the recovery continuation or its deadline: version=${originalApprovalOptions?.v} createdAt=${originalApprovalOptions?.createdAt} expiresAt=${originalApprovalOptions?.expiresAt}`,
);
}
originalApprovalPlan = JSON.parse(
runMysql(
mysqlPort,
`SELECT CONVERT(inline_bytes USING utf8mb4) FROM qwen_managed_agent.ma_resource WHERE tenant_id=${sqlString(tenant)} AND session_id=${sqlString(session.id)} AND resource_id=${sqlString(originalApprovalOptions.continuationRef.resourceId)}`,
),
);
const expectedOriginalPlanCalls =
approvalFault === 'sibling-approval' ? 2 : 1;
if (
!originalApprovalPlan ||
originalApprovalPlan.calls?.length !== expectedOriginalPlanCalls ||
originalApprovalPlan.actionId !== originalApprovalId ||
originalApprovalPlan.stage !== 'approval' ||
(approvalFault === 'sibling-approval' &&
originalApprovalPlan.approvalOrdinal !== 0)
) {
throw new Error(
`Original saved approval plan does not identify the original ${expectedOriginalPlanCalls} call(s) at the first approval`,
);
}
originalRuntimeSessionId = originalApprovalPlan.runtime.runtimeSessionId;
const runtime = runMysql(
mysqlPort,
`SELECT binding_id, runtime_generation, workspace_generation, session_state FROM qwen_managed_agent.ma_runtime_session WHERE runtime_session_id=${sqlString(originalRuntimeSessionId!)}`,
).split('\t');
if (
runtime[0] !== originalApprovalPlan.runtime.bindingId ||
runtime[1] !== originalApprovalPlan.runtime.generation ||
runtime[2] !== originalApprovalPlan.runtime.workspaceGeneration ||
runtime[3] !== 'READY'
) {
throw new Error(
`Saved approval Runtime binding differs from the real Broker: ${runtime.join(',')}`,
);
}
firstRuntimeHeartbeat = runtimeSessionHeartbeat(
originalRuntimeSessionId!,
);
await waitUntil(
'Original durable await_action checkpoint',
() => {
const id = runMysql(
mysqlPort,
`SELECT latest_checkpoint_resource_id FROM qwen_managed_agent.ma_session_head WHERE tenant_id=${sqlString(tenant)} AND session_id=${sqlString(session.id)}`,
);
if (!id) return false;
const bytes = runMysql(
mysqlPort,
`SELECT CONVERT(inline_bytes USING utf8mb4) FROM qwen_managed_agent.ma_resource WHERE tenant_id=${sqlString(tenant)} AND session_id=${sqlString(session.id)} AND resource_id=${sqlString(id)}`,
);
if (!bytes) return false;
const checkpoint = JSON.parse(bytes);
return (
checkpoint.continuation?.phase === 'await_action' &&
checkpoint.approval?.requestId === originalApprovalId
);
},
10_000,
harness,
);
const privateHead = runMysql(
mysqlPort,
`SELECT storage_version, latest_checkpoint_resource_id FROM qwen_managed_agent.ma_session_head WHERE tenant_id=${sqlString(tenant)} AND session_id=${sqlString(session.id)}`,
).split('\t');
const checkpoint = JSON.parse(
runMysql(
mysqlPort,
`SELECT CONVERT(inline_bytes USING utf8mb4) FROM qwen_managed_agent.ma_resource WHERE tenant_id=${sqlString(tenant)} AND session_id=${sqlString(session.id)} AND resource_id=${sqlString(privateHead[1])}`,
),
);
if (
privateHead[0] !== '2' ||
checkpoint.continuation?.phase !== 'await_action' ||
checkpoint.approval?.requestId !== originalApprovalId
) {
throw new Error(
'Approval snapshot was not committed before the crash barrier',
);
}
const executionCount = Number(
runMysql(
mysqlPort,
'SELECT COUNT(*) FROM qwen_managed_agent.ma_tool_execution',
),
);
if (executionCount !== 0 || existsSync(inflightSideEffect))
throw new Error('Tool ran before the original approval');
saveEvidence('original-approval.json', {
actionId: originalApprovalId,
options: originalApprovalOptions,
plan: originalApprovalPlan,
originalRuntime: runtime,
storageVersion: Number(privateHead[0]),
checkpointPhase: checkpoint.continuation.phase,
executionCount,
physicalFileExists: existsSync(inflightSideEffect),
});
} else if (continuationFailover) {
await waitUntil(
'Continuation partial text',
async () => {
const page = await fetchJson<PublicList<PublicEvent>>(
`${springUrl}/v1/agents/sessions/${session.id}/events?after=0&limit=100`,
{ headers: tenantHeaders(tenant) },
);
return page.data.some(
(event) =>
event.type === 'item.output_text.delta' &&
eventText(event).includes(continuationPartial),
);
},
120_000,
harness,
);
const execution = runMysql(
mysqlPort,
'SELECT execution_call_id, execution_state, dispatch_generation, runtime_session_id FROM qwen_managed_agent.ma_tool_execution',
).split('\t');
const sideEffectBytes = existsSync(inflightSideEffect)
? readFileSync(inflightSideEffect, 'utf8')
: '';
if (
execution.length !== 4 ||
execution[0]?.length === 0 ||
execution[1] !== 'SETTLED' ||
execution[2] !== '1' ||
execution[3]?.length === 0 ||
sideEffectBytes !== inflightSideEffectContent
) {
throw new Error(
`Continuation boundary was not durable: execution=${execution.join(',')} sideEffect=${JSON.stringify(sideEffectBytes)}`,
);
}
originalExecutionCallId = execution[0];
originalRuntimeSessionId = execution[3];
if (harnessOnly)
firstRuntimeHeartbeat = runtimeSessionHeartbeat(
originalRuntimeSessionId,
);
} else if (modelRoundFailover) {
// The crash point is the first model round of a Session with no
// Workspace: the provider already streamed a prefix the public feed
// shows, and nothing but the saved model request can continue it.
if (harnessOnly) {
// B2: subscribe to the live public view before the first prefix. The
// stream is control-plane-owned, so this one connection survives both
// Harness crashes; Spring reconciles from after=0 on connect, so a
// slightly late subscribe still sees every frame. The terminal is a
// progress signal; the drain window keeps reading so a double
// projection is observed (R1-2).
b2LiveCapture = readPublicSse(springUrl, session.id, {
tenantId: tenant,
after: 0,
until: (events) => events.some((event) => event.terminal),
timeoutMs: 240_000,
});
// Awaited only at the audit; without this observer a capture that
// fails — or that the teardown aborts on an early arm failure — is an
// unhandled rejection that exits before the children are stopped
// (R1-1). The audit's await still sees the real rejection.
b2LiveCapture.catch(() => undefined);
}
await waitUntil(
'First model round partial text',
async () =>
(await publicTranscript(springUrl, tenant, session.id)).text ===
modelRoundPartials[0],
120_000,
harness,
);
firstPrefixSequence = Math.min(
...deltaSequences(
(await publicTranscript(springUrl, tenant, session.id)).events,
modelRoundPartials[0],
),
);
} else {
const firstTurn = await waitForTerminal(
springUrl,
tenant,
session.id,
0,
spring,
);
firstTurnLastSequence = firstTurn.lastSequence;
const firstTerminal = firstTurn.events.find((event) => event.terminal);
if (firstTerminal?.type !== 'turn.completed') {
const storeHead = runMysql(
mysqlPort,
`SELECT state, writer_generation, writer_id, writer_lease_until, ${databaseEpochMillisSql} AS observed_now, journal_revision, committed_sequence FROM qwen_managed_agent.ma_session_head WHERE tenant_id=${sqlString(tenant)} AND session_id=${sqlString(session.id)}`,
);
throw new Error(
`First failover Turn ended with ${firstTerminal?.type ?? 'no terminal event'}; store head=${storeHead || 'missing'}`,
);
}
if (bigOutput) {
const modelRequests = fake?.requests.filter(
({ body }) => body['stream'] === true,
);
if (modelRequests?.length !== 1)
throw new Error('The long answer must complete without model retry');
assertStoredAnswer(
mysqlPort,
tenant,
session.id,
failoverFirstResponse,
);
assertPublicAnswer(firstTurn.events, failoverFirstResponse);
}
}
const sessionFilter = `tenant_id=${sqlString(tenant)} AND session_id=${sqlString(session.id)}`;
const firstBootId = runMysql(
mysqlPort,
`SELECT harness_boot_id FROM qwen_managed_agent.ma_session WHERE ${sessionFilter}`,
);
const firstHead = runMysql(
mysqlPort,
`SELECT writer_generation, journal_revision, committed_sequence FROM qwen_managed_agent.ma_session_head WHERE ${sessionFilter}`,
)
.split('\t')
.map(Number);
if (
firstBootId.length === 0 ||
firstHead.length !== 3 ||
firstHead.some((value) => !Number.isSafeInteger(value) || value < 1)
) {
throw new Error(
`First owner did not commit a durable private Session: boot=${firstBootId} head=${firstHead.join(',')}`,
);
}
if (inflightFailover) {
const execution = runMysql(
mysqlPort,
'SELECT execution_call_id, execution_state, dispatch_generation, runtime_session_id FROM qwen_managed_agent.ma_tool_execution',
).split('\t');
const latestCheckpoint = runMysql(
mysqlPort,
`SELECT latest_checkpoint_resource_id FROM qwen_managed_agent.ma_session_head WHERE ${sessionFilter}`,
);
if (
execution.length !== 4 ||
execution[0]?.length === 0 ||
execution[1] !== 'PREPARED' ||
execution[2] !== '0' ||
execution[3]?.length === 0 ||
latestCheckpoint.length === 0 ||
!heldExecutionStartPath?.includes(
encodeURIComponent(execution[0] ?? ''),
) ||
existsSync(inflightSideEffect)
) {
throw new Error(
`In-flight boundary was not durable before start: execution=${execution.join(',')} checkpoint=${latestCheckpoint || 'missing'} sideEffect=${existsSync(inflightSideEffect)}`,
);
}
originalExecutionCallId = execution[0];
originalRuntimeSessionId = execution[3];
if (harnessOnly)
firstRuntimeHeartbeat = runtimeSessionHeartbeat(
originalRuntimeSessionId,
);
}
if (freeze) {
// Freeze the writer side only: stopping the original Harness (the
// lease holder) fences it against mutation on wake. Assert liveness
// first — a setup that already died would make the freeze a no-op
// and every later wake assertion vacuous.
if (!processTreeExists(harness.child)) {
throw new Error(
'The original Harness died before the freeze: cannot freeze a dead owner',
);
}
// The original Spring must actually die — reclaiming its workspace
// binding requires death evidence from /proc liveness, and a
// SIGSTOPped JVM still reads as alive there.
await signalProcessTree(harness.child, 'SIGSTOP');
await crashProcess(
spring.child,
'Spring Managed Agent Server (original)',
);
} else if (frozenControlPlane) {
// Unlike --freeze, the former Spring survives too: no Workspace is
// bound, so no Runtime reclaim needs its death. Its claim is the stale
// authority the wake must not be able to use.
formerDispatchOwner = runMysql(
mysqlPort,
`SELECT dispatch_owner FROM qwen_managed_agent.ma_turn WHERE ${sessionFilter}`,
);
if (!/^[0-9a-f-]{36}$/.test(formerDispatchOwner)) {
throw new Error(
`The original control plane held no dispatch claim to freeze: owner=${formerDispatchOwner || 'none'}`,
);
}
const formerPair = [
[harness, `${hostedHarnessLabel} (original)`],
[spring, 'Spring Managed Agent Server (original)'],
] as const;
for (const [child, name] of formerPair) {
if (!processTreeExists(child.child)) {
throw new Error(`${name} died before the freeze`);
}
frozenChildren.push(child);
}
// A stop that lands inside a store transaction keeps its row locks for
// the whole freeze, and the replacement's claim then fails on a lock
// wait instead of meeting the fence. Only the frozen pair talks to
// MySQL at this point, so any open InnoDB transaction is theirs:
// resume them briefly and stop again.
for (let attempt = 0; ; attempt += 1) {
for (const [child] of formerPair) {
signalProcessTree(child.child, 'SIGSTOP');
}
if (
runMysql(
mysqlPort,
'SELECT COUNT(*) FROM information_schema.innodb_trx',
) === '0'
) {
break;
}
if (attempt === 100) {
throw new Error(
'The original control plane never stopped outside a store transaction',
);
}
for (const [child] of formerPair) {
signalProcessTree(child.child, 'SIGCONT');
}
// InnoDB reuses its transaction snapshot until no reader has
// queried it for 100ms; faster polling can keep a stale busy view.
await new Promise((resolve) => setTimeout(resolve, 150));
}
} else if (harnessOnly) {
// Kill only the Harness: a live control plane must adopt the next
// generation instead of failing every bound Session (G3).
await crashChild(harness.child, `${hostedHarnessLabel} (original)`);
} else {
await Promise.all([
crashChild(harness.child, `${hostedHarnessLabel} (original)`),
inflightFailover || continuationFailover
? crashProcess(spring.child, 'Spring Managed Agent Server (original)')
: crashChild(spring.child, 'Spring Managed Agent Server (original)'),
]);
}
acceptReplacementContinuation = true;
releaseContinuationHold();
if (modelRoundFailover) advanceModelRound();
await heldStartProxy?.close();
heldStartProxy = undefined;
if (!freeze && !frozenControlPlane) {
// The frozen Harness keeps its home; the killed Spring's disk is
// scrapped like any dead owner's.
rmSync(harnessHome, { recursive: true, force: true });
}
if (!harnessOnly && !frozenControlPlane) {
rmSync(runtimeHome, { recursive: true, force: true });
}
await waitUntil(
'Managed Session writer lease expiry',
(remainingMs) =>
runMysql(
mysqlPort,
`SELECT IF(writer_lease_until IS NULL OR writer_lease_until < (${databaseEpochMillisSql}), 1, 0) FROM qwen_managed_agent.ma_session_head WHERE ${sessionFilter}`,
remainingMs,
) === '1',
10_000,
);
if (inflightFailover || continuationFailover || modelRoundFailover) {
await waitUntil(
'Managed Turn dispatch lease expiry',
(remainingMs) =>
runMysql(
mysqlPort,
`SELECT IF(dispatch_lease_until IS NULL OR dispatch_lease_until < (${databaseEpochMillisSql}), 1, 0) FROM qwen_managed_agent.ma_turn WHERE ${sessionFilter}`,
remainingMs,
) === '1',
10_000,
);
}
let replacementSpring: typeof spring;
let replacementSpringUrl: string;
let replacementHarnessPort: number;
let replacementBrokerUrl: string;
if (harnessOnly) {
// The live Spring keeps its fixed Harness base URL, so the next
// generation must answer on the same port; the same audit selectors
// then read the original control plane.
replacementSpring = spring;
replacementSpringUrl = springUrl;
replacementHarnessPort = harnessPort;
// The two pre-dispatch crash barriers hold the approved execution's
// :start before the Broker sees it: after-prepare waits for the
// durable PREPARED row, while after-decision only needs the decision
// to commit. after-result must NOT be held here: its barrier is the
// committed result, so holding :start would keep the execution at
// PREPARED and its own wait for SETTLED could never succeed.
// cleanup-failed keeps :start live and refuses the release instead.
if (
approvalFault === 'after-decision' ||
approvalFault === 'after-prepare' ||
approvalFault === 'cleanup-failed'
) {
replacementBrokerProxy = await startHeldExecutionStartProxy(
`http://127.0.0.1:${brokerPort}`,
approvalFault !== 'cleanup-failed',
approvalFault === 'cleanup-failed',
);
}
replacementBrokerUrl =
replacementBrokerProxy?.baseUrl ?? `http://127.0.0.1:${brokerPort}`;
} else {
// Under --freeze the original Spring is dead, but the frozen Harness
// still holds the session-store URL the original Spring handed it at
// load (immutable after load). The replacement must answer on the
// original Spring port so the woken former writer's store calls meet
// a live, fencing control plane instead of a dead socket — otherwise
// every post-wake assertion would hold by disconnection, not fencing.
const replacementSpringPort = freeze ? springPort : await freePort();
replacementHarnessPort = await freePort();
const replacementBrokerPort = await freePort();
replacementSpringUrl = `http://127.0.0.1:${replacementSpringPort}`;
replacementSpring = start(
java,
springArguments,
{
env: {
...cleanEnvironment,
HOME: replacementRuntimeHome,
LANG: process.env['LANG'] ?? 'C',
LC_ALL: process.env['LC_ALL'] ?? 'C',
NO_PROXY: '127.0.0.1,localhost',
QWEN_HOME: path.join(replacementRuntimeHome, '.qwen'),
TMPDIR: temporary,
no_proxy: '127.0.0.1,localhost',
SERVER_PORT: String(replacementSpringPort),
SPRING_DATASOURCE_PASSWORD: '',
SPRING_DATASOURCE_URL: `jdbc:mysql://127.0.0.1:${mysqlPort}/qwen_managed_agent?useSSL=false&allowPublicKeyRetrieval=true`,
SPRING_DATASOURCE_USERNAME: 'root',
QWEN_MANAGED_AGENT_APPROVAL_MODE: approvalRecovery
? 'default'
: 'yolo',
QWEN_MANAGED_AGENT_APPROVAL_TIMEOUT:
verificationMode === 'expiry' ? '30s' : '5m',
QWEN_MANAGED_AGENT_CAPABILITY_DIGEST: capabilityDigest,
QWEN_MANAGED_AGENT_HARNESS_BASE_URL: `http://127.0.0.1:${replacementHarnessPort}`,
QWEN_MANAGED_AGENT_HARNESS_ENABLED: 'true',
QWEN_MANAGED_AGENT_HARNESS_REQUEST_TIMEOUT: '120s',
QWEN_MANAGED_AGENT_HARNESS_TOKEN: harnessToken,
QWEN_MANAGED_AGENT_RUNTIME_TRUSTED_LOCAL_REBOOT_RECOVERY: 'false',
...(registerStorage
? {
QWEN_MANAGED_AGENT_RUNTIME_VERIFIED_WORKSPACE_RECOVERY_ENABLED:
'true',
}
: {}),
QWEN_MANAGED_AGENT_TRUSTED_ACTOR_HEADER: trustedActorHeader,
QWEN_MANAGED_AGENT_WORKSPACE_FILES_ENABLED: 'true',
...(runtimeTakeover
? {
QWEN_MANAGED_AGENT_RUNTIME_DURABLE_LOCAL_PROCESS:
process.platform === 'linux' ? 'true' : 'false',
}
: {
QWEN_MANAGED_AGENT_RUNTIME_DURABLE_LOCAL_PROCESS: 'false',
}),
QWEN_MANAGED_AGENT_DISPATCH_LEASE_DURATION: '2s',
QWEN_MANAGED_AGENT_DISPATCH_LEASE_RENEW_INTERVAL: '500ms',
QWEN_MANAGED_AGENT_DISPATCH_SCAN_DELAY: '200ms',
QWEN_MANAGED_AGENT_SESSION_STORE_BASE_URL: replacementSpringUrl,
QWEN_MANAGED_AGENT_SESSION_STORE_ENABLED: 'true',
QWEN_MANAGED_AGENT_SESSION_STORE_WRITER_LEASE_DURATION: '5s',
QWEN_MANAGED_AGENT_WORKSPACE_ID: workspaceId,
QWEN_MANAGED_AGENT_RUNTIME_BROKER_ENABLED: 'true',
QWEN_MANAGED_AGENT_RUNTIME_BROKER_PORT: String(
replacementBrokerPort,
),
QWEN_MANAGED_AGENT_RUNTIME_BROKER_TOKEN: brokerToken,
QWEN_MANAGED_AGENT_RUNTIME_CREDENTIAL_KEY: credentialKey,
QWEN_MANAGED_AGENT_RUNTIME_CREDENTIAL_KEY_ID: 'e2e-local-v1',
QWEN_MANAGED_AGENT_RUNTIME_STATE_DIRECTORY: frozenControlPlane
? replacementRuntimeState
: runtimeState,
QWEN_MANAGED_AGENT_RUNTIME_WORKER_ENTRY: cliBundle,
QWEN_MANAGED_AGENT_NODE_EXECUTABLE: process.execPath,
QWEN_MANAGED_AGENT_WORKSPACE_CWD: workspace,
},
},
'Replacement Spring Managed Agent Server',
);
await waitUntil(
'Replacement Spring Managed Agent Server',
async () => {
const response = await fetch(
`${replacementSpringUrl}/actuator/health`,
);
return response.ok;
},
60_000,
replacementSpring,
);
if (inflightFailover) {
replacementBrokerProxy = await startHeldExecutionStartProxy(
`http://127.0.0.1:${replacementBrokerPort}`,
false,
);
}
replacementBrokerUrl =
replacementBrokerProxy?.baseUrl ??
`http://127.0.0.1:${replacementBrokerPort}`;
}
// Every replacement generation answers on the same port with a fresh
// home: the Harness-only model-round arm starts a third one after the
// second dies mid-round.
const startReplacementHarness = async (
name: string,
crashBeforeAssistantCommit = false,
holdPendingSiblingApproval = false,
): Promise<Child> => {
const next = start(
process.execPath,
[
cliBundle,
'serve',
'--profile',
'hosted-harness',
'--port',
String(replacementHarnessPort),
'--hostname',
'127.0.0.1',
'--require-auth',
'--no-web',
'--workspace',
workspace,
// The original harness keeps its broker unconditionally, so the
// replacement must too — non-workspace failover arms read the same
// execution store the broker feeds; replacementBrokerUrl already
// carries the harnessOnly alias and the held-start proxy base.
'--managed-runtime-broker-url',
replacementBrokerUrl,
`--managed-runtime-broker-token=${brokerToken}`,
],
{
env: {
...cleanEnvironment,
HOME: replacementHarnessHome,
LANG: process.env['LANG'] ?? 'C',
LC_ALL: process.env['LC_ALL'] ?? 'C',
QWEN_HOME: path.join(replacementHarnessHome, '.qwen'),
QWEN_CODE_TRUSTED_FOLDERS_PATH: trustedFolders,
QWEN_HOSTED_HARNESS_CAPABILITY_DIGEST: capabilityDigest,
QWEN_SERVER_TOKEN: harnessToken,
OPENAI_API_KEY: 'fake-key',
OPENAI_BASE_URL: fakeBaseUrl,
OPENAI_MODEL: 'fake-model',
QWEN_MODEL: 'fake-model',
QWEN_RUNTIME_BROKER_TOKEN: brokerToken,
QWEN_RUNTIME_BROKER_URL: replacementBrokerUrl,
// A4: the replacement loses its first load attach reply once, so
// the control plane must redrive into the now-active driver.
...(approvalFault === 'load-reply-lost'
? { QWEN_E2E_DROP_LOAD_REPLY: session.id }
: {}),
// B3a: only the generation that must crash carries this flag; the
// post-fault recovery generation must not inherit it, or it would
// crash in the same window forever.
...(crashBeforeAssistantCommit
? { QWEN_E2E_CRASH_BEFORE_ASSISTANT_COMMIT: session.id }
: {}),
// A3: only the generation that must crash carries this flag; it
// holds the first sibling approval it asks (the pending sibling
// after the decided one). The recovery generation must not inherit
// it, or it would hold the same approval forever.
...(holdPendingSiblingApproval
? { QWEN_E2E_HOLD_PENDING_SIBLING_APPROVAL: session.id }
: {}),
},
},
name,
);
await waitUntil(
name,
async () => {
const response = await fetch(
`http://127.0.0.1:${replacementHarnessPort}/health?deep=1`,
{ headers: { authorization: `Bearer ${harnessToken}` } },
);
return response.ok;
},
60_000,
next,
);
return next;
};
let replacementHarness = await startReplacementHarness(
'Replacement Hosted Harness',
approvalFault === 'finished-before-commit',
approvalFault === 'sibling-approval',
);
if (approvalRecovery) {
await waitUntil(
'Original approval attached to replacement Harness',
() => {
const boot = runMysql(
mysqlPort,
`SELECT harness_boot_id FROM qwen_managed_agent.ma_session WHERE ${sessionFilter}`,
);
return boot.length > 0 && boot !== firstBootId;
},
120_000,
replacementHarness,
);
if (approvalFault === 'load-reply-lost') {
// The fault must actually have landed: the replacement lost its first
// load attach reply (after its driver started). A redrive that never
// lost a reply would prove nothing about the active-driver handoff.
await waitUntil(
'Takeover load reply lost',
() =>
replacementHarness.log().includes('E2E dropping first load reply'),
120_000,
replacementHarness,
);
}
const actionBeforeResponse = await fetchJson<Record<string, unknown>>(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}/actions/${originalApprovalId}`,
{ headers: tenantHeaders(tenant) },
);
if (
actionBeforeResponse.id !== originalApprovalId ||
(verificationMode !== 'expiry' &&
actionBeforeResponse.state !== 'requested')
) {
throw new Error(
'Replacement did not preserve the original requested Action',
);
}
// The replacement must hand the parked wait the deadline it already
// had; a reset deadline would extend expiry and hide a lost clock.
const optionsAfterReplacement = JSON.parse(
runMysql(
mysqlPort,
`SELECT options_json FROM qwen_managed_agent.ma_session_record WHERE tenant_id=${sqlString(tenant)} AND session_id=${sqlString(session.id)} AND record_kind='ACTION' AND action_id=${sqlString(originalApprovalId!)}`,
),
);
if (
actionBeforeResponse.created_at !==
originalApprovalOptions!.createdAt ||
actionBeforeResponse.expires_at !==
originalApprovalOptions!.expiresAt ||
optionsAfterReplacement.createdAt !==
originalApprovalOptions!.createdAt ||
optionsAfterReplacement.expiresAt !== originalApprovalOptions!.expiresAt
) {
throw new Error(
`Replacement reset the original approval deadline: action=${String(actionBeforeResponse.created_at)}..${String(actionBeforeResponse.expires_at)} record=${optionsAfterReplacement.createdAt}..${optionsAfterReplacement.expiresAt} saved=${originalApprovalOptions!.createdAt}..${originalApprovalOptions!.expiresAt}`,
);
}
let responseOperation: Record<string, unknown> | undefined;
if (verificationMode === 'allow' || verificationMode === 'deny') {
const decisionUrl = `${replacementSpringUrl}/v1/agents/sessions/${session.id}/actions/${originalApprovalId}/responses`;
const decisionHeaders = {
'content-type': 'application/json',
'idempotency-key': `g3-answer-${originalApprovalId}`,
...tenantHeaders(tenant),
};
const decisionBody = JSON.stringify({
kind: 'permission',
input_revision: originalApprovalOptions!.inputRevision,
policy_revision: originalApprovalOptions!.policyRevision,
option_id: verificationMode,
});
if (
approvalFault === 'after-result' ||
approvalFault === 'after-earlier-tools'
) {
approvalResultHold = new Promise<void>((resolve) => {
releaseApprovalResultHold = resolve;
});
}
// C1/C2: arm the store boundary before the decision can drive the
// approved batch to its turn.settled commit. The fault must land on
// that terminal commit, not on an earlier record.
if (
approvalFault === 'terminal-reply-lost' ||
approvalFault === 'terminal-write-failed'
) {
storeFaultProxy?.armTerminal(
session.id,
approvalFault === 'terminal-reply-lost' ? 'drop-reply' : 'reject',
);
}
if (approvalFault === 'decision-reply-lost') {
await postAndDropReply(decisionUrl, decisionBody, decisionHeaders);
}
const response = await fetch(decisionUrl, {
method: 'POST',
headers: decisionHeaders,
body: decisionBody,
});
if (response.status !== 202)
throw new Error(
`Original Action response returned ${response.status}: ${await response.text()}`,
);
responseOperation = await response.json();
if (approvalFault === 'decision-reply-lost') {
const operations = runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_operation WHERE ${sessionFilter} AND operation_kind='ACTION_RESPONSE'`,
);
if (operations !== '1')
throw new Error(
`Lost decision reply produced ${operations} ACTION_RESPONSE operations, not one`,
);
}
if (approvalFault === 'after-earlier-tools') {
// The first Allow drives tool A (round 0); the model then asks approval
// for tool B (round 1) under a NEW Action. Detect that requested Action
// and allow it so both earlier rounds execute before the held round 2.
await waitUntil(
'Second earlier-tool approval requested',
() => {
const rows = runMysql(
mysqlPort,
`SELECT action_id, state FROM qwen_managed_agent.ma_session_record WHERE ${sessionFilter} AND record_kind='ACTION'`,
);
const requested = (rows ? rows.split('\n') : [])
.map((row) => row.split('\t'))
.filter(
([id, state]) =>
state === 'requested' && id !== originalApprovalId,
);
if (requested.length === 1) {
secondApprovalId = requested[0][0];
return true;
}
return false;
},
120_000,
replacementHarness,
);
const secondOptions = JSON.parse(
runMysql(
mysqlPort,
`SELECT options_json FROM qwen_managed_agent.ma_session_record WHERE ${sessionFilter} AND record_kind='ACTION' AND action_id=${sqlString(secondApprovalId!)}`,
),
);
const secondResponse = await fetch(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}/actions/${secondApprovalId}/responses`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
'idempotency-key': `g3-answer-${secondApprovalId}`,
...tenantHeaders(tenant),
},
body: JSON.stringify({
kind: 'permission',
input_revision: secondOptions.inputRevision,
policy_revision: secondOptions.policyRevision,
option_id: 'allow',
}),
},
);
if (secondResponse.status !== 202)
throw new Error(
`Second earlier-tool response returned ${secondResponse.status}: ${await secondResponse.text()}`,
);
}
} else if (verificationMode === 'cancel') {
const response = await fetch(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}/events`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
'idempotency-key': `g3-cancel-${originalApprovalId}`,
...tenantHeaders(tenant),
},
body: JSON.stringify({
type: 'agent.session.cancel',
turn_id: actionBeforeResponse.turn_id,
}),
},
);
if (response.status !== 202)
throw new Error(
`Original Turn cancel returned ${response.status}: ${await response.text()}`,
);
}
let publishedPrefixSequences: number[] = [];
let terminalAcceptsAtBarrier = 0;
let b3Before: B3RoundBudget | undefined;
if (
approvalFault === 'after-decision' ||
approvalFault === 'after-prepare' ||
approvalFault === 'after-result' ||
approvalFault === 'finished-before-commit' ||
approvalFault === 'sibling-approval' ||
approvalFault === 'after-earlier-tools' ||
approvalFault === 'cleanup-failed' ||
approvalFault === 'terminal-reply-lost' ||
approvalFault === 'terminal-write-failed'
) {
// A5/A5a: kill the generation that holds the committed decision (or,
// for after-result, the committed result with its model round still
// open) so the next generation must resume the stage instead of
// replaying the approval.
const crashedBootId = runMysql(
mysqlPort,
`SELECT harness_boot_id FROM qwen_managed_agent.ma_session WHERE ${sessionFilter}`,
);
if (approvalFault === 'after-prepare') {
await waitUntil(
'Approved execution prepared behind the held start',
() => {
const execution = runMysql(
mysqlPort,
'SELECT execution_state, dispatch_generation FROM qwen_managed_agent.ma_tool_execution',
);
return (
execution === 'PREPARED\t0' &&
replacementBrokerProxy?.heldPath() !== undefined
);
},
120_000,
replacementHarness,
);
} else if (approvalFault === 'after-result') {
await waitUntil(
'Approved result committed and its prefix published',
async () => {
const execution = runMysql(
mysqlPort,
'SELECT execution_state, IF(result_json IS NULL, 0, 1) FROM qwen_managed_agent.ma_tool_execution',
);
const heldResultRequest = (fake?.requests ?? []).some(
({ body }) => {
const messages = JSON.stringify(body['messages']);
return (
messages.includes(continuationMarker) &&
messages.includes('"role":"tool"')
);
},
);
if (execution !== 'SETTLED\t1' || !heldResultRequest)
return false;
// The request log is written before the fake server sends its
// response, so it alone does not prove the Harness consumed and
// published the held prefix. Require the public feed to carry
// it; otherwise a no-op retraction would pass vacuously.
const view = await publicTranscript(
replacementSpringUrl,
tenant,
session.id,
);
return view.text.includes(continuationPartial);
},
120_000,
replacementHarness,
);
} else if (approvalFault === 'after-earlier-tools') {
// Both earlier tool rounds must have committed their results and the
// round-2 post-tool request must be held with its prefix published, so
// the crash lands after the earlier tools but before the final output
// commits. Recovery must then resume the saved round and cost ledger.
await waitUntil(
'Earlier tools settled and the later round held',
async () => {
const rows = runMysql(
mysqlPort,
'SELECT execution_state, IF(result_json IS NULL, 0, 1) FROM qwen_managed_agent.ma_tool_execution ORDER BY execution_call_id',
);
const execs = rows ? rows.split('\n') : [];
if (execs.length !== 2 || execs.some((r) => r !== 'SETTLED\t1'))
return false;
const heldLaterRequest = (fake?.requests ?? []).some(
({ body }) => {
const messages = JSON.stringify(body['messages']);
return (
messages.includes(continuationMarker) &&
(messages.match(/"role":"tool"/g) ?? []).length >= 2
);
},
);
if (!heldLaterRequest) return false;
const view = await publicTranscript(
replacementSpringUrl,
tenant,
session.id,
);
return view.text.includes(continuationPartial);
},
120_000,
replacementHarness,
);
// Capture the durable round and cost ledger at the crash boundary so
// recovery can be proven to resume them rather than restart them.
b3Before = b3RoundAndBudget(mysqlPort, sessionFilter);
saveEvidence('b3-round-budget-before.json', b3Before);
} else if (
approvalFault === 'terminal-reply-lost' ||
approvalFault === 'terminal-write-failed'
) {
// C1/C2: wait until the terminal commit met the store boundary.
// C1 already committed it and lost only the reply; C2 refused the
// write, leaving the payable checkpoint without its terminal.
await waitUntil(
'Terminal commit store fault observed',
() => storeFaultProxy?.terminalFaulted() === true,
120_000,
replacementHarness,
);
// The barrier only arms on a confirmed 200, so this count proves
// the terminal was committed before the crash. The replacement
// projects it; a second store commit is not required.
terminalAcceptsAtBarrier =
storeFaultProxy?.terminalAcceptCount() ?? 0;
} else if (approvalFault === 'cleanup-failed') {
// C3: the release refusal is the fault; the crash must land after
// the control plane projected the terminal, or the dispatch scan
// would take a RUNNING Turn over and pay the debt through that
// path. Only a terminal Turn proves the cleanup-only retry.
await waitUntil(
'Runtime cleanup release refused',
() => (replacementBrokerProxy?.releaseFailureCount() ?? 0) > 0,
120_000,
replacementHarness,
);
await waitUntil(
'Original Turn projected before the cleanup crash',
() =>
runMysql(
mysqlPort,
`SELECT status FROM qwen_managed_agent.ma_turn WHERE ${sessionFilter} ORDER BY gmt_create DESC LIMIT 1`,
) === 'COMPLETED',
120_000,
replacementHarness,
);
} else if (approvalFault === 'after-decision') {
// The 202 admitted the response operation, not the decision. Wait
// for the durable decision row and its receipt before SIGKILL.
await waitUntil(
'Approval decision durably committed',
() =>
approvalDecisionCommitted(
runMysql(
mysqlPort,
`SELECT state, IF(decision_receipt_id IS NULL, 0, 1), IF(decision_digest IS NULL, 0, 1) FROM qwen_managed_agent.ma_session_record WHERE ${sessionFilter} AND record_kind='ACTION' AND action_id=${sqlString(originalApprovalId!)}`,
),
),
120_000,
replacementHarness,
);
} else if (approvalFault === 'finished-before-commit') {
// B3a: the replacement hooked the window after its post-tool stream
// Finished marker is durable but before the assistant commit. The
// barrier line proves the fault landed inside that window; the
// recovery parity check below proves the next generation reissued
// the saved request rather than compensating a committed terminal.
await waitUntil(
'Finished turn held before the assistant commit',
() =>
replacementHarness
.log()
.includes('E2E holding finished turn before assistant commit'),
120_000,
replacementHarness,
);
} else if (approvalFault === 'sibling-approval') {
// A3: the replacement decided sibling[0] (Allow[0]) and now holds
// the first approval it asks — the pending sibling[1]. The barrier
// line proves the hook fired; the durable shape proves the crash
// lands in the approval gap (sibling[0] decided, sibling[1] still
// requested), not after the whole batch finished approving.
await waitUntil(
'Pending sibling approval held',
() =>
replacementHarness
.log()
.includes('E2E holding pending sibling approval'),
120_000,
replacementHarness,
);
const siblingActionRows = runMysql(
mysqlPort,
`SELECT action_id, state FROM qwen_managed_agent.ma_session_record WHERE ${sessionFilter} AND record_kind='ACTION' ORDER BY action_id`,
);
const siblingActions = siblingActionRows
? siblingActionRows.split('\n').map((row) => row.split('\t'))
: [];
const decidedSibling = siblingActions.find(
([id]) => id === originalApprovalId,
);
const pendingSibling = siblingActions.find(
([id]) => id !== originalApprovalId,
);
if (
siblingActions.length !== 2 ||
decidedSibling?.[1] !== 'decided' ||
pendingSibling?.[1] !== 'requested'
) {
throw new Error(
`Sibling-approval crash did not land in the approval gap: ${siblingActionRows}`,
);
}
siblingApprovalId = pendingSibling![0];
siblingApprovalOptions = JSON.parse(
runMysql(
mysqlPort,
`SELECT options_json FROM qwen_managed_agent.ma_session_record WHERE ${sessionFilter} AND record_kind='ACTION' AND action_id=${sqlString(siblingApprovalId)}`,
),
);
const siblingPlan = JSON.parse(
runMysql(
mysqlPort,
`SELECT CONVERT(inline_bytes USING utf8mb4) FROM qwen_managed_agent.ma_resource WHERE tenant_id=${sqlString(tenant)} AND session_id=${sqlString(session.id)} AND resource_id=${sqlString(siblingApprovalOptions!.continuationRef.resourceId)}`,
),
) as SavedApprovalPlan;
if (
siblingPlan.stage !== 'approval' ||
siblingPlan.approvalOrdinal !== 1 ||
siblingPlan.calls?.length !== 2 ||
siblingPlan.actionId !== siblingApprovalId
) {
throw new Error(
`Sibling-approval crash plan is not the pending second sibling: ${JSON.stringify(siblingPlan)}`,
);
}
// The per-call Action ID may be null before the crash, so anchor each
// member to the first approval's saved call identities and reject any
// Action ID that is already present on the wrong member.
const expectedActionIds = [originalApprovalId!, siblingApprovalId!];
for (const [
ordinal,
originalCall,
] of originalApprovalPlan!.calls.entries()) {
const siblingCall = siblingPlan.calls[ordinal];
if (
!siblingCall ||
siblingCall.runtimeCallId !== originalCall.runtimeCallId ||
siblingCall.partIndex !== originalCall.partIndex ||
siblingCall.prepareKey !== originalCall.prepareKey ||
siblingCall.requestDigest !== originalCall.requestDigest ||
(siblingCall.actionId != null &&
siblingCall.actionId !== expectedActionIds[ordinal])
) {
throw new Error(
`Sibling-approval crash plan changed call/action binding at ordinal ${ordinal}: ${JSON.stringify(siblingCall)} != ${JSON.stringify(originalCall)}`,
);
}
}
saveEvidence('sibling-approval-barrier.json', {
fault: approvalFault,
decidedAction: decidedSibling,
pendingAction: pendingSibling,
approvalOrdinal: siblingPlan.approvalOrdinal,
calls: siblingPlan.calls,
});
} else {
// The enclosing condition lists every crash-type fault. A new one
// added without a barrier here must fail loudly instead of
// reaching SIGKILL with no wait at all.
throw new Error(
`Unhandled approval fault barrier: ${String(approvalFault)}`,
);
}
const executionAtCrash = runMysql(
mysqlPort,
'SELECT execution_state, dispatch_generation FROM qwen_managed_agent.ma_tool_execution',
);
if (
approvalFault === 'finished-before-commit' &&
executionAtCrash !== 'SETTLED\t1'
) {
// The crash must land after the approved result settled and its
// post-tool round reached the window, not before. Assert the durable
// precondition from MySQL like the sibling crash arms, rather than
// trusting the hook's log line alone.
throw new Error(
`Finished-before-commit crash did not land after the approved result settled: executionAtCrash=${executionAtCrash}`,
);
}
// The settled execution alone does not prove the crash landed inside
// B3a: the post-tool round could have been held before its durable
// Finished marker committed. Read the window from the same journal the
// recovery reads and retain the attempt identity as evidence.
const finishedWindow =
approvalFault === 'finished-before-commit'
? finishedBeforeCommitWindow(
mysqlPort,
sessionFilter,
runMysql(
mysqlPort,
`SELECT prompt_id FROM qwen_managed_agent.ma_turn WHERE ${sessionFilter} ORDER BY gmt_create DESC LIMIT 1`,
),
)
: undefined;
const publishedPrefix =
approvalFault === 'after-result' ||
approvalFault === 'after-earlier-tools'
? await publicTranscript(replacementSpringUrl, tenant, session.id)
: undefined;
publishedPrefixSequences = (publishedPrefix?.events ?? [])
.filter(
(event) =>
event.type === 'item.output_text.delta' &&
eventText(event).includes(continuationPartial),
)
.map((event) => event.sequence);
saveEvidence('approval-crash-barrier.json', {
fault: approvalFault,
crashedHarnessBootId: crashedBootId,
executionAtCrash,
finishedBeforeCommitWindow: finishedWindow,
heldExecutionStartPath: replacementBrokerProxy?.heldPath(),
releaseFailures: replacementBrokerProxy?.releaseFailureCount(),
turnStatusAtCrash:
approvalFault === 'cleanup-failed'
? runMysql(
mysqlPort,
`SELECT status FROM qwen_managed_agent.ma_turn WHERE ${sessionFilter} ORDER BY gmt_create DESC LIMIT 1`,
)
: undefined,
terminalFault: storeFaultProxy
? {
rejected: storeFaultProxy.terminalRejectCount(),
accepted: storeFaultProxy.terminalAcceptCount(),
acceptedAtBarrier: terminalAcceptsAtBarrier,
}
: undefined,
publishedPrefix:
approvalFault === 'after-result' ||
approvalFault === 'after-earlier-tools'
? {
sequences: publishedPrefixSequences,
text: publishedPrefix?.text,
}
: undefined,
});
await crashChild(
replacementHarness.child,
'Replacement Hosted Harness (fault barrier)',
);
if (
approvalFault === 'after-result' ||
approvalFault === 'after-earlier-tools'
) {
releaseApprovalResultHold();
approvalResultHold = undefined;
}
// C1/C2's doomed generation must not land or deliver the terminal after
// the injected fault; the replacement may, including a replayed commit
// for C1.
if (
approvalFault === 'terminal-reply-lost' ||
approvalFault === 'terminal-write-failed'
) {
storeFaultProxy?.allowTerminal();
}
if (replacementBrokerProxy) {
if (approvalFault === 'cleanup-failed') {
// Keep the proxy so the replacement's successful release of the
// original identity is witnessed on the same fault boundary.
replacementBrokerProxy.allowRelease();
} else {
await replacementBrokerProxy.close();
replacementBrokerProxy = undefined;
replacementBrokerUrl = `http://127.0.0.1:${brokerPort}`;
}
}
// The crashed generation's disk is scrapped like every replaced
// Harness home; the fresh one must recover from MySQL alone.
rmSync(replacementHarnessHome, { recursive: true, force: true });
mkdirSync(path.join(replacementHarnessHome, '.qwen'), {
recursive: true,
});
writeFileSync(
path.join(replacementHarnessHome, '.qwen', 'settings.json'),
JSON.stringify({ ui: { enableFollowupSuggestions: false } }),
{ mode: 0o600 },
);
await waitUntil(
'Post-fault writer lease expiry',
(remainingMs) =>
runMysql(
mysqlPort,
`SELECT IF(writer_lease_until IS NULL OR writer_lease_until < (${databaseEpochMillisSql}), 1, 0) FROM qwen_managed_agent.ma_session_head WHERE ${sessionFilter}`,
remainingMs,
) === '1',
10_000,
);
replacementHarness = await startReplacementHarness(
'Post-fault recovery Harness',
);
if (approvalFault === 'cleanup-failed') {
// C3 has no Turn left to adopt: the replacement's first contact is
// the cleanup-only load the debt scan issues, and it must not
// rebind the Session's turn identity. The release it performs on
// the still-open proxy is the proof that the retry reached it.
await waitUntil(
'Post-fault cleanup retry reached the replacement',
() => (replacementBrokerProxy?.releaseSuccessCount() ?? 0) > 0,
120_000,
replacementHarness,
);
} else {
await waitUntil(
'Post-fault recovery Harness adopted the Session',
() => {
const boot = runMysql(
mysqlPort,
`SELECT harness_boot_id FROM qwen_managed_agent.ma_session WHERE ${sessionFilter}`,
);
return boot.length > 0 && boot !== crashedBootId;
},
120_000,
replacementHarness,
);
}
}
if (approvalFault === 'sibling-approval') {
// The recovery generation must re-hang the ORIGINAL pending sibling
// Action (same ID, same deadline) and still require its own decision:
// it must not auto-allow sibling[1] from sibling[0]'s earlier Allow,
// nor re-ask the already-decided sibling[0]. Assert the preserved
// identity/deadline before answering, then decide the pending sibling.
// Read the decided sibling HERE, before answering the pending one: a
// recovery that resumes at the wrong ordinal and re-asks sibling[0]
// would flip it back to requested, and a post-terminal check could not
// tell "never touched" from "re-decided" once the state reconverges.
const decidedBefore = await fetchJson<Record<string, unknown>>(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}/actions/${originalApprovalId}`,
{ headers: tenantHeaders(tenant) },
);
if (
decidedBefore.id !== originalApprovalId ||
decidedBefore.state !== 'decided' ||
decidedBefore.created_at !== originalApprovalOptions!.createdAt ||
decidedBefore.expires_at !== originalApprovalOptions!.expiresAt
) {
throw new Error(
`Recovery re-touched the decided sibling Action/deadline: ${JSON.stringify(decidedBefore)}`,
);
}
const siblingBefore = await fetchJson<Record<string, unknown>>(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}/actions/${siblingApprovalId}`,
{ headers: tenantHeaders(tenant) },
);
if (
siblingBefore.id !== siblingApprovalId ||
siblingBefore.state !== 'requested' ||
siblingBefore.created_at !== siblingApprovalOptions!.createdAt ||
siblingBefore.expires_at !== siblingApprovalOptions!.expiresAt
) {
throw new Error(
`Recovery did not preserve the pending sibling Action/deadline: ${JSON.stringify(siblingBefore)}`,
);
}
const siblingResponse = await fetch(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}/actions/${siblingApprovalId}/responses`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
'idempotency-key': `g3-answer-${siblingApprovalId}`,
...tenantHeaders(tenant),
},
body: JSON.stringify({
kind: 'permission',
input_revision: siblingApprovalOptions!.inputRevision,
policy_revision: siblingApprovalOptions!.policyRevision,
option_id: 'allow',
}),
},
);
if (siblingResponse.status !== 202)
throw new Error(
`Pending sibling response returned ${siblingResponse.status}: ${await siblingResponse.text()}`,
);
}
const recovered = await waitForTerminal(
replacementSpringUrl,
tenant,
session.id,
0,
replacementSpring,
120_000,
);
const terminals = recovered.events.filter((event) => event.terminal);
const expectedTerminal = ['allow', 'deny'].includes(verificationMode)
? 'turn.completed'
: 'turn.cancelled';
if (terminals.length !== 1 || terminals[0].type !== expectedTerminal) {
throw new Error(
`Original approval terminal differs: expected=${expectedTerminal} actual=${JSON.stringify(terminals)}`,
);
}
verifiedOriginalTerminal = terminals[0].type;
// A terminal is not the answer. The surviving public transcript must
// still carry the one model response the recovered Turn produced, or
// a compensation that only projects a terminal would pass. The
// result-commit arm already owns the same check with its retraction
// diagnostic, so it keeps that message.
if (
verificationMode === 'allow' &&
approvalFault !== 'after-result' &&
approvalFault !== 'after-earlier-tools'
) {
const transcript = await publicTranscript(
replacementSpringUrl,
tenant,
session.id,
);
if (transcript.text !== continuationResponse)
throw new Error(
`Approval recovery answer differs: ${JSON.stringify(transcript.text)}`,
);
}
if (
approvalFault === 'after-result' ||
approvalFault === 'after-earlier-tools'
) {
// The reissue must retract the held prefix: only the recovered
// answer may remain in the public transcript.
const transcript = await publicTranscript(
replacementSpringUrl,
tenant,
session.id,
);
if (transcript.text !== continuationResponse)
throw new Error(
`Result-commit recovery left ${JSON.stringify(transcript.text)}, not the recovered answer`,
);
if (
publishedPrefixSequences.length === 0 ||
!reconciledBetween(
transcript.events,
Math.min(...publishedPrefixSequences),
Math.min(
...deltaSequences(transcript.events, continuationResponse),
),
)
)
throw new Error(
'Result-commit recovery did not reconcile the published prefix before the recovered answer',
);
}
const rows = runMysql(
mysqlPort,
`SELECT execution_call_id, idempotency_key, binding_id, runtime_generation, runtime_session_id, execution_state, dispatch_generation, request_digest, settled_at FROM qwen_managed_agent.ma_tool_execution ORDER BY execution_call_id`,
);
const executions = rows
? rows.split('\n').map((row) => row.split('\t'))
: [];
const expectedExecutions =
verificationMode === 'allow'
? approvalFault === 'sibling-approval' ||
approvalFault === 'after-earlier-tools'
? 2
: 1
: 0;
if (executions.length !== expectedExecutions)
throw new Error(
`Approval execution count ${executions.length} differs from ${expectedExecutions}`,
);
// Every approved call keeps its own original prepare key/digest and the
// original Runtime identity, settled once. Match by prepare key so the
// model's call order is checked, not the SQL row order; the A3 arm has
// two siblings and B3 two earlier rounds, every other allow arm has one.
if (approvalFault === 'after-earlier-tools') {
// B3's two earlier tools live in separate rounds, so the first approval
// plan carries only tool A. Assert both executions' Runtime identity and
// settled-once directly; the two physical file effects give per-tool
// evidence, and a re-run of either earlier tool reds the count above.
for (const [ordinal, execution] of executions.entries()) {
if (
execution[2] !== originalApprovalPlan!.runtime.bindingId ||
execution[3] !== originalApprovalPlan!.runtime.generation ||
execution[4] !== originalApprovalPlan!.runtime.runtimeSessionId ||
execution[5] !== 'SETTLED' ||
execution[6] !== '1'
) {
throw new Error(
`B3 earlier execution ${ordinal} changed its identity or re-ran: ${execution.join(',')}`,
);
}
}
} else if (verificationMode === 'allow') {
const plan = originalApprovalPlan!;
const settledAtByOrdinal: number[] = [];
for (const [ordinal, call] of plan.calls.entries()) {
const execution = executions.find(
(row) => row[1] === call.prepareKey,
);
if (
!execution ||
execution[2] !== plan.runtime.bindingId ||
execution[3] !== plan.runtime.generation ||
execution[4] !== plan.runtime.runtimeSessionId ||
execution[5] !== 'SETTLED' ||
execution[6] !== '1' ||
execution[7] !== call.requestDigest
) {
throw new Error(
`Replacement changed the original prepared execution identity for call ${ordinal}: ${(execution ?? []).join(',')}`,
);
}
settledAtByOrdinal.push(Number(execution[8]));
}
// The siblings must SETTLE in plan order, not merely exist with the
// right identities: a recovery that dispatches the batch in reverse
// keeps every identity assertion green. settled_at is epoch millis;
// require it non-decreasing along the plan so a reverse dispatch reds.
for (
let ordinal = 1;
ordinal < settledAtByOrdinal.length;
ordinal += 1
) {
if (
!Number.isFinite(settledAtByOrdinal[ordinal]) ||
settledAtByOrdinal[ordinal] < settledAtByOrdinal[ordinal - 1]
) {
throw new Error(
`Replacement settled the siblings out of plan order: settledAt=${settledAtByOrdinal.join(',')}`,
);
}
}
}
const bytes = existsSync(inflightSideEffect)
? readFileSync(inflightSideEffect, 'utf8')
: '';
if (
verificationMode === 'allow'
? bytes !== inflightSideEffectContent
: existsSync(inflightSideEffect)
) {
throw new Error(
`Approval physical file assertion failed for ${verificationMode}`,
);
}
if (approvalFault === 'sibling-approval') {
const siblingBytes = existsSync(siblingSideEffect)
? readFileSync(siblingSideEffect, 'utf8')
: '';
if (siblingBytes !== siblingSideEffectContent)
throw new Error(
`Sibling physical file assertion failed: ${JSON.stringify(siblingBytes)}`,
);
}
if (approvalFault === 'after-earlier-tools') {
const b3Bytes = existsSync(b3SecondSideEffect)
? readFileSync(b3SecondSideEffect, 'utf8')
: '';
if (b3Bytes !== b3SecondSideEffectContent)
throw new Error(
`B3 second physical file assertion failed: ${JSON.stringify(b3Bytes)}`,
);
// Round + cost ledger continuity: recovery must resume the saved round
// and restore the budget snapshot / token ledger, not restart them.
if (!b3Before)
throw new Error('B3 crash-boundary round/budget was not captured');
const b3After = b3RoundAndBudget(mysqlPort, sessionFilter);
saveEvidence('b3-round-budget-after.json', b3After);
if (b3After.round !== b3Before.round || b3After.round < 2)
throw new Error(
`B3 recovery reset the logical round: before=${b3Before.round} after=${b3After.round}`,
);
assertB3CostContinuity(b3Before, b3After);
}
const actionRows = runMysql(
mysqlPort,
`SELECT action_id, state FROM qwen_managed_agent.ma_session_record WHERE ${sessionFilter} AND record_kind='ACTION' ORDER BY action_id`,
);
const expectedActionState =
verificationMode === 'expiry'
? 'expired'
: verificationMode === 'cancel'
? 'cancelled'
: 'decided';
if (approvalFault === 'sibling-approval') {
// Both siblings end decided under their ORIGINAL Action IDs: the
// decided one was not re-asked, the pending one earned its own Allow.
const expectedSiblingActions = [
`${originalApprovalId}\t${expectedActionState}`,
`${siblingApprovalId}\t${expectedActionState}`,
].sort();
if (actionRows !== expectedSiblingActions.join('\n'))
throw new Error(
`Sibling approval identities/states differ: ${actionRows}`,
);
} else if (approvalFault === 'after-earlier-tools') {
// Both earlier tools end decided under their ORIGINAL Action IDs: A by
// the first Allow, B by the runner's second Allow. Recovery must resume
// the saved model round, not re-ask either approval.
const expectedB3Actions = [
`${originalApprovalId}\t${expectedActionState}`,
`${secondApprovalId}\t${expectedActionState}`,
].sort();
if (actionRows !== expectedB3Actions.join('\n'))
throw new Error(
`B3 approval identities/states differ: ${actionRows}`,
);
} else if (
actionRows !== `${originalApprovalId}\t${expectedActionState}`
) {
throw new Error(
`Original approval identity/state differs: ${actionRows}`,
);
}
// State alone proves a decision was committed, not which option won.
// Read the committed decision resource back for the browser-facing
// intent, and hold it to the revision and policy the browser answered.
const decisionOption =
verificationMode === 'allow' || verificationMode === 'deny'
? JSON.parse(
runMysql(
mysqlPort,
`SELECT CONVERT(r.inline_bytes USING utf8mb4) FROM qwen_managed_agent.ma_resource_ref rr JOIN qwen_managed_agent.ma_resource r ON r.resource_key = rr.resource_key WHERE rr.owner_kind='SESSION_RECORD' AND rr.owner_key = (SELECT record_key FROM qwen_managed_agent.ma_session_record WHERE tenant_id=${sqlString(tenant)} AND session_id=${sqlString(session.id)} AND record_kind='ACTION' AND action_id=${sqlString(originalApprovalId!)}) AND r.kind='managed-action-decision'`,
),
)
: undefined;
if (
decisionOption &&
(decisionOption.optionId !== verificationMode ||
decisionOption.inputRevision !==
originalApprovalOptions!.inputRevision ||
decisionOption.policyRevision !==
originalApprovalOptions!.policyRevision)
) {
throw new Error(
`Stored approval decision differs from the chosen option: ${JSON.stringify(decisionOption)}`,
);
}
if (approvalFault === 'sibling-approval') {
// The pending sibling carries its OWN Allow decision bound to its own
// revisions — it was not auto-allowed from sibling[0]'s earlier Allow.
const siblingDecision = JSON.parse(
runMysql(
mysqlPort,
`SELECT CONVERT(r.inline_bytes USING utf8mb4) FROM qwen_managed_agent.ma_resource_ref rr JOIN qwen_managed_agent.ma_resource r ON r.resource_key = rr.resource_key WHERE rr.owner_kind='SESSION_RECORD' AND rr.owner_key = (SELECT record_key FROM qwen_managed_agent.ma_session_record WHERE tenant_id=${sqlString(tenant)} AND session_id=${sqlString(session.id)} AND record_kind='ACTION' AND action_id=${sqlString(siblingApprovalId!)}) AND r.kind='managed-action-decision'`,
),
);
if (
siblingDecision.optionId !== 'allow' ||
siblingDecision.inputRevision !==
siblingApprovalOptions!.inputRevision ||
siblingDecision.policyRevision !==
siblingApprovalOptions!.policyRevision
) {
throw new Error(
`Stored sibling decision differs from the chosen option: ${JSON.stringify(siblingDecision)}`,
);
}
// Clause 2: the recovered final plan preserves the saved sibling
// order and identities, and each sibling keeps its original Action
// ID. Compare by position so a reordered or dropped sibling reds
// here, not only in the execution count.
const finalPlan = siblingFinalPlan(
mysqlPort,
sessionFilter,
originalApprovalPlan!.batchId,
);
const savedCalls = originalApprovalPlan!.calls;
if (finalPlan.calls.length !== savedCalls.length)
throw new Error(
`Sibling recovery changed the batch size: ${finalPlan.calls.length} != ${savedCalls.length}`,
);
for (const [ordinal, saved] of savedCalls.entries()) {
const recovered = finalPlan.calls[ordinal];
if (
recovered.runtimeCallId !== saved.runtimeCallId ||
recovered.partIndex !== saved.partIndex ||
recovered.prepareKey !== saved.prepareKey ||
recovered.requestDigest !== saved.requestDigest
) {
throw new Error(
`Sibling recovery reordered or rewrote call ${ordinal}: ${JSON.stringify(recovered)} != ${JSON.stringify(saved)}`,
);
}
}
if (
finalPlan.calls[0].actionId !== originalApprovalId ||
finalPlan.calls[1].actionId !== siblingApprovalId
) {
throw new Error(
`Sibling recovery changed the original Action IDs: ${JSON.stringify(finalPlan.calls.map((call) => call.actionId))}`,
);
}
}
// Expiry may only close the wait at or after the deadline it was
// given; an early terminal would mean the replacement invented one.
const expiryTerminalAt =
verificationMode === 'expiry'
? Number(
runMysql(
mysqlPort,
`SELECT gmt_create FROM qwen_managed_agent.ma_public_event WHERE ${sessionFilter} AND stream_kind='SESSION' AND stream_id=X'' AND is_terminal=TRUE ORDER BY sequence_id DESC LIMIT 1`,
),
)
: undefined;
if (
expiryTerminalAt !== undefined &&
(!Number.isSafeInteger(expiryTerminalAt) ||
expiryTerminalAt < originalApprovalOptions!.expiresAt)
) {
throw new Error(
`Expiry closed before the original deadline: terminalAt=${expiryTerminalAt} expiresAt=${originalApprovalOptions!.expiresAt}`,
);
}
const requestBodies = (fake?.requests ?? []).filter((request) =>
JSON.stringify(request.body.messages).includes(continuationMarker),
);
const initialRequests = requestBodies.filter(
(request) =>
!JSON.stringify(request.body.messages).includes('"role":"tool"'),
);
const modelAfterResults = requestBodies.filter((request) =>
JSON.stringify(request.body.messages).includes('"role":"tool"'),
);
const expectedModelsAfterResults =
approvalFault === 'after-earlier-tools'
? 3
: approvalFault === 'after-result' ||
approvalFault === 'finished-before-commit'
? 2
: ['allow', 'deny'].includes(verificationMode)
? 1
: 0;
if (
initialRequests.length !== 1 ||
modelAfterResults.length !== expectedModelsAfterResults
)
throw new Error(
`Approval replayed inference or skipped result inference: ${initialRequests.length}+${modelAfterResults.length}`,
);
if (
approvalFault === 'after-result' ||
approvalFault === 'finished-before-commit'
) {
const parity = isDeepStrictEqual(
modelAfterResults[0]?.body,
modelAfterResults[1]?.body,
);
saveEvidence('model-request-parity.json', {
requests: modelAfterResults.map((request) => request.body),
equal: parity,
});
if (!parity)
throw new Error(
'Result-commit recovery changed the saved model request',
);
}
if (approvalFault === 'after-earlier-tools') {
// The held round-2 request (index 1) must be reissued deep-equal by the
// recovery generation (index 2); index 0 is the earlier round-1 request.
const parity = isDeepStrictEqual(
modelAfterResults[1]?.body,
modelAfterResults[2]?.body,
);
saveEvidence('model-request-parity.json', {
requests: modelAfterResults.map((request) => request.body),
equal: parity,
});
if (!parity)
throw new Error(
'B3 recovery changed the saved later-round model request',
);
}
if (responseOperation) {
await waitUntil(
'Approval response command completion',
async () => {
const operation = await fetchJson<Record<string, unknown>>(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}/operations/${responseOperation!.id}`,
{ headers: tenantHeaders(tenant) },
);
if (operation.status === 'failed')
throw new Error(
`Approval response operation failed: ${JSON.stringify(operation)}`,
);
return operation.status === 'completed';
},
30_000,
replacementSpring,
);
}
await waitUntil(
'Original Runtime cleanup confirmed',
() => {
const state = runMysql(
mysqlPort,
`SELECT session_state FROM qwen_managed_agent.ma_runtime_session WHERE runtime_session_id=${sqlString(originalRuntimeSessionId!)}`,
);
const confirmed = runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_session_journal_tx WHERE ${sessionFilter} AND command_id=${sqlString(`hosted-cleanup:${originalRuntimeSessionId}:${originalRuntimeSessionId}:confirmed`)}`,
);
return state === 'RELEASED' && confirmed === '1';
},
30_000,
replacementHarness,
);
const runtimeAfter = runMysql(
mysqlPort,
`SELECT binding_id, runtime_generation, workspace_generation, session_state FROM qwen_managed_agent.ma_runtime_session WHERE runtime_session_id=${sqlString(originalRuntimeSessionId!)}`,
).split('\t');
const cleanupConfirmed = Number(
runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_session_journal_tx WHERE ${sessionFilter} AND command_id=${sqlString(`hosted-cleanup:${originalRuntimeSessionId}:${originalRuntimeSessionId}:confirmed`)}`,
),
);
if (
runtimeAfter[0] !== originalApprovalPlan!.runtime.bindingId ||
runtimeAfter[1] !== originalApprovalPlan!.runtime.generation ||
runtimeAfter[2] !== originalApprovalPlan!.runtime.workspaceGeneration ||
runtimeAfter[3] !== 'RELEASED' ||
cleanupConfirmed !== 1
)
throw new Error(
`Original cleanup did not release the original Runtime: state=${runtimeAfter.join(',')} confirmed=${cleanupConfirmed}`,
);
if (approvalFault === 'terminal-reply-lost') {
if (
storeFaultProxy?.terminalFaulted() !== true ||
terminalAcceptsAtBarrier < 1
)
throw new Error(
`Terminal reply loss did not commit the terminal before recovery: barrierAccepts=${terminalAcceptsAtBarrier} observations=${storeFaultProxy?.observations().join(' | ')}`,
);
}
if (approvalFault === 'terminal-write-failed') {
if (
(storeFaultProxy?.terminalRejectCount() ?? 0) < 1 ||
(storeFaultProxy?.terminalAcceptCount() ?? 0) < 1
)
throw new Error(
`Terminal write fault was not compensated: reject=${storeFaultProxy?.terminalRejectCount()} accept=${storeFaultProxy?.terminalAcceptCount()} observations=${storeFaultProxy?.observations().join(' | ')}`,
);
}
if (approvalFault === 'cleanup-failed') {
const failures = replacementBrokerProxy?.releaseFailureCount() ?? 0;
const successes = replacementBrokerProxy?.releaseSuccessCount() ?? 0;
// The debt projection is the scan's input; a confirmed cleanup
// removes it. A surviving row here means only the Turn's own driver
// (or an operator) could still pay it — exactly the gap this arm
// exists to keep closed.
const debt = Number(
runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_resource_ref WHERE ${sessionFilter} AND owner_kind='HOSTED_CLEANUP'`,
),
);
if (failures < 1 || successes < 1 || debt !== 0)
throw new Error(
`Cleanup fault was not retried after the replacement: failures=${failures} successes=${successes} debt=${debt}`,
);
}
const faultObservations = {
releaseFailures: replacementBrokerProxy?.releaseFailureCount() ?? 0,
releaseSuccesses: replacementBrokerProxy?.releaseSuccessCount() ?? 0,
terminalRejects: storeFaultProxy?.terminalRejectCount() ?? 0,
terminalAccepts: storeFaultProxy?.terminalAcceptCount() ?? 0,
terminalAcceptsAtBarrier,
postCrashTerminalAccepts: Math.max(
0,
(storeFaultProxy?.terminalAcceptCount() ?? 0) -
terminalAcceptsAtBarrier,
),
};
originalFaultObservations = faultObservations;
saveEvidence('approval-result.json', {
mode: verificationMode,
fault: approvalFault ?? null,
faultObservations,
sessionId: session.id,
actionId: originalApprovalId,
replacementActionBeforeResponse: actionBeforeResponse,
chosenOption: decisionOption?.optionId,
savedDeadline: {
createdAt: originalApprovalOptions!.createdAt,
expiresAt: originalApprovalOptions!.expiresAt,
},
recoveredDeadline: {
createdAt: optionsAfterReplacement.createdAt,
expiresAt: optionsAfterReplacement.expiresAt,
},
expiryTerminalAt: expiryTerminalAt ?? null,
expectedTerminal,
terminalCount: terminals.length,
executions,
physicalFileExists: existsSync(inflightSideEffect),
physicalFileBytes: bytes,
modelRequests: initialRequests.length + modelAfterResults.length,
initialRequests: initialRequests.length,
modelAfterResults: modelAfterResults.length,
runtimeAfter,
cleanupConfirmed,
});
approvalResultJson = JSON.stringify(
{
mode: verificationMode,
originalAction: originalApprovalId,
originalBatch: originalApprovalPlan!.batchId,
originalBinding: originalApprovalPlan!.runtime.bindingId,
executions: executions.length,
terminal: expectedTerminal,
cleanupConfirmed,
},
null,
2,
);
} else if (inflightFailover) {
const recoveredTurn = await waitForTerminal(
replacementSpringUrl,
tenant,
session.id,
0,
replacementSpring,
30_000,
);
const recoveredTerminal = recoveredTurn.events.find(
(event) => event.terminal,
);
const recoveredExecution = runMysql(
mysqlPort,
'SELECT execution_call_id, execution_state, dispatch_generation, IF(result_json IS NULL, 0, 1), runtime_session_id FROM qwen_managed_agent.ma_tool_execution',
).split('\t');
const executionCount = Number(
runMysql(
mysqlPort,
'SELECT COUNT(*) FROM qwen_managed_agent.ma_tool_execution',
),
);
const replacementBootId = runMysql(
mysqlPort,
`SELECT harness_boot_id FROM qwen_managed_agent.ma_session WHERE ${sessionFilter}`,
);
const replacementHead = runMysql(
mysqlPort,
`SELECT writer_generation, journal_revision, committed_sequence FROM qwen_managed_agent.ma_session_head WHERE ${sessionFilter}`,
)
.split('\t')
.map(Number);
const terminalCount = Number(
runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_public_event WHERE ${sessionFilter} AND stream_kind='SESSION' AND stream_id=X'' AND is_terminal=TRUE`,
),
);
const requests = (fake?.requests ?? []).filter(({ body }) =>
JSON.stringify(body['messages']).includes(inflightMarker),
);
const initialModelRequests = requests.filter(
({ body }) =>
!JSON.stringify(body['messages']).includes('"role":"tool"'),
);
const continuationRequests = requests.filter(({ body }) =>
JSON.stringify(body['messages']).includes('"role":"tool"'),
);
const sideEffectBytes = existsSync(inflightSideEffect)
? readFileSync(inflightSideEffect, 'utf8')
: '';
if (
recoveredTerminal?.type !== 'turn.completed' ||
recoveredExecution.length !== 5 ||
recoveredExecution[0] !== originalExecutionCallId ||
recoveredExecution[1] !== 'SETTLED' ||
// A replaced Broker bumps the re-dispatch to generation 1; a live
// Broker re-dispatches on the generation it already owned (0 or 1).
(harnessOnly
? !(recoveredExecution[2] === '0' || recoveredExecution[2] === '1')
: Number(recoveredExecution[2]) !== 1) ||
// Under --harness-only the surviving Spring, Broker and durable
// Worker keep serving the same runtime session; re-provisioning
// would redefine the arm as a kill-both.
(harnessOnly && recoveredExecution[4] !== originalRuntimeSessionId) ||
// The equality above cannot fail (the column settles at INSERT):
// "kept serving" is witnessed by the row's own liveness moving.
(harnessOnly &&
!(
firstRuntimeHeartbeat !== undefined &&
(runtimeSessionHeartbeat(recoveredExecution[4])[1] >
firstRuntimeHeartbeat[1] ||
runtimeSessionHeartbeat(recoveredExecution[4])[0] >
firstRuntimeHeartbeat[0])
)) ||
recoveredExecution[3] !== '1' ||
executionCount !== 1 ||
replacementBootId.length === 0 ||
replacementBootId === firstBootId ||
replacementHead.length !== 3 ||
replacementHead[0] <= firstHead[0] ||
replacementHead[1] <= firstHead[1] ||
replacementHead[2] <= firstHead[2] ||
terminalCount !== 1 ||
initialModelRequests.length !== 1 ||
continuationRequests.length !== 1 ||
sideEffectBytes !== inflightSideEffectContent
) {
throw new Error(
`In-flight failover audit failed: terminal=${recoveredTerminal?.type ?? 'missing'} terminalData=${JSON.stringify(recoveredTerminal?.data)} execution=${recoveredExecution.join(',')} rows=${executionCount} boot=${firstBootId}->${replacementBootId} head=${firstHead.join(',')}->${replacementHead.join(',')} terminals=${terminalCount} model=${initialModelRequests.length}+${continuationRequests.length} sideEffect=${JSON.stringify(sideEffectBytes)}`,
);
}
resultJson = JSON.stringify(
{
sessionId: session.id,
executionCallId: originalExecutionCallId,
executionState: recoveredExecution[1],
dispatchGeneration: Number(recoveredExecution[2]),
firstHarnessBootId: firstBootId,
replacementHarnessBootId: replacementBootId,
writerGeneration: `${firstHead[0]} -> ${replacementHead[0]}`,
journalRevision: `${firstHead[1]} -> ${replacementHead[1]}`,
committedSequence: `${firstHead[2]} -> ${replacementHead[2]}`,
promptReplayed: false,
physicalToolExecutions: 1,
terminalTurns: terminalCount,
oldHarnessDiskDeleted: !existsSync(harnessHome),
},
null,
2,
);
} else if (continuationFailover) {
await waitForTerminal(
replacementSpringUrl,
tenant,
session.id,
0,
replacementSpring,
120_000,
);
const finalEvents = await fetchJson<PublicList<PublicEvent>>(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}/events?after=0&limit=100`,
{ headers: tenantHeaders(tenant) },
);
const recoveredTerminal = finalEvents.data.find(
(event) => event.terminal,
);
const textDeltas = finalEvents.data.filter(
(event) => event.type === 'item.output_text.delta',
);
const visibleText = textDeltas.map((event) => eventText(event)).join('');
const recoveredExecution = runMysql(
mysqlPort,
'SELECT execution_call_id, execution_state, dispatch_generation, IF(result_json IS NULL, 0, 1), runtime_session_id FROM qwen_managed_agent.ma_tool_execution',
).split('\t');
const executionCount = Number(
runMysql(
mysqlPort,
'SELECT COUNT(*) FROM qwen_managed_agent.ma_tool_execution',
),
);
const replacementBootId = runMysql(
mysqlPort,
`SELECT harness_boot_id FROM qwen_managed_agent.ma_session WHERE ${sessionFilter}`,
);
const terminalCount = Number(
runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_public_event WHERE ${sessionFilter} AND stream_kind='SESSION' AND stream_id=X'' AND is_terminal=TRUE`,
),
);
const requests = (fake?.requests ?? []).filter(({ body }) =>
JSON.stringify(body['messages']).includes(continuationMarker),
);
const originalAndReplacementRequests = requests.filter(({ body }) =>
JSON.stringify(body['messages']).includes('"role":"tool"'),
);
if (
originalAndReplacementRequests.length === 2 &&
!isDeepStrictEqual(
originalAndReplacementRequests[0].body,
originalAndReplacementRequests[1].body,
)
) {
throw new Error(
'The replacement model request does not equal the original effective request',
);
}
saveEvidence('model-request-parity.json', {
requests: originalAndReplacementRequests.map((request) => request.body),
equal:
originalAndReplacementRequests.length === 2 &&
isDeepStrictEqual(
originalAndReplacementRequests[0].body,
originalAndReplacementRequests[1].body,
),
});
const initialModelRequests = requests.filter(
({ body }) =>
!JSON.stringify(body['messages']).includes('"role":"tool"'),
);
const continuationRequests = requests.filter(({ body }) =>
JSON.stringify(body['messages']).includes('"role":"tool"'),
);
const sideEffectBytes = existsSync(inflightSideEffect)
? readFileSync(inflightSideEffect, 'utf8')
: '';
if (
recoveredTerminal?.type !== 'turn.completed' ||
visibleText !== continuationResponse ||
visibleText.includes(continuationPartial) ||
recoveredExecution.length !== 5 ||
recoveredExecution[0] !== originalExecutionCallId ||
recoveredExecution[1] !== 'SETTLED' ||
// See the in-flight arm: a live Broker keeps the generation it owns.
(harnessOnly
? !(recoveredExecution[2] === '0' || recoveredExecution[2] === '1')
: Number(recoveredExecution[2]) !== 1) ||
// See the in-flight arm: the surviving owner keeps the durable
// runtime session; a new Worker is a kill-both in disguise.
(harnessOnly && recoveredExecution[4] !== originalRuntimeSessionId) ||
(harnessOnly &&
!(
firstRuntimeHeartbeat !== undefined &&
(runtimeSessionHeartbeat(recoveredExecution[4])[1] >
firstRuntimeHeartbeat[1] ||
runtimeSessionHeartbeat(recoveredExecution[4])[0] >
firstRuntimeHeartbeat[0])
)) ||
recoveredExecution[3] !== '1' ||
executionCount !== 1 ||
replacementBootId.length === 0 ||
replacementBootId === firstBootId ||
terminalCount !== 1 ||
initialModelRequests.length !== 1 ||
continuationRequests.length !== 2 ||
sideEffectBytes !== inflightSideEffectContent
) {
throw new Error(
`Continuation failover audit failed: terminal=${recoveredTerminal?.type ?? 'missing'} text=${JSON.stringify(visibleText)} deltas=${JSON.stringify(textDeltas)} execution=${recoveredExecution.join(',')} rows=${executionCount} boot=${firstBootId}->${replacementBootId} terminals=${terminalCount} model=${initialModelRequests.length}+${continuationRequests.length} sideEffect=${JSON.stringify(sideEffectBytes)}`,
);
}
resultJson = JSON.stringify(
{
sessionId: session.id,
executionCallId: originalExecutionCallId,
firstHarnessBootId: firstBootId,
replacementHarnessBootId: replacementBootId,
promptReplayed: initialModelRequests.length !== 1,
physicalToolExecutions: executionCount,
continuationModelRequests: continuationRequests.length,
visibleText,
terminalTurns: terminalCount,
// The frozen-owner arm keeps the home on purpose (the wake
// needs it), so one key cannot mean both arms' intent.
oldHarnessDiskDeleted: !freeze && !existsSync(harnessHome),
harnessHomeRetainedForWake: freeze && existsSync(harnessHome),
},
null,
2,
);
if (freeze) {
// Wake the frozen Harness only after the replacement finished: the
// journal writer fence the takeover installed must hold against a
// very alive former writer. The Turn is terminal by now, so nothing
// but a fencing defect could mutate the binding or the journal. The
// resumed Harness still needs its home, so it must exist too.
if (!existsSync(harnessHome)) {
throw new Error(
'Frozen owner arm lost the Harness home the wake depends on',
);
}
const headBeforeWake = runMysql(
mysqlPort,
`SELECT writer_generation, journal_revision, committed_sequence FROM qwen_managed_agent.ma_session_head WHERE ${sessionFilter}`,
);
// The rest of the wake block proves the fence holds; this line
// proves there IS a fence: the head must sit on the replacement's
// writer generation, past the frozen owner's.
if (Number(headBeforeWake.split('\t')[0]) <= firstHead[0]) {
throw new Error(
`The takeover did not advance the journal head past the frozen owner's writer generation: head=${headBeforeWake} first=${firstHead.join(',')}`,
);
}
const oldGenerationTxBeforeWake = runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_session_journal_tx WHERE ${sessionFilter} AND writer_generation < (SELECT writer_generation FROM qwen_managed_agent.ma_session_head WHERE ${sessionFilter})`,
);
// Non-vacuity, proven live rather than by signal-existence: the
// frozen Harness must stop answering /health now and answer it
// again after SIGCONT, or the wake never happened and every
// fencing assertion below reports against nothing.
const frozenSilent = await fetch(
`http://127.0.0.1:${harnessPort}/health`,
{
headers: { authorization: `Bearer ${harnessToken}` },
signal: AbortSignal.timeout(1_000),
},
).then(
() => false,
() => true,
);
if (!frozenSilent) {
throw new Error(
'Frozen Harness still answers /health before SIGCONT; the freeze did not take effect',
);
}
if (!processTreeExists(harness.child)) {
throw new Error(
'Frozen Harness did not survive the freeze: the fencing proof below would be vacuous',
);
}
signalProcessTree(harness.child, 'SIGCONT');
let resumed = false;
for (let attempt = 0; attempt < 10 && !resumed; attempt += 1) {
resumed = await fetch(`http://127.0.0.1:${harnessPort}/health`, {
headers: { authorization: `Bearer ${harnessToken}` },
signal: AbortSignal.timeout(1_000),
}).then(
(response) => response.ok,
() => false,
);
if (!resumed)
await new Promise((resolve) => setTimeout(resolve, 200));
}
if (!resumed) {
throw new Error(
'Frozen Harness never resumed /health after SIGCONT; the wake never happened and the fencing assertions would be vacuous',
);
}
await new Promise((resolve) => setTimeout(resolve, 5_000));
const headAfterWake = runMysql(
mysqlPort,
`SELECT writer_generation, journal_revision, committed_sequence FROM qwen_managed_agent.ma_session_head WHERE ${sessionFilter}`,
);
const bootAfterWake = runMysql(
mysqlPort,
`SELECT harness_boot_id FROM qwen_managed_agent.ma_session WHERE ${sessionFilter}`,
);
// The fencing claim the exit check words: no transaction out of
// the old writer generation after it wakes. Lease renewal touches
// writer_lease_until only, so the head's revision moves with real
// commits; identity, not revision, is what a fencing proof may
// freeze on.
const oldGenerationTxAfterWake = runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_session_journal_tx WHERE ${sessionFilter} AND writer_generation < (SELECT writer_generation FROM qwen_managed_agent.ma_session_head WHERE ${sessionFilter})`,
);
const awakeEvents = await fetchJson<PublicList<PublicEvent>>(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}/events?after=0&limit=100`,
{ headers: tenantHeaders(tenant) },
);
const awakeText = awakeEvents.data
.filter((event) => event.type === 'item.output_text.delta')
.map((event) => eventText(event))
.join('');
const awakeTerminalCount = Number(
runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_public_event WHERE ${sessionFilter} AND stream_kind='SESSION' AND stream_id=X'' AND is_terminal=TRUE`,
),
);
if (
headAfterWake.split('\t')[0] !== headBeforeWake.split('\t')[0] ||
oldGenerationTxAfterWake !== oldGenerationTxBeforeWake ||
bootAfterWake !== replacementBootId ||
awakeText !== visibleText ||
awakeTerminalCount !== terminalCount
) {
throw new Error(
`Frozen former Harness mutated the takeover after waking: head=${headBeforeWake}->${headAfterWake} oldWriterTx=${oldGenerationTxBeforeWake}->${oldGenerationTxAfterWake} boot=${replacementBootId}->${bootAfterWake} text=${JSON.stringify(visibleText)}->${JSON.stringify(awakeText)} terminals=${terminalCount}->${awakeTerminalCount}`,
);
}
fencedWriterJson = JSON.stringify(
{
fencedFormerWriter: true,
journalHead: headAfterWake,
oldGenerationTx: oldGenerationTxAfterWake,
harnessBootId: bootAfterWake,
visibleText: awakeText,
},
null,
2,
);
}
} else if (modelRoundFailover) {
const terminalRows = () =>
Number(
runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_public_event WHERE ${sessionFilter} AND stream_kind='SESSION' AND stream_id=X'' AND is_terminal=TRUE`,
),
);
const currentBoot = () =>
runMysql(
mysqlPort,
`SELECT harness_boot_id FROM qwen_managed_agent.ma_session WHERE ${sessionFilter}`,
);
const currentHead = () =>
runMysql(
mysqlPort,
`SELECT writer_generation, journal_revision, committed_sequence FROM qwen_managed_agent.ma_session_head WHERE ${sessionFilter}`,
)
.split('\t')
.map(Number);
const boots = [firstBootId];
const heads = [firstHead];
let retractedBeforeNewDeltas = true;
let previousPrefixSequence = firstPrefixSequence;
// The terminal wait watches the live control plane and the Harness
// generation finishing the round, so either one's death fails it at
// once.
let finishingHarness = replacementHarness;
if (harnessOnly) {
// Generation two reissues the saved request: by the time its own
// prefix is public, generation one's must already be retracted.
await waitUntil(
'Second generation partial text',
async () =>
(
await publicTranscript(replacementSpringUrl, tenant, session.id)
).text.includes(modelRoundPartials[1]),
120_000,
replacementHarness,
);
const secondView = await publicTranscript(
replacementSpringUrl,
tenant,
session.id,
);
const secondPrefixSequence = Math.min(
...deltaSequences(secondView.events, modelRoundPartials[1]),
);
retractedBeforeNewDeltas =
secondView.text === modelRoundPartials[1] &&
reconciledBetween(
secondView.events,
previousPrefixSequence,
secondPrefixSequence,
);
previousPrefixSequence = secondPrefixSequence;
b2SecondPrefixSequence = secondPrefixSequence;
boots.push(currentBoot());
heads.push(currentHead());
await crashChild(
replacementHarness.child,
'Replacement Hosted Harness',
);
advanceModelRound();
// The second generation's disk is scrapped like the first one's.
rmSync(replacementHarnessHome, { recursive: true, force: true });
mkdirSync(path.join(replacementHarnessHome, '.qwen'), {
recursive: true,
});
writeFileSync(
path.join(replacementHarnessHome, '.qwen', 'settings.json'),
JSON.stringify({ ui: { enableFollowupSuggestions: false } }),
{ mode: 0o600 },
);
await waitUntil(
'Second generation writer lease expiry',
(remainingMs) =>
runMysql(
mysqlPort,
`SELECT IF(writer_lease_until IS NULL OR writer_lease_until < (${databaseEpochMillisSql}), 1, 0) FROM qwen_managed_agent.ma_session_head WHERE ${sessionFilter}`,
remainingMs,
) === '1',
10_000,
);
finishingHarness = await startReplacementHarness(
'Third Hosted Harness',
);
}
await waitForTerminal(
replacementSpringUrl,
tenant,
session.id,
0,
[replacementSpring, finishingHarness],
120_000,
);
const recovered = await publicTranscript(
replacementSpringUrl,
tenant,
session.id,
);
boots.push(currentBoot());
heads.push(currentHead());
retractedBeforeNewDeltas &&= reconciledBetween(
recovered.events,
previousPrefixSequence,
Math.min(...deltaSequences(recovered.events, modelRoundResponse)),
);
// Streaming requests only: the original round and one reissue per
// replaced generation, each the same saved request.
const roundRequests = (fake?.requests ?? []).filter(({ body }) => {
const messages = JSON.stringify(body['messages']);
return (
body['stream'] === true &&
messages.includes(modelRoundMarker) &&
!messages.includes(modelRoundNextMarker)
);
});
const executionCount = Number(
runMysql(
mysqlPort,
'SELECT COUNT(*) FROM qwen_managed_agent.ma_tool_execution',
),
);
const turnCount = Number(
runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_turn WHERE ${sessionFilter}`,
),
);
// B1: the replacement reissues the model request but must not admit
// the prompt again. One journal transaction under the Harness prompt
// id proves one admission and one user record; the admission resource
// is published once by that same original command. The Turn's public
// id and the prompt id are generated independently, so the journal is
// keyed by the latter.
const firstPromptId = runMysql(
mysqlPort,
`SELECT prompt_id FROM qwen_managed_agent.ma_turn WHERE ${sessionFilter} ORDER BY gmt_create LIMIT 1`,
);
const admissionCount = Number(
runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_session_journal_tx WHERE ${sessionFilter} AND command_id=${sqlString(firstPromptId)}`,
),
);
const admissionResources = Number(
runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_resource WHERE ${sessionFilter} AND kind='managed-admission'`,
),
);
// B2: attempt identities are checked BEFORE the shared audit — a reused
// attemptId collides the journal command ids, dedups the later
// generations' attempts and breaks recovery routing itself; naming the
// identity root cause first keeps the diagnosis specific.
if (harnessOnly) {
const b2Attempts = b2ModelAttempts(
mysqlPort,
sessionFilter,
firstPromptId,
);
b2AttemptIds = [
...new Set(b2Attempts.map((attempt) => attempt.attemptId)),
];
if (b2AttemptIds.length !== modelRoundPartials.length + 1)
throw new Error(
`B2 attempt identities: expected ${modelRoundPartials.length + 1} distinct attempts, got ${b2AttemptIds.length}: ${b2Attempts.map((attempt) => `${attempt.attemptId}:${attempt.state}`).join(',')}`,
);
}
if (
recovered.terminals.length !== 1 ||
recovered.terminals[0].type !== 'turn.completed' ||
recovered.text !== modelRoundResponse ||
!retractedBeforeNewDeltas ||
terminalRows() !== 1 ||
roundRequests.length !== modelRoundPartials.length + 1 ||
roundRequests.some(
(request) => !isDeepStrictEqual(request.body, roundRequests[0].body),
) ||
executionCount !== 0 ||
turnCount !== 1 ||
admissionCount !== 1 ||
admissionResources !== 1 ||
boots.some((boot) => boot.length === 0) ||
new Set(boots).size !== boots.length ||
// Writer generation, journal revision and committed sequence all
// advance across each takeover. The negated form also fails a
// missing column or a NULL, which read as undefined or NaN.
heads.some(
(head, index) =>
index > 0 &&
![0, 1, 2].every(
(column) => head[column] > heads[index - 1][column],
),
)
) {
throw new Error(
`Model-round failover audit failed: terminals=${JSON.stringify(recovered.terminals)} text=${JSON.stringify(recovered.text)} retractedBeforeNewDeltas=${retractedBeforeNewDeltas} terminalRows=${terminalRows()} roundRequests=${roundRequests.length} equalRequests=${roundRequests.every((request) => isDeepStrictEqual(request.body, roundRequests[0]?.body))} executions=${executionCount} turns=${turnCount} admissions=${admissionCount} admissionResources=${admissionResources} boots=${boots.join('->')} heads=${heads.map((head) => head.join(',')).join('->')}`,
);
}
if (harnessOnly) {
// B2: the live SSE view captured across both crashes (drained past the
// terminal so a double projection is observed) and a Last-Event-ID: 0
// replay; the shared view audit owns the identity/blanking/terminal
// assertions so behavioural tests execute them.
if (!b2LiveCapture)
throw new Error('B2 live SSE capture was not started');
const b2Live = await b2LiveCapture;
const b2Replay = await readPublicSse(replacementSpringUrl, session.id, {
tenantId: tenant,
lastEventId: '0',
until: (events) => events.some((event) => event.terminal),
timeoutMs: 60_000,
});
const b2Views = assertB2StreamViews({
live: b2Live.events,
reload: recovered.events,
replay: b2Replay.events,
partials: modelRoundPartials,
answer: modelRoundResponse,
firstPrefixSequence,
secondPrefixSequence: b2SecondPrefixSequence,
originalTurnId: recovered.terminals[0]?.turn_id ?? undefined,
});
writeEvidenceFile(modelRoundEvidenceDirectory, 'b2-identities.json', {
attemptIds: b2AttemptIds,
live: {
partial1: b2Views.livePartial1,
partial2: b2Views.livePartial2,
answer: b2Views.liveAnswer,
frames: b2Live.events.length,
lastId: b2Live.lastId,
},
reloadAnswer: b2Views.reloadAnswer,
replay: {
answer: b2Views.replayAnswer,
frames: b2Replay.events.length,
},
});
}
if (frozenControlPlane) {
// Q2's binding half: the former control plane is alive, its claim
// expired and its Session binding was moved by the replacement. On
// wake it must still hold its stale claim in memory and try it —
// and nothing it tries may move the binding, the Turn, the public
// feed or the journal.
const fencedProbes = 7;
const fencedState = (timeoutMs = fencedProbes * 10_000) => {
// The probes are synchronous, so waitUntil cannot cut one short: a
// budgeted call splits its budget over them (runMysql still gives
// each at least 2 s), and a one-shot call keeps 10 s per probe.
const probeMs = Math.max(1, Math.floor(timeoutMs / fencedProbes));
return {
session: runMysql(
mysqlPort,
`SELECT harness_boot_id, harness_event_epoch, harness_last_event_id, status FROM qwen_managed_agent.ma_session WHERE ${sessionFilter}`,
probeMs,
),
turns: runMysql(
mysqlPort,
`SELECT turn_id, status, version, dispatch_owner, dispatch_lease_until, retry_count, is_submission_attempted, harness_event_epoch, harness_last_event_id, error_code FROM qwen_managed_agent.ma_turn WHERE ${sessionFilter} ORDER BY turn_id`,
probeMs,
),
// Row by row: a retraction rewrites delta payloads and identities
// in place, which no count or maximum would notice.
events: runMysql(
mysqlPort,
`SELECT stream_kind, HEX(stream_id), sequence_id, event_type, is_terminal, HEX(source_key), SHA2(data_json, 256), HEX(item_id), HEX(content_part_id), SHA2(text, 256), state FROM qwen_managed_agent.ma_public_event WHERE ${sessionFilter} ORDER BY stream_kind, stream_id, sequence_id`,
probeMs,
),
// Every Spring also sweeps expired operation claims on a timer, so
// the woken one writes ma_operation too; none of it may land here.
operations: runMysql(
mysqlPort,
`SELECT HEX(operation_key), operation_kind, state, phase, delivery_state, claim_owner FROM qwen_managed_agent.ma_operation WHERE ${sessionFilter} ORDER BY 1`,
probeMs,
),
// The head's state, writer, checkpoint and recovery columns. Its
// lease, activation and revision columns move with every
// activation renewal the live writer journals.
head: runMysql(
mysqlPort,
`SELECT state, storage_version, writer_generation, HEX(writer_id), HEX(latest_checkpoint_resource_id), compacted_through_revision, recovery_status, recovery_detail_code FROM qwen_managed_agent.ma_session_head WHERE ${sessionFilter}`,
probeMs,
),
// Every journal transaction except those renewals, by writer
// generation and operation; an older generation's renewals stay
// counted in staleWriterTx below.
journal: runMysql(
mysqlPort,
`SELECT writer_generation, operation, COUNT(*), MAX(journal_revision) FROM qwen_managed_agent.ma_session_journal_tx WHERE ${sessionFilter} AND operation <> 'renewActivation' GROUP BY writer_generation, operation ORDER BY writer_generation, operation`,
probeMs,
),
staleWriterTx: runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_session_journal_tx WHERE ${sessionFilter} AND writer_generation < (SELECT writer_generation FROM qwen_managed_agent.ma_session_head WHERE ${sessionFilter})`,
probeMs,
),
};
};
// The replacement must go quiet first: a late write of its own inside
// the wake window would otherwise read as a stale mutation.
// The window matches the wait after the wake, so a replacement-side
// writer slower than the window cannot pass here and fail there.
// A dead owner would hold every snapshot still as well, so each wait
// from here on watches the replacement pair, and so does the verdict.
const owners = [replacementSpring, finishingHarness];
let beforeWake = fencedState();
await waitUntil(
'Replacement quiet before the wake',
async (remainingMs) => {
await new Promise((resolve) => setTimeout(resolve, 6_000));
const settled = fencedState(Math.max(1, remainingMs - 6_000));
const moved = (
Object.keys(settled) as Array<keyof typeof settled>
).filter(
(key) => !isDeepStrictEqual(beforeWake[key], settled[key]),
);
const previous = beforeWake;
beforeWake = settled;
if (moved.length > 0) {
throw new Error(
`still moving: ${moved.map((key) => `${key}: ${JSON.stringify(previous[key]).slice(0, 1_000)} -> ${JSON.stringify(settled[key]).slice(0, 1_000)}`).join(' | ')}`,
);
}
return true;
},
90_000,
owners,
);
if (beforeWake.session.split('\t')[0] !== boots[boots.length - 1]) {
throw new Error(
`The replacement did not move the Session binding: ${beforeWake.session}`,
);
}
const formerProbes = [
[`${springUrl}/actuator/health`, 'former Spring'],
[`http://127.0.0.1:${harnessPort}/health`, 'former Harness'],
] as const;
const answers = (url: string) =>
fetch(url, {
headers: { authorization: `Bearer ${harnessToken}` },
signal: AbortSignal.timeout(1_000),
}).then(
() => true,
() => false,
);
// Non-vacuity: alive but silent before SIGCONT, answering after.
for (const [url, name] of formerProbes) {
if (await answers(url)) {
throw new Error(
`The ${name} answered before SIGCONT; the freeze did not take effect`,
);
}
}
for (const child of frozenChildren) {
if (!processTreeExists(child.child)) {
throw new Error(`${child.name} did not survive the freeze`);
}
}
// A woken Harness with no home would leave every check below
// holding by absence instead of by fencing.
if (!existsSync(harnessHome)) {
throw new Error(
'Frozen control-plane arm lost the Harness home the wake depends on',
);
}
// The writer axis has its own witness: the woken Harness still holds
// its stale writer grant, and only the store's writer-grant check
// answers with this refusal. It must not appear before the wake.
// After the wake the Harness's whole output is kept, not the log's
// rolling tail, so a busy Harness cannot push the refusal out.
const staleWriterRefusal =
'The Managed Session writer grant is stale or unavailable';
if (harness.log().includes(staleWriterRefusal)) {
throw new Error(
`The former Harness met the writer fence before the wake:\n${harness.log()}`,
);
}
let harnessOutputSinceWake = '';
const recordSinceWake = (chunk: Buffer) => {
harnessOutputSinceWake += chunk.toString();
};
harness.child.stdout?.on('data', recordSinceWake);
harness.child.stderr?.on('data', recordSinceWake);
const staleWriterRefusals = () =>
harnessOutputSinceWake.split(staleWriterRefusal).length - 1;
// The general log names every statement the woken Spring sends, so
// a claim UPDATE carrying the former owner id proves the wake really
// exercised the stale authority instead of idling past the check.
runMysql(mysqlPort, "SET GLOBAL log_output = 'TABLE'");
runMysql(mysqlPort, "SET GLOBAL general_log = 'ON'");
const wokeAt = runMysql(mysqlPort, 'SELECT NOW(6)');
const modelRequestsBeforeWake = fake?.requests.length ?? 0;
for (const child of frozenChildren.splice(0)) {
signalProcessTree(child.child, 'SIGCONT');
}
await waitUntil(
'Former control plane wake',
async () =>
(
await Promise.all(formerProbes.map(([url]) => answers(url)))
).every(Boolean),
30_000,
owners,
);
// The witness is a renewal or retry the woken coordinator still sends
// on its stale claim; a coordinator that stopped sending a write it
// knows is stale would need another witness, not a skipped one.
const staleClaimUpdates = (timeoutMs = 10_000) =>
Number(
runMysql(
mysqlPort,
`SELECT COUNT(*) FROM mysql.general_log WHERE event_time >= ${sqlString(wokeAt)} AND command_type = 'Query' AND CONVERT(argument USING utf8mb4) REGEXP 'UPDATE[[:space:]]+ma_turn[[:space:]]' AND CONVERT(argument USING utf8mb4) NOT LIKE '%general_log%' AND LOCATE(${sqlString(formerDispatchOwner)}, CONVERT(argument USING utf8mb4)) > 0`,
timeoutMs,
),
);
await waitUntil(
'Woken control plane exercises its stale claim',
(remainingMs) => staleClaimUpdates(remainingMs) > 0,
30_000,
owners,
);
await waitUntil(
'Woken Harness meets the writer fence',
() => staleWriterRefusals() > 0,
30_000,
owners,
);
// Several dispatch lease periods for anything the wake set in motion.
await new Promise((resolve) => setTimeout(resolve, 6_000));
const afterWake = fencedState();
// The probes are synchronous: let the loop record an exit during
// them before the owners are checked.
await new Promise((resolve) => setImmediate(resolve));
for (const owner of owners) {
if (childExited(owner.child)) {
throw new Error(
`${owner.name} exited inside the wake window\n${owner.log()}`,
);
}
}
const moved = (
Object.keys(afterWake) as Array<keyof typeof afterWake>
).filter((key) => !isDeepStrictEqual(beforeWake[key], afterWake[key]));
const staleAttempts = staleClaimUpdates();
// Evidence, not a gate: every write the woken Spring sent on the
// connections that carried its stale owner id, by statement head.
const staleConnectionWrites = runMysql(
mysqlPort,
`SELECT LEFT(CONVERT(argument USING utf8mb4), 48), COUNT(*) FROM mysql.general_log WHERE event_time >= ${sqlString(wokeAt)} AND command_type = 'Query' AND CONVERT(argument USING utf8mb4) REGEXP '^[[:space:]]*(UPDATE|INSERT|DELETE|REPLACE)' AND thread_id IN (SELECT thread_id FROM (SELECT DISTINCT thread_id FROM mysql.general_log WHERE event_time >= ${sqlString(wokeAt)} AND CONVERT(argument USING utf8mb4) NOT LIKE '%general_log%' AND LOCATE(${sqlString(formerDispatchOwner)}, CONVERT(argument USING utf8mb4)) > 0) AS stale) GROUP BY 1 ORDER BY 1`,
)
.split('\n')
.filter(Boolean);
runMysql(mysqlPort, "SET GLOBAL general_log = 'OFF'");
const awake = await publicTranscript(
replacementSpringUrl,
tenant,
session.id,
);
// staleWriterTx counts the former writer's commits from before the
// freeze too, so it is compared across the wake, never against zero.
if (
!isDeepStrictEqual(beforeWake, afterWake) ||
awake.text !== modelRoundResponse ||
awake.terminals.length !== 1
) {
throw new Error(
`Woken former control plane mutated the takeover: moved=${moved.join(',') || 'none'} before=${JSON.stringify(beforeWake)} after=${JSON.stringify(afterWake)} text=${JSON.stringify(awake.text)} terminals=${awake.terminals.length}`,
);
}
fencedWriterJson = JSON.stringify(
{
fencedFormerControlPlane: true,
formerDispatchOwner,
staleClaimUpdates: staleAttempts,
staleWriterRefusals: staleWriterRefusals(),
staleConnectionWrites,
harnessBootId: afterWake.session.split('\t')[0],
journalHead: afterWake.head,
staleWriterTx: afterWake.staleWriterTx,
turn: afterWake.turns,
modelRequestsAfterWake:
(fake?.requests.length ?? 0) - modelRequestsBeforeWake,
visibleText: awake.text,
},
null,
2,
);
// Two live control planes over one Session would race for the next
// Turn's claim — routing that is out of scope — so the former pair
// leaves before the Session is used again.
await crashChild(harness.child, `${hostedHarnessLabel} (original)`);
await crashChild(
spring.child,
'Spring Managed Agent Server (original)',
);
}
// The Session keeps working, and what the next Turn's model sees is
// the recovered history: one copy of the original prompt and answer,
// none of the retracted prefixes.
const cursor = Math.max(
0,
...recovered.events.map((event) => event.sequence),
);
const nextResponse = await fetch(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}/events`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
'idempotency-key': `model-round-next-${session.id}`,
...tenantHeaders(tenant),
},
body: JSON.stringify({
type: 'agent.session.input.message',
input: [
{
type: 'text',
text: `${modelRoundNextMarker}. Reply exactly ${modelRoundNextResponse}.`,
},
],
}),
},
);
if (nextResponse.status !== 202) {
throw new Error(
`Next model-round Turn returned ${nextResponse.status}: ${await nextResponse.text()}`,
);
}
const nextTurn = await waitForTerminal(
replacementSpringUrl,
tenant,
session.id,
cursor,
replacementSpring,
60_000,
);
const nextTerminals = nextTurn.events.filter((event) => event.terminal);
const nextText = nextTurn.events
.filter((event) => event.type === 'item.output_text.delta')
.map(eventText)
.join('');
const nextRequest = [...(fake?.requests ?? [])]
.reverse()
.find(({ body }) =>
JSON.stringify(body['messages']).includes(modelRoundNextMarker),
);
const history = JSON.stringify(nextRequest?.body['messages'] ?? []);
const occurrences = (needle: string) => history.split(needle).length - 1;
// The answer string also sits inside the original prompt, so one
// prompt plus one answer is exactly two.
if (
nextTerminals.length !== 1 ||
nextTerminals[0].type !== 'turn.completed' ||
nextText !== modelRoundNextResponse ||
terminalRows() !== 2 ||
occurrences(modelRoundMarker) !== 1 ||
occurrences(modelRoundResponse) !== 2 ||
modelRoundPartials.some((partial) => history.includes(partial))
) {
throw new Error(
`Next Turn after the model-round takeover failed: terminals=${JSON.stringify(nextTerminals)} text=${JSON.stringify(nextText)} terminalRows=${terminalRows()} history=${history}`,
);
}
resultJson = JSON.stringify(
{
sessionId: session.id,
harnessBootIds: boots,
writerGenerations: heads.map((head) => head[0]),
journalRevisions: heads.map((head) => head[1]),
committedSequences: heads.map((head) => head[2]),
replacedGenerations: modelRoundPartials.length,
modelRoundRequests: roundRequests.length,
equalSavedRequests: true,
retractedPrefixes: modelRoundPartials,
visibleText: recovered.text,
physicalToolExecutions: executionCount,
terminalTurns: terminalRows(),
journalAdmissions: admissionCount,
admissionResources,
nextTurnHistoryClean: true,
// As in the frozen-owner arm, one key cannot mean both intents.
oldHarnessDiskDeleted:
!frozenControlPlane && !existsSync(harnessHome),
harnessHomeRetainedForWake:
frozenControlPlane && existsSync(harnessHome),
},
null,
2,
);
} else {
const secondResponse = await fetch(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}/events`,
{
method: 'POST',
headers: {
'content-type': 'application/json',
'idempotency-key': `failover-second-${Date.now()}`,
...tenantHeaders(tenant),
},
body: JSON.stringify({
type: 'agent.session.input.message',
input: [
{
type: 'text',
text: `${failoverSecondMarker}. Use the prior conversation and reply exactly ${failoverSecondResponse}.`,
},
],
}),
},
);
if (secondResponse.status !== 202) {
throw new Error(
`Second failover Turn returned ${secondResponse.status}: ${await secondResponse.text()}`,
);
}
const secondTurn = await waitForTerminal(
replacementSpringUrl,
tenant,
session.id,
firstTurnLastSequence,
replacementSpring,
);
const secondTerminal = secondTurn.events.find((event) => event.terminal);
if (secondTerminal?.type !== 'turn.completed') {
throw new Error(
`Second failover Turn ended with ${secondTerminal?.type ?? 'no terminal event'}`,
);
}
if (bigOutput) {
assertPublicAnswer(secondTurn.events, failoverSecondResponse);
assertStoredAnswer(
mysqlPort,
tenant,
session.id,
failoverFirstResponse,
);
assertStoredAnswer(
mysqlPort,
tenant,
session.id,
failoverSecondResponse,
);
}
const secondRequest = [...(fake?.requests ?? [])]
.reverse()
.find(({ body }) =>
JSON.stringify(body['messages']).includes(failoverSecondMarker),
);
const restoredMessages = JSON.stringify(
secondRequest?.body['messages'] ?? [],
);
if (
!restoredMessages.includes(failoverFirstMarker) ||
!restoredMessages.includes(failoverFirstResponse) ||
restoredMessages.includes(failoverMissingResponse)
) {
throw new Error(
'Replacement Harness did not restore first-Turn context',
);
}
const secondBootId = runMysql(
mysqlPort,
`SELECT harness_boot_id FROM qwen_managed_agent.ma_session WHERE ${sessionFilter}`,
);
const secondHead = runMysql(
mysqlPort,
`SELECT writer_generation, journal_revision, committed_sequence FROM qwen_managed_agent.ma_session_head WHERE ${sessionFilter}`,
)
.split('\t')
.map(Number);
const terminalCount = Number(
runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_public_event WHERE ${sessionFilter} AND stream_kind='SESSION' AND stream_id=X'' AND is_terminal=TRUE`,
),
);
if (
secondBootId.length === 0 ||
secondBootId === firstBootId ||
secondHead.length !== 3 ||
secondHead[0] <= firstHead[0] ||
secondHead[1] <= firstHead[1] ||
secondHead[2] <= firstHead[2] ||
terminalCount !== 2
) {
throw new Error(
`Failover audit failed: boot=${firstBootId}->${secondBootId} head=${firstHead.join(',')}->${secondHead.join(',')} terminals=${terminalCount}`,
);
}
resultJson = JSON.stringify(
{
sessionId: session.id,
firstHarnessBootId: firstBootId,
replacementHarnessBootId: secondBootId,
writerGeneration: `${firstHead[0]} -> ${secondHead[0]}`,
journalRevision: `${firstHead[1]} -> ${secondHead[1]}`,
committedSequence: `${firstHead[2]} -> ${secondHead[2]}`,
terminalTurns: terminalCount,
...(bigOutput
? {
expectedCharacters: failoverFirstResponse.length,
expectedUtf8Bytes: Buffer.byteLength(failoverFirstResponse),
shortControlCharacters: failoverSecondResponse.length,
fullTextPreserved: true,
}
: {}),
restoredFirstTurnContext: true,
oldHarnessDiskDeleted: !existsSync(harnessHome),
},
null,
2,
);
}
if (verificationMode) {
// These assertions run after every A/B primary gate, never in place of it.
await waitUntil(
'Runtime cleanup before next Turn',
() => {
const state = runMysql(
mysqlPort,
`SELECT session_state FROM qwen_managed_agent.ma_runtime_session WHERE runtime_session_id=${sqlString(originalRuntimeSessionId!)}`,
);
const confirmed = runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_session_journal_tx WHERE ${sessionFilter} AND command_id=${sqlString(`hosted-cleanup:${originalRuntimeSessionId}:${originalRuntimeSessionId}:confirmed`)}`,
);
return state === 'RELEASED' && confirmed === '1';
},
30_000,
replacementHarness,
);
const originalPage = await fetchJson<PublicList<PublicEvent>>(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}/events?after=0&limit=100`,
{ headers: tenantHeaders(tenant) },
);
if (originalPage.data.filter((event) => event.terminal).length !== 1)
throw new Error(
'Original Turn must have exactly one public terminal before relief',
);
const originalCursor = Math.max(
0,
...originalPage.data.map((event) => event.sequence),
);
const reliefUrl = `${replacementSpringUrl}/v1/agents/sessions/${session.id}/events`;
const reliefHeaders = {
'content-type': 'application/json',
'idempotency-key': `g3-relief-${session.id}`,
...tenantHeaders(tenant),
};
const reliefBody = JSON.stringify({
type: 'agent.session.input.message',
input: [
{
type: 'text',
text: `${reliefMarker}. Reply exactly ${reliefResponse}. Use no tools.`,
},
],
});
if (approvalFault === 'admission-reply-lost') {
await postAndDropReply(reliefUrl, reliefBody, reliefHeaders);
}
const relief = await fetch(reliefUrl, {
method: 'POST',
headers: reliefHeaders,
body: reliefBody,
});
if (relief.status !== 202)
throw new Error(
`Relief Turn admission returned ${relief.status}: ${await relief.text()}`,
);
const reliefTurn = await waitForTerminal(
replacementSpringUrl,
tenant,
session.id,
originalCursor,
replacementSpring,
60_000,
);
const reliefTerminals = reliefTurn.events.filter(
(event) => event.terminal,
);
const reliefText = reliefTurn.events
.filter((event) => event.type === 'item.output_text.delta')
.map(eventText)
.join('');
if (
reliefTerminals.length !== 1 ||
reliefTerminals[0].type !== 'turn.completed' ||
reliefText !== reliefResponse
)
throw new Error(
`Relief Turn did not finish: terminals=${JSON.stringify(reliefTerminals)} text=${JSON.stringify(reliefText)}`,
);
const reliefRequests = (fake?.requests ?? []).filter((request) =>
JSON.stringify(request.body.messages).includes(reliefMarker),
);
if (reliefRequests.length !== 1)
throw new Error(
`Relief Turn inference count differs: ${reliefRequests.length}`,
);
if (approvalFault === 'admission-reply-lost') {
// C4: the retried Idempotency-Key must land on the original
// admission. A second Turn row would mean the lost reply was
// re-admitted instead of reconciled.
const turnRows = Number(
runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_turn WHERE ${sessionFilter}`,
),
);
if (turnRows !== 2)
throw new Error(
`Lost admission reply produced ${turnRows} Turn rows, not two`,
);
}
const finalTerminalCount = Number(
runMysql(
mysqlPort,
`SELECT COUNT(*) FROM qwen_managed_agent.ma_public_event WHERE ${sessionFilter} AND stream_kind='SESSION' AND stream_id=X'' AND is_terminal=TRUE`,
),
);
if (finalTerminalCount !== 2)
throw new Error(
`Expected one terminal per original/relief Turn, observed ${finalTerminalCount}`,
);
saveEvidence('post-turn-events.json', {
cursorBeforeRelief: originalCursor,
original: originalPage.data,
relief: reliefTurn.events,
sqlTerminalCount: finalTerminalCount,
});
const close = await fetch(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}/close`,
{
method: 'POST',
headers: {
'idempotency-key': `g3-close-${session.id}`,
...tenantHeaders(tenant),
},
},
);
if (close.status !== 202)
throw new Error(
`Close admission returned ${close.status}: ${await close.text()}`,
);
const closeOperation = (await close.json()) as Record<string, unknown>;
await waitUntil(
'Reliable close operation completion',
async () => {
const operation = await fetchJson<Record<string, unknown>>(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}/operations/${closeOperation.id}`,
{ headers: tenantHeaders(tenant) },
);
if (operation.status === 'failed')
throw new Error(
`Close operation failed: ${JSON.stringify(operation)}`,
);
return operation.status === 'completed';
},
60_000,
replacementSpring,
);
const closedSession = await fetchJson<Record<string, unknown>>(
`${replacementSpringUrl}/v1/agents/sessions/${session.id}`,
{ headers: tenantHeaders(tenant) },
);
if (closedSession.status !== 'closed')
throw new Error(
`Reliable close completed with Session status ${closedSession.status}`,
);
const summary = {
mode: verificationMode,
fault: approvalFault ?? null,
// C1/C2 must not report the later relief Turn's terminal commit as
// part of the original fault window.
faultObservations: originalFaultObservations ?? {
terminalRejects: 0,
terminalAccepts: 0,
terminalAcceptsAtBarrier: 0,
postCrashTerminalAccepts: 0,
},
sessionId: session.id,
originalRuntimeSessionId,
originalActionId: originalApprovalId,
originalTerminal: verifiedOriginalTerminal ?? 'turn.completed',
nextTurnCompleted: true,
nextTurnRequests: reliefRequests.length,
terminalCount: finalTerminalCount,
closeAdmission: close.status,
closeCompleted: true,
oldHarnessHomeDeleted: !existsSync(harnessHome),
};
saveEvidence('summary.json', summary);
g3SummaryJson = JSON.stringify(summary, null, 2);
}
} else {
const idempotencyKey = `create-${Date.now()}`;
const prompt = [
'Before using any tool, emit the visible text MODEL_READY.',
'Then call write_file exactly once to write the exact text',
JSON.stringify(sideEffectContent),
'to the relative path',
JSON.stringify(sideEffectName),
'under the session working directory.',
'Call no other tool before or after it.',
'After the tool succeeds, reply TOOL_DONE. Do not ask a question.',
].join(' ');
const body = JSON.stringify({
agent_id: 'qwen-code',
input: [{ type: 'text', text: prompt }],
workspace: { workspace_id: boundWorkspaceId },
metadata: { title: 'Real model cold Runtime E2E' },
});
const requestStartedAt = Date.now();
const createResponse = await fetch(`${springUrl}/v1/agents/sessions`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'idempotency-key': idempotencyKey,
...tenantHeaders(tenant),
},
body,
});
if (createResponse.status !== 202) {
throw new Error(
`Session create returned ${createResponse.status}: ${await createResponse.text()}`,
);
}
const createdAt = Date.now();
const session = (await createResponse.json()) as PublicSession;
const observed = new Map<
number,
{ event: PublicEvent; observedAt: number }
>();
let after = 0;
await waitUntil(
'Managed Turn terminal event',
async () => {
const page = await fetchJson<PublicList<PublicEvent>>(
`${springUrl}/v1/agents/sessions/${session.id}/events?after=${after}&limit=100`,
{ headers: tenantHeaders(tenant) },
);
const now = Date.now();
for (const event of page.data) {
observed.set(event.sequence, { event, observedAt: now });
after = Math.max(after, event.sequence);
}
return page.data.some((event) => event.terminal);
},
180_000,
spring,
);
const ordered = [...observed.values()].sort(
(left, right) => left.event.sequence - right.event.sequence,
);
const firstModel = ordered.find(({ event }) =>
['item.output_text.delta', 'item.reasoning.delta'].includes(event.type),
);
const runtimeReady = ordered.find(
({ event }) => event.type === 'environment.ready',
);
const terminal = ordered.find(({ event }) => event.terminal);
if (!firstModel || !runtimeReady || !terminal) {
throw new Error(
`Expected model, Runtime, and terminal events; got ${ordered.map(({ event }) => event.type).join(', ')}`,
);
}
if (terminal.event.type !== 'turn.completed') {
throw new Error(`Managed Turn ended with ${terminal.event.type}`);
}
if (
runtimeDelayMs >= modelBeforeRuntimeAssertionDelayMs &&
firstModel.event.sequence >= runtimeReady.event.sequence
) {
throw new Error(
'First model event did not precede Runtime readiness ' +
`(firstModelSequence=${firstModel.event.sequence}, ` +
`runtimeReadySequence=${runtimeReady.event.sequence}, ` +
`firstModelEventMs=${firstModel.observedAt - requestStartedAt}, ` +
`runtimeReadyMs=${runtimeReady.observedAt - requestStartedAt}, ` +
`runtimeDelayMs=${runtimeDelayMs})`,
);
}
if (!existsSync(sideEffect)) {
throw new Error('Tool side effect file was not created');
}
if (readFileSync(sideEffect, 'utf8') !== sideEffectContent) {
throw new Error('Tool side effect content did not match');
}
// Tool calls execute through the Broker worker, which does not publish
// item.tool_call.* public events without O2 publication; the durable
// execution record is the public-feed-independent proof.
const executions = runMysql(
mysqlPort,
`SELECT COUNT(*), GROUP_CONCAT(DISTINCT execution_state) FROM qwen_managed_agent.ma_tool_execution`,
).split('\t');
if (executions[0] !== '1' || executions[1] !== 'SETTLED') {
// The temporary MySQL data dir is deleted on exit, so the failure
// message is the only place the offending executions can still be
// identified: two tool_call_ids mean the model called another tool,
// one tool_call_id across two rows means a duplicate dispatch.
const rows = runMysql(
mysqlPort,
'SELECT execution_call_id, tool_call_id, execution_state FROM qwen_managed_agent.ma_tool_execution',
);
throw new Error(
`Tool execution audit failed: ${executions.join(',')}\n${rows}`,
);
}
const replay = await fetch(`${springUrl}/v1/agents/sessions`, {
method: 'POST',
headers: {
'content-type': 'application/json',
'idempotency-key': idempotencyKey,
...tenantHeaders(tenant),
},
body,
});
const replaySession = (await replay.json()) as PublicSession;
if (
replay.status !== 202 ||
replaySession.id !== session.id ||
replay.headers.get('x-qwen-idempotent-replay') !== 'true'
) {
throw new Error('Idempotent create replay did not return the Session');
}
// The probe carries an actor holding a read grant on the Session's
// Workspace, so its 404 can only come from the tenant scope: without the
// actor, a bound Session 404s through the grant check and would mask a
// tenant-scoping leak.
const crossTenant = await fetch(
`${springUrl}/v1/agents/sessions/${session.id}`,
{ headers: tenantHeaders('other-tenant') },
);
if (crossTenant.status !== 404) {
throw new Error(`Cross-tenant lookup returned ${crossTenant.status}`);
}
const durable = runMysql(
mysqlPort,
`SELECT COUNT(*), COUNT(DISTINCT sequence_id), SUM(is_terminal) FROM qwen_managed_agent.ma_public_event WHERE tenant_id='${tenant}' AND session_id='${session.id}' AND stream_kind='SESSION' AND stream_id=X''`,
).split('\t');
if (
durable.length !== 3 ||
durable[0] !== durable[1] ||
durable[2] !== '1'
) {
throw new Error(`Durable event audit failed: ${durable.join(',')}`);
}
resultJson = JSON.stringify(
{
model,
sessionId: session.id,
createAdmissionMs: createdAt - requestStartedAt,
firstModelEventMs: firstModel.observedAt - requestStartedAt,
runtimeReadyMs: runtimeReady.observedAt - requestStartedAt,
terminalMs: terminal.observedAt - requestStartedAt,
modelBeforeRuntimeReady:
firstModel.event.sequence < runtimeReady.event.sequence,
toolSideEffect: true,
idempotentReplay: true,
crossTenantStatus: crossTenant.status,
durableEventCount: Number(durable[0]),
},
null,
2,
);
}
} catch (error) {
failure = error;
console.error(error);
if (dumpPort !== undefined) {
try {
console.error(
`\n--- Durable state ---\nturns:\n${runMysql(dumpPort, 'SELECT turn_id, status, error_code, is_submission_attempted, harness_event_epoch, dispatch_owner, dispatch_lease_until FROM qwen_managed_agent.ma_turn')}\nevents:\n${runMysql(dumpPort, 'SELECT stream_kind, HEX(stream_id), sequence_id, turn_id, event_type, is_terminal, source_key FROM qwen_managed_agent.ma_public_event ORDER BY stream_kind, stream_id, sequence_id')}\nsessions:\n${runMysql(dumpPort, 'SELECT session_id, harness_boot_id, harness_event_epoch, harness_last_event_id FROM qwen_managed_agent.ma_session')}\nexecutions:\n${runMysql(dumpPort, 'SELECT execution_call_id, execution_state, dispatch_generation FROM qwen_managed_agent.ma_tool_execution')}\njournal:\n${runMysql(dumpPort, 'SELECT session_id, state, writer_generation, journal_revision, committed_sequence FROM qwen_managed_agent.ma_session_head')}`,
);
} catch (dumpError) {
console.error(`Durable state dump failed: ${String(dumpError)}`);
}
}
if (replacementBrokerProxy !== undefined) {
console.error(
`\n--- Replacement Runtime Broker requests ---\n${replacementBrokerProxy.observations().join('\n')}`,
);
}
for (const child of children) {
console.error(`\n--- ${child.name} tail ---\n${child.log()}`);
}
if (existsSync(mysqlError)) {
console.error(
`\n--- MySQL error tail ---\n${readFileSync(mysqlError, 'utf8').slice(-16_384)}`,
);
}
} finally {
// Abort any pending B2 SSE capture so an early arm failure exits promptly
// instead of waiting out the capture's budget on an open socket.
for (const observer of b2SseObservers) observer.controller.abort();
// A frozen Harness holds SIGTERM pending from stopChild; wake it before
// teardown so teardown does not burn the 10-second stall on every path,
// failure or success. `harness` is try-block scoped, so reach it through
// the children registry by its start() name — and fail loudly rather
// than leaving a wedged writer behind when the name drifts.
if (freeze) {
let woke = false;
for (const child of children) {
if (child.name === hostedHarnessLabel) {
signalProcessTree(child.child, 'SIGCONT');
woke = true;
}
}
if (!woke) {
failure ??= new Error(
'Frozen-owner arm could not find the Harness child to wake: the registry key drifted',
);
}
}
for (const child of frozenChildren.splice(0)) {
signalProcessTree(child.child, 'SIGCONT');
}
for (const child of children.reverse()) {
await stopChild(child.child);
}
await heldStartProxy?.close();
await replacementBrokerProxy?.close();
await storeFaultProxy?.close();
// A failed G3 arm still owes the operator the model traffic that decided
// whether the recovery resumed, replayed or never inferred at all.
if (verificationMode && failure) {
saveEvidence('model-requests.json', { requests: fake?.requests ?? [] });
}
releaseContinuationHold();
for (const hold of modelRoundHolds) hold.release();
await fake?.close();
// An interrupt during teardown is a non-pass: the keep switch must still
// hold, or a stopped run deletes its own evidence before the post-finally
// throw reports it.
if (
(failure || receivedSignal) &&
process.env['QWEN_MANAGED_E2E_KEEP_TMP'] === '1'
) {
console.error(`Keeping temporary directory: ${temporary}`);
} else {
rmSync(temporary, { recursive: true, force: true });
}
process.removeListener('SIGINT', handleSignal);
process.removeListener('SIGTERM', handleSignal);
}
if (failure) throw failure;
// An interrupt after the last poll — or during the finally's teardown —
// must not print the success JSON and exit 0: a supervising script would
// record a clean pass for a run the operator explicitly stopped. The
// payload stays a variable until both guards have run, so the decision
// precedes the record.
if (receivedSignal) throw new Error(`Interrupted by ${receivedSignal}`);
if (approvalResultJson !== undefined) console.log(approvalResultJson);
if (g3SummaryJson !== undefined) console.log(g3SummaryJson);
if (resultJson !== undefined) {
console.log(resultJson);
if (fencedWriterJson !== undefined) console.log(fencedWriterJson);
}