Merge pull request #1328 from pikasTech/fix/1327-session-tab-status

fix: 同步 workbench session 标签状态
This commit is contained in:
Lyon
2026-06-16 13:48:51 +08:00
committed by GitHub
4 changed files with 120 additions and 22 deletions
+30 -3
View File
@@ -1035,7 +1035,8 @@ class AccessController {
const conversations = await this.repairVisibleConversationsIfNeeded(
auth.actor,
conversationsFromAgentSessions(sessions),
projectId
projectId,
{ deadlineMs: 1500 }
);
return sendJson(response, 200, {
ok: true,
@@ -1479,11 +1480,24 @@ class AccessController {
}
}
async repairVisibleConversationsIfNeeded(actor, conversations = [], projectId = "") {
async repairVisibleConversationsIfNeeded(actor, conversations = [], projectId = "", options = {}) {
if (!Array.isArray(conversations) || conversations.length === 0) return [];
const deadlineMs = positiveInteger(options.deadlineMs, 0);
const deadline = deadlineMs > 0 ? Date.now() + deadlineMs : 0;
const repaired = [];
for (const conversation of conversations) {
repaired.push(await this.repairVisibleConversationIfNeeded(actor, conversation, projectId) ?? conversation);
if (!terminalConversationRepairTraceId(conversation)) {
repaired.push(conversation);
continue;
}
const remainingMs = deadline > 0 ? deadline - Date.now() : 0;
if (deadline > 0 && remainingMs <= 0) {
repaired.push(conversation);
continue;
}
const repair = this.repairVisibleConversationIfNeeded(actor, conversation, projectId);
const next = deadline > 0 ? await resolveBeforeDeadline(repair, remainingMs) : await repair;
repaired.push(next ?? conversation);
}
return repaired;
}
@@ -2867,6 +2881,19 @@ function shouldUseSecureSessionCookie(request, env = process.env) {
return false;
}
function errorPayload(code, message, status) { return { ok: false, status, error: { code, message } }; }
function positiveInteger(value, fallback = 0) { const number = Number(value); return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback; }
async function resolveBeforeDeadline(promise, timeoutMs) {
/** @type {ReturnType<typeof setTimeout> | null} */
let timer = null;
const boundedTimeoutMs = Math.max(1, positiveInteger(timeoutMs, 1));
const guarded = Promise.resolve(promise).catch(() => null);
const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve(null), boundedTimeoutMs); });
try {
return await Promise.race([guarded, timeout]);
} finally {
if (timer) clearTimeout(timer);
}
}
function accessWriteErrorPayload(result = {}) {
const error = result.error ?? {};
const code = textOr(error.code, "openfga_write_failed");
@@ -107,11 +107,12 @@ export function mergeSelectedConversation(conversations: ConversationRecord[], w
return next;
}
export function workspaceWithClearedActiveTrace(workspace: WorkspaceRecord | null, traceId: string, reason: string): WorkspaceRecord | null {
export function workspaceWithClearedActiveTrace(workspace: WorkspaceRecord | null, traceId: string, reason: string, sessionStatus?: string | null): WorkspaceRecord | null {
if (!workspace || !traceId) return workspace;
const activeTraceId = activeTraceIdFromWorkspace(workspace);
if (activeTraceId !== traceId) return workspace;
const nested = workspace.workspace ?? {};
const nextSessionStatus = normalizeSessionStatus(sessionStatus) ?? (isActiveStatus(nested.sessionStatus) ? "failed" : normalizeSessionStatus(nested.sessionStatus));
return {
...workspace,
activeTraceId: null,
@@ -120,7 +121,7 @@ export function workspaceWithClearedActiveTrace(workspace: WorkspaceRecord | nul
activeTraceId: null,
staleActiveTraceId: traceId,
staleActiveTraceReason: reason,
sessionStatus: isActiveStatus(nested.sessionStatus) ? "failed" : nested.sessionStatus,
sessionStatus: nextSessionStatus,
updatedAt: new Date().toISOString(),
source: "cloud-web-vue-active-trace-repair",
valuesRedacted: true,
@@ -161,14 +162,33 @@ export function conversationToSessionTab(conversation: ConversationRecord, activ
};
}
export function resolveConversationSessionStatus(conversation: ConversationRecord): string {
export function resolveConversationSessionStatus(conversation: ConversationRecord, extraStatuses: unknown[] = []): string {
const latestAgent = latestAgentMessage(conversation.messages);
const latestMessage = conversation.messages?.at(-1);
return firstNonEmptyString(
conversation.status,
const stored = normalizeSessionStatus(conversation.status);
const terminalEvidence = firstNonActiveStatus(
...extraStatuses,
conversation.session?.status,
conversation.snapshot?.sessionStatus,
conversation.snapshot?.status,
latestAgent?.status,
latestAgent?.runnerTrace?.status,
latestAgent?.runnerTrace?.traceStatus,
runnerTraceAgentRunStatus(latestAgent?.runnerTrace, "terminalStatus"),
runnerTraceAgentRunStatus(latestAgent?.runnerTrace, "commandState"),
latestMessage?.status
);
if (stored && isActiveStatus(stored) && terminalEvidence) return terminalEvidence;
return stored ?? firstNormalizedSessionStatus(
...extraStatuses,
conversation.session?.status,
conversation.snapshot?.sessionStatus,
conversation.snapshot?.status,
latestAgent?.status,
latestAgent?.runnerTrace?.status,
latestAgent?.runnerTrace?.traceStatus,
runnerTraceAgentRunStatus(latestAgent?.runnerTrace, "terminalStatus"),
runnerTraceAgentRunStatus(latestAgent?.runnerTrace, "commandState"),
latestMessage?.status
) ?? "source";
}
@@ -268,7 +288,7 @@ function selectedConversationStub(workspace: WorkspaceRecord | null, selectedCon
const lastTraceId = firstNonEmptyString(workspace.workspace?.lastTraceId, workspace.activeTraceId, workspace.workspace?.activeTraceId, [...messages].reverse().find((message) => message.traceId)?.traceId) ?? null;
const lastUserMessageAt = latestUserMessageAtFromMessages(messages);
const latestAgent = latestAgentMessage(messages);
return {
const stub: ConversationRecord = {
conversationId: selectedConversationId,
projectId: firstNonEmptyString(workspace.projectId, workspace.workspace?.projectId) ?? null,
sessionId,
@@ -280,6 +300,7 @@ function selectedConversationStub(workspace: WorkspaceRecord | null, selectedCon
messageCount: messages.length,
messages
};
return { ...stub, status: resolveConversationSessionStatus(stub) };
}
function mergeConversationRecords(existing: ConversationRecord, selected: ConversationRecord): ConversationRecord {
@@ -289,7 +310,13 @@ function mergeConversationRecords(existing: ConversationRecord, selected: Conver
const supplementalMessages = supplemental.messages ?? [];
const messages = supplementalMessages.length > 0 ? supplementalMessages : primaryMessages;
const lastUserMessageAt = firstNonEmptyString(primary.lastUserMessageAt, primary.snapshot?.lastUserMessageAt, latestUserMessageAtFromMessages(primaryMessages), supplemental.lastUserMessageAt, supplemental.snapshot?.lastUserMessageAt, latestUserMessageAtFromMessages(supplementalMessages)) ?? null;
const status = firstNonEmptyString(primary.status, primary.snapshot?.sessionStatus, supplemental.status, supplemental.snapshot?.sessionStatus, statusFromMessages(messages)) ?? null;
const status = resolveConversationSessionStatus({ ...primary, messages }, [
supplemental.status,
supplemental.session?.status,
supplemental.snapshot?.sessionStatus,
supplemental.snapshot?.status,
statusFromMessages(messages)
]);
return {
...supplemental,
...primary,
@@ -335,11 +362,43 @@ function latestConversationMessage(messages: ChatMessage[], activeConversationId
}
function isActiveStatus(value: unknown): boolean {
return ["running", "pending", "accepted", "processing", "busy", "creating"].includes(String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"));
const status = normalizeSessionStatus(value);
return ["running", "pending", "accepted", "processing", "busy", "creating"].includes(status ?? "");
}
function isTerminalStatus(value: unknown): boolean {
return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled", "stale", "thread-resume-failed"].includes(String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"));
return ["completed", "failed", "blocked", "timeout", "canceled", "stale", "thread-resume-failed", "interrupted", "expired"].includes(normalizeSessionStatus(value) ?? "");
}
function firstNonActiveStatus(...values: unknown[]): string | null {
for (const value of values) {
const status = normalizeSessionStatus(value);
if (status && !isActiveStatus(status) && isTerminalStatus(status)) return status;
}
return null;
}
function firstNormalizedSessionStatus(...values: unknown[]): string | null {
for (const value of values) {
const status = normalizeSessionStatus(value);
if (status) return status;
}
return null;
}
function normalizeSessionStatus(value: unknown): string | null {
const text = firstNonEmptyString(value);
if (!text) return null;
const normalized = text.trim().toLowerCase().replace(/_/gu, "-");
if (normalized === "cancelled") return "canceled";
if (normalized === "in-flight") return "running";
if (normalized === "error") return "failed";
return normalized;
}
function runnerTraceAgentRunStatus(runnerTrace: ChatMessage["runnerTrace"], key: string): unknown {
const agentRun = runnerTrace && typeof runnerTrace === "object" ? (runnerTrace as Record<string, unknown>).agentRun : null;
return agentRun && typeof agentRun === "object" ? (agentRun as Record<string, unknown>)[key] : null;
}
function profileOptionFromUnknown(item: unknown): ProviderProfileOption | null {
+21 -9
View File
@@ -248,9 +248,10 @@ export const useWorkbenchStore = defineStore("workbench", () => {
async function cancelAgentMessage(message: ChatMessage): Promise<void> {
const traceId = message.traceId ?? message.runnerTrace?.traceId;
if (!traceId) return;
await api.agent.cancelAgentMessage({ traceId, projectId: activeProjectId.value, sessionId: message.sessionId ?? selectedSessionId.value, threadId: message.threadId ?? selectedThreadId.value, conversationId: message.conversationId ?? activeConversationId.value });
const response = await api.agent.cancelAgentMessage({ traceId, projectId: activeProjectId.value, sessionId: message.sessionId ?? selectedSessionId.value, threadId: message.threadId ?? selectedThreadId.value, conversationId: message.conversationId ?? activeConversationId.value });
const canceledStatus = workspaceSessionStatusFromChatStatus(firstNonEmptyString((response.data as Record<string, unknown> | null)?.status, "canceled"));
markMessage(traceId, { status: "canceled", text: "用户已取消该 turn。" });
void clearActiveTrace(traceId, "cancel-agent-message");
void clearActiveTrace(traceId, "cancel-agent-message", canceledStatus);
if (message.status === "running") chatPending.value = false;
currentRequest.value = null;
}
@@ -360,7 +361,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
reattachTrace(traceId);
return;
}
await clearActiveTrace(traceId, "reattach-terminal-result");
await clearActiveTrace(traceId, "reattach-terminal-result", workspaceSessionStatusFromChatStatus(statusFromResult(result.data.status)));
}
function applyTraceSnapshot(traceId: string, snapshot: TraceSnapshot): void {
@@ -371,17 +372,18 @@ export const useWorkbenchStore = defineStore("workbench", () => {
function completeTrace(traceId: string, result: AgentChatResultResponse): void {
const text = firstNonEmptyString(result.assistantText, finalResponseText(result.finalResponse), typeof result.reply === "string" ? result.reply : result.reply?.content, result.text, result.summary) ?? "Code Agent 已完成,但没有返回可展示的 final response。";
const terminalStatus = result.status === "completed" ? "completed" : statusFromResult(result.status);
messages.value = messages.value.map((message) => {
if (message.traceId !== traceId || message.role !== "agent") return message;
const runnerTrace = mergeTerminalResultTrace(message.runnerTrace, result);
const error = normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error);
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
return { ...message, status: result.status === "completed" ? "completed" : statusFromResult(result.status), text, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
return { ...message, status: terminalStatus, text, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
});
bumpConversationActivity({ conversationId: firstNonEmptyString((result as Record<string, unknown>).conversationId), sessionId: result.sessionId, threadId: result.threadId, traceId, status: statusFromResult(result.status), updatedAt: resultActivityUpdatedAt(result) });
bumpConversationActivity({ conversationId: firstNonEmptyString((result as Record<string, unknown>).conversationId), sessionId: result.sessionId, threadId: result.threadId, traceId, status: terminalStatus, updatedAt: resultActivityUpdatedAt(result) });
chatPending.value = false;
currentRequest.value = null;
void clearActiveTrace(traceId, "trace-terminal");
void clearActiveTrace(traceId, "trace-terminal", workspaceSessionStatusFromChatStatus(terminalStatus));
}
async function hydrateTerminalMessageDiagnostics(): Promise<void> {
@@ -411,7 +413,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
markMessage(traceId, { status: "failed", text: message });
chatPending.value = false;
currentRequest.value = null;
void clearActiveTrace(traceId, "trace-infrastructure-error");
void clearActiveTrace(traceId, "trace-infrastructure-error", "failed");
}
function markMessage(traceId: string, patch: Partial<ChatMessage>): void {
@@ -426,9 +428,9 @@ export const useWorkbenchStore = defineStore("workbench", () => {
return workspace.value;
}
async function clearActiveTrace(traceId: string, reason: string): Promise<void> {
async function clearActiveTrace(traceId: string, reason: string, sessionStatus?: string | null): Promise<void> {
const requestEpoch = workspaceSelectionEpoch.value;
const current = workspaceWithClearedActiveTrace(workspace.value, traceId, reason);
const current = workspaceWithClearedActiveTrace(workspace.value, traceId, reason, sessionStatus);
if (current === workspace.value) return;
workspace.value = current;
if (!current?.workspaceId) return;
@@ -716,6 +718,16 @@ function statusFromResult(status: string | undefined): ChatMessage["status"] {
return "failed";
}
function workspaceSessionStatusFromChatStatus(status: unknown): string | null {
const normalized = normalizedStatusText(status);
if (normalized === "completed") return "idle";
if (normalized === "canceled" || normalized === "cancelled") return "canceled";
if (normalized === "blocked") return "blocked";
if (normalized === "timeout") return "timeout";
if (normalized === "failed" || normalized === "error") return "failed";
return null;
}
function isTraceActiveStatus(status: unknown): boolean {
return ["accepted", "pending", "processing", "running", "busy", "creating"].includes(String(status ?? "").trim().toLowerCase().replace(/_/gu, "-"));
}
+1 -1
View File
@@ -113,7 +113,7 @@ export interface ConversationRecord {
name?: string | null;
snapshot?: Record<string, unknown> | null;
userPreview?: string | null;
session?: { sessionId?: string | null; threadId?: string | null };
session?: { sessionId?: string | null; threadId?: string | null; status?: string | null };
messages?: ChatMessage[];
}