From cf269289e4bbdd9e0f70c12adb3cedcd178562be Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Wed, 2 Sep 2026 12:59:48 -0700 Subject: [PATCH] fix(agents): preserve canonical coverage provenance --- .../pane-agent-identity-adapter.test.ts | 20 +++++++++++++++- src/shared/pane-agent-identity-adapter.ts | 23 +++++++++++++++---- ...ublished-pane-agent-identity-comparison.ts | 10 ++++++-- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/src/shared/pane-agent-identity-adapter.test.ts b/src/shared/pane-agent-identity-adapter.test.ts index 883ab4675f5..ea52fe50477 100644 --- a/src/shared/pane-agent-identity-adapter.test.ts +++ b/src/shared/pane-agent-identity-adapter.test.ts @@ -120,6 +120,19 @@ describe('uncovered compatibility lane', () => { }) expect(identity).toMatchObject({ agent: null, source: null, coverage: 'uncovered' }) }) + + it('does not label a foreground-only compatibility answer as title-only', () => { + const identity = resolveCanonicalPaneAgentIdentity({ + foregroundAgent: 'codex', + uncoveredFallback: { agent: 'codex' } + }) + expect(identity).toMatchObject({ + agent: 'codex', + source: null, + coverage: 'uncovered', + titleOnly: false + }) + }) }) describe('canonical ladder inside the covered lane', () => { @@ -178,7 +191,12 @@ describe('reclaim-versus-stale-hook discriminator (run keys, not title text)', ( currentRun: run2, title: 'STA-4011 Linux Antigravity Commit Messages - grok' }) - expect(identity).toMatchObject({ agent: 'grok', source: 'title' }) + expect(identity).toMatchObject({ + agent: 'grok', + source: 'title', + coverage: 'uncovered', + titleOnly: true + }) expect(identity.supersededSources).toEqual(['completed-hook']) }) diff --git a/src/shared/pane-agent-identity-adapter.ts b/src/shared/pane-agent-identity-adapter.ts index c9508c83e63..6562f5f551d 100644 --- a/src/shared/pane-agent-identity-adapter.ts +++ b/src/shared/pane-agent-identity-adapter.ts @@ -145,9 +145,24 @@ export function resolveCanonicalPaneAgentIdentity( input: CanonicalPaneAgentIdentityInput ): CanonicalPaneAgentIdentity { const processEvidence = processEvidenceFromProof(input) - // Coverage comes from authority-bearing sources only: hook, proven process, launch, sleeping - // session. A bare foreground name, a sibling, and a title never cover a pane. + // Coverage comes from authority-bearing sources that are still eligible for this run. A stale + // hook/launch row can remain in the input after a pane is replaced; it must not make a title-only + // answer look covered to a future action consumer. + const runIsEligible = (run: PaneAgentRunKey | undefined): boolean => + run === undefined || + input.currentRun === undefined || + run.authorityId !== input.currentRun.authorityId || + run.incarnation === input.currentRun.incarnation const covered = Boolean( + (input.hookAgent && runIsEligible(input.hookRun)) || + (input.completedHookAgent && runIsEligible(input.completedHookRun)) || + processEvidence || + (input.launchAgent && runIsEligible(input.launchRun)) || + (input.sleepingSessionAgent && runIsEligible(input.sleepingRun)) + ) + // Keep stale evidence in the resolver so diagnostics still report which source was superseded, + // even when it no longer qualifies the pane as covered. + const hasAuthorityEvidence = Boolean( input.hookAgent || input.completedHookAgent || processEvidence || @@ -156,7 +171,7 @@ export function resolveCanonicalPaneAgentIdentity( ) const titleAgent = input.title ? collectAgentTitleEvidence(input.title).agent : null - if (!covered) { + if (!hasAuthorityEvidence) { if (input.uncoveredFallback) { const agent = input.uncoveredFallback.agent const titleOnly = @@ -227,7 +242,7 @@ export function resolveCanonicalPaneAgentIdentity( return { agent: resolved.agent, source: resolved.source, - coverage: 'covered', + coverage: covered ? 'covered' : 'uncovered', titleOnly: resolved.source === 'title', ...(resolved.ambiguousAt ? { ambiguousAt: resolved.ambiguousAt } : {}), supersededSources: resolved.supersededSources diff --git a/src/shared/published-pane-agent-identity-comparison.ts b/src/shared/published-pane-agent-identity-comparison.ts index 91f1c27d8aa..c5cd0a3f990 100644 --- a/src/shared/published-pane-agent-identity-comparison.ts +++ b/src/shared/published-pane-agent-identity-comparison.ts @@ -65,15 +65,21 @@ function recordCanonicalDivergence( } // No host process PROOF exists yet, so the canonical lane sees the foreground name as a weak // hint only. Where that alone flips the answer is precisely what this window measures. + const titleAgent = args.title ? collectAgentTitleEvidence(args.title).agent : null const canonical = resolveCanonicalPaneAgentIdentity({ hookAgent: args.hookAgent, hookIsLive: args.hookIsLive, launchAgent: args.launchAgent, foregroundAgent: args.foregroundAgent, title: args.title, - uncoveredFallback: { agent: published, titleOnly: published !== null } + uncoveredFallback: { + agent: published, + // A bare foreground name is an uncovered compatibility answer, but it is not title proof. + // Mark title-only only when no foreground signal exists and the frozen answer matches title + // evidence exactly. + titleOnly: args.foregroundAgent == null && published !== null && published === titleAgent + } }) - const titleAgent = args.title ? collectAgentTitleEvidence(args.title).agent : null recorder.record({ surface: args.surface, paneId: args.paneId,