fix: seal workbench status authority merges

This commit is contained in:
UniDesk Codex
2026-07-01 17:46:11 +08:00
parent 47c97b7c29
commit ce7ac81c43
2 changed files with 68 additions and 3 deletions
@@ -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, {
@@ -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);