From d7202b808da4e2afc6468621633fc4776ba1a9d1 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:27:59 +0800 Subject: [PATCH] fix: unify session tab status authority (#1336) --- internal/cloud/server-code-agent-http.ts | 20 +++--- .../workbench-r2-session-parity.test.ts | 28 +++++--- web/hwlab-cloud-web/src/api/agent.ts | 1 + .../src/components/workbench/SessionRail.vue | 2 +- .../src/stores/workbench-session.ts | 20 ++++-- web/hwlab-cloud-web/src/stores/workbench.ts | 70 ++++++++++++++++++- 6 files changed, 115 insertions(+), 26 deletions(-) diff --git a/internal/cloud/server-code-agent-http.ts b/internal/cloud/server-code-agent-http.ts index 2e4ced19..a74dbbee 100644 --- a/internal/cloud/server-code-agent-http.ts +++ b/internal/cloud/server-code-agent-http.ts @@ -694,6 +694,17 @@ function firstNonEmptyValue(...values) { function submitCodeAgentChatTurn({ params, options, traceId }) { const traceStore = options.traceStore ?? defaultCodeAgentTraceStore; const results = options.codeAgentChatResults ?? createCodeAgentChatResultStore(); + const acceptedPayload = { + accepted: true, + status: "running", + traceId, + conversationId: safeConversationId(params.conversationId) || null, + sessionId: safeSessionId(params.sessionId) || null, + threadId: safeOpaqueId(params.threadId) || null, + projectId: params.projectId ?? null, + updatedAt: new Date().toISOString() + }; + void recordCodeAgentSessionOwner({ payload: acceptedPayload, params, options, status: "running" }); if (codeAgentAgentRunAdapterEnabled(options.env ?? process.env)) { const initial = withCodeAgentBillingReservation(initialAgentRunChatResult({ params, options, traceId }), params); results.set(traceId, annotateOwner(initial, params)); @@ -744,14 +755,7 @@ function submitCodeAgentChatTurn({ params, options, traceId }) { setImmediate(() => { run(); }); return; } - results.set(traceId, { - accepted: true, - status: "running", - traceId, - conversationId: safeConversationId(params.conversationId) || null, - sessionId: safeSessionId(params.sessionId) || null, - updatedAt: new Date().toISOString() - }); + results.set(traceId, acceptedPayload); traceStore.ensure(traceId, { runnerKind: "codex-app-server-stdio-runner", workspace: options.workspace ?? options.env?.HWLAB_CODE_AGENT_CODEX_WORKSPACE ?? options.env?.HWLAB_CODE_AGENT_WORKSPACE ?? null, diff --git a/web/hwlab-cloud-web/scripts/workbench-r2-session-parity.test.ts b/web/hwlab-cloud-web/scripts/workbench-r2-session-parity.test.ts index 01ac192f..299194d2 100644 --- a/web/hwlab-cloud-web/scripts/workbench-r2-session-parity.test.ts +++ b/web/hwlab-cloud-web/scripts/workbench-r2-session-parity.test.ts @@ -146,12 +146,12 @@ test("R2 running session tabs expose animation state without rewriting label", ( agentMessage({ conversationId: "cnv_running", status: "running", updatedAt: "2026-01-04T00:01:00.000Z" }) ] } - ], null); + ], null, { ses_running: { sessionId: "ses_running", status: "running" } }); assert.equal(tabs[0]?.label, "保持原始标题"); assert.equal(tabs[0]?.running, true); }); -test("R2 selected workspace stubs cannot clear a running conversation tab", () => { +test("R2 session tabs do not infer running state from workspace stubs", () => { const conversations: ConversationRecord[] = [{ conversationId: "cnv_r2", sessionId: "ses_r2", @@ -166,13 +166,16 @@ test("R2 selected workspace stubs cannot clear a running conversation tab", () = const workspace = workspaceRecord({ activeTraceId: null, sessionStatus: "idle" }); const merged = mergeSelectedConversation(conversations, workspace); const tabs = sortSessionTabs(merged, "cnv_r2"); - assert.equal(tabs[0]?.status, "running"); - assert.equal(tabs[0]?.running, true); + 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 the selected session tab before list refresh catches up", () => { +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" }) @@ -184,13 +187,16 @@ test("R2 active in-flight turn updates the selected session tab before list refr chatPending: true }); const tabs = sortSessionTabs(merged, "cnv_live"); - assert.equal(tabs[0]?.status, "running"); - assert.equal(tabs[0]?.running, true); + 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" } }); + assert.equal(authorityTabs[0]?.status, "running"); + assert.equal(authorityTabs[0]?.running, true); }); -test("R2 session tabs keep restored running message ahead of stale idle conversation status", () => { +test("R2 session tabs keep trace metadata but status comes only from session authority", () => { const conversation: ConversationRecord = { conversationId: "cnv_stale_snapshot", sessionId: "ses_stale_snapshot", @@ -200,8 +206,12 @@ test("R2 session tabs keep restored running message ahead of stale idle conversa }; assert.equal(resolveConversationSessionStatus(conversation), "running"); const tabs = sortSessionTabs([conversation], "cnv_stale_snapshot"); - assert.equal(tabs[0]?.running, true); + assert.equal(tabs[0]?.status, "unknown"); + assert.equal(tabs[0]?.running, false); assert.equal(tabs[0]?.lastTraceId, "trc_stale"); + const authorityTabs = sortSessionTabs([conversation], "cnv_stale_snapshot", { ses_stale_snapshot: { sessionId: "ses_stale_snapshot", status: "idle" } }); + assert.equal(authorityTabs[0]?.status, "idle"); + assert.equal(authorityTabs[0]?.running, false); }); test("R2 agent message titles drop transient running wording", () => { diff --git a/web/hwlab-cloud-web/src/api/agent.ts b/web/hwlab-cloud-web/src/api/agent.ts index b636b5d6..c6893678 100644 --- a/web/hwlab-cloud-web/src/api/agent.ts +++ b/web/hwlab-cloud-web/src/api/agent.ts @@ -5,5 +5,6 @@ export const agentAPI = { sendAgentMessage: (payload: Record, timeoutMs: number, activityRef?: ApiRequestOptions["activityRef"]): Promise> => fetchJson("/v1/agent/chat", { method: "POST", body: JSON.stringify(payload), timeoutMs, timeoutName: "Code Agent", activityRef }), steerAgentMessage: (payload: Record, timeoutMs: number, activityRef?: ApiRequestOptions["activityRef"]): Promise> => fetchJson("/v1/agent/chat/steer", { method: "POST", body: JSON.stringify(payload), timeoutMs, timeoutName: "Code Agent steer", activityRef }), getAgentChatResult: (resultUrl: string, timeoutMs = 8000, activityRef?: ApiRequestOptions["activityRef"]): Promise> => fetchJson(resultUrl, { timeoutMs, timeoutName: "Code Agent result", activityRef }), + getAgentSession: (sessionId: string): Promise }>> => fetchJson(`/v1/agent/sessions/${encodeURIComponent(sessionId)}`, { timeoutMs: 8000, timeoutName: "Code Agent session" }), cancelAgentMessage: (payload: Record): Promise> => fetchJson("/v1/agent/chat/cancel", { method: "POST", body: JSON.stringify(payload), timeoutMs: 30000, timeoutName: "Code Agent cancel" }) }; diff --git a/web/hwlab-cloud-web/src/components/workbench/SessionRail.vue b/web/hwlab-cloud-web/src/components/workbench/SessionRail.vue index 26a814da..b293472f 100644 --- a/web/hwlab-cloud-web/src/components/workbench/SessionRail.vue +++ b/web/hwlab-cloud-web/src/components/workbench/SessionRail.vue @@ -87,7 +87,7 @@ function formatSessionUpdatedTime(value: string | null | undefined): string {