From 50e9b17ff73267f0457e3f6a2bc57722cc052a18 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sun, 23 Aug 2026 20:47:18 +0800 Subject: [PATCH 1/3] fix: expose active model in stable prompts Prepend the selected model at the Runtime Host execution boundary after final target resolution. This keeps interactive composers model-agnostic while ensuring main, child, and headless provider requests share one stable execution identity. Generated-by: Maka --- .../execution-model-composition.test.ts | 20 +++++++++---------- .../src/server/execution-model-composition.ts | 7 ++++++- 2 files changed, 16 insertions(+), 11 deletions(-) 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 af100b503c..38ede3a6f1 100644 --- a/packages/runtime-host/src/__tests__/execution-model-composition.test.ts +++ b/packages/runtime-host/src/__tests__/execution-model-composition.test.ts @@ -134,7 +134,7 @@ const MIN_IMPLEMENTATION_CHILD_REQUESTS = 6; const MAX_IMPLEMENTATION_CHILD_REQUESTS = MIN_IMPLEMENTATION_CHILD_REQUESTS + MAX_IMPLEMENTATION_CHILD_PTY_READS - 1; const HEADLESS_CODING_V1_PROMPT_HASH = - 'sha256:0e3389e330b8b8f0db1c7a8b8e2126325fe4c672d6eff279afcd3f9412e52271'; + 'sha256:b2773282ac4755dc8d8a663eafdec68c3fa6f5680ec8557d261b5f723672b467'; const HEADLESS_CODING_V1_TOOLS_HASH = 'sha256:c062194603f93b568da5ca59b865b316156b5f218ba854c291aa9582859b3de4'; const execFileAsync = promisify(execFile); @@ -920,6 +920,7 @@ test('hosted execution freezes the headless coding provider wire contract', asyn const instructions = responsesDeveloperPrompt(request?.body); const tools = request?.body.tools; assert.equal(typeof instructions, 'string', JSON.stringify(request?.body)); + assert.match(instructions ?? '', /^Active model: deepseek-v4-flash$/mu); assert.ok(Array.isArray(tools)); assert.equal(stableHash(instructions), HEADLESS_CODING_V1_PROMPT_HASH); assert.equal(stableHash(tools), HEADLESS_CODING_V1_TOOLS_HASH); @@ -2831,15 +2832,14 @@ test('backend composition survives a moved saved Git Bash executable while Bash const capturedBash = childComposer.tools.find((tool) => tool.name === 'Bash'); assert.match(capturedBash?.description ?? '', /captured child shell/); assert.doesNotMatch(capturedBash?.description ?? '', /unavailable this turn/); - assert.match( - await childComposer.turnTailPrompt({ - sessionId: 'session', - turnId: 'turn-child', - cwd: '/workspace', - workspaceRoot: '/workspace', - }), - /captured child shell/, - ); + const childContext = { + sessionId: 'session', + turnId: 'turn-child', + cwd: '/workspace', + workspaceRoot: '/workspace', + } as const; + const childTail = await childComposer.turnTailPrompt(childContext); + assert.match(childTail, /captured child shell/); }); test('child execution Bash carries the configured shell guidance and spawn plan', async () => { diff --git a/packages/runtime-host/src/server/execution-model-composition.ts b/packages/runtime-host/src/server/execution-model-composition.ts index 015e9b6442..a18e6f2930 100644 --- a/packages/runtime-host/src/server/execution-model-composition.ts +++ b/packages/runtime-host/src/server/execution-model-composition.ts @@ -288,7 +288,7 @@ export async function createHostAiSdkBackend(input: HostAiSdkBackendInput): Prom readonly runId?: string; readonly emitSkillCatalogTrace?: (message: string, data?: Record) => void; }) => { - return await modelComposition.resolveSystemPrompt({ + const resolved = await modelComposition.resolveSystemPrompt({ sessionId: input.context.sessionId, turnId: context.turnId, ...(context.runId ? { runId: context.runId } : {}), @@ -298,6 +298,11 @@ export async function createHostAiSdkBackend(input: HostAiSdkBackendInput): Prom ? { emitSkillCatalogTrace: context.emitSkillCatalogTrace } : {}), }); + const model = target.model.replace(/[\r\n\t]+/g, ' ').trim(); + return Object.freeze({ + ...resolved, + text: [`Active model: ${model}`, resolved.text].filter(Boolean).join('\n\n'), + }); }; const recordRunComposition = input.context.recordRunComposition; const commitRunComposition = recordRunComposition From 62644cbcae9cfe325e7d69de2b6bbc408fbe3b80 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sun, 23 Aug 2026 21:05:04 +0800 Subject: [PATCH 2/3] refactor: collapse model switch transcript projection Return the latest assistant model directly instead of maintaining an unused conversation-presence field and object wrapper. The renderer only needs the latest actual model for switch feedback. Generated-by: Maka --- .../app-shell-session-settings-actions.ts | 6 ++--- packages/cli/src/pi-tui-runner.ts | 6 ++--- .../__tests__/model-switch-transcript.test.ts | 15 +++++-------- packages/core/src/session.ts | 22 ++++--------------- 4 files changed, 15 insertions(+), 34 deletions(-) diff --git a/apps/desktop/src/renderer/app-shell-session-settings-actions.ts b/apps/desktop/src/renderer/app-shell-session-settings-actions.ts index 0c4f37d782..2db4f149ab 100644 --- a/apps/desktop/src/renderer/app-shell-session-settings-actions.ts +++ b/apps/desktop/src/renderer/app-shell-session-settings-actions.ts @@ -21,7 +21,7 @@ import type { ChatDefaultPermissionMode } from '@maka/core/settings'; import type { LlmConnection } from '@maka/core/llm-connections'; import type { PermissionMode } from '@maka/core/permission'; import { - deriveModelSwitchTranscript, + latestAssistantModelId, type StoredMessage, } from '@maka/core/session'; import type { ThinkingLevel } from '@maka/core/model-thinking'; @@ -177,7 +177,7 @@ export function createAppShellSessionSettingsActions(deps: { const sessionId = activeIdRef.current; if (!sessionId) return; const previous = sessionsRef.current.find((session) => session.id === sessionId); - const transcript = deriveModelSwitchTranscript(messages); + const lastUsedModel = latestAssistantModelId(messages); if (pendingSessionModelChangesRef.current.has(sessionId)) return; pendingSessionModelChangesRef.current.add(sessionId); setPendingSessionModelBySession((current) => ({ @@ -190,7 +190,7 @@ export function createAppShellSessionSettingsActions(deps: { if (activeIdRef.current === sessionId) { const connectionChanged = previous?.llmConnectionSlug !== next.llmConnectionSlug; const to = modelEndpointLabel(next.llmConnectionSlug, next.model, connectionChanged); - const previousModel = transcript.lastUsedModel ?? previous?.model; + const previousModel = lastUsedModel ?? previous?.model; toastApi.success( copy.modelSwitchedTitle, previous && previousModel diff --git a/packages/cli/src/pi-tui-runner.ts b/packages/cli/src/pi-tui-runner.ts index 939c56b720..478c66411d 100644 --- a/packages/cli/src/pi-tui-runner.ts +++ b/packages/cli/src/pi-tui-runner.ts @@ -48,7 +48,7 @@ import { } from '@maka/core/slash-command-catalog'; import { type QueueEnqueueOutcome, type ShellRunUpdate } from '@maka/core/events'; import { - deriveModelSwitchTranscript, + latestAssistantModelId, type SessionSummary, type StoredMessage, } from '@maka/core/session'; @@ -1462,7 +1462,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { const setModel = async (nextModel: string) => { if (nextModel === model) return; - const previousModel = deriveModelSwitchTranscript(transcriptMessages).lastUsedModel ?? model; + const previousModel = latestAssistantModelId(transcriptMessages) ?? model; await input.driver.setModel(nextModel); model = nextModel; // Same-connection switch: scope the choice lookup to the live connection @@ -1488,7 +1488,7 @@ export async function runMakaPiTui(input: MakaPiTuiInput): Promise { // Updates the provider (and thus the thinking variants) and the status line. const setModelChoice = async (choice: ModelChoice) => { if (choice.model === model && choice.connectionSlug === connectionSlug) return; - const previousModel = deriveModelSwitchTranscript(transcriptMessages).lastUsedModel ?? model; + const previousModel = latestAssistantModelId(transcriptMessages) ?? model; const previousConnectionSlug = connectionSlug; const previousChoice = modelChoices?.find( (candidate) => diff --git a/packages/core/src/__tests__/model-switch-transcript.test.ts b/packages/core/src/__tests__/model-switch-transcript.test.ts index 2a67f5e75e..f7e8613102 100644 --- a/packages/core/src/__tests__/model-switch-transcript.test.ts +++ b/packages/core/src/__tests__/model-switch-transcript.test.ts @@ -19,10 +19,10 @@ import { strict as assert } from 'node:assert'; import { describe, it } from 'node:test'; -import { deriveModelSwitchTranscript, type StoredMessage } from '../session.js'; +import { latestAssistantModelId, type StoredMessage } from '../session.js'; -describe('deriveModelSwitchTranscript', () => { - it('treats any durable transcript as an existing conversation', () => { +describe('latestAssistantModelId', () => { + it('returns undefined until an assistant response exists', () => { const messages: StoredMessage[] = [ { type: 'user', @@ -33,9 +33,7 @@ describe('deriveModelSwitchTranscript', () => { }, ]; - assert.deepEqual(deriveModelSwitchTranscript(messages), { - hasConversation: true, - }); + assert.equal(latestAssistantModelId(messages), undefined); }); it('uses the latest assistant model as the actual baseline', () => { @@ -58,9 +56,6 @@ describe('deriveModelSwitchTranscript', () => { }, ]; - assert.deepEqual(deriveModelSwitchTranscript(messages), { - hasConversation: true, - lastUsedModel: 'model-b', - }); + assert.equal(latestAssistantModelId(messages), 'model-b'); }); }); diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index d9fc065994..985d1f3c5b 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -1205,27 +1205,13 @@ function isToolActivityIdentity(value: Record): boolean { export const STEP_LIMIT_NOTICE_TEXT = 'Reached the configured step limit. The task may be incomplete. Send “continue” to resume.'; -/** - * View-boundary facts for explaining a model selection without adding a - * second persisted model authority. Assistant rows already record the actual - * model used by each completed step, so the latest such row is the only - * durable model fact the switcher needs. - */ -export function deriveModelSwitchTranscript(messages: readonly StoredMessage[]): { - hasConversation: boolean; - lastUsedModel?: string; -} { - let lastUsedModel: string | undefined; +/** Latest actual model recorded by a completed assistant step. */ +export function latestAssistantModelId(messages: readonly StoredMessage[]): string | undefined { for (let index = messages.length - 1; index >= 0; index -= 1) { const message = messages[index]; - if (message?.type !== 'assistant') continue; - lastUsedModel = message.modelId; - break; + if (message?.type === 'assistant') return message.modelId; } - return { - hasConversation: messages.length > 0, - ...(lastUsedModel ? { lastUsedModel } : {}), - }; + return undefined; } export function deriveTurnRecords(messages: readonly StoredMessage[]): TurnRecord[] { From e7eab011b24907155f746bafe471e5987dca4709 Mon Sep 17 00:00:00 2001 From: AstroHan Date: Sun, 23 Aug 2026 21:09:29 +0800 Subject: [PATCH 3/3] refactor: trim session change invalidation payload Session change events invalidate renderer projections and correlate turn lifecycle changes. Remove the unused connection slug field and stop attaching model details to ordinary model-update invalidations; modelId remains only for rebound feedback. Generated-by: Maka --- apps/desktop/src/main/runtime-host-boot.ts | 6 ++---- .../src/main/runtime-host-desktop-candidate.ts | 4 ++-- .../main/runtime-host-session-catalog-ipc-main.ts | 12 +++--------- packages/core/src/session.ts | 2 -- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index 333dbe303d..a17900318b 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -941,7 +941,7 @@ function registerHostClientIpc( const emitTargetSessionsChanged = ( reason: SessionChangedReason, sessionId?: string, - extra?: Pick, + extra?: Pick, ): void => { if (isTargetActive()) emitSessionsChanged(scope, reason, sessionId, extra); }; @@ -1461,14 +1461,12 @@ function emitSessionsChanged( scope: DesktopTargetScope, reason: SessionChangedReason, sessionId?: string, - extra?: Pick, + extra?: Pick, ): void { const event: SessionChangedEvent = { - type: "sessions_changed", reason, ts: Date.now(), ...(sessionId ? { sessionId } : {}), - ...(extra?.connectionSlug ? { connectionSlug: extra.connectionSlug } : {}), ...(extra?.modelId ? { modelId: extra.modelId } : {}), ...(extra?.turnId ? { turnId: extra.turnId } : {}), }; diff --git a/apps/desktop/src/main/runtime-host-desktop-candidate.ts b/apps/desktop/src/main/runtime-host-desktop-candidate.ts index c6e9164e20..7d8f2027de 100644 --- a/apps/desktop/src/main/runtime-host-desktop-candidate.ts +++ b/apps/desktop/src/main/runtime-host-desktop-candidate.ts @@ -103,7 +103,7 @@ export interface DesktopRuntimeHostCandidateDeps { scope: DesktopTargetScope, reason: SessionChangedReason, sessionId?: string, - extra?: Pick, + extra?: Pick, ) => void; readonly completeComputerUseTurn: ( sessionId: string, @@ -366,7 +366,7 @@ export async function createDesktopRuntimeHostCandidate( const emitSessionsChanged = ( reason: SessionChangedReason, sessionId?: string, - extra?: Pick, + extra?: Pick, ): void => { if (isTargetActive()) deps.emitSessionsChanged(scope, reason, sessionId, extra); }; 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 a146d86ecf..cf5c147c9c 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 @@ -70,7 +70,7 @@ export interface RuntimeHostSessionCatalogIpcDeps { emitSessionsChanged: ( reason: SessionChangedReason, sessionId?: string, - extra?: Pick, + extra?: Pick, ) => void; releaseSessionResources: (sessionId: string) => void | Promise; sessionCopyCleanup: SessionCopyCleanupAuthority; @@ -199,13 +199,7 @@ export function registerRuntimeHostSessionCatalogIpc( ); ipcMain.handle('sessions:setModel', async (_event, sessionId: string, input: unknown) => { const modelTarget = normalizeExplicitModel(input); - return updateConfiguration( - deps, - sessionId, - { modelTarget, thinkingLevel: null }, - 'updated', - { connectionSlug: modelTarget.connectionSlug, modelId: modelTarget.model }, - ); + return updateConfiguration(deps, sessionId, { modelTarget, thinkingLevel: null }, 'updated'); }); ipcMain.handle('sessions:setThinkingLevel', async (_event, sessionId: string, level: unknown) => { if (level !== undefined && level !== null && !isThinkingLevel(level)) { @@ -263,7 +257,7 @@ async function updateConfiguration( sessionId: string, patch: DesktopSessionConfigurationPatch, reason: SessionChangedReason, - extra?: Pick, + extra?: Pick, ): Promise { const session = await deps.client.updateSessionConfiguration(sessionId, patch); deps.emitSessionsChanged(reason, sessionId, extra); diff --git a/packages/core/src/session.ts b/packages/core/src/session.ts index 985d1f3c5b..218a447a73 100644 --- a/packages/core/src/session.ts +++ b/packages/core/src/session.ts @@ -685,10 +685,8 @@ export type SessionChangedReason = | 'rebound'; export interface SessionChangedEvent { - type: 'sessions_changed'; reason: SessionChangedReason; sessionId?: string; - connectionSlug?: string; modelId?: string; /** * The turn this change is ABOUT, when the change has a turn to name.