Merge pull request #2341 from pikasTech/fix/2338-session-status-authority-v2

fix: keep workbench session status upstream
This commit is contained in:
Lyon
2026-07-02 11:40:07 +08:00
committed by GitHub
4 changed files with 158 additions and 67 deletions
@@ -104,7 +104,7 @@ test("Workbench server-state session detail updates session status authority", (
assert.equal(authority?.lastTraceId, "trc_backfill");
});
test("Workbench session status action preserves sealed completed authority for the same trace", () => {
test("Workbench turn status action seals completed authority for the same trace", () => {
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, {
type: "session.messages",
@@ -115,8 +115,8 @@ test("Workbench session status action preserves sealed completed authority for t
]
});
state = reduceWorkbenchServerState(state, {
type: "session.status",
session: { sessionId: "ses_status_seal", status: "running", lastTraceId: "trc_status_seal" }
type: "turn.status",
turn: { traceId: "trc_status_seal", sessionId: "ses_status_seal", status: "completed", running: false, terminal: true }
});
let authority = selectSessionStatusAuthority(state).ses_status_seal;
@@ -124,8 +124,8 @@ test("Workbench session status action preserves sealed completed authority for t
assert.equal(authority?.lastTraceId, "trc_status_seal");
state = reduceWorkbenchServerState(state, {
type: "session.status",
session: { sessionId: "ses_status_seal", status: "running", lastTraceId: "trc_status_next" }
type: "turn.status",
turn: { traceId: "trc_status_next", sessionId: "ses_status_seal", status: "running", running: true, terminal: false }
});
authority = selectSessionStatusAuthority(state).ses_status_seal;
@@ -252,6 +252,13 @@ test("Workbench sealed completed final body survives stale running session messa
assert.equal(agent?.status, "completed");
assert.equal(agent?.text, "final answer");
assert.equal((agent?.finalResponse as { text?: string } | undefined)?.text, "final answer");
assert.equal(selectSessionStatusAuthority(state).ses_final_seal?.status, "running");
state = reduceWorkbenchServerState(state, {
type: "turn.status",
turn: { traceId: "trc_final_seal", sessionId: "ses_final_seal", status: "completed", running: false, terminal: true, updatedAt: "2026-06-24T00:00:06.000Z" }
});
assert.equal(selectSessionStatusAuthority(state).ses_final_seal?.status, "completed");
state = reduceWorkbenchServerState(state, {
@@ -1,15 +1,42 @@
import assert from "node:assert/strict";
import { test } from "bun:test";
import type { ChatMessage, WorkbenchSessionRecord } from "../types";
import { createWorkbenchServerState, reduceWorkbenchServerState, selectActiveMessages, selectSessionStatusAuthority } from "./workbench-server-state";
function sessionRecord(input: WorkbenchSessionRecord): WorkbenchSessionRecord {
return input;
}
function agentMessage(input: Partial<ChatMessage> & Pick<ChatMessage, "id">): ChatMessage {
return {
role: "agent",
title: "Code Agent",
text: "",
status: "running",
createdAt: "2026-07-01T00:00:00.000Z",
...input
};
}
function userMessage(input: Partial<ChatMessage> & Pick<ChatMessage, "id">): ChatMessage {
return {
role: "user",
title: "用户",
text: "",
status: "sent",
createdAt: "2026-07-01T00:00:00.000Z",
...input
};
}
test("server state reducer keeps completed message without final response unsealed", () => {
const sessionId = "ses_state_completed_without_final";
const traceId = "trc_state_completed_without_final";
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, { type: "session.detail", session: { sessionId, status: "running", messages: [] } as any });
state = reduceWorkbenchServerState(state, { type: "session.messages", sessionId, messages: [{ id: "msg_state_completed_without_final", role: "agent", status: "running", traceId, sessionId, startedAt: "2026-07-01T00:00:00.000Z" } as any] });
state = reduceWorkbenchServerState(state, { type: "session.messages", sessionId, messages: [{ id: "msg_state_completed_without_final", role: "agent", status: "completed", traceId, sessionId, startedAt: "2026-07-01T00:00:00.000Z", finishedAt: "2026-07-01T00:00:02.000Z", durationMs: 2000 } as any] });
state = reduceWorkbenchServerState(state, { type: "session.detail", session: sessionRecord({ sessionId, status: "running", messages: [] }) });
state = reduceWorkbenchServerState(state, { type: "session.messages", sessionId, messages: [agentMessage({ id: "msg_state_completed_without_final", status: "running", traceId, sessionId, startedAt: "2026-07-01T00:00:00.000Z" })] });
state = reduceWorkbenchServerState(state, { type: "session.messages", sessionId, messages: [agentMessage({ id: "msg_state_completed_without_final", status: "completed", traceId, sessionId, startedAt: "2026-07-01T00:00:00.000Z", finishedAt: "2026-07-01T00:00:02.000Z", durationMs: 2000 })] });
const [message] = selectActiveMessages(state, sessionId);
assert.equal(message.status, "running");
@@ -22,9 +49,9 @@ test("server state reducer keeps failed message without terminal body unsealed",
const sessionId = "ses_state_failed_without_body";
const traceId = "trc_state_failed_without_body";
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, { type: "session.detail", session: { sessionId, status: "running", messages: [] } as any });
state = reduceWorkbenchServerState(state, { type: "session.messages", sessionId, messages: [{ id: "msg_state_failed_without_body", role: "agent", status: "running", traceId, sessionId, startedAt: "2026-07-01T00:00:00.000Z" } as any] });
state = reduceWorkbenchServerState(state, { type: "session.messages", sessionId, messages: [{ id: "msg_state_failed_without_body", role: "agent", status: "failed", traceId, sessionId, startedAt: "2026-07-01T00:00:00.000Z", finishedAt: "2026-07-01T00:00:02.000Z", durationMs: 2000 } as any] });
state = reduceWorkbenchServerState(state, { type: "session.detail", session: sessionRecord({ sessionId, status: "running", messages: [] }) });
state = reduceWorkbenchServerState(state, { type: "session.messages", sessionId, messages: [agentMessage({ id: "msg_state_failed_without_body", status: "running", traceId, sessionId, startedAt: "2026-07-01T00:00:00.000Z" })] });
state = reduceWorkbenchServerState(state, { type: "session.messages", sessionId, messages: [agentMessage({ id: "msg_state_failed_without_body", status: "failed", traceId, sessionId, startedAt: "2026-07-01T00:00:00.000Z", finishedAt: "2026-07-01T00:00:02.000Z", durationMs: 2000 })] });
const [message] = selectActiveMessages(state, sessionId);
assert.equal(message.status, "running");
@@ -37,8 +64,10 @@ test("server state reducer seals completed message when final response exists",
const sessionId = "ses_state_completed_with_final";
const traceId = "trc_state_completed_with_final";
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, { type: "session.detail", session: { sessionId, status: "running", messages: [] } as any });
state = reduceWorkbenchServerState(state, { type: "session.messages", sessionId, messages: [{ id: "msg_state_completed_with_final", role: "agent", status: "completed", traceId, sessionId, text: "final answer", finishedAt: "2026-07-01T00:00:02.000Z", durationMs: 2000 } as any] });
state = reduceWorkbenchServerState(state, { type: "session.detail", session: sessionRecord({ sessionId, status: "running", messages: [] }) });
state = reduceWorkbenchServerState(state, { type: "session.messages", sessionId, messages: [agentMessage({ id: "msg_state_completed_with_final", status: "completed", traceId, sessionId, text: "final answer", finishedAt: "2026-07-01T00:00:02.000Z", durationMs: 2000 })] });
assert.equal(selectSessionStatusAuthority(state)[sessionId]?.status, "running");
state = reduceWorkbenchServerState(state, { type: "turn.status", turn: { traceId, status: "completed", running: false, terminal: true, sessionId, updatedAt: "2026-07-01T00:00:02.000Z" } });
const [message] = selectActiveMessages(state, sessionId);
assert.equal(message.status, "completed");
@@ -50,24 +79,26 @@ test("server state reducer keeps sealed terminal messages when a stale partial s
const sessionId = "ses_state_partial_snapshot";
const traceId = "trc_state_partial_snapshot";
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, { type: "session.detail", session: { sessionId, status: "running", messages: [] } as any });
state = reduceWorkbenchServerState(state, { type: "session.detail", session: sessionRecord({ sessionId, status: "running", messages: [] }) });
state = reduceWorkbenchServerState(state, { type: "session.messages", sessionId, messages: [
{ id: "msg_state_partial_user_1", role: "user", status: "sent", text: "one", sessionId, traceId: "trc_state_partial_1" } as any,
{ id: "msg_state_partial_agent_1", role: "agent", status: "completed", text: "one done", sessionId, traceId: "trc_state_partial_1" } as any,
{ id: "msg_state_partial_user_2", role: "user", status: "sent", text: "two", sessionId, traceId } as any,
{ id: "msg_state_partial_agent_2", role: "agent", status: "completed", text: "final answer", finalResponse: { text: "final answer" }, sessionId, traceId, finishedAt: "2026-07-01T00:00:04.000Z", durationMs: 4000 } as any
userMessage({ id: "msg_state_partial_user_1", text: "one", sessionId, traceId: "trc_state_partial_1" }),
agentMessage({ id: "msg_state_partial_agent_1", status: "completed", text: "one done", sessionId, traceId: "trc_state_partial_1" }),
userMessage({ id: "msg_state_partial_user_2", text: "two", sessionId, traceId }),
agentMessage({ id: "msg_state_partial_agent_2", status: "completed", text: "final answer", finalResponse: { text: "final answer" }, sessionId, traceId, finishedAt: "2026-07-01T00:00:04.000Z", durationMs: 4000 })
] });
state = reduceWorkbenchServerState(state, { type: "session.messages", sessionId, messages: [
{ id: "msg_state_partial_user_1", role: "user", status: "sent", text: "one", sessionId, traceId: "trc_state_partial_1" } as any,
{ id: "msg_state_partial_agent_1", role: "agent", status: "completed", text: "one done", sessionId, traceId: "trc_state_partial_1" } as any
userMessage({ id: "msg_state_partial_user_1", text: "one", sessionId, traceId: "trc_state_partial_1" }),
agentMessage({ id: "msg_state_partial_agent_1", status: "completed", text: "one done", sessionId, traceId: "trc_state_partial_1" })
] });
const messages = selectActiveMessages(state, sessionId);
assert.equal(messages.length, 4);
const terminal = messages.find((message) => message.id === "msg_state_partial_agent_2") as any;
const terminal = messages.find((message) => message.id === "msg_state_partial_agent_2");
assert.equal(terminal.status, "completed");
assert.equal(terminal.text, "final answer");
assert.equal(terminal.finalResponse.text, "final answer");
assert.equal((terminal.finalResponse as { text?: string } | undefined)?.text, "final answer");
assert.equal(selectSessionStatusAuthority(state)[sessionId]?.status, "running");
state = reduceWorkbenchServerState(state, { type: "turn.status", turn: { traceId, status: "completed", running: false, terminal: true, sessionId, updatedAt: "2026-07-01T00:00:04.000Z" } });
assert.equal(selectSessionStatusAuthority(state)[sessionId]?.status, "completed");
});
@@ -75,15 +106,56 @@ test("server state reducer promotes failed terminal message authority and ignore
const sessionId = "ses_state_failed_terminal_authority";
const traceId = "trc_state_failed_terminal_authority";
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, { type: "session.detail", session: { sessionId, status: "running", lastTraceId: traceId, messages: [] } as any });
state = reduceWorkbenchServerState(state, { type: "session.detail", session: sessionRecord({ sessionId, status: "running", lastTraceId: traceId, messages: [] }) });
state = reduceWorkbenchServerState(state, { type: "session.messages", sessionId, messages: [
{ id: "msg_state_failed_user", role: "user", status: "sent", text: "one", sessionId, traceId } as any,
{ id: "msg_state_failed_agent", role: "agent", status: "failed", text: "Workbench terminal failed: provider-stream-disconnected", finalResponse: { text: "Workbench terminal failed: provider-stream-disconnected" }, sessionId, traceId, finishedAt: "2026-07-01T00:00:05.000Z", durationMs: 5000 } as any
userMessage({ id: "msg_state_failed_user", text: "one", sessionId, traceId }),
agentMessage({ id: "msg_state_failed_agent", status: "failed", text: "Workbench terminal failed: provider-stream-disconnected", finalResponse: { text: "Workbench terminal failed: provider-stream-disconnected" }, sessionId, traceId, finishedAt: "2026-07-01T00:00:05.000Z", durationMs: 5000 })
] });
assert.equal(selectSessionStatusAuthority(state)[sessionId]?.status, "running");
state = reduceWorkbenchServerState(state, { type: "turn.status", turn: { traceId, status: "failed", running: false, terminal: true, sessionId, updatedAt: "2026-07-01T00:00:05.000Z" } });
assert.equal(selectSessionStatusAuthority(state)[sessionId]?.status, "failed");
state = reduceWorkbenchServerState(state, { type: "session.list", sessions: [{ sessionId, status: "running", lastTraceId: traceId, messages: [] } as any] });
state = reduceWorkbenchServerState(state, { type: "session.list", sessions: [sessionRecord({ sessionId, status: "running", lastTraceId: traceId, messages: [] })] });
assert.equal(selectSessionStatusAuthority(state)[sessionId]?.status, "failed");
assert.equal(selectActiveMessages(state, sessionId).find((message) => message.id === "msg_state_failed_agent")?.status, "failed");
});
test("server state reducer keeps newer running turn authority over older terminal message", () => {
const sessionId = "ses_state_new_turn_running";
const oldTraceId = "trc_state_old_completed";
const newTraceId = "trc_state_new_running";
let state = createWorkbenchServerState();
state = reduceWorkbenchServerState(state, {
type: "session.detail",
session: {
sessionId,
status: "completed",
lastTraceId: oldTraceId,
messages: [
userMessage({ id: "msg_state_old_user", text: "one", sessionId, traceId: oldTraceId }),
agentMessage({ id: "msg_state_old_agent", status: "completed", text: "one done", finalResponse: { text: "one done" }, sessionId, traceId: oldTraceId }),
userMessage({ id: "msg_state_new_user", text: "two", sessionId, traceId: newTraceId }),
agentMessage({ id: "msg_state_new_agent", status: "running", text: "", sessionId, traceId: newTraceId })
]
}
});
state = reduceWorkbenchServerState(state, { type: "turn.status", turn: { traceId: newTraceId, status: "running", running: true, terminal: false, sessionId, updatedAt: "2026-07-02T00:00:04.000Z" } });
state = reduceWorkbenchServerState(state, { type: "session.status", session: { sessionId, status: "running", lastTraceId: newTraceId, updatedAt: "2026-07-02T00:00:04.000Z" } });
state = reduceWorkbenchServerState(state, { type: "session.list", sessions: [sessionRecord({ sessionId, status: "completed", lastTraceId: oldTraceId, messages: [] })] });
state = reduceWorkbenchServerState(state, { type: "session.detail", session: sessionRecord({ sessionId, status: "completed", lastTraceId: oldTraceId, messages: [] }) });
state = reduceWorkbenchServerState(state, {
type: "session.messages",
sessionId,
messages: [
userMessage({ id: "msg_state_old_user", text: "one", sessionId, traceId: oldTraceId }),
agentMessage({ id: "msg_state_old_agent", status: "completed", text: "one done", finalResponse: { text: "one done" }, sessionId, traceId: oldTraceId }),
userMessage({ id: "msg_state_new_user", text: "two", sessionId, traceId: newTraceId }),
agentMessage({ id: "msg_state_new_agent", status: "running", text: "", sessionId, traceId: newTraceId })
]
});
const authority = selectSessionStatusAuthority(state)[sessionId];
assert.equal(authority?.status, "running");
assert.equal(authority?.lastTraceId, newTraceId);
});
@@ -53,7 +53,7 @@ export function reduceWorkbenchServerState(state: WorkbenchServerState, action:
case "session.cleanupDropped":
return cleanupWorkbenchServerStateSessions(state, droppedSessionIds(state, new Set(action.keepSessionIds)));
case "turn.status":
return { ...state, turnStatusByTraceId: { ...state.turnStatusByTraceId, [action.turn.traceId]: mergeTurnStatusAuthority(state.turnStatusByTraceId[action.turn.traceId], action.turn) } };
return reduceTurnStatus(state, action.turn);
case "turn.forget":
return { ...state, turnStatusByTraceId: withoutKey(state.turnStatusByTraceId, action.traceId) };
case "trace.snapshot":
@@ -93,11 +93,12 @@ export function selectTraceAuthorityById(state: WorkbenchServerState): Readonly<
}
export function selectSessionList(state: WorkbenchServerState): WorkbenchSessionRecord[] {
return state.sessionOrder.map((sessionId) => state.sessionsById[sessionId]).filter((session): session is WorkbenchSessionRecord => Boolean(session));
return state.sessionOrder.map((sessionId) => projectSessionRecord(state.sessionsById[sessionId], state.sessionStatusById[sessionId])).filter((session): session is WorkbenchSessionRecord => Boolean(session));
}
export function selectActiveSession(state: WorkbenchServerState, sessionId: string | null): WorkbenchSessionRecord | null {
return sessionId ? state.sessionsById[sessionId] ?? null : null;
if (!sessionId) return null;
return projectSessionRecord(state.sessionsById[sessionId], state.sessionStatusById[sessionId]);
}
export function selectActiveMessages(state: WorkbenchServerState, sessionId: string | null): ChatMessage[] {
@@ -110,9 +111,8 @@ function reduceSessionDetail(state: WorkbenchServerState, session: WorkbenchSess
const existing = state.sessionsById[sessionId];
const existingMessages = state.messagesBySessionId[sessionId] ?? existing?.messages ?? [];
const messages = Array.isArray(session.messages) ? mergeMessageList(existingMessages, session.messages) : existingMessages;
const messageStatus = sessionStatusAuthorityFromMessages(sessionId, messages);
const merged = mergeSessionRecord(existing, { ...session, status: messageStatus?.status ?? session.status, messages });
const sessionStatus = messageStatus ?? sessionStatusAuthorityFromDetail(session);
const merged = mergeSessionRecord(existing, { ...session, messages });
const sessionStatus = sessionStatusAuthorityFromDetail(session);
return {
...state,
sessionOrder: state.sessionOrder.includes(sessionId) ? state.sessionOrder : [sessionId, ...state.sessionOrder],
@@ -139,9 +139,8 @@ function reduceSessionListItem(state: WorkbenchServerState, session: WorkbenchSe
const existing = state.sessionsById[sessionId];
const existingMessages = state.messagesBySessionId[sessionId] ?? existing?.messages;
const messages = Array.isArray(existingMessages) ? existingMessages : Array.isArray(session.messages) ? session.messages : [];
const messageStatus = sessionStatusAuthorityFromMessages(sessionId, messages);
const merged = mergeSessionRecord(existing, { ...session, status: messageStatus?.status ?? session.status, messages });
const sessionStatus = messageStatus ?? sessionStatusAuthorityFromDetail(session);
const merged = mergeSessionRecord(existing, { ...session, messages });
const sessionStatus = sessionStatusAuthorityFromDetail(session);
return {
...state,
sessionsById: { ...state.sessionsById, [sessionId]: merged },
@@ -156,18 +155,27 @@ function reduceSessionMessages(state: WorkbenchServerState, sessionId: string |
const existing = state.sessionsById[sessionId];
const existingMessages = state.messagesBySessionId[sessionId] ?? existing?.messages ?? [];
const mergedMessages = mergeMessageList(existingMessages, messages);
const reconciledStatus = sessionStatusAuthorityFromMessages(sessionId, mergedMessages);
const session = existing ? { ...existing, status: reconciledStatus?.status ?? existing.status, messages: mergedMessages, messageCount: mergedMessages.length } : null;
const session = existing ? { ...existing, messages: mergedMessages, messageCount: mergedMessages.length } : null;
return {
...state,
sessionsById: session ? { ...state.sessionsById, [sessionId]: session } : state.sessionsById,
sessionStatusById: reconciledStatus
? { ...state.sessionStatusById, [sessionId]: mergeSessionStatusAuthority(state.sessionStatusById[sessionId], reconciledStatus) }
: state.sessionStatusById,
messagesBySessionId: { ...state.messagesBySessionId, [sessionId]: mergedMessages }
};
}
function reduceTurnStatus(state: WorkbenchServerState, turn: TurnStatusAuthority): WorkbenchServerState {
const nextTurn = mergeTurnStatusAuthority(state.turnStatusByTraceId[turn.traceId], turn);
const nextTurns = { ...state.turnStatusByTraceId, [turn.traceId]: nextTurn };
const sessionStatus = sessionStatusAuthorityFromTurn(nextTurn);
return {
...state,
turnStatusByTraceId: nextTurns,
sessionStatusById: sessionStatus
? { ...state.sessionStatusById, [sessionStatus.sessionId]: mergeSessionStatusAuthority(state.sessionStatusById[sessionStatus.sessionId], sessionStatus) }
: state.sessionStatusById
};
}
function reduceMessageSnapshot(state: WorkbenchServerState, sessionId: string | null | undefined, message: ChatMessage | null | undefined): WorkbenchServerState {
const id = sessionId || message?.sessionId;
if (!id || !message) return state;
@@ -386,6 +394,21 @@ function mergeSessionRecord(existing: WorkbenchSessionRecord | undefined, incomi
};
}
function projectSessionRecord(session: WorkbenchSessionRecord | null | undefined, authority: SessionStatusAuthority | null | undefined): WorkbenchSessionRecord | null {
if (!session) return null;
if (!authority?.status || normalizedMessageStatus(session.status) === "archived") return session;
const projection = authority.projection ?? session.projection;
return {
...session,
status: authority.status,
updatedAt: textValue(authority.updatedAt) ?? session.updatedAt,
lastTraceId: textValue(authority.lastTraceId) ?? session.lastTraceId,
projection,
projectionHealth: authority.projection?.projectionHealth ?? session.projectionHealth,
projectionStatus: authority.projection?.projectionStatus ?? session.projectionStatus
};
}
function sessionStatusAuthorityFromDetail(session: WorkbenchSessionRecord): SessionStatusAuthority | null {
const sessionId = session.sessionId;
if (!sessionId || typeof session.status !== "string") return null;
@@ -399,22 +422,24 @@ function sessionStatusAuthorityFromDetail(session: WorkbenchSessionRecord): Sess
};
}
function sessionStatusAuthorityFromMessages(sessionId: string, messages: ChatMessage[]): SessionStatusAuthority | null {
const message = [...messages].reverse().find((item) => messageHasTerminalResponse(item));
if (!message) return null;
const status = canonicalTerminalMessageStatus(message.status);
function sessionStatusAuthorityFromTurn(turn: TurnStatusAuthority): SessionStatusAuthority | null {
const sessionId = textValue(turn.sessionId);
if (!sessionId) return null;
const status = textValue(turn.status) ?? (turn.running === true ? "running" : null);
if (!status) return null;
return {
sessionId,
status,
updatedAt: textValue(message.updatedAt) ?? textValue(message.finishedAt) ?? textValue(message.lastEventAt),
lastTraceId: textValue(message.traceId) ?? textValue(message.runnerTrace?.traceId),
projection: projectionFromMessageRecord(message),
loadedAt: new Date().toISOString()
updatedAt: textValue(turn.updatedAt),
lastTraceId: textValue(turn.traceId),
projection: turn.projection ?? null,
loadedAt: turn.loadedAt ?? new Date().toISOString()
};
}
function mergeSessionStatusAuthority(existing: SessionStatusAuthority | undefined, incoming: SessionStatusAuthority): SessionStatusAuthority {
if (isTerminalSessionStatusAuthority(existing) && !isTerminalSessionStatusAuthority(incoming) && isSameTraceAuthority(existing, incoming) && isRunningSessionStatus(incoming.status)) return existing;
if (isRunningSessionStatus(existing?.status) && isTerminalSessionStatusAuthority(incoming) && isDifferentTraceAuthority(existing, incoming)) return existing;
return {
...(existing ?? {}),
...incoming,
@@ -452,6 +477,12 @@ function isSameTraceAuthority(left: SessionStatusAuthority, right: SessionStatus
return Boolean(leftTrace && rightTrace && leftTrace === rightTrace);
}
function isDifferentTraceAuthority(left: SessionStatusAuthority | undefined, right: SessionStatusAuthority | undefined): boolean {
const leftTrace = textValue(left?.lastTraceId);
const rightTrace = textValue(right?.lastTraceId);
return Boolean(leftTrace && rightTrace && leftTrace !== rightTrace);
}
function isRunningSessionStatus(value: unknown): boolean {
return ["", "pending", "running", "accepted", "queued", "dispatching", "streaming", "processing", "retrying", "busy", "creating"].includes(normalizedMessageStatus(value));
}
@@ -463,23 +494,10 @@ function messageHasTerminalResponse(message: ChatMessage): boolean {
return Boolean(messageFinalResponseText(message));
}
function canonicalTerminalMessageStatus(value: unknown): string {
const status = normalizedMessageStatus(value);
return status === "cancelled" ? "canceled" : status;
}
function messageFinalResponseText(message: ChatMessage): string | null {
return textValue(message.text) ?? textValue(message.content) ?? nestedTextValue((message as Record<string, unknown>).finalResponse);
}
function projectionFromMessageRecord(message: ChatMessage): ProjectionDiagnostic | null {
const direct = message.projection && typeof message.projection === "object" ? message.projection : null;
if (direct) return direct;
const trace = message.runnerTrace && typeof message.runnerTrace === "object" ? message.runnerTrace as Record<string, unknown> : null;
const traceProjection = trace?.projection && typeof trace.projection === "object" ? trace.projection as ProjectionDiagnostic : null;
return traceProjection ?? null;
}
function nestedTextValue(value: unknown): string | null {
const direct = textValue(value);
if (direct) return direct;
@@ -1462,7 +1462,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const running = existing?.running === true || (!isTerminalMessageStatus(message?.status) && isTraceActiveStatus(message?.status));
const now = new Date().toISOString();
reduceServerState({ type: "turn.status", turn: { traceId, status, running, terminal: false, sessionId, threadId: firstNonEmptyString(message?.threadId, message?.runnerTrace?.threadId) ?? existing?.threadId ?? null, updatedAt: now, projection, loadedAt: now } });
if (sessionId) reduceServerState({ type: "session.status", session: { sessionId, status: activeSession.value?.status ?? status, updatedAt: now, lastTraceId: traceId, projection, loadedAt: now } });
const projectionError = agentErrorFromProjection(projection);
updateActiveMessages((source) => source.map((item) => {
if (!shouldApplyTraceToMessage(item, traceId, sessionId)) return item;
@@ -1556,10 +1555,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
type: "turn.status",
turn: { traceId: input.traceId, status: "failed", running: false, terminal: true, sessionId: input.sessionId, threadId: input.threadId, updatedAt: finishedAt, projection: input.projection, loadedAt: finishedAt }
});
reduceServerState({
type: "session.status",
session: { sessionId: input.sessionId, status: "failed", updatedAt: finishedAt, lastTraceId: input.traceId, projection: input.projection, loadedAt: finishedAt }
});
const existing = sessions.value.find((session) => session.sessionId === input.sessionId) ?? null;
const projectedMessages = serverState.value.messagesBySessionId[input.sessionId] ?? messages.value.filter((message) => firstNonEmptyString(message.sessionId, message.runnerTrace?.sessionId) === input.sessionId);
rememberSessionList(mergeSessionIntoList(sessions.value, {
@@ -1579,7 +1574,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
function projectOptimisticRunningTurn(input: { sessionId: string; threadId: string | null; traceId: string; userText: string }): void {
const now = new Date().toISOString();
reduceServerState({ type: "turn.status", turn: { traceId: input.traceId, status: "running", running: true, terminal: false, sessionId: input.sessionId, threadId: input.threadId, updatedAt: now, loadedAt: now } });
reduceServerState({ type: "session.status", session: { sessionId: input.sessionId, status: "running", updatedAt: now, lastTraceId: input.traceId, loadedAt: now } });
const existing = sessions.value.find((session) => session.sessionId === input.sessionId) ?? null;
rememberSessionList(mergeSessionIntoList(sessions.value, {
...(existing ?? {}),