From ce7ac81c437f1aa1bfdcb02a115fc284bb5acf94 Mon Sep 17 00:00:00 2001 From: UniDesk Codex Date: Wed, 1 Jul 2026 17:46:11 +0800 Subject: [PATCH] fix: seal workbench status authority merges --- .../scripts/workbench-server-state.test.ts | 53 ++++++++++++++++++- .../src/stores/workbench-server-state.ts | 18 ++++++- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/web/hwlab-cloud-web/scripts/workbench-server-state.test.ts b/web/hwlab-cloud-web/scripts/workbench-server-state.test.ts index cf1cb772..eaa418a3 100644 --- a/web/hwlab-cloud-web/scripts/workbench-server-state.test.ts +++ b/web/hwlab-cloud-web/scripts/workbench-server-state.test.ts @@ -6,7 +6,7 @@ import test from "node:test"; import type { WorkbenchSessionRecord } from "../src/types/index.ts"; import { initialWorkbenchSessionIdFromLocation } from "../src/stores/workbench-projection.ts"; -import { createWorkbenchServerState, reduceWorkbenchServerState, selectActiveMessages, selectActiveSession, selectSessionStatusAuthority, selectTraceAuthorityById } from "../src/stores/workbench-server-state.ts"; +import { createWorkbenchServerState, reduceWorkbenchServerState, selectActiveMessages, selectActiveSession, selectSessionStatusAuthority, selectTraceAuthorityById, selectTurnStatusAuthority } from "../src/stores/workbench-server-state.ts"; import { sessionToSessionTab, stableSessionList } from "../src/stores/workbench-session.ts"; test("Workbench route parsing accepts only session routes", () => { @@ -104,6 +104,57 @@ 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", () => { + let state = createWorkbenchServerState(); + state = reduceWorkbenchServerState(state, { + type: "session.messages", + sessionId: "ses_status_seal", + messages: [ + { id: "msg_user", messageId: "msg_user", role: "user", title: "用户", text: "ping", status: "sent", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_status_seal", traceId: "trc_status_seal" }, + { id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "OK", status: "completed", createdAt: "2026-06-24T00:00:01.000Z", sessionId: "ses_status_seal", traceId: "trc_status_seal" } + ] + }); + state = reduceWorkbenchServerState(state, { + type: "session.status", + session: { sessionId: "ses_status_seal", status: "running", lastTraceId: "trc_status_seal" } + }); + + let authority = selectSessionStatusAuthority(state).ses_status_seal; + assert.equal(authority?.status, "completed"); + assert.equal(authority?.lastTraceId, "trc_status_seal"); + + state = reduceWorkbenchServerState(state, { + type: "session.status", + session: { sessionId: "ses_status_seal", status: "running", lastTraceId: "trc_status_next" } + }); + + authority = selectSessionStatusAuthority(state).ses_status_seal; + assert.equal(authority?.status, "running"); + assert.equal(authority?.lastTraceId, "trc_status_next"); +}); + +test("Workbench turn status action preserves terminal authority over stale running refresh", () => { + let state = createWorkbenchServerState(); + state = reduceWorkbenchServerState(state, { + type: "turn.status", + turn: { traceId: "trc_turn_seal", status: "running", running: true, terminal: false, sessionId: "ses_turn_seal" } + }); + state = reduceWorkbenchServerState(state, { + type: "turn.status", + turn: { traceId: "trc_turn_seal", status: "completed", running: false, terminal: true, sessionId: "ses_turn_seal", loadedAt: "2026-06-24T00:00:02.000Z" } + }); + state = reduceWorkbenchServerState(state, { + type: "turn.status", + turn: { traceId: "trc_turn_seal", status: "running", running: true, terminal: false, sessionId: "ses_turn_seal", loadedAt: "2026-06-24T00:00:01.000Z" } + }); + + const turn = selectTurnStatusAuthority(state).trc_turn_seal; + assert.equal(turn?.status, "completed"); + assert.equal(turn?.running, false); + assert.equal(turn?.terminal, true); + assert.equal(turn?.loadedAt, "2026-06-24T00:00:02.000Z"); +}); + test("Workbench realtime agent snapshots cannot create duplicate assistant messages", () => { let state = createWorkbenchServerState(); state = reduceWorkbenchServerState(state, { diff --git a/web/hwlab-cloud-web/src/stores/workbench-server-state.ts b/web/hwlab-cloud-web/src/stores/workbench-server-state.ts index 90abd21d..70c78cc5 100644 --- a/web/hwlab-cloud-web/src/stores/workbench-server-state.ts +++ b/web/hwlab-cloud-web/src/stores/workbench-server-state.ts @@ -47,13 +47,13 @@ export function reduceWorkbenchServerState(state: WorkbenchServerState, action: case "message.snapshot": return reduceMessageSnapshot(state, action.sessionId, action.message); case "session.status": - return { ...state, sessionStatusById: { ...state.sessionStatusById, [action.session.sessionId]: action.session } }; + return { ...state, sessionStatusById: { ...state.sessionStatusById, [action.session.sessionId]: mergeSessionStatusAuthority(state.sessionStatusById[action.session.sessionId], action.session) } }; case "session.forget": return cleanupWorkbenchServerStateSessions(state, [action.sessionId]); case "session.cleanupDropped": return cleanupWorkbenchServerStateSessions(state, droppedSessionIds(state, new Set(action.keepSessionIds))); case "turn.status": - return { ...state, turnStatusByTraceId: { ...state.turnStatusByTraceId, [action.turn.traceId]: action.turn } }; + return { ...state, turnStatusByTraceId: { ...state.turnStatusByTraceId, [action.turn.traceId]: mergeTurnStatusAuthority(state.turnStatusByTraceId[action.turn.traceId], action.turn) } }; case "turn.forget": return { ...state, turnStatusByTraceId: withoutKey(state.turnStatusByTraceId, action.traceId) }; case "trace.snapshot": @@ -397,6 +397,20 @@ function mergeSessionStatusAuthority(existing: SessionStatusAuthority | undefine }; } +function mergeTurnStatusAuthority(existing: TurnStatusAuthority | undefined, incoming: TurnStatusAuthority): TurnStatusAuthority { + if (existing && isTerminalTurnStatusAuthority(existing) && !isTerminalTurnStatusAuthority(incoming) && isRunningSessionStatus(incoming.status)) return existing; + return { + ...(existing ?? {}), + ...incoming, + projection: incoming.projection ?? existing?.projection ?? null, + loadedAt: incoming.loadedAt ?? existing?.loadedAt ?? null + }; +} + +function isTerminalTurnStatusAuthority(turn: TurnStatusAuthority): boolean { + return turn.terminal === true || (turn.running !== true && isTerminalMessageStatus(turn.status)); +} + function isSameTraceAuthority(left: SessionStatusAuthority, right: SessionStatusAuthority): boolean { const leftTrace = textValue(left.lastTraceId); const rightTrace = textValue(right.lastTraceId);