fix: keep structured session status current

This commit is contained in:
Merge Sim
2026-09-06 15:37:37 -07:00
parent 25b9ba9536
commit 346c8e4030
4 changed files with 74 additions and 10 deletions
@@ -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 }
@@ -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 } } : {}) } }
}
@@ -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',
@@ -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()) {