Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,82 @@ describe('router decision identity', () => {
})

describe('appendEnsembleProgress', () => {
it('promotes a same-turn provisional card when its real decision arrives', () => {
const { runtime, messagesRef } = makeRuntime([
{ role: 'user', text: 'q', ts: 0, turnId: 'turn-1' },
])

runtime.appendEnsembleProgress({
event_type: 'proposer_finish',
turn_id: 'turn-1',
proposer_provider: 'openrouter',
proposer_model: 'qwen/qwen3.7-plus',
})
const provisional = messagesRef.value.find(message => message.role === 'router')!
;(provisional as ChatMessage & { routerExecutionModel?: string }).routerExecutionModel = 'fallback-model'

runtime.queueRouterDecision({
turn_id: 'turn-1',
stream_seq: 12,
tier: 'c1',
model: 'provider/selected',
source: 'squilla_router',
})

const routers = messagesRef.value.filter(message => message.role === 'router')
expect(routers).toHaveLength(1)
expect(routers[0]).toMatchObject({
messageId: 'router-sess-12',
turnId: 'turn-1',
routerDecision: { model: 'provider/selected' },
})
expect(routers[0]?.ensemble?.models).toHaveLength(1)
expect((routers[0] as ChatMessage & { routerExecutionModel?: string }).routerExecutionModel)
.toBe('fallback-model')
})

it('does not promote a provisional card across a router control replay boundary', () => {
const { runtime, messagesRef } = makeRuntime([
{ role: 'user', text: 'q', ts: 0, turnId: 'turn-1' },
])

runtime.appendEnsembleProgress({
event_type: 'proposer_start',
turn_id: 'turn-1',
proposer_provider: 'openrouter',
proposer_model: 'qwen/qwen3.7-plus',
})
runtime.handleRouterControlReplay()
runtime.queueRouterDecision({
turn_id: 'turn-1',
stream_seq: 13,
tier: 'c1',
model: 'provider/replayed',
source: 'squilla_router',
})

expect(messagesRef.value.filter(message => message.role === 'router')).toHaveLength(2)
})

it('promotes a handoff card without dropping its identity', () => {
const { runtime, messagesRef } = makeRuntime([
{ role: 'user', text: 'q', ts: 0, turnId: 'turn-1' },
])

runtime.markEnsembleHandoff()
runtime.queueRouterDecision({
turn_id: 'turn-1',
stream_seq: 14,
tier: 'c1',
model: 'provider/selected',
source: 'squilla_router',
})

const routers = messagesRef.value.filter(message => message.role === 'router')
expect(routers).toHaveLength(1)
expect(routers[0]).toMatchObject({ messageId: 'router-sess-14', turnId: 'turn-1' })
})

it('normalizes every internal candidate label to the public Proposer role', () => {
const { runtime, messagesRef } = makeRuntime([{ role: 'user', text: 'q', ts: 0 }])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export function useChatRouterDecisionRuntime(options: UseChatRouterDecisionRunti
messageId: string
} | null>(null)
let localRouterMessageSeq = 0
let routerReplayGeneration = 0
const provisionalRouterMessageGenerations = new Map<string, number>()

// Router and ensemble events can arrive throughout a long streamed answer.
// They should follow the live edge only while the reader has elected to stay
Expand All @@ -43,6 +45,7 @@ export function useChatRouterDecisionRuntime(options: UseChatRouterDecisionRunti
}

function handleRouterControlReplay() {
routerReplayGeneration += 1
if (!options.isStreaming.value) options.startStreaming()
pendingRouterDecision.value = null
options.resetStreamForRouterReplay()
Expand Down Expand Up @@ -169,6 +172,27 @@ export function useChatRouterDecisionRuntime(options: UseChatRouterDecisionRunti
const acceptedDecision = freezeAcceptedRoutingMode(decision, turnId)
if (options.messages.value.some(message => message.messageId === messageId)) return

if (turnId) {
for (let i = options.messages.value.length - 1; i >= 0; i--) {
const message = options.messages.value[i]
const provisionalMessageId = message.messageId || ''
if (
message.role === 'router'
&& message.provenanceKind === 'router_decision'
&& provisionalMessageId.startsWith(`router-${options.sessionKey.value}-`)
&& message.turnId === turnId
&& provisionalRouterMessageGenerations.get(provisionalMessageId) === routerReplayGeneration
) {
message.routerDecision = acceptedDecision
message.messageId = messageId
message.turnId = turnId
provisionalRouterMessageGenerations.delete(provisionalMessageId)
scrollToBottomIfFollowing()
return
}
}
}

options.messages.value.push({
role: 'router',
text: '',
Expand Down Expand Up @@ -303,6 +327,7 @@ export function useChatRouterDecisionRuntime(options: UseChatRouterDecisionRunti
...(turnId ? { turnId } : {}),
}
options.messages.value.push(message)
provisionalRouterMessageGenerations.set(message.messageId!, routerReplayGeneration)
return message
}

Expand Down Expand Up @@ -343,7 +368,7 @@ export function useChatRouterDecisionRuntime(options: UseChatRouterDecisionRunti
let target = findLiveRouterMessage(turnId)

if (!target) {
options.messages.value.push({
const provisionalMessage: ChatMessage = {
role: 'router',
text: '',
ts: new Date().toISOString(),
Expand All @@ -352,7 +377,9 @@ export function useChatRouterDecisionRuntime(options: UseChatRouterDecisionRunti
messageId: `router-${options.sessionKey.value}-ensemble`,
ensemble: emptyEnsemble(),
...(turnId ? { turnId } : {}),
})
}
options.messages.value.push(provisionalMessage)
provisionalRouterMessageGenerations.set(provisionalMessage.messageId!, routerReplayGeneration)
// Re-read through the reactive array so nested mutations below trigger.
target = options.messages.value[options.messages.value.length - 1]
}
Expand Down
Loading