diff --git a/apps/desktop/e2e/fixtures.ts b/apps/desktop/e2e/fixtures.ts index 885c83d7e8..0c0e85a38a 100644 --- a/apps/desktop/e2e/fixtures.ts +++ b/apps/desktop/e2e/fixtures.ts @@ -135,7 +135,6 @@ async function seedParentRemovalSessions(userDataDir: string): Promise { try { const parent = await store.create({ cwd: path.join(userDataDir, 'project'), - backend: 'fake', llmConnectionSlug: 'e2e', model: 'claude-sonnet-4-5-20250929', permissionMode: 'ask', @@ -144,7 +143,6 @@ async function seedParentRemovalSessions(userDataDir: string): Promise { }); await store.createSubagent({ cwd: path.join(userDataDir, 'project'), - backend: 'fake', llmConnectionSlug: 'e2e', model: 'claude-sonnet-4-5-20250929', permissionMode: 'execute', diff --git a/apps/desktop/src/main/__tests__/pending-session-view.test.ts b/apps/desktop/src/main/__tests__/pending-session-view.test.ts new file mode 100644 index 0000000000..30b1f601a3 --- /dev/null +++ b/apps/desktop/src/main/__tests__/pending-session-view.test.ts @@ -0,0 +1,45 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { pendingSessionView } from '../../renderer/pending-session-view.js'; + +test('the pending chat view names no connection or model it cannot know', () => { + const view = pendingSessionView({ + sessionId: 'session-1', + name: '新任务', + permissionMode: 'ask', + }); + + // #3211: the placeholder used to claim `backend: 'fake'` / `model: + // 'fake-model'`, borrowing a retired backend to mean "not loaded". + assert.equal(view.backend, 'ai-sdk'); + assert.equal(view.id, 'session-1'); + assert.equal(view.permissionMode, 'ask'); + assert.equal(view.connectionLocked, false); + + // The empty pair is load-bearing: this fallback covers any active id whose + // summary has not arrived, so naming a plausible connection/model would let + // the model switcher drop a real switch onto that model as a no-op against a + // session that was never on it. + assert.equal(view.llmConnectionSlug, ''); + assert.equal(view.model, ''); +}); + +test('the pending chat view matches no offered model choice', () => { + const view = pendingSessionView({ + sessionId: 'session-2', + name: '新任务', + permissionMode: 'execute', + }); + const offered = [ + { connectionSlug: 'anthropic', model: 'claude-sonnet-4-5-20250929' }, + { connectionSlug: 'openai', model: 'gpt-5' }, + ]; + + assert.equal( + offered.some( + (choice) => + choice.connectionSlug === view.llmConnectionSlug && choice.model === view.model, + ), + false, + ); +}); diff --git a/apps/desktop/src/main/__tests__/stale-sessions.test.ts b/apps/desktop/src/main/__tests__/stale-sessions.test.ts index 4872cc4310..9e5a5e1017 100644 --- a/apps/desktop/src/main/__tests__/stale-sessions.test.ts +++ b/apps/desktop/src/main/__tests__/stale-sessions.test.ts @@ -4,10 +4,10 @@ import { deriveStaleSessionIds } from '../../renderer/stale-sessions.js'; test('derives stale rows from each Session Host readiness projection', () => { const sessions = [ - session('local-ready'), - session('remote-missing'), - session('remote-rebind'), - session('legacy-fake', 'fake'), + { id: 'local-ready' }, + { id: 'remote-missing' }, + { id: 'remote-rebind' }, + { id: 'legacy-fake' }, ]; assert.deepEqual( @@ -25,12 +25,19 @@ test('derives stale rows from each Session Host readiness projection', () => { connectionSlug: 'replacement', model: 'model', }, + // #3211: a retired backend reaches the rail as a projection reason like + // any other. The row is no longer identified by reading its `backend`. + 'legacy-fake': { + kind: 'blocked', + reason: 'fake_backend', + connectionLocked: false, + }, }, })], ['remote-missing', 'legacy-fake'], ); }); -function session(id: string, backend = 'ai-sdk') { - return { id, backend }; -} +test('a row whose readiness has not arrived yet is not called stale', () => { + assert.deepEqual([...deriveStaleSessionIds({ sessions: [{ id: 'unknown' }], sendOutcomes: {} })], []); +}); diff --git a/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts b/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts index 96f2169258..7ba474b507 100644 --- a/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-session-catalog-ipc-main.ts @@ -96,9 +96,6 @@ export function registerRuntimeHostSessionCatalogIpc( pendingCleanup.add(sessionId); }); ipcMain.handle('sessions:create', async (_event, input?: CreateSessionRequestInput) => { - if (input?.backend !== undefined && input.backend !== 'ai-sdk') { - throw new Error('Unsupported Runtime Host Session backend'); - } const request = resolveCreateSessionRequest(input); const workspace = await deps.resolveCreateProject({ ...(input?.cwd === undefined ? {} : { cwd: input.cwd }), diff --git a/apps/desktop/src/main/search/thread-search.ts b/apps/desktop/src/main/search/thread-search.ts index c8b4e39fd5..9c00cc29a0 100644 --- a/apps/desktop/src/main/search/thread-search.ts +++ b/apps/desktop/src/main/search/thread-search.ts @@ -13,8 +13,9 @@ * UserMessage / AssistantMessage / ToolCallMessage / ToolResultMessage. * Excluded: SystemNoteMessage / TokenUsageMessage / TurnStateMessage / * PermissionDecisionMessage. - * - Excludes sessions with `backend === 'fake'` (e2e-fixture fixtures) and - * archived sessions (managed in Settings › 活动 › 已归档任务). + * - Excludes sessions with `backend === 'fake'` (retired local simulation, + * plus the e2e fixtures that still seed it) and archived sessions + * (managed in Settings › 活动 › 已归档任务). * - Snippets are redacted via `@maka/core/redaction.redactSecrets()`. * - `ToolResultMessage.content` is JSON-serialized for scan and capped to * the first `TOOL_RESULT_SCAN_CAP_BYTES` bytes (worst-case bound). @@ -160,8 +161,11 @@ export async function runThreadSearch( const maxResults = limitResult.value; const sessions = collapseSessionRevisions(await deps.listSessions()) - // Exclude fake-backend sessions — e2e-fixture fixtures and - // similar dev-only state should not surface as real chat hits. + // Exclude fake-backend sessions. The rail still shows them (marked + // stale) because they are task records, but their transcripts are + // simulator output — returning fabricated text as a hit on the user's own + // history is worse than returning nothing. Retiring the backend (#3211) + // did not make that content real, so the filter stays. // // Archived tasks are excluded for the same reason the command palette // skips them: archiving a task says it is out of the working set, and a diff --git a/apps/desktop/src/renderer/app-shell.tsx b/apps/desktop/src/renderer/app-shell.tsx index d16786aee4..ca451850ff 100644 --- a/apps/desktop/src/renderer/app-shell.tsx +++ b/apps/desktop/src/renderer/app-shell.tsx @@ -125,6 +125,7 @@ import { useShellAppearance } from './use-shell-appearance'; import { useShellSearch } from './use-shell-search'; import { useSessionGoal } from './use-session-goal'; import { deriveStaleSessionIds } from './stale-sessions'; +import { pendingSessionView } from './pending-session-view'; import { deriveProjectGroups, deriveWorktreeSessionIds } from './session-project-grouping'; import { deriveSessionRail } from './session-rail'; import { useAppShellTurnPresentation } from './app-shell-turn-view-model'; @@ -1270,26 +1271,16 @@ function AppShellContent({ }; }, [railParentSession]); + // Transient placeholder while the real SessionSummary loads, so the composer + // does not flash a value the session never had. const activeSessionForView: SessionSummary | undefined = activeSession ?? (activeId - ? { - id: activeId, + ? pendingSessionView({ + sessionId: activeId, name: shellCopy.newConversation, - isFlagged: false, - isArchived: false, - labels: [], - hasUnread: false, - status: 'active', - backend: 'fake', - llmConnectionSlug: 'default', - connectionLocked: false, - model: 'fake-model', - // Transient placeholder while the real SessionSummary loads -- - // matches the configured default so the composer doesn't flash a - // hardcoded value before the real session data settles. - permissionMode: defaultPermissionMode, - } + permissionMode: defaultPermissionMode, + }) : undefined); const { boundary: activeExecutionBoundary, diff --git a/apps/desktop/src/renderer/model-catalog-choices.ts b/apps/desktop/src/renderer/model-catalog-choices.ts index e2bba7c712..fef518a109 100644 --- a/apps/desktop/src/renderer/model-catalog-choices.ts +++ b/apps/desktop/src/renderer/model-catalog-choices.ts @@ -7,6 +7,7 @@ import { CODEX_SUBSCRIPTION_UNSUPPORTED_CHATGPT_MODELS, PROVIDER_DEFAULTS, connectionEnabledModelIds, + providerDefaultsOf, } from '@maka/core/llm-connections'; import type { LlmConnection, ProviderType } from '@maka/core/llm-connections'; import type { UiLocale } from '@maka/core/ui-locale'; @@ -138,13 +139,10 @@ function dailyReviewModelDisplayLabel( } function isModelConsumerConnection(connection: Pick): boolean { - const defaults = PROVIDER_DEFAULTS[connection.providerType]; // Unknown providerType (legacy seed, or a connection persisted on a branch // that registers a provider this build doesn't know) → not a model consumer. - // Mirrors `isFakeBackend` in connection-readiness.ts; without this guard the - // `.backendKind` read below throws on load for an orphan connection. - if (!connection.enabled || !defaults || defaults.backendKind !== 'ai-sdk') return false; - return true; + // Mirrors `isRealConnection` in connection-readiness.ts. + return connection.enabled && providerDefaultsOf(connection.providerType) !== undefined; } function enabledProviderCounts(connections: readonly LlmConnection[]): Map { diff --git a/apps/desktop/src/renderer/pending-session-view.ts b/apps/desktop/src/renderer/pending-session-view.ts new file mode 100644 index 0000000000..5b10443065 --- /dev/null +++ b/apps/desktop/src/renderer/pending-session-view.ts @@ -0,0 +1,41 @@ +import type { PermissionMode } from '@maka/core/permission'; +import type { SessionSummary } from '@maka/core/session'; + +export interface PendingSessionViewInput { + sessionId: string; + name: string; + permissionMode: PermissionMode; +} + +/** + * The `SessionSummary` the chat view shows between "a session id became active" + * and "its real summary arrived". + * + * The connection and model read empty because they are genuinely unknown: this + * fallback covers every active id without a loaded summary, not just a freshly + * created task, so the session behind it may be an existing one bound to any + * model. An empty pair matches no offered choice, which is how the model + * switcher's current-value and no-op comparisons read "not yet known" — naming + * a plausible model instead would let a switch onto that model be silently + * dropped as a no-op against a session that was never on it. + * + * It used to say `backend: 'fake'` / `model: 'fake-model'`, borrowing a retired + * backend (#3211) to mean "not loaded". The unknown-ness is the same; the + * borrowed name is gone. + */ +export function pendingSessionView(input: PendingSessionViewInput): SessionSummary { + return { + id: input.sessionId, + name: input.name, + isFlagged: false, + isArchived: false, + labels: [], + hasUnread: false, + status: 'active', + backend: 'ai-sdk', + llmConnectionSlug: '', + connectionLocked: false, + model: '', + permissionMode: input.permissionMode, + }; +} diff --git a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx index 1e8bc22210..886e273ecf 100644 --- a/apps/desktop/src/renderer/settings/provider-connection-detail.tsx +++ b/apps/desktop/src/renderer/settings/provider-connection-detail.tsx @@ -61,7 +61,7 @@ export function ConnectionDetail(props: ConnectionDetailProps) { // Unknown providerType (a connection persisted on a branch that registers a // provider this build doesn't know) → render a non-actionable fallback so // opening the orphan connection doesn't crash on `.authKind`/`.baseUrl`. - // Mirrors `isFakeBackend` in @maka/core/connection-readiness.ts. + // Mirrors `isRealConnection` in @maka/core/connection-readiness.ts. if (!defaults) return ; return ; } diff --git a/apps/desktop/src/renderer/stale-sessions.ts b/apps/desktop/src/renderer/stale-sessions.ts index 04a631daef..ac121750d0 100644 --- a/apps/desktop/src/renderer/stale-sessions.ts +++ b/apps/desktop/src/renderer/stale-sessions.ts @@ -4,7 +4,6 @@ export interface StaleSessionsInput { /** Sessions visible in the sidebar (already filtered + grouped). */ sessions: ReadonlyArray<{ id: string; - backend: string; }>; /** Readiness already resolved by each Session's owning Runtime Host. */ sendOutcomes: Readonly>; @@ -13,17 +12,22 @@ export interface StaleSessionsInput { export function deriveStaleSessionIds(input: StaleSessionsInput): Set { const stale = new Set(); for (const session of input.sessions) { - if (isStale(session, input.sendOutcomes[session.id])) { + if (isStale(input.sendOutcomes[session.id])) { stale.add(session.id); } } return stale; } -function isStale( - session: { backend: string }, - outcome: SessionSendProjection | undefined, -): boolean { - if (session.backend === 'fake') return true; - return outcome?.kind === 'blocked' && outcome.reason === 'connection_missing'; +/** + * A row is stale when its owning Runtime Host says the next send cannot go + * anywhere the user can fix from the rail. + * + * `fake_backend` is read from the projection rather than from `session.backend` + * (#3211): the readiness projection is the single authority on whether a task + * is usable, and a retired backend is one of its answers like any other. + */ +function isStale(outcome: SessionSendProjection | undefined): boolean { + if (outcome?.kind !== 'blocked') return false; + return outcome.reason === 'connection_missing' || outcome.reason === 'fake_backend'; } diff --git a/apps/desktop/src/renderer/use-shell-chat-model.ts b/apps/desktop/src/renderer/use-shell-chat-model.ts index 724753700c..c2a65279cb 100644 --- a/apps/desktop/src/renderer/use-shell-chat-model.ts +++ b/apps/desktop/src/renderer/use-shell-chat-model.ts @@ -119,13 +119,20 @@ export function useShellChatModel(options: { catalogDefault: catalogDefaultNewChatModel, choices: chatModelChoices, }); - const activeConnectionLabel = activeSession?.backend === 'fake' + // A task whose backend was retired has no model to name (#3211). That verdict + // comes from the readiness projection, not from reading `activeSession.backend` + // here: the projection is the single authority on whether a task is usable, + // and it already answers `fake_backend` for these rows. + const isRetiredBackend = + options.sessionSendOutcome?.kind === 'blocked' && + options.sessionSendOutcome.reason === 'fake_backend'; + const activeConnectionLabel = isRetiredBackend ? conversationCopy.model.fakeBackendLabel : activeConnection?.name ?? activeSession?.llmConnectionSlug; - const activeModel = activeSession?.backend === 'fake' + const activeModel = isRetiredBackend ? undefined : activeSession?.model || activeConnection?.defaultModel; - const activeModelLabel = activeSession?.backend === 'fake' + const activeModelLabel = isRetiredBackend ? undefined : chatModelChoiceLabel(chatModelChoices, activeSession?.llmConnectionSlug, activeModel); const activeThinkingLevels = useMemo( diff --git a/packages/cli/src/__tests__/runtime-host-run-command.test.ts b/packages/cli/src/__tests__/runtime-host-run-command.test.ts index 99f636cdaf..298e820ad4 100644 --- a/packages/cli/src/__tests__/runtime-host-run-command.test.ts +++ b/packages/cli/src/__tests__/runtime-host-run-command.test.ts @@ -205,7 +205,6 @@ describe('Runtime Host maka run adapter', () => { const fixture = runFixture({ observed, graph: true, graphProjectionRace: true }); const session = await fixture.context.runtime.createSession({ cwd: '/workspace', - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', @@ -229,7 +228,6 @@ describe('Runtime Host maka run adapter', () => { const fixture = runFixture({ observed, graph: true }); const session = await fixture.context.runtime.createSession({ cwd: '/workspace', - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', @@ -253,7 +251,6 @@ describe('Runtime Host maka run adapter', () => { const fixture = runFixture({ observed, graph: true, graphMultiWakeRace: true }); const session = await fixture.context.runtime.createSession({ cwd: '/workspace', - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', @@ -282,7 +279,6 @@ describe('Runtime Host maka run adapter', () => { const context = fixture.context; const session = await context.runtime.createSession({ cwd: '/workspace', - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', @@ -315,7 +311,6 @@ describe('Runtime Host maka run adapter', () => { }); const session = await fixture.context.runtime.createSession({ cwd: '/workspace', - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', @@ -338,7 +333,6 @@ describe('Runtime Host maka run adapter', () => { const fixture = runFixture({ maxSteps: 3 }); const session = await fixture.context.runtime.createSession({ cwd: '/workspace', - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', @@ -390,7 +384,6 @@ describe('Runtime Host maka run adapter', () => { const fixture = runFixture({ graph: true }); const session = await fixture.context.runtime.createSession({ cwd: '/workspace', - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', @@ -412,7 +405,6 @@ describe('Runtime Host maka run adapter', () => { }); const session = await fixture.context.runtime.createSession({ cwd: '/workspace', - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', @@ -444,7 +436,6 @@ describe('Runtime Host maka run adapter', () => { }); const session = await fixture.context.runtime.createSession({ cwd: '/workspace', - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', @@ -471,7 +462,6 @@ describe('Runtime Host maka run adapter', () => { }); const session = await fixture.context.runtime.createSession({ cwd: '/workspace', - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', @@ -499,7 +489,6 @@ describe('Runtime Host maka run adapter', () => { }); const session = await fixture.context.runtime.createSession({ cwd: '/workspace', - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', @@ -545,7 +534,6 @@ describe('Runtime Host maka run adapter', () => { }); const session = await fixture.context.runtime.createSession({ cwd: '/workspace', - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', diff --git a/packages/cli/src/__tests__/runtime-host-session-driver.test.ts b/packages/cli/src/__tests__/runtime-host-session-driver.test.ts index 7dda7d111c..4671af76c0 100644 --- a/packages/cli/src/__tests__/runtime-host-session-driver.test.ts +++ b/packages/cli/src/__tests__/runtime-host-session-driver.test.ts @@ -60,7 +60,6 @@ describe('Runtime Host Maka Session driver', () => { cwd: '/client/workspace', llmConnectionSlug: 'openai-main', model: 'gpt-5', - backend: 'ai-sdk', permissionMode: 'ask', }), /requires an explicit Project/, @@ -92,7 +91,6 @@ describe('Runtime Host Maka Session driver', () => { await driver.createSession({ cwd: '/repo', - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', @@ -174,7 +172,6 @@ describe('Runtime Host Maka Session driver', () => { await driver.createSession({ cwd: '/repo', - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', @@ -304,7 +301,6 @@ describe('Runtime Host Maka Session driver', () => { await driver.createSession({ cwd: candidate.cwd, ...('projectId' in candidate ? { projectId: candidate.projectId } : {}), - backend: 'ai-sdk', llmConnectionSlug: 'openai-main', model: 'gpt-5', permissionMode: 'ask', @@ -364,7 +360,6 @@ describe('Runtime Host Maka Session driver', () => { cwd: '/repo', llmConnectionSlug: 'openai-main', model: 'gpt-5', - backend: 'ai-sdk', permissionMode: 'ask', }); connection.executionBoundary = { kind: 'bypass', revision: 2 }; diff --git a/packages/cli/src/activation-command.ts b/packages/cli/src/activation-command.ts index cfa5f194b7..fbdb10f246 100644 --- a/packages/cli/src/activation-command.ts +++ b/packages/cli/src/activation-command.ts @@ -449,7 +449,6 @@ export async function runMakaActivationCli( session = await context.runtime.createSession({ cwd: roots.workspaceRoot, name: `Cloud activation ${request.activationId}`.slice(0, 80), - backend: 'ai-sdk', llmConnectionSlug: context.target.connection.slug, model: context.target.model, permissionMode: options.permissionMode ?? 'explore', diff --git a/packages/cli/src/run-command-core.ts b/packages/cli/src/run-command-core.ts index a47d2881cb..e46a5c9249 100644 --- a/packages/cli/src/run-command-core.ts +++ b/packages/cli/src/run-command-core.ts @@ -314,7 +314,6 @@ export async function runMakaTextCliCore( : await context.runtime.createSession({ cwd: selection.cwd, name: makaRunSessionName(prompt), - backend: 'ai-sdk', llmConnectionSlug: context.target.connection.slug, model: context.target.model, permissionMode: parsed.options.yolo ? 'bypass' : 'ask', diff --git a/packages/core/src/__tests__/llm-connections.test.ts b/packages/core/src/__tests__/llm-connections.test.ts index e6cf144aed..e6d1f75cd4 100644 --- a/packages/core/src/__tests__/llm-connections.test.ts +++ b/packages/core/src/__tests__/llm-connections.test.ts @@ -12,11 +12,14 @@ import { normalizeConnectionBaseUrl, persistedBaseUrl, providerAuthRequiresSecret, + providerDefaultsOf, providerAuthSupportsApiKey, reconcileConnectionAfterModelFetch, validateConnectionBaseUrl, type ProviderType, } from '../llm-connections.js'; +import { isRealConnection } from '../connection-readiness.js'; +import { buildChatModelChoices } from '../chat-model-choice.js'; test('connection base URLs allow HTTP(S) and reject unsafe or malformed inputs', () => { assert.equal(validateConnectionBaseUrl(undefined), null); @@ -58,7 +61,12 @@ test('base URL normalization preserves clear intent and rejects untrusted runtim test('unknown provider ids fail closed without breaking persisted connections', () => { const unknown = 'branch-only-provider' as ProviderType; - assert.equal(backendKindOf({ providerType: unknown }), 'fake'); + // `backendKindOf` no longer invents a backend for a provider this build + // cannot describe (#3211); the readiness projection is the non-throwing + // answer to "can this connection be used?". + assert.throws(() => backendKindOf({ providerType: unknown }), /Unknown providerType/); + assert.equal(isRealConnection({ providerType: unknown }), false); + assert.equal(providerDefaultsOf(unknown), undefined); assert.equal( effectiveBaseUrl({ providerType: unknown, baseUrl: 'https://example.test/v1' }), 'https://example.test/v1', @@ -194,3 +202,31 @@ test('the alias table is selected by provider and names only renames', () => { assert.notEqual(lookupModelMetadata('anthropic', renamed).lifecycle, 'deprecated'); } }); + +test('provider recognition does not resolve inherited object members', () => { + // `PROVIDER_DEFAULTS` is an object literal, so plain indexing answers truthy + // for `__proto__` / `toString` / `constructor` and they would read as + // registered providers. #3211 made `backendKindOf` throw for unknown types, + // which turns that leak from a wrong-but-closed `'fake'` into an `undefined` + // masquerading as a BackendKind — so recognition owns the own-property check. + for (const inherited of ['__proto__', 'toString', 'constructor', 'valueOf']) { + const providerType = inherited as ProviderType; + assert.equal(providerDefaultsOf(inherited), undefined, inherited); + assert.equal(isRealConnection({ providerType }), false, inherited); + assert.throws(() => backendKindOf({ providerType }), /Unknown providerType/, inherited); + assert.deepEqual( + buildChatModelChoices([ + { + slug: 'inherited', + name: 'inherited', + providerType, + enabled: true, + defaultModel: 'm', + models: [{ id: 'm' }], + } as unknown as Parameters[0][number], + ]), + [], + inherited, + ); + } +}); diff --git a/packages/core/src/__tests__/scheduled-task.test.ts b/packages/core/src/__tests__/scheduled-task.test.ts index e070779b0f..249164a722 100644 --- a/packages/core/src/__tests__/scheduled-task.test.ts +++ b/packages/core/src/__tests__/scheduled-task.test.ts @@ -147,6 +147,36 @@ describe('scheduled-task catalog', () => { assert.deepEqual(result, { ok: false, message: 'Schedule must fire before expiresAt' }); }); + it('refuses to create an Automation on the retired backend', () => { + // #3211: this is create/update input, not a decoder — stored Automations + // are read back with JSON.parse and never reach here. Accepting `'fake'` + // would let a brand new Automation be written that can only fail later at + // activation. + const now = Date.UTC(2026, 0, 5, 8, 0, 0); + const execution = { + cwd: '/tmp/project', + llmConnectionSlug: 'anthropic', + model: 'claude-sonnet-4-5-20250929', + permissionMode: 'ask', + collaborationMode: 'agent', + orchestrationMode: 'default', + }; + const create = (backend: string) => + normalizeCreateScheduledTaskInput( + { + title: 'Nightly run', + intentBody: 'do the thing', + schedule: { kind: 'once', runAt: now + 60_000 }, + effect: { kind: 'agent_run', execution: { ...execution, backend } }, + createdBy: { kind: 'user' }, + }, + now, + ); + + assert.deepEqual(create('fake'), { ok: false, message: 'execution.backend is invalid' }); + assert.equal(create('ai-sdk').ok, true); + }); + it('rejects future recurrence anchors outside the scheduling horizon', () => { const now = Date.UTC(2026, 0, 5, 8, 0, 0); for (const schedule of [ diff --git a/packages/core/src/__tests__/session-send-projection.test.ts b/packages/core/src/__tests__/session-send-projection.test.ts index aad8d13e48..f241a8f855 100644 --- a/packages/core/src/__tests__/session-send-projection.test.ts +++ b/packages/core/src/__tests__/session-send-projection.test.ts @@ -166,8 +166,8 @@ describe('projectSessionSendOutcome — silent rebind walk', () => { const outcome = projectSessionSendOutcome( input({ session: { - backend: 'fake', - llmConnectionSlug: 'fake', + backend: 'ai-sdk', + llmConnectionSlug: 'deleted-connection', model: 'fake-model', connectionLocked: false, }, @@ -180,8 +180,8 @@ describe('projectSessionSendOutcome — silent rebind walk', () => { const outcome = projectSessionSendOutcome( input({ session: { - backend: 'fake', - llmConnectionSlug: 'fake', + backend: 'ai-sdk', + llmConnectionSlug: 'deleted-connection', model: 'fake-model', connectionLocked: false, }, @@ -203,7 +203,11 @@ describe('projectSessionSendOutcome — silent rebind walk', () => { }); }); - it('rebind requires a secret on the candidate connection', () => { + it('a retired backend never rebinds, even unlocked with a ready connection available', () => { + // #3211: activation dispatches off the session header's own `backend`, so + // pointing this session at a healthy connection would still leave `'fake'` + // in the header and still be refused. Promising a rebind nothing performs + // is what pushed the rail and the composer to read `backend` directly. const outcome = projectSessionSendOutcome( input({ session: { @@ -212,11 +216,33 @@ describe('projectSessionSendOutcome — silent rebind walk', () => { model: 'fake-model', connectionLocked: false, }, + }), + ); + assert.deepEqual(outcome, { + kind: 'blocked', + reason: 'fake_backend', + connectionLocked: false, + }); + }); + + it('rebind requires a secret on the candidate connection', () => { + const outcome = projectSessionSendOutcome( + input({ + session: { + backend: 'ai-sdk', + llmConnectionSlug: 'deleted-connection', + model: 'fake-model', + connectionLocked: false, + }, hasSecret: () => false, }), ); // Default exists and is enabled but has no key → send still fails. - assert.deepEqual(outcome, { kind: 'blocked', reason: 'fake_backend', connectionLocked: false }); + assert.deepEqual(outcome, { + kind: 'blocked', + reason: 'connection_missing', + connectionLocked: false, + }); }); it('non-rebindable failure blocks even when another ready connection exists', () => { @@ -236,8 +262,8 @@ describe('projectSessionSendOutcome — silent rebind walk', () => { const outcome = projectSessionSendOutcome( input({ session: { - backend: 'fake', - llmConnectionSlug: 'fake', + backend: 'ai-sdk', + llmConnectionSlug: 'deleted-connection', model: 'fake-model', connectionLocked: false, }, @@ -253,8 +279,8 @@ describe('projectSessionSendOutcome — codex normalization', () => { const outcome = projectSessionSendOutcome( input({ session: { - backend: 'fake', - llmConnectionSlug: 'fake', + backend: 'ai-sdk', + llmConnectionSlug: 'deleted-connection', model: 'fake-model', connectionLocked: false, }, diff --git a/packages/core/src/agent-run.ts b/packages/core/src/agent-run.ts index d793670ba4..1d4459df4a 100644 --- a/packages/core/src/agent-run.ts +++ b/packages/core/src/agent-run.ts @@ -8,7 +8,7 @@ import { type EffectiveOrchestrationSource, type OrchestrationMode, } from './orchestration.js'; -import type { BackendKind } from './session.js'; +import type { PersistedBackendKind } from './session.js'; import { defineObjectShape, hasExactShape, @@ -129,7 +129,7 @@ export interface AgentRunHeader { sessionId: string; turnId: string; status: AgentRunStatus; - backendKind: BackendKind; + backendKind: PersistedBackendKind; llmConnectionSlug: string; modelId: string; cwd: string; @@ -593,7 +593,7 @@ export function decodeAgentRunHeader(value: unknown): AgentRunHeader { typeof value.sessionId === 'string' && typeof value.turnId === 'string' && (AGENT_RUN_STATUSES as readonly unknown[]).includes(status) && - isBackendKind(value.backendKind) && + isPersistedBackendKind(value.backendKind) && typeof value.llmConnectionSlug === 'string' && typeof value.modelId === 'string' && typeof value.cwd === 'string' && @@ -708,7 +708,11 @@ export function decodeAgentRunEvent(value: unknown): AgentRunEvent { return value as unknown as AgentRunEvent; } -function isBackendKind(value: unknown): value is BackendKind { +/** + * Decode guard for a durable run header. `'fake'` stays accepted: runs written + * by builds that shipped FakeBackend must keep decoding (#3211). + */ +function isPersistedBackendKind(value: unknown): value is PersistedBackendKind { return value === 'ai-sdk' || value === 'fake'; } diff --git a/packages/core/src/backend-types.ts b/packages/core/src/backend-types.ts index c42648b50b..d62adb4952 100644 --- a/packages/core/src/backend-types.ts +++ b/packages/core/src/backend-types.ts @@ -19,7 +19,7 @@ import type { import type { InteractionClosureReason } from './interaction.js'; import type { RuntimeEvent } from './runtime-event.js'; import type { SandboxBoundaryResponse, SandboxBoundarySettlement } from './sandbox-boundary.js'; -import type { StoredMessage, BackendKind } from './session.js'; +import type { StoredMessage, PersistedBackendKind } from './session.js'; import type { UserQuestionResponse } from './user-question.js'; import type { ContextBudgetDiagnostic } from './usage-stats/types.js'; import type { EffectiveOrchestration } from './orchestration.js'; @@ -181,7 +181,7 @@ export type BackendSessionEvent = Exclude< >; export interface AgentBackend { - readonly kind: BackendKind; + readonly kind: PersistedBackendKind; readonly sessionId: string; send(input: BackendSendInput): AsyncIterable; compactHistory?(input: BackendCompactHistoryInput): Promise; diff --git a/packages/core/src/chat-model-choice.ts b/packages/core/src/chat-model-choice.ts index aa886b275b..e87fd19160 100644 --- a/packages/core/src/chat-model-choice.ts +++ b/packages/core/src/chat-model-choice.ts @@ -3,8 +3,8 @@ import { buildConnectionModelCatalogEntries } from './model-catalog.js'; import { thinkingVariantsForConnection, type ThinkingLevel } from './model-thinking.js'; import { CODEX_SUBSCRIPTION_UNSUPPORTED_CHATGPT_MODELS, - PROVIDER_DEFAULTS, connectionEnabledModelIds, + providerDefaultsOf, type LlmConnection, type ProviderType, } from './llm-connections.js'; @@ -39,8 +39,8 @@ export function buildChatModelChoices(connections: readonly LlmConnection[]): Ch const choices: ChatModelChoice[] = []; for (const rawConnection of connections) { const connection = normalizeOpenAiCodexConnection(rawConnection); - const provider = PROVIDER_DEFAULTS[connection.providerType]; - if (!connection.enabled || !provider || provider.backendKind !== 'ai-sdk') { + const provider = providerDefaultsOf(connection.providerType); + if (!connection.enabled || !provider) { continue; } const enabledModelIds = new Set(connectionEnabledModelIds(connection)); diff --git a/packages/core/src/connection-readiness.ts b/packages/core/src/connection-readiness.ts index 4e0b4d630f..43de415b7d 100644 --- a/packages/core/src/connection-readiness.ts +++ b/packages/core/src/connection-readiness.ts @@ -9,7 +9,7 @@ * touches the credential store, filesystem, or IPC. * * The single helper here is the only place these criteria live: - * - real backend (not `fake`) + * - the provider is one this build knows * - `enabled === true` * - has usable secret OR provider's `authKind === 'none'` * - effective model exists (caller's `requestedModel` if provided, @@ -27,6 +27,7 @@ import { PROVIDER_DEFAULTS, connectionEnabledModelIds, providerAuthRequiresSecret, + providerDefaultsOf, type LlmConnection, } from './llm-connections.js'; import { isModelExplicitlyUnsupportedForChat } from './model-catalog.js'; @@ -84,7 +85,7 @@ export interface IsConnectionReadyInput { * contract change. * * Order: - * 1. backend is `fake` → `fake_backend` + * 1. `providerType` is not in the registry → `fake_backend` * 2. `enabled === false` → `connection_disabled` * 3. `authKind !== 'none' && !hasSecret` → `missing_api_key` * 4. effective model is empty/missing → `missing_model` @@ -99,7 +100,7 @@ export interface IsConnectionReadyInput { export function isConnectionReady(input: IsConnectionReadyInput): IsConnectionReadyResult { const { connection, hasSecret, requestedModel } = input; - if (isFakeBackend(connection)) { + if (!isKnownProvider(connection)) { return { ready: false, reason: 'fake_backend' }; } if (!connection.enabled) { @@ -167,23 +168,22 @@ export function normalizeOpenAiCodexConnection(connection: LlmConnection): LlmCo } /** - * Whether a connection is backed by a real LLM provider (anything - * whose `backendKind === 'ai-sdk'`), as opposed to the in-process - * `fake` backend or an unrecognized legacy provider type. + * Whether a connection is backed by a real LLM provider. + * + * Since the in-process `fake` backend was retired (#3211) every registered + * provider runs on `ai-sdk`, so this is exactly "is this `providerType` one + * the build knows". An unknown one (legacy seed, future provider not yet in + * PROVIDER_DEFAULTS) is treated as non-real — onboarding then routes the user + * to the add-provider flow which will rebuild a real connection. * * @kenji PR110a review gate: telemetry / lastTestStatus must NOT - * influence this judgment. A `fake` connection that happens to have - * `lastTestStatus: 'verified'` is still fake. An unknown providerType - * (legacy seed, future provider not yet in PROVIDER_DEFAULTS) is also - * treated as non-real — onboarding then routes the user to the - * add-provider flow which will rebuild a real connection. + * influence this judgment. A connection that cannot describe its provider is + * still unusable when it happens to carry `lastTestStatus: 'verified'`. */ export function isRealConnection(connection: Pick): boolean { - return !isFakeBackend(connection); + return isKnownProvider(connection); } -function isFakeBackend(connection: Pick): boolean { - const defaults = PROVIDER_DEFAULTS[connection.providerType]; - if (!defaults) return true; // unknown providerType → treat as non-real - return defaults.backendKind === 'fake'; +function isKnownProvider(connection: Pick): boolean { + return providerDefaultsOf(connection.providerType) !== undefined; } diff --git a/packages/core/src/llm-connections.ts b/packages/core/src/llm-connections.ts index 5f0cf88707..66653b63ca 100644 --- a/packages/core/src/llm-connections.ts +++ b/packages/core/src/llm-connections.ts @@ -349,6 +349,22 @@ export interface ConnectionTestResult { export const PROVIDER_DEFAULTS = PROVIDER_REGISTRY; +/** + * The registry entry for a provider, or `undefined` when this build does not + * register one. + * + * Sole owner of the question "is this `providerType` one we know". Plain + * indexing cannot answer it: `PROVIDER_DEFAULTS` is an object literal, so + * `PROVIDER_DEFAULTS['__proto__']` and `['toString']` resolve to inherited + * members and read as registered providers. Every recognition site goes + * through here rather than repeating the own-property check. + */ +export function providerDefaultsOf(providerType: string): ProviderDefaults | undefined { + return Object.hasOwn(PROVIDER_DEFAULTS, providerType) + ? PROVIDER_DEFAULTS[providerType as ProviderType] + : undefined; +} + export function defaultEnabledModelIdsWhenOmitted( providerType: ProviderType, ): readonly string[] | undefined { @@ -370,11 +386,21 @@ export function providerSupportsModelDiscovery(providerType: ProviderType): bool return discovery !== undefined && discovery.kind !== 'fallback'; } +/** + * The backend that runs a connection. + * + * Throws for an unknown `providerType` (a legacy seed, or a connection + * persisted on a branch that registers a provider this build doesn't know). + * It used to answer `'fake'` there, which was the last live producer of that + * value (#3211); there is no honest backend to name for a provider this build + * cannot describe. Callers that need a non-throwing answer are asking whether + * the connection is usable, not which backend runs it — use `isRealConnection` + * / `isConnectionReady` from `connection-readiness.ts`. + */ export function backendKindOf(c: Pick): BackendKind { - // Unknown providerType (legacy seed, or a connection persisted on a branch - // that registers a provider this build doesn't know) → treat as non-real, - // matching `isFakeBackend` in connection-readiness.ts. - return PROVIDER_DEFAULTS[c.providerType]?.backendKind ?? 'fake'; + const defaults = providerDefaultsOf(c.providerType); + if (!defaults) throw new Error(`Unknown providerType: ${c.providerType}`); + return defaults.backendKind; } export function effectiveBaseUrl(c: Pick): string { diff --git a/packages/core/src/model-catalog.ts b/packages/core/src/model-catalog.ts index bbb789cb0b..060fa2a1ba 100644 --- a/packages/core/src/model-catalog.ts +++ b/packages/core/src/model-catalog.ts @@ -206,7 +206,7 @@ export function buildConnectionModelCatalogEntries( const defaults = PROVIDER_DEFAULTS[connection.providerType]; // Unknown providerType (legacy seed, or a connection persisted on a branch // that registers a provider this build doesn't know) → no catalog entries. - // Mirrors `isFakeBackend` in connection-readiness.ts. + // Mirrors `isRealConnection` in connection-readiness.ts. if (!defaults) return []; const supportsModelDiscovery = providerSupportsModelDiscovery(connection.providerType); const catalogFallbackModels = curatedCatalogFallbackModelsForProvider(connection.providerType); diff --git a/packages/core/src/provider-auth.ts b/packages/core/src/provider-auth.ts index 0c78455f54..ab13f8cce6 100644 --- a/packages/core/src/provider-auth.ts +++ b/packages/core/src/provider-auth.ts @@ -66,7 +66,7 @@ export function deriveProviderAuthContract(input: ProviderAuthContractInput): Pr // Unknown providerType (legacy seed, or a connection persisted on a branch // that registers a provider this build doesn't know) → surface a non-real, // non-actionable contract so the settings row renders instead of crashing. - // Mirrors `isFakeBackend` in connection-readiness.ts. + // Mirrors `isRealConnection` in connection-readiness.ts. if (!defaults) { return { providerType: input.providerType, diff --git a/packages/core/src/runtime-inputs.ts b/packages/core/src/runtime-inputs.ts index 6104b785bf..e7e3281d15 100644 --- a/packages/core/src/runtime-inputs.ts +++ b/packages/core/src/runtime-inputs.ts @@ -4,7 +4,6 @@ import type { MessageContent } from './events.js'; import type { - BackendKind, SessionBlockedReason, SessionStatus, SessionToolProfile, @@ -35,7 +34,14 @@ export interface CreateSessionInput { projectId?: string | null; /** If omitted, runtime auto-derives a placeholder; users may rename later. */ name?: string; - backend: BackendKind; + /** + * No `backend`: a live build has exactly one, so the field carried no choice + * — only the chance of writing the retired `'fake'` into a new row (#3211). + * The store stamps every new header instead. Sessions derived from an older + * one (branch, revision, subagent) no longer inherit its backend; a copy of a + * legacy row is a real session whose connection slug resolves to nothing, + * which is what the readiness projection already says about it. + */ llmConnectionSlug: string; /** Falls back to the connection's defaultModel if omitted. */ model?: string; diff --git a/packages/core/src/runtime-policy/connection-catalog-codec.ts b/packages/core/src/runtime-policy/connection-catalog-codec.ts index 9164336979..306b8e184c 100644 --- a/packages/core/src/runtime-policy/connection-catalog-codec.ts +++ b/packages/core/src/runtime-policy/connection-catalog-codec.ts @@ -1,4 +1,9 @@ -import { PROVIDER_DEFAULTS, validateSlug, type ProviderType } from '../llm-connections.js'; +import { + PROVIDER_DEFAULTS, + providerDefaultsOf, + validateSlug, + type ProviderType, +} from '../llm-connections.js'; import { DECLARABLE_RELAY_THINKING_LEVELS, isThinkingLevel, @@ -37,8 +42,6 @@ import { stringValue, } from './domain-codec.js'; -const PROVIDER_TYPES = new Set(Object.keys(PROVIDER_DEFAULTS)); - export const CONNECTION_CATALOG_MAX_CONNECTIONS = 1_024; export const CONNECTION_CATALOG_MAX_MODELS_PER_CONNECTION = 2_048; export const CONNECTION_CATALOG_MAX_ENABLED_MODEL_IDS = 512; @@ -566,7 +569,7 @@ function decodeConnectionModelIds(value: unknown): string[] { } export function decodeProviderType(value: unknown): ProviderType { - if (typeof value !== 'string' || !PROVIDER_TYPES.has(value)) { + if (typeof value !== 'string' || providerDefaultsOf(value) === undefined) { throw domainError('connection provider type is not registered'); } return value as ProviderType; diff --git a/packages/core/src/scheduled-task.ts b/packages/core/src/scheduled-task.ts index 18c1ccddea..8409b712a3 100644 --- a/packages/core/src/scheduled-task.ts +++ b/packages/core/src/scheduled-task.ts @@ -11,7 +11,7 @@ import { isOrchestrationMode, type OrchestrationMode } from './orchestration.js' import { isThinkingLevel, type ThinkingLevel } from './model-thinking.js'; import { isPermissionMode, type PermissionMode } from './permission.js'; import { isBotDeliveryProvider, type BotProvider } from './bot-chat-settings.js'; -import type { BackendKind } from './session.js'; +import type { PersistedBackendKind } from './session.js'; export const SCHEDULED_TASK_TITLE_MAX_CHARS = 120; export const SCHEDULED_TASK_INTENT_MAX_CHARS = 8_000; @@ -52,7 +52,7 @@ export type ScheduledTaskEffect = export interface ScheduledTaskExecutionTemplate { readonly cwd: string; readonly projectId?: string | null; - readonly backend: BackendKind; + readonly backend: PersistedBackendKind; readonly llmConnectionSlug: string; readonly model: string; readonly thinkingLevel?: ThinkingLevel; @@ -489,7 +489,11 @@ function normalizeExecution( if (!isObject(value)) return fail('agent_run requires execution template'); if (typeof value.cwd !== 'string' || !value.cwd.trim()) return fail('execution.cwd is required'); if (typeof value.backend !== 'string') return fail('execution.backend is required'); - if (value.backend !== 'ai-sdk' && value.backend !== 'fake') { + // Create/update input, not a decoder: stored Automations are read back with + // `JSON.parse` in scheduled-task-store.ts and never pass through here. So the + // retired `'fake'` is refused (#3211) — accepting it would let a brand new + // Automation be written that can only fail later at activation. + if (value.backend !== 'ai-sdk') { return fail('execution.backend is invalid'); } if (typeof value.llmConnectionSlug !== 'string' || !value.llmConnectionSlug.trim()) { diff --git a/packages/core/src/session-send-projection.ts b/packages/core/src/session-send-projection.ts index 3958aa312c..282a53b6df 100644 --- a/packages/core/src/session-send-projection.ts +++ b/packages/core/src/session-send-projection.ts @@ -32,9 +32,9 @@ import type { LlmConnection } from './llm-connections.js'; export interface SessionSendProjectionSession { /** - * Session backend kind. `string` (not `BackendKind`) so legacy on-disk - * values like `'claude'` are surfaced exactly as the JSONL stored them; - * only `'fake'` is special-cased, everything else goes through the + * Session backend kind. `string` (not `PersistedBackendKind`) so legacy + * on-disk values like `'claude'` are surfaced exactly as the JSONL stored + * them; only `'fake'` is special-cased, everything else goes through the * normal connection readiness gate. */ backend: string; @@ -80,7 +80,7 @@ export function projectSessionSendOutcome( } for (const slug of new Set([defaultSlug, ...connections.map((connection) => connection.slug)])) { - if (!slug || slug === 'fake') continue; + if (!slug) continue; const connection = connections.find((entry) => entry.slug === slug); if (!connection) continue; const normalized = normalizeOpenAiCodexConnection(connection); @@ -106,9 +106,12 @@ function sessionOwnConnectionBlockReason( ownConnection: LlmConnection | null, hasSecret: (slug: string) => boolean, ): ChatConfigurationReason | undefined { + // Sessions written by builds that shipped FakeBackend keep `'fake'` on disk + // forever (#3211). They are refused here — and at activation — rather than + // rewritten, because their `llmConnectionSlug` still points at nothing. if (session.backend === 'fake') return 'fake_backend'; const slug = session.llmConnectionSlug; - if (!slug || slug === 'fake') return 'missing_default_connection'; + if (!slug) return 'missing_default_connection'; if (!ownConnection) return 'connection_missing'; const normalized = normalizeOpenAiCodexConnection(ownConnection); const verdict = isConnectionReady({ @@ -124,10 +127,7 @@ function ownConnectionBlockReason( connections: readonly LlmConnection[], hasSecret: (slug: string) => boolean, ): ChatConfigurationReason | undefined { - const own = - session.backend === 'fake' - ? null - : (connections.find((entry) => entry.slug === session.llmConnectionSlug) ?? null); + const own = connections.find((entry) => entry.slug === session.llmConnectionSlug) ?? null; return sessionOwnConnectionBlockReason(session, own, hasSecret); } @@ -137,10 +137,18 @@ function ownConnectionBlockReason( * listed here (e.g. `missing_api_key`, `connection_disabled`) stay blocked * even when unlocked because masking an explicitly configured connection * would make the health/readiness UI misleading. + * + * `fake_backend` is deliberately absent (#3211). Every reason listed here + * names a broken *connection*, which another connection can stand in for. A + * retired backend is not: activation dispatches off the session header's own + * `backend`, so pointing the session at a healthy connection still leaves + * `'fake'` in the header and still gets refused. Claiming a rebind for these + * rows made the projection promise a recovery nothing performs — and the + * surfaces that must answer "is this task usable?" had to bypass the + * projection and read `backend` themselves to work around it. */ function shouldRebindSessionToDefault(reason: string | undefined): boolean { return ( - reason === 'fake_backend' || reason === 'connection_missing' || reason === 'missing_model' || reason === 'empty_model_list' || diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 4ea17a49cd..3711a20632 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -239,7 +239,7 @@ export interface SessionHeader { hasUnread: boolean; // Backend / model config - backend: BackendKind; + backend: PersistedBackendKind; llmConnectionSlug: string; /** True after first UserMessage is flushed. Storage self-heals (§5.2). */ connectionLocked: boolean; @@ -266,7 +266,28 @@ export type SessionHeaderPatch = Partial> & { readonly isArchived?: never; }; -export type BackendKind = 'ai-sdk' | 'fake'; +/** + * The backend a live build may select. + * + * `'fake'` was retired with the in-process FakeBackend (#3211): nothing in a + * shipped build may choose it, so it is not a member here. Values read back + * from durable state use {@link PersistedBackendKind} instead. + */ +export type BackendKind = 'ai-sdk'; + +/** + * The backend value a persisted record may carry. + * + * Sessions, runs and Automations written by builds that still shipped + * FakeBackend hold `'fake'` forever. Decode keeps accepting it so those rows + * stay readable — rewriting them to `'ai-sdk'` would only make an unrunnable + * task look runnable, since their `llmConnectionSlug` still points at nothing. + * Activation refuses them with the product's `fake_backend` reason (see the + * refusal registered in `execution-composition.ts`). + * + * Never write this type: writers take {@link BackendKind}. + */ +export type PersistedBackendKind = BackendKind | 'fake'; export interface SessionSummary { id: string; @@ -315,7 +336,7 @@ export interface SessionSummary { revisionOfTurnId?: string; revisionIndex?: number; revisionState?: 'preparing' | 'committed'; - backend: BackendKind; + backend: PersistedBackendKind; llmConnectionSlug: string; /** * True once the session has user messages — its connection/model is diff --git a/packages/core/src/workspace.ts b/packages/core/src/workspace.ts index 3e49e450d4..a7b9f82cf7 100644 --- a/packages/core/src/workspace.ts +++ b/packages/core/src/workspace.ts @@ -2,7 +2,7 @@ * Workspace config types. */ -import type { BackendKind } from './session.js'; +import type { PersistedBackendKind } from './session.js'; import type { PermissionMode } from './permission.js'; export interface WorkspaceConfig { @@ -13,7 +13,7 @@ export interface WorkspaceConfig { createdAt: number; defaults: { permissionMode: PermissionMode; - backend: BackendKind; + backend: PersistedBackendKind; llmConnectionSlug?: string; model?: string; }; diff --git a/packages/runtime-host/src/__tests__/artifact-two-client-uds.test.ts b/packages/runtime-host/src/__tests__/artifact-two-client-uds.test.ts index 2269a2cca5..135cfb4bc4 100644 --- a/packages/runtime-host/src/__tests__/artifact-two-client-uds.test.ts +++ b/packages/runtime-host/src/__tests__/artifact-two-client-uds.test.ts @@ -273,14 +273,12 @@ async function seedExecutionRoot( stores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await stores.sessionStore.create({ cwd: root, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', }); const otherSession = await stores.sessionStore.create({ cwd: root, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime-host/src/__tests__/deep-research-two-client-uds.test.ts b/packages/runtime-host/src/__tests__/deep-research-two-client-uds.test.ts index 8dbd7b3fb1..a85f50ae84 100644 --- a/packages/runtime-host/src/__tests__/deep-research-two-client-uds.test.ts +++ b/packages/runtime-host/src/__tests__/deep-research-two-client-uds.test.ts @@ -36,7 +36,6 @@ test('two Clients and a restarted production Host share one Deep Research projec const deepResearch = await openInteractiveDeepResearchStoreForWrite(owner.lease); const session = await setupStores.sessionStore.create({ cwd: root, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'explore', diff --git a/packages/runtime-host/src/__tests__/execution-composition.test.ts b/packages/runtime-host/src/__tests__/execution-composition.test.ts index cab65c95da..ad6ca2bb11 100644 --- a/packages/runtime-host/src/__tests__/execution-composition.test.ts +++ b/packages/runtime-host/src/__tests__/execution-composition.test.ts @@ -4,6 +4,7 @@ import { createRequire } from 'node:module'; import { mkdir, mkdtemp, rm, stat, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { DatabaseSync } from 'node:sqlite'; import { test } from 'node:test'; import type { AgentGraphIntentClaim, @@ -81,7 +82,6 @@ test('production composition closes long-term memory after a later startup failu const stores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await stores.sessionStore.create({ cwd: root, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -121,14 +121,12 @@ test('production recovery preserves legacy Automation history and closes an orph const stores = await openInteractiveExecutionStoresForWrite(owner.lease); const historical = await stores.sessionStore.create({ cwd: root, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', }); const pending = await stores.sessionStore.create({ cwd: root, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -349,7 +347,6 @@ test('production composition commits automatic titles through Host-owned Session try { const session = await manager.createSession({ cwd: root, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -381,20 +378,16 @@ test('production composition commits automatic titles through Host-owned Session test('a legacy fake-backend session is refused with the product reason, not a registry error', async () => { await withCompositionRoot(async ({ root, owner }) => { - const { composition, manager } = await createCapturedExecutionComposition(owner); + // Written by an older build: no creation path here can produce `fake`, so + // the row is seeded under the writer — which is the only way it was ever + // produced — and then read back by a Host that starts up against it, since + // activation dispatches straight off the durable header. + const legacyId = await seedLegacyFakeBackendSession(root, owner); + const { composition } = await createCapturedExecutionComposition(owner); try { - // Written by an older build: this one never produces `fake`, but the - // durable header survives and activation dispatches straight off it. - const legacy = await manager.createSession({ - cwd: root, - backend: 'fake', - llmConnectionSlug: 'fake', - model: 'fake-model', - permissionMode: 'ask', - }); const failure = await composition.handlers['turn.start']( { - sessionId: legacy.id, + sessionId: legacyId, turnId: 'turn-legacy-fake', content: { text: 'resume a retired local simulation' }, }, @@ -423,7 +416,6 @@ test('production composition orphans ownerless ShellRuns before serving Resource const stores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await stores.sessionStore.create({ cwd: root, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -471,7 +463,6 @@ test('production Skill catalog resolves a Graph child durable tool surface', asy const stores = await openInteractiveExecutionStoresForWrite(owner.lease); const parent = await stores.sessionStore.create({ cwd: root, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -639,7 +630,6 @@ test('Skill capability previews omit unavailable Tavily search surfaces', async const stores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await stores.sessionStore.create({ cwd: root, - backend: 'ai-sdk', llmConnectionSlug: connection.slug, model: 'fake-model', permissionMode: 'bypass', @@ -696,7 +686,6 @@ test('production composition validates graph stop before aborting a claimed chil const claims = createAgentGraphControlStore(root); const parent = await stores.sessionStore.create({ cwd: root, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -900,6 +889,43 @@ function shellRunRecord( }; } +/** + * Writes one session whose durable header says `backend: 'fake'`. + * + * Nothing in this build can write that value, so the row goes in underneath the + * session writer: create a normal row through the real store, then rewrite the + * persisted backend the way an older build left it on disk. The database + * filename is `OPERATIONAL_STATE_DATABASE_NAME` in `@maka/storage`, which the + * package does not export. + */ +async function seedLegacyFakeBackendSession( + root: string, + owner: InteractiveRootOwner, +): Promise { + const stores = await openInteractiveExecutionStoresForWrite(owner.lease); + const { id: sessionId } = await stores.sessionStore.create({ + cwd: root, + llmConnectionSlug: 'fake', + model: 'fake-model', + permissionMode: 'ask', + }); + + const legacy = new DatabaseSync(join(root, 'runtime.sqlite')); + try { + const row = legacy + .prepare(`SELECT payload_json FROM session_metadata WHERE session_id = ?`) + .get(sessionId) as { payload_json: string }; + const payload = JSON.parse(row.payload_json) as Record; + payload.backend = 'fake'; + legacy + .prepare(`UPDATE session_metadata SET payload_json = ?, backend = ? WHERE session_id = ?`) + .run(JSON.stringify(payload), 'fake', sessionId); + } finally { + legacy.close(); + } + return sessionId; +} + async function createCapturedExecutionComposition(owner: InteractiveRootOwner): Promise<{ composition: Awaited>; manager: SessionManager; @@ -939,7 +965,6 @@ async function createClaimedGraphChild(input: { const child = await input.stores.sessionStore.createSubagent({ cwd: input.root, name: `Graph operator ${input.suffix}`, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'explore', diff --git a/packages/runtime-host/src/__tests__/execution-inspect-uds.test.ts b/packages/runtime-host/src/__tests__/execution-inspect-uds.test.ts index 633dcbad83..b9af046ec1 100644 --- a/packages/runtime-host/src/__tests__/execution-inspect-uds.test.ts +++ b/packages/runtime-host/src/__tests__/execution-inspect-uds.test.ts @@ -31,7 +31,6 @@ test('a live Host serves Interactive inspection over its real endpoint while ret const session = await stores.sessionStore.create({ cwd: root, name: 'Live inspection', - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts index 4507920821..2f6695fcfa 100644 --- a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts +++ b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts @@ -1059,7 +1059,6 @@ test('production Host executes a canonical ai-sdk Session against a real provide const execution = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await execution.sessionStore.create({ cwd: root, - backend: 'ai-sdk', llmConnectionSlug: 'hosted-real-provider', model: MODEL_ID, permissionMode: 'ask', @@ -1375,7 +1374,6 @@ test('production Host executes and durably supervises an Agent Graph over a real const execution = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await execution.sessionStore.create({ cwd: project, - backend: 'ai-sdk', llmConnectionSlug: 'hosted-graph-provider', model: MODEL_ID, permissionMode: 'bypass', @@ -1576,7 +1574,6 @@ test('production Host executes a durable runnable child with an exact tool ceili const execution = await openInteractiveExecutionStoresForWrite(owner.lease); const parent = await execution.sessionStore.create({ cwd: project, - backend: 'ai-sdk', llmConnectionSlug: 'hosted-child-provider', model: MODEL_ID, permissionMode: 'bypass', @@ -1774,7 +1771,6 @@ test('production Host publishes and retires an implementation child patch', asyn const execution = await openInteractiveExecutionStoresForWrite(owner.lease); const parent = await execution.sessionStore.create({ cwd: project, - backend: 'ai-sdk', llmConnectionSlug: 'hosted-child-provider', model: MODEL_ID, permissionMode: 'bypass', @@ -2018,7 +2014,6 @@ test('Host auxiliary calls preserve resolved DeepSeek reasoning settings', async await publishConnectionModel(policy, connection.connectionId, 'deepseek-v4-flash'); const session = await execution.sessionStore.create({ cwd: capability.canonicalPath, - backend: 'ai-sdk', llmConnectionSlug: 'deepseek-auxiliary', model: 'deepseek-v4-flash', thinkingLevel: 'high', @@ -2097,7 +2092,6 @@ test('Host auxiliary models meter provider usage and abort physical requests', { await publishConnectionModel(policy, connection.connectionId, MODEL_ID); const session = await execution.sessionStore.create({ cwd: capability.canonicalPath, - backend: 'ai-sdk', llmConnectionSlug: 'goal-evaluator-provider', model: MODEL_ID, permissionMode: 'ask', diff --git a/packages/runtime-host/src/__tests__/external-session-coordinator.test.ts b/packages/runtime-host/src/__tests__/external-session-coordinator.test.ts index 1c0d8d2ba8..7848588f26 100644 --- a/packages/runtime-host/src/__tests__/external-session-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/external-session-coordinator.test.ts @@ -240,7 +240,6 @@ test('imports through the generic importer and treats repeats as independent cop fixture.creates.map(({ input, messages, externalOrigin }) => ({ cwd: input.cwd, name: input.name, - backend: input.backend, messageTypes: messages.map(({ type }) => type), externalOrigin, })), @@ -248,14 +247,12 @@ test('imports through the generic importer and treats repeats as independent cop { cwd: '/external', name: 'Source 0', - backend: 'ai-sdk', messageTypes: ['user'], externalOrigin: { adapterId: 'codex', sourceSessionId: 'source-0' }, }, { cwd: '/external', name: 'Source 0', - backend: 'ai-sdk', messageTypes: ['user'], externalOrigin: { adapterId: 'codex', sourceSessionId: 'source-0' }, }, @@ -567,7 +564,6 @@ function coordinatorFixture( defaultCreate( { cwd: '/external', - backend: 'ai-sdk', llmConnectionSlug: 'default', model: 'gpt-5', permissionMode: 'ask', diff --git a/packages/runtime-host/src/__tests__/fixtures/execution-host-suite.ts b/packages/runtime-host/src/__tests__/fixtures/execution-host-suite.ts index c22b242dcb..ca60aaca76 100644 --- a/packages/runtime-host/src/__tests__/fixtures/execution-host-suite.ts +++ b/packages/runtime-host/src/__tests__/fixtures/execution-host-suite.ts @@ -125,7 +125,6 @@ export class ExecutionFixture { stores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await stores.sessionStore.create({ cwd: this.root, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -475,7 +474,6 @@ export class ExecutionFixture { const child = await stores.sessionStore.createSubagent({ cwd: this.root, name: `${agentName} ${kind}`, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'explore', @@ -1025,7 +1023,6 @@ export async function withExecutionRoot( stores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await stores.sessionStore.create({ cwd: root, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime-host/src/__tests__/goal-coordinator.test.ts b/packages/runtime-host/src/__tests__/goal-coordinator.test.ts index 648d4d6337..09bf58ef89 100644 --- a/packages/runtime-host/src/__tests__/goal-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/goal-coordinator.test.ts @@ -27,7 +27,6 @@ test('one Host Goal is shared across clients with CAS control and crash-clear re const goalStore = await openInteractiveGoalAuthorityForWrite(owner.lease); const session = await stores.sessionStore.create({ cwd: capability.canonicalPath, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -225,7 +224,6 @@ test('session retirement forgets a terminal Goal without recreating deleted auth try { const session = await stores.sessionStore.create({ cwd: capability.canonicalPath, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -308,7 +306,6 @@ test('restart settles the durable current Goal execution through Hosted Executio try { const session = await stores.sessionStore.create({ cwd: capability.canonicalPath, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -425,7 +422,6 @@ test('restart replaces a stale current execution with the current durable Goal i try { const session = await stores.sessionStore.create({ cwd: capability.canonicalPath, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime-host/src/__tests__/goal-root-authority.test.ts b/packages/runtime-host/src/__tests__/goal-root-authority.test.ts index 8ca97aafba..3f52a6f918 100644 --- a/packages/runtime-host/src/__tests__/goal-root-authority.test.ts +++ b/packages/runtime-host/src/__tests__/goal-root-authority.test.ts @@ -521,7 +521,6 @@ async function createFixture(options: { recoverAdmissions?: boolean } = {}): Pro const goalStore = await openInteractiveGoalAuthorityForWrite(owner.lease); const session = await stores.sessionStore.create({ cwd: capability.canonicalPath, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -599,7 +598,7 @@ async function createFixture(options: { recoverAdmissions?: boolean } = {}): Pro onSandboxBoundarySettled: async () => {}, }); const backends = new BackendRegistry(); - backends.register('fake', (context) => new FakeBackend(context)); + backends.register('ai-sdk', (context) => new FakeBackend(context)); const authority: RuntimeHostedRootAuthority = { bindRun: (identity) => messages.bindRun(identity), executeRoot: (input) => diff --git a/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts b/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts index 5169d17863..d30ba09b86 100644 --- a/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/interaction-coordinator.test.ts @@ -105,7 +105,6 @@ describe('HostInteractionCoordinator', () => { await mkdir(workspace); const session = await stores.sessionStore.create({ cwd: workspace, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -237,7 +236,6 @@ describe('HostInteractionCoordinator', () => { await mkdir(workspace); const session = await stores.sessionStore.create({ cwd: workspace, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -309,7 +307,6 @@ describe('HostInteractionCoordinator', () => { await mkdir(workspace); const session = await stores.sessionStore.create({ cwd: workspace, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -485,7 +482,6 @@ describe('HostInteractionCoordinator', () => { await mkdir(workspace); const session = await stores.sessionStore.create({ cwd: workspace, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime-host/src/__tests__/plan-two-client-uds.test.ts b/packages/runtime-host/src/__tests__/plan-two-client-uds.test.ts index 00e8f0930b..eb28f54f2f 100644 --- a/packages/runtime-host/src/__tests__/plan-two-client-uds.test.ts +++ b/packages/runtime-host/src/__tests__/plan-two-client-uds.test.ts @@ -37,7 +37,6 @@ test('two Clients and a restarted production Host share one retry-safe Plan auth const planStore = await openInteractivePlanStoreForWrite(owner.lease); const session = await setupStores.sessionStore.create({ cwd: root, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'explore', diff --git a/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts b/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts index 739f60973a..91c7079371 100644 --- a/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/root-turn-coordinator.test.ts @@ -100,7 +100,8 @@ test('prepares a fresh Agent Graph epoch before durable external Turn admission' let fixture!: FailureFixture; let cutovers = 0; fixture = await createFailureFixture({ - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), agentGraphEpochs: { currentGraphId: async (rootSessionId) => agentGraphIdForRootSession(rootSessionId), beginNextGraphEpoch: async (rootSessionId) => { @@ -137,7 +138,8 @@ test('prepares a fresh Agent Graph epoch before durable external Turn admission' test('does not advance a finished graph for an ordinary default Turn', async () => { let cutovers = 0; const fixture = await createFailureFixture({ - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), agentGraphEpochs: { currentGraphId: async (rootSessionId) => agentGraphIdForRootSession(rootSessionId), beginNextGraphEpoch: async (rootSessionId) => { @@ -168,7 +170,8 @@ test('does not advance a finished graph for an ordinary default Turn', async () test('startup recovery replays one admitted safe-boundary continuation without a UserMessage', async () => { const workspaceIdentity = 'workspace-safe-boundary-recovery'; const fixture = await createFailureFixture({ - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), continuationSafety: { workspaceIdentity, availableToolNames: [] }, }); let observer: ReturnType | undefined; @@ -254,7 +257,8 @@ test('a failed exact Capability retry does not poison the parked continuation bi } }, }, - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), }); const seedConnection = capabilities.attachConnection( clientCapabilityConnectionIdentity('provider-seed'), @@ -395,7 +399,8 @@ test('resume query preserves Session-before-activation lock ordering', async () } }, }, - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), }); const connection = capabilities.attachConnection( clientCapabilityConnectionIdentity('provider-lock-order'), @@ -455,7 +460,8 @@ test('resume query preserves Session-before-activation lock ordering', async () test('turn.start durably applies one exact per-Turn orchestration override', async () => { const fixture = await createFailureFixture({ - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), }); const input = { sessionId: fixture.sessionId, @@ -518,7 +524,8 @@ test('turn.start resolves explicit Skills once before durable admission and repl { send: async () => {} }, ); const fixture = await createFailureFixture({ - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), clientCapabilities: capabilities, prepareSkillInvocation: async ({ sessionId }): Promise => { preparationCount += 1; @@ -635,7 +642,8 @@ test('turn.start durably replays an all-failed invocation without creating a Tur ], }; const fixture = await createFailureFixture({ - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), prepareSkillInvocation: async (): Promise => { preparationCount += 1; return { disposition: 'blocked', skillInvocation }; @@ -669,7 +677,8 @@ test('turn.start durably replays an all-failed invocation without creating a Tur test('idle turn.message.submit applies hosted Skill preparation before durable admission', async () => { let preparationCount = 0; const fixture = await createFailureFixture({ - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), prepareSkillInvocation: async (): Promise => { preparationCount += 1; return { @@ -734,7 +743,8 @@ test('turn.start rejects oversized preparation before admission and preserves no let preparationCount = 0; let preparation: 'blocked' | 'oversized_content' | 'oversized_feedback' = 'blocked'; const fixture = await createFailureFixture({ - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), prepareSkillInvocation: async () => { preparationCount += 1; if (preparation === 'blocked') { @@ -859,7 +869,8 @@ test('turn.start rejects oversized preparation before admission and preserves no test('turn.start admits only canonical live Session Artifact attachments', async () => { const fixture = await createFailureFixture({ - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), withArtifacts: true, }); try { @@ -989,13 +1000,13 @@ test('safe-boundary continuation safety identity uses the exact canonical tool c test('linked child Sessions reject public safe-boundary continuation', async () => { let recoveryCoordinator: RootTurnCoordinator | undefined; const fixture = await createFailureFixture({ - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), }); try { const parent = await fixture.stores.sessionStore.readHeaderSnapshot(fixture.sessionId); const { header: child } = await fixture.stores.sessionStore.createSubagent({ cwd: parent.cwd, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'execute', @@ -1121,7 +1132,7 @@ test('worktree child Sessions reject roots outside managed child execution', asy let recoveryCoordinator: RootTurnCoordinator | undefined; const fixture = await createFailureFixture({ registerBackend: (backends) => - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new LinkedChildAuthorityBackend(context.sessionId); return backend; }), @@ -1139,7 +1150,6 @@ test('worktree child Sessions reject roots outside managed child execution', asy try { const { header: child } = await fixture.stores.sessionStore.createSubagent({ cwd: binding.worktreePath, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'execute', @@ -1304,7 +1314,7 @@ test('Agent Graph supervisor stop owns only graph-capable root Turns', async () let backend: LinkedChildAuthorityBackend | undefined; const fixture = await createFailureFixture({ registerBackend: (backends) => - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new LinkedChildAuthorityBackend(context.sessionId); return backend; }), @@ -1367,7 +1377,7 @@ test('Agent Graph supervisor stop rejects a stale graph identity inside session let currentGraphId = 'graph-before-rollover'; const fixture = await createFailureFixture({ registerBackend: (backends) => - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new LinkedChildAuthorityBackend(context.sessionId); return backend; }), @@ -1419,7 +1429,7 @@ test('Agent Graph supervisor stop owns a graph safe-boundary continuation', asyn const fixture = await createFailureFixture({ continuationSafety: { workspaceIdentity, availableToolNames: [] }, registerBackend: (backends) => - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new BlockingRootBackend(context.sessionId); return backend; }), @@ -1457,7 +1467,7 @@ test('Agent Graph supervisor wake waits for root idle and binds one durable exec let backend: LinkedChildAuthorityBackend | undefined; const fixture = await createFailureFixture({ registerBackend: (backends) => - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new LinkedChildAuthorityBackend(context.sessionId); return backend; }), @@ -1551,7 +1561,7 @@ test('Agent Graph supervisor wake waits for root idle and binds one durable exec test('Agent Graph supervisor wake preserves structured context-overflow outcomes', async () => { const fixture = await createFailureFixture({ registerBackend: (backends) => - backends.register('fake', (context) => new ContextFailureBackend(context.sessionId)), + backends.register('ai-sdk', (context) => new ContextFailureBackend(context.sessionId)), }); const graphId = agentGraphIdForRootSession(fixture.sessionId); try { @@ -1595,7 +1605,7 @@ test('Agent Graph context recovery fences competing root turns while compaction let backend: BlockingContextRecoveryBackend | undefined; const fixture = await createFailureFixture({ registerBackend: (backends) => - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new BlockingContextRecoveryBackend(context.sessionId); return backend; }), @@ -1654,7 +1664,7 @@ test('manual context compact uses durable root query, stop, and exact retry auth let backend: BlockingContextRecoveryBackend | undefined; const fixture = await createFailureFixture({ registerBackend: (backends) => - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new BlockingContextRecoveryBackend(context.sessionId); return backend; }), @@ -1727,7 +1737,7 @@ test('startup recovery replays an admitted context compact with its exact Run id let backend: BlockingContextRecoveryBackend | undefined; const fixture = await createFailureFixture({ registerBackend: (backends) => - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new BlockingContextRecoveryBackend(context.sessionId); return backend; }), @@ -1785,7 +1795,7 @@ test('Agent Graph context recovery abort stops compaction and releases Host clos let backend: BlockingContextRecoveryBackend | undefined; const fixture = await createFailureFixture({ registerBackend: (backends) => - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new BlockingContextRecoveryBackend(context.sessionId); return backend; }), @@ -1823,7 +1833,7 @@ test('Agent Graph context recovery waits for a confirmed follow-up root', async let backend: GraphFollowupRecoveryBackend | undefined; const fixture = await createFailureFixture({ registerBackend: (backends) => - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new GraphFollowupRecoveryBackend(context.sessionId); return backend; }), @@ -1894,7 +1904,8 @@ test('Agent Graph context recovery waits for a confirmed follow-up root', async test('Agent Graph supervisor wake revalidates freshness before durable root admission', async () => { const fixture = await createFailureFixture({ - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), }); const graphId = agentGraphIdForRootSession(fixture.sessionId); const turnId = 'turn-stale-graph-supervisor-wake'; @@ -1937,7 +1948,8 @@ test('Agent Graph supervisor wake revalidates freshness before durable root admi test('Agent Graph supervisor recovery closes a durable admission that has no Run', async () => { const fixture = await createFailureFixture({ - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), }); const graphId = agentGraphIdForRootSession(fixture.sessionId); const wakeId = `${graphId}:snapshot-recovery`; @@ -2007,7 +2019,8 @@ test('Agent Graph supervisor recovery closes a durable admission that has no Run test('hosted root target unavailability is retryable without poisoning the Host', async () => { const fixture = await createFailureFixture({ - registerBackend: (backends) => backends.register('fake', (context) => new FakeBackend(context)), + registerBackend: (backends) => + backends.register('ai-sdk', (context) => new FakeBackend(context)), }); try { await assert.rejects( @@ -2047,7 +2060,6 @@ test('hosted linked child roots share admission, message, terminal, and stop aut const stores = await openInteractiveExecutionStoresForWrite(owner.lease); const parent = await stores.sessionStore.create({ cwd: capability.canonicalPath, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -2147,7 +2159,7 @@ test('hosted linked child roots share admission, message, terminal, and stop aut }; const backends = new BackendRegistry(); const linkedBackends = new Map(); - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { if (!context.header.subagentRuntime) { return new QuestionWaitingBackend(context.sessionId); } @@ -2653,7 +2665,7 @@ test('pre-bind startup failure fail-stops without orphaning an admitted queued M const releaseBackendFactory = deferred(); const fixture = await createFailureFixture({ registerBackend: (backends) => { - backends.register('fake', async () => { + backends.register('ai-sdk', async () => { backendFactoryEntered.resolve(); await releaseBackendFactory.promise; throw new Error('injected backend startup failure'); @@ -2751,7 +2763,7 @@ test('successor admission failure retains the terminal transition and its confir let backend: LinkedChildAuthorityBackend | undefined; const fixture = await createFailureFixture({ registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new LinkedChildAuthorityBackend(context.sessionId); return backend; }); @@ -2878,7 +2890,7 @@ test('shutdown contains a successor backend start rejected by Interaction drain' store.listRootTurnAdmissionsForRecovery(sessionId), }), registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new LinkedChildAuthorityBackend(context.sessionId); return backend; }); @@ -2968,7 +2980,7 @@ test('Client Capability ambiguity fails before durable root admission', async () const fixture = await createFailureFixture({ clientCapabilities, registerBackend: (backends) => { - backends.register('fake', (context) => new FakeBackend(context)); + backends.register('ai-sdk', (context) => new FakeBackend(context)); }, }); const first = clientCapabilities.attachConnection( @@ -3048,7 +3060,7 @@ test('an exact active retry preserves the Client Capability admission binding', const fixture = await createFailureFixture({ clientCapabilities, registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new LinkedChildAuthorityBackend(context.sessionId); return backend; }); @@ -3143,7 +3155,7 @@ test('mixed-Client queued follow-ups preserve each submitting connection through const fixture = await createFailureFixture({ clientCapabilities, registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new LinkedChildAuthorityBackend(context.sessionId); return backend; }); @@ -3288,7 +3300,7 @@ async function assertFollowupCapabilityRebinding(affinity: 'call' | 'turn'): Pro const fixture = await createFailureFixture({ clientCapabilities, registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new LinkedChildAuthorityBackend(context.sessionId); return backend; }); @@ -3479,7 +3491,7 @@ test('an exact terminal retry does not require a live Client Capability binding' const fixture = await createFailureFixture({ clientCapabilities, registerBackend: (backends) => { - backends.register('fake', (context) => new FakeBackend(context)); + backends.register('ai-sdk', (context) => new FakeBackend(context)); }, }); const provider = clientCapabilities.attachConnection( @@ -3571,7 +3583,7 @@ test('turn.start returns a published fast terminal before backend iterator clean let backend: TerminalThenCleanupBackend | undefined; const fixture = await createFailureFixture({ registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new TerminalThenCleanupBackend(context.sessionId); return backend; }); @@ -3629,7 +3641,7 @@ test('public turn.stop rejects an admission queued behind its exact-Run closure const fixture = await createFailureFixture({ withInteractions: true, registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new QueuedAdmissionBackend(context.sessionId); return backend; }); @@ -3704,7 +3716,7 @@ test('public turn.interrupt contains a question admission rejected by its own st const fixture = await createFailureFixture({ withInteractions: true, registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new StopReleasedAdmissionBackend(context.sessionId); return backend; }); @@ -3759,7 +3771,7 @@ test('public turn.interrupt releases the Session lane while a queried Run is sti const fixture = await createFailureFixture({ withInteractions: true, registerBackend: (backends) => { - backends.register('fake', async (context) => { + backends.register('ai-sdk', async (context) => { backendFactoryEntered.resolve(); return await new Promise((_resolve, reject) => { const abort = () => { @@ -3849,7 +3861,7 @@ test('Runtime stop lets a running admission publish before its exact-Run closure await releasePreflight.promise; }, registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new RunningAdmissionBackend(context.sessionId); return backend; }); @@ -3904,7 +3916,7 @@ test('post-start backend failure closes its owner without draining an unrelated const fixture = await createFailureFixture({ withInteractions: true, registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { if (context.sessionId !== failingSessionId) { unrelatedBackend = new LinkedChildAuthorityBackend(context.sessionId); return unrelatedBackend; @@ -3920,7 +3932,6 @@ test('post-start backend failure closes its owner without draining an unrelated try { const unrelatedSession = await fixture.stores.sessionStore.create({ cwd: '/tmp/unrelated-active-root', - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -4012,7 +4023,7 @@ test('claimed graph backend failure is contained after its failed terminal trans const fixture = await createFailureFixture({ withInteractions: true, registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new AdmissionThenFailureBackend(context.sessionId); backendReady.resolve(backend); return backend; @@ -4070,7 +4081,7 @@ test('failed claimed graph Run identity mismatch drains instead of being contain const fixture = await createFailureFixture({ withInteractions: true, registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new AdmissionThenFailureBackend(context.sessionId); return backend; }); @@ -4119,7 +4130,7 @@ test('post-start backend AggregateError is contained after its failed terminal t const fixture = await createFailureFixture({ withInteractions: true, registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new AdmissionThenFailureBackend(context.sessionId, aggregateFailure); return backend; }); @@ -4192,7 +4203,7 @@ test('post-start message owner cleanup failure drains after its failed terminal const cleanupFailure = new Error('message owner release failed'); const fixture = await createFailureFixture({ registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new LinkedChildAuthorityBackend(context.sessionId); return backend; }); @@ -4257,7 +4268,7 @@ test('public turn.stop wins the Session lane before a wire answer for the same R const fixture = await createFailureFixture({ withInteractions: true, registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new PendingQuestionBackend(context.sessionId); return backend; }); @@ -4337,7 +4348,7 @@ test('public turn.stop takes over an earlier closure claim queued behind its lea const fixture = await createFailureFixture({ withInteractions: true, registerBackend: (backends) => { - backends.register('fake', (context) => { + backends.register('ai-sdk', (context) => { backend = new TakeoverClosureBackend(context.sessionId); return backend; }); @@ -4656,7 +4667,6 @@ async function createFailureFixture(options: { await artifacts?.recover(); const session = await stores.sessionStore.create({ cwd: capability.canonicalPath, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -4957,7 +4967,7 @@ async function waitUntil( } class LinkedChildAuthorityBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly externalHoldStarted = deferred(); readonly questionStarted = deferred(); sendCount = 0; @@ -5052,7 +5062,7 @@ class LinkedChildAuthorityBackend implements AgentBackend { } class BlockingRootBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly started = deferred(); readonly #released = deferred(); stopCount = 0; @@ -5088,7 +5098,7 @@ class BlockingRootBackend implements AgentBackend { } class ContextFailureBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; constructor(readonly sessionId: string) {} @@ -5128,7 +5138,7 @@ class ContextFailureBackend implements AgentBackend { } class BlockingContextRecoveryBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly compactStarted = deferred(); readonly #compactReleased = deferred(); compactInput: BackendCompactHistoryInput | undefined; @@ -5168,7 +5178,7 @@ class BlockingContextRecoveryBackend implements AgentBackend { } class GraphFollowupRecoveryBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly graphTurnStarted = deferred(); readonly followupStarted = deferred(); readonly compactStarted = deferred(); @@ -5246,7 +5256,7 @@ class GraphFollowupRecoveryBackend implements AgentBackend { } class QueuedAdmissionBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly readyForAdmission = deferred(); readonly admissionQueued = deferred(); readonly admissionFailure = deferred(); @@ -5328,7 +5338,7 @@ class QueuedAdmissionBackend implements AgentBackend { } class StopReleasedAdmissionBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly ready = deferred(); private readonly stopped = deferred(); @@ -5383,7 +5393,7 @@ class StopReleasedAdmissionBackend implements AgentBackend { } class RunningAdmissionBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly admitted = deferred(); readonly stopRequested = deferred(); readonly closureReasons: string[] = []; @@ -5456,7 +5466,7 @@ class RunningAdmissionBackend implements AgentBackend { } class AdmissionThenFailureBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly admitted = deferred(); readonly closureReasons: string[] = []; private readonly fail = deferred(); @@ -5512,7 +5522,7 @@ class AdmissionThenFailureBackend implements AgentBackend { } class TerminalThenCleanupBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; cleanupReleased = false; private readonly cleanup = deferred(); @@ -5544,7 +5554,7 @@ class TerminalThenCleanupBackend implements AgentBackend { } class PendingQuestionBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly pendingRequest = deferred(); readonly closureReasons: string[] = []; answerApplications = 0; @@ -5615,7 +5625,7 @@ class PendingQuestionBackend implements AgentBackend { } class TakeoverClosureBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sendStarted = deferred(); readonly stopStarted = deferred(); private readonly sendReleased = deferred(); @@ -5665,7 +5675,7 @@ class TakeoverClosureBackend implements AgentBackend { } class QuestionWaitingBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; private stopped = false; private resolveAnswer: ((answers: readonly (string | null)[] | null) => void) | undefined; private releaseAfterAnswer: (() => void) | undefined; diff --git a/packages/runtime-host/src/__tests__/runtime-policy-coordinator.test.ts b/packages/runtime-host/src/__tests__/runtime-policy-coordinator.test.ts index 5f01fe125a..3f67ec5cba 100644 --- a/packages/runtime-host/src/__tests__/runtime-policy-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/runtime-policy-coordinator.test.ts @@ -105,7 +105,6 @@ test('production composition shares one gate across mutation and backend activat const setupStores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await setupStores.sessionStore.create({ cwd: root, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -209,7 +208,6 @@ test('production mutation releases the gate before active-turn backend disposal const setupStores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await setupStores.sessionStore.create({ cwd: root, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -310,7 +308,6 @@ test('production policy mutation drains and poisons activation when cached backe const setupStores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await setupStores.sessionStore.create({ cwd: root, - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime-host/src/__tests__/scheduled-task-protocol.test.ts b/packages/runtime-host/src/__tests__/scheduled-task-protocol.test.ts index 27e9df3e63..bf1f4b10ef 100644 --- a/packages/runtime-host/src/__tests__/scheduled-task-protocol.test.ts +++ b/packages/runtime-host/src/__tests__/scheduled-task-protocol.test.ts @@ -12,6 +12,7 @@ import { authorizeRuntimeHostOperation, createRuntimeHostConnectionAuthority, } from '../server/connection-authority.js'; +import { decodeScheduledTask, decodeScheduledTaskMutateInput } from '../protocol/scheduled-task.js'; describe('ScheduledTask protocol', () => { test('requires Host-path authority only when a mutation submits a Host path', () => { @@ -34,6 +35,39 @@ describe('ScheduledTask protocol', () => { } }); + test('a retired backend decodes on the way out but not on the way in', () => { + // #3211: the same execution decoder serves both directions, and they carry + // different backend invariants. A stored Automation frozen by a build that + // shipped FakeBackend must stay readable; a live create/update may not + // introduce the retired value. + const retired = (effect: ScheduledTaskEffect): ScheduledTaskEffect => + effect.kind === 'agent_run' + ? { ...effect, execution: { ...effect.execution, backend: 'fake' } } + : effect; + + const stored = { ...scheduledTask('task-1'), effect: retired(agentRunEffect('project-1')) }; + assert.equal(decodeScheduledTask(stored).effect.kind, 'agent_run'); + + for (const input of [ + { + kind: 'create' as const, + input: { + title: 'Inspect workspace', + intentBody: 'Summarize the workspace.', + schedule: { kind: 'once' as const, runAt: 1 }, + effect: retired(agentRunEffect('project-1')), + }, + }, + { + kind: 'update' as const, + taskId: 'task-1', + patch: { effect: retired(agentRunEffect('project-1')) }, + }, + ]) { + assert.throws(() => decodeScheduledTaskMutateInput(input), /Invalid ScheduledTask backend/); + } + }); + test('accepts signal-only catalog changes', () => { const frame = { kind: 'scheduled-task.changed' as const, diff --git a/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts b/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts index 414df2ca45..12dd3d53bd 100644 --- a/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts +++ b/packages/runtime-host/src/__tests__/session-catalog-two-client-uds.test.ts @@ -714,7 +714,6 @@ async function seedAuthority( const execution = await openInteractiveExecutionStoresForWrite(owner.lease); const unread = await execution.sessionStore.create({ cwd: root, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -751,7 +750,6 @@ async function seedAuthority( 'visible', ...Array.from({ length: 700 }, (_, index) => `label-${index}`), ], - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -759,7 +757,6 @@ async function seedAuthority( const oversized = await execution.sessionStore.create({ cwd: root, projectId: 'p'.repeat(257), - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -767,7 +764,6 @@ async function seedAuthority( const retirement = await execution.sessionStore.create({ cwd: root, name: 'Retirement sidecars', - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -775,7 +771,6 @@ async function seedAuthority( const recovery = await execution.sessionStore.create({ cwd: root, name: 'Retirement recovery', - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime-host/src/__tests__/session-retirement-coordinator.test.ts b/packages/runtime-host/src/__tests__/session-retirement-coordinator.test.ts index 471eed4950..08ded21cf7 100644 --- a/packages/runtime-host/src/__tests__/session-retirement-coordinator.test.ts +++ b/packages/runtime-host/src/__tests__/session-retirement-coordinator.test.ts @@ -1237,7 +1237,6 @@ function sessionInput( ): CreateSessionInput { return { cwd: '/workspace', - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts b/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts index 68021f80ba..7fcc8ee974 100644 --- a/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts +++ b/packages/runtime-host/src/__tests__/session-revision-two-client-uds.test.ts @@ -584,7 +584,6 @@ async function seedSource( const source = await execution.sessionStore.create({ cwd: root, name: 'Source Session', - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -592,7 +591,6 @@ async function seedSource( const busy = await execution.sessionStore.create({ cwd: root, name: 'Busy Session', - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -600,7 +598,6 @@ async function seedSource( const linkedChildSource = await execution.sessionStore.create({ cwd: root, name: 'Linked Child Source Session', - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -608,7 +605,6 @@ async function seedSource( const metadataLinkedSource = await execution.sessionStore.create({ cwd: root, name: 'Metadata-linked Source Session', - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -616,7 +612,6 @@ async function seedSource( const archivedOwnedSource = await execution.sessionStore.create({ cwd: root, name: 'Archived-owned Source Session', - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -624,7 +619,6 @@ async function seedSource( const continuationSource = await execution.sessionStore.create({ cwd: root, name: 'Continuation Source Session', - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -894,7 +888,6 @@ async function seedSource( { cwd: root, name: 'Graph Worker', - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -1132,7 +1125,6 @@ async function seedSource( const ordinaryLinkedChild = await execution.sessionStore.createSubagent({ cwd: root, name: 'Metadata-linked Child Session', - backend: 'ai-sdk', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime-host/src/__tests__/session-transcript-reader.test.ts b/packages/runtime-host/src/__tests__/session-transcript-reader.test.ts index c5f9a79fc4..d4ee4998b6 100644 --- a/packages/runtime-host/src/__tests__/session-transcript-reader.test.ts +++ b/packages/runtime-host/src/__tests__/session-transcript-reader.test.ts @@ -25,7 +25,6 @@ test('keeps durable history separate from the canonical active overlay', async ( const stores = await openInteractiveExecutionStoresForWrite(owner.lease); const session = await stores.sessionStore.create({ cwd: capability.canonicalPath, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime-host/src/protocol/scheduled-task.ts b/packages/runtime-host/src/protocol/scheduled-task.ts index 694b87fc37..2768ae584f 100644 --- a/packages/runtime-host/src/protocol/scheduled-task.ts +++ b/packages/runtime-host/src/protocol/scheduled-task.ts @@ -24,7 +24,7 @@ import { type UpdateScheduledTaskInput, } from '@maka/core/scheduled-task'; import { isThinkingLevel } from '@maka/core/model-thinking'; -import type { BackendKind } from '@maka/core/session'; +import type { PersistedBackendKind } from '@maka/core/session'; import { requireCount, requireEncodedByteLimit, @@ -304,7 +304,7 @@ export function decodeScheduledTask(value: unknown): ScheduledTask { body: boundedText(intent.body, 'ScheduledTask intent body', SCHEDULED_TASK_INTENT_MAX_CHARS), }, schedule: decodeSchedule(task.schedule), - effect: decodeEffect(task.effect), + effect: decodeEffect(task.effect, 'stored'), status: task.status, nextFireAt: nullableCount(task.nextFireAt, 'ScheduledTask nextFireAt'), lastFireAt: nullableCount(task.lastFireAt, 'ScheduledTask lastFireAt'), @@ -341,7 +341,7 @@ function decodeCreateInput(value: unknown): Omit { const runtimeStore = createWorkspaceRuntimeStore(root); const session = await sessionStore.create({ cwd: '/tmp/workspace', - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime/src/__tests__/latest-context-commit.test.ts b/packages/runtime/src/__tests__/latest-context-commit.test.ts index ecff35ce09..96c41bcdb0 100644 --- a/packages/runtime/src/__tests__/latest-context-commit.test.ts +++ b/packages/runtime/src/__tests__/latest-context-commit.test.ts @@ -44,7 +44,7 @@ test('a real send seals its row all the way into SQLite, with nothing injected', let clock = 1_000; const now = () => (clock += 1); - backends.register('fake', (ctx) => + backends.register('ai-sdk', (ctx) => createTestAiSdkBackend({ sessionId: ctx.sessionId, header: ctx.header, @@ -79,7 +79,6 @@ test('a real send seals its row all the way into SQLite, with nothing injected', }); const session = await manager.createSession({ cwd: root, - backend: 'fake', llmConnectionSlug: 'mock-main', permissionMode: 'bypass', }); diff --git a/packages/runtime/src/__tests__/runtime-continuation-crash.test.ts b/packages/runtime/src/__tests__/runtime-continuation-crash.test.ts index 013f974376..0a69757089 100644 --- a/packages/runtime/src/__tests__/runtime-continuation-crash.test.ts +++ b/packages/runtime/src/__tests__/runtime-continuation-crash.test.ts @@ -135,7 +135,7 @@ async function runCrashChild(): Promise { const runtimeEventStore = createCrashRuntimeStore(workspaceRoot); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new FakeBackend({ sessionId: ctx.sessionId, @@ -173,7 +173,6 @@ async function runCrashChild(): Promise { }); const session = await manager.createSession({ cwd: workspaceRoot, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'execute', @@ -229,7 +228,7 @@ function createManager(workspaceRoot: string): { const runtimeEventStore = createCrashRuntimeStore(workspaceRoot); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new FakeBackend({ sessionId: ctx.sessionId, diff --git a/packages/runtime/src/__tests__/runtime-kernel-interaction.test.ts b/packages/runtime/src/__tests__/runtime-kernel-interaction.test.ts index c8dbbabcdf..b1461b3422 100644 --- a/packages/runtime/src/__tests__/runtime-kernel-interaction.test.ts +++ b/packages/runtime/src/__tests__/runtime-kernel-interaction.test.ts @@ -25,7 +25,7 @@ describe('RuntimeKernel Interaction close cleanup', () => { const updateHeader = store.updateHeader; const backends = new BackendRegistry(); const backend = new BlockingBackend(SESSION_ID, {}); - backends.register('fake', () => backend); + backends.register('ai-sdk', () => backend); const startupFailure = new Error('mark running rejected after reservation'); let stoppedFailure: Promise | undefined; let kernel!: RuntimeKernel; @@ -63,7 +63,7 @@ describe('RuntimeKernel Interaction close cleanup', () => { const store = memoryStore(); const backends = new BackendRegistry(); const backend = new BlockingBackend(SESSION_ID, {}); - backends.register('fake', () => backend); + backends.register('ai-sdk', () => backend); const closeFailure = new Error('bind-race close rejected'); const closeStarted = deferred(); const releaseClose = deferred(); @@ -261,7 +261,7 @@ describe('RuntimeKernel Interaction close cleanup', () => { releaseSendOnStop: false, releaseSendOnDispose: false, }); - backends.register('fake', () => backend); + backends.register('ai-sdk', () => backend); const secondReserved = deferred(); const releaseSecond = deferred(); let reservations = 0; @@ -311,7 +311,7 @@ describe('RuntimeKernel Interaction close cleanup', () => { const store = memoryStore(); const backends = new BackendRegistry(); const built: BlockingBackend[] = []; - backends.register('fake', () => { + backends.register('ai-sdk', () => { const backend = new BlockingBackend( SESSION_ID, built.length === 0 @@ -380,7 +380,7 @@ describe('RuntimeKernel Interaction close cleanup', () => { const built: BlockingBackend[] = []; const stopFailure = new Error('backend stop failed'); const disposeFailure = new Error('backend disposal failed'); - backends.register('fake', () => { + backends.register('ai-sdk', () => { const backend = new BlockingBackend(SESSION_ID, { stopFailure, disposeFailure, @@ -447,7 +447,7 @@ function runtimeFixture(options: RuntimeFixtureOptions = {}): { const store = memoryStore(); const backends = new BackendRegistry(); const backend = new BlockingBackend(SESSION_ID, options); - backends.register('fake', () => backend); + backends.register('ai-sdk', () => backend); const closeFailure = new Error('durable close rejected'); let markCloseStarted!: () => void; const closeStarted = new Promise((resolve) => { @@ -510,7 +510,7 @@ function runtimeFixture(options: RuntimeFixtureOptions = {}): { } class BlockingBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly stopCalls: Array<{ reason: 'user_stop' | 'redirect'; mode: BackendStopMode | undefined; @@ -610,7 +610,7 @@ function memoryStore(): SessionStore { status: 'active', statusUpdatedAt: 1, hasUnread: false, - backend: 'fake', + backend: 'ai-sdk', llmConnectionSlug: 'test', connectionLocked: true, model: 'test', diff --git a/packages/runtime/src/__tests__/runtime-ledger-repair.test.ts b/packages/runtime/src/__tests__/runtime-ledger-repair.test.ts index 8f5d0312a5..d3903ed1fd 100644 --- a/packages/runtime/src/__tests__/runtime-ledger-repair.test.ts +++ b/packages/runtime/src/__tests__/runtime-ledger-repair.test.ts @@ -70,7 +70,6 @@ test('repairs imported transcript turns into provider-neutral canonical history' const session = await sessions.createImportedSession( { cwd: '/repo', - backend: 'fake', llmConnectionSlug: 'deepseek', model: 'deepseek-v4-flash', permissionMode: 'ask', diff --git a/packages/runtime/src/__tests__/sandbox-boundary-restart-recovery.test.ts b/packages/runtime/src/__tests__/sandbox-boundary-restart-recovery.test.ts index 77eff8a38f..69357e17b9 100644 --- a/packages/runtime/src/__tests__/sandbox-boundary-restart-recovery.test.ts +++ b/packages/runtime/src/__tests__/sandbox-boundary-restart-recovery.test.ts @@ -232,7 +232,6 @@ function runEvent(sessionId: string): EmittedAgentRunEvent { function sessionInput(cwd: string): CreateSessionInput { return { cwd, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime/src/__tests__/session-manager-terminal-ledger.test.ts b/packages/runtime/src/__tests__/session-manager-terminal-ledger.test.ts index ea88125ed3..bf484a033b 100644 --- a/packages/runtime/src/__tests__/session-manager-terminal-ledger.test.ts +++ b/packages/runtime/src/__tests__/session-manager-terminal-ledger.test.ts @@ -220,7 +220,7 @@ describe('SessionManager terminal ledger invariants', () => { const runStore = new TinyAgentRunStore(); const backends = new BackendRegistry(); let backend: StopDuringSendBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new StopDuringSendBackend(ctx); return backend; }); @@ -264,7 +264,7 @@ describe('SessionManager terminal ledger invariants', () => { const store = new TinySessionStore(); const runStore = new TinyAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new NeverEndingBackend(ctx)); + backends.register('ai-sdk', (ctx) => new NeverEndingBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -305,7 +305,7 @@ describe('SessionManager terminal ledger invariants', () => { }, }); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new NeverEndingBackend(ctx)); + backends.register('ai-sdk', (ctx) => new NeverEndingBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -338,7 +338,7 @@ describe('SessionManager terminal ledger invariants', () => { const store = new TinySessionStore(); const runStore = new TinyAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new NeverEndingBackend(ctx)); + backends.register('ai-sdk', (ctx) => new NeverEndingBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -378,7 +378,7 @@ describe('SessionManager terminal ledger invariants', () => { }); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new ScriptBackend(ctx, [{ type: 'complete', stopReason: 'step_limit' }]), ); const manager = new SessionManager({ @@ -2137,7 +2137,7 @@ async function makeHarness( const store = options.store ?? new TinySessionStore(); const runStore = new TinyAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new ScriptBackend(ctx, events)); + backends.register('ai-sdk', (ctx) => new ScriptBackend(ctx, events)); const manager = new SessionManager({ store, runStore, @@ -2152,7 +2152,7 @@ async function makeHarness( } class ScriptBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor( @@ -2181,7 +2181,7 @@ class ScriptBackend implements AgentBackend { } class StopDuringSendBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; private readonly stopStarted = deferred(); private readonly stopReturned = deferred(); @@ -2228,7 +2228,7 @@ class StopDuringSendBackend implements AgentBackend { * can finalize the run. */ class NeverEndingBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor(ctx: BackendFactoryContext) { @@ -2282,7 +2282,7 @@ class TinySessionStore implements SessionStore { ...(input.parentSessionId ? { parentSessionId: input.parentSessionId } : {}), ...(input.branchOfTurnId ? { branchOfTurnId: input.branchOfTurnId } : {}), hasUnread: false, - backend: input.backend, + backend: 'ai-sdk', llmConnectionSlug: input.llmConnectionSlug, connectionLocked: false, model: input.model ?? 'fake-model', @@ -2565,7 +2565,6 @@ class BatchingRuntimeEventStore implements RuntimeEventStore { function makeInput(overrides: Partial = {}): CreateSessionInput { return { cwd: '/tmp/cwd', - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime/src/__tests__/session-manager.test.ts b/packages/runtime/src/__tests__/session-manager.test.ts index 2835b575b5..be00dd12f6 100644 --- a/packages/runtime/src/__tests__/session-manager.test.ts +++ b/packages/runtime/src/__tests__/session-manager.test.ts @@ -446,7 +446,7 @@ describe('SessionManager graph operator provisioning', () => { const runStore = new MemoryAgentRunStore(); const parentGate = makeGate(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx, parentGate)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx, parentGate)); const manager = new SessionManager({ store, runStore, @@ -1106,7 +1106,7 @@ describe('SessionManager claimed graph intent execution', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backendBuilds = 0; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backendBuilds += 1; return new TestBackend(ctx); }); @@ -1138,7 +1138,7 @@ describe('SessionManager claimed graph intent execution', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const executions: Parameters[0][] = []; const authority = hostedRootAuthority(); authority.executeRoot = async (input) => { @@ -1232,7 +1232,7 @@ describe('SessionManager claimed graph intent execution', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backendBuilds = 0; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backendBuilds += 1; return new TestBackend(ctx); }); @@ -1311,7 +1311,7 @@ describe('SessionManager claimed graph intent execution', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const parent = await store.create(makeInput({ name: 'Supervisor' })); const child = await createGraphOperatorSession(store, parent.id); const claim = graphIntentClaim( @@ -1383,7 +1383,7 @@ describe('SessionManager claimed graph intent execution', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backendBuilds = 0; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backendBuilds += 1; return new TestBackend(ctx); }); @@ -1446,7 +1446,7 @@ describe('SessionManager claimed graph intent execution', () => { const backends = new BackendRegistry(); const backendGate = makeGate(); const ready = makeGate(); - backends.register('fake', (ctx) => new TestBackend(ctx, backendGate)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx, backendGate)); const stoppedRoots: RuntimeMessageRunIdentity[] = []; let stoppedSessions = 0; const authority = hostedRootAuthority(); @@ -1562,7 +1562,7 @@ describe('SessionManager claimed graph intent execution', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); const backendsBySession = new Map(); - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { const backend = new TestBackend(ctx); backendsBySession.set(ctx.sessionId, backend); return backend; @@ -1659,7 +1659,7 @@ describe('SessionManager claimed graph intent execution', () => { const childGate = makeGate(); const started = makeGate(); let childBackend: TestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { childBackend = new TestBackend(ctx, childGate); return childBackend; }); @@ -1743,7 +1743,7 @@ describe('SessionManager claimed graph intent execution', () => { const firstGate = makeGate(); const firstReady = makeGate(); let backend: TestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new TestBackend(ctx, firstGate); return backend; }); @@ -1801,7 +1801,7 @@ describe('SessionManager claimed graph intent execution', () => { const admissionStarted = makeGate(); const releaseAdmission = makeGate(); let backend: TestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new TestBackend(ctx); return backend; }); @@ -1913,7 +1913,7 @@ describe('SessionManager claimed graph intent execution', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backendBuilds = 0; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backendBuilds += 1; return new TestBackend(ctx); }); @@ -1967,7 +1967,7 @@ describe('SessionManager claimed graph intent execution', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backend: TestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new TestBackend(ctx); return backend; }); @@ -2018,7 +2018,7 @@ describe('SessionManager claimed graph intent execution', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backendBuilds = 0; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backendBuilds += 1; return new TestBackend(ctx); }); @@ -2133,7 +2133,7 @@ describe('SessionManager child-session runtime primitive', () => { const contexts: BackendFactoryContext[] = []; const backendActivationSessions: string[] = []; const backendsBySession = new Map(); - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { contexts.push(ctx); const backend = new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate); backendsBySession.set(ctx.sessionId, backend); @@ -2191,7 +2191,6 @@ describe('SessionManager child-session runtime primitive', () => { expect(childHeader.cwd).toBe('/tmp/project'); expect(childHeader.projectId).toBe('project-1'); expect(childHeader.workspaceRoot).toBe((await store.readHeader(parent.id)).workspaceRoot); - expect(childHeader.backend).toBe('fake'); expect(childHeader.llmConnectionSlug).toBe('connection-1'); expect(childHeader.model).toBe('model-1'); expect(childHeader.thinkingLevel).toBe('medium'); @@ -2306,7 +2305,7 @@ describe('SessionManager child-session runtime primitive', () => { const backends = new BackendRegistry(); const parentGate = makeGate(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate), ); const manager = new SessionManager({ @@ -2391,7 +2390,7 @@ describe('SessionManager child-session runtime primitive', () => { const backends = new BackendRegistry(); const parentGate = makeGate(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate), ); const manager = new SessionManager({ @@ -2433,7 +2432,7 @@ describe('SessionManager child-session runtime primitive', () => { const backends = new BackendRegistry(); const parentGate = makeGate(); const backendsBySession = new Map(); - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { const backend = new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate); backendsBySession.set(ctx.sessionId, backend); return backend; @@ -2504,7 +2503,7 @@ describe('SessionManager child-session runtime primitive', () => { const backends = new BackendRegistry(); const parentGate = makeGate(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate), ); const manager = new SessionManager({ @@ -2556,7 +2555,7 @@ describe('SessionManager child-session runtime primitive', () => { const childInputs: BackendSendInput[] = []; let childAttempts = 0; let failClaimOnce = true; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { if (!ctx.header.subagentRuntime) return new TestBackend(ctx, parentGate); return { kind: 'fake' as const, @@ -2815,7 +2814,7 @@ describe('SessionManager child-session runtime primitive', () => { const backends = new BackendRegistry(); const parentGate = makeGate(); const childInputs: BackendSendInput[] = []; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { if (!ctx.header.subagentRuntime) return new TestBackend(ctx, parentGate); return { kind: 'fake' as const, @@ -2933,7 +2932,7 @@ describe('SessionManager child-session runtime primitive', () => { const parentGate = makeGate(); const childGate = makeGate(); const backendsBySession = new Map(); - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { const backend = new TestBackend(ctx, ctx.header.subagentRuntime ? childGate : parentGate); backendsBySession.set(ctx.sessionId, backend); return backend; @@ -3011,7 +3010,7 @@ describe('SessionManager child-session runtime primitive', () => { const backends = new BackendRegistry(); const parentGate = makeGate(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate), ); const manager = new SessionManager({ @@ -3088,7 +3087,7 @@ describe('SessionManager child-session runtime primitive', () => { const backends = new BackendRegistry(); const parentGate = makeGate(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate), ); const manager = new SessionManager({ @@ -3227,7 +3226,7 @@ describe('SessionManager child-session runtime primitive', () => { const backends = new BackendRegistry(); const parentGate = makeGate(); const contexts: BackendFactoryContext[] = []; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { contexts.push(ctx); return new TestBackend(ctx, ctx.header.subagentRuntime ? undefined : parentGate); }); @@ -3321,7 +3320,7 @@ describe('SessionManager child-session runtime primitive', () => { const parentGate = makeGate(); const compactCalls: Array<{ turnId: string; runtimeContextCount: number }> = []; const childBackends: LifecycleChildBackend[] = []; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { if (!ctx.header.subagentRuntime) return new TestBackend(ctx, parentGate); const backend = new LifecycleChildBackend(ctx, compactCalls); childBackends.push(backend); @@ -3418,7 +3417,7 @@ describe('SessionManager child-session runtime primitive', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); const parentGate = makeGate(); - backends.register('fake', (ctx) => new TestBackend(ctx, parentGate)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx, parentGate)); const manager = new SessionManager({ store, runStore, @@ -3533,7 +3532,7 @@ describe('SessionManager child-session runtime primitive', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -3569,7 +3568,7 @@ describe('SessionManager child-session runtime primitive', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); let externalParent: | { sessionId: string; @@ -3623,7 +3622,7 @@ describe('SessionManager child-session runtime primitive', () => { const childGates = [makeGate(), makeGate()]; let childGateIndex = 0; const backendsBySession = new Map(); - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { const gate = ctx.header.subagentRuntime ? childGates[childGateIndex++] : parentGate; const backend = new TestBackend(ctx, gate); backendsBySession.set(ctx.sessionId, backend); @@ -3709,7 +3708,7 @@ describe('SessionManager child-session runtime primitive', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -3815,7 +3814,7 @@ describe('SessionManager automatic titles', () => { test('falls back once on generation failure', async () => { const store = new MemorySessionStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const changed = makeGate(); let calls = 0; const manager = new SessionManager({ @@ -3844,7 +3843,7 @@ describe('SessionManager automatic titles', () => { test('does not overwrite or notify after a racing manual rename', async () => { const store = new MemorySessionStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const release = makeGate(); const attempted = makeGate(); store.generatedTitleAttempted = attempted; @@ -3880,7 +3879,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => const runStore = new OrderingAgentRunStore(); const backends = new BackendRegistry(); const compactCalls: Array<{ turnId: string; runtimeContextCount: number }> = []; - backends.register('fake', (ctx) => new CompactingTestBackend(ctx, compactCalls)); + backends.register('ai-sdk', (ctx) => new CompactingTestBackend(ctx, compactCalls)); const manager = new SessionManager({ store, runStore, @@ -3889,9 +3888,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => newId: nextId(), now: nextNow(10_000), }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })); runStore.operations = []; @@ -3964,7 +3961,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => warnings: [], }, }); - backends.register('fake', (ctx) => + backends.register('ai-sdk', (ctx) => createTestAiSdkBackend({ sessionId: ctx.sessionId, header: ctx.header, @@ -4025,9 +4022,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => newId: nextId(), now: nextNow(13_000), }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'first '.repeat(400) })); await drain(manager.sendMessage(session.id, { turnId: 'turn-2', text: 'second' })); @@ -4056,7 +4051,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new FailOpenCompactingBackend(ctx)); + backends.register('ai-sdk', (ctx) => new FailOpenCompactingBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -4065,9 +4060,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => newId: nextId(), now: nextNow(12_000), }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })); await drain(manager.compactSession(session.id, { turnId: 'turn-compact' })); @@ -4095,7 +4088,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => }); const backends = new BackendRegistry(); const compactCalls: Array<{ turnId: string; runtimeContextCount: number }> = []; - backends.register('fake', (ctx) => new CompactingTestBackend(ctx, compactCalls)); + backends.register('ai-sdk', (ctx) => new CompactingTestBackend(ctx, compactCalls)); const manager = new SessionManager({ store, runStore, @@ -4104,9 +4097,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => newId: nextId(), now: nextNow(15_000), }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })); blockPriorRead = true; @@ -4141,7 +4132,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => const backends = new BackendRegistry(); const factoryStarts = new Map>(); const factoryModes = new Map(); - backends.register('fake', async (ctx) => { + backends.register('ai-sdk', async (ctx) => { const started = factoryStarts.get(ctx.sessionId); const mode = factoryModes.get(ctx.sessionId); if (!started || !mode) throw new Error('cold compact factory was not configured'); @@ -4168,12 +4159,8 @@ describe('SessionManager manual compaction and quiescent session changes', () => newId: nextId(), now: nextNow(15_500), }); - const cancelledSession = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); - const abortErrorSession = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const cancelledSession = await manager.createSession(makeInput({ permissionMode: 'bypass' })); + const abortErrorSession = await manager.createSession(makeInput({ permissionMode: 'bypass' })); const cancelledStart = makeGate(); const abortErrorStart = makeGate(); factoryStarts.set(cancelledSession.id, cancelledStart); @@ -4212,7 +4199,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); const compactCalls: Array<{ turnId: string; runtimeContextCount: number }> = []; - backends.register('fake', (ctx) => new CompactingTestBackend(ctx, compactCalls)); + backends.register('ai-sdk', (ctx) => new CompactingTestBackend(ctx, compactCalls)); const manager = new SessionManager({ store, runStore, @@ -4221,9 +4208,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => newId: nextId(), now: nextNow(17_000), }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); const readStarted = makeGate(); const releaseRead = makeGate(); store.nextReadHeaderGate = { started: readStarted, release: releaseRead }; @@ -4260,7 +4245,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => let compactingBackend: BlockingCompactBackend | undefined; const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new BlockingCompactBackend(ctx, { compactGate, @@ -4278,9 +4263,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => newId: nextId(), now: nextNow(20_000), }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })); const compactPromise = collectSessionEvents( @@ -4307,7 +4290,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => const compactCalls: Array<{ turnId: string; runtimeContextCount: number }> = []; const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new ActiveTurnBackend(ctx, { turnStarted, sendGate, compactCalls }), ); const manager = new SessionManager({ @@ -4318,9 +4301,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => newId: nextId(), now: nextNow(25_000), }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); const sendPromise = (async () => { for await (const _event of manager.sendMessage(session.id, { @@ -4486,7 +4467,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => const readStarted = makeGate(); const releaseRead = makeGate(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const manager = new SessionManager({ store, backends, @@ -4533,7 +4514,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => const releaseUpdate = makeGate(); const activatedModels: string[] = []; const backends = new BackendRegistry(); - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { activatedModels.push(ctx.header.model); return new TestBackend(ctx); }); @@ -4583,7 +4564,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => const turnStarted = makeGate(); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new (class extends ActiveTurnBackend { override async dispose(): Promise { @@ -4620,7 +4601,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => let disposeCalls = 0; const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new (class extends TestBackend { override async dispose(): Promise { @@ -4674,7 +4655,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => let disposeCalls = 0; const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new (class extends TestBackend { constructor() { @@ -4752,7 +4733,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => let builds = 0; const disposed: number[] = []; const backends = new BackendRegistry(); - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { const generation = ++builds; return new (class extends TestBackend { override async dispose(): Promise { @@ -4828,7 +4809,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => let builds = 0; const disposed: number[] = []; const backends = new BackendRegistry(); - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { const generation = ++builds; return new (class extends TestBackend { override async dispose(): Promise { @@ -4898,7 +4879,7 @@ describe('SessionManager manual compaction and quiescent session changes', () => const disposed: string[] = []; const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new (class extends TestBackend { override async dispose(): Promise { @@ -4964,7 +4945,7 @@ describe('SessionManager permission mode updates', () => { const store = new AtomicBoundaryMemorySessionStore(); const calls: string[] = []; const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const manager = new SessionManager({ store, backends, @@ -5078,7 +5059,7 @@ describe('SessionManager permission mode updates', () => { const firstGate = makeGate(); const secondGate = makeGate(); const gates = [firstGate, secondGate]; - backends.register('fake', (ctx) => new TestBackend(ctx, gates.shift())); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx, gates.shift())); const manager = new SessionManager({ store, runStore, @@ -5129,7 +5110,7 @@ describe('SessionManager permission mode updates', () => { test('leaving explore clears the deep research label so visible read-only copy stays truthful', async () => { const store = new MemorySessionStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(6_000) }); const session = await manager.createSession( makeInput({ @@ -5156,7 +5137,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new FinalTextTestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new FinalTextTestBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -5189,7 +5170,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let inspectionCalls = 0; - backends.register('fake', (ctx) => new FinalTextTestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new FinalTextTestBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -5221,39 +5202,6 @@ describe('SessionManager permission mode updates', () => { expect(run?.workspaceIdentity).toBeUndefined(); }); - test('does not declare the T1 protocol for a backend without the durable tool boundary', async () => { - const store = new MemorySessionStore(); - const runStore = new MemoryAgentRunStore(); - const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new CountingFinalTextBackend(ctx, () => {})); - const runtimeEventStore = Object.assign(runStore, { - toolBoundaryProtocol: 't1_after_preflight_v1' as const, - }); - const manager = new SessionManager({ - store, - runStore, - runtimeEventStore, - toolBoundaryProtocol: 't1_after_preflight_v1', - backends, - newId: nextId(), - now: nextNow(6_050), - runtimeSource: 'test', - }); - const session = await manager.createSession(makeInput()); - - await collectSessionEvents( - manager.sendMessage(session.id, { - turnId: 'turn-no-tool-boundary', - text: 'hello', - }), - ); - - const [run] = await runStore.listSessionRuns(session.id); - if (!run) throw new Error('expected run'); - const events = await runStore.readRuntimeEvents(session.id, run.runId); - expect(events[0]?.actions?.runtimeProtocol).toBeUndefined(); - }); - test('declares the T1 protocol for an AiSdk run when the host wires the durable boundary', async () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); @@ -5269,7 +5217,7 @@ describe('SessionManager permission mode updates', () => { now: nextNow(6_060), runtimeSource: 'test', }); - const session = await manager.createSession(makeInput({ backend: 'ai-sdk' })); + const session = await manager.createSession(makeInput()); await collectSessionEvents( manager.sendMessage(session.id, { @@ -5501,7 +5449,7 @@ describe('SessionManager permission mode updates', () => { const backends = new BackendRegistry(); const observed: InvocationResult[] = []; let backend: FinalTextTestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new FinalTextTestBackend(ctx); return backend; }); @@ -5591,7 +5539,7 @@ describe('SessionManager permission mode updates', () => { now: nextNow(6_550), runtimeSource: 'test', }); - const session = await manager.createSession(makeInput({ backend: 'ai-sdk' })); + const session = await manager.createSession(makeInput()); const header = await store.readHeader(session.id); const sourceRunId = 'source-run'; const sourceTurnId = 'source-turn'; @@ -5731,7 +5679,7 @@ describe('SessionManager permission mode updates', () => { const sendGate = makeGate(); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new ActiveTurnBackend(ctx, { turnStarted, @@ -5839,7 +5787,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new FinalTextTestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new FinalTextTestBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -5984,7 +5932,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new FinalTextTestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new FinalTextTestBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -6098,7 +6046,7 @@ describe('SessionManager permission mode updates', () => { const backends = new BackendRegistry(); let backendCalls = 0; backends.register( - 'fake', + 'ai-sdk', (ctx) => new CountingFinalTextBackend(ctx, () => { backendCalls += 1; @@ -6185,7 +6133,7 @@ describe('SessionManager permission mode updates', () => { const backends = new BackendRegistry(); let backendCalls = 0; backends.register( - 'fake', + 'ai-sdk', (ctx) => new CountingFinalTextBackend(ctx, () => { backendCalls += 1; @@ -6280,7 +6228,7 @@ describe('SessionManager permission mode updates', () => { const backends = new BackendRegistry(); let backendCalls = 0; backends.register( - 'fake', + 'ai-sdk', (ctx) => new CountingFinalTextBackend(ctx, () => { backendCalls += 1; @@ -6391,7 +6339,7 @@ describe('SessionManager permission mode updates', () => { const releaseContinuationCommit = makeGate(); let backendCalls = 0; backends.register( - 'fake', + 'ai-sdk', (ctx) => new CountingFinalTextBackend(ctx, () => { backendCalls += 1; @@ -6489,7 +6437,7 @@ describe('SessionManager permission mode updates', () => { const backends = new BackendRegistry(); let backendCalls = 0; backends.register( - 'fake', + 'ai-sdk', (ctx) => new CountingFinalTextBackend(ctx, () => { backendCalls += 1; @@ -6571,7 +6519,7 @@ describe('SessionManager permission mode updates', () => { const backends = new BackendRegistry(); let backendCalls = 0; backends.register( - 'fake', + 'ai-sdk', (ctx) => new CountingFinalTextBackend(ctx, () => { backendCalls += 1; @@ -6659,7 +6607,7 @@ describe('SessionManager permission mode updates', () => { let backendCalls = 0; let failOnce = true; backends.register( - 'fake', + 'ai-sdk', (ctx) => new CountingFinalTextBackend(ctx, () => { backendCalls += 1; @@ -6803,7 +6751,7 @@ describe('SessionManager permission mode updates', () => { let workspaceIdentity = 'workspace-1'; const lifecycleEvents: Array<{ type: string; errorClass?: string }> = []; backends.register( - 'fake', + 'ai-sdk', (ctx) => new CountingFinalTextBackend(ctx, () => { backendCalls += 1; @@ -6907,7 +6855,7 @@ describe('SessionManager permission mode updates', () => { let backendCalls = 0; let availableToolNames: readonly string[] = scenario.planned; backends.register( - 'fake', + 'ai-sdk', (ctx) => new CountingFinalTextBackend(ctx, () => { backendCalls += 1; @@ -6995,7 +6943,7 @@ describe('SessionManager permission mode updates', () => { let backendCalls = 0; let availableToolNames: readonly string[] = ['Write']; backends.register( - 'fake', + 'ai-sdk', (ctx) => new CountingFinalTextBackend(ctx, () => { backendCalls += 1; @@ -7103,7 +7051,7 @@ describe('SessionManager permission mode updates', () => { const backends = new BackendRegistry(); let backendCalls = 0; backends.register( - 'fake', + 'ai-sdk', (ctx) => new CountingFinalTextBackend(ctx, () => { backendCalls += 1; @@ -7181,7 +7129,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backend: TestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new TestBackend(ctx); return backend; }); @@ -7291,7 +7239,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore({ failRuntimeEventAppendAfter: 3 }); const backends = new BackendRegistry(); let backend: TestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new TestBackend(ctx); return backend; }); @@ -7418,7 +7366,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -7444,7 +7392,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backend: TestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new TestBackend(ctx); return backend; }); @@ -7537,7 +7485,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backend: TestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new TestBackend(ctx); return backend; }); @@ -9026,7 +8974,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]), ); const manager = new SessionManager({ @@ -9076,7 +9024,7 @@ describe('SessionManager permission mode updates', () => { }); const backends = new BackendRegistry(); let backend: TestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new TestBackend(ctx); return backend; }); @@ -9132,7 +9080,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]), ); const manager = new SessionManager({ @@ -9245,7 +9193,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); const backendInstances: TestBackend[] = []; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { const backend = new TestBackend(ctx); backendInstances.push(backend); return backend; @@ -9370,7 +9318,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); const backendInstances: TestBackend[] = []; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { const backend = new TestBackend(ctx); backendInstances.push(backend); return backend; @@ -9540,7 +9488,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); const backendInstances: TestBackend[] = []; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { const backend = new TestBackend(ctx); backendInstances.push(backend); return backend; @@ -9623,7 +9571,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore({ failUpdateRunStatusOnce: 'failed' }); const backends = new BackendRegistry(); let backend: TurnScriptBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new TurnScriptBackend(ctx, [ [{ type: 'complete', stopReason: 'error' }], [ @@ -9677,7 +9625,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); const backendInstances: TestBackend[] = []; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { const backend = new TestBackend(ctx); backendInstances.push(backend); return backend; @@ -9766,7 +9714,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); const backendInstances: TestBackend[] = []; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { const backend = new TestBackend(ctx); backendInstances.push(backend); return backend; @@ -9812,7 +9760,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); const childBackends: TestBackend[] = []; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { const backend = new TestBackend(ctx); if (ctx.systemPrompt) childBackends.push(backend); return backend; @@ -9890,7 +9838,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -10006,7 +9954,7 @@ describe('SessionManager permission mode updates', () => { async respondToSandboxBoundary(): Promise {}, async dispose(): Promise {}, }); - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { contexts.push(ctx); return createBackend(ctx); }); @@ -10179,7 +10127,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let providerDispatches = 0; - backends.register('fake', (ctx) => ({ + backends.register('ai-sdk', (ctx) => ({ kind: 'fake' as const, sessionId: ctx.sessionId, async *send(input: BackendSendInput): AsyncIterable { @@ -10321,7 +10269,7 @@ describe('SessionManager permission mode updates', () => { yield* super.send(input); } } - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { contexts.push(ctx); return new SeamProbeBackend(ctx); }); @@ -10371,7 +10319,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); const contexts: BackendFactoryContext[] = []; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { contexts.push(ctx); return new TestBackend(ctx); }); @@ -10414,7 +10362,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); const contexts: BackendFactoryContext[] = []; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { contexts.push(ctx); return new TestBackend(ctx); }); @@ -10478,7 +10426,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore({ failUpdateRunStatusOnce: 'completed' }); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -10533,7 +10481,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'step_limit' }]), ); const manager = new SessionManager({ @@ -10561,7 +10509,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new HighVolumeDeltaBackend(ctx, 512)); + backends.register('ai-sdk', (ctx) => new HighVolumeDeltaBackend(ctx, 512)); const manager = new SessionManager({ store, runStore, @@ -10603,7 +10551,7 @@ describe('SessionManager permission mode updates', () => { const backends = new BackendRegistry(); const childGate = makeGate(); const backendInstances: TestBackend[] = []; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { const backend = new TestBackend( ctx, ctx.header.permissionMode === 'explore' ? childGate : undefined, @@ -10658,7 +10606,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let childBackend: TestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { childBackend = new TestBackend(ctx); return childBackend; }); @@ -10711,7 +10659,7 @@ describe('SessionManager permission mode updates', () => { const backends = new BackendRegistry(); const activeGate = makeGate(); let backend: TestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new TestBackend(ctx, activeGate); return backend; }); @@ -10766,7 +10714,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backend: TestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new TestBackend(ctx); return backend; }); @@ -10819,7 +10767,7 @@ describe('SessionManager permission mode updates', () => { const backends = new BackendRegistry(); const buildStarted = makeGate(); const releaseBuild = makeGate(); - backends.register('fake', async () => { + backends.register('ai-sdk', async () => { buildStarted.release(); await releaseBuild.promise; const error = new Error('backend activation timed out'); @@ -10871,7 +10819,7 @@ describe('SessionManager permission mode updates', () => { const buildStarted = makeGate(); let factorySignal: AbortSignal | undefined; let dispatches = 0; - backends.register('fake', async (ctx) => { + backends.register('ai-sdk', async (ctx) => { factorySignal = ctx.abortSignal; if (!factorySignal) throw new Error('backend factory did not receive an abort signal'); buildStarted.release(); @@ -10916,7 +10864,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); const buildStarted = makeGate(); - backends.register('fake', async (ctx) => { + backends.register('ai-sdk', async (ctx) => { if (!ctx.abortSignal) throw new Error('backend factory did not receive an abort signal'); buildStarted.release(); await timerDelay(60_000, undefined, { signal: ctx.abortSignal }); @@ -10958,7 +10906,7 @@ describe('SessionManager permission mode updates', () => { let builds = 0; let firstDisposeCalls = 0; let firstDispatches = 0; - backends.register('fake', async (ctx) => { + backends.register('ai-sdk', async (ctx) => { builds += 1; if (builds === 1) { buildStarted.release(); @@ -11025,7 +10973,7 @@ describe('SessionManager permission mode updates', () => { const releaseBuild = makeGate(); let builds = 0; let disposeCalls = 0; - backends.register('fake', async (ctx) => { + backends.register('ai-sdk', async (ctx) => { builds += 1; buildStarted.release(); await releaseBuild.promise; @@ -11099,7 +11047,7 @@ describe('SessionManager permission mode updates', () => { const backends = new BackendRegistry(); const childGate = makeGate(); let childBackend: RetryStopBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { if (ctx.header.permissionMode !== 'explore') return new TestBackend(ctx); childBackend = new RetryStopBackend(ctx, childGate); return childBackend; @@ -11152,7 +11100,7 @@ describe('SessionManager permission mode updates', () => { const stopStarted = makeGate(); const releaseStop = makeGate(); let backend: ConcurrentStopBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new ConcurrentStopBackend(ctx, stopStarted, releaseStop); return backend; }); @@ -11204,7 +11152,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backend: TestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new TestBackend(ctx); return backend; }); @@ -11262,7 +11210,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backend: TestBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new TestBackend(ctx); return backend; }); @@ -11312,7 +11260,7 @@ describe('SessionManager permission mode updates', () => { const releaseSend = makeGate(); let builds = 0; let backend: PermissionBroadcastBackend | undefined; - backends.register('fake', async (ctx) => { + backends.register('ai-sdk', async (ctx) => { builds += 1; buildStarted.release(); await releaseBuild.promise; @@ -11368,7 +11316,7 @@ describe('SessionManager permission mode updates', () => { const childGate = makeGate(); let parentBackend: SandboxBoundaryWaitBackend | undefined; let childBackend: PermissionBroadcastBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { if (ctx.header.permissionMode === 'explore') { childBackend = new PermissionBroadcastBackend(ctx, childGate); return childBackend; @@ -11422,7 +11370,7 @@ describe('SessionManager permission mode updates', () => { const childGate = makeGate(); let parentBackend: CountingStopBackend | undefined; let childBackend: RetryStopBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { if (ctx.header.permissionMode === 'explore') { childBackend = new RetryStopBackend(ctx, childGate); return childBackend; @@ -11480,7 +11428,7 @@ describe('SessionManager permission mode updates', () => { const backends = new BackendRegistry(); const sendGate = makeGate(); let backend: CountingStopBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new CountingStopBackend(ctx, sendGate); return backend; }); @@ -11530,7 +11478,7 @@ describe('SessionManager permission mode updates', () => { const backends = new BackendRegistry(); const sendGate = makeGate(); let backend: CountingStopBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new CountingStopBackend(ctx, sendGate); return backend; }); @@ -11580,7 +11528,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); const backendInstances: TestBackend[] = []; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { const backend = new TestBackend(ctx); backendInstances.push(backend); return backend; @@ -11642,7 +11590,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -11844,7 +11792,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -11932,7 +11880,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -12019,7 +11967,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -12096,7 +12044,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', () => { + backends.register('ai-sdk', () => { throw new Error('backend init failed'); }); const manager = new SessionManager({ @@ -12145,7 +12093,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backend: SandboxBoundaryWaitBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new SandboxBoundaryWaitBackend(ctx); return backend; }); @@ -12192,7 +12140,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new FakeBackend(ctx)); + backends.register('ai-sdk', (ctx) => new FakeBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -12201,7 +12149,7 @@ describe('SessionManager permission mode updates', () => { newId: nextId(), now: nextNow(9_000), }); - const session = await manager.createSession(makeInput({ backend: 'fake' })); + const session = await manager.createSession(makeInput()); const iterator = manager .sendMessage(session.id, { turnId: 'turn-1', text: FAKE_ASK_USER_QUESTION_PROMPT }) @@ -12248,7 +12196,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new FakeBackend(ctx)); + backends.register('ai-sdk', (ctx) => new FakeBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -12257,7 +12205,7 @@ describe('SessionManager permission mode updates', () => { newId: nextId(), now: nextNow(9_000), }); - const session = await manager.createSession(makeInput({ backend: 'fake' })); + const session = await manager.createSession(makeInput()); const iterator = manager .sendMessage(session.id, { turnId: 'turn-1', text: FAKE_ASK_USER_QUESTION_PROMPT }) @@ -12282,7 +12230,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new SandboxBoundaryWaitBackend(ctx)); + backends.register('ai-sdk', (ctx) => new SandboxBoundaryWaitBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -12317,7 +12265,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new UnmappedSessionEventBackend(ctx)); + backends.register('ai-sdk', (ctx) => new UnmappedSessionEventBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -12359,7 +12307,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new UnmappedSessionEventBackend(ctx)); + backends.register('ai-sdk', (ctx) => new UnmappedSessionEventBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -12410,7 +12358,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'error' }]), ); const manager = new SessionManager({ @@ -12437,7 +12385,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'step_limit' }]), ); const manager = new SessionManager({ @@ -12473,7 +12421,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new EventBackend(ctx, [ { type: 'error', recoverable: false, reason: 'tool_failed', message: 'Tool failed' }, @@ -12501,7 +12449,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const backends = new BackendRegistry(); const gate = makeGate(); - backends.register('fake', (ctx) => new TestBackend(ctx, gate)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx, gate)); const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(12_500) }); const session = await manager.createSession(makeInput()); @@ -12527,7 +12475,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backend: StopControlledAbortBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new StopControlledAbortBackend(ctx); return backend; }); @@ -12579,7 +12527,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new EventBackend(ctx, [ { type: 'text_delta', messageId: 'm1', text: 'before' }, @@ -12632,7 +12580,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new ThrowAfterTerminalBackend(ctx)); + backends.register('ai-sdk', (ctx) => new ThrowAfterTerminalBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -12675,7 +12623,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); const gate = makeGate(); - backends.register('fake', (ctx) => new LateErrorBackend(ctx, gate)); + backends.register('ai-sdk', (ctx) => new LateErrorBackend(ctx, gate)); const manager = new SessionManager({ store, runStore, @@ -12713,7 +12661,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TraceBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TraceBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -12727,7 +12675,7 @@ describe('SessionManager permission mode updates', () => { await drain(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'hello' })); const [run] = await runStore.listSessionRuns(session.id); - expect(run?.backendKind).toBe('fake'); + expect(run?.backendKind).toBe('ai-sdk'); expect(run?.llmConnectionSlug).toBe('fake'); expect(run?.modelId).toBe('fake-model'); expect(run?.permissionMode).toBe('ask'); @@ -12755,7 +12703,7 @@ describe('SessionManager permission mode updates', () => { }); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new ProviderCaptureAfterAttemptFailureBackend( ctx, @@ -12806,7 +12754,7 @@ describe('SessionManager permission mode updates', () => { }, }); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new ProviderRequestTraceBackend(ctx)); + backends.register('ai-sdk', (ctx) => new ProviderRequestTraceBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -12842,7 +12790,7 @@ describe('SessionManager permission mode updates', () => { }); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new ProviderCaptureGateBackend(ctx, () => (providerDispatches += 1)), ); const manager = new SessionManager({ @@ -12871,7 +12819,7 @@ describe('SessionManager permission mode updates', () => { const cleanupCalled = makeGate(); let observedEventIds: string[] = []; const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new HistoryCompactCheckpointBackend(ctx)); + backends.register('ai-sdk', (ctx) => new HistoryCompactCheckpointBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -13008,7 +12956,7 @@ describe('SessionManager permission mode updates', () => { const observedCheckpointIds: Array = []; const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new HistoryCompactCheckpointCacheProbeBackend(ctx, observedCheckpointIds), ); const manager = new SessionManager({ @@ -13054,7 +13002,7 @@ describe('SessionManager permission mode updates', () => { const observedCoverage: Array = []; const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new HistoryCompactCheckpointMonotonicProbeBackend(ctx, observedCoverage), ); const manager = new SessionManager({ @@ -13109,7 +13057,7 @@ describe('SessionManager permission mode updates', () => { const writeOutcomes: string[] = []; const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new SameCoverageCheckpointReplacementProbeBackend(ctx, writeOutcomes), ); const manager = new SessionManager({ @@ -13166,7 +13114,7 @@ describe('SessionManager permission mode updates', () => { }); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new SerializedCheckpointProbeBackend(ctx, childRecorderCalled, recorderReturnedPromises), ); @@ -13234,7 +13182,7 @@ describe('SessionManager permission mode updates', () => { }); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new RecoveringCheckpointWriteProbeBackend( ctx, @@ -13307,7 +13255,7 @@ describe('SessionManager permission mode updates', () => { }); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new CheckpointRecorderContractProbeBackend( ctx, @@ -13350,7 +13298,7 @@ describe('SessionManager permission mode updates', () => { }); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new CheckpointRecorderContractProbeBackend( ctx, @@ -13418,7 +13366,7 @@ describe('SessionManager permission mode updates', () => { }); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new InitialCheckpointLoadRaceProbeBackend( ctx, @@ -13522,7 +13470,7 @@ describe('SessionManager permission mode updates', () => { const writeOutcomes: string[] = []; const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new CheckpointRecorderContractProbeBackend(ctx, async () => {}, writeOutcomes), ); const manager = new SessionManager({ @@ -13598,7 +13546,7 @@ describe('SessionManager permission mode updates', () => { test('startup recovery marks persisted running turns as failed instead of leaving them stuck', async () => { const store = new MemorySessionStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const manager = new SessionManager({ store, backends, newId: nextId(), now: nextNow(12_800) }); const running = await manager.createSession(makeInput({ status: 'running' })); const waiting = await manager.createSession(makeInput({ status: 'waiting_for_user' })); @@ -13729,7 +13677,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); let sessionId = ''; let outcomeCommitFailuresRemaining = 3; let outcomeCommitAttempts = 0; @@ -13862,7 +13810,7 @@ describe('SessionManager permission mode updates', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -13982,7 +13930,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]), ); const manager = new SessionManager({ @@ -14015,7 +13963,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]), ); const manager = new SessionManager({ @@ -14561,7 +14509,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]), ); const manager = new SessionManager({ @@ -14634,7 +14582,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]), ); const manager = new SessionManager({ @@ -14673,7 +14621,7 @@ describe('SessionManager permission mode updates', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); backends.register( - 'fake', + 'ai-sdk', (ctx) => new EventBackend(ctx, [{ type: 'complete', stopReason: 'end_turn' }]), ); const manager = new SessionManager({ @@ -14704,7 +14652,7 @@ describe('SessionManager steering and followup queues', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new FakeBackend(ctx)); + backends.register('ai-sdk', (ctx) => new FakeBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -14741,7 +14689,7 @@ describe('SessionManager steering and followup queues', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new FakeBackend(ctx)); + backends.register('ai-sdk', (ctx) => new FakeBackend(ctx)); const identities: RuntimeMessageRunIdentity[] = []; const acked: string[] = []; const nacked: string[] = []; @@ -14779,9 +14727,7 @@ describe('SessionManager steering and followup queues', () => { }, }, }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); const events = await drainAll( manager.sendMessage(session.id, { turnId: 'turn-host-message', text: 'start' }), @@ -14822,7 +14768,7 @@ describe('SessionManager steering and followup queues', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new FakeBackend(ctx)); + backends.register('ai-sdk', (ctx) => new FakeBackend(ctx)); const identities: RuntimeInteractionRunIdentity[] = []; const lifecycle: string[] = []; const manager = new SessionManager({ @@ -14848,9 +14794,7 @@ describe('SessionManager steering and followup queues', () => { }, canonicalPermissionOutcomes: noCanonicalPermissionOutcomes, }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); await drainAll( manager.sendMessage(session.id, { turnId: 'turn-host-interaction', text: 'go' }), @@ -14866,7 +14810,7 @@ describe('SessionManager steering and followup queues', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new FakeBackend(ctx)); + backends.register('ai-sdk', (ctx) => new FakeBackend(ctx)); const lifecycle: string[] = []; let question: RuntimeUserQuestionContinuation | undefined; const manager = new SessionManager({ @@ -14893,9 +14837,7 @@ describe('SessionManager steering and followup queues', () => { }, canonicalPermissionOutcomes: noCanonicalPermissionOutcomes, }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'ask' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'ask' })); const iterator = manager .sendMessage(session.id, { turnId: 'turn-host-question-abandoned', @@ -14920,7 +14862,7 @@ describe('SessionManager steering and followup queues', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new UnadmittedQuestionBackend(ctx)); + backends.register('ai-sdk', (ctx) => new UnadmittedQuestionBackend(ctx)); let releases = 0; const manager = new SessionManager({ store, @@ -14942,9 +14884,7 @@ describe('SessionManager steering and followup queues', () => { }, canonicalPermissionOutcomes: noCanonicalPermissionOutcomes, }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'ask' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'ask' })); let failure: unknown; try { @@ -14960,7 +14900,7 @@ describe('SessionManager steering and followup queues', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new ThrowBeforeTerminalBackend(ctx)); + backends.register('ai-sdk', (ctx) => new ThrowBeforeTerminalBackend(ctx)); let releases = 0; const manager = new SessionManager({ store, @@ -14981,9 +14921,7 @@ describe('SessionManager steering and followup queues', () => { }), }, }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); let failure: unknown; try { @@ -15002,7 +14940,7 @@ describe('SessionManager steering and followup queues', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let failBuilds = 1; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { if (failBuilds > 0) { failBuilds -= 1; throw new Error('backend build failed'); @@ -15017,9 +14955,7 @@ describe('SessionManager steering and followup queues', () => { newId: nextId(), now: nextNow(1_000), }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); let failed: unknown; try { @@ -15057,7 +14993,7 @@ describe('SessionManager steering and followup queues', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backend: GatedSteeringBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new GatedSteeringBackend(ctx); return backend; }); @@ -15069,9 +15005,7 @@ describe('SessionManager steering and followup queues', () => { newId: nextId(), now: nextNow(1_000), }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); const first = drainAll(manager.sendMessage(session.id, { turnId: 'turn-a', text: 'first' })); await waitUntil(() => backend?.gates.has('turn-a') === true); @@ -15306,7 +15240,7 @@ describe('SessionManager steering and followup queues', () => { const store = new MemorySessionStore(); const backends = new BackendRegistry(); let backend: GatedSteeringBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new GatedSteeringBackend(ctx); return backend; }); @@ -15316,9 +15250,7 @@ describe('SessionManager steering and followup queues', () => { newId: nextId(), now: nextNow(1_000), }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); const turn = drainAll(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'go' })); await waitUntil(() => backend?.gates.has('turn-1') === true); @@ -15468,7 +15400,7 @@ describe('SessionManager steering and followup queues', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new ForgingQueueBackend(ctx)); + backends.register('ai-sdk', (ctx) => new ForgingQueueBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -15477,9 +15409,7 @@ describe('SessionManager steering and followup queues', () => { newId: nextId(), now: nextNow(1_000), }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); const events = await drainAll( manager.sendMessage(session.id, { turnId: 'turn-1', text: 'go' }), @@ -15504,7 +15434,7 @@ describe('SessionManager steering and followup queues', () => { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new ProviderRetryProgressBackend(ctx)); + backends.register('ai-sdk', (ctx) => new ProviderRetryProgressBackend(ctx)); const manager = new SessionManager({ store, runStore, @@ -15513,9 +15443,7 @@ describe('SessionManager steering and followup queues', () => { newId: nextId(), now: nextNow(1_000), }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); const events = await drainAll( manager.sendMessage(session.id, { turnId: 'turn-1', text: 'go' }), @@ -15601,7 +15529,7 @@ describe('SessionManager steering and followup queues', () => { const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); let backend: GatedSteeringBackend | undefined; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new GatedSteeringBackend(ctx); return backend; }); @@ -15613,9 +15541,7 @@ describe('SessionManager steering and followup queues', () => { newId: nextId(), now: nextNow(1_000), }); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); const turn = drainAll(manager.sendMessage(session.id, { turnId: 'turn-1', text: 'go' })); await waitUntil(() => backend?.gates.has('turn-1') === true); @@ -15704,7 +15630,7 @@ async function steeringDeliverySession( const backends = new BackendRegistry(); let manager!: SessionManager; let sessionId = ''; - backends.register('fake', (ctx) => + backends.register('ai-sdk', (ctx) => createTestAiSdkBackend({ sessionId: ctx.sessionId, header: ctx.header, @@ -15743,9 +15669,7 @@ async function steeringDeliverySession( now: nextNow(1_000), }; manager = new SessionManager(managerDeps); - const session = await manager.createSession( - makeInput({ backend: 'fake', permissionMode: 'bypass' }), - ); + const session = await manager.createSession(makeInput({ permissionMode: 'bypass' })); sessionId = session.id; return { manager, session, store }; } @@ -15757,7 +15681,7 @@ async function steeringDeliverySession( * land after the final step boundary (stranded steering). */ class GatedSteeringBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; readonly gates = new Map(); readonly pullDone = new Map(); @@ -15955,7 +15879,7 @@ class DelegatingRuntimeKernel implements RuntimeKernelLike { } class UnadmittedQuestionBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor(ctx: BackendFactoryContext) { @@ -15987,7 +15911,7 @@ class UnadmittedQuestionBackend implements AgentBackend { } class TestBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; readonly sendInputs: BackendSendInput[] = []; stopCalls = 0; @@ -16279,7 +16203,7 @@ function compactHistoryFailOpenResult() { } class HighVolumeDeltaBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor( @@ -16316,7 +16240,7 @@ class HighVolumeDeltaBackend implements AgentBackend { } class LateErrorBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor( @@ -16357,7 +16281,7 @@ class LateErrorBackend implements AgentBackend { } class StopControlledAbortBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; private releaseAbort: () => void = () => {}; private releaseStop: () => void = () => {}; @@ -16419,7 +16343,7 @@ type PartialEvent = | Omit, 'id' | 'turnId' | 'ts'>; class TurnScriptBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; readonly sendInputs: BackendSendInput[] = []; @@ -16454,7 +16378,7 @@ class TurnScriptBackend implements AgentBackend { } class EventBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor( @@ -16483,7 +16407,7 @@ class EventBackend implements AgentBackend { } class SandboxBoundaryWaitBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; readonly responses: SandboxBoundaryResponse[] = []; private readonly responseGate = makeGate(); @@ -16548,7 +16472,7 @@ class SandboxBoundaryWaitBackend implements AgentBackend { * it into a control-only RuntimeEvent, and the ledger keeps it. */ class UnmappedSessionEventBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor(ctx: BackendFactoryContext) { @@ -16587,7 +16511,7 @@ class UnmappedSessionEventBackend implements AgentBackend { } class ThrowAfterTerminalBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor(ctx: BackendFactoryContext) { @@ -16619,7 +16543,7 @@ class ThrowAfterTerminalBackend implements AgentBackend { } class ThrowBeforeTerminalBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor(ctx: BackendFactoryContext) { @@ -16644,7 +16568,7 @@ class ThrowBeforeTerminalBackend implements AgentBackend { } class TraceBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor(private readonly ctx: BackendFactoryContext) { @@ -16698,7 +16622,7 @@ class TraceBackend implements AgentBackend { } class ProviderRequestTraceBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor(private readonly ctx: BackendFactoryContext) { @@ -16760,7 +16684,7 @@ class ProviderRequestTraceBackend implements AgentBackend { } class ProviderCaptureGateBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor( @@ -16801,7 +16725,7 @@ class ProviderCaptureGateBackend implements AgentBackend { } class ProviderCaptureAfterAttemptFailureBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor( @@ -16884,7 +16808,7 @@ class ProviderCaptureAfterAttemptFailureBackend implements AgentBackend { } class HistoryCompactCheckpointBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor(private readonly ctx: BackendFactoryContext) { @@ -16930,7 +16854,7 @@ class HistoryCompactCheckpointBackend implements AgentBackend { } class HistoryCompactCheckpointCacheProbeBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor( @@ -16983,7 +16907,7 @@ class HistoryCompactCheckpointCacheProbeBackend implements AgentBackend { } class HistoryCompactCheckpointMonotonicProbeBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor( @@ -17041,7 +16965,7 @@ class HistoryCompactCheckpointMonotonicProbeBackend implements AgentBackend { } class SameCoverageCheckpointReplacementProbeBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor( @@ -17097,7 +17021,7 @@ class SameCoverageCheckpointReplacementProbeBackend implements AgentBackend { } class SerializedCheckpointProbeBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor( @@ -17156,7 +17080,7 @@ class SerializedCheckpointProbeBackend implements AgentBackend { } class RecoveringCheckpointWriteProbeBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor( @@ -17220,7 +17144,7 @@ class RecoveringCheckpointWriteProbeBackend implements AgentBackend { } class CheckpointRecorderContractProbeBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor( @@ -17278,7 +17202,7 @@ class CheckpointRecorderContractProbeBackend implements AgentBackend { } class InitialCheckpointLoadRaceProbeBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor( @@ -17448,7 +17372,7 @@ class MemorySessionStore implements SessionStore { ...(input.revisionIndex !== undefined ? { revisionIndex: input.revisionIndex } : {}), ...(input.revisionState ? { revisionState: input.revisionState } : {}), hasUnread: false, - backend: input.backend, + backend: 'ai-sdk', llmConnectionSlug: input.llmConnectionSlug, connectionLocked: false, model: input.model ?? 'fake-model', @@ -18134,7 +18058,7 @@ class ContinuationClaimBarrierRunStore extends MemoryAgentRunStore { /** Yields a forged queue_update before completing — round-6 R3's attacker. */ class ForgingQueueBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor(ctx: BackendFactoryContext) { @@ -18175,7 +18099,7 @@ class ForgingQueueBackend implements AgentBackend { } class ProviderRetryProgressBackend implements AgentBackend { - readonly kind = 'fake' as const; + readonly kind = 'ai-sdk' as const; readonly sessionId: string; constructor(ctx: BackendFactoryContext) { @@ -18396,7 +18320,6 @@ function hostedRootAuthority(): RuntimeHostedRootAuthority { function makeInput(overrides: Partial = {}): CreateSessionInput { return { cwd: '/tmp/cwd', - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', @@ -18520,7 +18443,7 @@ async function createQueuedGraphScenario(firstAbortSignal?: AbortSignal) { const firstReady = makeGate(); const stopStarted = makeGate(); let backend!: TestBackend; - backends.register('fake', (ctx) => { + backends.register('ai-sdk', (ctx) => { backend = new (class extends TestBackend { override async stop( reason: 'user_stop' | 'redirect', @@ -18615,7 +18538,7 @@ function makeRunHeader(overrides: Partial = {}): AgentRunHeader sessionId: 'session-1', turnId: 'turn-1', status: 'running', - backendKind: 'fake', + backendKind: 'ai-sdk', llmConnectionSlug: 'fake', modelId: 'fake-model', cwd: '/tmp/cwd', @@ -18643,7 +18566,7 @@ function makeManagerForReadCutover( runStore: AgentRunStore & RuntimeEventStore, ): SessionManager { const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); return new SessionManager({ store, runStore, @@ -19029,7 +18952,7 @@ async function seedBoundaryRestartSession(input: { const store = new MemorySessionStore(); const runStore = new MemoryAgentRunStore(); const backends = new BackendRegistry(); - backends.register('fake', (ctx) => new TestBackend(ctx)); + backends.register('ai-sdk', (ctx) => new TestBackend(ctx)); const managerDeps = { store, runStore, diff --git a/packages/runtime/src/__tests__/shell-run-tool-result.test.ts b/packages/runtime/src/__tests__/shell-run-tool-result.test.ts index 4084c1917d..7578b6642c 100644 --- a/packages/runtime/src/__tests__/shell-run-tool-result.test.ts +++ b/packages/runtime/src/__tests__/shell-run-tool-result.test.ts @@ -116,7 +116,6 @@ describe('shell run sandbox denial projection', () => { const store = createSessionStore(root); const session = await store.create({ cwd: '/workspace', - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/runtime/src/__tests__/stream-graph-coordinator.test.ts b/packages/runtime/src/__tests__/stream-graph-coordinator.test.ts index aab0a61fac..208336dab3 100644 --- a/packages/runtime/src/__tests__/stream-graph-coordinator.test.ts +++ b/packages/runtime/src/__tests__/stream-graph-coordinator.test.ts @@ -258,7 +258,7 @@ describe('host-managed agent graph coordinator', () => { }, }); const backends = new BackendRegistry(); - backends.register('fake', (context) => new FakeBackend(context)); + backends.register('ai-sdk', (context) => new FakeBackend(context)); const manager = new SessionManager({ store: sessionStore, runStore, @@ -279,7 +279,6 @@ describe('host-managed agent graph coordinator', () => { try { const rootSession = await manager.createSession({ cwd: root, - backend: 'fake', llmConnectionSlug: 'fake', permissionMode: 'ask', name: 'Graph supervisor', @@ -1075,7 +1074,7 @@ describe('host-managed agent graph coordinator', () => { join(root, OPERATIONAL_STATE_DATABASE_NAME), ); const backends = new BackendRegistry(); - backends.register('fake', (context) => new FakeBackend(context)); + backends.register('ai-sdk', (context) => new FakeBackend(context)); const manager = new SessionManager({ store: sessionStore, runStore, @@ -1090,7 +1089,6 @@ describe('host-managed agent graph coordinator', () => { try { const rootSession = await manager.createSession({ cwd: root, - backend: 'fake', llmConnectionSlug: 'fake', permissionMode: 'ask', orchestrationMode: 'swarm', diff --git a/packages/runtime/src/model-fetcher.ts b/packages/runtime/src/model-fetcher.ts index 7a599438dc..2af026c8f1 100644 --- a/packages/runtime/src/model-fetcher.ts +++ b/packages/runtime/src/model-fetcher.ts @@ -164,7 +164,7 @@ async function fetchProviderModelsStrict( const definition = PROVIDER_DEFAULTS[connection.providerType]; // Unknown providerType → no discovery path. Throw a clear error (caught and // generalized by the caller) rather than crashing on `.modelDiscovery`. - // Mirrors `isFakeBackend` in @maka/core/connection-readiness.ts. + // Mirrors `isRealConnection` in @maka/core/connection-readiness.ts. if (!definition) { throw new Error(`Unknown provider type "${connection.providerType}"`); } diff --git a/packages/runtime/src/model-runtime.ts b/packages/runtime/src/model-runtime.ts index 6929c8fb13..39d6cec3b3 100644 --- a/packages/runtime/src/model-runtime.ts +++ b/packages/runtime/src/model-runtime.ts @@ -49,7 +49,7 @@ export function resolveModelRuntime( const defaults = PROVIDER_DEFAULTS[connection.providerType]; // Unknown providerType with no per-model override → can't resolve an adapter. // Throw a clear error rather than crashing on `.runtimeAdapter`. Mirrors - // `isFakeBackend` in @maka/core/connection-readiness.ts. + // `isRealConnection` in @maka/core/connection-readiness.ts. if (!override && !defaults) { throw new Error( `Unknown provider type "${connection.providerType}"; cannot resolve model runtime.`, diff --git a/packages/runtime/src/runtime-kernel.ts b/packages/runtime/src/runtime-kernel.ts index 23b612ffec..18ea7e1082 100644 --- a/packages/runtime/src/runtime-kernel.ts +++ b/packages/runtime/src/runtime-kernel.ts @@ -692,8 +692,8 @@ export class RuntimeKernel implements RuntimeKernelLike { store: this.deps.store, runStore: this.deps.runStore, runtimeEventStore: this.deps.runtimeEventStore, - ...(runtimeToolBoundaryProtocol(this.deps, header) - ? { toolBoundaryProtocol: runtimeToolBoundaryProtocol(this.deps, header) } + ...(this.deps.toolBoundaryProtocol + ? { toolBoundaryProtocol: this.deps.toolBoundaryProtocol } : {}), repairRunRuntimeLedger: this.deps.repairRunRuntimeLedger, newId: this.deps.newId, @@ -835,7 +835,7 @@ export class RuntimeKernel implements RuntimeKernelLike { ); } - const continuationToolBoundaryProtocol = runtimeToolBoundaryProtocol(this.deps, header); + const continuationToolBoundaryProtocol = this.deps.toolBoundaryProtocol; const run = new AgentRun({ sessionId: continuation.sessionId, header, @@ -1003,8 +1003,8 @@ export class RuntimeKernel implements RuntimeKernelLike { store: this.deps.store, runStore: this.deps.runStore, runtimeEventStore: this.deps.runtimeEventStore, - ...(runtimeToolBoundaryProtocol(this.deps, header) - ? { toolBoundaryProtocol: runtimeToolBoundaryProtocol(this.deps, header) } + ...(this.deps.toolBoundaryProtocol + ? { toolBoundaryProtocol: this.deps.toolBoundaryProtocol } : {}), repairRunRuntimeLedger: this.deps.repairRunRuntimeLedger, newId: this.deps.newId, @@ -1182,8 +1182,8 @@ export class RuntimeKernel implements RuntimeKernelLike { store: this.deps.store, runStore: this.deps.runStore, runtimeEventStore: this.deps.runtimeEventStore, - ...(runtimeToolBoundaryProtocol(this.deps, childHeader) - ? { toolBoundaryProtocol: runtimeToolBoundaryProtocol(this.deps, childHeader) } + ...(this.deps.toolBoundaryProtocol + ? { toolBoundaryProtocol: this.deps.toolBoundaryProtocol } : {}), repairRunRuntimeLedger: this.deps.repairRunRuntimeLedger, newId: this.deps.newId, @@ -1329,7 +1329,7 @@ export class RuntimeKernel implements RuntimeKernelLike { ); } await this.deps.continuationFailpoint?.('after_continuation_claim_committed'); - const continuationToolBoundaryProtocol = runtimeToolBoundaryProtocol(this.deps, childHeader); + const continuationToolBoundaryProtocol = this.deps.toolBoundaryProtocol; const durableAdmission: { claimedRunHeader: AgentRunHeader; commitContinuationStart: ( @@ -3752,13 +3752,6 @@ function childActiveKey(sessionId: string, turnId: string): string { return `${sessionId}:${turnId}`; } -function runtimeToolBoundaryProtocol( - deps: Pick, - header: Pick, -): ToolBoundaryProtocol | undefined { - return header.backend === 'ai-sdk' ? deps.toolBoundaryProtocol : undefined; -} - function effectiveOrchestrationForRun( run: AgentRunHeader, session: SessionHeader, diff --git a/packages/runtime/src/session-manager.ts b/packages/runtime/src/session-manager.ts index b14b96c9e9..fa04e7fa07 100644 --- a/packages/runtime/src/session-manager.ts +++ b/packages/runtime/src/session-manager.ts @@ -39,7 +39,7 @@ import type { UserMessage, PermissionDecisionMessage, SystemNoteMessage, - BackendKind, + PersistedBackendKind, } from '@maka/core/session'; import type { AgentSpec, @@ -700,7 +700,7 @@ export interface StrictRecoveryStores { } // ============================================================================ -// BackendRegistry — factory dispatch by BackendKind +// BackendRegistry — factory dispatch by the session header's durable backend // ============================================================================ export interface BackendFactoryContext { @@ -772,19 +772,19 @@ export interface BackendFactoryContext { export type BackendFactory = (ctx: BackendFactoryContext) => AgentBackend | Promise; export class BackendRegistry { - private readonly factories = new Map(); + private readonly factories = new Map(); - register(kind: BackendKind, factory: BackendFactory): void { + register(kind: PersistedBackendKind, factory: BackendFactory): void { this.factories.set(kind, factory); } - async build(kind: BackendKind, ctx: BackendFactoryContext): Promise { + async build(kind: PersistedBackendKind, ctx: BackendFactoryContext): Promise { const f = this.factories.get(kind); if (!f) throw new Error(`No backend factory registered for kind="${kind}"`); return await f(ctx); } - has(kind: BackendKind): boolean { + has(kind: PersistedBackendKind): boolean { return this.factories.has(kind); } } @@ -2550,7 +2550,6 @@ export class SessionManager { cwd: workspace?.worktreePath ?? parentHeader.cwd, ...(parentHeader.projectId !== undefined ? { projectId: parentHeader.projectId } : {}), name: resolvedPreset?.name ?? definition.name, - backend: parentHeader.backend, llmConnectionSlug: resolvedPreset?.connectionSlug ?? parentHeader.llmConnectionSlug, model: resolvedPreset?.model ?? parentHeader.model, ...(resolvedPreset @@ -3089,7 +3088,6 @@ export class SessionManager { cwd: workspace?.worktreePath ?? parentHeader.cwd, ...(parentHeader.projectId !== undefined ? { projectId: parentHeader.projectId } : {}), name: input.name ?? input.resolvedPreset?.name ?? definition.name, - backend: parentHeader.backend, llmConnectionSlug: input.resolvedPreset?.connectionSlug ?? parentHeader.llmConnectionSlug, model: input.resolvedPreset?.model ?? parentHeader.model, ...(input.resolvedPreset @@ -4953,7 +4951,6 @@ export class SessionManager { { cwd: header.cwd, ...(header.projectId !== undefined ? { projectId: header.projectId } : {}), - backend: header.backend, llmConnectionSlug: header.llmConnectionSlug, model: header.model, thinkingLevel: header.thinkingLevel, @@ -5016,7 +5013,6 @@ export class SessionManager { { cwd: header.cwd, ...(header.projectId !== undefined ? { projectId: header.projectId } : {}), - backend: header.backend, llmConnectionSlug: header.llmConnectionSlug, model: header.model, thinkingLevel: header.thinkingLevel, diff --git a/packages/runtime/src/test-connection.ts b/packages/runtime/src/test-connection.ts index 723e347962..07277741c1 100644 --- a/packages/runtime/src/test-connection.ts +++ b/packages/runtime/src/test-connection.ts @@ -129,7 +129,7 @@ async function testConnectionStrict( ): Promise { const defaults = PROVIDER_DEFAULTS[connection.providerType]; // Unknown providerType → can't pick an auth path or fallback model. Return a - // clear failure rather than crashing. Mirrors `isFakeBackend`. + // clear failure rather than crashing. Mirrors `isRealConnection`. if (!defaults) { return { ok: false, errorMessage: `Unknown provider type "${connection.providerType}"` }; } diff --git a/packages/runtime/src/test-only/fake-backend.ts b/packages/runtime/src/test-only/fake-backend.ts index 8bbb9552b6..0a8df15488 100644 --- a/packages/runtime/src/test-only/fake-backend.ts +++ b/packages/runtime/src/test-only/fake-backend.ts @@ -1,5 +1,5 @@ import { randomUUID } from 'node:crypto'; -import type { BackendKind, SessionHeader, StoredMessage } from '@maka/core/session'; +import type { PersistedBackendKind, SessionHeader, StoredMessage } from '@maka/core/session'; import type { SessionEvent } from '@maka/core/events'; import type { AgentBackend, @@ -46,7 +46,7 @@ type PendingSandboxBoundary = { }; export class FakeBackend implements AgentBackend { - readonly kind: BackendKind = 'fake'; + readonly kind: PersistedBackendKind = 'fake'; readonly sessionId: string; private stopped = false; private pendingQuestion: PendingQuestion | undefined; diff --git a/packages/storage/src/__tests__/external-session-importer.test.ts b/packages/storage/src/__tests__/external-session-importer.test.ts index dc849c0849..565368057f 100644 --- a/packages/storage/src/__tests__/external-session-importer.test.ts +++ b/packages/storage/src/__tests__/external-session-importer.test.ts @@ -150,7 +150,6 @@ describe('ExternalSessionImporter', () => { function target(overrides: Partial = {}): ExternalSessionImportTarget { return { - backend: 'fake', llmConnectionSlug: 'fake', model: 'maka-model', permissionMode: 'ask', diff --git a/packages/storage/src/__tests__/goal-authority.test.ts b/packages/storage/src/__tests__/goal-authority.test.ts index 662e31a62c..1be86d8957 100644 --- a/packages/storage/src/__tests__/goal-authority.test.ts +++ b/packages/storage/src/__tests__/goal-authority.test.ts @@ -117,7 +117,6 @@ test('Session retirement atomically removes its Goal authority', async () => { ['archive', 'remove'].map((name) => stores.sessionStore.create({ cwd: capability.canonicalPath, - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/storage/src/__tests__/operational-state-backup.test.ts b/packages/storage/src/__tests__/operational-state-backup.test.ts index 3502fb2f0d..3c88be9674 100644 --- a/packages/storage/src/__tests__/operational-state-backup.test.ts +++ b/packages/storage/src/__tests__/operational-state-backup.test.ts @@ -36,7 +36,6 @@ test('backs up and restores runtime.sqlite plus artifact bytes', async () => { const session = await sessions.create({ projectId: project.id, cwd: '/tmp/cwd', - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask', diff --git a/packages/storage/src/__tests__/session-bundle-policy.test.ts b/packages/storage/src/__tests__/session-bundle-policy.test.ts index 206eddb94b..9e833dfcb5 100644 --- a/packages/storage/src/__tests__/session-bundle-policy.test.ts +++ b/packages/storage/src/__tests__/session-bundle-policy.test.ts @@ -141,7 +141,6 @@ test('retains only the selected Session partial stream segments', async () => { function input(name: string): CreateSessionInput { return { cwd: '/tmp/cwd', - backend: 'fake', llmConnectionSlug: 'fake', model: 'fake-model', permissionMode: 'ask' as const, diff --git a/packages/storage/src/__tests__/session-store.test.ts b/packages/storage/src/__tests__/session-store.test.ts index ce45685b92..87d853d0d1 100644 --- a/packages/storage/src/__tests__/session-store.test.ts +++ b/packages/storage/src/__tests__/session-store.test.ts @@ -999,6 +999,55 @@ describe('SQLite SessionStore', () => { } }); + test('reads back a legacy fake-backend session instead of migrating or rejecting it', async () => { + // #3211: `'fake'` was retired as a live backend but never migrated out of + // storage. Narrowing the header validator would make these rows decode as + // malformed and rewriting them to `'ai-sdk'` would make an unrunnable task + // look runnable, since `llmConnectionSlug` still points at nothing. + // + // The legacy row is seeded under the writer, not through `create`: `'fake'` + // is a value only an older build could write, so a test that asks today's + // creation path for one would be asserting a write that must not exist. + const root = await mkdtemp(join(tmpdir(), 'maka-session-legacy-fake-')); + const store = createSessionStore(root); + let sessionId: string; + try { + sessionId = (await store.create(makeInput())).id; + } finally { + await store.close?.(); + } + + const legacy = new DatabaseSync(join(root, OPERATIONAL_STATE_DATABASE_NAME)); + try { + const row = legacy + .prepare(`SELECT payload_json FROM session_metadata WHERE session_id = ?`) + .get(sessionId) as { payload_json: string }; + const payload = JSON.parse(row.payload_json) as Record; + payload.backend = 'fake'; + payload.llmConnectionSlug = 'fake'; + legacy + .prepare( + `UPDATE session_metadata + SET payload_json = ?, backend = ?, llm_connection_slug = ? + WHERE session_id = ?`, + ) + .run(JSON.stringify(payload), 'fake', 'fake', sessionId); + } finally { + legacy.close(); + } + + const reopened = createSessionStore(root); + try { + const [header] = await reopened.listHeaders(); + assert.equal(header?.backend, 'fake'); + assert.equal(header?.llmConnectionSlug, 'fake'); + assert.equal((await reopened.readHeaderSnapshot(sessionId)).backend, 'fake'); + } finally { + await reopened.close?.(); + await rm(root, { recursive: true, force: true }); + } + }); + test('deletes metadata and messages through the same transaction boundary', async () => { const root = await mkdtemp(join(tmpdir(), 'maka-session-delete-')); const store = createSessionStore(root); @@ -1030,9 +1079,8 @@ describe('SQLite SessionStore', () => { function makeInput(overrides: Partial = {}): CreateSessionInput { return { cwd: '/tmp/cwd', - backend: 'fake', - llmConnectionSlug: 'fake', - model: 'fake-model', + llmConnectionSlug: 'test-connection', + model: 'test-model', permissionMode: 'ask', name: 'Session', labels: [], diff --git a/packages/storage/src/__tests__/settings-store-usage.test.ts b/packages/storage/src/__tests__/settings-store-usage.test.ts index d1cfc90247..7f88b83cef 100644 --- a/packages/storage/src/__tests__/settings-store-usage.test.ts +++ b/packages/storage/src/__tests__/settings-store-usage.test.ts @@ -13,7 +13,6 @@ async function seedSession(workspaceRoot: string, messages: StoredMessage[]): Pr try { const header = await sessions.create({ cwd: '/tmp/maka-workspace', - backend: 'ai-sdk', llmConnectionSlug: 'anthropic', model: 'claude-sonnet-4', permissionMode: 'ask', @@ -77,7 +76,6 @@ describe('SettingsStore.usageStats request logs', () => { try { const header = await sessions.create({ cwd: '/tmp/maka-workspace', - backend: 'ai-sdk', llmConnectionSlug: 'sqlite-provider', model: 'sqlite-default-model', permissionMode: 'ask', diff --git a/packages/storage/src/session-store.ts b/packages/storage/src/session-store.ts index 3664a3fa32..4dc5e36d5b 100644 --- a/packages/storage/src/session-store.ts +++ b/packages/storage/src/session-store.ts @@ -1036,7 +1036,7 @@ function buildSessionHeader( ...(input.revisionIndex !== undefined ? { revisionIndex: input.revisionIndex } : {}), ...(input.revisionState ? { revisionState: input.revisionState } : {}), hasUnread: false, - backend: input.backend, + backend: 'ai-sdk', llmConnectionSlug: input.llmConnectionSlug, connectionLocked: false, model: input.model ?? 'default', @@ -1090,7 +1090,7 @@ export function normalizeSessionHeader( isValidSessionExternalOrigin(header.externalOrigin) && (header.lastReadMessageId === undefined || typeof header.lastReadMessageId === 'string') && typeof header.hasUnread === 'boolean' && - isBackendKind(header.backend) && + isPersistedBackendKind(header.backend) && typeof header.llmConnectionSlug === 'string' && typeof header.connectionLocked === 'boolean' && typeof header.model === 'string' && @@ -1220,7 +1220,12 @@ function isValidSubagentSessionLineage(header: SessionHeader): boolean { ); } -function isBackendKind(value: unknown): value is SessionHeader['backend'] { +/** + * Decode guard for a durable session header. `'fake'` stays accepted: + * narrowing it here would make every session written by a build that shipped + * FakeBackend fail `normalizeSessionHeader` and read back as malformed (#3211). + */ +function isPersistedBackendKind(value: unknown): value is SessionHeader['backend'] { return value === 'ai-sdk' || value === 'fake'; }