Merge pull request #1339 from pikasTech/fix/1338-session-list-stability
fix: make session rail use stateless list authority
This commit is contained in:
@@ -15,6 +15,8 @@
|
||||
|
||||
Workbench 页面和组件必须明确区分“未加载完成”和“已加载但为空”。未加载完成时不得渲染由 workspace、localStorage、默认对象或 stub 记录拼出来的伪数据;这类数据只能作为内部恢复线索,不能在列表、表格、卡片或状态栏中冒充真实加载结果。已经确认后端返回且集合为空时,才显示空态文案。
|
||||
|
||||
Workbench 状态对象必须服从 UniDesk OA Web SPEC 中已经定下来的单一权威 API 绑定。Session rail 的会话集合只允许消费 `/v1/agent/conversations` 成功返回的 conversation 集合;当前 selected conversation 如果需要出现在左侧列表中,必须通过显式 query/path/body 传入稳定 conversation id,由该列表 API 在同一响应中返回,后端不得隐藏读取 workspace selected、localStorage、Web snapshot 或上一轮页面状态。前端不得用 workspace selected snapshot、stub、localStorage 或 route 状态补出 session tab,也不得把前端拼接出的 status、final response、markdown、running 动效或 stub 传回后端变成事实。Session 运行状态必须按 `sessionId` 绑定到单一 session 状态 API;Code Agent turn、trace 阅读和 final response 必须按 `traceId` 绑定到单一 trace/result snapshot API。任何权威 API 失败时只能保留上一份成功结果或显示未加载/错误态,不得切换到另一条 fallback 路径形成一条会话、旧 running 态或劣化 markdown。
|
||||
|
||||
Cloud Web 的通用加载态使用 `web/hwlab-cloud-web/src/components/common/LoadingState.vue`。新增或修复页面加载态时优先复用该组件,并通过明确的 ready/loading 状态控制展示;不要在每个组件里重新实现一套 spinner、点状动画或默认占位数据。紧凑区域可以使用组件的 compact 形态,文案默认保持“加载中”。
|
||||
|
||||
Session rail 是该规则的高频区域。`/v1/agent/conversations` 还未返回时,即使 workspace 中已有 `selectedConversationId`、sessionId、traceId 或 selected conversation snapshot,也不能把选中 session stub 渲染成单条 `.session-tab`,更不能让它占满整个 session 列表高度。加载窗口应只显示 `LoadingState`,并隐藏当前 trace 元信息、复制/删除等依赖真实 active tab 的动作;待 conversations ready 后再渲染真实 session tabs,或在真实空集合时显示空态。
|
||||
|
||||
@@ -1104,6 +1104,69 @@ test("cloud api stores and restores Code Agent conversations by authenticated ac
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api includes an explicitly requested conversation in the conversations list API", async () => {
|
||||
let now = "2026-06-16T08:00:00.000Z";
|
||||
const server = createCloudApiServer({
|
||||
env: {
|
||||
HWLAB_ACCESS_CONTROL_REQUIRED: "1",
|
||||
HWLAB_BOOTSTRAP_ADMIN_USERNAME: "admin",
|
||||
HWLAB_BOOTSTRAP_ADMIN_PASSWORD: "admin-pass"
|
||||
},
|
||||
now: () => now
|
||||
});
|
||||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||
|
||||
try {
|
||||
const { port } = server.address();
|
||||
const adminLogin = await postJson(port, "/auth/login", { username: "admin", password: "admin-pass" });
|
||||
const aliceCreate = await postJson(port, "/v1/admin/users", { username: "alice-issue1338", password: "alice-pass" }, adminLogin.cookie);
|
||||
assert.equal(aliceCreate.status, 201);
|
||||
const aliceLogin = await postJson(port, "/auth/login", { username: "alice-issue1338", password: "alice-pass" });
|
||||
|
||||
const selected = await putJson(port, "/v1/agent/conversations/cnv_issue1338_selected", {
|
||||
projectId: "prj_hwpod_workbench",
|
||||
sessionId: "ses_issue1338_selected",
|
||||
threadId: "thread-issue1338-selected",
|
||||
sessionStatus: "completed",
|
||||
messages: [
|
||||
{ id: "msg_issue1338_selected_user", role: "user", title: "用户", text: "被选中的旧会话", status: "sent", conversationId: "cnv_issue1338_selected", sessionId: "ses_issue1338_selected", threadId: "thread-issue1338-selected" }
|
||||
]
|
||||
}, aliceLogin.cookie);
|
||||
assert.equal(selected.status, 200);
|
||||
|
||||
now = "2026-06-16T08:05:00.000Z";
|
||||
const recent = await putJson(port, "/v1/agent/conversations/cnv_issue1338_recent", {
|
||||
projectId: "prj_hwpod_workbench",
|
||||
sessionId: "ses_issue1338_recent",
|
||||
threadId: "thread-issue1338-recent",
|
||||
sessionStatus: "completed",
|
||||
messages: [
|
||||
{ id: "msg_issue1338_recent_user", role: "user", title: "用户", text: "列表窗口内较新的会话", status: "sent", conversationId: "cnv_issue1338_recent", sessionId: "ses_issue1338_recent", threadId: "thread-issue1338-recent" }
|
||||
]
|
||||
}, aliceLogin.cookie);
|
||||
assert.equal(recent.status, 200);
|
||||
|
||||
const limited = await getJson(port, "/v1/agent/conversations?projectId=prj_hwpod_workbench&limit=1", aliceLogin.cookie);
|
||||
assert.equal(limited.status, 200);
|
||||
assert.deepEqual(limited.body.conversations.map((conversation) => conversation.conversationId), ["cnv_issue1338_recent"]);
|
||||
assert.equal(limited.body.count, 1);
|
||||
|
||||
const included = await getJson(port, "/v1/agent/conversations?projectId=prj_hwpod_workbench&limit=1&includeConversationId=cnv_issue1338_selected", aliceLogin.cookie);
|
||||
assert.equal(included.status, 200);
|
||||
assert.deepEqual(included.body.conversations.map((conversation) => conversation.conversationId), ["cnv_issue1338_selected", "cnv_issue1338_recent"]);
|
||||
assert.equal(included.body.defaultConversation.conversationId, "cnv_issue1338_selected");
|
||||
assert.equal(included.body.count, 2);
|
||||
|
||||
const invalidInclude = await getJson(port, "/v1/agent/conversations?projectId=prj_hwpod_workbench&includeConversationId=bad", aliceLogin.cookie);
|
||||
assert.equal(invalidInclude.status, 400);
|
||||
assert.equal(invalidInclude.body.error.code, "invalid_include_conversation_id");
|
||||
} finally {
|
||||
await new Promise((resolve, reject) => {
|
||||
server.close((error) => (error ? reject(error) : resolve()));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test("cloud api exposes terminal conversation status when stored session status is stale running", async () => {
|
||||
const server = createCloudApiServer({
|
||||
env: {
|
||||
|
||||
@@ -1023,6 +1023,10 @@ class AccessController {
|
||||
if (!auth.ok) return sendJson(response, auth.status, auth);
|
||||
const projectId = textOr(url.searchParams.get("projectId"), "");
|
||||
const limit = boundedListLimit(url.searchParams.get("limit"));
|
||||
const includeConversationId = textOr(url.searchParams.get("includeConversationId"), "");
|
||||
if (includeConversationId && !safeConversationIdLocal(includeConversationId)) {
|
||||
return sendJson(response, 400, errorPayload("invalid_include_conversation_id", "includeConversationId must start with cnv_", 400));
|
||||
}
|
||||
const sessions = await this.store.listAgentSessionsForUser?.({
|
||||
ownerUserId: auth.actor.id,
|
||||
actorRole: auth.actor.role,
|
||||
@@ -1032,12 +1036,13 @@ class AccessController {
|
||||
includeArchived: false,
|
||||
now: this.now()
|
||||
}) ?? [];
|
||||
const conversations = await this.repairVisibleConversationsIfNeeded(
|
||||
let conversations = await this.repairVisibleConversationsIfNeeded(
|
||||
auth.actor,
|
||||
conversationsFromAgentSessions(sessions),
|
||||
projectId,
|
||||
{ deadlineMs: 1500 }
|
||||
);
|
||||
conversations = includeConversation(conversations, includeConversationId ? await this.visibleConversationForActor(auth.actor, includeConversationId, projectId) : null);
|
||||
return sendJson(response, 200, {
|
||||
ok: true,
|
||||
status: "succeeded",
|
||||
@@ -2693,6 +2698,12 @@ function conversationsFromAgentSessions(sessions = []) {
|
||||
}
|
||||
return [...byConversation.values()].sort((a, b) => String(b.updatedAt ?? "").localeCompare(String(a.updatedAt ?? "")));
|
||||
}
|
||||
function includeConversation(conversations = [], included = null) {
|
||||
const includedConversationId = textOr(included?.conversationId, "");
|
||||
if (!includedConversationId) return conversations;
|
||||
if (conversations.some((conversation) => conversation.conversationId === includedConversationId)) return conversations;
|
||||
return [included, ...conversations];
|
||||
}
|
||||
function publicAgentConversation(session) {
|
||||
const snapshot = normalizeObject(session.session);
|
||||
const rawMessages = Array.isArray(snapshot.messages) ? snapshot.messages : Array.isArray(snapshot.chatMessages) ? snapshot.chatMessages : [];
|
||||
|
||||
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import type { ChatMessage, ConversationRecord, WorkspaceRecord } from "../src/types/index.ts";
|
||||
import { defaultProviderProfileOptions, mergeActiveConversationActivity, mergeSelectedConversation, normalizeRecentDrafts, normalizeWorkbenchMessageTitle, providerProfileOptionsFromPayload, recordRecentDraft, resolveCancelableAgentMessage, resolveComposerState, resolveConversationSessionStatus, shouldApplyWorkspaceSnapshot, shouldShowSessionListLoading, sortSessionTabs, workspaceWithClearedActiveTrace } from "../src/stores/workbench-session.ts";
|
||||
import { defaultProviderProfileOptions, normalizeRecentDrafts, normalizeWorkbenchMessageTitle, providerProfileOptionsFromPayload, recordRecentDraft, resolveCancelableAgentMessage, resolveComposerState, resolveConversationSessionStatus, shouldApplyWorkspaceSnapshot, shouldShowSessionListLoading, sortSessionTabs, workspaceWithClearedActiveTrace } from "../src/stores/workbench-session.ts";
|
||||
|
||||
test("R2 composer ignores stale workspace activeTraceId without verified active message", () => {
|
||||
const workspace = workspaceRecord({ activeTraceId: "trc_stale", sessionStatus: "running" });
|
||||
@@ -93,6 +93,11 @@ test("R2 session list shows loading until conversations are ready", () => {
|
||||
assert.equal(shouldShowSessionListLoading({ loading: false, conversationsReady: false }), false);
|
||||
});
|
||||
|
||||
test("R2 session tabs render only conversations supplied by the list API", () => {
|
||||
const tabs = sortSessionTabs([], "cnv_selected_only");
|
||||
assert.equal(tabs.length, 0);
|
||||
});
|
||||
|
||||
test("R2 drafts keep recent unique values and normalize corrupt storage", () => {
|
||||
const next = recordRecentDraft([{ text: "旧输入", ts: "2026-01-01T00:00:00.000Z" }], "新输入", "2026-01-02T00:00:00.000Z");
|
||||
assert.deepEqual(next.map((item) => item.text), ["新输入", "旧输入"]);
|
||||
@@ -163,35 +168,12 @@ test("R2 session tabs do not infer running state from workspace stubs", () => {
|
||||
agentMessage({ conversationId: "cnv_r2", status: "running", updatedAt: "2026-01-04T00:01:00.000Z" })
|
||||
]
|
||||
}];
|
||||
const workspace = workspaceRecord({ activeTraceId: null, sessionStatus: "idle" });
|
||||
const merged = mergeSelectedConversation(conversations, workspace);
|
||||
const tabs = sortSessionTabs(merged, "cnv_r2");
|
||||
const tabs = sortSessionTabs(conversations, "cnv_r2");
|
||||
assert.equal(tabs[0]?.status, "unknown");
|
||||
assert.equal(tabs[0]?.running, false);
|
||||
assert.equal(tabs[0]?.messageCount, 2);
|
||||
assert.equal(tabs[0]?.label, "保持原始标题");
|
||||
const authorityTabs = sortSessionTabs(merged, "cnv_r2", { ses_r2: { sessionId: "ses_r2", status: "running" } });
|
||||
assert.equal(authorityTabs[0]?.status, "running");
|
||||
assert.equal(authorityTabs[0]?.running, true);
|
||||
});
|
||||
|
||||
test("R2 active in-flight turn updates tab content but not status without session authority", () => {
|
||||
const messages: ChatMessage[] = [
|
||||
userMessage({ conversationId: "cnv_live", sessionId: "ses_live", text: "立即显示左侧运行态", createdAt: "2026-01-04T00:00:00.000Z" }),
|
||||
agentMessage({ conversationId: "cnv_live", sessionId: "ses_live", status: "running", traceId: "trc_live", updatedAt: "2026-01-04T00:00:01.000Z" })
|
||||
];
|
||||
const merged = mergeActiveConversationActivity([{ conversationId: "cnv_live", sessionId: "ses_live", status: "active", messageCount: 0 }], {
|
||||
activeConversationId: "cnv_live",
|
||||
messages,
|
||||
currentRequest: { conversationId: "cnv_live", sessionId: "ses_live", traceId: "trc_live", status: "running" },
|
||||
chatPending: true
|
||||
});
|
||||
const tabs = sortSessionTabs(merged, "cnv_live");
|
||||
assert.equal(tabs[0]?.status, "unknown");
|
||||
assert.equal(tabs[0]?.running, false);
|
||||
assert.equal(tabs[0]?.label, "立即显示左侧运行态");
|
||||
assert.equal(tabs[0]?.messageCount, 2);
|
||||
const authorityTabs = sortSessionTabs(merged, "cnv_live", { ses_live: { sessionId: "ses_live", status: "running" } });
|
||||
const authorityTabs = sortSessionTabs(conversations, "cnv_r2", { ses_r2: { sessionId: "ses_r2", status: "running" } });
|
||||
assert.equal(authorityTabs[0]?.status, "running");
|
||||
assert.equal(authorityTabs[0]?.running, true);
|
||||
});
|
||||
@@ -221,32 +203,6 @@ test("R2 agent message titles drop transient running wording", () => {
|
||||
assert.equal(normalizeWorkbenchMessageTitle("user", "用户"), "用户");
|
||||
});
|
||||
|
||||
test("R2 session tabs keep the workspace-selected conversation when the list window omits it", () => {
|
||||
const workspace: WorkspaceRecord = {
|
||||
...workspaceRecord({ activeTraceId: null, sessionStatus: "completed" }),
|
||||
selectedConversationId: "cnv_selected_omitted",
|
||||
selectedAgentSessionId: "ses_selected_omitted",
|
||||
selectedConversation: {
|
||||
conversationId: "cnv_selected_omitted",
|
||||
sessionId: "ses_selected_omitted",
|
||||
firstUserMessagePreview: "被列表窗口省略的当前会话",
|
||||
updatedAt: "2026-01-05T00:00:00.000Z",
|
||||
messages: [agentMessage({ conversationId: "cnv_selected_omitted", sessionId: "ses_selected_omitted", status: "completed", updatedAt: "2026-01-05T00:00:00.000Z" })]
|
||||
},
|
||||
workspace: {
|
||||
selectedConversationId: "cnv_selected_omitted",
|
||||
selectedAgentSessionId: "ses_selected_omitted",
|
||||
sessionStatus: "completed"
|
||||
}
|
||||
};
|
||||
const conversations: ConversationRecord[] = [{ conversationId: "cnv_other", sessionId: "ses_other", firstUserMessagePreview: "列表里较新的其他会话", updatedAt: "2026-01-06T00:00:00.000Z" }];
|
||||
const merged = mergeSelectedConversation(conversations, workspace);
|
||||
assert.equal(merged.some((conversation) => conversation.conversationId === "cnv_selected_omitted"), true);
|
||||
const tabs = sortSessionTabs(merged, "cnv_selected_omitted");
|
||||
assert.equal(tabs.find((tab) => tab.conversationId === "cnv_selected_omitted")?.active, true);
|
||||
assert.equal(tabs.find((tab) => tab.conversationId === "cnv_selected_omitted")?.label, "被列表窗口省略的当前会话");
|
||||
});
|
||||
|
||||
test("R2 session tab labels normalize object previews and reject object placeholders", () => {
|
||||
const tabs = sortSessionTabs([
|
||||
{ conversationId: "cnv_object", sessionId: "ses_object", firstUserMessagePreview: { text: "对象预览标题" } as unknown as string, updatedAt: "2026-01-03T00:00:00.000Z" },
|
||||
|
||||
@@ -1,12 +1,23 @@
|
||||
import { fetchJson } from "./client";
|
||||
import type { ApiResult, ConversationRecord, WorkspaceRecord } from "@/types";
|
||||
|
||||
export interface ConversationListOptions {
|
||||
includeConversationId?: string | null;
|
||||
}
|
||||
|
||||
export const workbenchAPI = {
|
||||
workspace: (projectId: string): Promise<ApiResult<{ workspace?: WorkspaceRecord }>> => fetchJson(`/v1/workbench/workspace?projectId=${encodeURIComponent(projectId)}`, { timeoutMs: 8000, timeoutName: "workspace" }),
|
||||
updateWorkspace: (workspaceId: string, payload: Record<string, unknown>): Promise<ApiResult<{ workspace?: WorkspaceRecord }>> => fetchJson(`/v1/workbench/workspace/${encodeURIComponent(workspaceId)}`, { method: "PATCH", body: JSON.stringify(payload), timeoutMs: 8000, timeoutName: "workspace update" }),
|
||||
selectConversation: (workspaceId: string, payload: Record<string, unknown>): Promise<ApiResult<{ workspace?: WorkspaceRecord }>> => fetchJson(`/v1/workbench/workspace/${encodeURIComponent(workspaceId)}/select-conversation`, { method: "POST", body: JSON.stringify(payload), timeoutMs: 8000, timeoutName: "select conversation" }),
|
||||
conversations: (projectId: string): Promise<ApiResult<{ conversations?: ConversationRecord[] }>> => fetchJson(`/v1/agent/conversations?projectId=${encodeURIComponent(projectId)}`, { timeoutMs: 8000, timeoutName: "conversations" }),
|
||||
conversations: (projectId: string, options: ConversationListOptions = {}): Promise<ApiResult<{ conversations?: ConversationRecord[] }>> => fetchJson(conversationListPath(projectId, options), { timeoutMs: 8000, timeoutName: "conversations" }),
|
||||
conversation: (conversationId: string): Promise<ApiResult<{ conversation?: ConversationRecord }>> => fetchJson(`/v1/agent/conversations/${encodeURIComponent(conversationId)}`, { timeoutMs: 8000, timeoutName: "conversation" }),
|
||||
deleteConversation: (conversationId: string, payload: Record<string, unknown>): Promise<ApiResult<{ workspace?: WorkspaceRecord }>> => fetchJson(`/v1/agent/conversations/${encodeURIComponent(conversationId)}`, { method: "DELETE", body: JSON.stringify(payload), timeoutMs: 8000, timeoutName: "delete conversation" }),
|
||||
saveConversation: (conversationId: string, payload: Record<string, unknown>): Promise<ApiResult<{ ok?: boolean }>> => fetchJson(`/v1/agent/conversations/${encodeURIComponent(conversationId)}`, { method: "PUT", body: JSON.stringify(payload), timeoutMs: 8000, timeoutName: "save conversation" })
|
||||
};
|
||||
|
||||
function conversationListPath(projectId: string, options: ConversationListOptions): string {
|
||||
const params = new URLSearchParams({ projectId });
|
||||
const includeConversationId = typeof options.includeConversationId === "string" ? options.includeConversationId.trim() : "";
|
||||
if (includeConversationId) params.set("includeConversationId", includeConversationId);
|
||||
return `/v1/agent/conversations?${params.toString()}`;
|
||||
}
|
||||
|
||||
@@ -41,13 +41,6 @@ export interface ProviderProfileOption {
|
||||
configured?: boolean;
|
||||
}
|
||||
|
||||
export interface ActiveConversationActivityInput {
|
||||
activeConversationId: string | null;
|
||||
messages: ChatMessage[];
|
||||
currentRequest?: { traceId?: string | null; conversationId?: string | null; sessionId?: string | null; threadId?: string | null; status?: string | null } | null;
|
||||
chatPending: boolean;
|
||||
}
|
||||
|
||||
export const RECENT_DRAFTS_STORAGE_KEY = "hwlab.workbench.recentDrafts.v1";
|
||||
export const RECENT_DRAFTS_LIMIT = 8;
|
||||
const OBJECT_OBJECT_TEXT = "[object Object]";
|
||||
@@ -116,20 +109,6 @@ export function shouldShowSessionListLoading(input: { loading: boolean; conversa
|
||||
return input.loading && !input.conversationsReady;
|
||||
}
|
||||
|
||||
export function mergeSelectedConversation(conversations: ConversationRecord[], workspace: WorkspaceRecord | null): ConversationRecord[] {
|
||||
const selectedConversationId = selectedConversationIdFromWorkspace(workspace);
|
||||
if (!selectedConversationId) return conversations;
|
||||
const selected = selectedConversationFromWorkspace(workspace, selectedConversationId);
|
||||
if (!selected) return conversations;
|
||||
const existingIndex = conversations.findIndex((conversation) => conversation.conversationId === selectedConversationId);
|
||||
if (existingIndex < 0) return [selected, ...conversations];
|
||||
const existing = conversations[existingIndex];
|
||||
if (!existing) return [selected, ...conversations];
|
||||
const next = conversations.slice();
|
||||
next[existingIndex] = mergeConversationRecords(existing, selected);
|
||||
return next;
|
||||
}
|
||||
|
||||
export function workspaceWithClearedActiveTrace(workspace: WorkspaceRecord | null, traceId: string, reason: string, sessionStatus?: string | null): WorkspaceRecord | null {
|
||||
if (!workspace || !traceId) return workspace;
|
||||
const activeTraceId = activeTraceIdFromWorkspace(workspace);
|
||||
@@ -239,44 +218,6 @@ export function sortSessionTabs(conversations: ConversationRecord[], activeConve
|
||||
return conversations.map((conversation) => conversationToSessionTab(conversation, activeConversationId, sessionStatusAuthority)).sort((left, right) => timestampMs(right.updatedAt) - timestampMs(left.updatedAt));
|
||||
}
|
||||
|
||||
export function mergeActiveConversationActivity(conversations: ConversationRecord[], input: ActiveConversationActivityInput): ConversationRecord[] {
|
||||
const conversationId = firstNonEmptyString(input.activeConversationId, input.currentRequest?.conversationId, input.messages.find((message) => message.conversationId)?.conversationId);
|
||||
if (!conversationId) return conversations;
|
||||
const activityMessages = input.messages.filter((message) => !message.conversationId || message.conversationId === conversationId);
|
||||
const latestAgent = latestAgentMessage(activityMessages);
|
||||
const requestMatches = !input.currentRequest?.conversationId || input.currentRequest.conversationId === conversationId;
|
||||
const liveStatus = requestMatches && isActiveStatus(input.currentRequest?.status)
|
||||
? input.currentRequest?.status
|
||||
: input.chatPending && isActiveStatus(latestAgent?.status)
|
||||
? latestAgent?.status
|
||||
: null;
|
||||
if (!liveStatus || activityMessages.length === 0) return conversations;
|
||||
const existing = conversations.find((conversation) => conversation.conversationId === conversationId) ?? { conversationId };
|
||||
const userMessage = activityMessages.find((message) => message.role === "user");
|
||||
const lastUserMessageAt = latestUserMessageAtFromMessages(activityMessages) ?? firstNonEmptyString(existing.lastUserMessageAt, existing.snapshot?.lastUserMessageAt) ?? null;
|
||||
const updatedAt = lastUserMessageAt ?? conversationDisplayUpdatedAt(existing) ?? new Date().toISOString();
|
||||
const next: ConversationRecord = {
|
||||
...existing,
|
||||
conversationId,
|
||||
sessionId: firstNonEmptyString(input.currentRequest?.sessionId, latestAgent?.sessionId, existing.sessionId, existing.session?.sessionId) ?? null,
|
||||
threadId: firstNonEmptyString(input.currentRequest?.threadId, latestAgent?.threadId, existing.threadId, existing.session?.threadId) ?? null,
|
||||
status: liveStatus,
|
||||
lastTraceId: firstNonEmptyString(input.currentRequest?.traceId, latestAgent?.traceId, latestAgent?.runnerTrace?.traceId, existing.lastTraceId) ?? null,
|
||||
firstUserMessagePreview: firstNonEmptyString(existing.firstUserMessagePreview, existing.snapshot?.firstUserMessagePreview, userMessage?.text, userMessage?.content) ?? null,
|
||||
messages: activityMessages,
|
||||
messageCount: activityMessages.length,
|
||||
lastUserMessageAt,
|
||||
updatedAt,
|
||||
snapshot: {
|
||||
...(existing.snapshot ?? {}),
|
||||
sessionStatus: liveStatus,
|
||||
...(lastUserMessageAt ? { lastUserMessageAt } : {}),
|
||||
updatedAt
|
||||
}
|
||||
};
|
||||
return [next, ...conversations.filter((conversation) => conversation.conversationId !== conversationId)];
|
||||
}
|
||||
|
||||
export function recordRecentDraft(existing: DraftEntry[], text: string, now = new Date().toISOString()): DraftEntry[] {
|
||||
const trimmed = text.trim();
|
||||
if (!trimmed) return existing;
|
||||
@@ -307,72 +248,6 @@ function activeConversation(conversations: ConversationRecord[], conversationId:
|
||||
return conversations.find((item) => item.conversationId === conversationId) ?? null;
|
||||
}
|
||||
|
||||
function selectedConversationFromWorkspace(workspace: WorkspaceRecord | null, selectedConversationId: string): ConversationRecord | null {
|
||||
const selected = workspace?.selectedConversation;
|
||||
const stub = selectedConversationStub(workspace, selectedConversationId);
|
||||
if (selected?.conversationId === selectedConversationId) return stub ? mergeConversationRecords(selected, stub) : selected;
|
||||
return stub;
|
||||
}
|
||||
|
||||
function selectedConversationStub(workspace: WorkspaceRecord | null, selectedConversationId: string): ConversationRecord | null {
|
||||
if (!workspace) return null;
|
||||
const messages = Array.isArray(workspace.workspace?.messages) ? workspace.workspace.messages : [];
|
||||
const sessionId = firstNonEmptyString(workspace.selectedAgentSessionId, workspace.workspace?.selectedAgentSessionId, messages.find((message) => message.sessionId)?.sessionId) ?? null;
|
||||
const threadId = firstNonEmptyString(workspace.workspace?.threadId, messages.find((message) => message.threadId)?.threadId) ?? null;
|
||||
const lastTraceId = firstNonEmptyString([...messages].reverse().find((message) => message.traceId)?.traceId, workspace.workspace?.lastTraceId, workspace.activeTraceId, workspace.workspace?.activeTraceId) ?? null;
|
||||
const lastUserMessageAt = latestUserMessageAtFromMessages(messages);
|
||||
const latestAgent = latestAgentMessage(messages);
|
||||
const stub: ConversationRecord = {
|
||||
conversationId: selectedConversationId,
|
||||
projectId: firstNonEmptyString(workspace.projectId, workspace.workspace?.projectId) ?? null,
|
||||
sessionId,
|
||||
threadId,
|
||||
status: firstNonEmptyString(workspace.workspace?.sessionStatus, latestAgent?.status, latestAgent?.runnerTrace?.status, messages.at(-1)?.status) ?? null,
|
||||
updatedAt: lastUserMessageAt ?? firstNonEmptyString(workspace.createdAt, workspace.updatedAt, workspace.workspace?.updatedAt),
|
||||
lastUserMessageAt,
|
||||
lastTraceId,
|
||||
messageCount: messages.length,
|
||||
messages
|
||||
};
|
||||
return { ...stub, status: resolveConversationSessionStatus(stub) };
|
||||
}
|
||||
|
||||
function mergeConversationRecords(existing: ConversationRecord, selected: ConversationRecord): ConversationRecord {
|
||||
const primary = existing;
|
||||
const supplemental = selected;
|
||||
const primaryMessages = primary.messages ?? [];
|
||||
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 = resolveConversationSessionStatus({ ...primary, messages }, [
|
||||
supplemental.status,
|
||||
supplemental.session?.status,
|
||||
supplemental.snapshot?.sessionStatus,
|
||||
supplemental.snapshot?.status,
|
||||
statusFromMessages(messages)
|
||||
]);
|
||||
return {
|
||||
...supplemental,
|
||||
...primary,
|
||||
conversationId: primary.conversationId,
|
||||
projectId: firstNonEmptyString(primary.projectId, supplemental.projectId) ?? null,
|
||||
sessionId: firstNonEmptyString(primary.sessionId, primary.session?.sessionId, supplemental.sessionId, supplemental.session?.sessionId) ?? null,
|
||||
threadId: firstNonEmptyString(primary.threadId, primary.session?.threadId, supplemental.threadId, supplemental.session?.threadId) ?? null,
|
||||
status,
|
||||
lastTraceId: firstNonEmptyString(latestAgentMessage(messages)?.traceId, messages.at(-1)?.traceId, primary.lastTraceId, supplemental.lastTraceId) ?? null,
|
||||
firstUserMessagePreview: firstNonEmptyString(primary.firstUserMessagePreview, primary.snapshot?.firstUserMessagePreview, supplemental.firstUserMessagePreview, supplemental.snapshot?.firstUserMessagePreview) ?? null,
|
||||
messageCount: messages.length > 0 ? messages.length : primary.messageCount ?? supplemental.messageCount ?? primaryMessages.length,
|
||||
messages: messages.length > 0 ? messages : primary.messages,
|
||||
lastUserMessageAt,
|
||||
updatedAt: lastUserMessageAt ?? firstNonEmptyString(primary.startedAt, primary.updatedAt, primary.snapshot?.updatedAt, supplemental.startedAt, supplemental.updatedAt, supplemental.snapshot?.updatedAt)
|
||||
};
|
||||
}
|
||||
|
||||
function statusFromMessages(messages: ChatMessage[]): string | null {
|
||||
const latestAgent = latestAgentMessage(messages);
|
||||
return firstNonEmptyString(latestAgent?.status, latestAgent?.runnerTrace?.status, messages.at(-1)?.status) ?? null;
|
||||
}
|
||||
|
||||
function latestAgentMessage(messages: ChatMessage[] | undefined): ChatMessage | null {
|
||||
return [...(messages ?? [])].reverse().find((message) => message.role === "agent") ?? null;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { api } from "@/api";
|
||||
import { mergeRunnerTrace, snapshotToRunnerTrace, subscribeToTrace, type TraceSnapshot } from "@/composables/useTraceSubscription";
|
||||
import type { AgentChatResponse, AgentChatResultResponse, AgentRunProvenance, ApiResult, ChatMessage, ConversationRecord, LiveSurface, ProviderProfile, TraceEvent, WorkspaceRecord } from "@/types";
|
||||
import { DEFAULT_WORKBENCH_PROJECT_ID, firstNonEmptyString, nextProtocolId, normalizeWorkbenchConversationId, rememberWorkbenchProjectId, resolveInitialWorkbenchProjectId, workspaceProjectId } from "@/utils";
|
||||
import { RECENT_DRAFTS_STORAGE_KEY, activeTraceIdFromWorkspace, conversationDisplayUpdatedAt, defaultProviderProfileOptions, latestUserMessageAtFromMessages, mergeSelectedConversation, normalizeRecentDrafts, normalizeWorkbenchMessageTitle, providerProfileOptionsFromPayload, recordRecentDraft, resolveCancelableAgentMessage, resolveComposerState, selectedConversationIdFromWorkspace, shouldApplyWorkspaceSnapshot, shouldShowSessionListLoading, sortSessionTabs, workspaceWithClearedActiveTrace, type DraftEntry, type ProviderProfileOption, type SessionStatusAuthority } from "./workbench-session";
|
||||
import { RECENT_DRAFTS_STORAGE_KEY, activeTraceIdFromWorkspace, defaultProviderProfileOptions, normalizeRecentDrafts, normalizeWorkbenchMessageTitle, providerProfileOptionsFromPayload, recordRecentDraft, resolveCancelableAgentMessage, resolveComposerState, selectedConversationIdFromWorkspace, shouldApplyWorkspaceSnapshot, shouldShowSessionListLoading, sortSessionTabs, workspaceWithClearedActiveTrace, type DraftEntry, type ProviderProfileOption, type SessionStatusAuthority } from "./workbench-session";
|
||||
|
||||
const DEFAULT_CODE_AGENT_TIMEOUT_MS = 1_800_000;
|
||||
const DEFAULT_GATEWAY_TIMEOUT_MS = 120_000;
|
||||
@@ -31,7 +31,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const workspaceSelectionEpoch = ref(0);
|
||||
const sessionStatusAuthority = ref<Record<string, SessionStatusAuthority>>({});
|
||||
|
||||
const visibleConversations = computed(() => mergeSelectedConversation(conversations.value, workspace.value));
|
||||
const visibleConversations = computed(() => conversations.value);
|
||||
const activeConversationId = computed(() => firstNonEmptyString(switchingConversationId.value, workspace.value?.selectedConversationId, workspace.value?.workspace?.selectedConversationId, messages.value.find((message) => message.conversationId)?.conversationId));
|
||||
const selectedSessionId = computed(() => firstNonEmptyString(workspace.value?.selectedAgentSessionId, workspace.value?.workspace?.selectedAgentSessionId, activeConversation.value?.sessionId));
|
||||
const selectedThreadId = computed(() => firstNonEmptyString(workspace.value?.workspace?.threadId, activeConversation.value?.threadId));
|
||||
@@ -51,17 +51,24 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
conversationsReady.value = conversations.value.length > 0;
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
const [workspaceResult, conversationsResult] = await Promise.all([api.workbench.workspace(projectId.value), api.workbench.conversations(projectId.value)]);
|
||||
loading.value = false;
|
||||
conversationsReady.value = true;
|
||||
const workspaceResult = await api.workbench.workspace(projectId.value);
|
||||
if (!workspaceResult.ok) {
|
||||
loading.value = false;
|
||||
error.value = workspaceResult.error ?? "workspace unavailable";
|
||||
return;
|
||||
}
|
||||
const nextWorkspace = workspaceResult.data?.workspace ?? null;
|
||||
const nextConversations = conversationsResult.ok ? conversationsResult.data?.conversations ?? [] : [];
|
||||
conversations.value = nextConversations;
|
||||
void refreshSessionStatusAuthority(mergeSelectedConversation(nextConversations, nextWorkspace));
|
||||
const conversationsResult = await api.workbench.conversations(projectId.value, { includeConversationId: selectedConversationIdFromWorkspace(nextWorkspace) });
|
||||
loading.value = false;
|
||||
const nextConversations = conversationsResult.ok ? conversationsResult.data?.conversations ?? [] : conversations.value;
|
||||
if (conversationsResult.ok) {
|
||||
conversations.value = nextConversations;
|
||||
conversationsReady.value = true;
|
||||
void refreshSessionStatusAuthority(nextConversations);
|
||||
} else {
|
||||
conversationsReady.value = conversations.value.length > 0;
|
||||
error.value = conversations.value.length > 0 ? null : conversationsResult.error ?? "session list unavailable";
|
||||
}
|
||||
if (shouldApplyWorkspaceSnapshot({ requestEpoch, currentEpoch: workspaceSelectionEpoch.value, currentConversationId: activeConversationId.value, workspace: nextWorkspace })) {
|
||||
workspace.value = nextWorkspace;
|
||||
providerProfile.value = firstNonEmptyString(workspace.value?.providerProfile, workspace.value?.workspace?.providerProfile, providerProfile.value) ?? providerProfile.value;
|
||||
@@ -98,7 +105,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
messages.value = messagesFromWorkspace(workspace.value);
|
||||
currentRequest.value = null;
|
||||
await refreshSelectedSessionStatus();
|
||||
await refreshConversations();
|
||||
await refreshConversations(selectedConversationIdFromWorkspace(workspace.value));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +130,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
reattachRestoredActiveTrace();
|
||||
currentRequest.value = null;
|
||||
await refreshSelectedSessionStatus(conversation);
|
||||
await refreshConversations(conversation.conversationId);
|
||||
return;
|
||||
}
|
||||
if (response.status === 404 && await retrySelectConversationWithFreshWorkspace(conversation, tabProjectId, requestEpoch)) return;
|
||||
@@ -152,7 +160,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
error.value = response.error ?? "session URL not found";
|
||||
return false;
|
||||
}
|
||||
conversations.value = [conversation, ...conversations.value.filter((item) => item.conversationId !== normalized)];
|
||||
void refreshSessionStatusById(sessionIdFromConversation(conversation));
|
||||
await selectConversation(conversation);
|
||||
return activeConversationId.value === normalized;
|
||||
@@ -166,20 +173,23 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
workspace.value = response.data?.workspace ?? workspace.value;
|
||||
messages.value = messagesFromWorkspace(workspace.value);
|
||||
currentRequest.value = null;
|
||||
await refreshConversations();
|
||||
await refreshConversations(selectedConversationIdFromWorkspace(workspace.value));
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshConversations(): Promise<void> {
|
||||
const response = await api.workbench.conversations(activeProjectId.value);
|
||||
conversationsReady.value = true;
|
||||
async function refreshConversations(includeConversationId: string | null = currentListIncludeConversationId()): Promise<void> {
|
||||
const response = await api.workbench.conversations(activeProjectId.value, { includeConversationId });
|
||||
if (response.ok) {
|
||||
conversations.value = response.data?.conversations ?? [];
|
||||
await refreshSessionStatusAuthority(visibleConversations.value);
|
||||
conversationsReady.value = true;
|
||||
await refreshSessionStatusAuthority(conversations.value);
|
||||
return;
|
||||
}
|
||||
conversationsReady.value = conversations.value.length > 0;
|
||||
if (conversations.value.length === 0) error.value = response.error ?? "session list unavailable";
|
||||
}
|
||||
|
||||
async function refreshSessionStatusAuthority(source: ConversationRecord[] = visibleConversations.value): Promise<void> {
|
||||
async function refreshSessionStatusAuthority(source: ConversationRecord[] = conversations.value): Promise<void> {
|
||||
await Promise.all(uniqueSessionIds(source).map((sessionId) => refreshSessionStatusById(sessionId)));
|
||||
}
|
||||
|
||||
@@ -217,46 +227,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
};
|
||||
}
|
||||
|
||||
function bumpConversationActivity(input: { conversationId?: string | null; sessionId?: string | null; threadId?: string | null; traceId?: string | null; status?: string | null; updatedAt?: string | null }): void {
|
||||
const traceMessage = input.traceId ? messages.value.find((message) => message.traceId === input.traceId) : null;
|
||||
const conversationId = firstNonEmptyString(input.conversationId, traceMessage?.conversationId, activeConversationId.value);
|
||||
if (!conversationId) return;
|
||||
const existing = conversations.value.find((conversation) => conversation.conversationId === conversationId) ?? null;
|
||||
const activityMessages = activeConversationId.value === conversationId ? messages.value : messages.value.filter((message) => message.conversationId === conversationId);
|
||||
const latestAgent = [...activityMessages].reverse().find((message) => message.role === "agent");
|
||||
const latestTrace = [...activityMessages].reverse().find((message) => message.traceId);
|
||||
const userMessage = activityMessages.find((message) => message.role === "user");
|
||||
const status = firstNonEmptyString(input.status, latestAgent?.status, existing?.status) ?? existing?.status ?? null;
|
||||
const sessionId = firstNonEmptyString(input.sessionId, traceMessage?.sessionId, latestAgent?.sessionId, existing?.sessionId, existing?.session?.sessionId, selectedSessionId.value) ?? null;
|
||||
const threadId = firstNonEmptyString(input.threadId, traceMessage?.threadId, latestAgent?.threadId, existing?.threadId, existing?.session?.threadId, selectedThreadId.value) ?? null;
|
||||
const lastTraceId = firstNonEmptyString(input.traceId, latestTrace?.traceId, existing?.lastTraceId) ?? null;
|
||||
const nextMessages = activityMessages.length > 0 ? activityMessages : existing?.messages ?? [];
|
||||
const lastUserMessageAt = latestUserMessageAtFromMessages(nextMessages) ?? firstNonEmptyString(existing?.lastUserMessageAt, existing?.snapshot?.lastUserMessageAt) ?? null;
|
||||
const updatedAt = lastUserMessageAt ?? conversationDisplayUpdatedAt(existing ?? { conversationId }) ?? firstNonEmptyString(input.updatedAt, traceMessage?.updatedAt, latestAgent?.updatedAt, latestAgent?.runnerTrace?.updatedAt, latestTrace?.updatedAt, new Date().toISOString()) ?? new Date().toISOString();
|
||||
const firstUserMessagePreview = firstNonEmptyString(existing?.firstUserMessagePreview, existing?.snapshot?.firstUserMessagePreview, userMessage?.text) ?? null;
|
||||
const next: ConversationRecord = {
|
||||
...(existing ?? { conversationId }),
|
||||
conversationId,
|
||||
projectId: existing?.projectId ?? activeProjectId.value,
|
||||
sessionId,
|
||||
threadId,
|
||||
status,
|
||||
lastTraceId,
|
||||
updatedAt,
|
||||
lastUserMessageAt,
|
||||
messages: nextMessages,
|
||||
messageCount: nextMessages.length || existing?.messageCount,
|
||||
firstUserMessagePreview,
|
||||
snapshot: {
|
||||
...(existing?.snapshot ?? {}),
|
||||
...(status ? { sessionStatus: status } : {}),
|
||||
...(lastUserMessageAt ? { lastUserMessageAt } : {}),
|
||||
updatedAt
|
||||
}
|
||||
};
|
||||
conversations.value = [next, ...conversations.value.filter((conversation) => conversation.conversationId !== conversationId)];
|
||||
}
|
||||
|
||||
async function submitMessage(text: string): Promise<void> {
|
||||
const value = text.trim();
|
||||
if (!value) return;
|
||||
@@ -274,7 +244,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const user = makeMessage("user", value, "sent", { traceId: steerTraceId ?? traceId, conversationId, sessionId, threadId, title: steerMode ? "用户引导" : "用户" });
|
||||
const pending = makeMessage("agent", "", "running", { traceId, conversationId, sessionId, threadId, title: "Code Agent", retryInput: value });
|
||||
messages.value.push(user, pending);
|
||||
bumpConversationActivity({ conversationId, sessionId, threadId, traceId, status: "running" });
|
||||
void refreshSessionStatusById(sessionId);
|
||||
chatPending.value = true;
|
||||
currentRequest.value = { traceId, conversationId, sessionId, threadId, status: "running" };
|
||||
@@ -292,6 +261,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
return;
|
||||
}
|
||||
void refreshSessionStatusById(sessionId);
|
||||
void refreshConversations(conversationId);
|
||||
subscribe(traceId, response.data);
|
||||
}
|
||||
|
||||
@@ -423,7 +393,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
function applyTraceSnapshot(traceId: string, snapshot: TraceSnapshot): void {
|
||||
const trace = snapshotToRunnerTrace(snapshot);
|
||||
messages.value = messages.value.map((message) => message.traceId === traceId ? { ...message, runnerTrace: mergeRunnerTrace(message.runnerTrace, trace), updatedAt: new Date().toISOString() } : message);
|
||||
bumpConversationActivity({ traceId, sessionId: trace.sessionId, threadId: trace.threadId, status: firstNonEmptyString(trace.status), updatedAt: firstNonEmptyString(trace.updatedAt) });
|
||||
void refreshSessionStatusById(trace.sessionId ?? selectedSessionId.value);
|
||||
}
|
||||
|
||||
@@ -437,11 +406,11 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
|
||||
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: terminalStatus, updatedAt: resultActivityUpdatedAt(result) });
|
||||
chatPending.value = false;
|
||||
currentRequest.value = null;
|
||||
void clearActiveTrace(traceId, "trace-terminal", workspaceSessionStatusFromChatStatus(terminalStatus));
|
||||
void refreshSessionStatusById(result.sessionId ?? selectedSessionId.value);
|
||||
void refreshConversations(firstNonEmptyString((result as Record<string, unknown>).conversationId, activeConversationId.value));
|
||||
}
|
||||
|
||||
async function hydrateTerminalMessageDiagnostics(): Promise<void> {
|
||||
@@ -464,8 +433,8 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
const status = statusFromResult(result.status);
|
||||
return { ...message, status, title: normalizeWorkbenchMessageTitle(message.role, message.title), 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) });
|
||||
void refreshSessionStatusById(result.sessionId ?? selectedSessionId.value);
|
||||
void refreshConversations(firstNonEmptyString((result as Record<string, unknown>).conversationId, activeConversationId.value));
|
||||
}
|
||||
|
||||
function failTrace(traceId: string, message: string): void {
|
||||
@@ -478,8 +447,6 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
|
||||
function markMessage(traceId: string, patch: Partial<ChatMessage>): void {
|
||||
messages.value = messages.value.map((message) => message.traceId === traceId && message.role === "agent" ? { ...message, ...patch, updatedAt: new Date().toISOString() } : message);
|
||||
const updated = messages.value.find((message) => message.traceId === traceId && message.role === "agent");
|
||||
bumpConversationActivity({ conversationId: updated?.conversationId, sessionId: updated?.sessionId, threadId: updated?.threadId, traceId, status: patch.status ?? updated?.status, updatedAt: updated?.updatedAt });
|
||||
}
|
||||
|
||||
async function ensureWorkspace(): Promise<WorkspaceRecord | null> {
|
||||
@@ -540,9 +507,14 @@ export const useWorkbenchStore = defineStore("workbench", () => {
|
||||
reattachRestoredActiveTrace();
|
||||
currentRequest.value = null;
|
||||
await refreshSelectedSessionStatus(conversation);
|
||||
await refreshConversations(conversation.conversationId);
|
||||
return true;
|
||||
}
|
||||
|
||||
function currentListIncludeConversationId(): string | null {
|
||||
return firstNonEmptyString(switchingConversationId.value, activeConversationId.value, selectedConversationIdFromWorkspace(workspace.value));
|
||||
}
|
||||
|
||||
function reattachRestoredActiveTrace(): void {
|
||||
const traceId = firstNonEmptyString(activeTraceIdFromMessages(messages.value), activeTraceIdFromWorkspace(workspace.value));
|
||||
if (traceId) void validateAndReattachTrace(traceId);
|
||||
@@ -737,12 +709,6 @@ function recordValue(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
function resultActivityUpdatedAt(result: AgentChatResultResponse): string | null {
|
||||
const finalResponse = recordValue(result.finalResponse);
|
||||
const reply = recordValue(result.reply);
|
||||
return firstNonEmptyString(result.updatedAt, finalResponse?.updatedAt, reply?.updatedAt, finalResponse?.createdAt, reply?.createdAt);
|
||||
}
|
||||
|
||||
function firstArray(...values: unknown[]): TraceEvent[] {
|
||||
for (const value of values) {
|
||||
if (Array.isArray(value)) return value as TraceEvent[];
|
||||
|
||||
Reference in New Issue
Block a user