mirror of
https://github.com/stablyai/orca.git
synced 2026-10-29 16:17:51 +00:00
wip(agent-status): alternative resolver design from the abandoned #16157 review
Recovered from an uncommitted worktree, not authored fresh. The first #16157 review agent drafted this before its session died on an "Invalid previous_response_id" API error; I reported the work as lost with the session, which was wrong — it was never committed but never gone either. Its shape differs from what shipped: a discriminated `PaneAgentDisplayIdentity` union (`kind: 'resolved' | 'unresolved'` with a `reason`) instead of a nullable agent, an explicit `PaneAgentDisplayPolicy`, and a `PaneAgentEvidenceOrder` that lets a consumer state its own ranking rather than sharing one global order. Kept for reference while the run-generation work is designed — the display/action split it proposes is the same question the delta review raised about incomparable evidence. Not proposed for merge as is; not rebased; not verified.
This commit is contained in:
@@ -1,145 +1,285 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
PANE_AGENT_EVIDENCE_SOURCES,
|
||||
PANE_AGENT_DISPLAY_POLICIES,
|
||||
type PaneAgentEvidence,
|
||||
resolvePaneAgentIdentity
|
||||
type PaneAgentRun,
|
||||
resolvePaneAgentDisplayIdentity
|
||||
} from './pane-agent-identity-resolver'
|
||||
|
||||
const resolve = (evidence: PaneAgentEvidence[], extra = {}) =>
|
||||
resolvePaneAgentIdentity({ evidence, ...extra })
|
||||
const currentRun: PaneAgentRun = { hostAuthorityId: 'host-1', generation: 8 }
|
||||
const previousRun: PaneAgentRun = { hostAuthorityId: 'host-1', generation: 7 }
|
||||
const otherHostRun: PaneAgentRun = { hostAuthorityId: 'host-2', generation: 8 }
|
||||
|
||||
describe('resolvePaneAgentIdentity', () => {
|
||||
describe('a display title is the last thing consulted', () => {
|
||||
it.each(PANE_AGENT_EVIDENCE_SOURCES.filter((s) => s !== 'title' && s !== 'sibling'))(
|
||||
'lets %s outrank a conflicting title',
|
||||
const resolveFocused = (evidence: PaneAgentEvidence[], extra = {}) =>
|
||||
resolvePaneAgentDisplayIdentity({ policy: 'focused-pane', evidence, ...extra })
|
||||
|
||||
describe('resolvePaneAgentDisplayIdentity', () => {
|
||||
describe('named display policies', () => {
|
||||
it.each(PANE_AGENT_DISPLAY_POLICIES['focused-pane'].filter((s) => s !== 'title'))(
|
||||
'lets focused-pane %s outrank a conflicting title',
|
||||
(source) => {
|
||||
const result = resolve([
|
||||
{ source: 'title', agent: 'codex' },
|
||||
{ source, agent: 'grok' }
|
||||
])
|
||||
expect(result.agent).toBe('grok')
|
||||
expect(result.source).toBe(source)
|
||||
expect(
|
||||
resolveFocused([
|
||||
{ source: 'title', agent: 'codex' },
|
||||
{ source, agent: 'grok' }
|
||||
])
|
||||
).toMatchObject({ kind: 'resolved', agent: 'grok', source })
|
||||
}
|
||||
)
|
||||
|
||||
it('uses the title only when nothing else is eligible', () => {
|
||||
expect(resolve([{ source: 'title', agent: 'codex' }])).toMatchObject({
|
||||
agent: 'codex',
|
||||
source: 'title'
|
||||
it('lets a host-observed process override Orca launch intent', () => {
|
||||
expect(
|
||||
resolveFocused([
|
||||
{ source: 'launch', agent: 'claude' },
|
||||
{ source: 'process', agent: 'codex' }
|
||||
])
|
||||
).toMatchObject({ kind: 'resolved', agent: 'codex', source: 'process' })
|
||||
})
|
||||
|
||||
it('keeps sibling evidence out of focused-pane identity', () => {
|
||||
expect(resolveFocused([{ source: 'sibling', agent: 'codex' }])).toEqual({
|
||||
kind: 'unresolved',
|
||||
reason: 'no-evidence'
|
||||
})
|
||||
})
|
||||
|
||||
it('outranks the launch record with nothing weaker than the launch record', () => {
|
||||
// The tab ladder currently puts the parsed title ABOVE activeLaunchAgent, so a string Orca
|
||||
// parsed beats a fact Orca owns. That inversion cannot be expressed here.
|
||||
const result = resolve([
|
||||
{ source: 'launch', agent: 'claude' },
|
||||
{ source: 'title', agent: 'gemini' }
|
||||
])
|
||||
expect(result.agent).toBe('claude')
|
||||
it('uses sibling only after the active pane title for a tab aggregate', () => {
|
||||
expect(
|
||||
resolvePaneAgentDisplayIdentity({
|
||||
policy: 'tab-aggregate',
|
||||
evidence: [
|
||||
{ source: 'sibling', agent: 'codex' },
|
||||
{ source: 'title', agent: 'grok' }
|
||||
]
|
||||
})
|
||||
).toMatchObject({ kind: 'resolved', agent: 'grok', source: 'title' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('run generation separates the bug from the legitimate reclaim', () => {
|
||||
// Both shapes are `completed hook = A, title = B`. Ordering alone cannot tell them apart.
|
||||
const shape = (hookRun: number, titleRun: number): PaneAgentEvidence[] => [
|
||||
{ source: 'completed-hook', agent: 'claude', runId: hookRun },
|
||||
{ source: 'title', agent: 'codex', runId: titleRun }
|
||||
describe('host-owned agent-run identity', () => {
|
||||
const shape = (hookRun: PaneAgentRun, titleRun: PaneAgentRun): PaneAgentEvidence[] => [
|
||||
{ source: 'completed-hook', agent: 'claude', run: hookRun },
|
||||
{ source: 'title', agent: 'codex', run: titleRun }
|
||||
]
|
||||
|
||||
it('keeps the completed hook when both belong to the current run', () => {
|
||||
// The reported bug: nothing new started, so the hook is still the truth.
|
||||
const result = resolvePaneAgentIdentity({ evidence: shape(7, 7), currentRunId: 7 })
|
||||
expect(result).toMatchObject({ agent: 'claude', source: 'completed-hook' })
|
||||
expect(result.supersededSources).toEqual([])
|
||||
})
|
||||
|
||||
it('drops the completed hook once a new run has started', () => {
|
||||
// The legitimate reclaim: the pane was reused, so run 7's hook describes an agent that is
|
||||
// no longer there. It is ineligible, not merely outranked.
|
||||
const result = resolvePaneAgentIdentity({ evidence: shape(7, 8), currentRunId: 8 })
|
||||
expect(result).toMatchObject({ agent: 'codex', source: 'title' })
|
||||
expect(result.supersededSources).toEqual(['completed-hook'])
|
||||
})
|
||||
|
||||
it('produces opposite answers from identical evidence, given only the run ids', () => {
|
||||
// The whole point, stated as one assertion.
|
||||
const bug = resolvePaneAgentIdentity({ evidence: shape(7, 7), currentRunId: 7 })
|
||||
const reclaim = resolvePaneAgentIdentity({ evidence: shape(7, 8), currentRunId: 8 })
|
||||
expect(bug.agent).not.toBe(reclaim.agent)
|
||||
})
|
||||
})
|
||||
|
||||
describe('mixed-version peers', () => {
|
||||
it('treats evidence with no run id as eligible', () => {
|
||||
// An old host publishes no run ids. Treating unknown as stale would blank every row.
|
||||
const result = resolvePaneAgentIdentity({
|
||||
evidence: [{ source: 'completed-hook', agent: 'claude' }],
|
||||
currentRunId: 9
|
||||
it('keeps the completed hook when both observations belong to the current run', () => {
|
||||
expect(
|
||||
resolvePaneAgentDisplayIdentity({
|
||||
policy: 'focused-pane',
|
||||
evidence: shape(currentRun, currentRun),
|
||||
currentRun
|
||||
})
|
||||
).toEqual({
|
||||
kind: 'resolved',
|
||||
agent: 'claude',
|
||||
source: 'completed-hook',
|
||||
runScope: 'current'
|
||||
})
|
||||
expect(result.agent).toBe('claude')
|
||||
})
|
||||
|
||||
it('disables run filtering entirely when the pane has no current run', () => {
|
||||
const result = resolvePaneAgentIdentity({
|
||||
evidence: [{ source: 'completed-hook', agent: 'claude', runId: 3 }]
|
||||
it('drops the completed hook after positive evidence advances the agent run', () => {
|
||||
expect(
|
||||
resolvePaneAgentDisplayIdentity({
|
||||
policy: 'focused-pane',
|
||||
evidence: shape(previousRun, currentRun),
|
||||
currentRun
|
||||
})
|
||||
).toEqual({
|
||||
kind: 'resolved',
|
||||
agent: 'codex',
|
||||
source: 'title',
|
||||
runScope: 'current'
|
||||
})
|
||||
expect(result).toMatchObject({ agent: 'claude', supersededSources: [] })
|
||||
})
|
||||
|
||||
it('includes host authority rather than comparing naked generation numbers', () => {
|
||||
expect(
|
||||
resolvePaneAgentDisplayIdentity({
|
||||
policy: 'focused-pane',
|
||||
evidence: [
|
||||
{ source: 'completed-hook', agent: 'claude', run: otherHostRun },
|
||||
{ source: 'title', agent: 'codex', run: currentRun }
|
||||
],
|
||||
currentRun
|
||||
})
|
||||
).toMatchObject({ kind: 'resolved', agent: 'codex', source: 'title' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('siblings are tab-scoped', () => {
|
||||
it('ignores a sibling by default', () => {
|
||||
expect(resolve([{ source: 'sibling', agent: 'codex' }]).agent).toBeNull()
|
||||
})
|
||||
|
||||
it('accepts a sibling when the caller opts in', () => {
|
||||
expect(resolve([{ source: 'sibling', agent: 'codex' }], { allowSibling: true }).agent).toBe(
|
||||
'codex'
|
||||
)
|
||||
})
|
||||
|
||||
it('still ranks a sibling above a title', () => {
|
||||
const result = resolve(
|
||||
[
|
||||
{ source: 'title', agent: 'grok' },
|
||||
{ source: 'sibling', agent: 'codex' }
|
||||
],
|
||||
{ allowSibling: true }
|
||||
)
|
||||
expect(result.source).toBe('sibling')
|
||||
})
|
||||
})
|
||||
|
||||
describe('no eligible evidence', () => {
|
||||
it('returns null rather than guessing', () => {
|
||||
expect(resolve([])).toMatchObject({ agent: null, source: null })
|
||||
})
|
||||
|
||||
it('returns null when every source belongs to a superseded run', () => {
|
||||
const result = resolvePaneAgentIdentity({
|
||||
evidence: [
|
||||
{ source: 'live-hook', agent: 'claude', runId: 1 },
|
||||
{ source: 'title', agent: 'codex', runId: 1 }
|
||||
],
|
||||
currentRunId: 2
|
||||
describe('mixed-version display evidence', () => {
|
||||
it('preserves source authority when a stronger source cannot publish a run', () => {
|
||||
expect(
|
||||
resolvePaneAgentDisplayIdentity({
|
||||
policy: 'focused-pane',
|
||||
evidence: [
|
||||
{ source: 'live-hook', agent: 'claude' },
|
||||
{ source: 'title', agent: 'codex', run: currentRun }
|
||||
],
|
||||
currentRun
|
||||
})
|
||||
).toEqual({
|
||||
kind: 'resolved',
|
||||
agent: 'claude',
|
||||
source: 'live-hook',
|
||||
runScope: 'compatibility'
|
||||
})
|
||||
expect(result.agent).toBeNull()
|
||||
expect(result.supersededSources).toEqual(['live-hook', 'title'])
|
||||
})
|
||||
|
||||
it('uses exact current-run scope as a tie-breaker within one source', () => {
|
||||
expect(
|
||||
resolvePaneAgentDisplayIdentity({
|
||||
policy: 'focused-pane',
|
||||
evidence: [
|
||||
{ source: 'live-hook', agent: 'claude', run: currentRun },
|
||||
{ source: 'live-hook', agent: 'codex' }
|
||||
],
|
||||
currentRun
|
||||
})
|
||||
).toEqual({
|
||||
kind: 'resolved',
|
||||
agent: 'claude',
|
||||
source: 'live-hook',
|
||||
runScope: 'current'
|
||||
})
|
||||
})
|
||||
|
||||
it('disables run filtering when the peer has no current run identity', () => {
|
||||
expect(
|
||||
resolvePaneAgentDisplayIdentity({
|
||||
policy: 'focused-pane',
|
||||
evidence: [{ source: 'completed-hook', agent: 'claude', run: previousRun }]
|
||||
})
|
||||
).toMatchObject({ kind: 'resolved', agent: 'claude', runScope: 'compatibility' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('input order does not decide the answer', () => {
|
||||
it('resolves the same regardless of how evidence is listed', () => {
|
||||
describe('equal-source ordering', () => {
|
||||
const conflict: PaneAgentEvidence[] = [
|
||||
{ source: 'live-hook', agent: 'claude' },
|
||||
{ source: 'live-hook', agent: 'codex' },
|
||||
{ source: 'title', agent: 'grok' }
|
||||
]
|
||||
|
||||
it('refuses incomparable claims instead of letting a weaker source decide', () => {
|
||||
expect(resolveFocused(conflict)).toEqual({
|
||||
kind: 'unresolved',
|
||||
reason: 'conflicting-evidence',
|
||||
source: 'live-hook'
|
||||
})
|
||||
})
|
||||
|
||||
it('does not let input order break an incomparable tie', () => {
|
||||
expect(resolveFocused(conflict)).toEqual(resolveFocused(conflict.toReversed()))
|
||||
})
|
||||
|
||||
it('chooses the newest observation sequenced by one authority', () => {
|
||||
const evidence: PaneAgentEvidence[] = [
|
||||
{ source: 'title', agent: 'codex' },
|
||||
{ source: 'launch', agent: 'grok' },
|
||||
{ source: 'live-hook', agent: 'claude' }
|
||||
{
|
||||
source: 'process',
|
||||
agent: 'claude',
|
||||
order: { authorityId: 'main', revision: 4 }
|
||||
},
|
||||
{
|
||||
source: 'process',
|
||||
agent: 'codex',
|
||||
order: { authorityId: 'main', revision: 5 }
|
||||
}
|
||||
]
|
||||
const forward = resolve([...evidence])
|
||||
const reverse = resolve(evidence.toReversed())
|
||||
expect(forward).toEqual(reverse)
|
||||
expect(forward.source).toBe('live-hook')
|
||||
expect(resolveFocused(evidence)).toMatchObject({
|
||||
kind: 'resolved',
|
||||
agent: 'codex',
|
||||
source: 'process'
|
||||
})
|
||||
expect(resolveFocused(evidence)).toEqual(resolveFocused(evidence.toReversed()))
|
||||
})
|
||||
|
||||
it('refuses ordering claims from different authorities', () => {
|
||||
expect(
|
||||
resolveFocused([
|
||||
{
|
||||
source: 'process',
|
||||
agent: 'claude',
|
||||
order: { authorityId: 'main', revision: 5 }
|
||||
},
|
||||
{
|
||||
source: 'process',
|
||||
agent: 'codex',
|
||||
order: { authorityId: 'renderer', revision: 6 }
|
||||
}
|
||||
])
|
||||
).toMatchObject({ kind: 'unresolved', reason: 'conflicting-evidence' })
|
||||
})
|
||||
|
||||
it('refuses different agents tied at the same authoritative revision', () => {
|
||||
expect(
|
||||
resolveFocused([
|
||||
{
|
||||
source: 'live-hook',
|
||||
agent: 'claude',
|
||||
order: { authorityId: 'main', revision: 5 }
|
||||
},
|
||||
{
|
||||
source: 'live-hook',
|
||||
agent: 'codex',
|
||||
order: { authorityId: 'main', revision: 5 }
|
||||
}
|
||||
])
|
||||
).toMatchObject({ kind: 'unresolved', reason: 'conflicting-evidence' })
|
||||
})
|
||||
|
||||
it('accepts duplicate observations that agree', () => {
|
||||
expect(
|
||||
resolveFocused([
|
||||
{ source: 'process', agent: 'codex' },
|
||||
{ source: 'process', agent: 'codex' }
|
||||
])
|
||||
).toMatchObject({ kind: 'resolved', agent: 'codex', source: 'process' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('unresolved and run-boundary states', () => {
|
||||
it('returns no-evidence for an empty display input', () => {
|
||||
expect(resolveFocused([])).toEqual({ kind: 'unresolved', reason: 'no-evidence' })
|
||||
})
|
||||
|
||||
it('returns run-transition when every ranked observation is superseded', () => {
|
||||
expect(
|
||||
resolvePaneAgentDisplayIdentity({
|
||||
policy: 'focused-pane',
|
||||
evidence: [
|
||||
{ source: 'live-hook', agent: 'claude', run: previousRun },
|
||||
{ source: 'title', agent: 'codex', run: previousRun }
|
||||
],
|
||||
currentRun
|
||||
})
|
||||
).toEqual({ kind: 'unresolved', reason: 'run-transition' })
|
||||
})
|
||||
|
||||
it('resolves as soon as the positive evidence for the new run is present', () => {
|
||||
expect(
|
||||
resolvePaneAgentDisplayIdentity({
|
||||
policy: 'focused-pane',
|
||||
evidence: [
|
||||
{ source: 'completed-hook', agent: 'claude', run: previousRun },
|
||||
{ source: 'launch', agent: 'codex', run: currentRun }
|
||||
],
|
||||
currentRun
|
||||
})
|
||||
).toMatchObject({ kind: 'resolved', agent: 'codex', source: 'launch' })
|
||||
})
|
||||
})
|
||||
|
||||
it('does not let input order decide between different sources', () => {
|
||||
const evidence: PaneAgentEvidence[] = [
|
||||
{ source: 'title', agent: 'codex' },
|
||||
{ source: 'launch', agent: 'grok' },
|
||||
{ source: 'live-hook', agent: 'claude' }
|
||||
]
|
||||
expect(resolveFocused(evidence)).toEqual(resolveFocused(evidence.toReversed()))
|
||||
expect(resolveFocused(evidence)).toMatchObject({
|
||||
kind: 'resolved',
|
||||
agent: 'claude',
|
||||
source: 'live-hook'
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,109 +1,164 @@
|
||||
import type { AgentStatusObservation } from './agent-status-observation'
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
|
||||
/**
|
||||
* One place that answers "which agent is in this pane".
|
||||
*
|
||||
* Today four ladders answer it independently — the tab icon, the open-tab/search occupant, the
|
||||
* sidebar title rows, and the sidebar hook-row fallback — and they disagree. Two of them consult
|
||||
* the terminal title before the launch record, so a string Orca parsed outranks a fact Orca owns.
|
||||
*
|
||||
* Two rules make this resolvable where reordering alone could not:
|
||||
*
|
||||
* 1. Evidence is ranked by how directly it observes the process, and a display title is last.
|
||||
* 2. Every observation carries the `runId` of the agent run it belongs to. Evidence from a run
|
||||
* that has since been replaced is INELIGIBLE rather than merely outranked.
|
||||
*
|
||||
* Rule 2 is what separates two situations that are otherwise identical. A completed hook naming
|
||||
* A plus a title naming B is either a bug (the hook is right, the title is stale) or a legitimate
|
||||
* reclaim (the pane was reused and B really is there). Same signals, opposite answers. With run
|
||||
* ids they are different facts: in the bug both belong to the current run, and in the reclaim the
|
||||
* hook belongs to a previous one.
|
||||
*
|
||||
* A run is advanced only on positive evidence that a new agent started in the pane — an accepted
|
||||
* launch, a recognized command at a shell prompt, a host-confirmed foreground change, a new
|
||||
* provider session. Never by a title changing, and never by transport loss.
|
||||
*/
|
||||
export const PANE_AGENT_EVIDENCE_SOURCES = [
|
||||
/** A live provider hook for a turn in progress. The agent is running and said so. */
|
||||
'live-hook',
|
||||
/** The pane's foreground process, as read on the execution host. */
|
||||
'process',
|
||||
/** Orca launched, resumed, or accepted a command for this agent. A fact Orca owns. */
|
||||
'launch',
|
||||
/** A provider hook from a turn that finished. Still authoritative about identity. */
|
||||
'completed-hook',
|
||||
/** A sleeping session record restored for this pane. */
|
||||
'sleeping-session',
|
||||
/** Another pane in the same tab. Tab-level surfaces only; never pane-scoped routing. */
|
||||
'sibling',
|
||||
/** Parsed from the terminal title. A decoration channel; anyone can type an agent's name. */
|
||||
'title'
|
||||
] as const
|
||||
export type PaneAgentEvidenceSource = (typeof PANE_AGENT_EVIDENCE_SOURCES)[number]
|
||||
export const PANE_AGENT_DISPLAY_POLICIES = {
|
||||
'focused-pane': ['live-hook', 'process', 'launch', 'completed-hook', 'sleeping-session', 'title'],
|
||||
'tab-aggregate': [
|
||||
'live-hook',
|
||||
'process',
|
||||
'launch',
|
||||
'completed-hook',
|
||||
'sleeping-session',
|
||||
'title',
|
||||
'sibling'
|
||||
]
|
||||
} as const
|
||||
|
||||
/** Authority order, strongest first. Position here is the ONLY place precedence is expressed. */
|
||||
const SOURCE_RANK: readonly PaneAgentEvidenceSource[] = PANE_AGENT_EVIDENCE_SOURCES
|
||||
export type PaneAgentDisplayPolicy = keyof typeof PANE_AGENT_DISPLAY_POLICIES
|
||||
export type PaneAgentEvidenceSource =
|
||||
(typeof PANE_AGENT_DISPLAY_POLICIES)[PaneAgentDisplayPolicy][number]
|
||||
|
||||
/** Execution-host-owned identity of one agent run behind a pane key. */
|
||||
export type PaneAgentRun = {
|
||||
hostAuthorityId: string
|
||||
generation: number
|
||||
}
|
||||
|
||||
/** Comparable only when both observations carry the same authority id. */
|
||||
export type PaneAgentEvidenceOrder = Readonly<
|
||||
Pick<AgentStatusObservation, 'authorityId' | 'revision'>
|
||||
>
|
||||
|
||||
export type PaneAgentEvidence = {
|
||||
source: PaneAgentEvidenceSource
|
||||
agent: TuiAgent
|
||||
/**
|
||||
* The agent run this evidence describes. Evidence whose run is not the pane's current run is
|
||||
* ineligible. Undefined means unknown — from an old peer that does not publish run ids, or an
|
||||
* ingress not yet stamped — and is treated as eligible, so a missing field never blanks a row.
|
||||
*/
|
||||
runId?: number
|
||||
/** Absent when an older peer or unstamped ingress cannot identify the agent run. */
|
||||
run?: PaneAgentRun
|
||||
/** Used only to settle equal-source claims that one authority sequenced. */
|
||||
order?: PaneAgentEvidenceOrder
|
||||
}
|
||||
|
||||
export type PaneAgentIdentityInput = {
|
||||
export type PaneAgentDisplayIdentityInput = {
|
||||
policy: PaneAgentDisplayPolicy
|
||||
evidence: readonly PaneAgentEvidence[]
|
||||
/** The pane's current run. Undefined disables run filtering entirely (old peer, mixed version). */
|
||||
currentRunId?: number
|
||||
/**
|
||||
* Pane-scoped consumers must not inherit another pane's agent. Sibling evidence is dropped
|
||||
* unless the caller is a tab-level surface that opted in.
|
||||
*/
|
||||
allowSibling?: boolean
|
||||
/** Absent on older peers; display resolution then preserves compatibility. */
|
||||
currentRun?: PaneAgentRun
|
||||
}
|
||||
|
||||
export type PaneAgentIdentity = {
|
||||
agent: TuiAgent | null
|
||||
/** Which class of evidence decided it. Null when nothing eligible remained. */
|
||||
source: PaneAgentEvidenceSource | null
|
||||
/** Evidence discarded because it belongs to a superseded run. Surfaced for diagnostics. */
|
||||
supersededSources: readonly PaneAgentEvidenceSource[]
|
||||
export type ResolvedPaneAgentDisplayIdentity = {
|
||||
kind: 'resolved'
|
||||
agent: TuiAgent
|
||||
source: PaneAgentEvidenceSource
|
||||
/** Compatibility means one side cannot publish agent-run identity. */
|
||||
runScope: 'current' | 'compatibility'
|
||||
}
|
||||
|
||||
export type UnresolvedPaneAgentDisplayIdentity = {
|
||||
kind: 'unresolved'
|
||||
reason: 'no-evidence' | 'run-transition' | 'conflicting-evidence'
|
||||
/** Present when equally ranked, incomparable evidence names different agents. */
|
||||
source?: PaneAgentEvidenceSource
|
||||
}
|
||||
|
||||
export type PaneAgentDisplayIdentity =
|
||||
| ResolvedPaneAgentDisplayIdentity
|
||||
| UnresolvedPaneAgentDisplayIdentity
|
||||
|
||||
/**
|
||||
* Resolves one pane's agent from ranked evidence.
|
||||
*
|
||||
* Returns null rather than guessing. A pane with no eligible evidence shows no agent, which is
|
||||
* recoverable; showing the wrong agent is not, and at the action surfaces (orchestration routing,
|
||||
* mailbox delivery, prompt-cache timers) it is a misdelivery rather than a cosmetic slip.
|
||||
* Display-only identity resolution. Write authorization requires proof-bearing process, hook, or
|
||||
* launch capabilities and deliberately has no resolver in this module.
|
||||
*/
|
||||
export function resolvePaneAgentIdentity(input: PaneAgentIdentityInput): PaneAgentIdentity {
|
||||
const superseded: PaneAgentEvidenceSource[] = []
|
||||
const eligible = input.evidence.filter((item) => {
|
||||
if (item.source === 'sibling' && input.allowSibling !== true) {
|
||||
return false
|
||||
}
|
||||
// Why undefined is eligible: absence means "this peer does not publish run ids", not "this
|
||||
// belongs to an old run". Treating unknown as stale would blank every row from an old host.
|
||||
if (input.currentRunId === undefined || item.runId === undefined) {
|
||||
return true
|
||||
}
|
||||
if (item.runId === input.currentRunId) {
|
||||
return true
|
||||
}
|
||||
superseded.push(item.source)
|
||||
return false
|
||||
})
|
||||
export function resolvePaneAgentDisplayIdentity(
|
||||
input: PaneAgentDisplayIdentityInput
|
||||
): PaneAgentDisplayIdentity {
|
||||
const sources = PANE_AGENT_DISPLAY_POLICIES[input.policy]
|
||||
let rejectedSupersededRun = false
|
||||
|
||||
for (const source of SOURCE_RANK) {
|
||||
const match = eligible.find((item) => item.source === source)
|
||||
if (match) {
|
||||
return { agent: match.agent, source, supersededSources: superseded }
|
||||
for (const source of sources) {
|
||||
const eligible: PaneAgentEvidence[] = []
|
||||
for (const item of input.evidence) {
|
||||
if (item.source !== source) {
|
||||
continue
|
||||
}
|
||||
if (isSupersededRun(item.run, input.currentRun)) {
|
||||
rejectedSupersededRun = true
|
||||
continue
|
||||
}
|
||||
eligible.push(item)
|
||||
}
|
||||
if (eligible.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
const exactCurrent = eligible.filter((item) => isSameRun(item.run, input.currentRun))
|
||||
const candidates = exactCurrent.length > 0 ? exactCurrent : eligible
|
||||
const selected = selectEqualSourceCandidate(candidates)
|
||||
if (selected === null) {
|
||||
return { kind: 'unresolved', reason: 'conflicting-evidence', source }
|
||||
}
|
||||
return {
|
||||
kind: 'resolved',
|
||||
agent: selected.agent,
|
||||
source,
|
||||
runScope: isSameRun(selected.run, input.currentRun) ? 'current' : 'compatibility'
|
||||
}
|
||||
}
|
||||
return { agent: null, source: null, supersededSources: superseded }
|
||||
|
||||
return {
|
||||
kind: 'unresolved',
|
||||
reason: rejectedSupersededRun ? 'run-transition' : 'no-evidence'
|
||||
}
|
||||
}
|
||||
|
||||
function selectEqualSourceCandidate(
|
||||
candidates: readonly PaneAgentEvidence[]
|
||||
): PaneAgentEvidence | null {
|
||||
const first = candidates[0]
|
||||
if (candidates.every((item) => item.agent === first.agent)) {
|
||||
return first
|
||||
}
|
||||
|
||||
const firstOrder = first.order
|
||||
if (
|
||||
firstOrder === undefined ||
|
||||
candidates.some(
|
||||
(item) => item.order === undefined || item.order.authorityId !== firstOrder.authorityId
|
||||
)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
let newest = first
|
||||
for (const candidate of candidates.slice(1)) {
|
||||
if (
|
||||
candidate.order !== undefined &&
|
||||
newest.order !== undefined &&
|
||||
candidate.order.revision > newest.order.revision
|
||||
) {
|
||||
newest = candidate
|
||||
}
|
||||
}
|
||||
if (newest.order === undefined) {
|
||||
return null
|
||||
}
|
||||
const sameRevision = candidates.filter((item) => item.order?.revision === newest.order?.revision)
|
||||
return sameRevision.every((item) => item.agent === newest.agent) ? newest : null
|
||||
}
|
||||
|
||||
function isSupersededRun(
|
||||
evidenceRun: PaneAgentRun | undefined,
|
||||
currentRun: PaneAgentRun | undefined
|
||||
): boolean {
|
||||
return (
|
||||
evidenceRun !== undefined && currentRun !== undefined && !isSameRun(evidenceRun, currentRun)
|
||||
)
|
||||
}
|
||||
|
||||
function isSameRun(left: PaneAgentRun | undefined, right: PaneAgentRun | undefined): boolean {
|
||||
return (
|
||||
left !== undefined &&
|
||||
right !== undefined &&
|
||||
left.hostAuthorityId === right.hostAuthorityId &&
|
||||
left.generation === right.generation
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user