Merge pull request #2347 from pikasTech/fix/1445-lane-a-web-tsc-gate

fix: harden Cloud Web tsc gate
This commit is contained in:
Lyon
2026-07-02 17:25:16 +08:00
committed by GitHub
9 changed files with 173 additions and 57 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
"scripts": {
"deps": "node ../../scripts/worktree-deps.mjs --web",
"dev": "bun run deps --quiet && vite",
"check": "bun run deps --quiet && bun run scripts/check.ts && bun run scripts/tsc-check.ts && bun test scripts",
"check": "bun run deps --quiet && bun run scripts/tsc-check.ts && bun test scripts",
"check:tsc": "bun run deps --quiet && bun run scripts/tsc-check.ts",
"check:tsc-strict": "bun run deps --quiet && bun run scripts/tsc-check.ts --strict",
"typecheck": "bun run deps --quiet && vue-tsc --noEmit",
+82 -24
View File
@@ -4,41 +4,94 @@ import path from "node:path";
import { fileURLToPath } from "node:url";
const rootDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const sourceFiles = collectSourceFiles(path.join(rootDir, "src"));
const explicitAnyLabel = "explicit " + "any";
const sourceFiles = [
...collectSourceFiles(path.join(rootDir, "src")),
...collectSourceFiles(path.join(rootDir, "scripts"))
];
let explicitAnyCount = 0;
for (const file of sourceFiles) {
const text = readFileSync(file, "utf8");
const stripped = stripComments(text);
const matches = [
...stripped.matchAll(/:\s*any\b/gu),
...stripped.matchAll(/<\s*any\s*>/gu),
...stripped.matchAll(/as\s+any\b/gu),
...stripped.matchAll(/Array\s*<\s*any\s*>/gu)
];
if (matches.length > 0) {
explicitAnyCount += matches.length;
console.error(` ${path.relative(rootDir, file)}: ${matches.length} explicit any usage(s)`);
const stripped = stripCommentsAndStrings(text);
const count = countExplicitAny(stripped);
if (count > 0) {
explicitAnyCount += count;
console.error(` ${path.relative(rootDir, file)}: ${count} ${explicitAnyLabel} usage(s)`);
}
}
if (explicitAnyCount > 0) {
console.error(`hwlab-cloud-web tsc-check: ${explicitAnyCount} explicit any usage(s) in Vue source`);
process.exit(2);
console.error(`hwlab-cloud-web tsc-check: ${explicitAnyCount} ${explicitAnyLabel} usage(s) in Web source`);
} else {
console.log(`hwlab-cloud-web tsc-check: ${explicitAnyLabel} scan passed (0 usages)`);
}
const sourceShapeStatus = runBunScript("source-shape", path.join(rootDir, "scripts/check.ts"));
const vueTscBin = resolveVueTscBin(rootDir);
let vueTscStatus = 0;
if (!vueTscBin) {
console.error("hwlab-cloud-web tsc-check: vue-tsc not found in ancestor node_modules/.bin or PATH");
process.exit(2);
vueTscStatus = 2;
} else {
const result = spawnSync(vueTscBin, ["--noEmit", "--project", path.join(rootDir, "tsconfig.json")], { cwd: rootDir, encoding: "utf8" });
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
if (result.error) console.error(`hwlab-cloud-web tsc-check: failed to start ${vueTscBin}: ${result.error.message}`);
vueTscStatus = result.status ?? (result.error ? 2 : 0);
if (vueTscStatus === 0) {
console.log("hwlab-cloud-web tsc-check: vue-tsc passed");
} else {
console.error(`hwlab-cloud-web tsc-check: vue-tsc failed with exit ${result.status ?? "unknown"}`);
}
}
const result = spawnSync(vueTscBin, ["--noEmit", "--project", path.join(rootDir, "tsconfig.json")], { cwd: rootDir, encoding: "utf8" });
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
if (result.error) console.error(`hwlab-cloud-web tsc-check: failed to start ${vueTscBin}: ${result.error.message}`);
if (result.status !== 0) {
console.error(`hwlab-cloud-web tsc-check: vue-tsc failed with exit ${result.status ?? "unknown"}`);
process.exit(result.status ?? 2);
if (explicitAnyCount > 0 || sourceShapeStatus !== 0 || vueTscStatus !== 0) {
console.error(`hwlab-cloud-web tsc-check: failed (source-shape=${sourceShapeStatus}, vue-tsc=${vueTscStatus}, ${explicitAnyLabel}=${explicitAnyCount})`);
process.exit(firstFailureStatus(sourceShapeStatus, vueTscStatus, explicitAnyCount > 0 ? 2 : 0));
}
console.log(`hwlab-cloud-web tsc-check: passed (source-shape, vue-tsc, 0 ${explicitAnyLabel})`);
function runBunScript(label: string, scriptPath: string): number {
const result = spawnSync(process.execPath, ["run", scriptPath], { cwd: rootDir, encoding: "utf8" });
if (result.stdout) process.stdout.write(result.stdout);
if (result.stderr) process.stderr.write(result.stderr);
if (result.error) console.error(`hwlab-cloud-web tsc-check: failed to start ${label}: ${result.error.message}`);
const status = result.status ?? (result.error ? 2 : 0);
if (status === 0) console.log(`hwlab-cloud-web tsc-check: ${label} passed`);
else console.error(`hwlab-cloud-web tsc-check: ${label} failed with exit ${result.status ?? "unknown"}`);
return status;
}
function firstFailureStatus(...statuses: number[]): number {
return statuses.find((status) => status !== 0) ?? 2;
}
function explicitAnyPatterns(): RegExp[] {
const token = "a" + "ny";
return [
new RegExp(`:\\s*${token}\\b`, "gu"),
new RegExp(`\\bas\\s+${token}\\b`, "gu"),
new RegExp(`<[^>\\n]*\\b${token}\\b[^>\\n]*>`, "gu"),
new RegExp(`\\b${token}\\s*\\[\\s*\\]`, "gu"),
new RegExp(`\\b${token}\\s*(?:[|&;,)=]|$)`, "gu")
];
}
function countExplicitAny(strippedSource: string): number {
const token = "a" + "ny";
const tokenPattern = new RegExp(`\\b${token}\\b`, "gu");
const positions = new Set<number>();
for (const pattern of explicitAnyPatterns()) {
for (const match of strippedSource.matchAll(pattern)) {
const index = match.index;
if (index === undefined) continue;
for (const tokenMatch of match[0].matchAll(tokenPattern)) {
if (tokenMatch.index !== undefined) positions.add(index + tokenMatch.index);
}
}
}
return positions.size;
}
console.log("hwlab-cloud-web tsc-check: passed (strict Vue TS, 0 explicit any)");
function resolveVueTscBin(startDir: string): string | null {
let current = startDir;
@@ -76,6 +129,11 @@ function collectSourceFiles(dir: string): string[] {
return out;
}
function stripComments(text: string): string {
return text.replace(/\/\*[\s\S]*?\*\//gu, "").replace(/(^|[^:])\/\/.*$/gmu, "$1");
function stripCommentsAndStrings(text: string): string {
return text
.replace(/\/\*[\s\S]*?\*\//gu, "")
.replace(/(^|[^:])\/\/.*$/gmu, "$1")
.replace(/`(?:\\[\s\S]|[^`\\])*`/gu, "``")
.replace(/"(?:\\.|[^"\\])*"/gu, '""')
.replace(/'(?:\\.|[^'\\])*'/gu, "''");
}
@@ -202,16 +202,22 @@ test("terminal seal blocks stale running snapshots after session restore", () =>
test("terminal projection page seal preserves final response object", () => {
const previous = {
id: "msg_terminal_projection_previous",
role: "agent",
title: "Code Agent",
status: "completed",
createdAt: "2026-01-01T00:00:00.000Z",
text: "final answer",
finalResponse: { text: "final answer", status: "completed", sealed: true },
traceAutoLifecycle: "terminal",
timing: { startedAt: "2026-01-01T00:00:00.000Z", finishedAt: "2026-01-01T00:00:05.000Z", durationMs: 5000, valuesRedacted: true }
} as ChatMessage;
const incoming = {
id: "msg_terminal_projection_incoming",
role: "agent",
title: "Code Agent",
status: "running",
createdAt: "2026-01-01T00:00:00.000Z",
text: "",
traceAutoLifecycle: "running",
timing: { startedAt: "2026-01-01T00:00:00.000Z", valuesRedacted: true }
@@ -1,6 +1,7 @@
import assert from "node:assert/strict";
import { test } from "bun:test";
import type { AgentChatResultResponse, ChatMessage } from "../types";
import {
messageHasTerminalResponse,
messageNeedsTerminalDiagnostics,
@@ -10,10 +11,29 @@ import {
turnResultStatusForMerge
} from "./workbench-message-projection-runtime";
function agentMessage(input: Partial<ChatMessage> & Pick<ChatMessage, "id" | "traceId">): ChatMessage {
return {
role: "agent",
title: "Code Agent",
text: "",
status: "running",
createdAt: "2026-07-01T00:00:00.000Z",
...input
};
}
function turnResult(input: AgentChatResultResponse): AgentChatResultResponse {
return input;
}
function record(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" ? value as Record<string, unknown> : null;
}
test("turn result merge keeps completed result without final response unsealed", () => {
const traceId = "trc_frontend_terminal_without_final";
const message = { id: "msg_frontend_terminal_without_final", role: "agent", status: "running", traceId } as any;
const result = {
const message = agentMessage({ id: "msg_frontend_terminal_without_final", status: "running", traceId });
const result = turnResult({
traceId,
status: "completed",
terminal: true,
@@ -26,7 +46,7 @@ test("turn result merge keeps completed result without final response unsealed",
{ seq: 2, type: "terminal_status", terminal: true, terminalStatus: "completed" }
]
}
} as any;
});
assert.equal(turnResultStatusForMerge(result), "running");
assert.equal(turnResultIsTerminalForMerge(result), false);
@@ -35,45 +55,45 @@ test("turn result merge keeps completed result without final response unsealed",
test("turn result merge seals completed result with final response", () => {
const traceId = "trc_frontend_terminal_with_final";
const message = { id: "msg_frontend_terminal_with_final", role: "agent", status: "running", traceId } as any;
const result = {
const message = agentMessage({ id: "msg_frontend_terminal_with_final", status: "running", traceId });
const result = turnResult({
traceId,
status: "completed",
terminal: true,
finalResponse: { text: "final answer" }
} as any;
});
const patch = terminalMessagePatchFromTurnResult(message, result);
assert.equal(turnResultStatusForMerge(result), "completed");
assert.equal(turnResultIsTerminalForMerge(result), true);
assert.equal(patch?.status, "completed");
assert.equal((patch?.finalResponse as any)?.text, "final answer");
assert.equal(record(patch?.finalResponse)?.text, "final answer");
assert.equal(patch?.text, "final answer");
});
test("turn result merge seals failed result with failure final response", () => {
const traceId = "trc_frontend_terminal_failed_with_body";
const message = { id: "msg_frontend_terminal_failed_with_body", role: "agent", status: "running", traceId } as any;
const result = {
const message = agentMessage({ id: "msg_frontend_terminal_failed_with_body", status: "running", traceId });
const result = turnResult({
traceId,
status: "failed",
terminal: true,
finalResponse: { text: "Workbench terminal failed: provider-stream-disconnected", status: "failed" }
} as any;
});
const patch = terminalMessagePatchFromTurnResult(message, result);
assert.equal(turnResultStatusForMerge(result), "failed");
assert.equal(turnResultIsTerminalForMerge(result), true);
assert.equal(patch?.status, "failed");
assert.equal((patch?.finalResponse as any)?.status, "failed");
assert.equal((patch?.finalResponse as any)?.text, "Workbench terminal failed: provider-stream-disconnected");
assert.equal(record(patch?.finalResponse)?.status, "failed");
assert.equal(record(patch?.finalResponse)?.text, "Workbench terminal failed: provider-stream-disconnected");
assert.equal(patch?.text, "Workbench terminal failed: provider-stream-disconnected");
});
test("terminal response helpers treat failed body as sealed authority", () => {
const traceId = "trc_frontend_terminal_failed_helper";
const sealed = { id: "msg_frontend_terminal_failed_helper", role: "agent", status: "failed", traceId, text: "Workbench terminal failed: provider-stream-disconnected", finalResponse: { text: "Workbench terminal failed: provider-stream-disconnected" } } as any;
const unsealed = { id: "msg_frontend_terminal_failed_helper_unsealed", role: "agent", status: "failed", traceId } as any;
const sealed = agentMessage({ id: "msg_frontend_terminal_failed_helper", status: "failed", traceId, text: "Workbench terminal failed: provider-stream-disconnected", finalResponse: { text: "Workbench terminal failed: provider-stream-disconnected" } });
const unsealed = agentMessage({ id: "msg_frontend_terminal_failed_helper_unsealed", status: "failed", traceId });
assert.equal(messageHasTerminalResponse(sealed), true);
assert.equal(messageNeedsTerminalDiagnostics(sealed), false);
@@ -42,6 +42,8 @@ export type WorkbenchRealtimeRecoveryStep =
| { type: "refresh-turn-status"; traceId: string; reason: string; force: false; authority: WorkbenchRealtimeRecoveryAuthority }
| { type: "hydrate-trace-events"; traceId: string; reason: string; force: false; authority: WorkbenchRealtimeRecoveryAuthority };
type WorkbenchRealtimeRecoveryStepInput<T> = T extends WorkbenchRealtimeRecoveryStep ? Omit<T, "authority" | "force"> : never;
export interface WorkbenchRealtimeRecoveryPlan {
sessionId: string | null;
traceId: string | null;
@@ -94,7 +96,7 @@ export function planWorkbenchRealtimeRecovery(recovery: WorkbenchStreamTransport
};
}
function automaticRecoveryStep(step: Omit<WorkbenchRealtimeRecoveryStep, "authority" | "force">): WorkbenchRealtimeRecoveryStep {
function automaticRecoveryStep(step: WorkbenchRealtimeRecoveryStepInput<WorkbenchRealtimeRecoveryStep>): WorkbenchRealtimeRecoveryStep {
return { ...step, authority: "automatic-recovery", force: false } as WorkbenchRealtimeRecoveryStep;
}
@@ -39,6 +39,7 @@ test("server state reducer keeps completed message without final response unseal
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.ok(message);
assert.equal(message.status, "running");
assert.equal(message.finishedAt, null);
assert.equal(message.durationMs, null);
@@ -54,6 +55,7 @@ test("server state reducer keeps failed message without terminal body unsealed",
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.ok(message);
assert.equal(message.status, "running");
assert.equal(message.finishedAt, null);
assert.equal(message.durationMs, null);
@@ -70,6 +72,7 @@ test("server state reducer seals completed message when final response exists",
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.ok(message);
assert.equal(message.status, "completed");
assert.equal(message.text, "final answer");
assert.equal(selectSessionStatusAuthority(state)[sessionId]?.status, "completed");
@@ -94,6 +97,7 @@ test("server state reducer keeps sealed terminal messages when a stale partial s
const messages = selectActiveMessages(state, sessionId);
assert.equal(messages.length, 4);
const terminal = messages.find((message) => message.id === "msg_state_partial_agent_2");
assert.ok(terminal);
assert.equal(terminal.status, "completed");
assert.equal(terminal.text, "final answer");
assert.equal((terminal.finalResponse as { text?: string } | undefined)?.text, "final answer");
@@ -213,7 +213,8 @@ function mergeMessageList(existing: ChatMessage[], incoming: ChatMessage[]): Cha
const message = normalizeUnsealedTerminalMessage(rawMessage);
const index = merged.findIndex((item) => messageMatchesSnapshot(item, message));
if (index >= 0) {
merged[index] = sealExistingMessageTiming(merged[index], message);
const existing = merged[index];
merged[index] = existing ? sealExistingMessageTiming(existing, message) : message;
} else {
merged.push(message);
}
@@ -438,8 +439,8 @@ function sessionStatusAuthorityFromTurn(turn: TurnStatusAuthority): SessionStatu
}
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;
if (existing && isTerminalSessionStatusAuthority(existing) && !isTerminalSessionStatusAuthority(incoming) && isSameTraceAuthority(existing, incoming) && isRunningSessionStatus(incoming.status)) return existing;
if (existing && isRunningSessionStatus(existing.status) && isTerminalSessionStatusAuthority(incoming) && isDifferentTraceAuthority(existing, incoming)) return existing;
return {
...(existing ?? {}),
...incoming,
@@ -1,18 +1,35 @@
import assert from "node:assert/strict";
import { test } from "bun:test";
import type { ChatMessage } from "../types";
import { resolveCancelableAgentMessage, resolveComposerState } from "./workbench-session";
import type { TurnStatusAuthority } from "./workbench-session";
function agentMessage(input: Partial<ChatMessage> & Pick<ChatMessage, "id" | "traceId" | "sessionId">): ChatMessage {
return {
role: "agent",
title: "Code Agent",
text: "",
status: "running",
createdAt: "2026-07-01T00:00:00.000Z",
...input
};
}
function turnStatus(input: TurnStatusAuthority): TurnStatusAuthority {
return input;
}
test("composer treats completed message without final response as unsealed running turn", () => {
const traceId = "trc_session_completed_without_final";
const sessionId = "ses_session_completed_without_final";
const message = { id: "msg_session_completed_without_final", role: "agent", status: "completed", traceId, sessionId } as any;
const message = agentMessage({ id: "msg_session_completed_without_final", status: "completed", traceId, sessionId });
const composer = resolveComposerState({
messages: [message],
activeSessionId: sessionId,
chatPending: true,
currentRequest: { traceId, sessionId, status: "running" },
turnStatusAuthority: { [traceId]: { traceId, sessionId, status: "completed", terminal: true, running: false } as any }
turnStatusAuthority: { [traceId]: turnStatus({ traceId, sessionId, status: "completed", terminal: true, running: false }) }
});
assert.equal(composer.submitMode, "steer");
assert.equal(composer.targetTraceId, traceId);
@@ -21,13 +38,13 @@ test("composer treats completed message without final response as unsealed runni
test("composer treats failed message without terminal body as unsealed running turn", () => {
const traceId = "trc_session_failed_without_body";
const sessionId = "ses_session_failed_without_body";
const message = { id: "msg_session_failed_without_body", role: "agent", status: "failed", traceId, sessionId } as any;
const message = agentMessage({ id: "msg_session_failed_without_body", status: "failed", traceId, sessionId });
const composer = resolveComposerState({
messages: [message],
activeSessionId: sessionId,
chatPending: true,
currentRequest: { traceId, sessionId, status: "running" },
turnStatusAuthority: { [traceId]: { traceId, sessionId, status: "failed", terminal: true, running: false } as any }
turnStatusAuthority: { [traceId]: turnStatus({ traceId, sessionId, status: "failed", terminal: true, running: false }) }
});
assert.equal(composer.submitMode, "steer");
assert.equal(composer.targetTraceId, traceId);
@@ -36,13 +53,13 @@ test("composer treats failed message without terminal body as unsealed running t
test("composer treats failed message with terminal body as sealed terminal turn", () => {
const traceId = "trc_session_failed_with_body";
const sessionId = "ses_session_failed_with_body";
const message = { id: "msg_session_failed_with_body", role: "agent", status: "failed", traceId, sessionId, text: "Workbench terminal failed: provider-stream-disconnected" } as any;
const message = agentMessage({ id: "msg_session_failed_with_body", status: "failed", traceId, sessionId, text: "Workbench terminal failed: provider-stream-disconnected" });
const composer = resolveComposerState({
messages: [message],
activeSessionId: sessionId,
chatPending: true,
currentRequest: { traceId, sessionId, status: "running" },
turnStatusAuthority: { [traceId]: { traceId, sessionId, status: "failed", terminal: true, running: false } as any }
turnStatusAuthority: { [traceId]: turnStatus({ traceId, sessionId, status: "failed", terminal: true, running: false }) }
});
assert.equal(composer.submitMode, "turn");
assert.equal(composer.targetTraceId, null);
@@ -51,19 +68,19 @@ test("composer treats failed message with terminal body as sealed terminal turn"
test("cancel target remains available for completed message until final response exists", () => {
const traceId = "trc_cancel_completed_without_final";
const sessionId = "ses_cancel_completed_without_final";
const unsealed = { id: "msg_cancel_completed_without_final", role: "agent", status: "completed", traceId, sessionId } as any;
const sealed = { ...unsealed, id: "msg_cancel_completed_with_final", text: "final answer" } as any;
const unsealed = agentMessage({ id: "msg_cancel_completed_without_final", status: "completed", traceId, sessionId });
const sealed = agentMessage({ ...unsealed, id: "msg_cancel_completed_with_final", text: "final answer" });
assert.equal(resolveCancelableAgentMessage({ messages: [unsealed], targetTraceId: traceId, targetSessionId: sessionId, turnStatusAuthority: { [traceId]: { traceId, sessionId, status: "running", running: true, terminal: false } as any } })?.id, unsealed.id);
assert.equal(resolveCancelableAgentMessage({ messages: [sealed], targetTraceId: traceId, targetSessionId: sessionId, turnStatusAuthority: { [traceId]: { traceId, sessionId, status: "running", running: true, terminal: false } as any } }), null);
assert.equal(resolveCancelableAgentMessage({ messages: [unsealed], targetTraceId: traceId, targetSessionId: sessionId, turnStatusAuthority: { [traceId]: turnStatus({ traceId, sessionId, status: "running", running: true, terminal: false }) } })?.id, unsealed.id);
assert.equal(resolveCancelableAgentMessage({ messages: [sealed], targetTraceId: traceId, targetSessionId: sessionId, turnStatusAuthority: { [traceId]: turnStatus({ traceId, sessionId, status: "running", running: true, terminal: false }) } }), null);
});
test("cancel target remains available for failed message until terminal body exists", () => {
const traceId = "trc_cancel_failed_without_body";
const sessionId = "ses_cancel_failed_without_body";
const unsealed = { id: "msg_cancel_failed_without_body", role: "agent", status: "failed", traceId, sessionId } as any;
const sealed = { ...unsealed, id: "msg_cancel_failed_with_body", text: "Workbench terminal failed: provider-stream-disconnected" } as any;
const unsealed = agentMessage({ id: "msg_cancel_failed_without_body", status: "failed", traceId, sessionId });
const sealed = agentMessage({ ...unsealed, id: "msg_cancel_failed_with_body", text: "Workbench terminal failed: provider-stream-disconnected" });
assert.equal(resolveCancelableAgentMessage({ messages: [unsealed], targetTraceId: traceId, targetSessionId: sessionId, turnStatusAuthority: { [traceId]: { traceId, sessionId, status: "running", running: true, terminal: false } as any } })?.id, unsealed.id);
assert.equal(resolveCancelableAgentMessage({ messages: [sealed], targetTraceId: traceId, targetSessionId: sessionId, turnStatusAuthority: { [traceId]: { traceId, sessionId, status: "running", running: true, terminal: false } as any } }), null);
assert.equal(resolveCancelableAgentMessage({ messages: [unsealed], targetTraceId: traceId, targetSessionId: sessionId, turnStatusAuthority: { [traceId]: turnStatus({ traceId, sessionId, status: "running", running: true, terminal: false }) } })?.id, unsealed.id);
assert.equal(resolveCancelableAgentMessage({ messages: [sealed], targetTraceId: traceId, targetSessionId: sessionId, turnStatusAuthority: { [traceId]: turnStatus({ traceId, sessionId, status: "running", running: true, terminal: false }) } }), null);
});
@@ -1442,6 +1442,14 @@ export const useWorkbenchStore = defineStore("workbench", () => {
return messageHasTerminalResponse(message);
}
function traceAuthorityIsSealed(turn: TurnStatusAuthority | undefined, message: ChatMessage | null): boolean {
return Boolean((turn?.terminal === true || isTerminalMessageStatus(turn?.status)) && messageHasSealedTerminalResult(message));
}
function messageIsSealedTerminal(message: ChatMessage | null): boolean {
return messageHasSealedTerminalResult(message);
}
function failTrace(traceId: string, message: string): void {
markMessage(traceId, { status: "failed", text: message });
chatPending.value = false;