diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-option-settlement.test.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-option-settlement.test.ts index 41054d10ece..5b4f0556915 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-option-settlement.test.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-option-settlement.test.ts @@ -3,7 +3,10 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from 'vitest' import { computeAgentSessionPayloadFingerprint } from '../../../shared/agent-session-mutation-envelope' -import type { AgentSessionMutationEnvelope } from '../../../shared/agent-session-wire' +import type { + AgentSessionMutationEnvelope, + AgentSessionStatusEvent +} from '../../../shared/agent-session-wire' import { AgentSessionRecordStore } from '../../runtime/agent-session-record-store' import type { StructuredAgentSessionAdapter } from './structured-agent-session-adapter' import { AgentSessionOptionRejectedError } from './structured-agent-session-option-error' @@ -212,6 +215,39 @@ afterEach(async () => { }) describe('structured session options and close', () => { + it('publishes an acknowledged model without waiting for journal traffic', async () => { + const body = { + kind: 'message' as const, + role: 'user' as const, + blocks: [{ type: 'text' as const, text: 'first task' }] + } + await host.send(CALLER, { + envelope: envelope('agentSession.send', { body }), + body + }) + const events: AgentSessionStatusEvent[] = [] + host.subscribeStatus({ id: 'session-list', emit: (event) => events.push(event) }) + expect(events).toEqual([ + { + type: 'snapshot', + sessions: [expect.objectContaining({ status: 'idle', model: DEFAULT_MODEL })] + } + ]) + const fields = { key: 'model', value: PICKED_MODEL } + + await host.setOption(CALLER, { + envelope: envelope('agentSession.setOption', fields), + ...fields + }) + + expect(events.slice(1)).toEqual([ + { + type: 'status', + session: expect.objectContaining({ sessionId: SESSION, model: PICKED_MODEL }) + } + ]) + }) + it('settles a pre-mutation rejection so a fresh retry can succeed', async () => { optionFailure = new AgentSessionOptionRejectedError('model list unavailable') const fields = { key: 'model', value: PICKED_MODEL } diff --git a/src/main/native-chat/agent-session-wire/structured-agent-session-turns-options.ts b/src/main/native-chat/agent-session-wire/structured-agent-session-turns-options.ts index 68e5b290847..cb1685405a1 100644 --- a/src/main/native-chat/agent-session-wire/structured-agent-session-turns-options.ts +++ b/src/main/native-chat/agent-session-wire/structured-agent-session-turns-options.ts @@ -23,5 +23,6 @@ export async function performSetOption( throw error } await ctx.persistOptions(applied ?? { [input.key]: input.value }) + ctx.publish() return { ok: true, value: { ...input, ...(applied ? { options: { ...applied } } : {}) } } } diff --git a/src/shared/structured-agent-session-projection.test.ts b/src/shared/structured-agent-session-projection.test.ts index 9e34e578233..08d4de2fa0c 100644 --- a/src/shared/structured-agent-session-projection.test.ts +++ b/src/shared/structured-agent-session-projection.test.ts @@ -86,16 +86,16 @@ describe('structured agent session status projection', () => { role: 'user', blocks: [{ type: 'text', text: 'look at the sidebar' }] }) - const said = item('said', 2, { - kind: 'message', - role: 'assistant', - blocks: [{ type: 'text', text: 'Reading the card first.' }] - }) - const running = item('running', 3, { + const running = item('running', 2, { kind: 'status', text: 'Working', turnLifecycle: { turnId: 'turn-1', state: 'running' } }) + const said = item('said', 3, { + kind: 'message', + role: 'assistant', + blocks: [{ type: 'text', text: 'Reading the card first.' }] + }) const tool = item('tool', 4, { kind: 'tool-call', name: 'Read', @@ -103,7 +103,7 @@ describe('structured agent session status projection', () => { state: 'running' }) - expect(projectStructuredAgentSessionStatusSummary([ask, said, running, tool])).toEqual({ + expect(projectStructuredAgentSessionStatusSummary([ask, running, said, tool])).toEqual({ status: 'working', latestPrompt: 'look at the sidebar', toolName: 'Read', @@ -112,6 +112,30 @@ describe('structured agent session status projection', () => { }) }) + it('clears the previous answer as soon as the next prompt is persisted', () => { + const firstAsk = item('first-ask', 1, { + kind: 'message', + role: 'user', + blocks: [{ type: 'text', text: 'first task' }] + }) + const previousAnswer = item('previous-answer', 2, { + kind: 'message', + role: 'assistant', + blocks: [{ type: 'text', text: 'The first task is done.' }] + }) + const nextAsk = item('next-ask', 3, { + kind: 'message', + role: 'user', + blocks: [{ type: 'text', text: 'second task' }] + }) + expect(projectStructuredAgentSessionStatusSummary([firstAsk, previousAnswer, nextAsk])).toEqual( + { + status: 'idle', + latestPrompt: 'second task' + } + ) + }) + it('reports no tool line once the turn settles, even with an abandoned running call', () => { const ask = item('ask', 1, { kind: 'message', diff --git a/src/shared/structured-agent-session-projection.ts b/src/shared/structured-agent-session-projection.ts index fec044a08bc..7c55f2b8379 100644 --- a/src/shared/structured-agent-session-projection.ts +++ b/src/shared/structured-agent-session-projection.ts @@ -192,13 +192,16 @@ export function latestStructuredAgentSessionPrompt( return '' } -/** The newest assistant prose. Tool-only assistant items are skipped — they carry - * no words, and stopping at one would blank a row that has something to say. */ +/** The newest assistant prose in the latest user turn. Tool-only assistant items + * are skipped; the user boundary clears prose from the preceding turn. */ export function latestStructuredAgentSessionAssistantMessage( items: readonly AgentJournalRenderItem[] ): string { for (let index = items.length - 1; index >= 0; index -= 1) { const body = items[index]?.body + if (body?.kind === 'message' && body.role === 'user') { + return '' + } if (body?.kind === 'message' && body.role === 'assistant') { const prose = messageProse(body.blocks) if (prose.trim()) {