fix: seal workbench terminal final state
This commit is contained in:
@@ -1164,13 +1164,18 @@ test("workbench read model projects terminal result atomically across session, m
|
||||
assert.equal(assistant.text, finalText);
|
||||
assert.equal(assistant.parts[0].status, "completed");
|
||||
assert.equal(assistant.parts[0].type, "final_response");
|
||||
assert.equal(assistant.parts[0].messageId, assistant.messageId);
|
||||
assert.equal(assistant.parts[0].traceId, traceId);
|
||||
assert.equal(assistant.parts[0].text, finalText);
|
||||
|
||||
const turn = await getJson(port, `/v1/workbench/turns/${encodeURIComponent(traceId)}`);
|
||||
assert.equal(turn.status, 200);
|
||||
assert.equal(turn.body.turn.status, "completed");
|
||||
assert.equal(turn.body.turn.terminal, true);
|
||||
assert.equal(turn.body.turn.assistantMessageId, assistant.messageId);
|
||||
assert.equal(turn.body.turn.assistantText, finalText);
|
||||
assert.equal(turn.body.turn.finalResponse.source, "message-part");
|
||||
assert.equal(turn.body.turn.finalResponse.sealed, true);
|
||||
assert.equal(turn.body.turn.finalResponse.text, finalText);
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
||||
|
||||
@@ -83,6 +83,87 @@ test("message timing and terminal status patches stay independent from store sta
|
||||
assert.equal(terminalPatch.durationMs, 3500);
|
||||
});
|
||||
|
||||
test("terminal turn result seals completed final response body", () => {
|
||||
const restored = {
|
||||
role: "agent",
|
||||
status: "running",
|
||||
text: "",
|
||||
traceAutoLifecycle: "running",
|
||||
traceId: "trc_unit_final",
|
||||
runnerTrace: { traceId: "trc_unit_final", status: "running" }
|
||||
} as ChatMessage;
|
||||
|
||||
const terminal = terminalMessagePatchFromTurnResult(restored, {
|
||||
traceId: "trc_unit_final",
|
||||
status: "completed",
|
||||
terminal: true,
|
||||
finalResponse: { text: "final answer" },
|
||||
startedAt: "2026-01-01T00:00:00.000Z",
|
||||
finishedAt: "2026-01-01T00:00:04.000Z",
|
||||
durationMs: 4000
|
||||
} as AgentChatResultResponse);
|
||||
|
||||
assert.equal(terminal?.status, "completed");
|
||||
assert.equal(terminal?.traceAutoLifecycle, "terminal");
|
||||
assert.equal(terminal?.text, "final answer");
|
||||
assert.equal((terminal?.finalResponse as { text?: string } | undefined)?.text, "final answer");
|
||||
});
|
||||
|
||||
test("terminal turn result accepts authoritative assistant trace final", () => {
|
||||
const restored = {
|
||||
role: "agent",
|
||||
status: "running",
|
||||
text: "",
|
||||
traceId: "trc_trace_final",
|
||||
runnerTrace: { traceId: "trc_trace_final", status: "running" }
|
||||
} as ChatMessage;
|
||||
|
||||
const terminal = terminalMessagePatchFromTurnResult(restored, {
|
||||
traceId: "trc_trace_final",
|
||||
status: "completed",
|
||||
terminal: true,
|
||||
runnerTrace: {
|
||||
traceId: "trc_trace_final",
|
||||
status: "completed",
|
||||
events: [
|
||||
{ seq: 1, type: "assistant_message", status: "running", message: "progress only" },
|
||||
{ seq: 2, type: "assistant_message", status: "completed", message: "final from trace", replyAuthority: true, final: true }
|
||||
]
|
||||
}
|
||||
} as unknown as AgentChatResultResponse);
|
||||
|
||||
assert.equal(terminal?.status, "completed");
|
||||
assert.equal(terminal?.text, "final from trace");
|
||||
assert.equal((terminal?.finalResponse as { text?: string } | undefined)?.text, "final from trace");
|
||||
});
|
||||
|
||||
test("terminal failure does not synthesize completed final response from progress text", () => {
|
||||
const restored = {
|
||||
role: "agent",
|
||||
status: "running",
|
||||
text: "",
|
||||
traceId: "trc_failed_progress",
|
||||
runnerTrace: { traceId: "trc_failed_progress", status: "running" }
|
||||
} as ChatMessage;
|
||||
|
||||
const terminal = terminalMessagePatchFromTurnResult(restored, {
|
||||
traceId: "trc_failed_progress",
|
||||
status: "failed",
|
||||
terminal: true,
|
||||
runnerTrace: {
|
||||
traceId: "trc_failed_progress",
|
||||
status: "failed",
|
||||
events: [{ seq: 1, type: "assistant_message", status: "running", message: "progress only" }]
|
||||
},
|
||||
error: { message: "provider failed" }
|
||||
} as unknown as AgentChatResultResponse);
|
||||
|
||||
assert.equal(terminal?.status, "failed");
|
||||
assert.equal(terminal?.text, undefined);
|
||||
assert.equal(terminal?.finalResponse, undefined);
|
||||
assert.equal(terminal?.error?.message, "provider failed");
|
||||
});
|
||||
|
||||
test("terminal seal blocks stale running snapshots after session restore", () => {
|
||||
const sealed = {
|
||||
role: "agent",
|
||||
|
||||
@@ -168,6 +168,50 @@ test("Workbench session messages keep sealed terminal timing on bulk refresh", (
|
||||
assert.equal(agent?.finishedAt, "2026-06-24T00:00:12.000Z");
|
||||
});
|
||||
|
||||
test("Workbench sealed completed final body survives stale running session messages", () => {
|
||||
let state = createWorkbenchServerState();
|
||||
state = reduceWorkbenchServerState(state, {
|
||||
type: "session.status",
|
||||
session: { sessionId: "ses_final_seal", status: "running", lastTraceId: "trc_final_seal" }
|
||||
});
|
||||
state = reduceWorkbenchServerState(state, {
|
||||
type: "session.messages",
|
||||
sessionId: "ses_final_seal",
|
||||
messages: [
|
||||
{ id: "msg_user", messageId: "msg_user", role: "user", title: "用户", text: "ping", status: "sent", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_final_seal", traceId: "trc_final_seal" },
|
||||
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-24T00:00:01.000Z", sessionId: "ses_final_seal", traceId: "trc_final_seal", traceAutoLifecycle: "running", timing: { startedAt: "2026-06-24T00:00:01.000Z", lastEventAt: "2026-06-24T00:00:02.000Z", durationMs: null, valuesRedacted: true }, startedAt: "2026-06-24T00:00:01.000Z", lastEventAt: "2026-06-24T00:00:02.000Z", durationMs: null }
|
||||
]
|
||||
});
|
||||
|
||||
state = reduceWorkbenchServerState(state, {
|
||||
type: "message.snapshot",
|
||||
sessionId: "ses_final_seal",
|
||||
message: { id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "final answer", finalResponse: { text: "final answer", sealed: true }, status: "completed", createdAt: "2026-06-24T00:00:01.000Z", updatedAt: "2026-06-24T00:00:06.000Z", sessionId: "ses_final_seal", traceId: "trc_final_seal", traceAutoLifecycle: "terminal", timing: { startedAt: "2026-06-24T00:00:01.000Z", finishedAt: "2026-06-24T00:00:06.000Z", durationMs: 5_000, valuesRedacted: true }, startedAt: "2026-06-24T00:00:01.000Z", finishedAt: "2026-06-24T00:00:06.000Z", durationMs: 5_000 }
|
||||
});
|
||||
state = reduceWorkbenchServerState(state, {
|
||||
type: "session.messages",
|
||||
sessionId: "ses_final_seal",
|
||||
messages: [
|
||||
{ id: "msg_user", messageId: "msg_user", role: "user", title: "用户", text: "ping", status: "sent", createdAt: "2026-06-24T00:00:00.000Z", sessionId: "ses_final_seal", traceId: "trc_final_seal" },
|
||||
{ id: "msg_agent", messageId: "msg_agent", role: "agent", title: "Code Agent", text: "", status: "running", createdAt: "2026-06-24T00:00:01.000Z", sessionId: "ses_final_seal", traceId: "trc_final_seal", traceAutoLifecycle: "running", timing: { startedAt: "2026-06-24T00:00:01.000Z", lastEventAt: "2026-06-24T00:00:04.000Z", durationMs: null, valuesRedacted: true }, startedAt: "2026-06-24T00:00:01.000Z", lastEventAt: "2026-06-24T00:00:04.000Z", durationMs: null }
|
||||
]
|
||||
});
|
||||
|
||||
const agent = selectActiveMessages(state, "ses_final_seal").find((message) => message.role === "agent");
|
||||
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, "completed");
|
||||
|
||||
state = reduceWorkbenchServerState(state, {
|
||||
type: "session.list",
|
||||
sessions: [{ sessionId: "ses_final_seal", threadId: "thr_final_seal", status: "running", lastTraceId: "trc_final_seal", updatedAt: "2026-06-24T00:00:07.000Z" }]
|
||||
});
|
||||
|
||||
assert.equal(selectSessionStatusAuthority(state).ses_final_seal?.status, "completed");
|
||||
assert.equal(selectActiveSession(state, "ses_final_seal")?.status, "completed");
|
||||
});
|
||||
|
||||
test("Workbench session messages repair terminal zero timing from running projection", () => {
|
||||
let state = createWorkbenchServerState();
|
||||
state = reduceWorkbenchServerState(state, {
|
||||
|
||||
@@ -282,9 +282,11 @@ export function terminalMessagePatchFromTurnResult(message: ChatMessage, result:
|
||||
const error = resultError ?? (clearCompletedDiagnostics ? null : normalizeAgentError(runnerTrace?.error ?? message.error));
|
||||
const projection = clearCompletedDiagnostics ? nonBlockingProjection(resultProjection) : resultProjection ?? runnerTrace.projection ?? message.projection ?? null;
|
||||
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
|
||||
const bodyPatch = terminalMessageBodyPatchFromTurnResult(message, result, resultStatus);
|
||||
return {
|
||||
...messageTimingPatchForMerge(message, result),
|
||||
...messageStatusPatchForTerminalMerge(message, resultStatus, terminal),
|
||||
...bodyPatch,
|
||||
runnerTrace,
|
||||
error,
|
||||
projection,
|
||||
@@ -297,6 +299,87 @@ export function terminalMessagePatchFromTurnResult(message: ChatMessage, result:
|
||||
};
|
||||
}
|
||||
|
||||
function terminalMessageBodyPatchFromTurnResult(message: ChatMessage, result: AgentChatResultResponse, resultStatus: string | null): Partial<ChatMessage> {
|
||||
if (normalizedStatusText(resultStatus) !== "completed") return {};
|
||||
const finalText = terminalFinalResponseTextFromTurnResult(result);
|
||||
if (!finalText) return {};
|
||||
const traceId = firstNonEmptyString(result.traceId, message.traceId, message.runnerTrace?.traceId) ?? null;
|
||||
return {
|
||||
finalResponse: {
|
||||
text: finalText,
|
||||
status: "completed",
|
||||
traceId,
|
||||
sealed: true,
|
||||
source: "turn-result",
|
||||
valuesRedacted: true
|
||||
},
|
||||
text: projectedAgentMessageText({ status: "completed", finalText })
|
||||
};
|
||||
}
|
||||
|
||||
function terminalFinalResponseTextFromTurnResult(result: AgentChatResultResponse): string | null {
|
||||
const record = recordValue(result);
|
||||
const direct = firstNonEmptyString(
|
||||
finalResponseText(record?.finalResponse),
|
||||
messageText(record?.assistantText),
|
||||
messageText(record?.finalText),
|
||||
messageText(record?.reply),
|
||||
messageText(record?.text),
|
||||
messageText(record?.content),
|
||||
messageText(record?.message)
|
||||
);
|
||||
if (direct) return direct;
|
||||
const resultTrace = recordValue(record?.runnerTrace);
|
||||
return firstNonEmptyString(
|
||||
terminalEvidenceFinalText(record?.terminalEvidence),
|
||||
terminalEvidenceFinalText(resultTrace?.terminalEvidence),
|
||||
terminalAssistantFinalEventText(firstArray(record?.events, record?.traceEvents, resultTrace?.events))
|
||||
) ?? null;
|
||||
}
|
||||
|
||||
function terminalEvidenceFinalText(value: unknown): string | null {
|
||||
const record = recordValue(value);
|
||||
if (!record) return null;
|
||||
return firstNonEmptyString(
|
||||
finalResponseText(record.finalResponse),
|
||||
messageText(record.assistantText),
|
||||
messageText(record.finalText),
|
||||
messageText(record.reply),
|
||||
messageText(record.text),
|
||||
messageText(record.content),
|
||||
messageText(record.message),
|
||||
terminalEvidenceFinalText(record.evidence),
|
||||
terminalEvidenceFinalText(record.payload)
|
||||
) ?? null;
|
||||
}
|
||||
|
||||
function terminalAssistantFinalEventText(events: TraceEvent[]): string | null {
|
||||
for (const event of [...events].reverse()) {
|
||||
if (!traceEventHasTerminalEvidence(event)) continue;
|
||||
const record = recordValue(event);
|
||||
if (!record) continue;
|
||||
const payload = recordValue(record.payload);
|
||||
const text = firstNonEmptyString(
|
||||
finalResponseText(record.finalResponse),
|
||||
messageText(record.assistantText),
|
||||
messageText(record.finalText),
|
||||
messageText(record.reply),
|
||||
messageText(record.text),
|
||||
messageText(record.content),
|
||||
messageText(record.message),
|
||||
finalResponseText(payload?.finalResponse),
|
||||
messageText(payload?.assistantText),
|
||||
messageText(payload?.finalText),
|
||||
messageText(payload?.reply),
|
||||
messageText(payload?.text),
|
||||
messageText(payload?.content),
|
||||
messageText(payload?.message)
|
||||
);
|
||||
if (text) return text;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function firstPositiveFiniteNumber(...values: unknown[]): number | null {
|
||||
for (const value of values) {
|
||||
const number = firstFiniteNumber(value);
|
||||
|
||||
@@ -110,14 +110,15 @@ 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 merged = mergeSessionRecord(existing, { ...session, messages });
|
||||
const sessionStatus = sessionStatusAuthorityFromDetail(session);
|
||||
const messageStatus = sessionStatusAuthorityFromMessages(sessionId, messages);
|
||||
const merged = mergeSessionRecord(existing, { ...session, status: messageStatus?.status ?? session.status, messages });
|
||||
const sessionStatus = messageStatus ?? sessionStatusAuthorityFromDetail(session);
|
||||
return {
|
||||
...state,
|
||||
sessionOrder: state.sessionOrder.includes(sessionId) ? state.sessionOrder : [sessionId, ...state.sessionOrder],
|
||||
sessionsById: { ...state.sessionsById, [sessionId]: merged },
|
||||
sessionStatusById: sessionStatus
|
||||
? { ...state.sessionStatusById, [sessionId]: sessionStatus }
|
||||
? { ...state.sessionStatusById, [sessionId]: mergeSessionStatusAuthority(state.sessionStatusById[sessionId], sessionStatus) }
|
||||
: state.sessionStatusById,
|
||||
messagesBySessionId: {
|
||||
...state.messagesBySessionId,
|
||||
@@ -138,13 +139,14 @@ 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 merged = mergeSessionRecord(existing, { ...session, messages });
|
||||
const sessionStatus = sessionStatusAuthorityFromDetail(session);
|
||||
const messageStatus = sessionStatusAuthorityFromMessages(sessionId, messages);
|
||||
const merged = mergeSessionRecord(existing, { ...session, status: messageStatus?.status ?? session.status, messages });
|
||||
const sessionStatus = messageStatus ?? sessionStatusAuthorityFromDetail(session);
|
||||
return {
|
||||
...state,
|
||||
sessionsById: { ...state.sessionsById, [sessionId]: merged },
|
||||
sessionStatusById: sessionStatus
|
||||
? { ...state.sessionStatusById, [sessionId]: sessionStatus }
|
||||
? { ...state.sessionStatusById, [sessionId]: mergeSessionStatusAuthority(state.sessionStatusById[sessionId], sessionStatus) }
|
||||
: state.sessionStatusById
|
||||
};
|
||||
}
|
||||
@@ -154,10 +156,14 @@ function reduceSessionMessages(state: WorkbenchServerState, sessionId: string |
|
||||
const existing = state.sessionsById[sessionId];
|
||||
const existingMessages = state.messagesBySessionId[sessionId] ?? existing?.messages ?? [];
|
||||
const mergedMessages = mergeMessageList(existingMessages, messages);
|
||||
const session = existing ? { ...existing, messages: mergedMessages, messageCount: mergedMessages.length } : null;
|
||||
const reconciledStatus = sessionStatusAuthorityFromMessages(sessionId, mergedMessages);
|
||||
const session = existing ? { ...existing, status: reconciledStatus?.status ?? existing.status, 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 }
|
||||
};
|
||||
}
|
||||
@@ -224,18 +230,26 @@ function sealExistingRunningMessageTiming(existing: ChatMessage, incoming: ChatM
|
||||
function sealExistingTerminalMessageTiming(existing: ChatMessage, incoming: ChatMessage): ChatMessage {
|
||||
if (!isTerminalMessageStatus(existing.status)) return incoming;
|
||||
const timing = sealedTerminalTimingProjection(existing, incoming);
|
||||
if (!timing || timing.durationMs == null) return incoming;
|
||||
const patch: Partial<ChatMessage> = {
|
||||
status: existing.status,
|
||||
const patch: Partial<ChatMessage> = sealedTerminalMessagePatch(existing);
|
||||
if (timing && timing.durationMs != null) Object.assign(patch, {
|
||||
timing,
|
||||
startedAt: timing.startedAt ?? null,
|
||||
lastEventAt: timing.lastEventAt ?? null,
|
||||
finishedAt: timing.finishedAt ?? null,
|
||||
durationMs: timing.durationMs ?? null,
|
||||
durationMs: timing.durationMs ?? null
|
||||
});
|
||||
return { ...incoming, ...patch };
|
||||
}
|
||||
|
||||
function sealedTerminalMessagePatch(existing: ChatMessage): Partial<ChatMessage> {
|
||||
const patch: Partial<ChatMessage> = {
|
||||
status: existing.status,
|
||||
traceAutoLifecycle: existing.traceAutoLifecycle ?? "terminal"
|
||||
};
|
||||
if (typeof existing.text === "string" && existing.text.trim()) patch.text = existing.text;
|
||||
return { ...incoming, ...patch };
|
||||
const finalResponse = (existing as Record<string, unknown>).finalResponse;
|
||||
if (finalResponse) patch.finalResponse = finalResponse;
|
||||
return patch;
|
||||
}
|
||||
|
||||
function sealTerminalTransitionMessageTiming(existing: ChatMessage, incoming: ChatMessage): ChatMessage {
|
||||
@@ -360,6 +374,72 @@ function sessionStatusAuthorityFromDetail(session: WorkbenchSessionRecord): Sess
|
||||
};
|
||||
}
|
||||
|
||||
function sessionStatusAuthorityFromMessages(sessionId: string, messages: ChatMessage[]): SessionStatusAuthority | null {
|
||||
const message = [...messages].reverse().find((item) => messageHasCompletedFinalResponse(item));
|
||||
if (!message) return null;
|
||||
return {
|
||||
sessionId,
|
||||
status: "completed",
|
||||
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()
|
||||
};
|
||||
}
|
||||
|
||||
function mergeSessionStatusAuthority(existing: SessionStatusAuthority | undefined, incoming: SessionStatusAuthority): SessionStatusAuthority {
|
||||
if (existing?.status === "completed" && incoming.status !== "completed" && isSameTraceAuthority(existing, incoming) && isRunningSessionStatus(incoming.status)) return existing;
|
||||
return {
|
||||
...(existing ?? {}),
|
||||
...incoming,
|
||||
projection: incoming.projection ?? existing?.projection ?? null,
|
||||
loadedAt: incoming.loadedAt ?? existing?.loadedAt ?? null
|
||||
};
|
||||
}
|
||||
|
||||
function isSameTraceAuthority(left: SessionStatusAuthority, right: SessionStatusAuthority): 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));
|
||||
}
|
||||
|
||||
function messageHasCompletedFinalResponse(message: ChatMessage): boolean {
|
||||
if (message.role !== "agent") return false;
|
||||
if (normalizedMessageStatus(message.status) !== "completed") return false;
|
||||
return Boolean(messageFinalResponseText(message));
|
||||
}
|
||||
|
||||
function messageFinalResponseText(message: ChatMessage): string | null {
|
||||
return textValue(message.text) ?? 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;
|
||||
if (Array.isArray(value)) return value.map(nestedTextValue).filter((item): item is string => Boolean(item)).join("\n") || null;
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const record = value as Record<string, unknown>;
|
||||
return textValue(record.text) ?? textValue(record.content) ?? textValue(record.message) ?? textValue(record.summary) ?? textValue(record.preview);
|
||||
}
|
||||
|
||||
function textValue(value: unknown): string | null {
|
||||
if (typeof value !== "string") return null;
|
||||
const text = value.trim();
|
||||
return text || null;
|
||||
}
|
||||
|
||||
function projectionFromSessionRecord(session: WorkbenchSessionRecord): ProjectionDiagnostic | null {
|
||||
const turnSummary = session.turnSummary && typeof session.turnSummary === "object" ? session.turnSummary as Record<string, unknown> : null;
|
||||
const direct = session.projection && typeof session.projection === "object" ? session.projection : null;
|
||||
|
||||
Reference in New Issue
Block a user