From 20543f7bf8826d2b5ba6f0b5a2d1bbc719974405 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 16 Jul 2026 18:17:02 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=9B=BA=E5=AE=9A=20AgentRun=20?= =?UTF-8?q?=E5=85=B3=E7=B3=BB=E6=A0=91=E8=8A=82=E7=82=B9=E9=A1=BA=E5=BA=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../scripts/agent-observer-contract.test.ts | 7 +++++++ web/hwlab-cloud-web/src/stores/agent-observer.ts | 13 +++++++++++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/web/hwlab-cloud-web/scripts/agent-observer-contract.test.ts b/web/hwlab-cloud-web/scripts/agent-observer-contract.test.ts index 5ef315aa..ae75d83b 100644 --- a/web/hwlab-cloud-web/scripts/agent-observer-contract.test.ts +++ b/web/hwlab-cloud-web/scripts/agent-observer-contract.test.ts @@ -27,9 +27,16 @@ test("Agent observer relation tree consumes the YAML node limit", () => { assert.equal(window.rows.length, 3); }); +test("Agent observer relation tree keeps a stable order across live activity reordering", () => { + const first = buildRelationWindow([observerRun("run_2"), observerRun("run_1")], null, new Set(), new Set(), 10); + const second = buildRelationWindow([observerRun("run_1"), observerRun("run_2")], null, new Set(), new Set(), 10); + assert.deepEqual(first.rows.map((row) => row.id), second.rows.map((row) => row.id)); +}); + function observerRun(id: string): AgentObserverRun { return { id, + title: null, taskId: null, taskStatus: null, commandId: null, diff --git a/web/hwlab-cloud-web/src/stores/agent-observer.ts b/web/hwlab-cloud-web/src/stores/agent-observer.ts index 54245523..bee8376f 100644 --- a/web/hwlab-cloud-web/src/stores/agent-observer.ts +++ b/web/hwlab-cloud-web/src/stores/agent-observer.ts @@ -591,7 +591,8 @@ export function buildRelationWindow( }); let omittedRunCount = 0; - for (const [runIndex, run] of runs.entries()) { + const stableRuns = [...runs].sort((left, right) => compareText(relationRunOrderKey(left), relationRunOrderKey(right))); + for (const [runIndex, run] of stableRuns.entries()) { const hasCompleteContainment = Boolean((run.projectId || run.workspace) && run.sessionId && run.taskId); const scopeId = hasCompleteContainment ? relationId("scope", `${run.projectId ?? ""}\u0000${run.workspace ?? ""}`) : unassociatedId; const sessionId = hasCompleteContainment && run.sessionId ? relationId("session", `${scopeId}\u0000${run.sessionId}`) : null; @@ -603,7 +604,7 @@ export function buildRelationWindow( const commandId = run.commandId ? relationId("command", `${commandParentId}\u0000${run.commandId}`) : null; const missingNodeCount = [scopeId, sessionId, taskId, runNodeId, attemptId, commandId].filter((id): id is string => typeof id === "string" && !nodes.has(id)).length; if (nodes.size + missingNodeCount > nodeLimit) { - omittedRunCount = runs.length - runIndex; + omittedRunCount = stableRuns.length - runIndex; break; } let parent = hasCompleteContainment @@ -713,6 +714,14 @@ function relationId(kind: AgentObserverRelationKind, value: string): string { return `${kind}:${encodeURIComponent(value)}`; } +function relationRunOrderKey(run: AgentObserverRun): string { + return [run.projectId, run.workspace, run.sessionId, run.taskId, run.id].map((value) => value ?? "").join("\u0000"); +} + +function compareText(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0; +} + function latestTimestamp(previous: string | null, next: string | null): string | null { if (!previous) return next; if (!next) return previous;