From 4ea4596617500b6c57fbad8c00a780e8bbfdf1ce Mon Sep 17 00:00:00 2001 From: Josh Avant <830519+joshavant@users.noreply.github.com> Date: Thu, 6 Aug 2026 04:30:35 -0500 Subject: [PATCH] fix: bind semantic progress to run owner --- src/infra/diagnostic-events.ts | 2 +- src/logging/diagnostic-run-activity.test.ts | 75 ++++++++++++++++++--- src/logging/diagnostic-run-activity.ts | 9 +-- 3 files changed, 70 insertions(+), 16 deletions(-) diff --git a/src/infra/diagnostic-events.ts b/src/infra/diagnostic-events.ts index 5f057b6823ff..87420dc850fd 100644 --- a/src/infra/diagnostic-events.ts +++ b/src/infra/diagnostic-events.ts @@ -372,7 +372,7 @@ export type DiagnosticRunProgressEvent = DiagnosticBaseEvent & { sessionId?: string; runId?: string; reason: string; - /** Only explicit semantic progress resets evidence; liveness or omission only keeps work alive. */ + /** Semantic progress resets evidence only for its matching runId; other progress is liveness. */ progressKind?: "semantic" | "liveness"; }; diff --git a/src/logging/diagnostic-run-activity.test.ts b/src/logging/diagnostic-run-activity.test.ts index fe0e7e5c6785..6cc12f312e13 100644 --- a/src/logging/diagnostic-run-activity.test.ts +++ b/src/logging/diagnostic-run-activity.test.ts @@ -598,7 +598,39 @@ describe("repeated request liveness", () => { ).toBeUndefined(); }); - it("orders semantic progress across merged session aliases", () => { + it("keeps replacement-owner evidence across delayed semantic event delivery", async () => { + const ref = { sessionId: "queued-owner-session", sessionKey: "agent:main:queued-owner" }; + + startDiagnosticRunActivityTracking(); + markDiagnosticEmbeddedRunStarted({ ...ref, runId: "queued-old-owner" }); + emitTrustedDiagnosticEvent({ + type: "run.progress", + ...ref, + runId: "queued-old-owner", + reason: "delayed-old-owner-output", + progressKind: "semantic", + }); + + markDiagnosticEmbeddedRunStarted({ ...ref, runId: "queued-new-owner" }); + for (let attempt = 0; attempt < 2; attempt += 1) { + markDiagnosticModelStartedForTest({ + ...ref, + runId: "queued-new-owner", + provider: "mock", + model: "request-model", + observationUnit: "request", + }); + } + + await waitForDiagnosticEventsDrained(); + + expect(getDiagnosticSessionActivitySnapshot(ref)).toMatchObject({ + lastProgressReason: "delayed-old-owner-output", + repeatedRequestNoProgressAgeMs: expect.any(Number), + }); + }); + + it("requires an owned semantic event across merged session aliases", () => { vi.useFakeTimers(); const startedAt = Date.parse("2026-08-04T02:00:00Z"); vi.setSystemTime(startedAt); @@ -607,24 +639,45 @@ describe("repeated request liveness", () => { const runId = "merge-run"; markDiagnosticEmbeddedRunStarted({ sessionId, runId }); - markDiagnosticModelStartedForTest({ + for (let attempt = 0; attempt < 2; attempt += 1) { + markDiagnosticModelStartedForTest({ + sessionId, + runId, + provider: "mock", + model: "request-model", + observationUnit: "request", + }); + } + markDiagnosticRunProgress({ sessionId, - runId, - provider: "mock", - model: "request-model", - observationUnit: "request", + sessionKey, + reason: "ownerless:semantic", + progressKind: "semantic", }); markDiagnosticRunProgress({ + sessionId, sessionKey, - reason: "reply:delivered", + runId: " ", + reason: "whitespace-owner:semantic", progressKind: "semantic", }); vi.setSystemTime(startedAt + 6 * 60_000); - expect( - getDiagnosticSessionActivitySnapshot({ sessionId, sessionKey }) - .repeatedRequestNoProgressAgeMs, - ).toBeUndefined(); + expect(getDiagnosticSessionActivitySnapshot({ sessionId, sessionKey })).toMatchObject({ + lastProgressReason: "whitespace-owner:semantic", + repeatedRequestNoProgressAgeMs: 6 * 60_000, + }); + + markDiagnosticRunProgress({ + sessionKey, + runId: ` ${runId} `, + reason: "owned:semantic", + progressKind: "semantic", + }); + expect(getDiagnosticSessionActivitySnapshot({ sessionId, sessionKey })).toMatchObject({ + lastProgressReason: "owned:semantic", + repeatedRequestNoProgressAgeMs: undefined, + }); }); it("keeps repeated request evidence across same-logical-owner attempt rearming", async () => { diff --git a/src/logging/diagnostic-run-activity.ts b/src/logging/diagnostic-run-activity.ts index ef8e5e30ca0b..f4490137e723 100644 --- a/src/logging/diagnostic-run-activity.ts +++ b/src/logging/diagnostic-run-activity.ts @@ -338,16 +338,17 @@ export function markDiagnosticArgumentChurnObservation( } export function markDiagnosticRunProgress(params: DiagnosticRunProgressActivityEvent): void { - const activity = resolveSessionActivity({ ...params, create: true }); + const runId = params.runId?.trim() || undefined; + const activity = resolveSessionActivity({ ...params, runId, create: true }); if (!activity) { return; } - // run.progress predates progressKind; only an explicit semantic fact may clear evidence. - if (params.progressKind !== "semantic") { + // Only an explicit fact from the current owner may clear its recovery evidence. + if (params.progressKind !== "semantic" || !runId) { touchSessionActivity(activity, params.reason); return; } - touchSemanticSessionActivity(activity, params.reason, { runId: params.runId }); + touchSemanticSessionActivity(activity, params.reason, { runId }); } function recordRunCompleted(