Merge pull request #2362 from pikasTech/fix/2356-response-json-budget
fix(workbench): budget realtime session detail refresh
This commit is contained in:
@@ -139,6 +139,7 @@ test("workbench active terminal paths seal final response from turn authority",
|
|||||||
const terminalRestBlock = source.slice(source.indexOf("async function refreshTerminalTraceFromRest"), source.indexOf("function installRealtimeVisibilityHandler"));
|
const terminalRestBlock = source.slice(source.indexOf("async function refreshTerminalTraceFromRest"), source.indexOf("function installRealtimeVisibilityHandler"));
|
||||||
const completeBlock = source.slice(source.indexOf("function completeTrace"), source.indexOf("async function hydrateTerminalMessageDiagnostics"));
|
const completeBlock = source.slice(source.indexOf("function completeTrace"), source.indexOf("async function hydrateTerminalMessageDiagnostics"));
|
||||||
const realtimeSessionDetailBlock = source.slice(source.indexOf("async function refreshRealtimeSessionFromRest"), source.indexOf("function completeTrace"));
|
const realtimeSessionDetailBlock = source.slice(source.indexOf("async function refreshRealtimeSessionFromRest"), source.indexOf("function completeTrace"));
|
||||||
|
const sessionDetailReadBlock = source.slice(source.indexOf("function fetchSessionDetailPage"), source.indexOf("function sessionMessageProjectionWindowLimit"));
|
||||||
const loadBlock = source.slice(source.indexOf("async function loadWorkbenchSession"), source.indexOf("async function sealRestoredActiveTurnMessages"));
|
const loadBlock = source.slice(source.indexOf("async function loadWorkbenchSession"), source.indexOf("async function sealRestoredActiveTurnMessages"));
|
||||||
const restoreSealBlock = source.slice(source.indexOf("async function sealRestoredActiveTurnMessages"), source.indexOf("function reattachRestoredActiveTrace"));
|
const restoreSealBlock = source.slice(source.indexOf("async function sealRestoredActiveTurnMessages"), source.indexOf("function reattachRestoredActiveTrace"));
|
||||||
|
|
||||||
@@ -154,8 +155,12 @@ test("workbench active terminal paths seal final response from turn authority",
|
|||||||
assert.match(realtimeSessionDetailBlock, /traceIdFromRealtimeRefreshReason\(reason\)/u);
|
assert.match(realtimeSessionDetailBlock, /traceIdFromRealtimeRefreshReason\(reason\)/u);
|
||||||
assert.match(realtimeSessionDetailBlock, /traceTerminalBodyIsVisible\(traceId, id\)[\s\S]*terminal_low_priority_session_detail_skip[\s\S]*return/u);
|
assert.match(realtimeSessionDetailBlock, /traceTerminalBodyIsVisible\(traceId, id\)[\s\S]*terminal_low_priority_session_detail_skip[\s\S]*return/u);
|
||||||
assert.match(realtimeSessionDetailBlock, /traceTerminalBodyIsVisible\(traceId, id\)[\s\S]*terminal_low_priority_session_detail_apply_skip[\s\S]*return/u);
|
assert.match(realtimeSessionDetailBlock, /traceTerminalBodyIsVisible\(traceId, id\)[\s\S]*terminal_low_priority_session_detail_apply_skip[\s\S]*return/u);
|
||||||
|
assert.match(realtimeSessionDetailBlock, /fetchSessionDetailPage\(id, \{ reason: `realtime-session-detail:\$\{reason\}` \}\)/u);
|
||||||
|
assert.doesNotMatch(realtimeSessionDetailBlock, /loadWorkbenchSession|applySelectedSessionDetail/u);
|
||||||
|
assert.match(sessionDetailReadBlock, /workbenchSessionDetailReadKey\(\{ sessionId, force: options\.force \}\)/u);
|
||||||
|
assert.match(sessionDetailReadBlock, /fetchSession\(sessionId, \{ includeMessages: false,/u);
|
||||||
assert.match(loadBlock, /const messageLimit = sessionMessageProjectionWindowLimit\(\);/u);
|
assert.match(loadBlock, /const messageLimit = sessionMessageProjectionWindowLimit\(\);/u);
|
||||||
assert.match(loadBlock, /fetchSession\(requestId, \{ includeMessages: false,/u);
|
assert.match(loadBlock, /fetchSessionDetailPage\(requestId, \{ reason: "load-session:detail", force: true \}\)/u);
|
||||||
assert.match(loadBlock, /sessionFromWorkbenchSession\(detail\.data\?\.session, \{ includeMessages: false \}\)/u);
|
assert.match(loadBlock, /sessionFromWorkbenchSession\(detail\.data\?\.session, \{ includeMessages: false \}\)/u);
|
||||||
assert.match(loadBlock, /const fallbackMessages = seed\?\.sessionId === id \? seed\.messages \?\? \[\] : \[\];/u);
|
assert.match(loadBlock, /const fallbackMessages = seed\?\.sessionId === id \? seed\.messages \?\? \[\] : \[\];/u);
|
||||||
assert.doesNotMatch(loadBlock, /limit:\s*100/u);
|
assert.doesNotMatch(loadBlock, /limit:\s*100/u);
|
||||||
|
|||||||
@@ -46,6 +46,19 @@ test("server state reducer keeps completed message without final response unseal
|
|||||||
assert.notEqual(selectSessionStatusAuthority(state)[sessionId]?.status, "completed");
|
assert.notEqual(selectSessionStatusAuthority(state)[sessionId]?.status, "completed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("server state reducer keeps session detail reference stable when detail has no semantic change", () => {
|
||||||
|
const sessionId = "ses_state_stable_detail";
|
||||||
|
const traceId = "trc_state_stable_detail";
|
||||||
|
let state = createWorkbenchServerState();
|
||||||
|
const messages = [agentMessage({ id: "msg_state_stable_detail", status: "running", traceId, sessionId })];
|
||||||
|
state = reduceWorkbenchServerState(state, { type: "session.detail", session: sessionRecord({ sessionId, status: "running", lastTraceId: traceId, updatedAt: "2026-07-01T00:00:00.000Z", messageCount: 1, messages }) });
|
||||||
|
const first = state;
|
||||||
|
state = reduceWorkbenchServerState(state, { type: "session.detail", session: sessionRecord({ sessionId, status: "running", lastTraceId: traceId, updatedAt: "2026-07-01T00:00:00.000Z", messageCount: 1, messages: [...messages] }) });
|
||||||
|
|
||||||
|
assert.equal(state, first);
|
||||||
|
assert.equal(selectActiveMessages(state, sessionId), selectActiveMessages(first, sessionId));
|
||||||
|
});
|
||||||
|
|
||||||
test("server state reducer keeps failed message without terminal body unsealed", () => {
|
test("server state reducer keeps failed message without terminal body unsealed", () => {
|
||||||
const sessionId = "ses_state_failed_without_body";
|
const sessionId = "ses_state_failed_without_body";
|
||||||
const traceId = "trc_state_failed_without_body";
|
const traceId = "trc_state_failed_without_body";
|
||||||
|
|||||||
@@ -116,12 +116,14 @@ function reduceSessionDetail(state: WorkbenchServerState, session: WorkbenchSess
|
|||||||
const messages = Array.isArray(session.messages) ? mergeMessageList(existingMessages, session.messages) : existingMessages;
|
const messages = Array.isArray(session.messages) ? mergeMessageList(existingMessages, session.messages) : existingMessages;
|
||||||
const merged = mergeSessionRecord(existing, { ...session, messages });
|
const merged = mergeSessionRecord(existing, { ...session, messages });
|
||||||
const sessionStatus = sessionStatusAuthorityFromDetail(session);
|
const sessionStatus = sessionStatusAuthorityFromDetail(session);
|
||||||
|
const nextSessionStatus = sessionStatus ? mergeSessionStatusAuthority(state.sessionStatusById[sessionId], sessionStatus) : null;
|
||||||
|
if (existing && state.sessionOrder.includes(sessionId) && sessionRecordsEquivalent(existing, merged) && messageListsEquivalent(existingMessages, messages) && sessionStatusAuthorityEquivalent(state.sessionStatusById[sessionId], nextSessionStatus)) return state;
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
sessionOrder: state.sessionOrder.includes(sessionId) ? state.sessionOrder : [sessionId, ...state.sessionOrder],
|
sessionOrder: state.sessionOrder.includes(sessionId) ? state.sessionOrder : [sessionId, ...state.sessionOrder],
|
||||||
sessionsById: { ...state.sessionsById, [sessionId]: merged },
|
sessionsById: { ...state.sessionsById, [sessionId]: merged },
|
||||||
sessionStatusById: sessionStatus
|
sessionStatusById: sessionStatus
|
||||||
? { ...state.sessionStatusById, [sessionId]: mergeSessionStatusAuthority(state.sessionStatusById[sessionId], sessionStatus) }
|
? { ...state.sessionStatusById, [sessionId]: nextSessionStatus ?? sessionStatus }
|
||||||
: state.sessionStatusById,
|
: state.sessionStatusById,
|
||||||
messagesBySessionId: {
|
messagesBySessionId: {
|
||||||
...state.messagesBySessionId,
|
...state.messagesBySessionId,
|
||||||
@@ -415,6 +417,58 @@ function mergeSessionRecord(existing: WorkbenchSessionRecord | undefined, incomi
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sessionRecordsEquivalent(left: WorkbenchSessionRecord | null | undefined, right: WorkbenchSessionRecord | null | undefined): boolean {
|
||||||
|
if (left === right) return true;
|
||||||
|
if (!left || !right) return false;
|
||||||
|
const leftMessages = Array.isArray(left.messages) ? left.messages : [];
|
||||||
|
const rightMessages = Array.isArray(right.messages) ? right.messages : [];
|
||||||
|
const leftRecord = { ...left, messages: undefined };
|
||||||
|
const rightRecord = { ...right, messages: undefined };
|
||||||
|
return valuesEquivalent(leftRecord, rightRecord) && messageListsEquivalent(leftMessages, rightMessages);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sessionStatusAuthorityEquivalent(left: SessionStatusAuthority | null | undefined, right: SessionStatusAuthority | null | undefined): boolean {
|
||||||
|
if (left === right) return true;
|
||||||
|
if (!left || !right) return !left && !right;
|
||||||
|
return left.sessionId === right.sessionId
|
||||||
|
&& left.status === right.status
|
||||||
|
&& (left.updatedAt ?? null) === (right.updatedAt ?? null)
|
||||||
|
&& (left.lastTraceId ?? null) === (right.lastTraceId ?? null)
|
||||||
|
&& valuesEquivalent(left.projection ?? null, right.projection ?? null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function messageListsEquivalent(left: ChatMessage[], right: ChatMessage[]): boolean {
|
||||||
|
if (left === right) return true;
|
||||||
|
if (left.length !== right.length) return false;
|
||||||
|
for (let index = 0; index < left.length; index += 1) {
|
||||||
|
if (!valuesEquivalent(left[index], right[index])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function valuesEquivalent(left: unknown, right: unknown): boolean {
|
||||||
|
if (Object.is(left, right)) return true;
|
||||||
|
if (!left || !right || typeof left !== "object" || typeof right !== "object") return false;
|
||||||
|
if (Array.isArray(left) || Array.isArray(right)) {
|
||||||
|
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) return false;
|
||||||
|
for (let index = 0; index < left.length; index += 1) {
|
||||||
|
if (!valuesEquivalent(left[index], right[index])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const leftRecord = left as Record<string, unknown>;
|
||||||
|
const rightRecord = right as Record<string, unknown>;
|
||||||
|
const leftKeys = Object.keys(leftRecord).filter((key) => leftRecord[key] !== undefined).sort();
|
||||||
|
const rightKeys = Object.keys(rightRecord).filter((key) => rightRecord[key] !== undefined).sort();
|
||||||
|
if (leftKeys.length !== rightKeys.length) return false;
|
||||||
|
for (let index = 0; index < leftKeys.length; index += 1) {
|
||||||
|
const key = leftKeys[index];
|
||||||
|
if (key !== rightKeys[index]) return false;
|
||||||
|
if (!valuesEquivalent(leftRecord[key], rightRecord[key])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function projectSessionRecord(session: WorkbenchSessionRecord | null | undefined, authority: SessionStatusAuthority | null | undefined): WorkbenchSessionRecord | null {
|
function projectSessionRecord(session: WorkbenchSessionRecord | null | undefined, authority: SessionStatusAuthority | null | undefined): WorkbenchSessionRecord | null {
|
||||||
if (!session) return null;
|
if (!session) return null;
|
||||||
if (!authority?.status || normalizedMessageStatus(session.status) === "archived") return session;
|
if (!authority?.status || normalizedMessageStatus(session.status) === "archived") return session;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
|||||||
import { test } from "bun:test";
|
import { test } from "bun:test";
|
||||||
|
|
||||||
import type { ChatMessage } from "@/types";
|
import type { ChatMessage } from "@/types";
|
||||||
import { projectionMergeCommitSummary, workbenchSessionMessagesReadKey } from "./workbench-session-messages-read-budget";
|
import { projectionMergeCommitSummary, workbenchSessionDetailReadKey, workbenchSessionMessagesReadKey } from "./workbench-session-messages-read-budget";
|
||||||
import { selectActiveTurnStatusRefreshTraceIds } from "./workbench-session";
|
import { selectActiveTurnStatusRefreshTraceIds } from "./workbench-session";
|
||||||
|
|
||||||
function message(id: string): ChatMessage {
|
function message(id: string): ChatMessage {
|
||||||
@@ -19,6 +19,14 @@ test("session messages singleflight key separates limit and force class", () =>
|
|||||||
assert.equal(limit20, workbenchSessionMessagesReadKey({ sessionId: "ses_key", limit: 20, force: false }));
|
assert.equal(limit20, workbenchSessionMessagesReadKey({ sessionId: "ses_key", limit: 20, force: false }));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("session detail singleflight key separates force freshness class", () => {
|
||||||
|
const fresh = workbenchSessionDetailReadKey({ sessionId: "ses_key", force: false });
|
||||||
|
const forced = workbenchSessionDetailReadKey({ sessionId: "ses_key", force: true });
|
||||||
|
|
||||||
|
assert.notEqual(fresh, forced);
|
||||||
|
assert.equal(fresh, workbenchSessionDetailReadKey({ sessionId: "ses_key" }));
|
||||||
|
});
|
||||||
|
|
||||||
test("projection merge summary reports no write without blocking follow-up hydrate", () => {
|
test("projection merge summary reports no write without blocking follow-up hydrate", () => {
|
||||||
const existing = [{ ...message("msg_same"), traceId: "trc_current", sessionId: "ses_same" }];
|
const existing = [{ ...message("msg_same"), traceId: "trc_current", sessionId: "ses_same" }];
|
||||||
const summary = projectionMergeCommitSummary(existing, existing);
|
const summary = projectionMergeCommitSummary(existing, existing);
|
||||||
|
|||||||
@@ -11,11 +11,25 @@ export interface SessionMessagesReadKeyInput {
|
|||||||
force?: boolean | null;
|
force?: boolean | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SessionDetailReadKeyInput {
|
||||||
|
sessionId: string | null | undefined;
|
||||||
|
force?: boolean | null;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ProjectionMergeCommitSummary {
|
export interface ProjectionMergeCommitSummary {
|
||||||
changed: boolean;
|
changed: boolean;
|
||||||
changedCount: number;
|
changedCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function workbenchSessionDetailReadKey(input: SessionDetailReadKeyInput): string {
|
||||||
|
return composeWorkbenchScopedKey(
|
||||||
|
"workbench.session-detail.read",
|
||||||
|
firstNonEmptyString(input.sessionId) ?? "unknown-session",
|
||||||
|
"messages:false",
|
||||||
|
input.force === true ? "force" : "fresh"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function workbenchSessionMessagesReadKey(input: SessionMessagesReadKeyInput): string {
|
export function workbenchSessionMessagesReadKey(input: SessionMessagesReadKeyInput): string {
|
||||||
return composeWorkbenchScopedKey(
|
return composeWorkbenchScopedKey(
|
||||||
"workbench.session-messages.read",
|
"workbench.session-messages.read",
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { agentErrorFromProjection, normalizeApiErrorRecord, normalizeErrorDiagno
|
|||||||
import { readWorkbenchJson, readWorkbenchNumber, readWorkbenchString, removeWorkbenchStorageKey, writeWorkbenchJson, writeWorkbenchString } from "@/utils/workbench-storage-runtime";
|
import { readWorkbenchJson, readWorkbenchNumber, readWorkbenchString, removeWorkbenchStorageKey, writeWorkbenchJson, writeWorkbenchString } from "@/utils/workbench-storage-runtime";
|
||||||
import { createWorkbenchStreamTransportRuntime, type WorkbenchRealtimeEvent, type WorkbenchStreamTransportRecovery } from "@/utils/workbench-realtime-runtime";
|
import { createWorkbenchStreamTransportRuntime, type WorkbenchRealtimeEvent, type WorkbenchStreamTransportRecovery } from "@/utils/workbench-realtime-runtime";
|
||||||
import { mergeRunnerTrace, snapshotToRunnerTrace, type TraceSnapshot } from "@/composables/useTraceSubscription";
|
import { mergeRunnerTrace, snapshotToRunnerTrace, type TraceSnapshot } from "@/composables/useTraceSubscription";
|
||||||
import type { WorkbenchMessagePageResponse } from "@/api/workbench";
|
import type { WorkbenchMessagePageResponse, WorkbenchSessionDetailResponse } from "@/api/workbench";
|
||||||
import type { AgentChatResponse, AgentChatResultResponse, AgentRunProvenance, ApiError, ApiResult, ChatMessage, ErrorDiagnostic, LiveSurface, ProjectionBlocker, ProjectionDiagnostic, ProviderProfile, TraceEvent, WorkbenchSessionRecord, WorkbenchTurnTimingProjection } from "@/types";
|
import type { AgentChatResponse, AgentChatResultResponse, AgentRunProvenance, ApiError, ApiResult, ChatMessage, ErrorDiagnostic, LiveSurface, ProjectionBlocker, ProjectionDiagnostic, ProviderProfile, TraceEvent, WorkbenchSessionRecord, WorkbenchTurnTimingProjection } from "@/types";
|
||||||
import { firstNonEmptyString, nextProtocolId, normalizeWorkbenchSessionId, normalizeWorkbenchSessionRouteId } from "@/utils";
|
import { firstNonEmptyString, nextProtocolId, normalizeWorkbenchSessionId, normalizeWorkbenchSessionRouteId } from "@/utils";
|
||||||
import { composeWorkbenchScopedKey } from "@/utils/workbench-key";
|
import { composeWorkbenchScopedKey } from "@/utils/workbench-key";
|
||||||
@@ -70,7 +70,7 @@ import { planWorkbenchRealtimeApply, planWorkbenchRealtimeRecovery, type Workben
|
|||||||
import { useWorkbenchColadaMutations } from "./workbench-colada-mutations";
|
import { useWorkbenchColadaMutations } from "./workbench-colada-mutations";
|
||||||
import { useWorkbenchColadaQueries } from "./workbench-colada-queries";
|
import { useWorkbenchColadaQueries } from "./workbench-colada-queries";
|
||||||
import { useWorkbenchColadaReducer } from "./workbench-colada-reducer";
|
import { useWorkbenchColadaReducer } from "./workbench-colada-reducer";
|
||||||
import { projectionMergeCommitSummary, workbenchSessionMessagesReadKey } from "./workbench-session-messages-read-budget";
|
import { projectionMergeCommitSummary, workbenchSessionDetailReadKey, workbenchSessionMessagesReadKey } from "./workbench-session-messages-read-budget";
|
||||||
|
|
||||||
const WORKBENCH_SESSION_PROJECTION_SIGNAL_CHANNEL = "hwlab.workbench.sessionProjection.v1";
|
const WORKBENCH_SESSION_PROJECTION_SIGNAL_CHANNEL = "hwlab.workbench.sessionProjection.v1";
|
||||||
const WORKBENCH_SESSION_PROJECTION_SIGNAL_KEY = "hwlab.workbench.sessionProjectionSignal.v1";
|
const WORKBENCH_SESSION_PROJECTION_SIGNAL_KEY = "hwlab.workbench.sessionProjectionSignal.v1";
|
||||||
@@ -92,6 +92,11 @@ interface SessionMessagesReadOptions {
|
|||||||
force?: boolean;
|
force?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SessionDetailReadOptions {
|
||||||
|
reason: string;
|
||||||
|
force?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const useWorkbenchStore = defineStore("workbench", () => {
|
export const useWorkbenchStore = defineStore("workbench", () => {
|
||||||
const runtimePolicy = workbenchRuntimePolicy();
|
const runtimePolicy = workbenchRuntimePolicy();
|
||||||
const workbenchColadaReducer = useWorkbenchColadaReducer();
|
const workbenchColadaReducer = useWorkbenchColadaReducer();
|
||||||
@@ -100,6 +105,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
|||||||
const turnStatusReadSingleflight = createKeyedSingleflight<ApiResult<AgentChatResultResponse>>();
|
const turnStatusReadSingleflight = createKeyedSingleflight<ApiResult<AgentChatResultResponse>>();
|
||||||
const traceHydrationSingleflight = createKeyedSingleflight<void>();
|
const traceHydrationSingleflight = createKeyedSingleflight<void>();
|
||||||
const sessionMessagesReadSingleflight = createKeyedSingleflight<ApiResult<WorkbenchMessagePageResponse>>();
|
const sessionMessagesReadSingleflight = createKeyedSingleflight<ApiResult<WorkbenchMessagePageResponse>>();
|
||||||
|
const sessionDetailReadSingleflight = createKeyedSingleflight<ApiResult<WorkbenchSessionDetailResponse>>();
|
||||||
const providerProfile = ref<ProviderProfile>(readString("hwlab.workbench.providerProfile.v1", "codex"));
|
const providerProfile = ref<ProviderProfile>(readString("hwlab.workbench.providerProfile.v1", "codex"));
|
||||||
const providerOptions = ref<ProviderProfileOption[]>(defaultProviderProfileOptions(providerProfile.value));
|
const providerOptions = ref<ProviderProfileOption[]>(defaultProviderProfileOptions(providerProfile.value));
|
||||||
const recentDrafts = ref<DraftEntry[]>(readRecentDrafts());
|
const recentDrafts = ref<DraftEntry[]>(readRecentDrafts());
|
||||||
@@ -553,6 +559,11 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
|||||||
return sessionMessagesReadSingleflight.run(key, () => workbenchColadaQueries.fetchSessionMessages(sessionId, { limit: options.limit, minIntervalMs: runtimePolicy.workbenchSessionMessagesMinRefreshMs, force: options.force }), { reason: options.reason });
|
return sessionMessagesReadSingleflight.run(key, () => workbenchColadaQueries.fetchSessionMessages(sessionId, { limit: options.limit, minIntervalMs: runtimePolicy.workbenchSessionMessagesMinRefreshMs, force: options.force }), { reason: options.reason });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fetchSessionDetailPage(sessionId: string, options: SessionDetailReadOptions): Promise<ApiResult<WorkbenchSessionDetailResponse>> {
|
||||||
|
const key = workbenchSessionDetailReadKey({ sessionId, force: options.force });
|
||||||
|
return sessionDetailReadSingleflight.run(key, () => workbenchColadaQueries.fetchSession(sessionId, { includeMessages: false, minIntervalMs: runtimePolicy.workbenchSessionDetailMinRefreshMs, force: options.force }), { reason: options.reason });
|
||||||
|
}
|
||||||
|
|
||||||
function sessionMessageProjectionWindowLimit(): number {
|
function sessionMessageProjectionWindowLimit(): number {
|
||||||
return boundedProjectionMessageLimit(runtimePolicy.workbenchSessionMessagesWindowLimit, runtimePolicy.sessionListPageLimit);
|
return boundedProjectionMessageLimit(runtimePolicy.workbenchSessionMessagesWindowLimit, runtimePolicy.sessionListPageLimit);
|
||||||
}
|
}
|
||||||
@@ -1413,15 +1424,20 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
recordActivity(reason);
|
recordActivity(reason);
|
||||||
const existing = sessions.value.find((item) => item.sessionId === id) ?? null;
|
const detail = await fetchSessionDetailPage(id, { reason: `realtime-session-detail:${reason}` });
|
||||||
const selected = await loadWorkbenchSession(id, existing);
|
if (!detail.ok || activeSessionId.value !== id) return;
|
||||||
if (selected && activeSessionId.value === id) {
|
const detailSession = sessionFromWorkbenchSession(detail.data?.session, { includeMessages: false });
|
||||||
|
if (detailSession) {
|
||||||
|
const existingMessages = serverState.value.messagesBySessionId[id] ?? [];
|
||||||
|
rememberSessionDetail({ ...detailSession, messages: existingMessages, messageCount: detailSession.messageCount ?? existingMessages.length });
|
||||||
if (traceTerminalBodyIsVisible(traceId, id)) {
|
if (traceTerminalBodyIsVisible(traceId, id)) {
|
||||||
recordWorkbenchRuntimeDiagnostic({ module: "workbench-terminal-priority", sessionId: id, traceId, outcome: "ok", diagnostic: { code: "terminal_low_priority_session_detail_apply_skip", reason, source: "realtime-session-detail", valuesRedacted: true } });
|
recordWorkbenchRuntimeDiagnostic({ module: "workbench-terminal-priority", sessionId: id, traceId, outcome: "ok", diagnostic: { code: "terminal_low_priority_session_detail_apply_skip", reason, source: "realtime-session-detail", valuesRedacted: true } });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
applySelectedSessionDetail(selected, "system");
|
|
||||||
}
|
}
|
||||||
|
const activeMessages = serverState.value.messagesBySessionId[id] ?? messages.value;
|
||||||
|
await hydrateTurnStatusAuthority(activeMessages, { traceId, limit: 1, reason: "realtime-session-detail" });
|
||||||
|
if (traceId && !traceProjectionIsTerminalSealed(traceId, activeMessages)) hydrateTerminalTraceGaps(selectProjectionMessageWindow(activeMessages, { traceId, limit: traceMessageProjectionWindowLimit() }), `realtime-session-detail:${reason}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function completeTrace(traceId: string, result: AgentChatResultResponse, options: { forceRead?: boolean } = {}): void {
|
function completeTrace(traceId: string, result: AgentChatResultResponse, options: { forceRead?: boolean } = {}): void {
|
||||||
@@ -1758,7 +1774,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
|||||||
const normalizedRequestId = normalizeWorkbenchSessionId(requestId);
|
const normalizedRequestId = normalizeWorkbenchSessionId(requestId);
|
||||||
const messageLimit = sessionMessageProjectionWindowLimit();
|
const messageLimit = sessionMessageProjectionWindowLimit();
|
||||||
const eagerMessages = normalizedRequestId ? fetchSessionMessagesPage(normalizedRequestId, { limit: messageLimit, reason: "load-session:eager", force: true }) : null;
|
const eagerMessages = normalizedRequestId ? fetchSessionMessagesPage(normalizedRequestId, { limit: messageLimit, reason: "load-session:eager", force: true }) : null;
|
||||||
const detail = await workbenchColadaQueries.fetchSession(requestId, { includeMessages: false, force: true, minIntervalMs: runtimePolicy.workbenchSessionDetailMinRefreshMs });
|
const detail = await fetchSessionDetailPage(requestId, { reason: "load-session:detail", force: true });
|
||||||
if (!detail.ok) return null;
|
if (!detail.ok) return null;
|
||||||
const detailSession = sessionFromWorkbenchSession(detail.data?.session, { includeMessages: false });
|
const detailSession = sessionFromWorkbenchSession(detail.data?.session, { includeMessages: false });
|
||||||
const id = detailSession?.sessionId ?? normalizedRequestId ?? seed?.sessionId;
|
const id = detailSession?.sessionId ?? normalizedRequestId ?? seed?.sessionId;
|
||||||
|
|||||||
Reference in New Issue
Block a user