fix(agents): fence unknown canonical evidence sources

This commit is contained in:
Merge Sim
2026-09-03 13:53:21 -07:00
parent 3cd84bef0a
commit 8fe7794f64
5 changed files with 62 additions and 6 deletions
@@ -121,6 +121,21 @@ describe('uncovered compatibility lane', () => {
expect(identity).toMatchObject({ agent: null, source: null, coverage: 'uncovered' })
})
it('does not let a legacy title fallback bypass the ambiguity fence', () => {
expect(
resolveCanonicalPaneAgentIdentity({
title: 'OC | something - grok',
uncoveredFallback: { agent: 'opencode', titleOnly: true }
})
).toMatchObject({ agent: null, source: null, ambiguousAt: 'title' })
expect(
resolveCanonicalPaneAgentIdentity({
title: 'compare codex with grok',
uncoveredFallback: { agent: 'codex', titleOnly: true }
})
).toMatchObject({ agent: null, source: null, coverage: 'uncovered' })
})
it('does not label a foreground-only compatibility answer as title-only', () => {
const identity = resolveCanonicalPaneAgentIdentity({
foregroundAgent: 'codex',
+32 -5
View File
@@ -121,6 +121,15 @@ const SOURCE_RANK: readonly PaneAgentEvidenceSource[] = PANE_AGENT_EVIDENCE_SOUR
/** Exported for the source/rank drift ratchet; the rank is the canonical source list itself. */
export const PANE_AGENT_SOURCE_RANK = SOURCE_RANK
/** Reject an unrecognised source instead of silently dropping it from the ranking loop. */
function sourceRankIndex(source: PaneAgentEvidenceSource): number {
const index = SOURCE_RANK.indexOf(source)
if (index === -1) {
throw new Error(`Unknown pane-agent evidence source: ${String(source)}`)
}
return index
}
/** Run keys only supersede evidence from the same authority; unknown authorities stay eligible. */
function isPaneAgentRunEligible(
run: PaneAgentRunKey | undefined,
@@ -139,14 +148,12 @@ export function resolveCanonicalPaneAgentEvidence<A extends string = TuiAgent>(
input: PaneAgentIdentityInput<A>
): PaneAgentIdentity<A> {
const superseded: PaneAgentEvidenceSource[] = []
const floor = input.minimumSource
? SOURCE_RANK.indexOf(input.minimumSource)
: Number.MAX_SAFE_INTEGER
const floor = input.minimumSource ? sourceRankIndex(input.minimumSource) : Number.MAX_SAFE_INTEGER
const eligible = input.evidence.filter((item) => {
if (item.source === 'sibling' && input.allowSibling !== true) {
return false
}
if (SOURCE_RANK.indexOf(item.source) > floor) {
if (sourceRankIndex(item.source) > floor) {
return false
}
if (isPaneAgentRunEligible(item.run, input.currentRun)) {
@@ -219,11 +226,31 @@ export function resolveCanonicalPaneAgentIdentity(
input.launchAgent ||
input.sleepingSessionAgent
)
const titleAgent = input.title ? collectAgentTitleEvidence(input.title).agent : null
const titleEvidence = input.title ? collectAgentTitleEvidence(input.title) : null
const titleAgent = titleEvidence?.agent ?? null
if (!hasAuthorityEvidence) {
if (input.uncoveredFallback) {
const agent = input.uncoveredFallback.agent
// A legacy title parser may have picked the first token from an ambiguous or
// free-text-only title. Do not let that compatibility value bypass the canonical
// ambiguity fence when the caller marks it as title-only evidence.
const rejectTitleFallback =
input.uncoveredFallback.titleOnly === true &&
((titleEvidence?.reason === 'free-text-only' &&
(titleEvidence.freeTextNames?.length ?? 0) > 1) ||
titleEvidence?.reason === 'conflicting-anchored-names' ||
titleEvidence?.reason === 'conflicting-vendor-markers')
if (rejectTitleFallback) {
return {
agent: null,
source: null,
coverage: 'uncovered',
titleOnly: false,
...(titleEvidence?.reason === 'free-text-only' ? {} : { ambiguousAt: 'title' as const }),
supersededSources: []
}
}
const titleOnly =
input.uncoveredFallback.titleOnly ?? (agent !== null && agent === titleAgent)
return {
@@ -162,6 +162,17 @@ describe('resolvePaneAgentIdentity', () => {
})
})
it('fails loudly when an evidence source is missing from the rank', () => {
expect(() =>
resolve([
{
source: 'future-source' as PaneAgentEvidence['source'],
agent: 'codex'
}
])
).toThrow('Unknown pane-agent evidence source')
})
describe('input order does not decide the answer', () => {
it('resolves the same regardless of how evidence is listed', () => {
const evidence: PaneAgentEvidence[] = [
@@ -3,6 +3,7 @@ import type { PaneAgentEvidenceSource } from './pane-agent-evidence-sources'
import type { TuiAgent } from './tui-agent'
export { PANE_AGENT_EVIDENCE_SOURCES } from './pane-agent-evidence-sources'
export type { PaneAgentEvidenceSource } from './pane-agent-evidence-sources'
/**
* One place that answers "which agent is in this pane".
+3 -1
View File
@@ -266,7 +266,9 @@ export function resolveTerminalTitleAgentType(title: string): TuiAgent | null {
const parsed = label ? (TITLE_LABEL_TO_AGENT[label] ?? null) : null
return resolveCanonicalPaneAgentIdentity({
title,
uncoveredFallback: { agent: parsed, titleOnly: parsed !== null }
// Preserve this public title-parser adapter's historical answer; pane identity
// consumers pass raw titles to the canonical resolver and enforce its fence.
uncoveredFallback: { agent: parsed, titleOnly: false }
}).agent
}