blob: 26b45e7a3c1fcb356bca291dc1c183f7c0629f6a [file]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { deferred } from '@maka/core/test-only/async-primitives';
import assert from 'node:assert/strict';
import { describe, test } from 'node:test';
import type { SessionEvent } from '@maka/core/events';
import type { SessionSummary, StoredMessage } from '@maka/core/session';
import {
createRuntimeHostSessionProjectionSeed,
RuntimeHostSessionProjector,
} from '@maka/runtime-host/adapter';
import { LOCAL_RUNTIME_HOST_PROFILE, type RuntimeHostConnection } from '@maka/runtime-host/client';
import {
SESSION_CONTINUITY_SCHEMA_VERSION,
type InteractionPendingSnapshot,
type SessionCatalogProjection,
type SessionContinuitySnapshot,
type SubscriptionFrame,
} from '@maka/runtime-host/protocol';
import { resolveRuntimeHostCliTarget } from '../runtime-host-cli-context.js';
import { createRuntimeHostRunContext, runRuntimeHostTextCli } from '../runtime-host-run-command.js';
import type { RuntimeHostMakaSessionDriver } from '../runtime-host-session-driver.js';
import type { MakaRunContextInput, MakaRunOutcome } from '../run-command-core.js';
import type { MakaTranscriptReplacementReason } from '../session-driver.js';
describe('Runtime Host maka run adapter', () => {
test('stops before context creation when CLI preflight finds a confirmed blocker', async () => {
const stderr: string[] = [];
let contextCreations = 0;
const blockedCatalog = connectionCatalog();
blockedCatalog.connections[0]!.enabled = false;
const exitCode = await runRuntimeHostTextCli(
['answer once'],
{
workspaceRoot: () => '/runtime-host-data',
processCwd: () => process.cwd(),
stdinIsTTY: () => true,
readStdin: async () => '',
writeStdout: () => {},
writeStderr: (text) => stderr.push(text),
onSigint: () => () => {},
newId: () => 'turn-blocked',
},
{
connect: async () => ({
connection: {} as RuntimeHostConnection,
catalog: blockedCatalog,
profile: LOCAL_RUNTIME_HOST_PROFILE,
close: async () => {},
}),
createContext: (_connection, _catalog, input) => {
contextCreations += 1;
return publicCommandContext(input);
},
},
{ clientDataRoot: '/client-data', cliCommand: 'npm run cli:dev --' },
);
assert.equal(exitCode, 2);
assert.equal(contextCreations, 0);
assert.match(stderr.join(''), /model_connection_disabled/);
assert.match(stderr.join(''), /repair connection "openai-main" in `npm run cli:dev --`/);
});
test('routes both launch roots through the selected Host profile and canonical Project', async () => {
let selectedWorkspaceRoot: string | undefined;
let selectedClientDataRoot: string | undefined;
let selectedProfile: string | undefined;
let contextInput: MakaRunContextInput | undefined;
const connection = remoteReadinessConnection();
const exitCode = await runRuntimeHostTextCli(
['answer once', '--host', 'office', '--project', 'project-old'],
{
workspaceRoot: () => '/runtime-host-data',
processCwd: () => process.cwd(),
stdinIsTTY: () => true,
readStdin: async () => '',
writeStdout: () => {},
writeStderr: () => {},
onSigint: () => () => {},
newId: () => 'turn-remote',
},
{
connect: async (rootPath, profileId, clientDataRoot) => {
selectedWorkspaceRoot = rootPath;
selectedClientDataRoot = clientDataRoot;
selectedProfile = profileId;
return {
connection,
catalog: connectionCatalog(),
profile: {
id: 'office',
name: 'Office',
kind: 'remote',
transport: { kind: 'tls', url: 'wss://runtime.example.com/runtime-host' },
rootId: 'a'.repeat(64),
},
close: async () => {},
};
},
createContext: (_connection, _catalog, input) => {
contextInput = input;
return publicCommandContext(input);
},
},
{ clientDataRoot: '/client-data', cliCommand: 'npm run cli:dev --' },
);
assert.equal(exitCode, 0);
assert.equal(selectedWorkspaceRoot, '/runtime-host-data');
assert.equal(selectedClientDataRoot, '/client-data');
assert.equal(selectedProfile, 'office');
assert.equal(contextInput?.projectId, 'project-1');
});
test('continues the Host-owned cwd Session without creating another identity', async () => {
const cwd = process.cwd();
let creates = 0;
let contextInput: MakaRunContextInput | undefined;
const connection = {
request: async (operation: string) => {
if (operation !== 'session.catalog.query') {
throw new Error(`Unexpected operation: ${operation}`);
}
return {
kind: 'page',
revision: 1,
nextCursor: null,
sessions: [
{
...sessionProjection('session-existing'),
workspace: {
target: { kind: 'host_path', path: cwd },
hostCwd: cwd,
},
lastMessageAt: 10,
},
],
};
},
} as unknown as RuntimeHostConnection;
const exitCode = await runRuntimeHostTextCli(
['continue once', '--continue'],
{
workspaceRoot: () => '/runtime-host-data',
processCwd: () => cwd,
stdinIsTTY: () => true,
readStdin: async () => '',
writeStdout: () => {},
writeStderr: () => {},
onSigint: () => () => {},
newId: () => 'turn-continue',
},
{
connect: async () => ({
connection,
catalog: connectionCatalog(),
profile: LOCAL_RUNTIME_HOST_PROFILE,
close: async () => {},
}),
createContext: (_connection, _catalog, input) => {
contextInput = input;
return publicCommandContext(input, () => {
creates += 1;
});
},
},
);
assert.equal(exitCode, 0);
assert.equal(creates, 0);
assert.equal(contextInput?.sessionCwdOverride?.sessionId, 'session-existing');
});
test('fails the public command explicitly when an ordinary Host Turn requests permission', async () => {
const stderr: string[] = [];
const fixture = runFixture({
pendingInteractions: [pendingPermission('turn-1')],
pendingAfterTurnStarts: true,
});
const exitCode = await runRuntimeHostTextCli(
['run a protected tool'],
{
workspaceRoot: () => '/runtime-host-data',
processCwd: () => process.cwd(),
stdinIsTTY: () => true,
readStdin: async () => '',
writeStdout: () => {},
writeStderr: (text) => stderr.push(text),
onSigint: () => () => {},
newId: () => 'turn-1',
},
{
connect: async () => ({
connection: {} as RuntimeHostConnection,
catalog: connectionCatalog(),
profile: LOCAL_RUNTIME_HOST_PROFILE,
close: async () => {},
}),
createContext: () => fixture.context,
},
);
assert.equal(exitCode, 1);
assert.match(stderr.join(''), /interactive permission requests are unavailable/);
assert.deepEqual(fixture.exactTurnStops, [
{ sessionId: 'session-created', turnId: 'turn-1', runId: 'run-1' },
]);
});
test('returns exit code 1 when an ordinary Host Turn fails', async () => {
const stderr: string[] = [];
const fixture = runFixture({ turnEvents: failedEvents('turn-1', 'provider_failure') });
const exitCode = await runFixtureCommand(fixture, ['fail once'], undefined, (text) =>
stderr.push(text),
);
assert.equal(exitCode, 1);
assert.equal(stderr.join(''), 'maka run: Turn failed\n');
});
test('returns exit code 1 when a same-step sibling succeeds after a sandbox failure', async () => {
const stdout: string[] = [];
const stderr: string[] = [];
const fixture = runFixture({
turnEvents: projectedSameStepSandboxFailureEvents('turn-1'),
});
const exitCode = await runFixtureCommand(
fixture,
['run parallel tools'],
(text) => stdout.push(text),
(text) => stderr.push(text),
);
assert.equal(exitCode, 1);
assert.equal(stdout.join(''), '');
assert.equal(
stderr.join(''),
'maka run: sandbox boundary expansion is unavailable in non-interactive mode\n',
);
});
test('returns exit code 1 when a denied boundary request follows a sandbox failure', async () => {
const stdout: string[] = [];
const stderr: string[] = [];
const fixture = runFixture({
turnEvents: sandboxBoundaryEvents(
'turn-1',
'step-1',
'step-2',
'Boundary was not widened',
'request_sandbox_boundary',
),
});
const exitCode = await runFixtureCommand(
fixture,
['request inaccessible work'],
(text) => stdout.push(text),
(text) => stderr.push(text),
);
assert.equal(exitCode, 1);
assert.equal(stdout.join(''), '');
assert.equal(
stderr.join(''),
'maka run: sandbox boundary expansion is unavailable in non-interactive mode\n',
);
});
test('keeps a sandbox failure unresolved after an unrelated tool succeeds', async () => {
const stdout: string[] = [];
const stderr: string[] = [];
const fixture = runFixture({
turnEvents: sandboxBoundaryEvents(
'turn-1',
'step-1',
'step-2',
'Boundary was not widened',
'tool_search',
),
});
const exitCode = await runFixtureCommand(
fixture,
['request inaccessible work'],
(text) => stdout.push(text),
(text) => stderr.push(text),
);
assert.equal(exitCode, 1);
assert.equal(stdout.join(''), '');
assert.equal(
stderr.join(''),
'maka run: sandbox boundary expansion is unavailable in non-interactive mode\n',
);
});
test('keeps the boundary unresolved when a later same-named call succeeds (live)', async () => {
const fixture = runFixture({
turnEvents: sandboxBoundaryEvents('turn-1', 'step-1', 'step-2', 'Partial answer', 'Read'),
});
const exitCode = await runFixtureCommand(fixture, ['accept same-name different target']);
assert.equal(exitCode, 1);
});
test('keeps the boundary unresolved when a later same-named call succeeds (durable)', async () => {
const fixture = runFixture({
graph: true,
finalMessages: sandboxBoundaryMessages('step-1', 'step-2', 'Read'),
});
const exitCode = await runFixtureCommand(fixture, [
'accept durable same-name different target',
'--graph',
]);
assert.equal(exitCode, 1);
});
test('returns exit code 1 when a denied widening precedes any later tool success', async () => {
const stderr: string[] = [];
const fixture = runFixture({
turnEvents: deniedWideningEvents('turn-1'),
});
const exitCode = await runFixtureCommand(
fixture,
['accept post-denial success'],
() => {},
(text) => stderr.push(text),
);
assert.equal(exitCode, 1);
assert.equal(
stderr.join(''),
'maka run: sandbox boundary expansion is unavailable in non-interactive mode\n',
);
});
test('returns exit code 1 when reconnect restores a missed sandbox failure', async () => {
let publishReplacement = () => {};
const fixture = runFixture({
turnEvents: eventsAfterTranscriptReplacement(() => publishReplacement()),
});
publishReplacement = () =>
fixture.publishTranscriptReplacement(
'turn-1',
[storedToolCall('turn-1', 'tool-1', 'step-1', 1), sandboxFailureToolResult('turn-1', 2)],
'reconnect',
);
const exitCode = await runFixtureCommand(fixture, ['resume protected work']);
assert.equal(exitCode, 1);
});
test('returns exit code 1 when one retry follows two sandbox failures', async () => {
const fixture = runFixture({
turnEvents: multipleSandboxFailureEvents('turn-1'),
});
const exitCode = await runFixtureCommand(fixture, ['retry one blocked operation']);
assert.equal(exitCode, 1);
});
test('returns exit code 0 when the final Graph Turn completes', async () => {
const stdout: string[] = [];
const fixture = runFixture({ graph: true });
const exitCode = await runFixtureCommand(fixture, ['delegate once', '--graph'], (text) =>
stdout.push(text),
);
assert.equal(exitCode, 0);
assert.equal(stdout.join(''), 'Final graph answer\n');
});
test('keeps a root Graph boundary failure unresolved even when a later same-named call succeeds', async () => {
const stdout: string[] = [];
const stderr: string[] = [];
const fixture = runFixture({
graph: true,
turnEvents: sandboxBoundaryEvents('turn-1', 'step-1', 'step-2', 'Recovered answer'),
});
const exitCode = await runFixtureCommand(
fixture,
['recover once', '--graph'],
(text) => stdout.push(text),
(text) => stderr.push(text),
);
assert.equal(exitCode, 1);
assert.equal(stdout.join(''), '');
assert.equal(
stderr.join(''),
'maka run: sandbox boundary expansion is unavailable in non-interactive mode\n',
);
});
test('returns exit code 1 when a same-step Graph sibling succeeds after a sandbox failure', async () => {
const fixture = runFixture({
graph: true,
finalMessages: sandboxBoundaryMessages('step-1', 'step-1'),
});
const exitCode = await runFixtureCommand(fixture, ['run parallel tools', '--graph']);
assert.equal(exitCode, 1);
});
test('returns exit code 1 when a denied Graph boundary request follows a sandbox failure', async () => {
const stdout: string[] = [];
const fixture = runFixture({
graph: true,
finalMessages: sandboxBoundaryMessages('step-1', 'step-2', 'request_sandbox_boundary'),
});
const exitCode = await runFixtureCommand(
fixture,
['request inaccessible work', '--graph'],
(text) => stdout.push(text),
);
assert.equal(exitCode, 1);
assert.equal(stdout.join(''), '');
});
test('returns exit code 1 when one Graph retry follows two sandbox failures', async () => {
const fixture = runFixture({
graph: true,
finalMessages: multipleSandboxFailureMessages(),
});
const exitCode = await runFixtureCommand(fixture, ['retry one blocked operation', '--graph']);
assert.equal(exitCode, 1);
});
test('returns exit code 1 when the final Graph Turn fails', async () => {
const stderr: string[] = [];
const fixture = runFixture({
graph: true,
finalMessages: failedGraphMessages('provider_failure'),
});
const exitCode = await runFixtureCommand(
fixture,
['delegate once', '--graph'],
undefined,
(text) => stderr.push(text),
);
assert.equal(exitCode, 1);
assert.equal(stderr.join(''), 'maka run: Agent Graph final Turn failed\n');
});
test('waits for Host-started graph supervisor Turns before returning', async () => {
const observed: MakaRunOutcome[] = [];
const fixture = runFixture({ observed, graph: true, graphProjectionRace: true });
const session = await fixture.context.runtime.createSession({
cwd: '/workspace',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
await collect(
fixture.context.runtime.sendMessage(session.id, {
turnId: 'turn-1',
text: 'delegate',
turnOrchestration: { mode: 'graph', source: 'host_api' },
}),
);
await fixture.context.agentGraph?.waitForCompletion(session.id);
assert.equal(observed.length, 2);
assert.equal(observed.at(-1)?.outcomeId, 'turn-2');
assert.equal(observed.at(-1)?.finalOutput, 'Final graph answer');
});
test('uses the durable Graph supervisor outcome independently of live projection', async () => {
const observed: MakaRunOutcome[] = [];
const fixture = runFixture({ observed, graph: true });
const session = await fixture.context.runtime.createSession({
cwd: '/workspace',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
await collect(
fixture.context.runtime.sendMessage(session.id, {
turnId: 'turn-1',
text: 'delegate',
turnOrchestration: { mode: 'graph', source: 'host_api' },
}),
);
await fixture.context.agentGraph?.waitForCompletion(session.id);
assert.equal(observed.at(-1)?.outcomeId, 'turn-2');
assert.equal(observed.at(-1)?.finalOutput, 'Final graph answer');
});
test('keeps the sandbox boundary unresolved across live and durable Turns', async () => {
const live = await observeFixtureOutcome({
turnEvents: sandboxBoundaryEvents('turn-1', 'step-1', 'step-2', 'Recovered answer'),
});
const durable = await observeFixtureOutcome({
graph: true,
finalMessages: sandboxBoundaryMessages('step-1', 'step-2'),
});
assert.equal(live.sandboxBoundary, 'unresolved');
assert.equal(durable.sandboxBoundary, 'unresolved');
});
test('leaves sandbox failures unresolved when their provider steps are unavailable', async () => {
const live = await observeFixtureOutcome({
turnEvents: sandboxBoundaryEvents('turn-1', undefined, undefined, 'Incomplete answer'),
});
const durable = await observeFixtureOutcome({
graph: true,
finalMessages: sandboxBoundaryMessages(undefined, undefined),
});
assert.equal(live.sandboxBoundary, 'unresolved');
assert.equal(durable.sandboxBoundary, 'unresolved');
});
test('keeps the boundary unresolved across interleaved successes and a later sandbox failure', async () => {
const outcome = await observeFixtureOutcome({
turnEvents: sandboxFailureAfterRecoveryEvents('turn-1'),
});
assert.equal(outcome.sandboxBoundary, 'unresolved');
});
test('classifies live and durable Turn cancellations as aborted', async () => {
const live = await observeFixtureOutcome({ turnEvents: abortedEvents('turn-1') });
const durable = await observeFixtureOutcome({
graph: true,
finalMessages: abortedGraphMessages(),
});
assert.equal(live.status, 'failed');
assert.equal(live.failure?.class, 'aborted');
assert.equal(durable.status, 'failed');
assert.equal(durable.failure?.class, 'aborted');
});
test('classifies live and durable step-cap failures equally', async () => {
const live = await observeFixtureOutcome({
turnEvents: completionEvents('turn-1', 'step_limit'),
});
const durable = await observeFixtureOutcome({
graph: true,
finalMessages: failedGraphMessages('tool_step_cap_reached'),
});
assert.equal(live.status, 'failed');
assert.equal(live.failure?.class, 'tool_step_cap_reached');
assert.equal(durable.status, 'failed');
assert.equal(durable.failure?.class, 'tool_step_cap_reached');
});
test('uses the latest durable terminal state for a Graph Turn', async () => {
const outcome = await observeFixtureOutcome({
graph: true,
finalMessages: failedThenCompletedGraphMessages(),
});
assert.equal(outcome.status, 'completed');
assert.equal(outcome.finalOutput, 'Final graph answer');
});
test('waits for the exact final Graph wake after an earlier wake already settled', async () => {
const observed: MakaRunOutcome[] = [];
const fixture = runFixture({ observed, graph: true, graphMultiWakeRace: true });
const session = await fixture.context.runtime.createSession({
cwd: '/workspace',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
await collect(
fixture.context.runtime.sendMessage(session.id, {
turnId: 'turn-1',
text: 'delegate twice',
turnOrchestration: { mode: 'graph', source: 'host_api' },
}),
);
await fixture.context.agentGraph?.waitForCompletion(session.id);
assert.equal(observed.at(-1)?.outcomeId, 'turn-3');
assert.equal(observed.at(-1)?.finalOutput, 'Final wake answer');
});
test('releases a pending Graph durable-terminal wait when the context closes', async () => {
const finalRead = deferred<void>();
const fixture = runFixture({
graph: true,
graphProjectionNeverCompletes: true,
onFinalGraphRead: () => finalRead.resolve(),
});
const context = fixture.context;
const session = await context.runtime.createSession({
cwd: '/workspace',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
await collect(
context.runtime.sendMessage(session.id, {
turnId: 'turn-1',
text: 'delegate',
turnOrchestration: { mode: 'graph', source: 'host_api' },
}),
);
const waiting = context.agentGraph?.waitForCompletion(session.id);
assert.ok(waiting);
await finalRead.promise;
await new Promise((resolve) => setImmediate(resolve));
await context.close();
await assert.rejects(waiting, new Error('Runtime Host run context closed'));
});
test('does not reuse a historical Graph outcome when this execution has no successor', async () => {
const observed: MakaRunOutcome[] = [];
const historical = graphMessages();
const fixture = runFixture({
observed,
graph: true,
initialMessages: historical,
finalMessages: historical,
});
const session = await fixture.context.runtime.createSession({
cwd: '/workspace',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
await collect(
fixture.context.runtime.sendMessage(session.id, {
turnId: 'turn-1',
text: 'finish without another wake',
turnOrchestration: { mode: 'graph', source: 'host_api' },
}),
);
await fixture.context.agentGraph?.waitForCompletion(session.id);
assert.equal(observed.length, 1);
assert.equal(observed[0]?.finalOutput, 'Host answer');
});
test('applies the requested step cap through the Host turn', async () => {
const fixture = runFixture({ maxSteps: 3 });
const session = await fixture.context.runtime.createSession({
cwd: '/workspace',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
await collect(
fixture.context.runtime.sendMessage(session.id, {
turnId: 'turn-with-step-cap',
text: 'answer within the cap',
}),
);
assert.deepEqual(fixture.preparedMaxSteps, [3]);
});
test('never selects a discovered model that the Host has not enabled', () => {
const catalog = {
revision: 1,
defaultTarget: null,
connections: [
{
connectionId: 'connection-1',
revision: 1,
slug: 'openai-main',
name: 'OpenAI',
providerType: 'openai' as const,
enabled: true,
enabledModelIds: ['gpt-5'],
catalogEntries: [],
models: [{ id: 'gpt-5' }, { id: 'gpt-6-preview' }],
},
],
};
assert.equal(
resolveRuntimeHostCliTarget(catalog, { connectionSlug: 'openai-main' }).model,
'gpt-5',
);
assert.throws(
() =>
resolveRuntimeHostCliTarget(catalog, {
connectionSlug: 'openai-main',
model: 'gpt-6-preview',
}),
new Error('Runtime Host model is unavailable for openai-main: gpt-6-preview'),
);
});
test('stops both the active Host Turn and its Graph on cancellation', async () => {
const fixture = runFixture({ graph: true });
const session = await fixture.context.runtime.createSession({
cwd: '/workspace',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
await fixture.context.runtime.stopSession(session.id);
assert.equal(fixture.turnStops, 1);
assert.deepEqual(fixture.graphStops, [session.id]);
});
test('stops a Turn that starts after cancellation was requested', async () => {
const prepareGate = deferred<void>();
const prepareStarted = deferred<void>();
const fixture = runFixture({
graph: true,
strictTurnStopInput: true,
prepareGate: prepareGate.promise,
onPrepareStarted: () => prepareStarted.resolve(),
});
const session = await fixture.context.runtime.createSession({
cwd: '/workspace',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
const sending = collect(
fixture.context.runtime.sendMessage(session.id, {
turnId: 'turn-1',
text: 'answer once',
turnOrchestration: { mode: 'graph', source: 'host_api' },
}),
);
await prepareStarted.promise;
await fixture.context.runtime.stopSession(session.id);
prepareGate.resolve();
await sending;
assert.deepEqual(fixture.exactTurnStops, [
{ sessionId: session.id, turnId: 'turn-1', runId: 'run-1' },
]);
assert.deepEqual(fixture.graphStops, [session.id, session.id]);
});
test('fails and stops instead of waiting for an interactive question', async () => {
const fixture = runFixture({
turnEvents: questionEvents('turn-1'),
pendingInteractions: [pendingQuestion('turn-1')],
pendingAfterTurnStarts: true,
});
const session = await fixture.context.runtime.createSession({
cwd: '/workspace',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
await assert.rejects(
collect(
fixture.context.runtime.sendMessage(session.id, {
turnId: 'turn-1',
text: 'ask me something',
}),
),
new Error('interactive user questions are unavailable in non-interactive mode'),
);
assert.deepEqual(fixture.exactTurnStops, [
{ sessionId: session.id, turnId: 'turn-1', runId: 'run-1' },
]);
});
test('fails and stops instead of dropping an interactive form', async () => {
const fixture = runFixture({
turnEvents: formEvents('turn-1'),
pendingInteractions: [pendingForm('turn-1')],
pendingAfterTurnStarts: true,
});
const session = await fixture.context.runtime.createSession({
cwd: '/workspace',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
await assert.rejects(
collect(
fixture.context.runtime.sendMessage(session.id, {
turnId: 'turn-1',
text: 'configure deployment',
}),
),
new Error('interactive user forms are unavailable in non-interactive mode'),
);
assert.deepEqual(fixture.exactTurnStops, [
{ sessionId: session.id, turnId: 'turn-1', runId: 'run-1' },
]);
});
test('stops Graph Mode when a successor waits for an interactive question', async () => {
const fixture = runFixture({
graph: true,
pendingInteractions: [pendingQuestion('turn-2')],
});
const session = await fixture.context.runtime.createSession({
cwd: '/workspace',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
const graph = fixture.context.agentGraph;
assert.ok(graph);
await assert.rejects(
graph.waitForCompletion(session.id),
new Error('interactive user questions are unavailable in non-interactive mode'),
);
assert.deepEqual(fixture.graphStops, [session.id]);
});
test('preserves the interaction error when Graph stop races an in-flight query', async () => {
const queryStarted = deferred<void>();
const queryGate = deferred<void>();
const graphStopped = deferred<void>();
const fixture = runFixture({
graph: true,
graphQueryGate: queryGate.promise,
graphQueryStatus: 'stopped',
onGraphQueryStarted: () => queryStarted.resolve(),
onGraphStop: () => graphStopped.resolve(),
});
const session = await fixture.context.runtime.createSession({
cwd: '/workspace',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
const waiting = fixture.context.agentGraph?.waitForCompletion(session.id);
assert.ok(waiting);
await queryStarted.promise;
fixture.publishPendingInteraction(pendingQuestion('turn-2'));
await graphStopped.promise;
queryGate.resolve();
await assert.rejects(
waiting,
new Error('interactive user questions are unavailable in non-interactive mode'),
);
});
test('denies a Graph successor sandbox expansion in non-interactive mode', async () => {
const fixture = runFixture({
graph: true,
pendingInteractions: [
{
schemaVersion: 1,
sessionId: 'session-created',
turnId: 'turn-2',
runId: 'run-2',
interactionId: 'boundary-1',
revision: 1,
status: 'pending',
outcome: null,
request: {
kind: 'sandbox_boundary',
justification: 'Needs broader access',
expansion: {
filesystem: {
entries: [{ path: '/outside', access: 'read', scope: 'subtree' }],
},
},
},
},
],
});
const session = await fixture.context.runtime.createSession({
cwd: '/workspace',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
await fixture.context.agentGraph?.waitForCompletion(session.id);
assert.deepEqual(fixture.sandboxResponses, [{ requestId: 'boundary-1', decision: 'deny' }]);
});
});
function publicCommandContext(input: MakaRunContextInput, onCreate: () => void = () => {}) {
return {
runtime: {
createSession: async () => {
onCreate();
return sessionSummary('session-public');
},
readExecutionBoundary: async () => ({
kind: 'managed' as const,
access: 'writable' as const,
revision: 0,
}),
sendMessage: async function* (_sessionId: string, message: { turnId: string }) {
yield* eventsFor(message.turnId, 'Host answer');
await input.runOutcomeObserver?.({
outcomeId: 'run-public',
status: 'completed',
finalOutput: 'Host answer',
sandboxBoundary: 'none',
});
},
respondToSandboxBoundary: async () => {},
stopSession: async () => {},
setExecutionBoundaryKind: async () => {},
},
target: { connection: { slug: 'openai-main' }, model: 'gpt-5' },
close: async () => {},
};
}
function runFixture(input: {
observed?: MakaRunOutcome[];
graph?: boolean;
maxSteps?: number;
prepareGate?: Promise<void>;
onPrepareStarted?: () => void;
turnEvents?: AsyncIterable<SessionEvent>;
pendingInteractions?: InteractionPendingSnapshot[];
pendingAfterTurnStarts?: boolean;
graphProjectionRace?: boolean;
graphMultiWakeRace?: boolean;
graphProjectionNeverCompletes?: boolean;
onFinalGraphRead?: () => void;
sessionCwdOverride?: { sessionId: string; cwd: string };
switchSummaryCwd?: string;
graphQueryGate?: Promise<void>;
graphQueryStatus?: 'completed' | 'stopped';
onGraphQueryStarted?: () => void;
onGraphStop?: () => void;
initialMessages?: StoredMessage[];
finalMessages?: StoredMessage[];
strictTurnStopInput?: boolean;
}) {
const switches: string[] = [];
const moves: string[] = [];
const graphStops: string[] = [];
const exactTurnStops: { sessionId: string; turnId: string; runId: string }[] = [];
const sandboxResponses: { requestId: string; decision: 'deny' }[] = [];
let turnStops = 0;
const pendingInteractionListeners = new Set<(pending: InteractionPendingSnapshot) => void>();
const transcriptListeners = new Set<
(
sessionId: string,
turnId: string,
messages: StoredMessage[],
reason: MakaTranscriptReplacementReason,
) => void
>();
let messageReads = 0;
const preparedMaxSteps: Array<number | undefined> = [];
const driver = {
createSession: async () => sessionSummary('session-created'),
readMessages: async () => {
messageReads += 1;
if (messageReads === 1) {
if (input.graphMultiWakeRace) {
queueMicrotask(() => {
for (const listener of transcriptListeners) {
listener('session-created', 'turn-2', structuredClone(graphMessages()), 'reconcile');
}
});
}
return structuredClone(input.initialMessages ?? []);
}
if (input.graphMultiWakeRace) {
queueMicrotask(() => {
const terminal = multiWakeGraphMessages(true);
for (const listener of transcriptListeners) {
listener('session-created', 'turn-3', structuredClone(terminal), 'reconcile');
}
});
return multiWakeGraphMessages(false);
}
if (input.graphProjectionNeverCompletes) {
input.onFinalGraphRead?.();
return graphMessages(false);
}
const messages = input.finalMessages ?? graphMessages();
if (input.graphProjectionRace) {
queueMicrotask(() => {
for (const listener of transcriptListeners) {
listener('session-created', 'turn-2', structuredClone(messages), 'reconcile');
}
});
return graphMessages(false);
}
return structuredClone(messages);
},
listPendingInteractions: () => input.pendingInteractions ?? [],
subscribePendingInteractions: (listener: (pending: InteractionPendingSnapshot) => void) => {
pendingInteractionListeners.add(listener);
if (!input.pendingAfterTurnStarts) {
for (const pending of input.pendingInteractions ?? []) {
queueMicrotask(() => listener(structuredClone(pending)));
}
}
return () => pendingInteractionListeners.delete(listener);
},
switchSession: async (sessionId: string) => {
switches.push(sessionId);
return {
summary: { ...sessionSummary(sessionId), cwd: input.switchSummaryCwd ?? '/workspace' },
messages: [],
};
},
moveSession: async (cwd: string) => {
moves.push(cwd);
return {
previousCwd: input.switchSummaryCwd ?? '/workspace',
cwd,
changed: true,
oldCwdDirty: false,
};
},
preparePrompt: async (
_prompt: string,
options: { turnId?: string; maxSteps?: number } = {},
) => {
preparedMaxSteps.push(options.maxSteps);
input.onPrepareStarted?.();
await input.prepareGate;
const events = input.turnEvents ?? eventsFor(options.turnId ?? 'turn-1', 'Host answer');
return {
sessionId: switches.at(-1) ?? 'session-created',
turnId: options.turnId ?? 'turn-1',
runId: 'run-1',
events: input.pendingAfterTurnStarts
? eventsAfterPendingNotification(
events,
pendingInteractionListeners,
input.pendingInteractions ?? [],
)
: events,
};
},
respondToSandboxBoundary: async (response: { requestId: string; decision: 'deny' }) => {
sandboxResponses.push(response);
},
setPermissionMode: async () => {},
stop: async () => {
turnStops += 1;
},
subscribeStartedTurns: () => () => {},
subscribeTranscriptReplacements: (
listener: (
sessionId: string,
turnId: string,
messages: StoredMessage[],
reason: MakaTranscriptReplacementReason,
) => void,
) => {
transcriptListeners.add(listener);
return () => transcriptListeners.delete(listener);
},
} as unknown as RuntimeHostMakaSessionDriver;
const connection = {
hostEpoch: 'host-1',
request: async (operation: string, requestInput: Record<string, unknown>) => {
if (operation === 'session.execution_boundary.query') {
return { kind: 'managed', access: 'writable', revision: 0 };
}
if (operation === 'agent.graph.query') {
input.onGraphQueryStarted?.();
await input.graphQueryGate;
return { status: input.graphQueryStatus ?? 'completed' };
}
if (operation === 'agent.graph.stop') {
graphStops.push(String(requestInput.rootSessionId));
input.onGraphStop?.();
return { rootSessionId: requestInput.rootSessionId, graphId: 'graph-1' };
}
if (operation === 'turn.stop') {
if (input.strictTurnStopInput) {
const allowed = new Set(['sessionId', 'turnId', 'runId']);
const unexpected = Object.keys(requestInput).filter((key) => !allowed.has(key));
if (unexpected.length > 0) {
throw new Error(`Unknown turn.stop input field: ${unexpected.join(', ')}`);
}
}
exactTurnStops.push({
sessionId: String(requestInput.sessionId),
turnId: String(requestInput.turnId),
runId: String(requestInput.runId),
});
return { kind: 'stopped' };
}
throw new Error(`Unexpected operation: ${operation}`);
},
} as unknown as RuntimeHostConnection;
const createContext = (contextInput: MakaRunContextInput) =>
createRuntimeHostRunContext(connection, connectionCatalog(), contextInput, {
createDriver: () => driver,
});
let create = () =>
createContext({
workspaceRoot: '/data',
cwd: '/workspace',
...(input.graph ? { enableAgentGraph: true } : {}),
...(input.maxSteps ? { maxSteps: input.maxSteps } : {}),
...(input.sessionCwdOverride ? { sessionCwdOverride: input.sessionCwdOverride } : {}),
...(input.observed
? {
runOutcomeObserver: (result: MakaRunOutcome) => {
input.observed?.push(result);
},
}
: {}),
});
return {
get context() {
const context = create();
create = () => context;
return context;
},
switches,
moves,
graphStops,
exactTurnStops,
preparedMaxSteps,
sandboxResponses,
createContext,
publishPendingInteraction(pending: InteractionPendingSnapshot) {
for (const listener of pendingInteractionListeners) listener(structuredClone(pending));
},
publishTranscriptReplacement(
turnId: string,
messages: StoredMessage[],
reason: MakaTranscriptReplacementReason,
) {
for (const listener of transcriptListeners) {
listener('session-created', turnId, structuredClone(messages), reason);
}
},
get turnStops() {
return turnStops;
},
};
}
async function observeFixtureOutcome(
input: Parameters<typeof runFixture>[0],
): Promise<MakaRunOutcome> {
const observed: MakaRunOutcome[] = [];
const fixture = runFixture({ ...input, observed });
const session = await fixture.context.runtime.createSession({
cwd: '/workspace',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
await collect(
fixture.context.runtime.sendMessage(session.id, {
turnId: 'turn-1',
text: 'observe outcome',
...(input.graph
? { turnOrchestration: { mode: 'graph' as const, source: 'host_api' as const } }
: {}),
}),
);
if (input.graph) await fixture.context.agentGraph?.waitForCompletion(session.id);
const outcome = observed.at(-1);
assert.ok(outcome);
return outcome;
}
function runFixtureCommand(
fixture: ReturnType<typeof runFixture>,
argv: readonly string[],
writeStdout: (text: string) => void = () => {},
writeStderr: (text: string) => void = () => {},
): Promise<number> {
return runRuntimeHostTextCli(
argv,
{ ...publicCommandEnvironment(), writeStdout, writeStderr },
{
connect: async () => ({
connection: readinessConnection(),
catalog: connectionCatalog(),
profile: LOCAL_RUNTIME_HOST_PROFILE,
close: async () => {},
}),
createContext: (_connection, _catalog, input) => fixture.createContext(input),
},
);
}
function publicCommandEnvironment() {
return {
workspaceRoot: () => '/runtime-host-data',
processCwd: () => process.cwd(),
stdinIsTTY: () => true,
readStdin: async () => '',
writeStderr: () => {},
onSigint: () => () => {},
newId: () => 'turn-1',
};
}
function connectionCatalog() {
return {
revision: 1,
defaultTarget: { connectionId: 'connection-1', modelId: 'gpt-5' },
connections: [
{
connectionId: 'connection-1',
revision: 1,
slug: 'openai-main',
name: 'OpenAI',
providerType: 'openai' as const,
enabled: true,
enabledModelIds: ['gpt-5'],
catalogEntries: [],
models: [{ id: 'gpt-5' }],
},
],
};
}
function readinessConnection(): RuntimeHostConnection {
return {
request: async (operation: string) => {
if (operation !== 'credential.vault.query') {
throw new Error(`Unexpected readiness operation: ${operation}`);
}
return {
kind: 'status',
status: {
locator: { scope: 'connection', connectionId: 'connection-1', kind: 'api_key' },
configured: true,
credentialId: 'credential-1',
revision: 1,
updatedAt: 1,
},
};
},
} as unknown as RuntimeHostConnection;
}
function remoteReadinessConnection(): RuntimeHostConnection {
return {
request: async (operation: string) => {
if (operation === 'session.catalog.query') {
return {
kind: 'page',
revision: 1,
nextCursor: null,
sessions: [sessionProjection('session-existing')],
};
}
if (operation === 'project.catalog.query') {
return {
kind: 'page',
view: 'summary',
revision: `sha256:${'1'.repeat(64)}`,
projectCount: 1,
items: [
{
kind: 'project',
projectIndex: 0,
id: 'project-1',
name: 'Project',
aliasCount: 1,
locationCount: 1,
preferredLocationIndex: 0,
archivedAt: null,
available: true,
},
{
kind: 'alias',
projectIndex: 0,
itemIndex: 0,
alias: 'project-old',
},
],
nextCursor: null,
};
}
if (operation === 'credential.vault.query') {
return {
kind: 'status',
status: {
locator: { scope: 'connection', connectionId: 'connection-1', kind: 'api_key' },
configured: true,
credentialId: 'credential-1',
revision: 1,
updatedAt: 1,
},
};
}
throw new Error(`Unexpected readiness operation: ${operation}`);
},
} as unknown as RuntimeHostConnection;
}
async function* questionEvents(turnId: string): AsyncIterable<SessionEvent> {
yield {
type: 'user_question_request',
id: `${turnId}-question`,
turnId,
ts: 1,
requestId: 'question-1',
toolUseId: 'tool-1',
questions: [
{
question: 'Choose one',
options: [{ label: 'One' }, { label: 'Two' }],
},
],
};
}
async function* formEvents(turnId: string): AsyncIterable<SessionEvent> {
yield {
type: 'form_request',
id: `${turnId}-form`,
turnId,
ts: 1,
requestId: 'form-1',
toolUseId: 'tool-1',
message: 'Configure deployment',
requester: { name: 'deploy', source: 'Acme MCP' },
fields: [{ kind: 'string', name: 'version', label: 'Version', required: true }],
};
}
function pendingQuestion(turnId: string): InteractionPendingSnapshot {
return {
schemaVersion: 1,
interactionId: 'interaction-1',
sessionId: 'session-created',
turnId,
runId: turnId === 'turn-1' ? 'run-1' : 'run-2',
revision: 1,
status: 'pending',
outcome: null,
request: {
kind: 'question',
toolUseId: 'tool-question',
questions: [{ question: 'Continue?', options: [{ label: 'Yes' }] }],
},
};
}
function pendingForm(turnId: string): InteractionPendingSnapshot {
return {
schemaVersion: 1,
interactionId: 'form-1',
sessionId: 'session-created',
turnId,
runId: turnId === 'turn-1' ? 'run-1' : 'run-2',
revision: 1,
status: 'pending',
outcome: null,
request: {
kind: 'form',
toolUseId: 'tool-form',
message: 'Configure deployment',
requester: { name: 'deploy', source: 'Acme MCP' },
fields: [{ kind: 'string', name: 'version', label: 'Version', required: true }],
},
};
}
function pendingPermission(turnId: string): InteractionPendingSnapshot {
return {
schemaVersion: 1,
interactionId: 'permission-1',
sessionId: 'session-created',
turnId,
runId: 'run-1',
revision: 1,
status: 'pending',
outcome: null,
request: {
kind: 'permission',
toolUseId: 'tool-permission',
prompt: {
kind: 'tool_permission',
toolName: 'Bash',
category: 'shell_unsafe',
reason: 'shell_dangerous',
review: { kind: 'command', command: 'echo protected', cwd: '/workspace' },
rememberForTurnAllowed: true,
},
},
};
}
function graphMessages(includeTerminal = true): StoredMessage[] {
const messages: StoredMessage[] = [
{
type: 'user',
id: 'user-turn-2',
turnId: 'turn-2',
ts: 3,
text: 'Graph wake',
origin: {
kind: 'agent_graph',
graphId: 'graph-1',
wakeId: 'wake-1',
attemptId: 'attempt-1',
},
},
{
type: 'assistant',
id: 'assistant-turn-2',
turnId: 'turn-2',
ts: 4,
text: 'Final graph answer',
modelId: 'gpt-5',
},
];
if (includeTerminal) {
messages.push({
type: 'turn_state',
id: 'state-turn-2',
turnId: 'turn-2',
ts: 5,
status: 'completed',
});
}
return messages;
}
function sandboxBoundaryMessages(
failureStepId: string | undefined,
successStepId: string | undefined,
successToolName = 'Read',
): StoredMessage[] {
const sameStep = failureStepId !== undefined && failureStepId === successStepId;
return [
...graphMessages(false),
...(failureStepId === undefined ? [] : [storedToolCall('turn-2', 'tool-1', failureStepId, 5)]),
...(sameStep ? [storedToolCall('turn-2', 'tool-2', successStepId, 6, successToolName)] : []),
sandboxFailureToolResult('turn-2', 7),
...(successStepId === undefined || sameStep
? []
: [storedToolCall('turn-2', 'tool-2', successStepId, 8, successToolName)]),
successfulToolResult('turn-2', 9),
{
type: 'turn_state',
id: 'state-turn-2',
turnId: 'turn-2',
ts: 10,
status: 'completed',
},
];
}
function multipleSandboxFailureMessages(): StoredMessage[] {
return [
...graphMessages(false),
storedToolCall('turn-2', 'tool-1', 'step-1', 5),
storedToolCall('turn-2', 'tool-2', 'step-1', 6),
sandboxFailureToolResult('turn-2', 7, 'tool-1'),
sandboxFailureToolResult('turn-2', 8, 'tool-2'),
storedToolCall('turn-2', 'tool-3', 'step-2', 9),
successfulToolResult('turn-2', 10, 'tool-3'),
{
type: 'turn_state',
id: 'state-turn-2',
turnId: 'turn-2',
ts: 11,
status: 'completed',
},
];
}
function abortedGraphMessages(): StoredMessage[] {
return [
...graphMessages(false),
{
type: 'turn_state',
id: 'state-turn-2',
turnId: 'turn-2',
ts: 5,
status: 'aborted',
abortSource: 'user_interrupt',
},
];
}
function failedGraphMessages(errorClass: string): StoredMessage[] {
return [
...graphMessages(false),
{
type: 'turn_state',
id: 'state-turn-2',
turnId: 'turn-2',
ts: 5,
status: 'failed',
errorClass,
},
];
}
function failedThenCompletedGraphMessages(): StoredMessage[] {
return [
...failedGraphMessages('provider_failure'),
{
type: 'turn_state',
id: 'completed-state-turn-2',
turnId: 'turn-2',
ts: 6,
status: 'completed',
},
];
}
function multiWakeGraphMessages(includeFinalTerminal: boolean): StoredMessage[] {
const messages = [
...graphMessages(),
{
type: 'user' as const,
id: 'user-turn-3',
turnId: 'turn-3',
ts: 6,
text: 'Final Graph wake',
origin: {
kind: 'agent_graph' as const,
graphId: 'graph-1',
wakeId: 'wake-2',
attemptId: 'attempt-2',
},
},
{
type: 'assistant' as const,
id: 'assistant-turn-3',
turnId: 'turn-3',
ts: 7,
text: 'Final wake answer',
modelId: 'gpt-5',
},
];
if (includeFinalTerminal) {
messages.push({
type: 'turn_state',
id: 'state-turn-3',
turnId: 'turn-3',
ts: 8,
status: 'completed',
});
}
return messages;
}
async function* eventsFor(turnId: string, text: string, ts = 1): AsyncIterable<SessionEvent> {
yield {
type: 'text_complete',
id: `${turnId}-text`,
turnId,
messageId: `${turnId}-message`,
ts,
text,
};
yield { type: 'complete', id: `${turnId}-complete`, turnId, ts: ts + 1, stopReason: 'end_turn' };
}
async function* eventsAfterTranscriptReplacement(publish: () => void): AsyncIterable<SessionEvent> {
publish();
yield* eventsFor('turn-1', 'Incomplete answer', 3);
}
async function* sandboxBoundaryEvents(
turnId: string,
failureStepId: string | undefined,
successStepId: string | undefined,
text: string,
successToolName = 'Read',
): AsyncIterable<SessionEvent> {
const sameStep = failureStepId !== undefined && failureStepId === successStepId;
if (failureStepId !== undefined) {
yield toolStart(turnId, 'tool-1', failureStepId, 1);
}
if (sameStep) {
yield toolStart(turnId, 'tool-2', successStepId, 2, successToolName);
}
yield sandboxFailureToolResult(turnId, 3);
if (successStepId !== undefined && !sameStep) {
yield toolStart(turnId, 'tool-2', successStepId, 4, successToolName);
}
yield successfulToolResult(turnId, 5);
yield* eventsFor(turnId, text, 6);
}
async function* deniedWideningEvents(turnId: string): AsyncIterable<SessionEvent> {
yield toolStart(turnId, 'tool-1', 'step-1', 1);
yield sandboxFailureToolResult(turnId, 2);
yield toolStart(turnId, 'tool-2', 'step-2', 3, 'request_sandbox_boundary');
yield successfulToolResult(turnId, 4, 'tool-2');
yield toolStart(turnId, 'tool-3', 'step-3', 5);
yield successfulToolResult(turnId, 6, 'tool-3');
yield* eventsFor(turnId, 'Recovered answer', 7);
}
async function* projectedSameStepSandboxFailureEvents(turnId: string): AsyncIterable<SessionEvent> {
yield toolStart(turnId, 'tool-1', 'step-1', 1);
yield toolStart(turnId, 'tool-2', 'step-1', 2);
const initial = continuitySnapshot(turnId);
const projector = new RuntimeHostSessionProjector(
initial,
createRuntimeHostSessionProjectionSeed([], initial),
() => 10,
);
yield* projector.accept({
kind: 'subscription.session_event',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
sessionId: 'session-created',
runId: 'run-1',
event: {
type: 'tool_result',
id: 'tool-1-result',
turnId,
ts: 3,
toolUseId: 'tool-1',
status: 'errored',
sandboxFailureReason: 'sandbox_boundary_required',
},
} satisfies SubscriptionFrame).events;
yield successfulToolResult(turnId, 4);
yield* eventsFor(turnId, 'Incomplete answer', 5);
}
async function* multipleSandboxFailureEvents(turnId: string): AsyncIterable<SessionEvent> {
yield toolStart(turnId, 'tool-1', 'step-1', 1);
yield toolStart(turnId, 'tool-2', 'step-1', 2);
yield sandboxFailureToolResult(turnId, 3, 'tool-1');
yield sandboxFailureToolResult(turnId, 4, 'tool-2');
yield toolStart(turnId, 'tool-3', 'step-2', 5);
yield successfulToolResult(turnId, 6, 'tool-3');
yield* eventsFor(turnId, 'Incomplete answer', 7);
}
function continuitySnapshot(
turnId: string,
overrides: Partial<SessionContinuitySnapshot> = {},
): SessionContinuitySnapshot {
return {
schemaVersion: SESSION_CONTINUITY_SCHEMA_VERSION,
session: {
sessionId: 'session-created',
metadataRevision: 1,
status: 'running',
createdAt: 1,
isArchived: false,
},
projectionRevision: 1,
rootTurn: {
sessionId: 'session-created',
turnId,
runId: 'run-1',
status: 'running',
},
goal: null,
queue: { hostEpoch: 'host-1', queueRevision: 0, steering: [], followup: [] },
interactions: { pending: [] },
...overrides,
};
}
async function* sandboxFailureAfterRecoveryEvents(turnId: string): AsyncIterable<SessionEvent> {
yield toolStart(turnId, 'tool-1', 'step-1', 1);
yield sandboxFailureToolResult(turnId, 2);
yield toolStart(turnId, 'tool-2', 'step-2', 3);
yield successfulToolResult(turnId, 4);
yield toolStart(turnId, 'tool-3', 'step-3', 5);
yield sandboxFailureToolResult(turnId, 6, 'tool-3');
yield* eventsFor(turnId, 'Incomplete answer', 7);
}
async function* abortedEvents(turnId: string): AsyncIterable<SessionEvent> {
yield {
type: 'abort',
id: `${turnId}-abort`,
turnId,
ts: 1,
reason: 'user_stop',
};
yield {
type: 'complete',
id: `${turnId}-complete`,
turnId,
ts: 2,
stopReason: 'user_stop',
};
}
async function* failedEvents(turnId: string, reason: string): AsyncIterable<SessionEvent> {
yield {
type: 'text_complete',
id: `${turnId}-text`,
turnId,
messageId: `${turnId}-message`,
ts: 1,
text: 'Partial answer',
};
yield {
type: 'error',
id: `${turnId}-error`,
turnId,
ts: 2,
recoverable: false,
reason,
message: 'Turn failed',
};
yield {
type: 'complete',
id: `${turnId}-complete`,
turnId,
ts: 3,
stopReason: 'error',
};
}
async function* completionEvents(
turnId: string,
stopReason: Extract<SessionEvent, { type: 'complete' }>['stopReason'],
): AsyncIterable<SessionEvent> {
yield {
type: 'complete',
id: `${turnId}-complete`,
turnId,
ts: 1,
stopReason,
};
}
type SharedToolResult = Extract<SessionEvent, { type: 'tool_result' }> &
Extract<StoredMessage, { type: 'tool_result' }>;
function toolStart(
turnId: string,
toolUseId: string,
stepId: string,
ts: number,
toolName = 'Read',
): Extract<SessionEvent, { type: 'tool_start' }> {
return {
type: 'tool_start',
id: `${turnId}-${toolUseId}-start`,
turnId,
ts,
toolUseId,
toolName,
args: {},
stepId,
};
}
function storedToolCall(
turnId: string,
toolUseId: string,
stepId: string,
ts: number,
toolName = 'Read',
): Extract<StoredMessage, { type: 'tool_call' }> {
return {
type: 'tool_call',
id: toolUseId,
turnId,
ts,
toolName,
args: {},
stepId,
};
}
function sandboxFailureToolResult(
turnId: string,
ts: number,
toolUseId = 'tool-1',
): SharedToolResult {
return {
type: 'tool_result',
id: `${turnId}-${toolUseId}-sandbox-failure`,
turnId,
ts,
toolUseId,
isError: true,
content: sandboxFailureContent(),
};
}
function successfulToolResult(turnId: string, ts: number, toolUseId = 'tool-2'): SharedToolResult {
return {
type: 'tool_result',
id: `${turnId}-${toolUseId}-success`,
turnId,
ts,
toolUseId,
isError: false,
content: { kind: 'text', text: 'ok' },
};
}
function sandboxFailureContent() {
return {
kind: 'text' as const,
text: 'Write requires an approved sandbox boundary expansion.',
sandboxFailure: {
reason: 'sandbox_boundary_required' as const,
requiredExpansion: {
filesystem: {
entries: [{ path: '/outside', access: 'write' as const, scope: 'subtree' as const }],
},
},
},
};
}
async function* eventsAfterPendingNotification(
events: AsyncIterable<SessionEvent>,
listeners: ReadonlySet<(pending: InteractionPendingSnapshot) => void>,
pending: readonly InteractionPendingSnapshot[],
): AsyncIterable<SessionEvent> {
let notified = false;
for await (const event of events) {
if (!notified) {
notified = true;
for (const interaction of pending) {
for (const listener of listeners) listener(structuredClone(interaction));
}
}
yield event;
}
}
async function collect(events: AsyncIterable<SessionEvent>): Promise<SessionEvent[]> {
const collected: SessionEvent[] = [];
for await (const event of events) collected.push(event);
return collected;
}
function sessionProjection(id: string): SessionCatalogProjection {
return {
id,
revision: 1,
workspace: {
target: { kind: 'host_path', path: '/workspace' },
hostCwd: '/workspace',
},
createdAt: 1,
activityAt: 1,
name: 'Run once',
isFlagged: false,
isArchived: false,
labels: [],
labelsTruncated: false,
hasUnread: false,
status: 'active',
backend: 'ai-sdk',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
connectionLocked: true,
model: 'gpt-5',
permissionMode: 'ask',
collaborationMode: 'agent',
orchestrationMode: 'default',
};
}
function sessionSummary(id: string): SessionSummary {
return {
id,
cwd: '/workspace',
name: 'Run once',
isFlagged: false,
isArchived: false,
labels: [],
hasUnread: false,
status: 'active',
backend: 'ai-sdk',
llmConnectionSlug: 'openai-main',
connectionLocked: true,
model: 'gpt-5',
permissionMode: 'ask',
collaborationMode: 'agent',
orchestrationMode: 'default',
};
}