feat(agents): pane-identity canonical adapter, comparison telemetry, inventory ratchet (phase 1)

Output-neutral first slice of the pane-identity consolidation: a parallel
canonical resolver adapter with a per-pane coverage gate and process-proof
contract, old-vs-new disagreement telemetry at the host summary and tab-icon
surfaces, a byte-for-byte characterization of the frozen host adapter, and an
extended consumer-inventory ratchet (helper census, surface markers, rebind
audit) plus a recorded-title corpus gate. No surface changes what it displays
and no wire content changes.
This commit is contained in:
Merge Sim
2026-08-31 11:53:50 -07:00
parent 5ff1aa540e
commit f1511f325b
13 changed files with 1950 additions and 10 deletions
+33 -7
View File
@@ -404,7 +404,7 @@ import type {
TerminalPaneLayoutNode,
TerminalTab
} from '../../shared/terminal-tab-types'
import { resolvePublishedPaneAgentIdentity } from '../../shared/published-pane-agent-identity'
import { comparePublishedPaneAgentIdentity } from '../../shared/published-pane-agent-identity-comparison'
import type { TuiAgent } from '../../shared/tui-agent'
import type { BranchPrefixStrategy } from '../../shared/ui-chrome-types'
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
@@ -35987,12 +35987,20 @@ export class OrcaRuntimeService {
return null
}
/** Thin adapter so the summary builders stay declarative; the decision lives in `src/shared`. */
/** Thin adapter so the summary builders stay declarative; the decision lives in `src/shared`.
* Published output is the FROZEN resolver's, verbatim; the wrapper only counts where the
* canonical ladder would disagree (identity-ladder comparison window). */
private resolvePaneAgentIdentityField(
launchAgent: TuiAgent | null | undefined,
foregroundAgent: TuiAgent | null | undefined,
title: string | null,
paneKey: string | null
paneKey: string | null,
comparison: {
surface: 'terminal-summary' | 'pty-terminal-summary'
paneId: string
worktreeId: string
remote: boolean
}
): { agentIdentity?: TuiAgent } {
// Why hooks here: an agent the USER started from a shell has no launch record, and on WSL the
// Windows host reads its foreground process as `wsl.exe` rather than the agent inside the
@@ -36001,12 +36009,16 @@ export class OrcaRuntimeService {
? this.getHookAgentRowForPane(this.getAgentProviderSessionRowsForPaneFn?.(paneKey) ?? [])
: null
const hookAgent = isTuiAgent(hookRow?.agentType) ? hookRow.agentType : null
const agentIdentity = resolvePublishedPaneAgentIdentity({
const agentIdentity = comparePublishedPaneAgentIdentity({
hookAgent,
hookIsLive: hookRow?.agentIsLive,
launchAgent,
foregroundAgent,
title
title,
surface: comparison.surface,
paneId: comparison.paneId,
worktreeId: comparison.worktreeId,
hostScope: comparison.remote ? 'remote' : 'local'
})
return agentIdentity ? { agentIdentity } : {}
}
@@ -36059,7 +36071,15 @@ export class OrcaRuntimeService {
title,
// Why guarded: makePaneKey THROWS on a non-UUID leaf id, and an unguarded call here took
// down terminal.list for every pane in the list, not just the odd one.
isTerminalLeafId(leaf.leafId) ? makePaneKey(leaf.tabId, leaf.leafId) : null
isTerminalLeafId(leaf.leafId) ? makePaneKey(leaf.tabId, leaf.leafId) : null,
{
surface: 'terminal-summary',
paneId: `${leaf.tabId}:${leaf.leafId}`,
worktreeId: leaf.worktreeId,
remote:
leaf.ptyId !== null &&
(leaf.ptyId.startsWith('remote:') || parseAppSshPtyId(leaf.ptyId) !== null)
}
)
}
}
@@ -38232,7 +38252,13 @@ export class OrcaRuntimeService {
pty.launchAgent,
pty.foregroundAgent,
title,
pty.paneKey ?? null
pty.paneKey ?? null,
{
surface: 'pty-terminal-summary',
paneId: pty.paneKey ?? `pty:${pty.ptyId}`,
worktreeId: pty.worktreeId,
remote: pty.ptyId.startsWith('remote:') || parseAppSshPtyId(pty.ptyId) !== null
}
)
}
}
@@ -0,0 +1,120 @@
import { describe, expect, it } from 'vitest'
import { PaneAgentIdentityComparisonRecorder } from '../../../shared/pane-agent-identity-comparison'
import { recordTabAgentLadderComparison } from './tab-agent-identity-comparison'
import { resolveTabAgentFromSignals } from './use-tab-agent'
const GROK_ADVERSARIAL_TITLE = 'STA-4011 Linux Antigravity Commit Messages - grok'
describe('tab-icon comparison lane', () => {
it('counts the title-above-hook reclaim that the rendered ladder allows and the canonical one refuses', () => {
// The rendered tab ladder consults the title before the completed hook, so a reused-looking
// title flips the icon. The canonical ladder keeps the completed hook until a run-key
// supersession proves a reclaim. This exact disagreement is what the window must surface.
const signals = {
hasObservedAgentSignal: true,
isRemote: false,
title: GROK_ADVERSARIAL_TITLE,
hookAgent: null,
focusedCompletedHookAgent: 'claude' as const,
launchAgent: undefined
}
const rendered = resolveTabAgentFromSignals(signals)
expect(rendered).toBe('grok')
const recorder = new PaneAgentIdentityComparisonRecorder()
recordTabAgentLadderComparison(
{
tabId: 'tab-1',
worktreeId: 'wt-1',
isRemote: false,
title: signals.title,
hookAgent: signals.hookAgent,
focusedCompletedHookAgent: signals.focusedCompletedHookAgent,
launchAgent: null
},
rendered,
recorder
)
expect(recorder.snapshot()).toMatchObject({
comparisons: 1,
disagreements: 1,
reclaimShapes: 1
})
})
it('agreement on a hook-covered pane records a comparison and no disagreement', () => {
const recorder = new PaneAgentIdentityComparisonRecorder()
recordTabAgentLadderComparison(
{
tabId: 'tab-2',
worktreeId: 'wt-1',
isRemote: false,
title: 'anything at all',
hookAgent: 'claude',
launchAgent: null
},
'claude',
recorder
)
expect(recorder.snapshot()).toMatchObject({ comparisons: 1, disagreements: 0 })
})
it('an uncovered pane preserves the rendered result as the compatibility lane', () => {
const recorder = new PaneAgentIdentityComparisonRecorder()
recordTabAgentLadderComparison(
{
tabId: 'tab-3',
worktreeId: 'wt-1',
isRemote: false,
title: GROK_ADVERSARIAL_TITLE,
hookAgent: null,
launchAgent: null
},
'grok',
recorder
)
expect(recorder.snapshot()).toMatchObject({
comparisons: 1,
disagreements: 0,
uncovered: 1
})
})
it('dedupes repeated renders of unchanged signals', () => {
const recorder = new PaneAgentIdentityComparisonRecorder()
const args = {
tabId: 'tab-4',
worktreeId: 'wt-1',
isRemote: false,
title: 'plain shell',
hookAgent: null,
launchAgent: null
}
recordTabAgentLadderComparison(args, null, recorder)
recordTabAgentLadderComparison(args, null, recorder)
expect(recorder.snapshot().comparisons).toBe(1)
})
it('a remote pane is recorded with remote host scope, never resolved differently', () => {
const emitted: Record<string, unknown>[] = []
const recorder = new PaneAgentIdentityComparisonRecorder((_line, detail) => {
if (detail && 'surface' in detail) {
emitted.push(detail)
}
})
recordTabAgentLadderComparison(
{
tabId: 'tab-5',
worktreeId: 'wt-1',
isRemote: true,
title: GROK_ADVERSARIAL_TITLE,
hookAgent: null,
focusedCompletedHookAgent: 'claude',
launchAgent: null
},
'grok',
recorder
)
expect(emitted[0]).toMatchObject({ hostScope: 'remote' })
})
})
@@ -0,0 +1,139 @@
import { useEffect } from 'react'
import { collectAgentTitleEvidence } from '../../../shared/agent-title-evidence'
import { resolveCanonicalPaneAgentIdentity } from '../../../shared/pane-agent-identity-adapter'
import {
PaneAgentIdentityComparisonRecorder,
type PaneIdentityComparisonInput
} from '../../../shared/pane-agent-identity-comparison'
import type { TuiAgent } from '../../../shared/tui-agent'
/**
* Tab-icon lane of the identity-ladder comparison window. The tab ladder is the one users
* actually see and the one that ranks a parsed title above the launch record; this wrapper
* computes the canonical answer beside the rendered one and counts where they disagree. The
* rendered result is untouched — the caller passes it in and keeps displaying it.
*/
export type TabAgentLadderComparisonArgs = {
tabId: string
worktreeId?: string | null
isRemote: boolean
title: string
hookAgent: TuiAgent | null
siblingHookAgent?: TuiAgent | null
focusedCompletedHookAgent?: TuiAgent | null
siblingCompletedHookAgent?: TuiAgent | null
/** Renderer foreground hint — no host process proof exists, so the canonical lane treats it as
* weak evidence rather than the process rung. */
processAgent?: TuiAgent | null
sleepingSessionAgent?: TuiAgent | null
launchAgent?: TuiAgent | null
}
const defaultRecorder = new PaneAgentIdentityComparisonRecorder((line, sample) => {
console.info(`[pane-identity-compare] ${line}`, sample ?? {})
})
export function getTabAgentLadderComparisonRecorder(): PaneAgentIdentityComparisonRecorder {
return defaultRecorder
}
/** The tab's already-built ladder signals; a strict subset of `resolveTabAgentFromSignals` args. */
export type TabAgentLadderSignals = {
isRemote: boolean
title: string
hookAgent: TuiAgent | null
siblingHookAgent?: TuiAgent | null
focusedCompletedHookAgent?: TuiAgent | null
siblingCompletedHookAgent?: TuiAgent | null
processAgent?: TuiAgent | null
sleepingSessionAgent?: TuiAgent | null
launchAgent?: TuiAgent | null
}
/** Post-render on purpose: render stays pure, the rendered icon stays untouched, and the
* recorder's signature gate keeps repeat commits free. */
export function useTabAgentLadderComparison(
tabId: string,
worktreeId: string | null | undefined,
signals: TabAgentLadderSignals,
renderedAgent: TuiAgent | null
): void {
useEffect(() => {
recordTabAgentLadderComparison(
{
tabId,
worktreeId,
isRemote: signals.isRemote,
title: signals.title,
hookAgent: signals.hookAgent,
siblingHookAgent: signals.siblingHookAgent,
focusedCompletedHookAgent: signals.focusedCompletedHookAgent,
siblingCompletedHookAgent: signals.siblingCompletedHookAgent,
processAgent: signals.processAgent,
sleepingSessionAgent: signals.sleepingSessionAgent,
launchAgent: signals.launchAgent ?? null
},
renderedAgent
)
})
}
export function recordTabAgentLadderComparison(
args: TabAgentLadderComparisonArgs,
renderedAgent: TuiAgent | null,
recorder: PaneAgentIdentityComparisonRecorder = defaultRecorder
): void {
try {
const signature = [
args.hookAgent ?? '-',
args.siblingHookAgent ?? '-',
args.focusedCompletedHookAgent ?? '-',
args.siblingCompletedHookAgent ?? '-',
args.processAgent ?? '-',
args.sleepingSessionAgent ?? '-',
args.launchAgent ?? '-',
String(args.isRemote),
renderedAgent ?? '-',
args.title
].join('|')
if (!recorder.shouldCompare('tab-icon', args.tabId, signature)) {
return
}
const canonical = resolveCanonicalPaneAgentIdentity({
hookAgent: args.hookAgent,
hookIsLive: true,
completedHookAgent: args.focusedCompletedHookAgent,
launchAgent: args.launchAgent,
foregroundAgent: args.processAgent,
sleepingSessionAgent: args.sleepingSessionAgent,
siblingAgent: args.siblingHookAgent ?? args.siblingCompletedHookAgent,
allowSibling: true,
title: args.title,
uncoveredFallback: { agent: renderedAgent }
})
const titleAgent = args.title ? collectAgentTitleEvidence(args.title).agent : null
const input: PaneIdentityComparisonInput = {
surface: 'tab-icon',
paneId: args.tabId,
worktreeId: args.worktreeId,
oldAgent: renderedAgent,
newAgent: canonical.agent,
newSource: canonical.source,
coverage: canonical.coverage,
titleOnly: canonical.titleOnly,
// Run keys reach the tab ladder with a later wave; absent means absent, not stale.
runKeyComparability: 'absent',
hostScope: args.isRemote ? 'remote' : 'local',
ambiguous: canonical.ambiguousAt !== undefined,
reclaimShape: Boolean(
args.focusedCompletedHookAgent &&
titleAgent &&
titleAgent !== args.focusedCompletedHookAgent
)
}
recorder.record(input)
} catch {
// Comparison telemetry must never break the tab bar; a lost sample is recoverable.
}
}
+8 -2
View File
@@ -19,6 +19,7 @@ import {
import { resolveCompatibleAgentTypeForOwner } from '../../../shared/agent-title-owner'
import { isOpenCodeNativeTitle } from '../../../shared/opencode-terminal-title'
import { resolvePaneAgentOwner } from '../../../shared/pane-agent-owner'
import { useTabAgentLadderComparison } from './tab-agent-identity-comparison'
import type { TerminalTab } from '../../../shared/terminal-tab-types'
import type { TuiAgent } from '../../../shared/tui-agent'
@@ -337,7 +338,7 @@ export function useTabAgent(tab: TerminalTab): TuiAgent | null {
tab.title
])
return resolveTabAgentFromSignals({
const signals = {
hasObservedAgentSignal,
isRemote: isRemoteLike,
title: tab.title,
@@ -350,5 +351,10 @@ export function useTabAgent(tab: TerminalTab): TuiAgent | null {
processShellForeground,
sleepingSessionAgent,
launchAgent: tab.launchAgent
})
}
const renderedAgent = resolveTabAgentFromSignals(signals)
// Identity-ladder comparison window (post-render): counts canonical-vs-rendered disagreement;
// the rendered icon stays untouched.
useTabAgentLadderComparison(tab.id, tab.worktreeId, signals, renderedAgent)
return renderedAgent
}
@@ -0,0 +1,245 @@
import { describe, expect, it } from 'vitest'
import {
buildPaneAgentIdentityEvidenceWire,
isForegroundProcessProofFresh,
resolveCanonicalPaneAgentIdentity,
type ForegroundProcessProof
} from './pane-agent-identity-adapter'
const freshProof: ForegroundProcessProof = {
agent: 'codex',
processIncarnation: 'opaque-pid-token',
authorityId: 'main:test',
capturedAgeMs: 50,
validForMs: 5_000
}
describe('per-pane coverage gate', () => {
it('covers a pane from hook, launch, or sleeping-session evidence alone', () => {
expect(
resolveCanonicalPaneAgentIdentity({ hookAgent: 'claude', hookIsLive: true }).coverage
).toBe('covered')
expect(resolveCanonicalPaneAgentIdentity({ completedHookAgent: 'claude' }).coverage).toBe(
'covered'
)
expect(resolveCanonicalPaneAgentIdentity({ launchAgent: 'codex' }).coverage).toBe('covered')
expect(resolveCanonicalPaneAgentIdentity({ sleepingSessionAgent: 'gemini' }).coverage).toBe(
'covered'
)
})
it('never covers a pane from a title, a sibling, or a bare foreground name', () => {
expect(resolveCanonicalPaneAgentIdentity({ title: 'claude' }).coverage).toBe('uncovered')
expect(
resolveCanonicalPaneAgentIdentity({ siblingAgent: 'claude', allowSibling: true }).coverage
).toBe('uncovered')
expect(resolveCanonicalPaneAgentIdentity({ foregroundAgent: 'codex' }).coverage).toBe(
'uncovered'
)
})
it('is computed from evidence, never from a platform or remote flag', () => {
// The input deliberately has no platform/isRemote field to branch on; this pins that a
// hook-covered pane resolves identically regardless of any caller-side host knowledge.
const identity = resolveCanonicalPaneAgentIdentity({ hookAgent: 'claude', hookIsLive: true })
expect(identity).toMatchObject({ agent: 'claude', source: 'live-hook', coverage: 'covered' })
})
})
describe('process rung requires a host-stamped proof', () => {
it('rejects a stale or malformed proof and accepts a fresh one', () => {
expect(isForegroundProcessProofFresh(freshProof)).toBe(true)
expect(isForegroundProcessProofFresh({ ...freshProof, capturedAgeMs: 6_000 })).toBe(false)
expect(isForegroundProcessProofFresh({ ...freshProof, capturedAgeMs: -1 })).toBe(false)
expect(isForegroundProcessProofFresh({ ...freshProof, validForMs: 0 })).toBe(false)
expect(isForegroundProcessProofFresh({ ...freshProof, capturedAgeMs: Number.NaN })).toBe(false)
})
it('a bare foreground name cannot outrank launch; a proven process can', () => {
const unproven = resolveCanonicalPaneAgentIdentity({
launchAgent: 'claude',
foregroundAgent: 'codex'
})
expect(unproven).toMatchObject({ agent: 'claude', source: 'launch' })
const proven = resolveCanonicalPaneAgentIdentity({
launchAgent: 'claude',
foregroundAgent: 'codex',
processProof: freshProof
})
expect(proven).toMatchObject({ agent: 'codex', source: 'process', coverage: 'covered' })
})
it('an expired proof and a name-mismatched proof both drop the process rung', () => {
const expired = resolveCanonicalPaneAgentIdentity({
launchAgent: 'claude',
foregroundAgent: 'codex',
processProof: { ...freshProof, capturedAgeMs: 10_000 }
})
expect(expired).toMatchObject({ agent: 'claude', source: 'launch' })
const mismatched = resolveCanonicalPaneAgentIdentity({
launchAgent: 'claude',
foregroundAgent: 'gemini',
processProof: freshProof
})
expect(mismatched).toMatchObject({ agent: 'claude', source: 'launch' })
})
})
describe('uncovered compatibility lane', () => {
it('preserves the caller-provided legacy result verbatim', () => {
const identity = resolveCanonicalPaneAgentIdentity({
title: 'Fix the parser - grok',
uncoveredFallback: { agent: 'grok', titleOnly: true }
})
expect(identity).toMatchObject({
agent: 'grok',
source: 'title',
coverage: 'uncovered',
titleOnly: true
})
})
it('answers from title evidence marked title-only when no fallback is supplied', () => {
const identity = resolveCanonicalPaneAgentIdentity({
title: 'STA-4011 Linux Antigravity Commit Messages - grok'
})
expect(identity).toMatchObject({
agent: 'grok',
source: 'title',
coverage: 'uncovered',
titleOnly: true
})
})
it('a legacy null stays null rather than re-deriving from the title', () => {
const identity = resolveCanonicalPaneAgentIdentity({
title: 'anything - grok',
uncoveredFallback: { agent: null }
})
expect(identity).toMatchObject({ agent: null, source: null, coverage: 'uncovered' })
})
})
describe('canonical ladder inside the covered lane', () => {
it('keeps title last: a covered launch beats a parsed title', () => {
const identity = resolveCanonicalPaneAgentIdentity({
launchAgent: 'claude',
title: 'STA-4011 Linux Antigravity Commit Messages - grok'
})
expect(identity).toMatchObject({ agent: 'claude', source: 'launch', titleOnly: false })
})
it('sibling evidence needs the explicit tab-scope opt-in', () => {
const withoutOptIn = resolveCanonicalPaneAgentIdentity({
launchAgent: 'claude',
siblingAgent: 'codex'
})
expect(withoutOptIn.agent).toBe('claude')
const optedIn = resolveCanonicalPaneAgentIdentity({
hookAgent: 'claude',
hookIsLive: true,
siblingAgent: 'codex',
allowSibling: true
})
expect(optedIn).toMatchObject({ agent: 'claude', source: 'live-hook' })
})
it('surfaces ambiguity instead of picking by array order', () => {
const identity = resolveCanonicalPaneAgentIdentity({
hookAgent: 'claude',
hookIsLive: false,
completedHookAgent: 'codex'
})
expect(identity).toMatchObject({ agent: null, ambiguousAt: 'completed-hook' })
})
})
describe('reclaim-versus-stale-hook discriminator (run keys, not title text)', () => {
const run1 = { authorityId: 'main:a', incarnation: 1 }
const run2 = { authorityId: 'main:a', incarnation: 2 }
const otherAuthority = { authorityId: 'renderer:b', incarnation: 9 }
it('bug shape: hook and pane share the current run, so the completed hook wins over the title', () => {
const identity = resolveCanonicalPaneAgentIdentity({
completedHookAgent: 'claude',
completedHookRun: run1,
currentRun: run1,
title: 'STA-4011 Linux Antigravity Commit Messages - grok'
})
expect(identity).toMatchObject({ agent: 'claude', source: 'completed-hook' })
})
it('reclaim shape: a superseded hook is ineligible and the current title evidence answers', () => {
const identity = resolveCanonicalPaneAgentIdentity({
completedHookAgent: 'claude',
completedHookRun: run1,
currentRun: run2,
title: 'STA-4011 Linux Antigravity Commit Messages - grok'
})
expect(identity).toMatchObject({ agent: 'grok', source: 'title' })
expect(identity.supersededSources).toEqual(['completed-hook'])
})
it('cross-authority runs are incomparable, so the hook stays eligible', () => {
const identity = resolveCanonicalPaneAgentIdentity({
completedHookAgent: 'claude',
completedHookRun: otherAuthority,
currentRun: run2,
title: 'STA-4011 Linux Antigravity Commit Messages - grok'
})
expect(identity).toMatchObject({ agent: 'claude', source: 'completed-hook' })
})
it('an absent run key keeps evidence eligible (old peer), never guessed stale', () => {
const identity = resolveCanonicalPaneAgentIdentity({
completedHookAgent: 'claude',
currentRun: run2,
title: 'STA-4011 Linux Antigravity Commit Messages - grok'
})
expect(identity).toMatchObject({ agent: 'claude', source: 'completed-hook' })
})
})
describe('action floor', () => {
it("minimumSource: 'launch' refuses title and completed-hook answers outright", () => {
const identity = resolveCanonicalPaneAgentIdentity({
completedHookAgent: 'claude',
title: 'claude',
minimumSource: 'launch'
})
expect(identity).toMatchObject({ agent: null, source: null, coverage: 'covered' })
})
})
describe('wire evidence projection', () => {
it('publishes nothing for an absent identity — absence stays absence', () => {
expect(
buildPaneAgentIdentityEvidenceWire(resolveCanonicalPaneAgentIdentity({}))
).toBeUndefined()
})
it('marks the uncovered title-only route explicitly and carries the run key when known', () => {
const wire = buildPaneAgentIdentityEvidenceWire(
resolveCanonicalPaneAgentIdentity({ title: 'claude - claude' }),
{ authorityId: 'main:a', incarnation: 3 },
{ capturedAgeMs: 10, validForMs: 1_000 }
)
expect(wire).toMatchObject({
coverage: 'uncovered',
titleOnlyActionFallback: true,
authorityId: 'main:a',
incarnation: 3,
freshness: { capturedAgeMs: 10, validForMs: 1_000 }
})
})
it('a covered identity never carries the title-only action marker', () => {
const wire = buildPaneAgentIdentityEvidenceWire(
resolveCanonicalPaneAgentIdentity({ launchAgent: 'claude' })
)
expect(wire).toMatchObject({ source: 'launch', coverage: 'covered' })
expect(wire?.titleOnlyActionFallback).toBeUndefined()
})
})
+257
View File
@@ -0,0 +1,257 @@
import { collectAgentTitleEvidence } from './agent-title-evidence'
import {
resolvePaneAgentIdentity,
type PaneAgentEvidence,
type PaneAgentEvidenceSource,
type PaneAgentRunKey
} from './pane-agent-identity-resolver'
import type { TuiAgent } from './tui-agent'
/**
* The parallel adapter entry point around `resolvePaneAgentIdentity`.
*
* The frozen host adapter (`published-pane-agent-identity.ts`) stays untouched and keeps
* publishing exactly what it publishes today. This adapter computes the CANONICAL answer the
* migration will eventually ship — per-pane coverage, provenance sidecar, process-proof gating —
* so comparison telemetry can log where the two disagree on real sessions BEFORE any surface
* changes what it displays. Nothing user-visible reads this module's answer yet.
*/
/**
* Whether the execution authority proved at least one identity-bearing source for this pane.
* Computed from evidence presence, never from `platform`, `isRemote`, or OS: a remote pane with a
* host-stamped hook is covered; a local pane with only a title is uncovered.
*/
export type PaneAgentCoverage = 'covered' | 'uncovered'
/**
* Host-stamped proof that a recognized agent process is the pane's foreground process.
*
* A process NAME is not a PID-reuse-safe identity, so a bare foreground read never enters the
* covered process rung. `processIncarnation` is an opaque token the execution host derives from
* the selected PID plus start/creation time (or an equivalent platform-native identity); the raw
* tuple never crosses the renderer/remote wire. The host emits no proof when the start identity
* is unavailable or ambiguous, so that pane reads `uncovered` rather than guessed.
*/
export type ForegroundProcessProof = {
agent: TuiAgent
/** Opaque host-derived PID+start-time token. Compared for equality only, never decoded. */
processIncarnation: string
ptyIncarnationId?: string
/** The execution authority that stamped the proof (see agent-status-observation.ts). */
authorityId: string
/** Age on the AUTHORITY's clock at capture. Replicas decay from this plus `validForMs`,
* never by subtracting a host wall clock from a local `Date.now()`. */
capturedAgeMs: number
validForMs: number
}
/**
* Positive evidence that a pane/process was REPLACED, required before any consumer may advance a
* pane incarnation. A retired-pane `restart` disposition, an accepted send, an ordinary provider
* turn boundary, a title change, a transport loss, or a renderer-only foreground change is never
* one of these. Defined here so the rebind-gate wave has a contract to be correct against; no
* sequencer call site consumes it yet.
*/
export type PaneReplacementProof =
| { kind: 'accepted-launch'; launchToken: string; ptyIncarnationId: string }
| { kind: 'process-replacement'; processIncarnation: string; authorityId: string }
| { kind: 'provider-session-attach'; providerSessionId: string }
/**
* The optional wire object a host will publish alongside `agentIdentity` after capability
* negotiation (host-publisher wave, not now). All fields bounded and JSON-safe; old peers ignore
* it. Never inferred from the bare `agentIdentity` string.
*/
export type PaneAgentIdentityEvidenceWire = {
source: PaneAgentEvidenceSource
coverage: PaneAgentCoverage
authorityId?: string
incarnation?: number
freshness?: { capturedAgeMs: number; validForMs: number }
/** Marks the scoped host-published title-only best-effort route (hand-started WSL panes).
* Counted separately, `unverifiable` for liveness, and never relabeled as covered proof. */
titleOnlyActionFallback?: true
}
export type CanonicalPaneAgentIdentityInput = {
hookAgent?: TuiAgent | null
hookIsLive?: boolean
hookRun?: PaneAgentRunKey
/** A distinct completed-hook signal for callers that hold live and completed rows separately
* (the tab ladder does); `hookAgent` + `hookIsLive: false` remains the single-slot spelling. */
completedHookAgent?: TuiAgent | null
completedHookRun?: PaneAgentRunKey
launchAgent?: TuiAgent | null
launchRun?: PaneAgentRunKey
/**
* Foreground process NAME as currently read. Without a fresh `processProof` this is a weak
* hint: it neither enters the covered process rung nor makes the pane covered.
*/
foregroundAgent?: TuiAgent | null
processProof?: ForegroundProcessProof | null
sleepingSessionAgent?: TuiAgent | null
sleepingRun?: PaneAgentRunKey
/** Tab-level display fallback only; ignored unless `allowSibling` opts in. */
siblingAgent?: TuiAgent | null
allowSibling?: boolean
title?: string | null
currentRun?: PaneAgentRunKey
minimumSource?: PaneAgentEvidenceSource
/**
* The caller's CURRENT ladder result, preserved verbatim while the pane is uncovered. The
* uncovered lane is a temporary compatibility lane, not a new host-specific ranking; absent a
* fallback, an uncovered pane answers from title evidence alone, marked title-only.
*/
uncoveredFallback?: { agent: TuiAgent | null; titleOnly?: boolean }
}
export type CanonicalPaneAgentIdentity = {
agent: TuiAgent | null
source: PaneAgentEvidenceSource | null
coverage: PaneAgentCoverage
/** True when the answer was derived from a parsed title (the uncovered/title-only marking). */
titleOnly: boolean
ambiguousAt?: PaneAgentEvidenceSource
supersededSources: readonly PaneAgentEvidenceSource[]
}
/** Freshness is judged on the authority's own clock: age at capture against its TTL. */
export function isForegroundProcessProofFresh(proof: ForegroundProcessProof): boolean {
return (
Number.isFinite(proof.capturedAgeMs) &&
Number.isFinite(proof.validForMs) &&
proof.capturedAgeMs >= 0 &&
proof.validForMs > 0 &&
proof.capturedAgeMs <= proof.validForMs
)
}
/** A proof only carries identity for the agent it names; a name mismatch is no proof at all. */
function processEvidenceFromProof(
input: CanonicalPaneAgentIdentityInput
): PaneAgentEvidence<TuiAgent> | null {
const proof = input.processProof
if (!proof || !isForegroundProcessProofFresh(proof)) {
return null
}
if (input.foregroundAgent && input.foregroundAgent !== proof.agent) {
return null
}
return { source: 'process', agent: proof.agent }
}
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.
const covered = Boolean(
input.hookAgent ||
input.completedHookAgent ||
processEvidence ||
input.launchAgent ||
input.sleepingSessionAgent
)
const titleAgent = input.title ? collectAgentTitleEvidence(input.title).agent : null
if (!covered) {
if (input.uncoveredFallback) {
const agent = input.uncoveredFallback.agent
const titleOnly =
input.uncoveredFallback.titleOnly ?? (agent !== null && agent === titleAgent)
return {
agent,
source: agent === null ? null : titleOnly ? 'title' : null,
coverage: 'uncovered',
titleOnly,
supersededSources: []
}
}
return {
agent: titleAgent,
source: titleAgent ? 'title' : null,
coverage: 'uncovered',
titleOnly: titleAgent !== null,
supersededSources: []
}
}
const resolved = resolvePaneAgentIdentity<TuiAgent>({
evidence: [
...(input.hookAgent
? [
{
source: input.hookIsLive ? ('live-hook' as const) : ('completed-hook' as const),
agent: input.hookAgent,
...(input.hookRun ? { run: input.hookRun } : {})
}
]
: []),
...(input.completedHookAgent
? [
{
source: 'completed-hook' as const,
agent: input.completedHookAgent,
...(input.completedHookRun ? { run: input.completedHookRun } : {})
}
]
: []),
...(processEvidence ? [processEvidence] : []),
...(input.launchAgent
? [
{
source: 'launch' as const,
agent: input.launchAgent,
...(input.launchRun ? { run: input.launchRun } : {})
}
]
: []),
...(input.sleepingSessionAgent
? [
{
source: 'sleeping-session' as const,
agent: input.sleepingSessionAgent,
...(input.sleepingRun ? { run: input.sleepingRun } : {})
}
]
: []),
...(input.siblingAgent ? [{ source: 'sibling' as const, agent: input.siblingAgent }] : []),
...(titleAgent ? [{ source: 'title' as const, agent: titleAgent }] : [])
],
currentRun: input.currentRun,
minimumSource: input.minimumSource,
allowSibling: input.allowSibling
})
return {
agent: resolved.agent,
source: resolved.source,
coverage: 'covered',
titleOnly: resolved.source === 'title',
...(resolved.ambiguousAt ? { ambiguousAt: resolved.ambiguousAt } : {}),
supersededSources: resolved.supersededSources
}
}
/** Projects the host-local sidecar onto the optional wire shape. Returns undefined when there is
* nothing to publish — absence stays absence, and a bare `agentIdentity` with no sidecar is
* never treated as covered proof by any consumer. */
export function buildPaneAgentIdentityEvidenceWire(
identity: CanonicalPaneAgentIdentity,
run?: PaneAgentRunKey,
freshness?: { capturedAgeMs: number; validForMs: number }
): PaneAgentIdentityEvidenceWire | undefined {
if (identity.agent === null || identity.source === null) {
return undefined
}
return {
source: identity.source,
coverage: identity.coverage,
...(run ? { authorityId: run.authorityId, incarnation: run.incarnation } : {}),
...(freshness ? { freshness } : {}),
...(identity.coverage === 'uncovered' && identity.titleOnly
? { titleOnlyActionFallback: true as const }
: {})
}
}
@@ -0,0 +1,116 @@
import { describe, expect, it } from 'vitest'
import {
PaneAgentIdentityComparisonRecorder,
type PaneIdentityComparisonInput
} from './pane-agent-identity-comparison'
function sample(overrides: Partial<PaneIdentityComparisonInput> = {}): PaneIdentityComparisonInput {
return {
surface: 'terminal-summary',
paneId: 'tab-1:leaf-1',
worktreeId: 'wt-1',
oldAgent: 'claude',
newAgent: 'claude',
newSource: 'launch',
coverage: 'covered',
titleOnly: false,
runKeyComparability: 'absent',
hostScope: 'local',
ambiguous: false,
reclaimShape: false,
...overrides
}
}
describe('comparison counters', () => {
it('counts disagreements and both absence-transition directions separately', () => {
const recorder = new PaneAgentIdentityComparisonRecorder()
recorder.record(sample())
recorder.record(sample({ oldAgent: 'codex', newAgent: 'claude' }))
recorder.record(sample({ oldAgent: null, newAgent: 'claude' }))
recorder.record(sample({ oldAgent: 'claude', newAgent: null, newSource: null }))
expect(recorder.snapshot()).toMatchObject({
comparisons: 4,
disagreements: 3,
oldAbsentNewPresent: 1,
oldPresentNewAbsent: 1
})
})
it('counts ambiguity, reclaim shapes, title-only, and uncovered lanes', () => {
const recorder = new PaneAgentIdentityComparisonRecorder()
recorder.record(sample({ ambiguous: true }))
recorder.record(sample({ reclaimShape: true }))
recorder.record(sample({ titleOnly: true, coverage: 'uncovered' }))
expect(recorder.snapshot()).toMatchObject({
ambiguous: 1,
reclaimShapes: 1,
titleOnly: 1,
uncovered: 1
})
})
})
describe('sampling and bounds', () => {
it('skips consecutive identical input signatures per pane, and resumes on change', () => {
const recorder = new PaneAgentIdentityComparisonRecorder()
expect(recorder.shouldCompare('tab-icon', 'tab-1', 'sig-a')).toBe(true)
expect(recorder.shouldCompare('tab-icon', 'tab-1', 'sig-a')).toBe(false)
expect(recorder.shouldCompare('tab-icon', 'tab-2', 'sig-a')).toBe(true)
expect(recorder.shouldCompare('tab-icon', 'tab-1', 'sig-b')).toBe(true)
expect(recorder.shouldCompare('tab-icon', 'tab-1', 'sig-a')).toBe(true)
})
it('emits one detail record per distinct disagreement shape, hard-capped', () => {
const emitted: Record<string, unknown>[] = []
const recorder = new PaneAgentIdentityComparisonRecorder((_line, detail) => {
if (detail && 'surface' in detail) {
emitted.push(detail)
}
})
recorder.record(sample({ oldAgent: 'codex' }))
recorder.record(sample({ oldAgent: 'codex' }))
expect(emitted).toHaveLength(1)
for (let i = 0; i < 100; i += 1) {
recorder.record(sample({ oldAgent: `agent-${i}` }))
}
expect(emitted.length).toBeLessThanOrEqual(41)
expect(recorder.snapshot().disagreements).toBe(102)
})
})
describe('privacy contract', () => {
it('emitted records pseudonymize ids and never contain a title-like payload', () => {
const rawTitle = 'SECRET /Users/someone/private/path — do not leak'
const emitted: { line: string; detail?: Record<string, unknown> }[] = []
const recorder = new PaneAgentIdentityComparisonRecorder((line, detail) => {
emitted.push({ line, detail })
})
recorder.record(
sample({ paneId: 'raw-pane-id', worktreeId: 'raw-worktree-id', oldAgent: 'codex' })
)
expect(emitted.length).toBeGreaterThan(0)
for (const { line, detail } of emitted) {
const serialized = `${line} ${JSON.stringify(detail ?? {})}`
expect(serialized).not.toContain(rawTitle)
expect(serialized).not.toContain('raw-pane-id')
expect(serialized).not.toContain('raw-worktree-id')
expect(detail).not.toHaveProperty('title')
}
})
it('two recorders pseudonymize the same id differently (per-process salt)', () => {
const captured: string[] = []
const capture = (_line: string, detail?: Record<string, unknown>) => {
if (detail && typeof detail.pane === 'string') {
captured.push(detail.pane)
}
}
const a = new PaneAgentIdentityComparisonRecorder(capture)
const b = new PaneAgentIdentityComparisonRecorder(capture)
a.record(sample({ oldAgent: 'codex' }))
b.record(sample({ oldAgent: 'codex' }))
expect(captured).toHaveLength(2)
expect(captured[0]).not.toBe(captured[1])
})
})
@@ -0,0 +1,176 @@
import type { PaneAgentCoverage } from './pane-agent-identity-adapter'
import type { PaneAgentEvidenceSource } from './pane-agent-identity-resolver'
/**
* Comparison telemetry for the identity-ladder migration: where do the old and canonical ladders
* DISAGREE on real sessions? Recorded BEFORE any surface changes what it displays, so each later
* flip is a measured decision instead of a guess.
*
* Privacy contract: emitted records carry pseudonymous salted ids, agent enum values, sources,
* coverage, and counters — never raw titles, prompts, commands, file paths, or tokens.
*/
export type PaneIdentityComparisonSurface = 'terminal-summary' | 'pty-terminal-summary' | 'tab-icon'
export type PaneIdentityRunKeyComparability = 'comparable' | 'incomparable' | 'absent'
export type PaneIdentityHostScope = 'local' | 'remote' | 'unknown'
export type PaneIdentityComparisonInput = {
surface: PaneIdentityComparisonSurface
/** Raw pane/tab identifier; pseudonymized before it reaches any emitted record. */
paneId: string
worktreeId?: string | null
oldAgent: string | null
newAgent: string | null
newSource: PaneAgentEvidenceSource | null
coverage: PaneAgentCoverage
titleOnly: boolean
runKeyComparability: PaneIdentityRunKeyComparability
hostScope: PaneIdentityHostScope
/** The canonical resolver saw equally-ranked conflicting evidence. */
ambiguous: boolean
/** The bug-versus-reclaim input shape: a completed hook naming A beside a title naming B. */
reclaimShape: boolean
}
export type PaneIdentityComparisonCounters = {
comparisons: number
disagreements: number
ambiguous: number
reclaimShapes: number
/** Flipping would turn a published absence into a presence — `groups.ts` reads absence as NO. */
oldAbsentNewPresent: number
oldPresentNewAbsent: number
titleOnly: number
uncovered: number
}
const MAX_PANE_SIGNATURES = 2048
const MAX_DISAGREEMENT_KEYS = 40
/** Counter snapshots go out on a log scale so a busy session cannot flood the sink. */
const SNAPSHOT_AT = [100, 1_000, 10_000, 100_000, 1_000_000]
function pseudonymize(salt: string, value: string): string {
// djb2: stable within one process, meaningless outside it. Pseudonymity, not secrecy — the raw
// id never leaves the process either way.
let hash = 5381
const input = `${salt}:${value}`
for (let i = 0; i < input.length; i += 1) {
hash = ((hash << 5) + hash + input.charCodeAt(i)) | 0
}
return (hash >>> 0).toString(16).padStart(8, '0')
}
export class PaneAgentIdentityComparisonRecorder {
private readonly counters: PaneIdentityComparisonCounters = {
comparisons: 0,
disagreements: 0,
ambiguous: 0,
reclaimShapes: 0,
oldAbsentNewPresent: 0,
oldPresentNewAbsent: 0,
titleOnly: 0,
uncovered: 0
}
private readonly lastSignatureByPane = new Map<string, string>()
private readonly emittedDisagreementKeys = new Set<string>()
private readonly salt: string
constructor(
private readonly emit: (line: string, sample?: Record<string, unknown>) => void = () => {}
) {
this.salt = globalThis.crypto.randomUUID()
}
/**
* Consecutive-duplicate gate, cheap enough for a render/summary path: callers build a signature
* from the ladder INPUTS and skip the (costlier) canonical resolution when nothing changed.
*/
shouldCompare(
surface: PaneIdentityComparisonSurface,
paneId: string,
signature: string
): boolean {
const key = `${surface}|${paneId}`
if (this.lastSignatureByPane.get(key) === signature) {
return false
}
this.lastSignatureByPane.set(key, signature)
while (this.lastSignatureByPane.size > MAX_PANE_SIGNATURES) {
const oldest = this.lastSignatureByPane.keys().next().value
if (oldest === undefined) {
break
}
this.lastSignatureByPane.delete(oldest)
}
return true
}
record(input: PaneIdentityComparisonInput): void {
this.counters.comparisons += 1
if (input.ambiguous) {
this.counters.ambiguous += 1
}
if (input.reclaimShape) {
this.counters.reclaimShapes += 1
}
if (input.titleOnly) {
this.counters.titleOnly += 1
}
if (input.coverage === 'uncovered') {
this.counters.uncovered += 1
}
const disagrees = input.oldAgent !== input.newAgent
if (disagrees) {
this.counters.disagreements += 1
if (input.oldAgent === null) {
this.counters.oldAbsentNewPresent += 1
}
if (input.newAgent === null) {
this.counters.oldPresentNewAbsent += 1
}
this.emitDisagreement(input)
}
if (SNAPSHOT_AT.includes(this.counters.comparisons)) {
this.emit('pane-identity-compare counters', { ...this.counters })
}
}
snapshot(): PaneIdentityComparisonCounters {
return { ...this.counters }
}
private emitDisagreement(input: PaneIdentityComparisonInput): void {
const key = [
input.surface,
input.oldAgent ?? '-',
input.newAgent ?? '-',
input.newSource ?? '-',
input.coverage,
input.hostScope
].join('|')
// One detail record per distinct disagreement shape, hard-capped; repeats only count.
if (
this.emittedDisagreementKeys.has(key) ||
this.emittedDisagreementKeys.size >= MAX_DISAGREEMENT_KEYS
) {
return
}
this.emittedDisagreementKeys.add(key)
this.emit('pane-identity-compare disagreement', {
surface: input.surface,
pane: pseudonymize(this.salt, input.paneId),
...(input.worktreeId ? { worktree: pseudonymize(this.salt, input.worktreeId) } : {}),
oldAgent: input.oldAgent,
newAgent: input.newAgent,
newSource: input.newSource,
coverage: input.coverage,
titleOnly: input.titleOnly,
runKeyComparability: input.runKeyComparability,
hostScope: input.hostScope,
ambiguous: input.ambiguous,
reclaimShape: input.reclaimShape
})
}
}
@@ -18,7 +18,16 @@ const HELPERS = [
'resolveExplicitTerminalTitleAgentType',
'resolveCommittedTitleAgentType',
'resolvePaneAgentOwner',
'resolveCompatibleAgentTypeForOwner'
'resolveCompatibleAgentTypeForOwner',
'classifyTitleActivity',
'detectAgentStatusFromTitle',
'resolveAgentTypeFromTerminalTitle',
'resolvePaneAgentIdentity',
'resolveCanonicalPaneAgentIdentity',
'resolvePublishedPaneAgentIdentity',
'comparePublishedPaneAgentIdentity',
'recordTabAgentLadderComparison',
'useTabAgentLadderComparison'
] as const
const TEST_SUPPORT_PATHS = new Set([
@@ -242,6 +251,157 @@ const INVENTORY: readonly InventoryGroup[] = [
['src/renderer/src/components/terminal-pane/pty-connection/direct-ssh-retry-status.ts', 2],
['src/renderer/src/components/terminal-pane/pty-connection/title-spawn-bell.ts', 2]
]
},
{
helper: 'classifyTitleActivity',
classification: 'identity-consumer',
paths: [
['src/renderer/src/components/sidebar/smart-attention.ts', 3],
['src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts', 2],
['src/renderer/src/components/status-bar/workspace-space-presentation.ts', 3],
['src/renderer/src/lib/active-agent-note-target.ts', 2],
['src/renderer/src/lib/worktree-status.ts', 3],
['src/renderer/src/store/slices/terminal-helpers.ts', 2]
]
},
{
helper: 'classifyTitleActivity',
classification: 'action-consumer',
paths: [
['src/renderer/src/components/terminal-pane/cache-timer-seeding.ts', 2],
['src/renderer/src/lib/agent-ready-wait.ts', 2],
['src/renderer/src/store/terminals/terminal-ephemeral-state.ts', 2]
]
},
{
helper: 'classifyTitleActivity',
classification: 'activity-only',
paths: [
['src/renderer/src/store/slices/workspace-cleanup-local-evidence.ts', 3],
['src/renderer/src/store/terminals/terminal-tab-presentation.ts', 4]
]
},
{
helper: 'classifyTitleActivity',
classification: 'evidence-producer',
paths: [
['src/renderer/src/lib/agent-send-title-status.ts', 2],
['src/renderer/src/lib/agent-status-terminal-title.ts', 2]
]
},
{
helper: 'classifyTitleActivity',
classification: 'parser-implementation',
paths: [
['src/renderer/src/lib/agent-status.ts', 5],
'src/renderer/src/lib/pane-agent-evidence.ts'
]
},
{
helper: 'detectAgentStatusFromTitle',
classification: 'evidence-producer',
paths: [
['src/main/runtime/orca-runtime.ts', 12],
['src/renderer/src/components/terminal-pane/agent-completion-title-observer.ts', 2],
['src/renderer/src/components/terminal-pane/pty-connection/shell-command-inference.ts', 4],
['src/renderer/src/components/terminal-pane/pty-output-title-observer.ts', 2],
['src/shared/terminal-output-side-effects.ts', 3]
]
},
{
helper: 'detectAgentStatusFromTitle',
classification: 'action-consumer',
paths: [
['src/renderer/src/components/terminal-pane/pty-connection/agent-task-complete-notify.ts', 2],
[
'src/renderer/src/components/terminal-pane/pty-connection/command-inferred-pane-agent.ts',
3
],
['src/renderer/src/components/terminal-pane/pty-connection/interrupt-input-intent.ts', 3]
]
},
{
helper: 'detectAgentStatusFromTitle',
classification: 'parser-implementation',
paths: [
['src/renderer/src/components/terminal-pane/title-agent-identity.ts', 2],
'src/renderer/src/lib/agent-status.ts',
['src/renderer/src/lib/pane-agent-evidence.ts', 3],
['src/shared/agent-decorative-title-signature.ts', 2],
'src/shared/agent-detection.ts',
['src/shared/agent-title-owner.ts', 2],
['src/shared/agent-title-status.ts', 6]
]
},
{
helper: 'resolveAgentTypeFromTerminalTitle',
classification: 'identity-consumer',
paths: [
['src/renderer/src/components/sidebar/worktree-agent-row-type.ts', 2],
'src/renderer/src/components/sidebar/worktree-title-derived-agent-rows.ts',
['src/renderer/src/lib/worktree-status.ts', 2]
]
},
{
helper: 'resolvePaneAgentIdentity',
classification: 'parser-implementation',
paths: [
['src/shared/pane-agent-identity-adapter.ts', 2],
'src/shared/pane-agent-identity-resolver.ts'
]
},
{
helper: 'resolvePaneAgentIdentity',
classification: 'identity-consumer',
paths: [['src/shared/published-pane-agent-identity.ts', 2]]
},
{
helper: 'resolveCanonicalPaneAgentIdentity',
classification: 'parser-implementation',
paths: ['src/shared/pane-agent-identity-adapter.ts']
},
{
helper: 'resolveCanonicalPaneAgentIdentity',
classification: 'identity-consumer',
paths: [
['src/renderer/src/lib/tab-agent-identity-comparison.ts', 2],
['src/shared/published-pane-agent-identity-comparison.ts', 2]
]
},
{
helper: 'resolvePublishedPaneAgentIdentity',
classification: 'parser-implementation',
paths: ['src/shared/published-pane-agent-identity.ts']
},
{
helper: 'resolvePublishedPaneAgentIdentity',
classification: 'identity-consumer',
paths: [['src/shared/published-pane-agent-identity-comparison.ts', 2]]
},
{
helper: 'comparePublishedPaneAgentIdentity',
classification: 'parser-implementation',
paths: ['src/shared/published-pane-agent-identity-comparison.ts']
},
{
helper: 'comparePublishedPaneAgentIdentity',
classification: 'identity-consumer',
paths: [['src/main/runtime/orca-runtime.ts', 2]]
},
{
helper: 'recordTabAgentLadderComparison',
classification: 'parser-implementation',
paths: [['src/renderer/src/lib/tab-agent-identity-comparison.ts', 2]]
},
{
helper: 'useTabAgentLadderComparison',
classification: 'parser-implementation',
paths: ['src/renderer/src/lib/tab-agent-identity-comparison.ts']
},
{
helper: 'useTabAgentLadderComparison',
classification: 'identity-consumer',
paths: [['src/renderer/src/lib/use-tab-agent.ts', 2]]
}
]
@@ -0,0 +1,270 @@
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { glob } from 'tinyglobby'
import { isTestFile, stripComments } from './source-scan/source-tree-scan'
/**
* Surface half of the identity inventory ratchet: every consumer decision point from the closed
* 65-row inventory (rows 32–65 — the direct title/native-chat selectors, tab projections, mobile
* sync graph, lifecycle selectors, status/OSC ingress, worktree status, attention, and
* title-reset paths) is pinned to a marker symbol in its file. The helper-name census
* (`pane-agent-identity-inventory.test.ts`) is necessary but not sufficient — these files reach
* identity through direct reads a name census cannot see. Moving or renaming a marker means the
* inventory row must be re-classified, deliberately, before review.
*/
type SurfaceRow = {
/** Row number in the closed consumer inventory. */
row: number
path: string
marker: string
}
const SURFACE_ROWS: readonly SurfaceRow[] = [
{
row: 32,
path: 'src/renderer/src/components/terminal-pane/native-chat-leaf-title-agent.ts',
marker: 'resolveNativeChatLeafTitleAgent'
},
{
row: 32,
path: 'src/renderer/src/components/terminal-pane/TerminalPane.tsx',
marker: 'resolveNativeChatLeafTitleAgent'
},
{
row: 33,
path: 'src/renderer/src/components/terminal-pane/pty-connection/pane-agent-identity.ts',
marker: 'installPaneAgentIdentity'
},
{ row: 34, path: 'src/main/runtime/orchestration/groups.ts', marker: 'terminalIsAgent' },
{
row: 35,
path: 'src/renderer/src/lib/active-agent-note-target.ts',
marker: 'getActiveTerminalNoteTarget'
},
{
row: 36,
path: 'src/renderer/src/components/terminal-pane/terminal-agent-paste-bracketing.ts',
marker: 'resolveProtectedMultilinePasteOptionsForPane'
},
{
row: 37,
path: 'src/renderer/src/components/terminal-pane/command-code-output-ownership.ts',
marker: 'canCommandCodeOutputOwnPane'
},
{ row: 38, path: 'src/renderer/src/lib/agent-ready-wait.ts', marker: 'waitForAgentReady' },
{
row: 39,
path: 'src/renderer/src/lib/agent-paste-draft.ts',
marker: 'getSettingsForAgentTabRuntimeOwner'
},
{
row: 40,
path: 'src/renderer/src/lib/agent-followup-delivery.ts',
marker: 'sendFollowupPromptWhenAgentReady'
},
{
row: 41,
path: 'src/renderer/src/lib/codex-session-restart.ts',
marker: 'markLiveCodexSessionsForRestart'
},
{
row: 41,
path: 'src/renderer/src/lib/codex-pane-restart-eligibility.ts',
marker: 'isCodexForegroundProcess'
},
{
row: 42,
path: 'src/renderer/src/components/native-chat/native-chat-availability.ts',
marker: 'canToggleNativeChat'
},
{
row: 43,
path: 'src/renderer/src/components/native-chat/native-chat-pane-resolution.ts',
marker: 'resolveNativeChatSession'
},
{
row: 44,
path: 'src/renderer/src/components/terminal-pane/terminal-agent-session-continuation.ts',
marker: 'canContinueAgentSessionInNewSession'
},
{
row: 45,
path: 'src/renderer/src/components/terminal-pane/terminal-agent-session-fork.ts',
marker: 'prepareAgentSessionForkFromPane'
},
{
row: 46,
path: 'src/renderer/src/components/terminal-pane/agent-interrupt-inference.ts',
marker: 'isPlainEscapeKeyEvent'
},
{
row: 46,
path: 'src/renderer/src/components/terminal-pane/agent-question-answered-inference.ts',
marker: 'inferQuestionAnsweredFromCurrentStatus'
},
{
row: 47,
path: 'src/renderer/src/components/terminal-pane/terminal-keyboard-protocol-pane-agent.ts',
marker: 'resolvePaneKeyboardProtocolAgent'
},
{
row: 47,
path: 'src/renderer/src/components/terminal-pane/use-terminal-pane-lifecycle.ts',
marker: 'resolvePaneKeyboardProtocolAgent'
},
{
row: 48,
path: 'src/renderer/src/components/tab-bar/tab-agent-types-by-tab-id.ts',
marker: 'selectTabAgentTypesByTabId'
},
{
row: 49,
path: 'src/renderer/src/components/terminal-pane/terminal-tab-agent-type-index.ts',
marker: 'createTerminalTabAgentTypeSelector'
},
{
row: 50,
path: 'src/renderer/src/lib/tab-agent-status-index.ts',
marker: 'selectLiveTabAgentPanes'
},
{
row: 51,
path: 'src/renderer/src/components/tab-bar/terminal-tab-activity-status.ts',
marker: 'resolveTerminalTabActivityStatus'
},
{
row: 52,
path: 'src/renderer/src/lib/workspace-tab-agent-metadata.ts',
marker: 'maxAgentActivityAt'
},
{
row: 52,
path: 'src/renderer/src/lib/workspace-tab-palette-entry-builder.ts',
marker: 'buildSearchableWorkspaceTabEntries'
},
{
row: 53,
path: 'src/renderer/src/lib/running-agent-targets.ts',
marker: 'deriveRunningAgentSendTargets'
},
{
row: 54,
path: 'src/renderer/src/runtime/sync-runtime-graph.ts',
marker: 'buildMobileSessionTabSnapshots'
},
{
row: 55,
path: 'src/renderer/src/lib/agent-hibernation-pane-eligibility.ts',
marker: 'toRuntimePtyId'
},
{
row: 56,
path: 'src/renderer/src/lib/resume-sleeping-agent-session.ts',
marker: 'resumeSleepingAgentSessionsForWorktree'
},
{
row: 57,
path: 'src/renderer/src/lib/automation-session-reuse.ts',
marker: 'findReusableAutomationSession'
},
{
row: 58,
path: 'src/renderer/src/components/terminal-pane/pty-connection/cold-restore-resume-startup.ts',
marker: 'bindBuildColdRestoreAgentResumeStartup'
},
{ row: 59, path: 'src/main/agent-hooks/server.ts', marker: 'recordCurrentAuthorityObservation' },
{ row: 59, path: 'src/main/runtime/orca-runtime.ts', marker: 'resolvePaneAgentIdentityField' },
{
row: 59,
path: 'src/renderer/src/hooks/ipc-events/agent-status-event-applicator.ts',
marker: 'createAgentStatusEventApplicator'
},
{
row: 59,
path: 'src/renderer/src/store/slices/agent-status.ts',
marker: 'transferAgentPaneAuthority'
},
{
row: 59,
path: 'src/renderer/src/store/slices/pane-foreground-agent.ts',
marker: 'createPaneForegroundAgentSlice'
},
{
row: 59,
path: 'src/renderer/src/hooks/ipc-events/agent-status-routing.ts',
marker: 'isAgentStatusForRecentlyClosedTab'
},
{
row: 60,
path: 'src/renderer/src/components/terminal-pane/pty-connection/title-spawn-bell.ts',
marker: 'installTitleSpawnBell'
},
{ row: 61, path: 'src/renderer/src/lib/worktree-status.ts', marker: 'getWorktreeStatus' },
{ row: 62, path: 'src/main/runtime/orca-runtime.ts', marker: 'getLeafWorktreeStatus' },
{
row: 63,
path: 'src/renderer/src/components/sidebar/smart-attention.ts',
marker: 'buildAttentionByWorktree'
},
{
row: 64,
path: 'src/renderer/src/components/status-bar/workspace-space-presentation.ts',
marker: 'countWorkspaceSpaceActiveAgents'
},
{ row: 65, path: 'src/renderer/src/store/slices/terminal-helpers.ts', marker: 'getResetTitle' },
{
row: 6,
path: 'src/renderer/src/runtime/web-session-tabs-sync.ts',
marker: 'applyWebSessionTabs'
}
]
describe('pane agent identity surface inventory (rows 6, 32–65)', () => {
it('every pinned surface still carries its marker symbol', () => {
for (const row of SURFACE_ROWS) {
const source = stripComments(readFileSync(join(process.cwd(), row.path), 'utf8'))
expect({ row: row.row, path: row.path, hasMarker: source.includes(row.marker) }).toEqual({
row: row.row,
path: row.path,
hasMarker: true
})
}
})
})
/**
* Identity-observation rebind audit. Advancing a pane incarnation without a positive replacement
* proof is how a legitimate reclaim and a stale-hook bug get conflated (see
* `PaneReplacementProof` in pane-agent-identity-adapter.ts). Every existing sequencer `rebind`
* call is pinned here by file and count: today they are the retired-pane `restart` disposition
* (three ingress paths) and the renderer pane-key transfer. Adding a rebind call, or changing
* these, requires updating this audit — and per the migration plan, a `replacementProof`.
*/
const IDENTITY_SEQUENCER_REBIND_RE = /\b(?:observations|rendererAgentStatusObservations)\.rebind\(/g
const EXPECTED_REBIND_SITES: readonly (readonly [path: string, occurrences: number])[] = [
['src/main/agent-hooks/server.ts', 3],
['src/renderer/src/store/slices/agent-status.ts', 1]
]
describe('identity observation rebind audit', () => {
it('pins every identity-sequencer rebind call site by file and count', async () => {
const files = await glob(['src/**/*.{ts,tsx}', 'mobile/src/**/*.{ts,tsx}'], {
ignore: ['**/*.test.*', '**/*.spec.*']
})
const actual: [string, number][] = []
for (const path of files.sort()) {
if (isTestFile(path)) {
continue
}
const source = stripComments(readFileSync(join(process.cwd(), path), 'utf8'))
const occurrences = source.match(IDENTITY_SEQUENCER_REBIND_RE)?.length ?? 0
if (occurrences > 0) {
actual.push([path, occurrences])
}
}
expect(actual).toEqual(EXPECTED_REBIND_SITES.map((site) => [...site]))
}, 30_000)
})
@@ -0,0 +1,156 @@
import { existsSync, readdirSync, readFileSync } from 'node:fs'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { createHash, randomBytes } from 'node:crypto'
import { describe, expect, it } from 'vitest'
import { collectAgentTitleEvidence } from './agent-title-evidence'
import { resolveCanonicalPaneAgentIdentity } from './pane-agent-identity-adapter'
import type { TuiAgent } from './tui-agent'
/**
* Title regression gates for the identity-ladder migration.
*
* Two layers: a controlled fixture table that always runs (CI-safe), and a local characterization
* gate over the machine's real recorded corpus. The corpus gate is not a CI prerequisite tied to
* one developer's home — when no history exists it reports `corpus unavailable — skipped`
* explicitly, never a silently green zero-title run. Raw titles never reach logs or failure
* output; changed titles are reported as salted hashes plus old/new agent summaries only.
*/
const RECORDED_HISTORY_DIR = 'terminal-history'
const QUARANTINE_DIR = '.recovery-quarantine'
function orcaAppSupportCandidates(): string[] {
if (process.platform === 'darwin') {
return [join(homedir(), 'Library', 'Application Support', 'Orca')]
}
if (process.platform === 'win32') {
return [join(process.env.APPDATA ?? join(homedir(), 'AppData', 'Roaming'), 'Orca')]
}
return [
join(process.env.XDG_CONFIG_HOME ?? join(homedir(), '.config'), 'Orca'),
join(process.env.XDG_DATA_HOME ?? join(homedir(), '.local', 'share'), 'Orca')
]
}
/** Deliberately shallow: `terminal-history/<session>/checkpoint.json` only, with the hidden
* quarantine subtree excluded BY NAME so a future recursive rewrite cannot silently turn
* quarantined recovery data into product regressions. */
function loadRecordedTitleCorpus(): { checkpointCount: number; titles: string[] } | null {
const root = orcaAppSupportCandidates()
.map((candidate) => join(candidate, RECORDED_HISTORY_DIR))
.find((candidate) => existsSync(candidate))
if (!root) {
return null
}
let checkpointCount = 0
const titles = new Set<string>()
for (const entry of readdirSync(root, { withFileTypes: true })) {
if (!entry.isDirectory() || entry.name === QUARANTINE_DIR || entry.name.startsWith('.')) {
continue
}
const checkpointPath = join(root, entry.name, 'checkpoint.json')
if (!existsSync(checkpointPath)) {
continue
}
const parsed: unknown = JSON.parse(readFileSync(checkpointPath, 'utf8'))
checkpointCount += 1
const lastTitle = (parsed as { lastTitle?: unknown }).lastTitle
if (typeof lastTitle === 'string' && lastTitle.length > 0) {
titles.add(lastTitle)
}
}
return { checkpointCount, titles: [...titles] }
}
/** What the canonical adapter answers when a title is all a pane has (the uncovered lane). */
function canonicalTitleOnlyAgent(title: string): TuiAgent | null {
return resolveCanonicalPaneAgentIdentity({ title }).agent
}
describe('controlled title fixtures (always run)', () => {
const FIXTURES: readonly { name: string; title: string; expected: TuiAgent | null }[] = [
{
name: 'mandatory adversarial owner suffix beats the agent names in task text',
title: 'STA-4011 Linux Antigravity Commit Messages - grok',
expected: 'grok'
},
{
name: 'task text mentioning other agents is not identity',
title: 'Compare Antigravity with Gemini 3.7 Flash',
expected: null
},
{
name: 'owner suffix still answers over mentioned agents',
title: 'Compare Antigravity with Gemini 3.7 Flash… - grok',
expected: 'grok'
},
{ name: 'Claude status sigil is a vendor marker', title: '✳', expected: 'claude' },
{ name: 'Claude management screen is not identity', title: 'claude agents', expected: null },
{ name: 'a shell title names no agent', title: 'zsh', expected: null },
{ name: 'a default worktree-ish title names no agent', title: 'my-claude-fix', expected: null },
{
name: 'conflicting vendor markers resolve to nothing',
title: '✳ | ✦ two sigils',
expected: null
},
{
name: 'conflicting anchored names resolve to nothing',
title: 'OC | something… - grok',
expected: null
},
{ name: 'a bare Pi title anchors as Pi', title: 'pi', expected: 'pi' },
{ name: 'an OMP status title anchors as OMP', title: 'omp ready', expected: 'omp' },
{
// Wrapper-frame π/OMP separators are handled by the synthetic-title path, not this
// evidence parser; pinned so a parser change here is a deliberate decision.
name: 'a π wrapper frame is declined by the evidence parser',
title: 'π : ready',
expected: null
}
]
for (const fixture of FIXTURES) {
it(fixture.name, () => {
expect(collectAgentTitleEvidence(fixture.title).agent).toBe(fixture.expected)
// The adapter's title-only lane must give the very same answer — phase 1 changes no
// parser semantics, only provenance.
expect(canonicalTitleOnlyAgent(fixture.title)).toBe(fixture.expected)
})
}
})
describe('recorded title corpus characterization (local gate)', () => {
it('the canonical title-only lane matches the shipped parser on every recorded title', (ctx) => {
const corpus = loadRecordedTitleCorpus()
if (corpus === null) {
console.info('corpus unavailable — skipped (no recorded terminal history on this machine)')
ctx.skip()
return
}
// A machine WITH history must never pass on an empty read — that would be a silently green
// zero-title run, not a characterization.
expect(corpus.checkpointCount).toBeGreaterThan(0)
expect(corpus.titles.length).toBeGreaterThan(0)
console.info(
`corpus: ${corpus.checkpointCount} checkpoints, ${corpus.titles.length} distinct titles`
)
const salt = randomBytes(16).toString('hex')
const changed: { titleHash: string; oldAgent: string | null; newAgent: string | null }[] = []
for (const title of corpus.titles) {
const oldAgent = collectAgentTitleEvidence(title).agent
const newAgent = canonicalTitleOnlyAgent(title)
if (oldAgent !== newAgent) {
changed.push({
titleHash: createHash('sha256').update(`${salt}:${title}`).digest('hex').slice(0, 16),
oldAgent,
newAgent
})
}
}
// Report hashes and agent summaries only; a reviewer who needs the raw value inspects the
// protected corpus on the machine that owns it.
expect(changed).toEqual([])
}, 60_000)
})
@@ -0,0 +1,175 @@
import { describe, expect, it } from 'vitest'
import { PaneAgentIdentityComparisonRecorder } from './pane-agent-identity-comparison'
import { resolvePublishedPaneAgentIdentity } from './published-pane-agent-identity'
import { comparePublishedPaneAgentIdentity } from './published-pane-agent-identity-comparison'
import type { TuiAgent } from './tui-agent'
/**
* Output-neutral characterization of the FROZEN host publication adapter. The host wave may only
* replace the frozen call after this table and the real-session comparison window are green, so
* every evidence shape the adapter accepts is pinned here byte-for-byte. These are observations
* of current behavior, not aspirations — do not "fix" a row without the host-wave migration.
*/
type Shape = {
name: string
args: {
hookAgent?: TuiAgent | null
hookIsLive?: boolean
launchAgent?: TuiAgent | null
foregroundAgent?: TuiAgent | null
title?: string | null
}
expected: TuiAgent | undefined
}
const GROK_ADVERSARIAL_TITLE = 'STA-4011 Linux Antigravity Commit Messages - grok'
const SHAPES: readonly Shape[] = [
{ name: 'nothing known', args: {}, expected: undefined },
{
name: 'all null/empty values',
args: { hookAgent: null, launchAgent: null, foregroundAgent: null, title: '' },
expected: undefined
},
{ name: 'live hook only', args: { hookAgent: 'claude', hookIsLive: true }, expected: 'claude' },
{
name: 'completed hook only',
args: { hookAgent: 'codex', hookIsLive: false },
expected: 'codex'
},
{ name: 'launch only', args: { launchAgent: 'gemini' }, expected: 'gemini' },
{ name: 'foreground only', args: { foregroundAgent: 'codex' }, expected: 'codex' },
{ name: 'unambiguous title only', args: { title: GROK_ADVERSARIAL_TITLE }, expected: 'grok' },
{
name: 'free-text title mention only',
args: { title: 'Review the Claude session-history fix' },
expected: undefined
},
{
name: 'live hook beats foreground',
args: { hookAgent: 'claude', hookIsLive: true, foregroundAgent: 'codex' },
expected: 'claude'
},
{
name: 'foreground beats launch (frozen process rung, no proof required)',
args: { launchAgent: 'claude', foregroundAgent: 'codex' },
expected: 'codex'
},
{
name: 'launch beats completed hook',
args: { hookAgent: 'codex', hookIsLive: false, launchAgent: 'claude' },
expected: 'claude'
},
{
name: 'launch beats unambiguous title',
args: { launchAgent: 'claude', title: GROK_ADVERSARIAL_TITLE },
expected: 'claude'
},
{
name: 'completed hook beats unambiguous title',
args: { hookAgent: 'claude', hookIsLive: false, title: GROK_ADVERSARIAL_TITLE },
expected: 'claude'
},
{
name: 'duplicate agreeing sources',
args: {
hookAgent: 'claude',
hookIsLive: true,
launchAgent: 'claude',
foregroundAgent: 'claude',
title: 'claude'
},
expected: 'claude'
},
{
name: 'full conflict resolves to strongest',
args: {
hookAgent: 'claude',
hookIsLive: true,
launchAgent: 'gemini',
foregroundAgent: 'codex',
title: GROK_ADVERSARIAL_TITLE
},
expected: 'claude'
}
]
describe('frozen host adapter characterization', () => {
for (const shape of SHAPES) {
it(`publishes ${shape.expected ?? 'nothing'} for: ${shape.name}`, () => {
expect(resolvePublishedPaneAgentIdentity(shape.args)).toBe(shape.expected)
})
}
})
describe('comparison wrapper output-neutrality', () => {
it('returns the frozen result verbatim for every characterized shape', () => {
const recorder = new PaneAgentIdentityComparisonRecorder()
for (const shape of SHAPES) {
const wrapped = comparePublishedPaneAgentIdentity(
{
...shape.args,
surface: 'terminal-summary',
paneId: `pane:${shape.name}`,
worktreeId: 'wt',
hostScope: 'local'
},
recorder
)
expect(wrapped).toBe(resolvePublishedPaneAgentIdentity(shape.args))
}
expect(recorder.snapshot().comparisons).toBe(SHAPES.length)
})
it('counts the frozen-vs-canonical divergence instead of publishing it', () => {
const recorder = new PaneAgentIdentityComparisonRecorder()
// The frozen ladder lets a bare foreground NAME outrank the launch record; the canonical
// ladder demands a host process proof for that rung. The published value must stay the
// frozen one while the disagreement is counted — that count is the migration gate.
const published = comparePublishedPaneAgentIdentity(
{
launchAgent: 'claude',
foregroundAgent: 'codex',
surface: 'terminal-summary',
paneId: 'pane:process-demotion',
worktreeId: 'wt',
hostScope: 'local'
},
recorder
)
expect(published).toBe('codex')
expect(recorder.snapshot()).toMatchObject({ comparisons: 1, disagreements: 1 })
})
it('records the reclaim/bad-hook input shape separately', () => {
const recorder = new PaneAgentIdentityComparisonRecorder()
comparePublishedPaneAgentIdentity(
{
hookAgent: 'claude',
hookIsLive: false,
title: GROK_ADVERSARIAL_TITLE,
surface: 'terminal-summary',
paneId: 'pane:reclaim-shape',
worktreeId: 'wt',
hostScope: 'local'
},
recorder
)
expect(recorder.snapshot()).toMatchObject({ comparisons: 1, reclaimShapes: 1 })
})
it('skips recomputation for consecutive identical pane inputs but still returns the frozen value', () => {
const recorder = new PaneAgentIdentityComparisonRecorder()
const args = {
launchAgent: 'claude' as const,
surface: 'terminal-summary' as const,
paneId: 'pane:dedupe',
worktreeId: 'wt',
hostScope: 'local' as const
}
expect(comparePublishedPaneAgentIdentity(args, recorder)).toBe('claude')
expect(comparePublishedPaneAgentIdentity(args, recorder)).toBe('claude')
expect(recorder.snapshot().comparisons).toBe(1)
})
})
@@ -0,0 +1,94 @@
import { collectAgentTitleEvidence } from './agent-title-evidence'
import { resolveCanonicalPaneAgentIdentity } from './pane-agent-identity-adapter'
import {
PaneAgentIdentityComparisonRecorder,
type PaneIdentityComparisonSurface,
type PaneIdentityHostScope
} from './pane-agent-identity-comparison'
import { resolvePublishedPaneAgentIdentity } from './published-pane-agent-identity'
import type { TuiAgent } from './tui-agent'
/**
* Output-neutral wrapper for the host publication path. The FROZEN adapter still decides what is
* published — this function returns its result verbatim — while the canonical adapter's answer is
* computed beside it and only disagreements are counted. `RuntimeTerminalSummary.agentIdentity`
* must not change while the comparison window runs.
*/
export type PublishedPaneAgentIdentityComparisonArgs = {
hookAgent?: TuiAgent | null
hookIsLive?: boolean
launchAgent?: TuiAgent | null
foregroundAgent?: TuiAgent | null
title?: string | null
surface: PaneIdentityComparisonSurface
paneId: string
worktreeId?: string | null
hostScope: PaneIdentityHostScope
}
const defaultRecorder = new PaneAgentIdentityComparisonRecorder((line, sample) => {
console.info(`[pane-identity-compare] ${line}`, sample ?? {})
})
export function getPublishedPaneIdentityComparisonRecorder(): PaneAgentIdentityComparisonRecorder {
return defaultRecorder
}
export function comparePublishedPaneAgentIdentity(
args: PublishedPaneAgentIdentityComparisonArgs,
recorder: PaneAgentIdentityComparisonRecorder = defaultRecorder
): TuiAgent | undefined {
const published = resolvePublishedPaneAgentIdentity(args)
try {
recordCanonicalDivergence(args, published ?? null, recorder)
} catch {
// Telemetry must never take down terminal.list; a lost sample is recoverable.
}
return published
}
function recordCanonicalDivergence(
args: PublishedPaneAgentIdentityComparisonArgs,
published: TuiAgent | null,
recorder: PaneAgentIdentityComparisonRecorder
): void {
const signature = [
args.hookAgent ?? '-',
args.hookIsLive === true ? 'live' : 'idle',
args.launchAgent ?? '-',
args.foregroundAgent ?? '-',
args.title ?? '-'
].join('|')
if (!recorder.shouldCompare(args.surface, args.paneId, signature)) {
return
}
// 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 canonical = resolveCanonicalPaneAgentIdentity({
hookAgent: args.hookAgent,
hookIsLive: args.hookIsLive,
launchAgent: args.launchAgent,
foregroundAgent: args.foregroundAgent,
title: args.title,
uncoveredFallback: { agent: published, titleOnly: published !== null }
})
const titleAgent = args.title ? collectAgentTitleEvidence(args.title).agent : null
recorder.record({
surface: args.surface,
paneId: args.paneId,
worktreeId: args.worktreeId,
oldAgent: published,
newAgent: canonical.agent,
newSource: canonical.source,
coverage: canonical.coverage,
titleOnly: canonical.titleOnly,
// Run keys are not plumbed into the publication path yet (host wave); absent, not stale.
runKeyComparability: 'absent',
hostScope: args.hostScope,
ambiguous: canonical.ambiguousAt !== undefined,
reclaimShape: Boolean(
args.hookAgent && args.hookIsLive !== true && titleAgent && titleAgent !== args.hookAgent
)
})
}