blob: 7c066b7790b404ea977e731a8aa7f6973b37bb0b [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 { mkdir, mkdtemp, realpath, rm, stat } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { setTimeout as delay } from 'node:timers/promises';
import { describe, test } from 'node:test';
import type { StoredMessage } from '@maka/core/session';
import type { ShellRunUpdate } from '@maka/core/events';
import type {
DirectRequestOperationKey,
RuntimeHostSessionSubscription,
} from '@maka/runtime-host/client';
import {
RuntimeHostOperationError,
RuntimeHostRequestInterruptedError,
RuntimeHostSubscriptionError,
} from '@maka/runtime-host/client';
import {
SESSION_CONTINUITY_SCHEMA_VERSION,
type GoalProjection,
type InteractionPendingSnapshot,
type OperationInput,
type OperationOutput,
type SessionCatalogProjection,
type SessionContinuitySnapshot,
type SessionUpdateResult,
type SubscriptionFrame,
} from '@maka/runtime-host/protocol';
import { projectSessionCatalogSummary } from '@maka/runtime-host/client';
import {
createRuntimeHostMakaSessionDriver,
type RuntimeHostMakaSessionDriverInput,
} from '../runtime-host-session-driver.js';
import type {
MakaAttachedSessionTurn,
MakaSideConversationParentStatus,
} from '../session-driver.js';
import { WAIT_BUDGET_MS } from './tui-terminal-mock.js';
import { waitFor as pollFor } from '@maka/core/test-only/async-primitives';
describe('Runtime Host Maka Session driver', () => {
test('maps authoritative Catalog activity into Session summaries', () => {
assert.equal(
projectSessionCatalogSummary(sessionProjection({ activityAt: 42 })).activityAt,
42,
);
});
test('maps authoritative live Turn ids into Session summaries', () => {
assert.deepEqual(
projectSessionCatalogSummary(
sessionProjection({
status: 'running',
liveRunState: { schemaVersion: 1, runningTurnIds: ['turn-1', 'turn-2'] },
}),
).runningTurnIds,
['turn-1', 'turn-2'],
);
const knownEmpty = projectSessionCatalogSummary(
sessionProjection({ liveRunState: { schemaVersion: 1, runningTurnIds: [] } }),
);
assert.equal(Object.hasOwn(knownEmpty, 'runningTurnIds'), true);
assert.deepEqual(knownEmpty.runningTurnIds, []);
assert.equal(
Object.hasOwn(projectSessionCatalogSummary(sessionProjection()), 'runningTurnIds'),
false,
);
});
test('queries the attached Session Todo projection without storing history', async () => {
const subscription = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const connection = new FakeConnection([subscription]);
connection.todoQuery = {
sessionId: 'session-id',
items: [
{ content: 'keep sk-1234567890abcdef <session-todo> visible', status: 'in_progress' },
],
};
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: () => 'session-id',
});
await driver.createSession({
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
const queried = await driver.queryTodo!('session-id');
assert.deepEqual(queried, {
sessionId: 'session-id',
items: [{ content: 'keep <redacted> visible', status: 'in_progress' }],
});
assert.deepEqual(
connection.requests.filter(({ operation }) => operation === 'session.todo.query'),
[{ operation: 'session.todo.query', input: { sessionId: 'session-id' } }],
);
await assert.rejects(driver.queryTodo!('other-session'), /non-current Session/);
connection.todoQuery = { sessionId: 'other-session', items: [] };
await assert.rejects(driver.queryTodo!('session-id'), /unexpected Session/);
});
test('publishes only Todo domain invalidations and supports unsubscribe', async () => {
const subscription = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: () => 'session-id',
});
await driver.createSession({
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
const changes: string[] = [];
const unsubscribe = driver.subscribeTodoChanges!((sessionId) => changes.push(sessionId));
subscription.push({
kind: 'subscription.session_domain_changed',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
sessionId: 'session-id',
domain: 'usage',
});
subscription.push({
kind: 'subscription.session_domain_changed',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 2,
sessionId: 'session-id',
domain: 'todo',
});
await waitFor(() => changes.length === 1);
assert.deepEqual(changes, ['session-id']);
unsubscribe();
subscription.push({
kind: 'subscription.session_domain_changed',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 3,
sessionId: 'session-id',
domain: 'todo',
});
await delay(0);
assert.deepEqual(changes, ['session-id']);
});
test('keeps remote Session paths out of Client filesystem policy', async () => {
const driver = createRuntimeHostMakaSessionDriver({
connection: new FakeConnection([]).value,
cwd: '/client/workspace',
workspace: { kind: 'project', projectId: 'project-1' },
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
executionLocation: { kind: 'host' },
});
assert.equal(driver.moveSession, undefined);
assert.deepEqual(
await driver.getSessionResumeAvailability!({ cwd: '/srv/remote-only' } as never),
{ available: true },
);
await assert.rejects(
driver.switchSession('session-1', { relocateCwd: '/client/workspace' }),
/cannot be relocated by this Client/,
);
const driverWithoutProject = createRuntimeHostMakaSessionDriver({
connection: new FakeConnection([]).value,
cwd: '/client/workspace',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
executionLocation: { kind: 'host' },
});
await assert.rejects(
driverWithoutProject.createSession({
cwd: '/client/workspace',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
}),
/requires an explicit Project/,
);
});
test('exposes the session goal from the pushed continuity snapshot', async () => {
const armedGoal = goalProjection({ status: 'active' });
const subscription = new FakeSubscription(
continuitySnapshot({ goal: armedGoal }),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: () => 'session-id',
});
// No session attached yet: no channel, no goal.
assert.equal(driver.getGoal!(), null);
const observations: Array<string | null> = [];
const unsubscribe = driver.subscribeGoalChanges!((goal) =>
observations.push(goal === null ? null : `${goal.status}@${goal.revision}`),
);
await driver.createSession({
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
// Channel adoption publishes the snapshot's goal without any RPC.
assert.equal(driver.getGoal!()?.goalId, 'goal-1');
assert.deepEqual(observations, ['active@1']);
assert.equal(
connection.requests.some(({ operation }) => operation === 'goal.query'),
false,
);
// A pushed projection frame with a bumped revision updates the read and
// notifies listeners — this is how an abort auto-pause reaches the TUI.
const pausedGoal = goalProjection({ status: 'paused', revision: 2, pausedAt: 90 });
subscription.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
snapshot: continuitySnapshot({ goal: pausedGoal, projectionRevision: 2 }),
});
await waitFor(() => driver.getGoal!()?.status === 'paused');
assert.deepEqual(observations, ['active@1', 'paused@2']);
// An unchanged goal in a later frame must not re-notify. Proven by the
// exact sequence: if it had notified, a duplicate 'paused@2' would appear
// before the 'cleared@3' below.
subscription.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 2,
snapshot: continuitySnapshot({ goal: pausedGoal, projectionRevision: 3 }),
});
subscription.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 3,
snapshot: continuitySnapshot({
goal: goalProjection({ status: 'cleared', revision: 3 }),
projectionRevision: 4,
}),
});
await waitFor(() => observations.length === 3);
assert.deepEqual(observations, ['active@1', 'paused@2', 'cleared@3']);
// startNewSession drops the channel: goal reads null and listeners hear it.
await driver.startNewSession();
assert.equal(driver.getGoal!(), null);
assert.deepEqual(observations, ['active@1', 'paused@2', 'cleared@3', null]);
unsubscribe();
});
test('controlGoal applies actions with the snapshot revision and retries conflicts', async () => {
const armedGoal = goalProjection({ status: 'active' });
const subscription = new FakeSubscription(
continuitySnapshot({ goal: armedGoal }),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: () => 'session-id',
});
// No session attached: no-op, no RPC.
assert.equal(await driver.controlGoal!('pause'), null);
assert.equal(
connection.requests.some(({ operation }) => operation === 'goal.control'),
false,
);
await driver.createSession({
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
// Clean path: one control request carrying the snapshot revision, no query.
connection.goalControlOutcomes.push(
goalProjection({ status: 'paused', revision: 2, pausedAt: 90 }),
);
assert.equal((await driver.controlGoal!('pause'))?.status, 'paused');
let controlRevisions = connection.requests
.filter(({ operation }) => operation === 'goal.control')
.map(({ input }) => (input as OperationInput<'goal.control'>).expectedRevision);
assert.deepEqual(controlRevisions, [1]);
assert.equal(
connection.requests.some(({ operation }) => operation === 'goal.query'),
false,
);
// The host broadcasts the pause; the snapshot folds it before the next action.
subscription.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
snapshot: continuitySnapshot({
goal: goalProjection({ status: 'paused', revision: 2, pausedAt: 90 }),
projectionRevision: 2,
}),
});
await waitFor(() => driver.getGoal!()?.revision === 2);
// Conflict path: re-query for the fresh revision and retry against it.
connection.goalControlOutcomes.push(
new RuntimeHostOperationError('goal.control', 'operation_conflict', 'revision conflict'),
goalProjection({ status: 'active', revision: 4 }),
);
connection.goalQueryResults.push(
goalProjection({ status: 'paused', revision: 3, pausedAt: 95 }),
);
assert.equal((await driver.controlGoal!('resume'))?.revision, 4);
controlRevisions = connection.requests
.filter(({ operation }) => operation === 'goal.control')
.map(({ input }) => (input as OperationInput<'goal.control'>).expectedRevision);
assert.deepEqual(controlRevisions, [1, 2, 3]);
assert.equal(
connection.requests.filter(({ operation }) => operation === 'goal.query').length,
1,
);
// Conflict where a concurrent controller removed the goal mid-flight: null
// (for clear, that is the desired end state).
connection.goalControlOutcomes.push(
new RuntimeHostOperationError('goal.control', 'operation_conflict', 'revision conflict'),
);
connection.goalQueryResults.push(null);
assert.equal(await driver.controlGoal!('clear'), null);
// Status conflict (invalid transition): the re-query returns the SAME
// revision — every accepted transition bumps it — proving a refusal, not
// a race. The host's reason is rethrown, not a misleading retry-exhaustion
// error, and the loop stops instead of burning the remaining attempts.
connection.goalControlOutcomes.push(
new RuntimeHostOperationError(
'goal.control',
'operation_conflict',
'Goal cannot pause from status paused',
),
);
connection.goalQueryResults.push(
goalProjection({ status: 'paused', revision: 2, pausedAt: 90 }),
);
await assert.rejects(driver.controlGoal!('pause'), /Goal cannot pause from status paused/);
const attempts = connection.requests.filter(
({ operation }) => operation === 'goal.control',
).length;
assert.equal(attempts, 5); // 1 clean + 2 raced + 1 raced-then-gone + 1 refused — no futile retries
// A third conflict has no retry left to serve, so preserve that final Host
// reason instead of replacing it with a generic retry-exhaustion message.
connection.goalControlOutcomes.push(
new RuntimeHostOperationError('goal.control', 'operation_conflict', 'revision conflict 1'),
new RuntimeHostOperationError('goal.control', 'operation_conflict', 'revision conflict 2'),
new RuntimeHostOperationError(
'goal.control',
'operation_conflict',
'Goal cannot resume from status active',
),
);
connection.goalQueryResults.push(
goalProjection({ status: 'paused', revision: 3, pausedAt: 95 }),
goalProjection({ status: 'paused', revision: 4, pausedAt: 95 }),
);
await assert.rejects(driver.controlGoal!('resume'), /Goal cannot resume from status active/);
});
test('honors explicit Project intent before inheriting the current workspace', async () => {
const cases = [
{ cwd: '/repo', projectId: null, expected: { kind: 'host_path', path: '/repo' } },
{
cwd: '/repo',
projectId: 'project-b',
expected: { kind: 'project', projectId: 'project-b' },
},
{
cwd: '/other',
projectId: 'project-b',
expected: { kind: 'project', projectId: 'project-b' },
},
{ cwd: '/repo', expected: { kind: 'project', projectId: 'project-a' } },
{ cwd: '/other', expected: { kind: 'host_path', path: '/other' } },
] as const;
for (const candidate of cases) {
const connection = new FakeConnection([
new FakeSubscription(continuitySnapshot(), Promise.resolve([])),
]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
workspace: { kind: 'project', projectId: 'project-a' },
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: () => 'session-id',
});
await driver.createSession({
cwd: candidate.cwd,
...('projectId' in candidate ? { projectId: candidate.projectId } : {}),
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
assert.deepEqual(
connection.requests.find(({ operation }) => operation === 'session.create')?.input,
{
sessionId: 'session-id',
workspace: candidate.expected,
name: 'New Chat',
modelTarget: {
kind: 'explicit',
connectionId: 'connection-1',
connectionSlug: 'openai-main',
model: 'gpt-5',
},
permissionMode: 'ask',
},
);
}
});
test('starts one user command without opening an agent turn', async () => {
const subscription = new FakeSubscription(
continuitySnapshot({
rootTurn: null,
session: {
sessionId: 'id-1',
metadataRevision: 1,
status: 'running',
createdAt: 1,
isArchived: false,
},
}),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: (() => {
let id = 0;
return () => `id-${++id}`;
})(),
});
const command = await driver.runUserCommand!('pwd');
assert.equal(command.commandId, 'user-command-id-2');
assert.equal(command.result.mode, 'pipes');
assert.deepEqual(
connection.requests.map((request) => request.operation),
['session.create', 'runtime.resource.start'],
);
assert.deepEqual(connection.requests[1]?.input, {
sessionId: 'id-1',
launchId: 'user-command-id-2',
command: 'pwd',
});
assert.equal(command.takeRacedUpdate(), undefined);
});
test('retains a terminal user-command update that arrives before its card is created', async () => {
const subscription = new FakeSubscription(
continuitySnapshot({
rootTurn: null,
session: {
sessionId: 'id-1',
metadataRevision: 1,
status: 'running',
createdAt: 1,
isArchived: false,
},
}),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: (() => {
let id = 0;
return () => `id-${++id}`;
})(),
});
connection.onRuntimeResourceStart = async () => {
const startRequest = connection.requests.at(-1);
if (!startRequest) throw new Error('Expected Runtime Resource start request');
const launchId = (startRequest.input as { launchId: string }).launchId;
connection.runtimeResourceQuery = {
kind: 'resource',
sessionId: 'id-1',
revision: `sha256:${'a'.repeat(64)}`,
resource: {
sessionId: 'id-1',
ownership: { kind: 'local' },
sourceTurnId: launchId,
sourceToolCallId: launchId,
result: {
...connection.userCommandResource,
status: 'completed',
output: { ...connection.userCommandResource.output, stdout: 'done\n' },
updatedAt: 2,
completedAt: 2,
exitCode: 0,
revision: 2,
},
} satisfies ShellRunUpdate,
};
subscription.push({
kind: 'subscription.session_domain_changed',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
sessionId: 'id-1',
domain: 'runtime_resource',
resources: [{ sourceSessionId: 'id-1', ref: connection.userCommandResource.ref }],
});
await waitFor(() =>
connection.requests.some((request) => request.operation === 'runtime.resource.query'),
);
await delay(0);
};
const command = await driver.runUserCommand!('printf done');
const raced = command.takeRacedUpdate();
assert.equal(raced?.status, 'completed');
assert.equal(raced?.output?.mode, 'pipes');
assert.equal(raced?.output?.mode === 'pipes' && raced.output.stdout, 'done\n');
});
test('stops an already-running user command when the driver closes', async () => {
const subscription = new FakeSubscription(
continuitySnapshot({
rootTurn: null,
session: {
sessionId: 'id-1',
metadataRevision: 1,
status: 'running',
createdAt: 1,
isArchived: false,
},
}),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: sequenceIds('id-1', 'id-2'),
});
const command = await driver.runUserCommand!('sleep 3600');
command.takeRacedUpdate();
await driver.stop();
const stop = connection.requests.find(
(request) => request.operation === 'runtime.resource.stop',
);
assert.deepEqual(stop?.input, {
sessionId: 'id-1',
ref: connection.userCommandResource.ref,
});
});
test('stops a user command whose start races driver close', async () => {
const subscription = new FakeSubscription(
continuitySnapshot({
rootTurn: null,
session: {
sessionId: 'id-1',
metadataRevision: 1,
status: 'running',
createdAt: 1,
isArchived: false,
},
}),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
const releaseStart = deferred<void>();
connection.onRuntimeResourceStart = () => releaseStart.promise;
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: sequenceIds('id-1', 'id-2'),
});
const starting = driver.runUserCommand!('sleep 3600');
await waitFor(() =>
connection.requests.some((request) => request.operation === 'runtime.resource.start'),
);
const stopping = driver.stop();
releaseStart.resolve();
const command = await starting;
command.takeRacedUpdate();
await stopping;
assert.equal(
connection.requests.filter((request) => request.operation === 'runtime.resource.stop').length,
1,
);
});
test('a rejecting user-command stop does not fail the turn interrupt (#3210)', async () => {
const subscription = new FakeSubscription(
continuitySnapshot({
rootTurn: runningTurn('turn-1', 'run-1'),
session: {
sessionId: 'id-1',
metadataRevision: 1,
status: 'running',
createdAt: 1,
isArchived: false,
},
}),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
connection.runtimeResourceStopFailure = new Error('host_draining');
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: sequenceIds('id-1', 'id-2'),
});
const command = await driver.runUserCommand!('sleep 3600');
command.takeRacedUpdate();
// turn.stop succeeds while the user-command stop rejects: the interrupt
// itself must still report success.
await driver.stop();
assert.ok(connection.requests.some((request) => request.operation === 'turn.stop'));
assert.ok(connection.requests.some((request) => request.operation === 'runtime.resource.stop'));
});
test('stops a running user command before switching Sessions (#3210)', async () => {
const subscription = new FakeSubscription(
continuitySnapshot({
rootTurn: null,
session: {
sessionId: 'id-1',
metadataRevision: 1,
status: 'running',
createdAt: 1,
isArchived: false,
},
}),
Promise.resolve([]),
);
const switchSubscription = new FakeSubscription(
continuitySnapshot({ rootTurn: null }),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription, switchSubscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: sequenceIds('id-1', 'id-2'),
});
const command = await driver.runUserCommand!('sleep 3600');
command.takeRacedUpdate();
await driver.switchSession('session-1');
const stopIndex = connection.requests.findIndex(
(request) => request.operation === 'runtime.resource.stop',
);
assert.notEqual(stopIndex, -1);
assert.deepEqual(connection.requests[stopIndex]?.input, {
sessionId: 'id-1',
ref: connection.userCommandResource.ref,
});
assert.equal(driver.getSessionId(), 'session-1');
});
test('a rejecting user-command stop aborts the switch before any durable relocation commits (#3210)', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-tui-switch-stop-failure-'));
const target = join(root, 'new-worktree');
await mkdir(target);
try {
const oldCwd = join(root, 'old-worktree');
const subscription = new FakeSubscription(
continuitySnapshot({
rootTurn: null,
session: {
sessionId: 'id-1',
metadataRevision: 1,
status: 'running',
createdAt: 1,
isArchived: false,
},
}),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
connection.sessionQueries.push(
sessionProjection({
workspace: { target: { kind: 'host_path', path: oldCwd }, hostCwd: oldCwd },
}),
);
connection.runtimeResourceStopFailure = new Error('host_draining');
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: root,
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: sequenceIds('id-1', 'id-2'),
inspectCwdChanges: async () => undefined,
});
const command = await driver.runUserCommand!('sleep 3600');
command.takeRacedUpdate();
await assert.rejects(
driver.switchSession('session-1', { relocateCwd: './new-worktree' }),
/host_draining/,
);
// The switch aborted before anything durable: no relocation was
// committed and the driver still owns the original Session.
assert.equal(
connection.requests.some(({ operation }) => operation === 'session.workspace.relocate'),
false,
);
assert.equal(driver.getSessionId(), 'id-1');
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('awaits the user-command stop before clearing identity on /new (#3210)', async () => {
const subscription = new FakeSubscription(
continuitySnapshot({
rootTurn: null,
session: {
sessionId: 'id-1',
metadataRevision: 1,
status: 'running',
createdAt: 1,
isArchived: false,
},
}),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: sequenceIds('id-1', 'id-2'),
});
const command = await driver.runUserCommand!('sleep 3600');
command.takeRacedUpdate();
await driver.startNewSession();
const stop = connection.requests.find(
(request) => request.operation === 'runtime.resource.stop',
);
assert.deepEqual(stop?.input, {
sessionId: 'id-1',
ref: connection.userCommandResource.ref,
});
assert.equal(driver.getSessionId(), null);
});
test('a rejected user-command stop aborts /new without clearing identity (#3210 review)', async () => {
const subscription = new FakeSubscription(
continuitySnapshot({
rootTurn: null,
session: {
sessionId: 'id-1',
metadataRevision: 1,
status: 'running',
createdAt: 1,
isArchived: false,
},
}),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: sequenceIds('id-1', 'id-2'),
});
const command = await driver.runUserCommand!('sleep 3600');
command.takeRacedUpdate();
connection.runtimeResourceStopFailure = new Error('host_draining');
await assert.rejects(() => driver.startNewSession(), /host_draining/);
// Nothing committed: the previous Session is still owned, so its card and
// Ctrl+C affordance remain live.
assert.equal(driver.getSessionId(), 'id-1');
// Once the Host recovers, /new proceeds normally.
connection.runtimeResourceStopFailure = undefined;
await driver.startNewSession();
assert.equal(driver.getSessionId(), null);
});
test('submits a same-slug model recovery with the newly selected Connection id', async () => {
const connection = new FakeConnection([
new FakeSubscription(continuitySnapshot(), Promise.resolve([])),
]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-a',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: () => 'session-id',
});
await driver.createSession({
cwd: '/repo',
llmConnectionId: 'connection-a',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
connection.sessionQueries.push(
sessionProjection({ revision: 1, llmConnectionId: 'connection-a' }),
sessionProjection({ revision: 2, llmConnectionId: 'connection-a' }),
);
connection.configurationOutcomes.push(
{ kind: 'revision_conflict', expectedRevision: 1, actualRevision: 2 },
{
kind: 'committed',
session: sessionProjection({
revision: 3,
llmConnectionId: 'connection-b',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
}),
},
);
await driver.setModel('gpt-5', 'openai-main', 'connection-b');
assert.deepEqual(
connection.requests
.filter(({ operation }) => operation === 'session.configuration.update')
.map(({ input }) => input),
[1, 2].map((expectedRevision) => ({
sessionId: 'session-id',
expectedRevision,
patch: {
modelTarget: {
kind: 'explicit',
connectionId: 'connection-b',
connectionSlug: 'openai-main',
model: 'gpt-5',
},
thinkingLevel: null,
},
})),
);
});
test('drops a per-session Full access elevation when a fresh Session starts (#3020)', async () => {
// The TUI flow behind /new: session A is elevated to bypass, then the
// driver is asked to start over. The next prompt lazily creates session B
// through preparePrompt. Session B must be created with the
// construction-time default — Full access is an explicit per-session
// opt-in, never inherited.
const connection = new FakeConnection([
new FakeSubscription(continuitySnapshot(), Promise.resolve([])),
new FakeSubscription(
continuitySnapshot({
session: {
sessionId: 'session-2',
metadataRevision: 1,
status: 'running',
createdAt: 1,
isArchived: false,
},
rootTurn: null,
}),
Promise.resolve([]),
),
]);
let nextId = 0;
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
prospectivePermissionMode: 'ask',
newId: () => `session-${++nextId}`,
});
await driver.createSession({
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
connection.executionBoundary = { kind: 'bypass', revision: 2 };
await driver.setPermissionMode('bypass');
assert.equal(driver.getPermissionMode?.(), 'bypass');
await driver.startNewSession();
assert.equal(driver.getPermissionMode?.(), 'ask');
// The fresh Session's boundary is managed again once it exists.
connection.executionBoundary = { kind: 'managed', access: 'writable', revision: 3 };
await driver.preparePrompt('hello');
const creates = connection.requests.filter(({ operation }) => operation === 'session.create');
assert.equal(creates.length, 2);
// The elevation does not leak, and the fresh Session carries no client
// claim at all: an omitted field is what leaves the starting mode to the
// Host's `chatDefaults`. Substituting the launch reading here would make
// the CLI a second authority over it.
assert.deepEqual(creates[1]!.input, {
sessionId: 'session-2',
workspace: { kind: 'host_path', path: '/repo' },
name: 'New Chat',
modelTarget: {
kind: 'explicit',
connectionId: 'connection-1',
connectionSlug: 'openai-main',
model: 'gpt-5',
},
});
});
test('relocates a moved Session through Host authority before attaching', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-tui-resume-moved-cwd-'));
const target = join(root, 'new-worktree');
await mkdir(target);
try {
const oldCwd = join(root, 'old-worktree');
const connection = new FakeConnection([
new FakeSubscription(continuitySnapshot(), Promise.resolve([])),
]);
connection.sessionQueries.push(
sessionProjection({
workspace: { target: { kind: 'host_path', path: oldCwd }, hostCwd: oldCwd },
}),
sessionProjection({
workspace: { target: { kind: 'host_path', path: oldCwd }, hostCwd: oldCwd },
}),
);
const inspected: string[] = [];
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: root,
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
inspectCwdChanges: async (cwd) => {
inspected.push(cwd);
return undefined;
},
});
const switched = await driver.switchSession('session-1', {
relocateCwd: './new-worktree',
});
const canonicalTarget = await realpath(target);
assert.equal(switched.summary.cwd, canonicalTarget);
assert.deepEqual(switched.relocation, {
previousCwd: oldCwd,
cwd: canonicalTarget,
changed: true,
oldCwdDirty: undefined,
});
assert.deepEqual(inspected, [oldCwd]);
assert.deepEqual(
connection.requests.map(({ operation }) => operation),
[
'session.catalog.query',
'session.execution_boundary.query',
'session.catalog.query',
'session.workspace.relocate',
],
);
assert.deepEqual(connection.requests.at(-1)?.input, {
sessionId: 'session-1',
expectedRevision: 1,
workspace: { kind: 'host_path', path: canonicalTarget },
});
} finally {
await rm(root, { recursive: true, force: true });
}
});
test('does not relocate an externally isolated Session during resume', async () => {
const connection = new FakeConnection([]);
connection.executionBoundary = { kind: 'external', harness: 'harbor', revision: 1 };
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: process.cwd(),
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
await assert.rejects(
driver.switchSession('session-1', { relocateCwd: process.cwd() }),
/Cannot resume externally isolated session/,
);
assert.equal(
connection.requests.some(({ operation }) => operation === 'session.workspace.relocate'),
false,
);
});
test('atomically joins an active turn without losing output produced during transcript load', async () => {
const transcript = deferred<StoredMessage[]>();
const subscription = new FakeSubscription(continuitySnapshot(), transcript.promise);
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
const switching = driver.switchSession('session-1');
await waitFor(() => subscription.nextCalls > 0);
subscription.push(deltaFrame(1, 'turn-1', 5, ' world'));
transcript.resolve([assistantMessage('turn-1', 'Hello')]);
const switched = await switching;
assert.deepEqual(switched.messages, [assistantMessage('turn-1', 'Hello')]);
assert.ok(switched.activeTurn);
const event = await nextEvent(switched.activeTurn.events);
assert.deepEqual(event, {
type: 'text_delta',
id: 'host-frame:host-1:subscription-1:1',
turnId: 'turn-1',
messageId: 'message-turn-1',
ts: 50,
startOffset: 5,
text: ' world',
});
});
test('delivers completed thinking while a later step remains active', async () => {
const subscription = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
subscription.push(thinkingFrame(1, 'step-1', 0, 'first'));
subscription.push(thinkingFrame(2, 'step-1', 5, '', true));
subscription.push(thinkingFrame(3, 'step-2', 0, 'second'));
assert.deepEqual(
[
await nextEvent(switched.activeTurn.events),
await nextEvent(switched.activeTurn.events),
await nextEvent(switched.activeTurn.events),
].map((event) => ({
type: event.type,
messageId: 'messageId' in event ? event.messageId : undefined,
})),
[
{ type: 'thinking_delta', messageId: 'step-1' },
{ type: 'thinking_complete', messageId: 'step-1' },
{ type: 'thinking_delta', messageId: 'step-2' },
],
);
});
test('restarts initial hydration when the first connection closes during transcript load', async () => {
const transcript = deferred<StoredMessage[]>();
const initial = new FakeSubscription(continuitySnapshot(), transcript.promise);
const replacementMessages = [assistantMessage('turn-1', 'Canonical replacement')];
const replacement = new FakeSubscription(
continuitySnapshot({ projectionRevision: 2 }),
Promise.resolve(replacementMessages),
'subscription-2',
);
const connection = new FakeConnection([initial, replacement], true);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
const switching = driver.switchSession('session-1');
await waitFor(() => initial.nextCalls > 0);
const disconnected = new RuntimeHostSubscriptionError(
'connection_closed',
'connection closed during initial hydration',
);
initial.fail(disconnected);
transcript.reject(disconnected);
const switched = await switching;
assert.deepEqual(switched.messages, replacementMessages);
assert.equal(connection.openedSubscriptions, 2);
});
test('drains the active cut when its turn completes during transcript load', async () => {
const transcript = deferred<StoredMessage[]>();
const subscription = new FakeSubscription(continuitySnapshot(), transcript.promise);
const connection = new FakeConnection([
subscription,
new FakeSubscription(
continuitySnapshot({ rootTurn: completedTurn('turn-1', 'run-1') }),
Promise.resolve([assistantMessage('turn-1', 'Hello world')]),
'subscription-2',
),
]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
const switching = driver.switchSession('session-1');
await waitFor(() => subscription.nextCalls > 0);
subscription.push(deltaFrame(1, 'turn-1', 5, ' world'));
subscription.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 2,
snapshot: continuitySnapshot({
projectionRevision: 2,
rootTurn: completedTurn('turn-1', 'run-1'),
}),
});
transcript.resolve([assistantMessage('turn-1', 'Hello')]);
const switched = await switching;
assert.ok(switched.activeTurn);
assert.equal((await nextEvent(switched.activeTurn.events)).type, 'text_delta');
assert.equal((await nextEvent(switched.activeTurn.events)).type, 'text_complete');
assert.equal((await nextEvent(switched.activeTurn.events)).type, 'complete');
assert.equal((await switched.activeTurn.events[Symbol.asyncIterator]().next()).done, true);
});
test('reattaches atomically when another client starts the successor turn', async () => {
const first = new FakeSubscription(
continuitySnapshot(),
Promise.resolve([assistantMessage('turn-1', 'Finished')]),
);
const refresh = new FakeSubscription(
continuitySnapshot({ rootTurn: completedTurn('turn-1', 'run-1') }),
Promise.resolve([assistantMessage('turn-1', 'Finished')]),
'subscription-refresh',
);
const second = new FakeSubscription(
continuitySnapshot({
projectionRevision: 3,
rootTurn: runningTurn('turn-2', 'run-2'),
}),
Promise.resolve([userMessage('turn-2', 'Follow up'), assistantMessage('turn-2', 'New')]),
'subscription-2',
);
const connection = new FakeConnection([first, refresh, second]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 60,
});
const initial = await driver.switchSession('session-1');
assert.ok(initial.activeTurn);
const started = deferred<MakaAttachedSessionTurn>();
driver.subscribeStartedTurns!((turn) => started.resolve(turn));
first.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
snapshot: continuitySnapshot({
projectionRevision: 2,
rootTurn: completedTurn('turn-1', 'run-1'),
}),
});
assert.equal((await nextEvent(initial.activeTurn.events)).type, 'complete');
assert.equal((await initial.activeTurn.events[Symbol.asyncIterator]().next()).done, true);
await waitFor(() => refresh.nextCalls > 0);
first.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 2,
snapshot: continuitySnapshot({
projectionRevision: 3,
rootTurn: runningTurn('turn-2', 'run-2'),
}),
});
const attached = await Promise.race([
started.promise,
delay(WAIT_BUDGET_MS).then(() => assert.fail('Timed out waiting for successor turn')),
]);
assert.deepEqual(attached.messages, [
userMessage('turn-2', 'Follow up'),
assistantMessage('turn-2', 'New'),
]);
second.push(deltaFrame(1, 'turn-2', 3, ' text', 'subscription-2', 'run-2'));
assert.equal((await nextEvent(attached.events)).type, 'text_delta');
});
test('hides the copied parent transcript when a Host starts the side successor turn', async () => {
const parentTranscript = [
userMessage('turn-parent', 'Parent question'),
assistantMessage('turn-parent', 'Parent answer'),
];
const first = new FakeSubscription(
continuitySnapshot(),
Promise.resolve([...parentTranscript]),
);
const refresh = new FakeSubscription(
continuitySnapshot({ rootTurn: completedTurn('turn-1', 'run-1') }),
Promise.resolve([...parentTranscript]),
'subscription-refresh',
);
const second = new FakeSubscription(
continuitySnapshot({
projectionRevision: 3,
rootTurn: runningTurn('turn-2', 'run-2'),
}),
Promise.resolve([
...parentTranscript,
userMessage('turn-2', 'Side follow up'),
assistantMessage('turn-2', 'Side answer'),
]),
'subscription-2',
);
const connection = new FakeConnection([first, refresh, second]);
// A side Session is a copy that carries the parent transcript; both the
// initial switch and the reattach's configuration load must see the side
// labels so the driver keeps hiding everything through `turn-parent`.
const sideProjection = () =>
sessionProjection({
labels: ['mode:side_conversation'],
parentSessionId: 'parent-1',
branchOfTurnId: 'turn-parent',
});
connection.sessionQueries.push(sideProjection(), sideProjection());
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 60,
});
const initial = await driver.switchSession('session-1');
assert.ok(initial.activeTurn);
// The visible side transcript starts empty even though the copy carried
// the parent's messages.
assert.deepEqual(initial.messages, []);
const started = deferred<MakaAttachedSessionTurn>();
driver.subscribeStartedTurns!((turn) => started.resolve(turn));
first.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
snapshot: continuitySnapshot({
projectionRevision: 2,
rootTurn: completedTurn('turn-1', 'run-1'),
}),
});
assert.equal((await nextEvent(initial.activeTurn.events)).type, 'complete');
assert.equal((await initial.activeTurn.events[Symbol.asyncIterator]().next()).done, true);
await waitFor(() => refresh.nextCalls > 0);
first.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 2,
snapshot: continuitySnapshot({
projectionRevision: 3,
rootTurn: runningTurn('turn-2', 'run-2'),
}),
});
const attached = await Promise.race([
started.promise,
delay(WAIT_BUDGET_MS).then(() => assert.fail('Timed out waiting for successor turn')),
]);
// Without the filter this replaced the visible transcript with the copied
// parent conversation the user opened `/side` to leave (#3881).
assert.deepEqual(attached.messages, [
userMessage('turn-2', 'Side follow up'),
assistantMessage('turn-2', 'Side answer'),
]);
});
test('adopts a successor that finishes before its atomic reattach completes', async () => {
const first = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const refresh = new FakeSubscription(
continuitySnapshot({ rootTurn: completedTurn('turn-1', 'run-1') }),
Promise.resolve([]),
'subscription-refresh',
);
const second = new FakeSubscription(
continuitySnapshot({
projectionRevision: 3,
rootTurn: completedTurn('turn-2', 'run-2'),
}),
Promise.resolve([
userMessage('turn-2', 'Fast follow up'),
assistantMessage('turn-2', 'Done'),
]),
'subscription-2',
);
const connection = new FakeConnection([first, refresh, second]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
const started = deferred<MakaAttachedSessionTurn>();
driver.subscribeStartedTurns!((turn) => started.resolve(turn));
first.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
snapshot: continuitySnapshot({
projectionRevision: 2,
rootTurn: completedTurn('turn-1', 'run-1'),
}),
});
assert.equal((await nextEvent(switched.activeTurn.events)).type, 'complete');
assert.equal((await switched.activeTurn.events[Symbol.asyncIterator]().next()).done, true);
await waitFor(() => refresh.nextCalls > 0);
first.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 2,
snapshot: continuitySnapshot({
projectionRevision: 3,
rootTurn: runningTurn('turn-2', 'run-2'),
}),
});
const attached = await started.promise;
assert.deepEqual(attached.messages, [
userMessage('turn-2', 'Fast follow up'),
assistantMessage('turn-2', 'Done'),
]);
const text = await nextEvent(attached.events);
assert.equal(text.type, 'text_complete');
if (text.type !== 'text_complete') assert.fail('Expected the durable assistant answer');
assert.equal(text.text, 'Done');
assert.equal((await nextEvent(attached.events)).type, 'complete');
});
test('serializes buffered successor reattach so transcript completion cannot reverse turn order', async () => {
const first = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const refreshFirst = new FakeSubscription(
continuitySnapshot({ rootTurn: completedTurn('turn-1', 'run-1') }),
Promise.resolve([]),
'subscription-refresh-1',
);
const refreshSecond = new FakeSubscription(
continuitySnapshot({ rootTurn: completedTurn('turn-2', 'run-2') }),
Promise.resolve([]),
'subscription-refresh-2',
);
const secondTranscript = deferred<StoredMessage[]>();
const thirdTranscript = deferred<StoredMessage[]>();
const second = new FakeSubscription(
continuitySnapshot({ projectionRevision: 4, rootTurn: runningTurn('turn-2', 'run-2') }),
secondTranscript.promise,
'subscription-2',
);
const third = new FakeSubscription(
continuitySnapshot({ projectionRevision: 5, rootTurn: runningTurn('turn-3', 'run-3') }),
thirdTranscript.promise,
'subscription-3',
);
const connection = new FakeConnection([first, refreshFirst, refreshSecond, second, third]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
const started: MakaAttachedSessionTurn[] = [];
driver.subscribeStartedTurns!((turn) => started.push(turn));
first.push(projectionFrame(1, completedTurn('turn-1', 'run-1'), 2));
first.push(projectionFrame(2, runningTurn('turn-2', 'run-2'), 3));
first.push(projectionFrame(3, completedTurn('turn-2', 'run-2'), 4));
first.push(projectionFrame(4, runningTurn('turn-3', 'run-3'), 5));
assert.equal((await nextEvent(switched.activeTurn.events)).type, 'complete');
assert.equal((await switched.activeTurn.events[Symbol.asyncIterator]().next()).done, true);
await waitFor(() => connection.openedSubscriptions === 4);
await new Promise((resolve) => setImmediate(resolve));
assert.equal(connection.openedSubscriptions, 4, 'the later successor must wait for reattach');
secondTranscript.resolve([userMessage('turn-2', 'Second')]);
await waitFor(() => started.length === 1 && connection.openedSubscriptions === 5);
assert.equal(started[0]?.turnId, 'turn-2');
thirdTranscript.resolve([userMessage('turn-3', 'Third')]);
await waitFor(() => started.length === 2);
assert.deepEqual(
started.map((turn) => turn.turnId),
['turn-2', 'turn-3'],
);
});
test('a retired intermediate channel cannot republish its buffered successor', async () => {
const first = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const refreshFirst = new FakeSubscription(
continuitySnapshot({ rootTurn: completedTurn('turn-1', 'run-1') }),
Promise.resolve([]),
'subscription-refresh-1',
);
const refreshSecondFromFirst = new FakeSubscription(
continuitySnapshot({ rootTurn: completedTurn('turn-2', 'run-2') }),
Promise.resolve([]),
'subscription-refresh-2-first',
);
const secondTranscript = deferred<StoredMessage[]>();
const second = new FakeSubscription(
continuitySnapshot({ projectionRevision: 3, rootTurn: runningTurn('turn-2', 'run-2') }),
secondTranscript.promise,
'subscription-2',
);
const refreshSecondFromSecond = new FakeSubscription(
continuitySnapshot({ rootTurn: completedTurn('turn-2', 'run-2') }),
Promise.resolve([]),
'subscription-refresh-2-second',
);
const third = new FakeSubscription(
continuitySnapshot({ projectionRevision: 5, rootTurn: runningTurn('turn-3', 'run-3') }),
Promise.resolve([userMessage('turn-3', 'Third')]),
'subscription-3',
);
const duplicateThird = new FakeSubscription(
continuitySnapshot({ projectionRevision: 5, rootTurn: runningTurn('turn-3', 'run-3') }),
Promise.resolve([userMessage('turn-3', 'Duplicate third')]),
'subscription-3-duplicate',
);
const connection = new FakeConnection([
first,
refreshFirst,
refreshSecondFromFirst,
second,
refreshSecondFromSecond,
third,
duplicateThird,
]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
const started: MakaAttachedSessionTurn[] = [];
driver.subscribeStartedTurns!((turn) => started.push(turn));
first.push(projectionFrame(1, completedTurn('turn-1', 'run-1'), 2));
first.push(projectionFrame(2, runningTurn('turn-2', 'run-2'), 3));
first.push(projectionFrame(3, completedTurn('turn-2', 'run-2'), 4));
first.push(projectionFrame(4, runningTurn('turn-3', 'run-3'), 5));
assert.equal((await nextEvent(switched.activeTurn.events)).type, 'complete');
assert.equal((await switched.activeTurn.events[Symbol.asyncIterator]().next()).done, true);
await waitFor(() => connection.openedSubscriptions === 4 && second.nextCalls > 0);
second.push(projectionFrame(1, completedTurn('turn-2', 'run-2'), 4, 'subscription-2'));
second.push(projectionFrame(2, runningTurn('turn-3', 'run-3'), 5, 'subscription-2'));
secondTranscript.resolve([userMessage('turn-2', 'Second')]);
await waitFor(
() =>
connection.openedSubscriptions === 6 &&
started.some((turn) => turn.turnId === 'turn-2') &&
started.some((turn) => turn.turnId === 'turn-3'),
);
const secondTurn = started.find((turn) => turn.turnId === 'turn-2');
assert.ok(secondTurn);
for await (const _event of secondTurn.events) {
// Draining the retired channel must not publish its buffered successor.
}
await new Promise((resolve) => setImmediate(resolve));
assert.equal(connection.openedSubscriptions, 6);
assert.deepEqual(
started.map((turn) => turn.turnId),
['turn-2', 'turn-3'],
);
});
test('an explicit Session switch fences an older successor reattach already loading', async () => {
const first = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const refresh = new FakeSubscription(
continuitySnapshot({ rootTurn: completedTurn('turn-1', 'run-1') }),
Promise.resolve([]),
'subscription-refresh',
);
const staleTranscript = deferred<StoredMessage[]>();
const stale = new FakeSubscription(
continuitySnapshot({ projectionRevision: 3, rootTurn: runningTurn('turn-2', 'run-2') }),
staleTranscript.promise,
'subscription-stale',
);
const switchedSubscription = new FakeSubscription(
continuitySnapshot({ projectionRevision: 4, rootTurn: runningTurn('turn-3', 'run-3') }),
Promise.resolve([userMessage('turn-3', 'Current')]),
'subscription-current',
);
const connection = new FakeConnection([first, refresh, stale, switchedSubscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
const initial = await driver.switchSession('session-1');
assert.ok(initial.activeTurn);
const started: string[] = [];
driver.subscribeStartedTurns!((turn) => started.push(turn.turnId));
first.push(projectionFrame(1, completedTurn('turn-1', 'run-1'), 2));
first.push(projectionFrame(2, runningTurn('turn-2', 'run-2'), 3));
assert.equal((await nextEvent(initial.activeTurn.events)).type, 'complete');
assert.equal((await initial.activeTurn.events[Symbol.asyncIterator]().next()).done, true);
await waitFor(() => connection.openedSubscriptions === 3);
const switched = await driver.switchSession('session-1');
assert.equal(switched.activeTurn?.turnId, 'turn-3');
staleTranscript.resolve([userMessage('turn-2', 'Stale')]);
await new Promise((resolve) => setImmediate(resolve));
assert.deepEqual(started, []);
});
test('a stale reattach cannot overwrite configuration adopted by an explicit switch', async () => {
const first = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const refresh = new FakeSubscription(
continuitySnapshot({ rootTurn: completedTurn('turn-1', 'run-1') }),
Promise.resolve([]),
'subscription-refresh',
);
const stale = new FakeSubscription(
continuitySnapshot({ projectionRevision: 3, rootTurn: runningTurn('turn-2', 'run-2') }),
Promise.resolve([userMessage('turn-2', 'Stale')]),
'subscription-stale',
);
const current = new FakeSubscription(
continuitySnapshot({ projectionRevision: 4, rootTurn: runningTurn('turn-3', 'run-3') }),
Promise.resolve([userMessage('turn-3', 'Current')]),
'subscription-current',
);
const staleConfiguration = deferred<SessionCatalogProjection>();
const connection = new FakeConnection([first, refresh, stale, current]);
connection.sessionQueries.push(
sessionProjection(),
staleConfiguration.promise,
sessionProjection({ orchestrationMode: 'graph' }),
);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
const initial = await driver.switchSession('session-1');
assert.ok(initial.activeTurn);
first.push(projectionFrame(1, completedTurn('turn-1', 'run-1'), 2));
first.push(projectionFrame(2, runningTurn('turn-2', 'run-2'), 3));
assert.equal((await nextEvent(initial.activeTurn.events)).type, 'complete');
assert.equal((await initial.activeTurn.events[Symbol.asyncIterator]().next()).done, true);
await waitFor(
() =>
connection.requests.filter((request) => request.operation === 'session.catalog.query')
.length === 2,
);
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
assert.equal(driver.getOrchestrationMode!(), 'graph');
staleConfiguration.resolve(sessionProjection({ orchestrationMode: 'default' }));
await new Promise((resolve) => setImmediate(resolve));
assert.equal(driver.getOrchestrationMode!(), 'graph');
});
test('a stale generation cannot block successor reattach after an explicit switch', async () => {
const first = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const oldRefresh = new FakeSubscription(
continuitySnapshot({ rootTurn: completedTurn('turn-1', 'run-1') }),
Promise.resolve([]),
'subscription-old-refresh',
);
const staleTranscript = deferred<StoredMessage[]>();
const stale = new FakeSubscription(
continuitySnapshot({ projectionRevision: 3, rootTurn: runningTurn('turn-2', 'run-2') }),
staleTranscript.promise,
'subscription-stale',
);
const current = new FakeSubscription(
continuitySnapshot({ projectionRevision: 4, rootTurn: runningTurn('turn-3', 'run-3') }),
Promise.resolve([userMessage('turn-3', 'Current')]),
'subscription-current',
);
const currentRefresh = new FakeSubscription(
continuitySnapshot({ projectionRevision: 5, rootTurn: completedTurn('turn-3', 'run-3') }),
Promise.resolve([assistantMessage('turn-3', 'Done')]),
'subscription-current-refresh',
);
const successor = new FakeSubscription(
continuitySnapshot({ projectionRevision: 6, rootTurn: runningTurn('turn-4', 'run-4') }),
Promise.resolve([userMessage('turn-4', 'Next')]),
'subscription-successor',
);
const connection = new FakeConnection([
first,
oldRefresh,
stale,
current,
currentRefresh,
successor,
]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
const initial = await driver.switchSession('session-1');
assert.ok(initial.activeTurn);
const started: string[] = [];
driver.subscribeStartedTurns!((turn) => started.push(turn.turnId));
first.push(projectionFrame(1, completedTurn('turn-1', 'run-1'), 2));
first.push(projectionFrame(2, runningTurn('turn-2', 'run-2'), 3));
assert.equal((await nextEvent(initial.activeTurn.events)).type, 'complete');
assert.equal((await initial.activeTurn.events[Symbol.asyncIterator]().next()).done, true);
await waitFor(() => connection.openedSubscriptions === 3);
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
current.push(projectionFrame(1, completedTurn('turn-3', 'run-3'), 5, 'subscription-current'));
current.push(projectionFrame(2, runningTurn('turn-4', 'run-4'), 6, 'subscription-current'));
assert.equal((await nextEvent(switched.activeTurn.events)).type, 'complete');
assert.equal((await switched.activeTurn.events[Symbol.asyncIterator]().next()).done, true);
await waitFor(() => connection.openedSubscriptions === 6 && started.includes('turn-4'));
assert.deepEqual(started, ['turn-4']);
});
test('routes queue and retract mutations through Host authority', async () => {
const subscription = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: sequenceIds('retract-1'),
});
await driver.switchSession('session-1');
await driver.submitMessage!('Later', {
messageId: 'message-1',
placement: 'next_turn',
});
assert.deepEqual(await driver.retractQueued!(), {
text: 'Later',
messageIds: ['message-1'],
});
assert.deepEqual(
connection.requests.filter(
(request) =>
request.operation === 'turn.message.submit' || request.operation === 'queue.retract',
),
[
{
operation: 'turn.message.submit',
input: {
originHostEpoch: 'host-1',
sessionId: 'session-1',
messageId: 'message-1',
content: { text: 'Later' },
placement: 'next_turn',
},
},
{
operation: 'queue.retract',
input: {
originHostEpoch: 'host-1',
sessionId: 'session-1',
retractId: 'retract-1',
},
},
],
);
});
test('submits an idle message under the caller-owned stable identity', async () => {
const subscription = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: sequenceIds('unused-generated-id'),
});
await driver.switchSession('session-1');
await driver.submitMessage!('Visible prompt', {
messageId: 'message-1',
placement: 'current_turn',
modelText: 'Expanded prompt',
});
assert.deepEqual(connection.requests.at(-1), {
operation: 'turn.message.submit',
input: {
originHostEpoch: 'host-1',
sessionId: 'session-1',
messageId: 'message-1',
content: { text: 'Expanded prompt', displayText: 'Visible prompt' },
placement: 'current_turn',
},
});
});
test('admits concurrent first messages into one Session in submission order', async () => {
// Two subscriptions so a driver that creates two Sessions fails on the
// claim rather than on missing fake infrastructure.
const connection = new FakeConnection([
new FakeSubscription(continuitySnapshot(), Promise.resolve([])),
new FakeSubscription(continuitySnapshot(), Promise.resolve([])),
]);
const create = deferred<void>();
connection.heldOperations.set('session.create', create.promise);
let nextId = 0;
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: () => `session-${++nextId}`,
});
// Two Enters before the first round trip resolves. Nothing about the TUI
// holds the second one back, so the driver is what has to keep them from
// racing into two Sessions or reaching the Host out of order.
const first = driver.submitMessage!('first', {
messageId: 'message-1',
placement: 'current_turn',
});
const second = driver.submitMessage!('second', {
messageId: 'message-2',
placement: 'current_turn',
});
create.resolve();
await Promise.all([first, second]);
const creates = connection.requests.filter(({ operation }) => operation === 'session.create');
assert.equal(creates.length, 1);
const submits = connection.requests.filter(
({ operation }) => operation === 'turn.message.submit',
);
assert.deepEqual(
submits.map(({ input }) => (input as OperationInput<'turn.message.submit'>).messageId),
['message-1', 'message-2'],
);
assert.deepEqual(
new Set(
submits.map(({ input }) => (input as OperationInput<'turn.message.submit'>).sessionId),
),
new Set(['session-1']),
);
});
test('keeps a configuration change from crossing a pending admission', async () => {
const subscription = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
await driver.switchSession('session-1');
const submit = deferred<void>();
connection.heldOperations.set('turn.message.submit', submit.promise);
const admitted = driver.submitMessage!('before the model change', {
messageId: 'message-1',
placement: 'current_turn',
});
// `/model` typed while the Message is still in flight. The Host must see
// it after the Message it was typed after, or the Turn that Message opens
// runs under a model the user had not chosen yet.
const changed = driver.setModel('gpt-5-codex');
submit.resolve();
await Promise.all([admitted, changed]);
const ordered = connection.requests
.map(({ operation }) => operation)
.filter(
(operation) =>
operation === 'turn.message.submit' || operation === 'session.configuration.update',
);
assert.deepEqual(ordered, ['turn.message.submit', 'session.configuration.update']);
});
test('keeps an unknown message admission available for transcript reconciliation', async () => {
const subscription = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const connection = new FakeConnection([subscription]);
connection.messageSubmitOutcomes.push(
new RuntimeHostOperationError(
'turn.message.submit',
'outcome_unknown',
'Message disposition cannot be proven in this Host Epoch',
),
);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
await driver.switchSession('session-1');
await assert.doesNotReject(() =>
driver.submitMessage!('Keep this visible', {
messageId: 'message-unknown',
placement: 'current_turn',
}),
);
});
test('keeps a dispatched interrupted admission available for transcript reconciliation', async () => {
const subscription = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const connection = new FakeConnection([subscription]);
connection.messageSubmitOutcomes.push(
new RuntimeHostRequestInterruptedError(
'turn.message.submit',
'command',
'dispatched',
'connection_lost',
),
);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
await driver.switchSession('session-1');
await assert.doesNotReject(() =>
driver.submitMessage!('Keep this visible', {
messageId: 'message-interrupted',
placement: 'current_turn',
}),
);
});
test('projects the acknowledgement that releases a question answered through the Host', async () => {
const subscription = new FakeSubscription(
continuitySnapshot({ interactions: { pending: [pendingQuestion()] } }),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 75,
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
assert.equal((await nextEvent(switched.activeTurn.events)).type, 'user_question_request');
await driver.respondToUserQuestion!({ requestId: 'question-1', answers: ['Yes'] });
assert.deepEqual(await nextEvent(switched.activeTurn.events), {
type: 'user_question_answer_ack',
id: 'host-interaction:question-1:2',
turnId: 'turn-1',
ts: 75,
requestId: 'question-1',
toolUseId: 'tool-question',
});
});
test('answers and releases a Host-owned form through the generic Interaction operation', async () => {
const subscription = new FakeSubscription(
continuitySnapshot({ interactions: { pending: [pendingForm()] } }),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 76,
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
assert.equal((await nextEvent(switched.activeTurn.events)).type, 'form_request');
await driver.respondToUserForm!({
requestId: 'form-1',
action: 'accept',
values: { version: 'v2' },
});
assert.deepEqual(connection.requests.at(-1), {
operation: 'interaction.answer',
input: {
sessionId: 'session-1',
interactionId: 'form-1',
answer: { kind: 'form', action: 'accept', values: { version: 'v2' } },
},
});
assert.deepEqual(await nextEvent(switched.activeTurn.events), {
type: 'form_answer_ack',
id: 'host-interaction:form-1:2',
turnId: 'turn-1',
ts: 76,
requestId: 'form-1',
toolUseId: 'tool-form',
});
});
test('publishes a pending permission that has no transcript event', async () => {
const permission = pendingPermission();
const subscription = new FakeSubscription(
continuitySnapshot({ interactions: { pending: [permission] } }),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
const published = deferred<InteractionPendingSnapshot>();
driver.subscribePendingInteractions((pending) => published.resolve(pending));
await driver.switchSession('session-1');
assert.deepEqual(await published.promise, permission);
});
test('keeps Host-triggered prompts out of rewind', async () => {
const attached = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const messages: StoredMessage[] = [
userMessage('turn-new', 'Newest prompt'),
{
...userMessage('turn-automation', 'Automated prompt'),
origin: { kind: 'legacy_automation', automationId: 'automation-1' },
},
{
...userMessage('turn-automation', 'Steer the automated turn'),
id: 'user-turn-automation-steering',
steeringEventId: 'runtime-event-steering',
},
];
const current = new FakeSubscription(
continuitySnapshot(),
Promise.resolve(messages),
'subscription-2',
);
const direct = new FakeSubscription(
continuitySnapshot(),
Promise.resolve(messages),
'subscription-3',
);
const connection = new FakeConnection([attached, current, direct]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
await driver.switchSession('session-1');
assert.deepEqual(await driver.listRewindTargets(), [
{ turnId: 'turn-new', label: 'Newest prompt' },
]);
await assert.rejects(
driver.rewindToTurn('turn-automation'),
/Host-triggered prompts are read-only/,
);
assert.equal(
connection.requests.some(({ operation }) => operation === 'session.revision.create'),
false,
);
});
test('fails rewind closed when the selected turn carries structured content', async () => {
// A rewind that refills only the human-facing text would silently drop
// the selected turn's quotes/attachments from the replacement submit —
// fail closed with a precise notice instead until the TUI can carry
// them (#5109).
const attachment = {
kind: 'image',
name: 'chart.png',
mimeType: 'image/png',
bytes: 10,
ref: { kind: 'session_file', sessionId: 'session-1', relativePath: 'a.png' },
} as const;
const messages: StoredMessage[] = [
userMessage('turn-plain', 'Plain prompt'),
{
...userMessage('turn-quoted', 'Quoted prompt'),
quotes: [{ text: 'a large pasted excerpt' }],
},
{
...userMessage('turn-attached', 'Attached prompt'),
attachments: [attachment],
},
{
...userMessage('turn-directory', 'Directory prompt'),
directoryReferences: [{ hostId: 'host-1', path: tmpdir() }],
},
];
const attached = new FakeSubscription(continuitySnapshot(), Promise.resolve(messages));
const current = new FakeSubscription(
continuitySnapshot(),
Promise.resolve(messages),
'subscription-2',
);
const direct = new FakeSubscription(
continuitySnapshot(),
Promise.resolve(messages),
'subscription-3',
);
const fourth = new FakeSubscription(
continuitySnapshot(),
Promise.resolve(messages),
'subscription-4',
);
const connection = new FakeConnection([attached, current, direct, fourth]);
// A directory that exists on every platform: the driver rejects a session
// whose cwd has disappeared, and the catalog projection's default `/tmp`
// only exists on POSIX.
const existingCwd = tmpdir();
connection.sessionQueries.push(
sessionProjection({
workspace: { target: { kind: 'host_path', path: existingCwd }, hostCwd: existingCwd },
}),
);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: existingCwd,
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
await driver.switchSession('session-1');
await assert.rejects(
driver.rewindToTurn('turn-quoted'),
/carries structured context the TUI cannot restore/,
);
await assert.rejects(
driver.rewindToTurn('turn-attached'),
/carries structured context the TUI cannot restore/,
);
await assert.rejects(
driver.rewindToTurn('turn-directory'),
/carries structured context the TUI cannot restore/,
);
await assert.rejects(
driver.rewindToTurn('turn-directory').catch((error: unknown) => {
const code = (error as { code?: unknown }).code;
assert.equal(code, 'rewind_unsupported_directory_references');
throw error;
}),
);
assert.equal(
connection.requests.some(({ operation }) => operation === 'session.revision.create'),
false,
'no revision is created for content the TUI cannot carry',
);
});
test('opens a hidden side copy at the latest completed Turn and removes it on close', async (t) => {
const cleanupRoot = await mkdtemp(join(tmpdir(), 'maka-tui-side-'));
t.after(() => rm(cleanupRoot, { recursive: true, force: true }));
const sourceMessages: StoredMessage[] = [
userMessage('turn-completed', 'Settled question'),
assistantMessage('turn-completed', 'Settled answer'),
turnStateMessage('turn-completed', 'completed'),
userMessage('turn-failed', 'Failed question'),
turnStateMessage('turn-failed', 'failed'),
];
const subscriptions = [
new FakeSubscription(continuitySnapshot({ rootTurn: null }), Promise.resolve(sourceMessages)),
new FakeSubscription(
continuitySnapshot({ rootTurn: null }),
Promise.resolve(sourceMessages),
'subscription-copy-source',
),
new FakeSubscription(
continuitySnapshot({ rootTurn: null }),
Promise.resolve(sourceMessages.slice(0, 3)),
'subscription-side',
),
new FakeSubscription(
continuitySnapshot({ rootTurn: null }),
Promise.resolve([
...sourceMessages.slice(0, 3),
userMessage('turn-side', 'Side question'),
assistantMessage('turn-side', 'Side answer'),
]),
'subscription-side-read',
),
new FakeSubscription(
continuitySnapshot({ rootTurn: null }),
Promise.resolve(sourceMessages),
'subscription-parent-return',
),
];
const connection = new FakeConnection(subscriptions);
connection.sessionQueries.push(
sessionProjection({ id: 'session-1' }),
sessionProjection({ id: 'session-1', revision: 4 }),
sessionProjection({
id: 'side-1',
labels: ['mode:side_conversation'],
parentSessionId: 'session-1',
branchOfTurnId: 'turn-completed',
}),
sessionProjection({ id: 'session-1' }),
sessionProjection({ id: 'side-1', labels: ['mode:side_conversation'] }),
);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: () => 'side-1',
sessionCopyCleanupRoot: cleanupRoot,
});
await driver.switchSession('session-1');
const opened = await driver.openSideConversation!();
assert.equal((await stat(join(cleanupRoot, 'a'.repeat(64), 'runtime.sqlite'))).isFile(), true);
assert.equal(opened.parentSessionId, 'session-1');
assert.equal(opened.sideSessionId, 'side-1');
assert.deepEqual(opened.messages, []);
assert.deepEqual(
(await driver.readMessages()).map((message) =>
'turnId' in message ? `${message.type}:${message.turnId}` : message.type,
),
['user:turn-side', 'assistant:turn-side'],
);
assert.deepEqual(
connection.requests.find(({ operation }) => operation === 'session.branch.create')?.input,
{
sourceSessionId: 'session-1',
targetSessionId: 'side-1',
sourceTurnId: 'turn-completed',
expectedSourceRevision: 4,
intent: 'side_conversation',
},
);
const closed = await driver.closeSideConversation!('side-1', 'session-1');
assert.equal(closed.summary.id, 'session-1');
assert.equal(closed.cleanup, 'removed');
assert.deepEqual(
connection.requests.find(({ operation }) => operation === 'session.remove')?.input,
{ sessionId: 'side-1', expectedRevision: 1 },
);
});
test('opens an empty side copy when the parent has no completed Turn yet', async (t) => {
const cleanupRoot = await mkdtemp(join(tmpdir(), 'maka-tui-side-empty-'));
t.after(() => rm(cleanupRoot, { recursive: true, force: true }));
// The parent's first Turn is still running (an explicit running turn_state),
// so there is no completed Turn to branch through. The side conversation
// must still open, forking with an empty context instead of erroring.
const sourceMessages: StoredMessage[] = [
userMessage('turn-running', 'In-flight question'),
{
type: 'turn_state',
id: 'state-turn-running',
turnId: 'turn-running',
ts: 80,
status: 'running',
},
];
const subscriptions = [
new FakeSubscription(continuitySnapshot({ rootTurn: null }), Promise.resolve(sourceMessages)),
new FakeSubscription(
continuitySnapshot({ rootTurn: null }),
Promise.resolve(sourceMessages),
'subscription-copy-source',
),
// The empty copy carries no source transcript.
new FakeSubscription(
continuitySnapshot({ rootTurn: null }),
Promise.resolve([]),
'subscription-side',
),
new FakeSubscription(
continuitySnapshot({ rootTurn: null }),
Promise.resolve([]),
'subscription-side-read',
),
new FakeSubscription(
continuitySnapshot({ rootTurn: null }),
Promise.resolve(sourceMessages),
'subscription-parent-return',
),
];
const connection = new FakeConnection(subscriptions);
connection.sessionQueries.push(
sessionProjection({ id: 'session-1' }),
sessionProjection({ id: 'session-1', revision: 4 }),
// The empty copy records provenance (parentSessionId) but fabricates no
// branchOfTurnId.
sessionProjection({
id: 'side-1',
labels: ['mode:side_conversation'],
parentSessionId: 'session-1',
}),
sessionProjection({ id: 'session-1' }),
sessionProjection({ id: 'side-1', labels: ['mode:side_conversation'] }),
);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: () => 'side-1',
sessionCopyCleanupRoot: cleanupRoot,
});
await driver.switchSession('session-1');
const opened = await driver.openSideConversation!();
assert.equal((await stat(join(cleanupRoot, 'a'.repeat(64), 'runtime.sqlite'))).isFile(), true);
assert.equal(opened.parentSessionId, 'session-1');
assert.equal(opened.sideSessionId, 'side-1');
assert.deepEqual(opened.messages, []);
assert.deepEqual(await driver.readMessages(), []);
// The branch omits sourceTurnId entirely (empty copy) while still carrying
// the side_conversation intent the Host requires for an empty copy.
assert.deepEqual(
connection.requests.find(({ operation }) => operation === 'session.branch.create')?.input,
{
sourceSessionId: 'session-1',
targetSessionId: 'side-1',
expectedSourceRevision: 4,
intent: 'side_conversation',
},
);
const closed = await driver.closeSideConversation!('side-1', 'session-1');
assert.equal(closed.summary.id, 'session-1');
assert.equal(closed.cleanup, 'removed');
});
test('observes actionable and terminal parent status from the Host projection', async () => {
const subscription = new FakeSubscription(
continuitySnapshot({ interactions: { pending: [pendingPermission()] } }),
Promise.resolve([]),
);
const driver = createRuntimeHostMakaSessionDriver({
connection: new FakeConnection([subscription]).value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
const statuses: Array<MakaSideConversationParentStatus | undefined> = [];
const stop = await driver.observeSideConversationParent!('session-1', (status) => {
statuses.push(status);
});
assert.equal(statuses.at(-1), 'needs_approval');
subscription.push(projectionFrame(1, runningTurn('turn-2', 'run-2'), 2));
await waitFor(() => statuses.at(-1) === undefined);
subscription.push(projectionFrame(2, completedTurn('turn-2', 'run-2'), 3));
await waitFor(() => statuses.at(-1) === 'finished');
await stop();
});
test('clears parent status when observer recovery is exhausted', async () => {
const snapshot = continuitySnapshot({ interactions: { pending: [pendingPermission()] } });
const initial = new FakeSubscription(snapshot, Promise.resolve([]));
const ended = Array.from({ length: 8 }, (_, index) => {
const subscription = new FakeSubscription(
{ ...snapshot, projectionRevision: index + 2 },
Promise.resolve([]),
`subscription-${index + 2}`,
);
void subscription.close();
return subscription;
});
const connection = new FakeConnection([initial, ...ended], true);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
const statuses: Array<MakaSideConversationParentStatus | undefined> = [];
const cleared = deferred<void>();
await driver.observeSideConversationParent!('session-1', (status) => {
statuses.push(status);
if (status === undefined) cleared.resolve();
});
assert.equal(statuses.at(-1), 'needs_approval');
await initial.close();
await Promise.race([
cleared.promise,
delay(3_000).then(() => assert.fail('Timed out waiting for observer recovery exhaustion')),
]);
assert.equal(connection.openedSubscriptions, 9);
assert.equal(statuses.at(-1), undefined);
});
test('reopens a failed Session channel before starting the next turn', async () => {
const first = new FakeSubscription(continuitySnapshot({ rootTurn: null }), Promise.resolve([]));
const second = new FakeSubscription(
continuitySnapshot({ rootTurn: null }),
Promise.resolve([]),
'subscription-2',
);
const connection = new FakeConnection([first, second]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: sequenceIds('turn-2'),
});
await driver.switchSession('session-1');
first.push({
kind: 'subscription.closed',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
reason: 'slow_consumer',
});
await new Promise((resolve) => setImmediate(resolve));
const turn = await driver.preparePrompt('Continue');
second.push(deltaFrame(1, 'turn-2', 0, 'Recovered', 'subscription-2', 'run-2'));
assert.equal((await nextEvent(turn.events)).text, 'Recovered');
});
test('starts explicit Skills through the Host command and preserves its typed feedback', async () => {
const subscription = new FakeSubscription(
continuitySnapshot({ rootTurn: null }),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: sequenceIds('turn-skill'),
});
await driver.switchSession('session-1');
const turn = await driver.preparePrompt('/skill:alpha Help');
assert.deepEqual(turn.skillInvocation?.loaded, [{ id: 'alpha', name: 'Alpha' }]);
assert.equal(connection.requests.at(-1)?.operation, 'turn.start');
connection.skillStartBlocked = true;
// The failure names what could not be resolved: headless `maka run` reports
// this message and nothing reads a structured payload off it.
await assert.rejects(driver.preparePrompt('/skill:missing', { turnId: 'turn-blocked' }), {
message: /Could not resolve the Skill this Turn asked for: \/skill:missing \(not found\)/,
});
});
test('retires a pending question when another client answers it', async () => {
const subscription = new FakeSubscription(
continuitySnapshot({ interactions: { pending: [pendingQuestion()] } }),
Promise.resolve([]),
);
const connection = new FakeConnection([subscription]);
connection.interactionQuery = {
...pendingQuestion(),
revision: 2,
status: 'answered',
outcome: { kind: 'question_answer', answers: ['Yes'], committedAt: 80 },
};
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
await driver.switchSession('session-1');
const resolved = deferred<string>();
driver.subscribeResolvedInteractions!((_sessionId, requestId) => resolved.resolve(requestId));
subscription.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
snapshot: continuitySnapshot({ projectionRevision: 2, interactions: { pending: [] } }),
});
assert.equal(await resolved.promise, 'question-1');
});
test('reconciles the durable transcript after a turn reaches its terminal boundary', async () => {
const attached = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const durableMessages = [userMessage('turn-1', 'Run it'), assistantMessage('turn-1', 'Done')];
const refresh = new FakeSubscription(
continuitySnapshot({ rootTurn: completedTurn('turn-1', 'run-1') }),
Promise.resolve(durableMessages),
'subscription-2',
);
const connection = new FakeConnection([attached, refresh]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
await driver.switchSession('session-1');
const replacement = deferred<readonly StoredMessage[]>();
driver.subscribeTranscriptReplacements!((_sessionId, _turnId, messages) =>
replacement.resolve(messages),
);
attached.push({
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
snapshot: continuitySnapshot({
projectionRevision: 2,
rootTurn: completedTurn('turn-1', 'run-1'),
}),
});
assert.deepEqual(await replacement.promise, durableMessages);
});
test('publishes only the newest live tool-result transcript refresh', async () => {
const attached = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const firstRefresh = new FakeSubscription(
continuitySnapshot(),
new Promise<StoredMessage[]>(() => undefined),
'subscription-2',
);
const secondMessages = [userMessage('turn-1', 'Run it'), assistantMessage('turn-1', 'Done')];
const secondRefresh = new FakeSubscription(
continuitySnapshot(),
Promise.resolve(secondMessages),
'subscription-3',
);
const connection = new FakeConnection([attached, firstRefresh, secondRefresh]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
await driver.switchSession('session-1');
const replacements: Array<readonly StoredMessage[]> = [];
driver.subscribeTranscriptReplacements!((_sessionId, _turnId, messages, reason) => {
assert.equal(reason, 'reconcile');
replacements.push(messages);
});
attached.push(toolResultFrame(1));
attached.push(toolResultFrame(2));
await waitFor(() => connection.openedSubscriptions === 3);
await waitFor(() => replacements.length === 1);
assert.deepEqual(replacements, [secondMessages]);
});
test('does not publish an older tool-result transcript after the terminal transcript', async () => {
const attached = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const liveTranscript = deferred<StoredMessage[]>();
const liveRefresh = new FakeSubscription(
continuitySnapshot(),
liveTranscript.promise,
'subscription-2',
);
const terminalMessages = [userMessage('turn-1', 'Run it'), assistantMessage('turn-1', 'Done')];
const terminalRefresh = new FakeSubscription(
continuitySnapshot({ rootTurn: completedTurn('turn-1', 'run-1') }),
Promise.resolve(terminalMessages),
'subscription-3',
);
const connection = new FakeConnection([attached, liveRefresh, terminalRefresh]);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
await driver.switchSession('session-1');
const replacements: Array<{ messages: readonly StoredMessage[]; reason: string }> = [];
driver.subscribeTranscriptReplacements!((_sessionId, _turnId, messages, reason) => {
replacements.push({ messages, reason });
});
attached.push(toolResultFrame(1));
await waitFor(() => connection.openedSubscriptions === 2);
attached.push(projectionFrame(2, completedTurn('turn-1', 'run-1'), 2));
await waitFor(() => replacements.length === 1);
assert.deepEqual(replacements, [{ messages: terminalMessages, reason: 'reconcile' }]);
liveTranscript.resolve([userMessage('turn-1', 'Run it')]);
await delay(0);
assert.deepEqual(replacements, [{ messages: terminalMessages, reason: 'reconcile' }]);
});
test('does not publish an older tool-result transcript after reconnect recovery', async () => {
const initial = new FakeSubscription(continuitySnapshot(), Promise.resolve([]));
const liveTranscript = deferred<StoredMessage[]>();
const liveRefresh = new FakeSubscription(
continuitySnapshot(),
liveTranscript.promise,
'subscription-2',
);
const recoveredMessages = [
userMessage('turn-1', 'Run it'),
assistantMessage('turn-1', 'Recovered'),
];
const recovered = new FakeSubscription(
continuitySnapshot({ projectionRevision: 2 }),
Promise.resolve(recoveredMessages),
'subscription-3',
);
const connection = new FakeConnection([initial, liveRefresh, recovered], true);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
await driver.switchSession('session-1');
const replacements: Array<{ messages: readonly StoredMessage[]; reason: string }> = [];
driver.subscribeTranscriptReplacements!((_sessionId, _turnId, messages, reason) => {
replacements.push({ messages, reason });
});
initial.push(toolResultFrame(1));
await waitFor(() => connection.openedSubscriptions === 2);
initial.fail(
new RuntimeHostSubscriptionError('connection_closed', 'connection lost during active Turn'),
);
await waitFor(() => replacements.length === 1);
assert.deepEqual(replacements, [{ messages: recoveredMessages, reason: 'reconnect' }]);
liveTranscript.resolve([userMessage('turn-1', 'Run it')]);
await delay(0);
assert.deepEqual(replacements, [{ messages: recoveredMessages, reason: 'reconnect' }]);
});
test('resnapshots an active Session after reconnect and continues its live stream', async () => {
const initial = new FakeSubscription(
continuitySnapshot(),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
);
const replacement = new FakeSubscription(
continuitySnapshot({ projectionRevision: 2 }),
Promise.resolve([assistantMessage('turn-1', 'Hello world')]),
'subscription-2',
);
const connection = new FakeConnection([initial, replacement], true);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
const transcript = deferred<readonly StoredMessage[]>();
driver.subscribeTranscriptReplacements!((_sessionId, _turnId, messages, reason) => {
assert.equal(reason, 'reconnect');
transcript.resolve(messages);
});
initial.fail(
new RuntimeHostSubscriptionError('connection_closed', 'connection lost during active Turn'),
);
assert.deepEqual(await transcript.promise, [assistantMessage('turn-1', 'Hello world')]);
assert.equal(connection.openedSubscriptions, 2);
replacement.push(deltaFrame(1, 'turn-1', 11, '!', 'subscription-2'));
assert.equal((await nextEvent(switched.activeTurn.events)).text, '!');
});
test('recovers the complete terminal answer when a Turn finishes during reconnect', async () => {
const initial = new FakeSubscription(
continuitySnapshot(),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
);
const replacement = new FakeSubscription(
continuitySnapshot({
projectionRevision: 2,
rootTurn: completedTurn('turn-1', 'run-1'),
}),
Promise.resolve([
assistantMessage('turn-1', 'Hello world'),
turnStateMessage('turn-1', 'completed'),
]),
'subscription-2',
);
const connection = new FakeConnection([initial, replacement], true);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
initial.fail(new RuntimeHostSubscriptionError('connection_closed', 'connection lost'));
const text = await nextEvent(switched.activeTurn.events);
assert.equal(text.type, 'text_complete');
assert.equal(text.text, 'Hello world');
assert.equal((await nextEvent(switched.activeTurn.events)).type, 'complete');
assert.equal((await switched.activeTurn.events[Symbol.asyncIterator]().next()).done, true);
});
test('settles an attached Turn before publishing its reconnect-gap successor', async () => {
const initial = new FakeSubscription(
continuitySnapshot(),
Promise.resolve([assistantMessage('turn-1', 'Working')]),
);
const replacementMessages = [
assistantMessage('turn-1', 'Finished'),
turnStateMessage('turn-1', 'completed'),
userMessage('turn-2', 'Continue'),
assistantMessage('turn-2', 'Continuing'),
];
const replacement = new FakeSubscription(
continuitySnapshot({
projectionRevision: 3,
rootTurn: runningTurn('turn-2', 'run-2'),
}),
Promise.resolve(replacementMessages),
'subscription-2',
);
const successor = new FakeSubscription(
continuitySnapshot({
projectionRevision: 3,
rootTurn: runningTurn('turn-2', 'run-2'),
}),
Promise.resolve(replacementMessages),
'subscription-3',
);
const connection = new FakeConnection([initial, replacement, successor], true);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
const started = deferred<MakaAttachedSessionTurn>();
driver.subscribeStartedTurns!((turn) => started.resolve(turn));
initial.fail(new RuntimeHostSubscriptionError('connection_closed', 'connection lost'));
assert.equal((await nextEvent(switched.activeTurn.events)).type, 'text_complete');
assert.equal((await nextEvent(switched.activeTurn.events)).type, 'complete');
assert.equal((await switched.activeTurn.events[Symbol.asyncIterator]().next()).done, true);
assert.equal((await started.promise).turnId, 'turn-2');
});
});
class FakeConnection {
readonly requests: Array<{ operation: string; input: unknown }> = [];
readonly sessionQueries: Array<SessionCatalogProjection | Promise<SessionCatalogProjection>> = [];
openedSubscriptions = 0;
interactionQuery: unknown;
runtimeResourceQuery: unknown;
todoQuery: OperationOutput<'session.todo.query'> | undefined;
onRuntimeResourceStart: (() => Promise<void>) | undefined;
executionBoundary: unknown = { kind: 'managed', access: 'read_write', revision: 1 };
skillStartBlocked = false;
/** When set, runtime.resource.stop rejects with this error (e.g. a draining Host). */
runtimeResourceStopFailure: Error | undefined;
/** Scripted outcomes for goal.control: return the result goal, or throw (e.g. operation_conflict). */
readonly goalControlOutcomes: Array<GoalProjection | Error> = [];
/** Scripted goal.query results, shifted per call; defaults to null (no goal). */
readonly goalQueryResults: Array<GoalProjection | null> = [];
readonly messageSubmitOutcomes: Array<OperationOutput<'turn.message.submit'> | Error> = [];
/**
* Operations held open by a test. The request is recorded on entry and then
* waits, so a test can hold one round trip and observe what the driver does
* with a second call while the first is still in flight.
*/
readonly heldOperations = new Map<string, Promise<void>>();
readonly userCommandResource = {
kind: 'shell_run' as const,
ref: 'maka://runtime/background-tasks/user-command',
mode: 'pipes' as const,
status: 'running' as const,
cwd: '/repo',
cmd: 'pwd',
startedAt: 1,
updatedAt: 1,
revision: 1,
output: {
mode: 'pipes' as const,
stdout: '',
stderr: '',
stdoutTruncated: false,
stderrTruncated: false,
redacted: false,
},
};
readonly configurationOutcomes: SessionUpdateResult[] = [];
readonly value: RuntimeHostMakaSessionDriverInput['connection'];
constructor(
private readonly subscriptions: FakeSubscription[],
reconnecting = false,
) {
this.value = {
...(reconnecting ? { reconnecting: true as const } : {}),
rootId: 'a'.repeat(64),
hostEpoch: 'host-1',
request: <K extends DirectRequestOperationKey>(operation: K, input: OperationInput<K>) =>
this.request(operation, input),
openSessionSubscription: async () => {
const subscription = this.subscriptions[this.openedSubscriptions];
this.openedSubscriptions += 1;
if (!subscription) throw new Error('No fake subscription available');
return subscription;
},
} satisfies RuntimeHostMakaSessionDriverInput['connection'];
}
async request<K extends DirectRequestOperationKey>(
operation: K,
input: OperationInput<K>,
): Promise<OperationOutput<K>> {
this.requests.push({ operation, input });
const held = this.heldOperations.get(operation);
if (held) await held;
if (operation === 'session.workspace.relocate') {
const workspace = (input as OperationInput<'session.workspace.relocate'>).workspace;
if (workspace.kind !== 'host_path') throw new Error('Expected Host-path workspace');
return {
kind: 'committed',
session: sessionProjection({
revision: 2,
workspace: { target: workspace, hostCwd: workspace.path },
}),
} as OperationOutput<K>;
}
if (operation === 'session.create') {
const create = input as OperationInput<'session.create'>;
return sessionProjection({
id: create.sessionId,
workspace: {
target: create.workspace,
hostCwd: create.workspace.kind === 'host_path' ? create.workspace.path : '/project',
},
}) as OperationOutput<K>;
}
if (operation === 'session.branch.create') {
const copy = input as OperationInput<'session.branch.create'>;
return {
kind: 'committed',
session: sessionProjection({
id: copy.targetSessionId,
labels: copy.intent === 'side_conversation' ? ['mode:side_conversation'] : [],
parentSessionId: copy.sourceSessionId,
branchOfTurnId: copy.sourceTurnId,
}),
} as OperationOutput<K>;
}
if (operation === 'session.remove') {
return {
kind: 'removed',
sessionId: (input as OperationInput<'session.remove'>).sessionId,
} as OperationOutput<K>;
}
if (operation === 'goal.control') {
const outcome = this.goalControlOutcomes.shift();
if (outcome === undefined) throw new Error('Unexpected goal.control request');
if (outcome instanceof Error) throw outcome;
return {
sessionId: (input as OperationInput<'goal.control'>).sessionId,
goal: outcome,
} as OperationOutput<K>;
}
if (operation === 'goal.query') {
return {
sessionId: (input as OperationInput<'goal.query'>).sessionId,
goal: this.goalQueryResults.shift() ?? null,
} as OperationOutput<K>;
}
if (operation === 'session.configuration.update') {
const update = input as OperationInput<'session.configuration.update'>;
const outcome = this.configurationOutcomes.shift();
if (outcome) return outcome as OperationOutput<K>;
return {
kind: 'committed',
session: sessionProjection({
revision: update.expectedRevision + 1,
permissionMode: update.patch.permissionMode ?? 'ask',
}),
} as OperationOutput<K>;
}
if (operation === 'runtime.resource.start') {
await this.onRuntimeResourceStart?.();
return { resource: this.userCommandResource } as OperationOutput<K>;
}
if (operation === 'runtime.resource.stop') {
if (this.runtimeResourceStopFailure) throw this.runtimeResourceStopFailure;
return {
resource: {
...this.userCommandResource,
status: 'cancelled',
updatedAt: 2,
completedAt: 2,
revision: 2,
},
} as OperationOutput<K>;
}
if (operation === 'runtime.resource.query') {
if (this.runtimeResourceQuery === undefined) {
throw new Error('Unexpected Runtime Resource query');
}
return this.runtimeResourceQuery as OperationOutput<K>;
}
if (operation === 'session.todo.query') {
if (this.todoQuery === undefined) throw new Error('Unexpected Session Todo query');
return this.todoQuery as OperationOutput<K>;
}
if (operation === 'turn.stop') {
return {} as OperationOutput<K>;
}
const turnInput = input as {
sessionId?: string;
turnId?: string;
content: { text: string };
};
const result: unknown =
operation === 'session.catalog.query'
? {
kind: 'session',
session: await (this.sessionQueries.shift() ?? sessionProjection()),
}
: operation === 'session.execution_boundary.query'
? this.executionBoundary
: operation === 'turn.message.submit'
? (() => {
const outcome = this.messageSubmitOutcomes.shift();
if (outcome instanceof Error) throw outcome;
return (
outcome ?? {
disposition:
(input as OperationInput<'turn.message.submit'>).placement === 'next_turn'
? 'followup'
: 'steering',
queueRevision: 2,
}
);
})()
: operation === 'queue.retract'
? {
hostEpoch: 'host-1',
queueRevision: 3,
retracted: [
{
entryId: 'entry-1',
messageId: 'message-1',
content: { text: 'Later' },
placement: 'next_turn',
},
],
}
: operation === 'interaction.answer'
? (input as OperationInput<'interaction.answer'>).answer.kind === 'form'
? {
...pendingForm(),
revision: 2,
status: 'answered',
outcome: {
kind: 'form_answer',
action: 'accept',
values: { version: 'v2' },
committedAt: 76,
},
}
: {
...pendingQuestion(),
revision: 2,
status: 'answered',
outcome: { kind: 'question_answer', answers: ['Yes'], committedAt: 75 },
}
: operation === 'interaction.query'
? this.interactionQuery
: operation === 'turn.start'
? this.skillStartBlocked
? {
kind: 'blocked',
skillInvocation: {
loaded: [],
failed: [{ request: 'missing', reason: 'not_found' }],
receipts: [],
},
}
: {
kind: 'started',
turn: {
sessionId: turnInput.sessionId,
turnId: turnInput.turnId,
runId: 'run-1',
status: 'running',
},
skillInvocation: turnInput.content.text.includes('/skill:')
? {
loaded: [{ id: 'alpha', name: 'Alpha' }],
failed: [],
receipts: [],
}
: { loaded: [], failed: [], receipts: [] },
}
: undefined;
if (result === undefined) throw new Error(`Unexpected fake operation: ${operation}`);
return result as OperationOutput<K>;
}
}
class FakeSubscription implements RuntimeHostSessionSubscription, AsyncIterator<SubscriptionFrame> {
subscribePtyData(): () => void {
return () => undefined;
}
readonly #sessionDomainListeners = new Set<
(frame: Extract<SubscriptionFrame, { kind: 'subscription.session_domain_changed' }>) => void
>();
subscribeSessionDomainChanges(
listener: (
frame: Extract<SubscriptionFrame, { kind: 'subscription.session_domain_changed' }>,
) => void,
): () => void {
this.#sessionDomainListeners.add(listener);
return () => this.#sessionDomainListeners.delete(listener);
}
readonly hostEpoch = 'host-1';
readonly activeAssistantStreams = [];
readonly transcriptBootstrap = null;
readonly subscriptionId: string;
readonly #frames: SubscriptionFrame[] = [];
readonly #waiters: Array<{
resolve(result: IteratorResult<SubscriptionFrame>): void;
reject(error: Error): void;
}> = [];
nextCalls = 0;
#closed = false;
#failure: Error | undefined;
constructor(
readonly snapshot: SessionContinuitySnapshot,
private readonly transcript: Promise<StoredMessage[]>,
subscriptionId = 'subscription-1',
) {
this.subscriptionId = subscriptionId;
}
[Symbol.asyncIterator](): AsyncIterator<SubscriptionFrame> {
return this;
}
next(): Promise<IteratorResult<SubscriptionFrame>> {
this.nextCalls += 1;
const frame = this.#frames.shift();
if (frame) return Promise.resolve({ done: false, value: frame });
if (this.#failure) return Promise.reject(this.#failure);
if (this.#closed) return Promise.resolve({ done: true, value: undefined });
return new Promise((resolve, reject) => this.#waiters.push({ resolve, reject }));
}
push(frame: SubscriptionFrame): void {
if (frame.kind === 'subscription.session_domain_changed') {
for (const listener of this.#sessionDomainListeners) listener(frame);
}
const waiter = this.#waiters.shift();
if (waiter) waiter.resolve({ done: false, value: frame });
else this.#frames.push(frame);
}
fail(error: Error): void {
this.#failure = error;
for (const waiter of this.#waiters.splice(0)) waiter.reject(error);
}
async loadTranscript<T>(decodeMessage: (value: unknown) => T): Promise<T[]> {
return (await this.transcript).map(decodeMessage);
}
async loadTranscriptOverlay<T>(_decodeMessage: (value: unknown) => T): Promise<T[]> {
return [];
}
async decodeTranscriptPage(): Promise<never> {
throw new Error('Fake subscription does not expose transcript pages');
}
async loadTranscriptPage(): Promise<never> {
throw new Error('Fake subscription does not expose transcript pages');
}
async close(): Promise<void> {
this.#closed = true;
for (const waiter of this.#waiters.splice(0)) {
waiter.resolve({ done: true, value: undefined });
}
}
}
function continuitySnapshot(
overrides: Partial<SessionContinuitySnapshot> = {},
): SessionContinuitySnapshot {
return {
schemaVersion: SESSION_CONTINUITY_SCHEMA_VERSION,
session: {
sessionId: 'session-1',
metadataRevision: 1,
status: 'running',
createdAt: 1,
isArchived: false,
},
projectionRevision: 1,
rootTurn: runningTurn('turn-1', 'run-1'),
goal: null,
queue: { hostEpoch: 'host-1', queueRevision: 0, steering: [], followup: [] },
interactions: { pending: [] },
...overrides,
};
}
function goalProjection(overrides: Partial<GoalProjection> = {}): GoalProjection {
return {
goalId: 'goal-1',
revision: 1,
sessionId: 'session-id',
condition: 'Ship the feature',
status: 'active',
setAt: 1,
iterations: 2,
maxIterations: 50,
consecutiveNoProgress: 0,
blockCap: 8,
tokenBudget: 100_000,
tokensSpent: 12_000,
lastReason: null,
achievedAt: null,
pausedAt: null,
...overrides,
};
}
function runningTurn(turnId: string, runId: string) {
return { sessionId: 'session-1', turnId, runId, status: 'running' as const };
}
function completedTurn(turnId: string, runId: string) {
return {
sessionId: 'session-1',
turnId,
runId,
status: 'completed' as const,
completedAt: 80,
terminalEventId: `terminal-${turnId}`,
};
}
function sessionProjection(
overrides: Partial<SessionCatalogProjection> = {},
): SessionCatalogProjection {
return {
id: 'session-1',
revision: 1,
workspace: {
target: { kind: 'host_path', path: '/tmp' },
hostCwd: '/tmp',
},
createdAt: 1,
activityAt: 2,
name: 'Session',
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',
...overrides,
};
}
function assistantMessage(turnId: string, text: string): StoredMessage {
return {
type: 'assistant',
id: `message-${turnId}`,
turnId,
ts: 10,
text,
modelId: 'gpt-5',
};
}
function userMessage(turnId: string, text: string): Extract<StoredMessage, { type: 'user' }> {
return { type: 'user', id: `user-${turnId}`, turnId, ts: 9, text };
}
function turnStateMessage(
turnId: string,
status: 'completed' | 'failed' | 'aborted',
): StoredMessage {
return {
type: 'turn_state',
id: `state-${turnId}`,
turnId,
ts: 80,
status,
};
}
function deltaFrame(
sequence: number,
turnId: string,
startOffset: number,
text: string,
subscriptionId = 'subscription-1',
runId = 'run-1',
): SubscriptionFrame {
return {
kind: 'subscription.session_delta',
hostEpoch: 'host-1',
subscriptionId,
sequence,
sessionId: 'session-1',
delta: {
kind: 'text',
turnId,
runId,
messageId: `message-${turnId}`,
startOffset,
text,
},
};
}
function textCompleteFrame(
sequence: number,
turnId: string,
startOffset: number,
text: string,
subscriptionId = 'subscription-1',
): SubscriptionFrame {
return {
kind: 'subscription.session_delta',
hostEpoch: 'host-1',
subscriptionId,
sequence,
sessionId: 'session-1',
delta: {
kind: 'text',
turnId,
runId: 'run-1',
messageId: `message-${turnId}`,
startOffset,
text,
complete: true,
},
};
}
function thinkingFrame(
sequence: number,
messageId: string,
startOffset: number,
text: string,
complete = false,
): SubscriptionFrame {
return {
kind: 'subscription.session_delta',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence,
sessionId: 'session-1',
delta: {
kind: 'thinking',
turnId: 'turn-1',
runId: 'run-1',
messageId,
startOffset,
text,
...(complete ? { complete: true } : {}),
},
};
}
function projectionFrame(
sequence: number,
rootTurn: NonNullable<SessionContinuitySnapshot['rootTurn']>,
projectionRevision: number,
subscriptionId = 'subscription-1',
): SubscriptionFrame {
return {
kind: 'subscription.session_projection',
hostEpoch: 'host-1',
subscriptionId,
sequence,
snapshot: continuitySnapshot({ projectionRevision, rootTurn }),
};
}
function pendingQuestion() {
return {
schemaVersion: 1 as const,
interactionId: 'question-1',
sessionId: 'session-1',
turnId: 'turn-1',
runId: 'run-1',
revision: 1 as const,
status: 'pending' as const,
outcome: null,
request: {
kind: 'question' as const,
toolUseId: 'tool-question',
questions: [{ question: 'Continue?', options: [{ label: 'Yes' }] }],
},
};
}
function pendingForm() {
return {
schemaVersion: 1 as const,
interactionId: 'form-1',
sessionId: 'session-1',
turnId: 'turn-1',
runId: 'run-1',
revision: 1 as const,
status: 'pending' as const,
outcome: null,
request: {
kind: 'form' as const,
toolUseId: 'tool-form',
message: 'Configure deployment',
requester: { name: 'deploy', source: 'Acme MCP' },
fields: [{ kind: 'string' as const, name: 'version', label: 'Version', required: true }],
},
};
}
function pendingPermission() {
return {
schemaVersion: 1 as const,
interactionId: 'permission-1',
sessionId: 'session-1',
turnId: 'turn-1',
runId: 'run-1',
revision: 1 as const,
status: 'pending' as const,
outcome: null,
request: {
kind: 'permission' as const,
toolUseId: 'tool-permission',
prompt: {
kind: 'tool_permission' as const,
toolName: 'Bash',
category: 'shell_unsafe' as const,
reason: 'shell_dangerous' as const,
review: { kind: 'command' as const, command: 'echo protected', cwd: '/tmp' },
rememberForTurnAllowed: true,
},
},
};
}
async function nextEvent(events: AsyncIterable<unknown>): Promise<any> {
const iterator = events[Symbol.asyncIterator]();
const result = await Promise.race([
iterator.next(),
delay(WAIT_BUDGET_MS).then(() => assert.fail('Timed out waiting for Session event')),
]);
assert.equal(result.done, false);
return result.value;
}
function sequenceIds(...ids: string[]): () => string {
let index = 0;
return () => ids[index++] ?? `id-${index}`;
}
async function waitFor(predicate: () => boolean): Promise<void> {
await pollFor(predicate, {
timeoutMs: WAIT_BUDGET_MS,
message: 'Timed out waiting for fake Host state',
});
}
describe('turn consumer lag recovery (#3180)', () => {
async function floodTurnStream(
subscription: InstanceType<typeof FakeSubscription>,
count: number,
startOffset: number,
subscriptionId = 'subscription-1',
): Promise<void> {
let offset = startOffset;
for (let index = 0; index < count; index += 1) {
const text = `x${String(index).padStart(4, '0')}`;
subscription.push(deltaFrame(index + 1, 'turn-1', offset, text, subscriptionId));
offset += text.length;
if (index % 64 === 63) await delay(0);
}
await delay(0);
}
async function floodToolStream(
subscription: InstanceType<typeof FakeSubscription>,
count: number,
subscriptionId = 'subscription-1',
startSequence = 1,
): Promise<void> {
for (let index = 0; index < count; index += 1) {
subscription.push(
toolStartFrame(startSequence + index, startSequence + index, subscriptionId),
);
if (index % 64 === 63) await delay(0);
}
await delay(0);
}
async function floodToolOutput(
subscription: InstanceType<typeof FakeSubscription>,
count: number,
subscriptionId = 'subscription-1',
startSequence = 1,
): Promise<void> {
for (let index = 0; index < count; index += 1) {
subscription.push(
toolOutputDeltaFrame(startSequence + index, startSequence + index, subscriptionId),
);
if (index % 64 === 63) await delay(0);
}
await delay(0);
}
async function waitForSubscriptions(connection: FakeConnection, count: number): Promise<void> {
const deadline = Date.now() + WAIT_BUDGET_MS;
while (connection.openedSubscriptions !== count && Date.now() < deadline) await delay(5);
assert.equal(connection.openedSubscriptions, count);
}
function lagRecoveryFixture() {
const initial = new FakeSubscription(
continuitySnapshot(),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
);
const replacement = new FakeSubscription(
continuitySnapshot({ projectionRevision: 2 }),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
'subscription-2',
);
const connection = new FakeConnection([initial, replacement], true);
const resynced = deferred<void>();
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
driver.subscribeTranscriptReplacements!((_sessionId, _turnId, _messages, reason) => {
if (reason === 'reconnect') resynced.resolve();
});
return { initial, replacement, connection, driver, resynced };
}
async function drainUntilDone(events: AsyncIterable<unknown>): Promise<boolean> {
const iterator = events[Symbol.asyncIterator]();
let completed = false;
for (let index = 0; index < 1_200; index += 1) {
const result = await iterator.next();
if (result.done) return completed;
if ((result.value as { type?: string }).type === 'complete') completed = true;
}
return false;
}
test('resubscribes instead of failing when a turn event consumer falls behind', async () => {
const { initial, replacement, connection, driver, resynced } = lagRecoveryFixture();
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
// Flood the unconsumed turn stream past its 1024-event bound.
await floodTurnStream(initial, 1_100, 5);
// The channel retires the lagged subscription, resubscribes, and compacts
// the sheddable backlog the canonical resync supersedes.
await waitForSubscriptions(connection, 2);
await resynced.promise;
// The stream never rejected, and live events land right away.
replacement.push(deltaFrame(1, 'turn-1', 5, ' world', 'subscription-2'));
assert.equal((await nextEvent(switched.activeTurn.events)).text, ' world');
});
test('lands terminal events while shedding deltas from a lagging consumer', async () => {
const { initial, replacement, connection, driver, resynced } = lagRecoveryFixture();
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
await floodTurnStream(initial, 1_100, 5);
await waitForSubscriptions(connection, 2);
await resynced.promise;
await floodTurnStream(replacement, 1_024, 5, 'subscription-2');
replacement.push(projectionFrame(1_025, completedTurn('turn-1', 'run-1'), 2, 'subscription-2'));
await delay(0);
assert.ok(
await drainUntilDone(switched.activeTurn.events),
'terminal complete event survived the lagged delta backlog',
);
});
test('admits a terminal outcome when the lagged backlog holds no deltas', async () => {
const { initial, replacement, connection, driver, resynced } = lagRecoveryFixture();
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
// Fill the bound with non-delta events: nothing sheddable to evict.
await floodToolStream(initial, 1_100);
await waitForSubscriptions(connection, 2);
await resynced.promise;
await floodToolStream(replacement, 1_024, 'subscription-2');
// The terminal outcome must land even though no delta can be evicted;
// process the frame before draining so the backlog is still full.
replacement.push(projectionFrame(1_025, completedTurn('turn-1', 'run-1'), 2, 'subscription-2'));
await delay(0);
assert.ok(
await drainUntilDone(switched.activeTurn.events),
'terminal complete event was admitted over a non-delta backlog',
);
});
test('admits assistant completion before the terminal outcome over a non-delta backlog', async () => {
const { initial, replacement, connection, driver, resynced } = lagRecoveryFixture();
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
await floodToolStream(initial, 1_100);
await waitForSubscriptions(connection, 2);
await resynced.promise;
await floodToolStream(replacement, 1_024, 'subscription-2');
replacement.push(textCompleteFrame(1_025, 'turn-1', 5, ' final answer', 'subscription-2'));
replacement.push(projectionFrame(1_026, completedTurn('turn-1', 'run-1'), 2, 'subscription-2'));
await delay(0);
let finalOutput: string | undefined;
let completed = false;
for await (const event of switched.activeTurn.events) {
if (event.type === 'text_complete') finalOutput = event.text;
if (event.type === 'complete') completed = true;
}
assert.equal(finalOutput, 'Hello final answer');
assert.equal(completed, true);
});
test('drops the entire pre-resync tool backlog at the canonical cut', async () => {
const { initial, replacement, connection, driver, resynced } = lagRecoveryFixture();
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
await floodToolStream(initial, 1_100);
await waitForSubscriptions(connection, 2);
await resynced.promise;
replacement.push(toolStartFrame(1, 9_000, 'subscription-2'));
replacement.push(projectionFrame(2, completedTurn('turn-1', 'run-1'), 2, 'subscription-2'));
await delay(0);
const iterator = switched.activeTurn.events[Symbol.asyncIterator]();
const first = await iterator.next();
assert.equal(first.done, false);
assert.equal(first.value.type, 'tool_start');
if (first.value.type === 'tool_start') assert.equal(first.value.toolUseId, 'tool-9000');
});
test('admits a tool result when the lagged backlog holds no deltas', async () => {
const { initial, replacement, connection, driver, resynced } = lagRecoveryFixture();
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
// Fill the bound with non-delta events: nothing sheddable to evict.
await floodToolStream(initial, 1_100);
await waitForSubscriptions(connection, 2);
await resynced.promise;
await floodToolStream(replacement, 1_024, 'subscription-2');
// The tool result is the authoritative terminal outcome for its tool and
// must land even though no delta can be evicted; otherwise the live tool
// card stays running until the durable transcript heals it.
replacement.push(toolResultFrame(1_025, 'subscription-2'));
replacement.push(projectionFrame(1_026, completedTurn('turn-1', 'run-1'), 2, 'subscription-2'));
await delay(0);
let sawToolResult = false;
const iterator = switched.activeTurn.events[Symbol.asyncIterator]();
for (let index = 0; index < 1_200; index += 1) {
const result = await iterator.next();
if (result.done) break;
if ((result.value as { type?: string }).type === 'tool_result') sawToolResult = true;
}
assert.ok(sawToolResult, 'tool_result was admitted over a non-delta backlog');
});
test('sheds lagged tool output deltas so the tool result and terminal outcome land', async () => {
const { initial, replacement, connection, driver, resynced } = lagRecoveryFixture();
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
// A noisy tool floods the unconsumed stream with seq-ordered output
// deltas, the realistic way a consumer falls behind.
await floodToolOutput(initial, 1_100);
await waitForSubscriptions(connection, 2);
await resynced.promise;
await floodToolOutput(replacement, 1_024, 'subscription-2');
// The canonical resync compacts the unseen tool deltas, so the tool
// result lands instead of being dropped behind a full non-delta backlog
// (which would leave the live card stuck at "running" until the durable
// transcript heals it).
replacement.push(toolResultFrame(1_025, 'subscription-2'));
replacement.push(projectionFrame(1_026, completedTurn('turn-1', 'run-1'), 2, 'subscription-2'));
await delay(0);
let sawToolResult = false;
const iterator = switched.activeTurn.events[Symbol.asyncIterator]();
for (let index = 0; index < 1_200; index += 1) {
const result = await iterator.next();
if (result.done) break;
if ((result.value as { type?: string }).type === 'tool_result') sawToolResult = true;
if ((result.value as { type?: string }).type === 'complete') {
assert.ok(sawToolResult, 'tool_result landed ahead of the terminal outcome');
return;
}
}
assert.fail('stream ended without the terminal complete event');
});
test('resubscribes when the live stream ends without a terminal close', async () => {
const initial = new FakeSubscription(
continuitySnapshot(),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
);
const replacement = new FakeSubscription(
continuitySnapshot({ projectionRevision: 2 }),
Promise.resolve([assistantMessage('turn-1', 'Hello world')]),
'subscription-2',
);
const connection = new FakeConnection([initial, replacement], true);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
const transcript = deferred<readonly StoredMessage[]>();
driver.subscribeTranscriptReplacements!((_sessionId, _turnId, messages, reason) => {
assert.equal(reason, 'reconnect');
transcript.resolve(messages);
});
// A clean iterator end with no subscription.closed frame — e.g. the Host
// evicted the subscription as a slow consumer while the channel was still
// buffering the catch-up transcript — used to fail the channel
// permanently. It must resubscribe and continue the live stream instead.
await initial.close();
assert.deepEqual(await transcript.promise, [assistantMessage('turn-1', 'Hello world')]);
assert.equal(connection.openedSubscriptions, 2);
replacement.push(deltaFrame(1, 'turn-1', 11, '!', 'subscription-2'));
assert.equal((await nextEvent(switched.activeTurn.events)).text, '!');
});
test('recovers when slow-consumer closure is buffered during initial hydration', async () => {
const transcript = deferred<StoredMessage[]>();
const initial = new FakeSubscription(continuitySnapshot(), transcript.promise);
const replacement = new FakeSubscription(
continuitySnapshot({ projectionRevision: 2 }),
Promise.resolve([assistantMessage('turn-1', 'Hello world')]),
'subscription-2',
);
const connection = new FakeConnection([initial, replacement], true);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
const switching = driver.switchSession('session-1');
await waitFor(() => initial.nextCalls === 1);
initial.push({
kind: 'subscription.closed',
hostEpoch: 'host-1',
subscriptionId: 'subscription-1',
sequence: 1,
reason: 'slow_consumer',
});
await waitFor(() => initial.nextCalls === 2);
transcript.resolve([assistantMessage('turn-1', 'Hello')]);
const switched = await switching;
assert.ok(switched.activeTurn);
assert.equal(connection.openedSubscriptions, 2);
replacement.push(deltaFrame(1, 'turn-1', 11, '!', 'subscription-2'));
assert.equal((await nextEvent(switched.activeTurn.events)).text, '!');
});
test('backs off several immediate clean-EOF replacements before recovering', async () => {
const initial = new FakeSubscription(
continuitySnapshot(),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
);
const ended = [2, 3, 4].map(
(index) =>
new FakeSubscription(
continuitySnapshot({ projectionRevision: index }),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
`subscription-${index}`,
),
);
for (const subscription of ended) await subscription.close();
const stable = new FakeSubscription(
continuitySnapshot({ projectionRevision: 5 }),
Promise.resolve([assistantMessage('turn-1', 'Hello world')]),
'subscription-5',
);
const connection = new FakeConnection([initial, ...ended, stable], true);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
const resynced = deferred<void>();
driver.subscribeTranscriptReplacements!((_sessionId, _turnId, _messages, reason) => {
if (reason === 'reconnect') resynced.resolve();
});
await initial.close();
await waitForSubscriptions(connection, 2);
await delay(5);
assert.equal(connection.openedSubscriptions, 2, 'the first repeated EOF is backoff-gated');
await resynced.promise;
assert.equal(connection.openedSubscriptions, 5);
stable.push(deltaFrame(1, 'turn-1', 11, '!', 'subscription-5'));
assert.equal((await nextEvent(switched.activeTurn.events)).text, '!');
});
for (const [name, replacementRoot] of [
['the same terminal turn', completedTurn('turn-1', 'run-1')],
['a successor turn', runningTurn('turn-2', 'run-2')],
] as const) {
test(`preserves an unconsumed terminal event across a replacement with ${name}`, async () => {
const initial = new FakeSubscription(
continuitySnapshot(),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
);
const replacement = new FakeSubscription(
continuitySnapshot({ projectionRevision: 3, rootTurn: replacementRoot }),
Promise.resolve([
assistantMessage('turn-1', 'Hello'),
turnStateMessage('turn-1', 'completed'),
...(replacementRoot.turnId === 'turn-2' ? [userMessage('turn-2', 'Continue')] : []),
]),
'subscription-2',
);
const connection = new FakeConnection([initial, replacement], true);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
initial.push(projectionFrame(1, completedTurn('turn-1', 'run-1'), 2));
await delay(0);
initial.fail(new RuntimeHostSubscriptionError('connection_closed', 'connection lost'));
await waitForSubscriptions(connection, 2);
assert.equal((await nextEvent(switched.activeTurn.events)).type, 'complete');
assert.equal((await switched.activeTurn.events[Symbol.asyncIterator]().next()).done, true);
});
}
test('exhausts recovery after repeated one-frame clean-EOF replacements', async () => {
const initial = new FakeSubscription(
continuitySnapshot(),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
);
const ended = Array.from({ length: 8 }, (_, index) => {
const subscription = new FakeSubscription(
continuitySnapshot({ projectionRevision: index + 2 }),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
`subscription-${index + 2}`,
);
subscription.push(deltaFrame(1, 'turn-1', 5, String(index), `subscription-${index + 2}`));
return subscription;
});
for (const subscription of ended) await subscription.close();
const connection = new FakeConnection([initial, ...ended], true);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
await initial.close();
await assert.rejects(async () => {
for await (const _event of switched.activeTurn!.events) {
// Drain each replacement's single live frame until recovery fails.
}
}, /recovery exhausted its retry budget/u);
assert.equal(connection.openedSubscriptions, 9);
});
test('does not reset recovery after a silent replacement outlives the stability window', async () => {
const initial = new FakeSubscription(
continuitySnapshot(),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
);
const silent = new FakeSubscription(
continuitySnapshot({ projectionRevision: 2 }),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
'subscription-2',
);
const ended = Array.from({ length: 7 }, (_, index) => {
const subscription = new FakeSubscription(
continuitySnapshot({ projectionRevision: index + 3 }),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
`subscription-${index + 3}`,
);
void subscription.close();
return subscription;
});
const connection = new FakeConnection([initial, silent, ...ended], true);
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
await initial.close();
await waitForSubscriptions(connection, 2);
await delay(1_100);
await silent.close();
await assert.rejects(async () => {
for await (const _event of switched.activeTurn!.events) {
// A silent hydrated subscription is not evidence of live stability.
}
}, /recovery exhausted its retry budget/u);
assert.equal(connection.openedSubscriptions, 9);
});
test('re-arms lag detection exactly at the hysteresis watermark', async () => {
const initial = new FakeSubscription(
continuitySnapshot(),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
);
const second = new FakeSubscription(
continuitySnapshot({ projectionRevision: 2 }),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
'subscription-2',
);
const third = new FakeSubscription(
continuitySnapshot({ projectionRevision: 3 }),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
'subscription-3',
);
const connection = new FakeConnection([initial, second, third], true);
let resyncs = 0;
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
driver.subscribeTranscriptReplacements!((_sessionId, _turnId, _messages, reason) => {
if (reason === 'reconnect') resyncs += 1;
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
// Latch the lag flag with a non-delta backlog. The canonical cut clears
// every pre-cut event, then a still-wedged consumer fills again without
// triggering a resubscribe loop.
await floodToolStream(initial, 1_100);
await waitForSubscriptions(connection, 2);
await waitFor(() => resyncs === 1);
await floodToolStream(second, 1_100, 'subscription-2', 1);
await delay(20);
assert.equal(connection.openedSubscriptions, 2, 'the post-cut lag latch stayed armed');
// Draining to one event above the watermark (513 pending) must NOT
// re-arm: a fresh overflow on the still-latched queue is the same lag
// episode and triggers no new recovery. The flood refills the backlog
// to the bound.
const iterator = switched.activeTurn.events[Symbol.asyncIterator]();
for (let index = 0; index < 511; index += 1) {
assert.equal((await iterator.next()).done, false);
}
await floodToolStream(second, 600, 'subscription-2', 1_101);
await delay(20);
assert.equal(connection.openedSubscriptions, 2, 'lag latch held above the watermark');
// Draining the refilled backlog down to the watermark (512 pending)
// re-arms: the next overflow is a new lag episode and resubscribes again.
for (let index = 0; index < 512; index += 1) {
assert.equal((await iterator.next()).done, false);
}
await floodToolStream(second, 600, 'subscription-2', 1_701);
await waitForSubscriptions(connection, 3);
await waitFor(() => resyncs === 2);
});
test('recovers again when the consumer lags again after making progress', async () => {
const initial = new FakeSubscription(
continuitySnapshot(),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
);
const second = new FakeSubscription(
continuitySnapshot({ projectionRevision: 2 }),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
'subscription-2',
);
const third = new FakeSubscription(
continuitySnapshot({ projectionRevision: 3 }),
Promise.resolve([assistantMessage('turn-1', 'Hello')]),
'subscription-3',
);
const connection = new FakeConnection([initial, second, third], true);
let resyncs = 0;
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/tmp',
llmConnectionId: 'connection-1',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
driver.subscribeTranscriptReplacements!((_sessionId, _turnId, _messages, reason) => {
if (reason === 'reconnect') resyncs += 1;
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
// First lag episode over a non-delta backlog. The canonical cut clears the
// retired subscription's events; a still-wedged consumer can fill again
// without immediately looping recovery.
await floodToolStream(initial, 1_100);
await waitForSubscriptions(connection, 2);
await waitFor(() => resyncs === 1);
await floodToolStream(second, 1_100, 'subscription-2', 1);
// The consumer drains past the hysteresis watermark, re-arming lag
// detection, and fresh output flows again. One hundred events stay queued
// behind the delta, so the backlog never empties.
const iterator = switched.activeTurn.events[Symbol.asyncIterator]();
for (let index = 0; index < 600; index += 1) {
const result = await iterator.next();
assert.equal(result.done, false);
}
second.push(deltaFrame(1_101, 'turn-1', 5, ' world', 'subscription-2'));
for (let index = 0; index < 100; index += 1) {
second.push(toolStartFrame(1_102 + index, 2_000 + index, 'subscription-2'));
}
await delay(0);
let fresh = '';
for (let index = 0; index < 425; index += 1) {
const result = await iterator.next();
assert.equal(result.done, false);
fresh = (result.value as { text?: string }).text ?? '';
}
assert.equal(fresh, ' world');
// A second lag episode is a new episode, not a dead latch: it triggers a
// fresh canonical resync. The stream stays contiguous on `second`.
await floodToolStream(second, 1_100, 'subscription-2', 1_202);
await waitForSubscriptions(connection, 3);
await waitFor(() => resyncs === 2);
third.push(projectionFrame(1, completedTurn('turn-1', 'run-1'), 3, 'subscription-3'));
await delay(0);
assert.ok(
await drainUntilDone(switched.activeTurn.events),
'stream still completes after repeated lag recoveries',
);
});
});
function toolStartFrame(
sequence: number,
index: number,
subscriptionId = 'subscription-1',
): SubscriptionFrame {
return {
kind: 'subscription.session_event',
hostEpoch: 'host-1',
subscriptionId,
sequence,
sessionId: 'session-1',
runId: 'run-1',
event: {
type: 'tool_start',
id: `tool-${index}`,
turnId: 'turn-1',
ts: 10,
toolUseId: `tool-${index}`,
toolName: 'Bash',
},
};
}
function toolOutputDeltaFrame(
sequence: number,
seq: number,
subscriptionId = 'subscription-1',
): SubscriptionFrame {
return {
kind: 'subscription.session_event',
hostEpoch: 'host-1',
subscriptionId,
sequence,
sessionId: 'session-1',
runId: 'run-1',
event: {
type: 'tool_output_delta',
id: `output-${seq}`,
turnId: 'turn-1',
ts: 10,
toolUseId: 'tool-1',
seq,
stream: 'stdout',
chunk: `chunk-${seq}`,
redacted: false,
createdAt: 10,
},
};
}
function toolResultFrame(sequence: number, subscriptionId = 'subscription-1'): SubscriptionFrame {
return {
kind: 'subscription.session_event',
hostEpoch: 'host-1',
subscriptionId,
sequence,
sessionId: 'session-1',
runId: 'run-1',
event: {
type: 'tool_result',
id: 'result-tool-1',
turnId: 'turn-1',
ts: 11,
toolUseId: 'tool-1',
status: 'completed',
},
};
}