blob: 371a2b241639083510a04d1b2cbfa2cfddd751e2 [file]
import assert from 'node:assert/strict';
import { mkdir, mkdtemp, realpath, rm } 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 {
DirectRequestOperationKey,
RuntimeHostSessionSubscription,
} from '@maka/runtime-host/client';
import { RuntimeHostOperationError, RuntimeHostSubscriptionError } from '@maka/runtime-host/client';
import {
SESSION_CONTINUITY_SCHEMA_VERSION,
type GoalProjection,
type InteractionPendingSnapshot,
type OperationInput,
type OperationOutput,
type SessionCatalogProjection,
type SessionContinuitySnapshot,
type SubscriptionFrame,
} from '@maka/runtime-host/protocol';
import {
createRuntimeHostMakaSessionDriver,
type RuntimeHostMakaSessionDriverInput,
} from '../runtime-host-session-driver.js';
import { SkillInvocationBlockedError, type MakaAttachedSessionTurn } from '../session-driver.js';
import { WAIT_BUDGET_MS } from './tui-terminal-mock.js';
describe('Runtime Host Maka Session driver', () => {
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' },
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',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
executionLocation: { kind: 'host' },
});
await assert.rejects(
driverWithoutProject.createSession({
cwd: '/client/workspace',
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',
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',
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.
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',
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',
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' },
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: () => 'session-id',
});
await driver.createSession({
cwd: candidate.cwd,
...('projectId' in candidate ? { projectId: candidate.projectId } : {}),
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',
connectionSlug: 'openai-main',
model: 'gpt-5',
},
permissionMode: 'ask',
},
);
}
});
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,
lastUsedAt: 1,
isArchived: false,
},
rootTurn: null,
}),
Promise.resolve([]),
),
]);
let nextId = 0;
const driver = createRuntimeHostMakaSessionDriver({
connection: connection.value,
cwd: '/repo',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
newId: () => `session-${++nextId}`,
});
await driver.createSession({
cwd: '/repo',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
permissionMode: 'ask',
});
connection.executionBoundary = { kind: 'bypass', revision: 2 };
await driver.setPermissionMode('bypass');
assert.equal(driver.getPermissionMode?.(), 'bypass');
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);
assert.deepEqual(creates[1]!.input, {
sessionId: 'session-2',
workspace: { kind: 'host_path', path: '/repo' },
name: 'New Chat',
modelTarget: {
kind: 'explicit',
connectionSlug: 'openai-main',
model: 'gpt-5',
},
permissionMode: 'ask',
});
});
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,
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(),
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',
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',
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',
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',
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',
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('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',
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',
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',
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',
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',
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',
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',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
newId: sequenceIds('message-1', 'retract-1'),
});
await driver.switchSession('session-1');
assert.deepEqual(await driver.queueMessage!('Later'), { kind: 'queued' });
assert.equal(await driver.retractQueued!(), 'Later');
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('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',
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('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',
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',
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('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',
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',
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;
await assert.rejects(
driver.preparePrompt('/skill:missing', { turnId: 'turn-blocked' }),
SkillInvocationBlockedError,
);
});
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',
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',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
});
await driver.switchSession('session-1');
const replacement = deferred<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('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',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
const transcript = deferred<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',
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',
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;
executionBoundary: unknown = { kind: 'managed', access: 'read_write', revision: 1 };
skillStartBlocked = false;
/** 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 value: RuntimeHostMakaSessionDriverInput['connection'];
constructor(
private readonly subscriptions: FakeSubscription[],
reconnecting = false,
) {
this.value = {
...(reconnecting ? { reconnecting: true as const } : {}),
hostEpoch: 'host-1',
request: <K extends DirectRequestOperationKey>(operation: K, input: OperationInput<K>) =>
this.request(operation, input),
startTurn: (input) => this.request('turn.start', 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 });
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 === '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'>;
return {
kind: 'committed',
session: sessionProjection({
revision: update.expectedRevision + 1,
permissionMode: update.configuration.permissionMode,
}),
} 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'
? { disposition: 'queued', 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'
? {
...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> {
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 {
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,
lastUsedAt: 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,
lastUsedAt: 2,
name: 'Session',
isFlagged: false,
isArchived: false,
labels: [],
labelsTruncated: false,
hasUnread: false,
status: 'active',
backend: 'ai-sdk',
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,
partialOutputRetained: true,
};
}
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 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}`;
}
function deferred<T>(): {
promise: Promise<T>;
resolve(value: T): void;
reject(error: unknown): void;
} {
let resolve!: (value: T) => void;
let reject!: (error: unknown) => void;
const promise = new Promise<T>((settle, fail) => {
resolve = settle;
reject = fail;
});
return { promise, resolve, reject };
}
async function waitFor(predicate: () => boolean): Promise<void> {
const deadline = Date.now() + WAIT_BUDGET_MS;
while (Date.now() < deadline) {
if (predicate()) return;
await new Promise((resolve) => setImmediate(resolve));
}
assert.fail('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',
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',
llmConnectionSlug: 'openai-main',
model: 'gpt-5',
now: () => 50,
});
const switched = await driver.switchSession('session-1');
assert.ok(switched.activeTurn);
const transcript = deferred<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',
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',
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',
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',
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',
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',
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',
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',
},
};
}