diff --git a/src/logging/diagnostic-run-activity.test.ts b/src/logging/diagnostic-run-activity.test.ts index 6cc12f312e13..0587fda743ba 100644 --- a/src/logging/diagnostic-run-activity.test.ts +++ b/src/logging/diagnostic-run-activity.test.ts @@ -3,6 +3,7 @@ import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures"; import { afterEach, describe, expect, it, vi } from "vitest"; import { hasInternalDiagnosticEventListeners } from "../infra/diagnostic-event-listener-presence.js"; import { + emitDiagnosticEvent, emitTrustedDiagnosticEvent, resetDiagnosticEventsForTest, waitForDiagnosticEventsDrained, @@ -630,6 +631,51 @@ describe("repeated request liveness", () => { }); }); + it("keeps active-owner evidence across untrusted semantic progress", async () => { + const ref = { sessionId: "untrusted-session", sessionKey: "agent:main:untrusted" }; + const runId = "untrusted-run"; + + startDiagnosticRunActivityTracking(); + markDiagnosticEmbeddedRunStarted({ ...ref, runId }); + for (let attempt = 0; attempt < 2; attempt += 1) { + markDiagnosticModelStartedForTest({ + ...ref, + runId, + provider: "mock", + model: "request-model", + observationUnit: "request", + }); + } + + emitDiagnosticEvent({ + type: "run.progress", + ...ref, + runId, + reason: "plugin:semantic", + progressKind: "semantic", + }); + await waitForDiagnosticEventsDrained(); + + expect(getDiagnosticSessionActivitySnapshot(ref)).toMatchObject({ + lastProgressReason: "plugin:semantic", + repeatedRequestNoProgressAgeMs: expect.any(Number), + }); + + emitTrustedDiagnosticEvent({ + type: "run.progress", + ...ref, + runId, + reason: "model_result:semantic", + progressKind: "semantic", + }); + await waitForDiagnosticEventsDrained(); + + expect(getDiagnosticSessionActivitySnapshot(ref)).toMatchObject({ + lastProgressReason: "model_result:semantic", + repeatedRequestNoProgressAgeMs: undefined, + }); + }); + it("requires an owned semantic event across merged session aliases", () => { vi.useFakeTimers(); const startedAt = Date.parse("2026-08-04T02:00:00Z"); diff --git a/src/logging/diagnostic-run-activity.ts b/src/logging/diagnostic-run-activity.ts index f4490137e723..0d2aecea83c8 100644 --- a/src/logging/diagnostic-run-activity.ts +++ b/src/logging/diagnostic-run-activity.ts @@ -324,8 +324,8 @@ function recordModelEnded( touchSessionActivity(activity, "model_call:ended"); } -function recordRunProgress(event: DiagnosticRunProgressActivityEvent): void { - markDiagnosticRunProgress(event); +function recordRunProgress(event: DiagnosticRunProgressActivityEvent, trusted: boolean): void { + markDiagnosticRunProgress(trusted ? event : { ...event, progressKind: "liveness" }); } export function markDiagnosticArgumentChurnObservation( @@ -733,7 +733,7 @@ export function startDiagnosticRunActivityTracking(): void { return; } const startAfterEventSequence = getInternalDiagnosticEventSequence(); - unregisterDiagnosticRunActivityListener = onInternalDiagnosticEvent((event) => { + unregisterDiagnosticRunActivityListener = onInternalDiagnosticEvent((event, metadata) => { // A prior lifecycle can leave already-sequenced events in the async queue. // Ignore them so a restart cannot recreate activity that stop cleared. if (event.seq <= startAfterEventSequence) { @@ -756,7 +756,7 @@ export function startDiagnosticRunActivityTracking(): void { recordModelEnded(event); return; case "run.progress": - recordRunProgress(event); + recordRunProgress(event, metadata.trusted); return; case "run.completed": recordRunCompleted(event);