mirror of
https://github.com/stablyai/orca.git
synced 2026-10-28 16:02:55 +00:00
instrument pane identity evidence availability
This commit is contained in:
@@ -79,6 +79,10 @@ import { registerCoreHandlers } from './ipc/register-core-handlers/register-core
|
||||
import { initObservability, shutdownObservability } from './observability'
|
||||
import { registerMobileHandlers } from './ipc/mobile'
|
||||
import { initTelemetry, shutdownTelemetry, trackAppOpenedOnce, track } from './telemetry/client'
|
||||
import {
|
||||
createDesktopPaneAgentIdentityCensus,
|
||||
createHeadlessPaneAgentIdentityCensus
|
||||
} from './telemetry/pane-agent-identity-census-composition'
|
||||
import { classifyError } from './telemetry/classify-error'
|
||||
import { recordManagedHookInstallFailure } from './agent-hooks/install-telemetry'
|
||||
import {
|
||||
@@ -2490,6 +2494,9 @@ void app.whenReady().then(async () => {
|
||||
runtime?.touchMobileSessionTabsForPane(enriched.paneKey, enriched.worktreeId ?? null)
|
||||
}
|
||||
})
|
||||
const unsubscribePaneAgentIdentityHook = agentHookServer.subscribeEnrichedStatus((enriched) => {
|
||||
runtime?.observeAgentHookStatus(enriched)
|
||||
})
|
||||
// Teardown: agent exit, pane close, and the SSH transient-disconnect batch all land
|
||||
// here. Without it the live state published above becomes a zombie question card.
|
||||
const unsubscribeHookStatusClear = agentHookServer.subscribePaneStatusClear((clear) => {
|
||||
@@ -2506,6 +2513,7 @@ void app.whenReady().then(async () => {
|
||||
unsubscribeStatusChanges()
|
||||
unsubscribeProviderSessionChanges()
|
||||
unsubscribeHookStatusSessionTabs()
|
||||
unsubscribePaneAgentIdentityHook()
|
||||
unsubscribeHookStatusClear()
|
||||
}
|
||||
// Why: telemetry must init before any IPC handler/renderer can call track(); it's a no-op in dev and while TELEMETRY_ENABLED is false, so it's safe early.
|
||||
@@ -2723,7 +2731,11 @@ void app.whenReady().then(async () => {
|
||||
envelope
|
||||
)
|
||||
}
|
||||
const paneAgentIdentityCensus = isServeMode
|
||||
? createHeadlessPaneAgentIdentityCensus()
|
||||
: createDesktopPaneAgentIdentityCensus()
|
||||
const runtimeService = new OrcaRuntimeService(store, stats, {
|
||||
paneAgentIdentityCensus,
|
||||
agentSessionClaimSigner: loadAgentSessionClaimSigner(
|
||||
getProfileUserDataPath(),
|
||||
getProfileUserDataPath()
|
||||
@@ -3508,6 +3520,7 @@ app.on('will-quit', (e) => {
|
||||
pluginService = null
|
||||
setUnreadDockBadgeCount(0)
|
||||
agentHookServer.stop()
|
||||
runtime?.shutdownPaneAgentIdentityCensus()
|
||||
// Why Windows only: POSIX hooks short-circuit on ORCA_PANE_KEY, while Windows must register a
|
||||
// bare script path that cannot express the guard and would otherwise keep spawning after quit.
|
||||
// Why bounded here: every other teardown member carries its own ceiling, and this one reaches
|
||||
|
||||
@@ -78,6 +78,7 @@ export async function commitPtyIpcSpawn(ctx: PtyIpcSpawnState): Promise<PtySpawn
|
||||
spawnEnv: ctx.spawnEnv,
|
||||
launchAgent: args.launchAgent,
|
||||
launchConfig: ctx.effectiveLaunchConfig,
|
||||
resumeProviderSession: args.resumeProviderSession,
|
||||
isReattach: ctx.result.isReattach === true,
|
||||
hasStablePaneOwner: ctx.stablePaneOwner !== null,
|
||||
incarnationId: ctx.result.incarnationId
|
||||
|
||||
@@ -163,7 +163,19 @@ export function createPtyWriteInput(deps: {
|
||||
if (visibleRendererPtys.has(args.id)) {
|
||||
clearHiddenRendererResizeOutput(args.id)
|
||||
}
|
||||
return writePtyProviderInput(provider, args.id, args.data)
|
||||
const result = writePtyProviderInput(provider, args.id, args.data)
|
||||
if (typeof result === 'boolean') {
|
||||
if (result) {
|
||||
runtime?.observeAcceptedPtyWrite?.(args.id, args.data)
|
||||
}
|
||||
return result
|
||||
}
|
||||
return result.then((accepted) => {
|
||||
if (accepted) {
|
||||
runtime?.observeAcceptedPtyWrite?.(args.id, args.data)
|
||||
}
|
||||
return accepted
|
||||
})
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
@@ -188,7 +200,19 @@ export function createPtyWriteInput(deps: {
|
||||
if (visibleRendererPtys.has(args.id)) {
|
||||
clearHiddenRendererResizeOutput(args.id)
|
||||
}
|
||||
return writePtyProviderInput(provider, args.id, args.data)
|
||||
const result = writePtyProviderInput(provider, args.id, args.data)
|
||||
if (typeof result === 'boolean') {
|
||||
if (result) {
|
||||
runtime?.observeAcceptedPtyWrite?.(args.id, args.data)
|
||||
}
|
||||
return result
|
||||
}
|
||||
return result.then((accepted) => {
|
||||
if (accepted) {
|
||||
runtime?.observeAcceptedPtyWrite?.(args.id, args.data)
|
||||
}
|
||||
return accepted
|
||||
})
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { admitRendererAgentLaunchAuthority } from './launch-authority'
|
||||
|
||||
function admit(resumeProviderSession: unknown) {
|
||||
return admitRendererAgentLaunchAuthority({
|
||||
launchToken: 'token',
|
||||
spawnEnv: { ORCA_AGENT_LAUNCH_TOKEN: 'token' },
|
||||
launchAgent: 'codex',
|
||||
launchConfig: {} as never,
|
||||
resumeProviderSession,
|
||||
isReattach: false,
|
||||
hasStablePaneOwner: false,
|
||||
incarnationId: 'incarnation-1'
|
||||
})
|
||||
}
|
||||
|
||||
describe('renderer agent launch authority', () => {
|
||||
it('classifies fresh launches and provider-session resumes distinctly', () => {
|
||||
expect(admit(undefined)).toMatchObject({ launchMode: 'orca-launch' })
|
||||
expect(admit('provider-session')).toMatchObject({ launchMode: 'resume' })
|
||||
})
|
||||
})
|
||||
@@ -10,10 +10,15 @@ export function admitRendererAgentLaunchAuthority(args: {
|
||||
spawnEnv: Record<string, string> | undefined
|
||||
launchAgent: unknown
|
||||
launchConfig: SleepingAgentLaunchConfig | undefined
|
||||
resumeProviderSession: unknown
|
||||
isReattach: boolean
|
||||
hasStablePaneOwner: boolean
|
||||
incarnationId: unknown
|
||||
}): { launchToken: string; launchAgent: TuiAgent } | null {
|
||||
}): {
|
||||
launchToken: string
|
||||
launchAgent: TuiAgent
|
||||
launchMode: 'orca-launch' | 'resume'
|
||||
} | null {
|
||||
if (
|
||||
args.isReattach ||
|
||||
args.hasStablePaneOwner ||
|
||||
@@ -27,7 +32,11 @@ export function admitRendererAgentLaunchAuthority(args: {
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return { launchToken: args.launchToken, launchAgent: args.launchAgent }
|
||||
return {
|
||||
launchToken: args.launchToken,
|
||||
launchAgent: args.launchAgent,
|
||||
launchMode: args.resumeProviderSession ? 'resume' : 'orca-launch'
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldRefreshNativeClaudeAgentTeamsEnv(args: {
|
||||
|
||||
@@ -458,7 +458,12 @@ describe('registerCoreHandlers', () => {
|
||||
|
||||
it('passes the store through to handler registrars that need it', async () => {
|
||||
const store = { marker: 'store' }
|
||||
const runtime = { marker: 'runtime', getAgentBrowserBridge: () => null }
|
||||
const paneAgentIdentityCensus = { marker: 'paneAgentIdentityCensus' }
|
||||
const runtime = {
|
||||
marker: 'runtime',
|
||||
getAgentBrowserBridge: () => null,
|
||||
getPaneAgentIdentityCensus: () => paneAgentIdentityCensus
|
||||
}
|
||||
const stats = { marker: 'stats' }
|
||||
const claudeUsage = { marker: 'claudeUsage' }
|
||||
const codexUsage = { marker: 'codexUsage' }
|
||||
@@ -539,7 +544,7 @@ describe('registerCoreHandlers', () => {
|
||||
expect(registerWorkspaceSpaceHandlersMock).toHaveBeenCalledWith(store)
|
||||
expect(registerWorkspacePortHandlersMock).toHaveBeenCalledWith(store)
|
||||
expect(registerLocalhostWorktreeLabelHandlersMock).toHaveBeenCalledWith(store)
|
||||
expect(registerTelemetryHandlersMock).toHaveBeenCalledWith(store)
|
||||
expect(registerTelemetryHandlersMock).toHaveBeenCalledWith(store, paneAgentIdentityCensus)
|
||||
expect(registerOrcaProfileHandlersMock).toHaveBeenCalledWith(store, { onBeforeRelaunch })
|
||||
expect(registerSessionHandlersMock).toHaveBeenCalledWith(store)
|
||||
expect(registerUIHandlersMock).toHaveBeenCalledWith(store, {
|
||||
|
||||
@@ -189,7 +189,7 @@ export function registerCoreHandlers(
|
||||
if (pluginService) {
|
||||
registerPluginHandlers(store, pluginService, runtime, marketplaceServices)
|
||||
}
|
||||
registerTelemetryHandlers(store)
|
||||
registerTelemetryHandlers(store, runtime.getPaneAgentIdentityCensus())
|
||||
registerOrcaProfileHandlers(store, {
|
||||
onBeforeRelaunch: lifecycleOptions.onBeforeRelaunch,
|
||||
onAuthMutation: lifecycleOptions.onOrcaProfileAuthMutation,
|
||||
|
||||
@@ -40,6 +40,7 @@ vi.mock('../telemetry/onboarding-cohort-classifier', () => ({
|
||||
}))
|
||||
|
||||
import { _resetStoreForTests, registerTelemetryHandlers } from './telemetry'
|
||||
import { PaneAgentIdentityCensus } from '../telemetry/pane-agent-identity-census'
|
||||
|
||||
function captureHandlers(): void {
|
||||
handlers.clear()
|
||||
@@ -71,7 +72,7 @@ function makeFakeStore(telemetry: GlobalSettings['telemetry']): {
|
||||
|
||||
function registerWith(telemetry: GlobalSettings['telemetry']): FakeStoreState {
|
||||
const { store, state } = makeFakeStore(telemetry)
|
||||
registerTelemetryHandlers(store)
|
||||
registerTelemetryHandlers(store, new PaneAgentIdentityCensus({ emit: null }))
|
||||
captureHandlers()
|
||||
return state
|
||||
}
|
||||
@@ -161,10 +162,38 @@ describe('telemetry IPC handlers', () => {
|
||||
bucket_source: 'crossed_now'
|
||||
})
|
||||
handler({}, 'daemon_audit_eligibility', {})
|
||||
handler({}, 'pane_agent_identity_availability', { rows: [] })
|
||||
expect(trackMock).not.toHaveBeenCalled()
|
||||
expect(getCohortAtEmitMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('accepts identity availability only through the dedicated validated ingestion path', () => {
|
||||
const census = new PaneAgentIdentityCensus({ emit: null })
|
||||
const ingest = vi.spyOn(census, 'ingestRelaySnapshot')
|
||||
const { store } = makeFakeStore({
|
||||
installId: 'x',
|
||||
existedBeforeTelemetryRelease: false,
|
||||
optedIn: true
|
||||
})
|
||||
registerTelemetryHandlers(store, census)
|
||||
captureHandlers()
|
||||
|
||||
handlers.get('telemetry:ingestPaneAgentIdentityAvailability')!(
|
||||
{},
|
||||
{
|
||||
environmentKey: 'relay-1',
|
||||
snapshot: { epoch: 'epoch-1', revision: 1, rows: [] }
|
||||
}
|
||||
)
|
||||
|
||||
expect(ingest).toHaveBeenCalledWith('relay-1', {
|
||||
epoch: 'epoch-1',
|
||||
revision: 1,
|
||||
rows: []
|
||||
})
|
||||
expect(trackMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('injects cohort for setup script prompt events', () => {
|
||||
registerWith({ installId: 'x', existedBeforeTelemetryRelease: false, optedIn: true })
|
||||
getCohortAtEmitMock.mockReturnValue({ nth_repo_added: 3 })
|
||||
|
||||
@@ -18,6 +18,9 @@ import { resolveConsent, type ConsentState } from '../telemetry/consent'
|
||||
import type { Store } from '../persistence'
|
||||
import { isCohortExtendedEvent, isOnboardingEvent } from '../../shared/telemetry-events'
|
||||
import type { EventName, EventProps, OptInVia } from '../../shared/telemetry-events'
|
||||
import type { PaneAgentIdentityCensus } from '../telemetry/pane-agent-identity-census'
|
||||
import type { PaneAgentIdentityAvailabilitySnapshot } from '../../shared/pane-agent-identity-availability'
|
||||
import { isPaneAgentIdentityAvailabilitySnapshot } from '../../shared/pane-agent-identity-availability-validator'
|
||||
|
||||
// Module-level store ref: handlers need a synchronous `settings.telemetry` read to derive `via` before any mutation.
|
||||
let storeRef: Store | null = null
|
||||
@@ -25,6 +28,7 @@ let storeRef: Store | null = null
|
||||
const MAIN_OWNED_TELEMETRY_EVENTS = new Set<EventName>([
|
||||
'app_starred_orca',
|
||||
'daemon_audit_eligibility',
|
||||
'pane_agent_identity_availability',
|
||||
'star_nag_outcome',
|
||||
'feature_interaction_usage_bucket_reached'
|
||||
])
|
||||
@@ -49,7 +53,10 @@ function deriveOptInVia(store: Store, incomingOptedIn: boolean): OptInVia {
|
||||
return 'settings'
|
||||
}
|
||||
|
||||
export function registerTelemetryHandlers(store: Store): void {
|
||||
export function registerTelemetryHandlers(
|
||||
store: Store,
|
||||
paneIdentityCensus: PaneAgentIdentityCensus
|
||||
): void {
|
||||
storeRef = store
|
||||
|
||||
ipcMain.handle('telemetry:track', (_event, name: unknown, props: unknown): void => {
|
||||
@@ -78,6 +85,34 @@ export function registerTelemetryHandlers(store: Store): void {
|
||||
track(eventName, finalProps as EventProps<EventName>)
|
||||
})
|
||||
|
||||
ipcMain.handle(
|
||||
'telemetry:ingestPaneAgentIdentityAvailability',
|
||||
(_event, payload: unknown): void => {
|
||||
const environmentKey =
|
||||
payload && typeof payload === 'object'
|
||||
? (payload as { environmentKey?: unknown }).environmentKey
|
||||
: undefined
|
||||
const snapshot =
|
||||
payload && typeof payload === 'object'
|
||||
? (payload as { snapshot?: unknown }).snapshot
|
||||
: undefined
|
||||
if (
|
||||
typeof environmentKey !== 'string' ||
|
||||
environmentKey.length === 0 ||
|
||||
environmentKey.length > 256
|
||||
) {
|
||||
return
|
||||
}
|
||||
if (snapshot !== undefined && !isPaneAgentIdentityAvailabilitySnapshot(snapshot)) {
|
||||
return
|
||||
}
|
||||
paneIdentityCensus.ingestRelaySnapshot(
|
||||
environmentKey,
|
||||
snapshot as PaneAgentIdentityAvailabilitySnapshot | undefined
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
ipcMain.handle('telemetry:setOptIn', (_event, optedIn: unknown): Promise<void> | void => {
|
||||
// Strict input typing — renderer can pass anything over IPC.
|
||||
if (typeof optedIn !== 'boolean') {
|
||||
|
||||
@@ -140,6 +140,8 @@ async function startOrcadRuntime(
|
||||
const { resolveAdvertisedPairingEndpoint } = await import('../runtime/pairing-endpoint')
|
||||
const { ServeReadinessPublisher } = await import('../server/serve-readiness')
|
||||
const { Store } = await import('../persistence/loading-store/store')
|
||||
const { createHeadlessPaneAgentIdentityCensus } =
|
||||
await import('../telemetry/pane-agent-identity-census-composition')
|
||||
const { ensureActiveOrcaProfile, initOrcaProfilePaths } =
|
||||
await import('../orca-profiles/profile-index-store')
|
||||
const { initSshHostKeyStoreFile } = await import('../ssh/ssh-host-key-store')
|
||||
@@ -165,7 +167,9 @@ async function startOrcadRuntime(
|
||||
// registerPtyHandlers so the IPC layer routes through the daemon from the first call.
|
||||
await startOrcadDaemon()
|
||||
|
||||
const paneAgentIdentityCensus = createHeadlessPaneAgentIdentityCensus()
|
||||
const runtime = new OrcaRuntimeService(store, undefined, {
|
||||
paneAgentIdentityCensus,
|
||||
// Why lazy: a daemon swap replaces the provider after construction, so an eager
|
||||
// reference would freeze the pre-daemon one.
|
||||
getLocalProvider: () => getLocalPtyProvider(),
|
||||
@@ -265,6 +269,9 @@ async function startOrcadRuntime(
|
||||
try {
|
||||
await rpc.stop()
|
||||
} finally {
|
||||
// Why the census first: it reads runtime state, so it must settle before the daemon
|
||||
// link goes away.
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
// Why disconnect and not shut down: the daemon must outlive this process, or an
|
||||
// orcad restart goes back to killing every running terminal. See
|
||||
// orcad-daemon-supervision.ts.
|
||||
|
||||
@@ -344,7 +344,6 @@ import type {
|
||||
TerminalPaneLayoutNode,
|
||||
TerminalTab
|
||||
} from '../../shared/terminal-tab-types'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import type { BranchPrefixStrategy } from '../../shared/ui-chrome-types'
|
||||
import type { WorkspaceSessionState } from '../../shared/workspace-session-state-types'
|
||||
import type { WorkspaceSource as WorkspaceCreateTelemetrySource } from '../../shared/workspace-source'
|
||||
@@ -449,6 +448,19 @@ import type {
|
||||
LinearStatusSetResult
|
||||
} from '../../shared/linear/agent-access'
|
||||
import { runtimeTerminalDegradation } from './native-terminal-availability'
|
||||
import {
|
||||
MAX_PENDING_IDENTITY_OBSERVATIONS,
|
||||
TITLE_CANDIDATE_WINDOW_MS
|
||||
} from '../../shared/pane-agent-identity-availability'
|
||||
import type {
|
||||
PaneAgentIdentityAvailability,
|
||||
PaneAgentIdentityHostKind
|
||||
} from '../../shared/pane-agent-identity-evidence'
|
||||
import { ManualAgentCommandTracker } from '../../shared/manual-agent-command-tracker'
|
||||
import { resolveExplicitTerminalTitleAgentType } from '../../shared/terminal-title-agent-type'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import { PaneAgentIdentityCensus } from '../telemetry/pane-agent-identity-census'
|
||||
import { PaneAgentIdentityObservationAuthority } from './pane-agent-identity-observation-authority'
|
||||
import {
|
||||
BROWSER_UNAVAILABLE_ERROR_CODE,
|
||||
browserUnavailableMessage,
|
||||
@@ -3130,6 +3142,21 @@ type ProviderSnapshotReadOptions = {
|
||||
|
||||
export class OrcaRuntimeService {
|
||||
private readonly runtimeId = randomUUID()
|
||||
private readonly paneAgentIdentityCensus: PaneAgentIdentityCensus
|
||||
private readonly paneAgentIdentityObservations: PaneAgentIdentityObservationAuthority
|
||||
private readonly paneAgentCommandTrackers = new Map<string, ManualAgentCommandTracker>()
|
||||
private readonly paneAgentIdentityOwnedIncarnations = new Map<string, PtyIncarnationId | null>()
|
||||
private readonly paneAgentIdentityLiveHookObservedAtByPty = new Map<string, number>()
|
||||
private readonly sshTypedAgentCandidates = new Map<
|
||||
string,
|
||||
{
|
||||
agent: TuiAgent
|
||||
incarnationId: PtyIncarnationId | null
|
||||
lifecycleGeneration: number
|
||||
commandOrdinal: number
|
||||
timer: ReturnType<typeof setTimeout>
|
||||
}
|
||||
>()
|
||||
private readonly startedAt = Date.now()
|
||||
private readonly store: RuntimeStore | null
|
||||
private managedHookReconciliationGeneration = 0
|
||||
@@ -3874,9 +3901,16 @@ export class OrcaRuntimeService {
|
||||
agentSessionClaimSigner?: AgentSessionClaimSigner
|
||||
orchestrationEnvironmentTransport?: OrchestrationEnvironmentTransport
|
||||
skillTransactionRecovery?: Promise<unknown>
|
||||
paneAgentIdentityCensus?: PaneAgentIdentityCensus
|
||||
}
|
||||
) {
|
||||
this.store = store
|
||||
this.paneAgentIdentityCensus =
|
||||
deps?.paneAgentIdentityCensus ?? new PaneAgentIdentityCensus({ emit: null })
|
||||
this.paneAgentIdentityObservations = new PaneAgentIdentityObservationAuthority(
|
||||
this.runtimeId,
|
||||
this.paneAgentIdentityCensus
|
||||
)
|
||||
// Why: per-device tab selections must survive host restarts, or every phone snaps back to the first tab on return.
|
||||
const persistedClientTabSelections = store?.getMobileClientTabSelections?.()
|
||||
if (persistedClientTabSelections) {
|
||||
@@ -11442,7 +11476,11 @@ export class OrcaRuntimeService {
|
||||
tabId: string
|
||||
leafId: string
|
||||
incarnationId?: PtyIncarnationId
|
||||
agentLaunchAuthority?: { launchToken: string; launchAgent: TuiAgent }
|
||||
agentLaunchAuthority?: {
|
||||
launchToken: string
|
||||
launchAgent: TuiAgent
|
||||
launchMode?: 'orca-launch' | 'resume'
|
||||
}
|
||||
},
|
||||
isWsl?: boolean
|
||||
): void {
|
||||
@@ -11480,6 +11518,11 @@ export class OrcaRuntimeService {
|
||||
pty.launchToken = agentLaunchAuthority.launchToken
|
||||
pty.launchIncarnationId = binding.incarnationId
|
||||
pty.launchAgent = agentLaunchAuthority.launchAgent
|
||||
this.observePtyAgentLaunch(
|
||||
pty,
|
||||
agentLaunchAuthority.launchMode ?? 'orca-launch',
|
||||
agentLaunchAuthority.launchToken
|
||||
)
|
||||
}
|
||||
const pendingIncarnation = this.pendingPtyRegistrationIncarnations.get(ptyId)
|
||||
if (
|
||||
@@ -12452,6 +12495,13 @@ export class OrcaRuntimeService {
|
||||
pty.lastOscTitleEpochMs = observedAtEpochMs
|
||||
pty.lastAgentStatus = agentStatus
|
||||
pty.lastAgentStatusObservedLive = true
|
||||
const titleAgent = resolveExplicitTerminalTitleAgentType(rawTitle)
|
||||
if (titleAgent) {
|
||||
this.paneAgentIdentityObservations.observeTitle(
|
||||
this.getPaneAgentIdentityPtyContext(pty),
|
||||
titleAgent
|
||||
)
|
||||
}
|
||||
if (prevStatus !== agentStatus) {
|
||||
pty.lastAgentStatusStartedAtEpochMs = observedAtEpochMs
|
||||
}
|
||||
@@ -12551,7 +12601,13 @@ export class OrcaRuntimeService {
|
||||
private resetTrackedTerminalStateForProviderGeneration(ptyId: string): void {
|
||||
// Why: a replacement daemon session can reuse the PTY id, but title/parser
|
||||
// state from the prior process must not bleed into its snapshots or chunks.
|
||||
const previousPty = this.ptysById.get(ptyId)
|
||||
this.releasePaneAgentIdentityOwner(ptyId, previousPty?.incarnationId)
|
||||
this.paneAgentIdentityObservations.exitOrRebind(ptyId, previousPty?.incarnationId)
|
||||
this.disposePtyTitleTracker(ptyId)
|
||||
this.paneAgentCommandTrackers.delete(ptyId)
|
||||
this.paneAgentIdentityLiveHookObservedAtByPty.delete(ptyId)
|
||||
this.clearSshTypedAgentCandidate(ptyId)
|
||||
this.oscTitleScanTailByPtyId.delete(ptyId)
|
||||
this.osc7ScanTailByPtyId.delete(ptyId)
|
||||
this.agentStatusOscProcessorsByPtyId.delete(ptyId)
|
||||
@@ -12721,6 +12777,19 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
let retainedChanged = false
|
||||
for (const payload of chunk.payloads) {
|
||||
if (pty && isTuiAgent(payload.agentType)) {
|
||||
this.rememberPaneAgentIdentityHookSurface(ptyId, payload.state)
|
||||
if (connectionId && payload.state !== 'done') {
|
||||
this.corroborateSshTypedAgent(ptyId, payload.agentType, 'live-hook')
|
||||
}
|
||||
this.paneAgentIdentityObservations.observeEvidence(
|
||||
this.getPaneAgentIdentityPtyContext(pty),
|
||||
{
|
||||
source: payload.state === 'done' ? 'completed-hook' : 'live-hook',
|
||||
agent: payload.agentType
|
||||
}
|
||||
)
|
||||
}
|
||||
this.recordAgentPromptLifecycleState(
|
||||
ptyId,
|
||||
mapExplicitAgentStateToRuntimeTerminalStatus(payload.state)
|
||||
@@ -14662,6 +14731,8 @@ export class OrcaRuntimeService {
|
||||
|
||||
private retirePtyAgentLaunchAuthority(ptyId: string): void {
|
||||
const pty = this.ptysById.get(ptyId)
|
||||
this.releasePaneAgentIdentityOwner(ptyId, pty?.incarnationId)
|
||||
this.paneAgentIdentityLiveHookObservedAtByPty.delete(ptyId)
|
||||
if (!pty) {
|
||||
return
|
||||
}
|
||||
@@ -15924,6 +15995,8 @@ export class OrcaRuntimeService {
|
||||
if (exitIncarnationId && pty?.incarnationId && exitIncarnationId !== pty.incarnationId) {
|
||||
return
|
||||
}
|
||||
this.paneAgentCommandTrackers.delete(ptyId)
|
||||
this.clearSshTypedAgentCandidate(ptyId)
|
||||
// Why intent first: a requested stop can still be delivered by the provider's
|
||||
// own exit event, whose status looks exactly like a natural finish.
|
||||
//
|
||||
@@ -15944,6 +16017,10 @@ export class OrcaRuntimeService {
|
||||
pty?.connectionId != null &&
|
||||
exitCode < 0 &&
|
||||
options?.hostExitConfirmed !== true
|
||||
if (!preservesAbnormalSshSurface && pty) {
|
||||
this.paneAgentIdentityLiveHookObservedAtByPty.delete(ptyId)
|
||||
this.paneAgentIdentityObservations.exitOrRebind(ptyId, pty.incarnationId)
|
||||
}
|
||||
// Why: collect before retirePtyAgentLaunchAuthority, which deletes the restored-authority
|
||||
// receipt a receipt-only pane's key comes from.
|
||||
const exitPaneKeys = this.collectPaneKeysForPty(ptyId)
|
||||
@@ -18022,6 +18099,7 @@ export class OrcaRuntimeService {
|
||||
|
||||
return {
|
||||
terminals: listedTerminals,
|
||||
agentIdentityAvailability: this.paneAgentIdentityCensus.snapshot(),
|
||||
hostScope: this.buildTerminalListHostScope(
|
||||
targetWorktreeId,
|
||||
matchingTerminals,
|
||||
@@ -18040,6 +18118,269 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
}
|
||||
|
||||
recordPaneAgentIdentityAvailability(observation: PaneAgentIdentityAvailability): void {
|
||||
this.paneAgentIdentityCensus.record(observation)
|
||||
}
|
||||
|
||||
getPaneAgentIdentityCensus(): PaneAgentIdentityCensus {
|
||||
return this.paneAgentIdentityCensus
|
||||
}
|
||||
|
||||
private getPaneAgentIdentityPtyContext(pty: RuntimePtyWorktreeRecord): {
|
||||
ptyId: string
|
||||
incarnationId: PtyIncarnationId | null
|
||||
hostKinds: readonly PaneAgentIdentityHostKind[]
|
||||
} {
|
||||
const hostKinds: readonly PaneAgentIdentityHostKind[] = pty.connectionId
|
||||
? ['ssh']
|
||||
: pty.isWsl || pty.wslDistro || this.wslDistroByPtyId.has(pty.ptyId)
|
||||
? ['wsl-host', 'wsl-distro']
|
||||
: ['native']
|
||||
return {
|
||||
ptyId: pty.ptyId,
|
||||
incarnationId: pty.incarnationId,
|
||||
hostKinds: this.paneAgentIdentityCensus.hostKindsForObservation(hostKinds)
|
||||
}
|
||||
}
|
||||
|
||||
private observePtyAgentLaunch(
|
||||
pty: RuntimePtyWorktreeRecord,
|
||||
launchMode: 'orca-launch' | 'resume',
|
||||
attestationId: string
|
||||
): void {
|
||||
if (!pty.launchAgent) {
|
||||
return
|
||||
}
|
||||
if (
|
||||
this.paneAgentIdentityObservations.attestRun(
|
||||
this.getPaneAgentIdentityPtyContext(pty),
|
||||
launchMode,
|
||||
pty.launchAgent,
|
||||
`${launchMode}:${attestationId}`
|
||||
)
|
||||
) {
|
||||
this.paneAgentIdentityOwnedIncarnations.set(pty.ptyId, pty.incarnationId)
|
||||
}
|
||||
}
|
||||
|
||||
private releasePaneAgentIdentityOwner(
|
||||
ptyId: string,
|
||||
incarnationId?: PtyIncarnationId | null
|
||||
): void {
|
||||
if (
|
||||
!this.paneAgentIdentityOwnedIncarnations.has(ptyId) ||
|
||||
(incarnationId !== undefined &&
|
||||
this.paneAgentIdentityOwnedIncarnations.get(ptyId) !== incarnationId)
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.paneAgentIdentityOwnedIncarnations.delete(ptyId)
|
||||
this.paneAgentCommandTrackers.get(ptyId)?.reset()
|
||||
}
|
||||
|
||||
observeAgentHookStatus(args: {
|
||||
paneKey: string
|
||||
connectionId?: string | null
|
||||
isReplay?: boolean
|
||||
restoredUnconfirmed?: boolean
|
||||
providerSessionOnly?: boolean
|
||||
payload: { state: string; agentType?: string }
|
||||
}): boolean {
|
||||
if (
|
||||
args.isReplay === true ||
|
||||
args.restoredUnconfirmed === true ||
|
||||
args.providerSessionOnly === true ||
|
||||
!isTuiAgent(args.payload.agentType)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
const pty = this.getPtyRecordForPaneKey(args.paneKey)
|
||||
const isWslRelay = isWslHookRelayConnectionId(args.connectionId)
|
||||
const hookOwnerConnectionId = isWslRelay ? null : (args.connectionId ?? null)
|
||||
const isWslPty =
|
||||
pty?.isWsl === true ||
|
||||
!!pty?.wslDistro ||
|
||||
(pty ? this.wslDistroByPtyId.has(pty.ptyId) : false)
|
||||
if (
|
||||
!pty?.connected ||
|
||||
(pty.connectionId ?? null) !== hookOwnerConnectionId ||
|
||||
(isWslRelay && !isWslPty)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (pty.connectionId && args.payload.state !== 'done') {
|
||||
this.corroborateSshTypedAgent(pty.ptyId, args.payload.agentType, 'live-hook')
|
||||
}
|
||||
this.rememberPaneAgentIdentityHookSurface(pty.ptyId, args.payload.state)
|
||||
return this.paneAgentIdentityObservations.observeEvidence(
|
||||
this.getPaneAgentIdentityPtyContext(pty),
|
||||
{
|
||||
source: args.payload.state === 'done' ? 'completed-hook' : 'live-hook',
|
||||
agent: args.payload.agentType
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
observeAcceptedPtyWrite(ptyId: string, data: string): void {
|
||||
const pty = this.ptysById.get(ptyId)
|
||||
if (!pty?.connected) {
|
||||
return
|
||||
}
|
||||
if (this.paneAgentIdentityOwnedIncarnations.get(ptyId) === pty.incarnationId) {
|
||||
this.paneAgentCommandTrackers.get(ptyId)?.reset()
|
||||
return
|
||||
}
|
||||
if (this.hasFreshPaneAgentIdentitySurface(pty)) {
|
||||
this.paneAgentCommandTrackers.get(ptyId)?.reset()
|
||||
return
|
||||
}
|
||||
const tracker = this.paneAgentCommandTrackers.get(ptyId) ?? new ManualAgentCommandTracker()
|
||||
this.paneAgentCommandTrackers.set(ptyId, tracker)
|
||||
const [agent] = tracker.ingest(data)
|
||||
if (!agent) {
|
||||
return
|
||||
}
|
||||
if (pty.connectionId) {
|
||||
this.rememberSshTypedAgentCandidate(ptyId, agent, pty, tracker.acceptedCommandCount)
|
||||
return
|
||||
}
|
||||
const context = this.getPaneAgentIdentityPtyContext(pty)
|
||||
if (
|
||||
this.paneAgentIdentityObservations.attestRun(
|
||||
context,
|
||||
'typed',
|
||||
agent,
|
||||
`typed:${this.getPtyLifecycleGeneration(ptyId)}:${tracker.acceptedCommandCount}`
|
||||
)
|
||||
) {
|
||||
this.paneAgentIdentityOwnedIncarnations.set(ptyId, pty.incarnationId)
|
||||
}
|
||||
}
|
||||
|
||||
private rememberSshTypedAgentCandidate(
|
||||
ptyId: string,
|
||||
agent: TuiAgent,
|
||||
pty: RuntimePtyWorktreeRecord,
|
||||
commandOrdinal: number
|
||||
): void {
|
||||
const previous = this.sshTypedAgentCandidates.get(ptyId)
|
||||
if (!previous && this.sshTypedAgentCandidates.size >= MAX_PENDING_IDENTITY_OBSERVATIONS) {
|
||||
this.paneAgentIdentityCensus.addCoverage('ssh', 'overflow')
|
||||
return
|
||||
}
|
||||
if (previous) {
|
||||
clearTimeout(previous.timer)
|
||||
}
|
||||
const timer = setTimeout(
|
||||
() => this.clearSshTypedAgentCandidate(ptyId),
|
||||
TITLE_CANDIDATE_WINDOW_MS
|
||||
)
|
||||
timer.unref?.()
|
||||
this.sshTypedAgentCandidates.set(ptyId, {
|
||||
agent,
|
||||
incarnationId: pty.incarnationId,
|
||||
lifecycleGeneration: this.getPtyLifecycleGeneration(ptyId),
|
||||
commandOrdinal,
|
||||
timer
|
||||
})
|
||||
}
|
||||
|
||||
private rememberPaneAgentIdentityHookSurface(ptyId: string, state: string): void {
|
||||
if (state === 'done') {
|
||||
this.paneAgentIdentityLiveHookObservedAtByPty.delete(ptyId)
|
||||
return
|
||||
}
|
||||
this.paneAgentIdentityLiveHookObservedAtByPty.set(ptyId, Date.now())
|
||||
}
|
||||
|
||||
private hasFreshPaneAgentIdentitySurface(pty: RuntimePtyWorktreeRecord): boolean {
|
||||
if (pty.foregroundAgent || (pty.lastAgentStatusObservedLive && pty.lastAgentStatus !== null)) {
|
||||
return true
|
||||
}
|
||||
const hookObservedAt = this.paneAgentIdentityLiveHookObservedAtByPty.get(pty.ptyId)
|
||||
if (hookObservedAt === undefined) {
|
||||
return false
|
||||
}
|
||||
if (Date.now() - hookObservedAt <= AGENT_STATUS_STALE_AFTER_MS) {
|
||||
return true
|
||||
}
|
||||
this.paneAgentIdentityLiveHookObservedAtByPty.delete(pty.ptyId)
|
||||
return false
|
||||
}
|
||||
|
||||
private clearSshTypedAgentCandidate(ptyId: string): void {
|
||||
const candidate = this.sshTypedAgentCandidates.get(ptyId)
|
||||
if (candidate) {
|
||||
clearTimeout(candidate.timer)
|
||||
}
|
||||
this.sshTypedAgentCandidates.delete(ptyId)
|
||||
}
|
||||
|
||||
corroborateSshTypedAgent(
|
||||
ptyId: string,
|
||||
targetAgent: TuiAgent,
|
||||
source: 'process' | 'live-hook' = 'process'
|
||||
): boolean {
|
||||
const candidate = this.sshTypedAgentCandidates.get(ptyId)
|
||||
const pty = this.ptysById.get(ptyId)
|
||||
if (
|
||||
candidate?.agent !== targetAgent ||
|
||||
!pty?.connected ||
|
||||
!pty.connectionId ||
|
||||
candidate.incarnationId !== pty.incarnationId ||
|
||||
candidate.lifecycleGeneration !== this.getPtyLifecycleGeneration(ptyId)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
this.clearSshTypedAgentCandidate(ptyId)
|
||||
const context = this.getPaneAgentIdentityPtyContext(pty)
|
||||
if (
|
||||
!this.paneAgentIdentityObservations.attestRun(
|
||||
context,
|
||||
'typed',
|
||||
targetAgent,
|
||||
`typed:${candidate.lifecycleGeneration}:${candidate.commandOrdinal}`
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
this.paneAgentIdentityOwnedIncarnations.set(ptyId, pty.incarnationId)
|
||||
if (source === 'process') {
|
||||
this.paneAgentIdentityObservations.observeEvidence(context, {
|
||||
source,
|
||||
agent: targetAgent,
|
||||
processProvenance: 'target-origin'
|
||||
})
|
||||
} else {
|
||||
this.paneAgentIdentityObservations.observeEvidence(context, {
|
||||
source,
|
||||
agent: targetAgent
|
||||
})
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
shutdownPaneAgentIdentityCensus(emitTelemetry = true): void {
|
||||
if (this.sshTypedAgentCandidates.size > 0) {
|
||||
this.paneAgentIdentityCensus.addCoverage(
|
||||
'ssh',
|
||||
'truncated',
|
||||
this.sshTypedAgentCandidates.size
|
||||
)
|
||||
}
|
||||
this.paneAgentIdentityObservations.shutdown()
|
||||
if (emitTelemetry) {
|
||||
this.paneAgentIdentityCensus.flush()
|
||||
}
|
||||
this.paneAgentIdentityCensus.shutdown()
|
||||
this.paneAgentCommandTrackers.clear()
|
||||
this.paneAgentIdentityOwnedIncarnations.clear()
|
||||
this.paneAgentIdentityLiveHookObservedAtByPty.clear()
|
||||
for (const ptyId of this.sshTypedAgentCandidates.keys()) {
|
||||
this.clearSshTypedAgentCandidate(ptyId)
|
||||
}
|
||||
}
|
||||
|
||||
// A worktree-scoped request answers for one host only, so name the hosts it
|
||||
// skipped: absence from a scoped listing is not evidence a worker exited.
|
||||
private buildTerminalListHostScope(
|
||||
@@ -19362,6 +19703,7 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
await assertTerminalInputWithinLimitWithYield(action.text)
|
||||
await this.writeTerminalAction(pty.pty.ptyId, action, payload, options)
|
||||
this.observeAcceptedPtyWrite(pty.pty.ptyId, payload)
|
||||
return {
|
||||
handle,
|
||||
accepted: true,
|
||||
@@ -19387,6 +19729,7 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
|
||||
await this.writeTerminalAction(leaf.ptyId, action, payload, options)
|
||||
this.observeAcceptedPtyWrite(leaf.ptyId, payload)
|
||||
|
||||
return {
|
||||
handle,
|
||||
@@ -19861,6 +20204,7 @@ export class OrcaRuntimeService {
|
||||
}
|
||||
return
|
||||
}
|
||||
this.releasePaneAgentIdentityOwner(ptyId, current.incarnationId)
|
||||
this.recordTerminalSideEffectFact(ptyId, { kind: 'agent-exited' })
|
||||
})
|
||||
}
|
||||
@@ -19984,9 +20328,24 @@ export class OrcaRuntimeService {
|
||||
const foregroundAgent = foregroundProcess
|
||||
? (recognizeAgentProcess(foregroundProcess)?.agent ?? null)
|
||||
: null
|
||||
const isWsl = pty.isWsl || pty.wslDistro || this.wslDistroByPtyId.has(ptyId)
|
||||
if (foregroundAgent && !isWsl) {
|
||||
const processProvenance = pty.connectionId ? 'target-origin' : 'execution-host'
|
||||
this.paneAgentIdentityObservations.observeEvidence(this.getPaneAgentIdentityPtyContext(pty), {
|
||||
source: 'process',
|
||||
agent: foregroundAgent,
|
||||
processProvenance
|
||||
})
|
||||
if (pty.connectionId) {
|
||||
this.corroborateSshTypedAgent(ptyId, foregroundAgent, 'process')
|
||||
}
|
||||
}
|
||||
if (pty.foregroundAgent === foregroundAgent) {
|
||||
return false
|
||||
}
|
||||
if (pty.foregroundAgent && !foregroundAgent) {
|
||||
this.releasePaneAgentIdentityOwner(ptyId, pty.incarnationId)
|
||||
}
|
||||
pty.foregroundAgent = foregroundAgent
|
||||
this.touchMobileSessionSnapshotsForPty(ptyId)
|
||||
return true
|
||||
@@ -29183,6 +29542,15 @@ export class OrcaRuntimeService {
|
||||
pty.launchToken = launchToken ?? null
|
||||
pty.launchIncarnationId = launchToken ? pty.incarnationId : null
|
||||
pty.launchAgent = launchOpts.launchAgent ?? null
|
||||
if (pty.launchAgent && launchToken) {
|
||||
this.observePtyAgentLaunch(
|
||||
pty,
|
||||
launchOpts.resumeProviderSession || launchOpts.agentSessionClaim
|
||||
? 'resume'
|
||||
: 'orca-launch',
|
||||
launchToken
|
||||
)
|
||||
}
|
||||
}
|
||||
pty.tabId = tabId
|
||||
pty.paneKey = paneKey
|
||||
@@ -33349,6 +33717,11 @@ export class OrcaRuntimeService {
|
||||
pty.worktreeId = worktreeId
|
||||
if (state.incarnationId !== undefined) {
|
||||
if (pty.incarnationId && state.incarnationId && pty.incarnationId !== state.incarnationId) {
|
||||
this.releasePaneAgentIdentityOwner(ptyId, pty.incarnationId)
|
||||
this.paneAgentCommandTrackers.delete(ptyId)
|
||||
this.paneAgentIdentityLiveHookObservedAtByPty.delete(ptyId)
|
||||
this.clearSshTypedAgentCandidate(ptyId)
|
||||
this.paneAgentIdentityObservations.exitOrRebind(ptyId, pty.incarnationId)
|
||||
this.invalidatePtyIncarnationHandle(ptyId)
|
||||
}
|
||||
pty.incarnationId = state.incarnationId
|
||||
@@ -33839,7 +34212,13 @@ export class OrcaRuntimeService {
|
||||
|
||||
private dropDisconnectedPtyRecord(ptyId: string): void {
|
||||
// Why: pruning can remove a PTY without the normal exit callback.
|
||||
const pty = this.ptysById.get(ptyId)
|
||||
this.releasePaneAgentIdentityOwner(ptyId, pty?.incarnationId)
|
||||
this.paneAgentIdentityObservations.exitOrRebind(ptyId, pty?.incarnationId)
|
||||
this.advancePtyLifecycleGeneration(ptyId)
|
||||
this.paneAgentCommandTrackers.delete(ptyId)
|
||||
this.paneAgentIdentityLiveHookObservedAtByPty.delete(ptyId)
|
||||
this.clearSshTypedAgentCandidate(ptyId)
|
||||
this.pairedRendererSessionOwnedPtyIds.delete(ptyId)
|
||||
this.ptysById.delete(ptyId)
|
||||
this.recentPtyOutputById.delete(ptyId)
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
import { track } from '../telemetry/client'
|
||||
import { PaneAgentIdentityCensus } from '../telemetry/pane-agent-identity-census'
|
||||
|
||||
vi.mock('../telemetry/client', () => ({ track: vi.fn() }))
|
||||
|
||||
async function availabilityRows(runtime: OrcaRuntimeService) {
|
||||
return ((await runtime.listTerminals()).agentIdentityAvailability?.rows ?? []).map((row) => ({
|
||||
hostKind: row[0],
|
||||
launchMode: row[1],
|
||||
attestedRuns: row[2],
|
||||
noEvidence: row[3],
|
||||
titleOnly: row[4],
|
||||
identityNull: row[5],
|
||||
ambiguousTopRank: row[6]
|
||||
}))
|
||||
}
|
||||
|
||||
describe('runtime pane identity census wiring', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('publishes paired WSL typed views without a false native row', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.mocked(track).mockClear()
|
||||
const runtime = new OrcaRuntimeService(undefined, undefined, {
|
||||
paneAgentIdentityCensus: new PaneAgentIdentityCensus()
|
||||
})
|
||||
runtime.registerPty('pty-wsl', 'folder:/tmp', null, undefined, true)
|
||||
|
||||
runtime.observeAcceptedPtyWrite('pty-wsl', 'codex\r')
|
||||
vi.advanceTimersByTime(5_000)
|
||||
|
||||
expect(await availabilityRows(runtime)).toEqual([
|
||||
expect.objectContaining({ hostKind: 'wsl-host', launchMode: 'typed', attestedRuns: 1 }),
|
||||
expect.objectContaining({ hostKind: 'wsl-distro', launchMode: 'typed', attestedRuns: 1 })
|
||||
])
|
||||
runtime.shutdownPaneAgentIdentityCensus()
|
||||
expect(track).toHaveBeenCalledTimes(1)
|
||||
expect(track).toHaveBeenCalledWith('pane_agent_identity_availability', {
|
||||
rows: expect.arrayContaining([
|
||||
expect.objectContaining({ host_kind: 'wsl-host', attested_runs: 1 }),
|
||||
expect.objectContaining({ host_kind: 'wsl-distro', attested_runs: 1 })
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
it('does not relabel a Windows-side WSL foreground read as distro process provenance', async () => {
|
||||
vi.useFakeTimers()
|
||||
const census = new PaneAgentIdentityCensus({ emit: null })
|
||||
const record = vi.spyOn(census, 'record')
|
||||
const runtime = new OrcaRuntimeService(undefined, undefined, {
|
||||
paneAgentIdentityCensus: census
|
||||
})
|
||||
runtime.registerPty('pty-wsl', 'folder:/tmp', null, undefined, true)
|
||||
runtime.setPtyController({ getForegroundProcess: async () => 'codex' } as never)
|
||||
runtime.observeAcceptedPtyWrite('pty-wsl', 'codex\r')
|
||||
await (
|
||||
runtime as unknown as {
|
||||
loadPtyForegroundAgentFromController: (ptyId: string) => Promise<boolean>
|
||||
}
|
||||
).loadPtyForegroundAgentFromController('pty-wsl')
|
||||
vi.advanceTimersByTime(5_000)
|
||||
|
||||
expect(record).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.objectContaining({ hostKind: 'wsl-host', sourceMask: 4 })
|
||||
)
|
||||
expect(record).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.objectContaining({ hostKind: 'wsl-distro', sourceMask: 4 })
|
||||
)
|
||||
expect(census.snapshot().rows).toEqual([
|
||||
['wsl-host', 'typed', 1, 0, 0, 0, 0],
|
||||
['wsl-distro', 'typed', 1, 0, 0, 0, 0]
|
||||
])
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
|
||||
it('requires same-incarnation SSH target corroboration', async () => {
|
||||
vi.useFakeTimers()
|
||||
const runtime = new OrcaRuntimeService()
|
||||
const binding = {
|
||||
tabId: '00000000-0000-4000-8000-000000000001',
|
||||
leafId: '00000000-0000-4000-8000-000000000002',
|
||||
incarnationId: 'incarnation-1'
|
||||
}
|
||||
runtime.registerPty('pty-ssh', 'folder:/tmp', 'ssh-1', binding)
|
||||
runtime.observeAcceptedPtyWrite('pty-ssh', 'codex\r')
|
||||
|
||||
expect(runtime.corroborateSshTypedAgent('pty-ssh', 'claude')).toBe(false)
|
||||
expect(await availabilityRows(runtime)).toEqual([])
|
||||
|
||||
runtime.registerPty('pty-ssh', 'folder:/tmp', 'ssh-1', {
|
||||
...binding,
|
||||
incarnationId: 'incarnation-2'
|
||||
})
|
||||
expect(runtime.corroborateSshTypedAgent('pty-ssh', 'codex')).toBe(false)
|
||||
expect(await availabilityRows(runtime)).toEqual([])
|
||||
|
||||
runtime.observeAcceptedPtyWrite('pty-ssh', 'codex\r')
|
||||
runtime.onPtyData(
|
||||
'pty-ssh',
|
||||
'\x1b]9999;{"state":"working","agentType":"codex"}\x07',
|
||||
Date.now()
|
||||
)
|
||||
vi.advanceTimersByTime(5_000)
|
||||
expect(await availabilityRows(runtime)).toEqual([
|
||||
expect.objectContaining({ hostKind: 'ssh', launchMode: 'typed', attestedRuns: 1 })
|
||||
])
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
|
||||
it('does not let a replayed SSH hook certify a typed run', async () => {
|
||||
vi.useFakeTimers()
|
||||
const runtime = new OrcaRuntimeService()
|
||||
const tabId = '00000000-0000-4000-8000-000000000001'
|
||||
const leafId = '00000000-0000-4000-8000-000000000002'
|
||||
runtime.registerPty('pty-ssh', 'folder:/tmp', 'ssh-1', {
|
||||
tabId,
|
||||
leafId,
|
||||
incarnationId: 'incarnation-1'
|
||||
})
|
||||
runtime.observeAcceptedPtyWrite('pty-ssh', 'codex\r')
|
||||
|
||||
expect(
|
||||
runtime.observeAgentHookStatus({
|
||||
paneKey: `${tabId}:${leafId}`,
|
||||
connectionId: 'ssh-1',
|
||||
isReplay: true,
|
||||
payload: { state: 'working', agentType: 'codex' }
|
||||
})
|
||||
).toBe(false)
|
||||
vi.advanceTimersByTime(5_000)
|
||||
expect(await availabilityRows(runtime)).toEqual([])
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
|
||||
it('accepts live WSL-relayed hook evidence only for its local WSL pane', async () => {
|
||||
vi.useFakeTimers()
|
||||
const census = new PaneAgentIdentityCensus({ emit: null })
|
||||
const record = vi.spyOn(census, 'record')
|
||||
const runtime = new OrcaRuntimeService(undefined, undefined, {
|
||||
paneAgentIdentityCensus: census
|
||||
})
|
||||
const tabId = '00000000-0000-4000-8000-000000000001'
|
||||
const wslLeafId = '00000000-0000-4000-8000-000000000002'
|
||||
const nativeLeafId = '00000000-0000-4000-8000-000000000003'
|
||||
runtime.registerPty(
|
||||
'pty-wsl',
|
||||
'folder:/tmp',
|
||||
null,
|
||||
{
|
||||
tabId,
|
||||
leafId: wslLeafId,
|
||||
incarnationId: 'incarnation-1'
|
||||
},
|
||||
true
|
||||
)
|
||||
runtime.registerPty('pty-native', 'folder:/tmp', null, {
|
||||
tabId,
|
||||
leafId: nativeLeafId,
|
||||
incarnationId: 'incarnation-1'
|
||||
})
|
||||
runtime.observeAcceptedPtyWrite('pty-wsl', 'codex\r')
|
||||
runtime.observeAcceptedPtyWrite('pty-native', 'codex\r')
|
||||
|
||||
expect(
|
||||
runtime.observeAgentHookStatus({
|
||||
paneKey: `${tabId}:${nativeLeafId}`,
|
||||
connectionId: 'wsl:LiveDistro',
|
||||
payload: { state: 'working', agentType: 'codex' }
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
runtime.observeAgentHookStatus({
|
||||
paneKey: `${tabId}:${wslLeafId}`,
|
||||
connectionId: 'wsl:LiveDistro',
|
||||
payload: { state: 'working', agentType: 'codex' }
|
||||
})
|
||||
).toBe(true)
|
||||
vi.advanceTimersByTime(5_000)
|
||||
|
||||
expect(record).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ hostKind: 'wsl-distro', sourceMask: 5 })
|
||||
)
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
|
||||
it('records launch and resume through renderer registration authority', async () => {
|
||||
vi.useFakeTimers()
|
||||
const runtime = new OrcaRuntimeService()
|
||||
const binding = {
|
||||
tabId: '00000000-0000-4000-8000-000000000001',
|
||||
leafId: '00000000-0000-4000-8000-000000000002',
|
||||
incarnationId: 'incarnation-1'
|
||||
}
|
||||
runtime.registerPty('pty-launch', 'folder:/tmp', null, {
|
||||
...binding,
|
||||
agentLaunchAuthority: {
|
||||
launchToken: 'launch-token',
|
||||
launchAgent: 'codex',
|
||||
launchMode: 'orca-launch'
|
||||
}
|
||||
})
|
||||
runtime.registerPty('pty-resume', 'folder:/tmp', null, {
|
||||
...binding,
|
||||
leafId: '00000000-0000-4000-8000-000000000003',
|
||||
agentLaunchAuthority: {
|
||||
launchToken: 'resume-token',
|
||||
launchAgent: 'claude',
|
||||
launchMode: 'resume'
|
||||
}
|
||||
})
|
||||
vi.advanceTimersByTime(5_000)
|
||||
|
||||
expect(await availabilityRows(runtime)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ hostKind: 'native', launchMode: 'orca-launch', attestedRuns: 1 }),
|
||||
expect.objectContaining({ hostKind: 'native', launchMode: 'resume', attestedRuns: 1 })
|
||||
])
|
||||
)
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
|
||||
it('does not guess a launch mode for an unstructured legacy terminal create', async () => {
|
||||
vi.useFakeTimers()
|
||||
const runtime = new OrcaRuntimeService()
|
||||
vi.spyOn(
|
||||
runtime as unknown as {
|
||||
resolveTerminalWorkspaceLaunchScope: (selector: string) => Promise<unknown>
|
||||
},
|
||||
'resolveTerminalWorkspaceLaunchScope'
|
||||
).mockResolvedValue({
|
||||
id: 'folder:/tmp',
|
||||
path: '/tmp',
|
||||
connectionId: null,
|
||||
repo: null,
|
||||
folderWorkspace: null
|
||||
})
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn().mockResolvedValue({ id: 'pty-legacy', incarnationId: 'incarnation-1' }),
|
||||
write: () => true,
|
||||
kill: () => true,
|
||||
getForegroundProcess: async () => null
|
||||
})
|
||||
|
||||
await runtime.createTerminal('id:folder:/tmp', {
|
||||
command: 'codex resume provider-session-1',
|
||||
launchAgent: 'codex'
|
||||
})
|
||||
vi.advanceTimersByTime(5_000)
|
||||
|
||||
expect(await availabilityRows(runtime)).toEqual([])
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
|
||||
it('does not classify live-agent prompt text as a typed launch', async () => {
|
||||
vi.useFakeTimers()
|
||||
const runtime = new OrcaRuntimeService()
|
||||
const binding = {
|
||||
tabId: '00000000-0000-4000-8000-000000000001',
|
||||
leafId: '00000000-0000-4000-8000-000000000002',
|
||||
incarnationId: 'incarnation-1',
|
||||
agentLaunchAuthority: {
|
||||
launchToken: 'launch-token',
|
||||
launchAgent: 'codex' as const,
|
||||
launchMode: 'orca-launch' as const
|
||||
}
|
||||
}
|
||||
runtime.registerPty('pty-native', 'folder:/tmp', null, binding)
|
||||
runtime.observeAcceptedPtyWrite('pty-native', 'claude\r')
|
||||
vi.advanceTimersByTime(5_000)
|
||||
|
||||
expect(await availabilityRows(runtime)).toEqual([
|
||||
expect.objectContaining({ hostKind: 'native', launchMode: 'orca-launch', attestedRuns: 1 })
|
||||
])
|
||||
|
||||
runtime.emitDaemonPtyTransientFact('pty-native', {
|
||||
kind: 'command-finished',
|
||||
exitCode: 0
|
||||
})
|
||||
runtime.observeAcceptedPtyWrite('pty-native', 'claude\r')
|
||||
vi.advanceTimersByTime(5_000)
|
||||
expect(await availabilityRows(runtime)).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ launchMode: 'orca-launch', attestedRuns: 1 }),
|
||||
expect.objectContaining({ launchMode: 'typed', attestedRuns: 1 })
|
||||
])
|
||||
)
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
|
||||
it('does not classify prompt text while a fresh hook owns an otherwise unattributed pane', async () => {
|
||||
vi.useFakeTimers()
|
||||
const runtime = new OrcaRuntimeService()
|
||||
const tabId = '00000000-0000-4000-8000-000000000001'
|
||||
const leafId = '00000000-0000-4000-8000-000000000002'
|
||||
runtime.registerPty('pty-native', 'folder:/tmp', null, {
|
||||
tabId,
|
||||
leafId,
|
||||
incarnationId: 'incarnation-1'
|
||||
})
|
||||
|
||||
runtime.observeAgentHookStatus({
|
||||
paneKey: `${tabId}:${leafId}`,
|
||||
payload: { state: 'working', agentType: 'codex' }
|
||||
})
|
||||
runtime.observeAcceptedPtyWrite('pty-native', 'claude\r')
|
||||
vi.advanceTimersByTime(5_000)
|
||||
|
||||
expect(await availabilityRows(runtime)).toEqual([])
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
|
||||
it('does not carry a partial typed command across a PTY incarnation rebind', async () => {
|
||||
vi.useFakeTimers()
|
||||
const runtime = new OrcaRuntimeService()
|
||||
const binding = {
|
||||
tabId: '00000000-0000-4000-8000-000000000001',
|
||||
leafId: '00000000-0000-4000-8000-000000000002'
|
||||
}
|
||||
runtime.registerPty('pty-native', 'folder:/tmp', null, {
|
||||
...binding,
|
||||
incarnationId: 'incarnation-1'
|
||||
})
|
||||
runtime.observeAcceptedPtyWrite('pty-native', 'co')
|
||||
runtime.registerPty('pty-native', 'folder:/tmp', null, {
|
||||
...binding,
|
||||
incarnationId: 'incarnation-2'
|
||||
})
|
||||
runtime.observeAcceptedPtyWrite('pty-native', 'dex\r')
|
||||
vi.advanceTimersByTime(5_000)
|
||||
expect(await availabilityRows(runtime)).toEqual([])
|
||||
|
||||
runtime.observeAcceptedPtyWrite('pty-native', 'codex\r')
|
||||
vi.advanceTimersByTime(5_000)
|
||||
expect(await availabilityRows(runtime)).toEqual([
|
||||
expect.objectContaining({ hostKind: 'native', launchMode: 'typed', attestedRuns: 1 })
|
||||
])
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
|
||||
it('attests only the first agent command in one accepted multiline write', async () => {
|
||||
vi.useFakeTimers()
|
||||
const runtime = new OrcaRuntimeService()
|
||||
const tabId = '00000000-0000-4000-8000-000000000001'
|
||||
const leafId = '00000000-0000-4000-8000-000000000002'
|
||||
runtime.registerPty('pty-ssh', 'folder:/tmp', 'ssh-1', {
|
||||
tabId,
|
||||
leafId,
|
||||
incarnationId: 'incarnation-1'
|
||||
})
|
||||
|
||||
runtime.observeAcceptedPtyWrite('pty-ssh', 'claude\rcodex\r')
|
||||
runtime.observeAgentHookStatus({
|
||||
paneKey: `${tabId}:${leafId}`,
|
||||
connectionId: 'ssh-1',
|
||||
payload: { state: 'working', agentType: 'claude' }
|
||||
})
|
||||
vi.advanceTimersByTime(5_000)
|
||||
|
||||
expect(await availabilityRows(runtime)).toEqual([
|
||||
expect.objectContaining({ hostKind: 'ssh', launchMode: 'typed', attestedRuns: 1 })
|
||||
])
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
|
||||
it('collects primary hook-server evidence for the exact live pane', async () => {
|
||||
vi.useFakeTimers()
|
||||
const census = new PaneAgentIdentityCensus({ emit: null })
|
||||
const record = vi.spyOn(census, 'record')
|
||||
const runtime = new OrcaRuntimeService(undefined, undefined, {
|
||||
paneAgentIdentityCensus: census
|
||||
})
|
||||
const tabId = '00000000-0000-4000-8000-000000000001'
|
||||
const leafId = '00000000-0000-4000-8000-000000000002'
|
||||
runtime.registerPty('pty-native', 'folder:/tmp', null, {
|
||||
tabId,
|
||||
leafId,
|
||||
incarnationId: 'incarnation-1',
|
||||
agentLaunchAuthority: {
|
||||
launchToken: 'launch-token',
|
||||
launchAgent: 'codex',
|
||||
launchMode: 'orca-launch'
|
||||
}
|
||||
})
|
||||
|
||||
expect(
|
||||
runtime.observeAgentHookStatus({
|
||||
paneKey: `${tabId}:${leafId}`,
|
||||
connectionId: 'wrong-host',
|
||||
payload: { state: 'working', agentType: 'codex' }
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
runtime.observeAgentHookStatus({
|
||||
paneKey: `${tabId}:${leafId}`,
|
||||
payload: { state: 'working', agentType: 'codex' }
|
||||
})
|
||||
).toBe(true)
|
||||
vi.advanceTimersByTime(5_000)
|
||||
|
||||
expect(record).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ hostKind: 'native', sourceMask: 5 })
|
||||
)
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
|
||||
it('normalizes headless authority observations to one relay row per run', async () => {
|
||||
vi.useFakeTimers()
|
||||
const census = new PaneAgentIdentityCensus({ emit: null, snapshotHostKind: 'relay' })
|
||||
const runtime = new OrcaRuntimeService(undefined, undefined, {
|
||||
paneAgentIdentityCensus: census
|
||||
})
|
||||
runtime.registerPty('pty-wsl', 'folder:/tmp', null, undefined, true)
|
||||
runtime.observeAcceptedPtyWrite('pty-wsl', 'codex\r')
|
||||
vi.advanceTimersByTime(5_000)
|
||||
|
||||
expect(await availabilityRows(runtime)).toEqual([
|
||||
expect.objectContaining({ hostKind: 'relay', launchMode: 'typed', attestedRuns: 1 })
|
||||
])
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
|
||||
it('collects current process, hook, and title evidence through PTY production paths', async () => {
|
||||
vi.useFakeTimers()
|
||||
const census = new PaneAgentIdentityCensus({ emit: null })
|
||||
const record = vi.spyOn(census, 'record')
|
||||
const runtime = new OrcaRuntimeService(undefined, undefined, {
|
||||
paneAgentIdentityCensus: census
|
||||
})
|
||||
runtime.registerPty('pty-native', 'folder:/tmp', null, {
|
||||
tabId: '00000000-0000-4000-8000-000000000001',
|
||||
leafId: '00000000-0000-4000-8000-000000000002',
|
||||
incarnationId: 'incarnation-1'
|
||||
})
|
||||
runtime.setPtyController({ getForegroundProcess: async () => 'codex' } as never)
|
||||
runtime.observeAcceptedPtyWrite('pty-native', 'codex\r')
|
||||
await (
|
||||
runtime as unknown as {
|
||||
loadPtyForegroundAgentFromController: (ptyId: string) => Promise<boolean>
|
||||
}
|
||||
).loadPtyForegroundAgentFromController('pty-native')
|
||||
runtime.onPtyData(
|
||||
'pty-native',
|
||||
'\x1b]9999;{"state":"working","agentType":"codex"}\x07' +
|
||||
'\x1b]9999;{"state":"done","agentType":"codex"}\x07' +
|
||||
'\x1b]0;Codex working\x07',
|
||||
Date.now()
|
||||
)
|
||||
vi.advanceTimersByTime(5_000)
|
||||
|
||||
expect(record).toHaveBeenCalledWith({
|
||||
hostKind: 'native',
|
||||
launchMode: 'typed',
|
||||
sourceMask: 79,
|
||||
identityNull: false,
|
||||
ambiguousTopRank: false
|
||||
})
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
|
||||
it('freezes an accepted run on a confirmed production exit', async () => {
|
||||
vi.useFakeTimers()
|
||||
const runtime = new OrcaRuntimeService()
|
||||
runtime.registerPty('pty-native', 'folder:/tmp', null, {
|
||||
tabId: '00000000-0000-4000-8000-000000000001',
|
||||
leafId: '00000000-0000-4000-8000-000000000002',
|
||||
incarnationId: 'incarnation-1'
|
||||
})
|
||||
runtime.observeAcceptedPtyWrite('pty-native', 'codex\r')
|
||||
runtime.onPtyExit('pty-native', 0, 'incarnation-1', { providerExitObserved: true })
|
||||
|
||||
expect(await availabilityRows(runtime)).toEqual([
|
||||
expect.objectContaining({ hostKind: 'native', launchMode: 'typed', attestedRuns: 1 })
|
||||
])
|
||||
vi.advanceTimersByTime(5_000)
|
||||
expect(await availabilityRows(runtime)).toHaveLength(1)
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
|
||||
it('publishes title-candidate coverage and freezes a run on production rebind', async () => {
|
||||
vi.useFakeTimers()
|
||||
const runtime = new OrcaRuntimeService()
|
||||
const binding = {
|
||||
tabId: '00000000-0000-4000-8000-000000000001',
|
||||
leafId: '00000000-0000-4000-8000-000000000002',
|
||||
incarnationId: 'incarnation-1'
|
||||
}
|
||||
runtime.registerPty('pty-title', 'folder:/tmp', null, binding)
|
||||
runtime.onPtyData('pty-title', '\x1b]0;Codex working\x07', Date.now())
|
||||
vi.advanceTimersByTime(30_000)
|
||||
expect((await runtime.listTerminals()).agentIdentityAvailability?.candidateCoverage).toEqual([
|
||||
['native', 1]
|
||||
])
|
||||
|
||||
runtime.registerPty('pty-rebind', 'folder:/tmp', null, binding)
|
||||
runtime.observeAcceptedPtyWrite('pty-rebind', 'codex\r')
|
||||
runtime.registerPty('pty-rebind', 'folder:/tmp', null, {
|
||||
...binding,
|
||||
incarnationId: 'incarnation-2'
|
||||
})
|
||||
expect(await availabilityRows(runtime)).toEqual([
|
||||
expect.objectContaining({ hostKind: 'native', launchMode: 'typed', attestedRuns: 1 })
|
||||
])
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,160 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { PaneAgentIdentityAvailability } from '../../shared/pane-agent-identity-evidence'
|
||||
import { MAX_LIVE_FINALIZED_RUN_KEYS } from '../../shared/pane-agent-identity-availability'
|
||||
import { PaneAgentIdentityObservationAuthority } from './pane-agent-identity-observation-authority'
|
||||
|
||||
function createHarness() {
|
||||
const records: PaneAgentIdentityAvailability[] = []
|
||||
const coverage: { hostKind: string; reason: string; amount: number }[] = []
|
||||
const authority = new PaneAgentIdentityObservationAuthority('authority', {
|
||||
record: (row) => records.push(row),
|
||||
addCoverage: (hostKind, reason, amount = 1) => coverage.push({ hostKind, reason, amount })
|
||||
})
|
||||
return { authority, records, coverage }
|
||||
}
|
||||
|
||||
describe('PaneAgentIdentityObservationAuthority', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('settles once after five seconds and dedupes the finalized attestation', () => {
|
||||
vi.useFakeTimers()
|
||||
const { authority, records } = createHarness()
|
||||
const context = { ptyId: 'pty', incarnationId: 'inc-1', hostKinds: ['native'] as const }
|
||||
|
||||
expect(authority.attestRun(context, 'typed', 'codex', 'typed:1')).toBe(true)
|
||||
vi.advanceTimersByTime(4_999)
|
||||
expect(records).toEqual([])
|
||||
vi.advanceTimersByTime(1)
|
||||
expect(records).toEqual([
|
||||
expect.objectContaining({ hostKind: 'native', launchMode: 'typed', sourceMask: 4 })
|
||||
])
|
||||
|
||||
expect(authority.attestRun(context, 'typed', 'codex', 'typed:1')).toBe(true)
|
||||
vi.advanceTimersByTime(5_000)
|
||||
expect(records).toHaveLength(1)
|
||||
|
||||
expect(authority.attestRun(context, 'typed', 'claude', 'typed:2')).toBe(true)
|
||||
vi.advanceTimersByTime(5_000)
|
||||
expect(records).toHaveLength(2)
|
||||
authority.shutdown()
|
||||
})
|
||||
|
||||
it('publishes paired WSL facts with execution evidence only on the distro view', () => {
|
||||
vi.useFakeTimers()
|
||||
const { authority, records } = createHarness()
|
||||
const context = {
|
||||
ptyId: 'pty',
|
||||
incarnationId: 'inc-1',
|
||||
hostKinds: ['wsl-host', 'wsl-distro'] as const
|
||||
}
|
||||
authority.attestRun(context, 'orca-launch', 'codex', 'launch:1')
|
||||
authority.observeEvidence(context, {
|
||||
source: 'process',
|
||||
agent: 'codex',
|
||||
processProvenance: 'distro-origin'
|
||||
})
|
||||
authority.observeEvidence(context, { source: 'live-hook', agent: 'codex' })
|
||||
authority.observeTitle(context, 'codex')
|
||||
|
||||
vi.advanceTimersByTime(5_000)
|
||||
|
||||
expect(records).toEqual([
|
||||
expect.objectContaining({ hostKind: 'wsl-host', sourceMask: 4 }),
|
||||
expect.objectContaining({ hostKind: 'wsl-distro', sourceMask: 71 })
|
||||
])
|
||||
authority.shutdown()
|
||||
})
|
||||
|
||||
it('freezes pending runs immediately on exit and truncates only live windows on shutdown', () => {
|
||||
vi.useFakeTimers()
|
||||
const { authority, records, coverage } = createHarness()
|
||||
const first = { ptyId: 'pty-1', incarnationId: 'inc-1', hostKinds: ['ssh'] as const }
|
||||
const second = { ptyId: 'pty-2', incarnationId: 'inc-1', hostKinds: ['native'] as const }
|
||||
authority.attestRun(first, 'resume', 'claude', 'resume:1')
|
||||
authority.exitOrRebind(first.ptyId, first.incarnationId)
|
||||
expect(records).toEqual([
|
||||
expect.objectContaining({ hostKind: 'ssh', launchMode: 'resume', sourceMask: 20 })
|
||||
])
|
||||
|
||||
authority.attestRun(second, 'typed', 'codex', 'typed:1')
|
||||
authority.shutdown()
|
||||
expect(coverage).toContainEqual({ hostKind: 'native', reason: 'truncated', amount: 1 })
|
||||
vi.advanceTimersByTime(5_000)
|
||||
expect(records).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('records a title candidate only after its authority window and cancels it on attestation', () => {
|
||||
vi.useFakeTimers()
|
||||
const { authority, coverage } = createHarness()
|
||||
const context = { ptyId: 'pty', incarnationId: 'inc-1', hostKinds: ['ssh'] as const }
|
||||
authority.observeTitle(context, 'codex')
|
||||
vi.advanceTimersByTime(29_999)
|
||||
expect(coverage).toEqual([])
|
||||
vi.advanceTimersByTime(1)
|
||||
expect(coverage).toEqual([{ hostKind: 'ssh', reason: 'candidate', amount: 1 }])
|
||||
|
||||
authority.exitOrRebind(context.ptyId, context.incarnationId)
|
||||
authority.observeTitle(context, 'codex')
|
||||
authority.attestRun(context, 'typed', 'codex', 'typed:1')
|
||||
vi.advanceTimersByTime(30_000)
|
||||
expect(coverage).toHaveLength(1)
|
||||
|
||||
authority.observeTitle(context, 'codex')
|
||||
vi.advanceTimersByTime(30_000)
|
||||
expect(coverage).toHaveLength(1)
|
||||
authority.shutdown()
|
||||
})
|
||||
|
||||
it('releases a title candidate after the finalized-key cap rejects it', () => {
|
||||
vi.useFakeTimers()
|
||||
const { authority, coverage } = createHarness()
|
||||
for (let index = 0; index < MAX_LIVE_FINALIZED_RUN_KEYS; index += 1) {
|
||||
authority.attestRun(
|
||||
{ ptyId: `run-${index}`, incarnationId: 'inc-1', hostKinds: ['native'] },
|
||||
'typed',
|
||||
'codex',
|
||||
`typed:${index}`
|
||||
)
|
||||
}
|
||||
vi.advanceTimersByTime(5_000)
|
||||
|
||||
const candidate = {
|
||||
ptyId: 'candidate',
|
||||
incarnationId: 'inc-1',
|
||||
hostKinds: ['native'] as const
|
||||
}
|
||||
authority.observeTitle(candidate, 'codex')
|
||||
vi.advanceTimersByTime(30_000)
|
||||
expect(coverage).toContainEqual({ hostKind: 'native', reason: 'overflow', amount: 1 })
|
||||
|
||||
authority.observeTitle(candidate, 'codex')
|
||||
vi.advanceTimersByTime(30_000)
|
||||
authority.shutdown()
|
||||
expect(coverage).toEqual([{ hostKind: 'native', reason: 'overflow', amount: 1 }])
|
||||
})
|
||||
|
||||
it('tombstones a title candidate when the pending-window cap rejects it', () => {
|
||||
vi.useFakeTimers()
|
||||
const { authority, coverage } = createHarness()
|
||||
for (let index = 0; index < MAX_LIVE_FINALIZED_RUN_KEYS; index += 1) {
|
||||
authority.attestRun(
|
||||
{ ptyId: `run-${index}`, incarnationId: 'inc-1', hostKinds: ['native'] },
|
||||
'typed',
|
||||
'codex',
|
||||
`typed:${index}`
|
||||
)
|
||||
}
|
||||
|
||||
const candidate = {
|
||||
ptyId: 'candidate',
|
||||
incarnationId: 'inc-1',
|
||||
hostKinds: ['native'] as const
|
||||
}
|
||||
authority.observeTitle(candidate, 'codex')
|
||||
authority.observeTitle(candidate, 'codex')
|
||||
expect(coverage).toEqual([{ hostKind: 'native', reason: 'overflow', amount: 1 }])
|
||||
authority.shutdown()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,301 @@
|
||||
import type { PtyIncarnationId } from '../../shared/pty-incarnation'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import {
|
||||
PaneAgentIdentityObservationScheduler,
|
||||
type PaneAgentIdentityObservationSchedulerDeps
|
||||
} from '../../shared/pane-agent-identity-availability'
|
||||
import {
|
||||
reducePaneAgentIdentityAvailability,
|
||||
type PaneAgentIdentityAvailability,
|
||||
type PaneAgentIdentityEvidenceFact,
|
||||
type PaneAgentIdentityHostKind,
|
||||
type PaneAgentIdentityLaunchMode
|
||||
} from '../../shared/pane-agent-identity-evidence'
|
||||
import type { PaneAgentRunKey } from '../../shared/pane-agent-identity-resolver'
|
||||
import {
|
||||
appendEvidenceToActiveRun,
|
||||
titleHostKind,
|
||||
uniqueHostKinds,
|
||||
type ActiveRun,
|
||||
type Candidate,
|
||||
type EvidenceObservation,
|
||||
type PtyObservationState
|
||||
} from './pane-agent-identity-observation-model'
|
||||
|
||||
type CensusCoverageReason = 'candidate' | 'overflow' | 'truncated'
|
||||
|
||||
export type PaneAgentIdentityObservationSink = {
|
||||
record(observation: PaneAgentIdentityAvailability): void
|
||||
addCoverage(
|
||||
hostKind: PaneAgentIdentityHostKind,
|
||||
reason: CensusCoverageReason,
|
||||
amount?: number
|
||||
): void
|
||||
}
|
||||
|
||||
export type PaneAgentIdentityPtyContext = {
|
||||
ptyId: string
|
||||
incarnationId: PtyIncarnationId | null
|
||||
hostKinds: readonly PaneAgentIdentityHostKind[]
|
||||
}
|
||||
|
||||
/** Owns run/candidate windows and reduces authority facts before they reach a sink. */
|
||||
export class PaneAgentIdentityObservationAuthority {
|
||||
private readonly states = new Map<string, PtyObservationState>()
|
||||
private readonly runsByKey = new Map<string, ActiveRun>()
|
||||
private readonly candidatesByKey = new Map<string, Candidate>()
|
||||
private readonly scheduler: PaneAgentIdentityObservationScheduler
|
||||
|
||||
constructor(
|
||||
private readonly authorityId: string,
|
||||
private readonly sink: PaneAgentIdentityObservationSink,
|
||||
schedulerDeps: Pick<PaneAgentIdentityObservationSchedulerDeps, 'now'> = {}
|
||||
) {
|
||||
this.scheduler = new PaneAgentIdentityObservationScheduler({
|
||||
...schedulerDeps,
|
||||
onRunFreeze: (key) => this.freezeRun(key),
|
||||
onTitleCandidateFreeze: (hostKind, key) => this.freezeCandidate(hostKind, key),
|
||||
onFinalizedOverflow: (kind, hostKind, key) =>
|
||||
this.recordFinalizedOverflow(kind, hostKind, key)
|
||||
})
|
||||
}
|
||||
|
||||
attestRun(
|
||||
context: PaneAgentIdentityPtyContext,
|
||||
launchMode: PaneAgentIdentityLaunchMode,
|
||||
owner: TuiAgent,
|
||||
attestationId: string
|
||||
): boolean {
|
||||
const state = this.getState(context)
|
||||
if (state.lastAttestationId === attestationId) {
|
||||
return true
|
||||
}
|
||||
if (state.activeRun) {
|
||||
const previousRun = state.activeRun
|
||||
if (!this.scheduler.freezeRun(previousRun.key)) {
|
||||
for (const hostKind of previousRun.hostKinds) {
|
||||
this.sink.addCoverage(hostKind, 'overflow')
|
||||
}
|
||||
this.runsByKey.delete(previousRun.key)
|
||||
state.activeRun = null
|
||||
}
|
||||
}
|
||||
if (state.candidate) {
|
||||
this.scheduler.cancelCandidate(state.candidate.key)
|
||||
this.scheduler.exitOrRebind(state.candidate.key)
|
||||
this.candidatesByKey.delete(state.candidate.key)
|
||||
state.candidate = null
|
||||
}
|
||||
if (state.finalizedRunKey) {
|
||||
this.scheduler.exitOrRebind(state.finalizedRunKey)
|
||||
state.finalizedRunKey = null
|
||||
}
|
||||
if (state.finalizedCandidateKey) {
|
||||
this.scheduler.exitOrRebind(state.finalizedCandidateKey)
|
||||
state.finalizedCandidateKey = null
|
||||
}
|
||||
state.ordinal += 1
|
||||
const run: PaneAgentRunKey = {
|
||||
authorityId: `${this.authorityId}:${context.ptyId}:${context.incarnationId ?? 'unknown'}`,
|
||||
incarnation: state.ordinal
|
||||
}
|
||||
const key = `${run.authorityId}:${run.incarnation}`
|
||||
const hostKinds = uniqueHostKinds(context.hostKinds)
|
||||
const factsByHostKind = new Map<PaneAgentIdentityHostKind, PaneAgentIdentityEvidenceFact[]>()
|
||||
for (const hostKind of hostKinds) {
|
||||
const facts: PaneAgentIdentityEvidenceFact[] = [{ source: 'launch', agent: owner, run }]
|
||||
if (launchMode === 'resume') {
|
||||
facts.push({ source: 'sleeping-session', agent: owner, run })
|
||||
}
|
||||
factsByHostKind.set(hostKind, facts)
|
||||
}
|
||||
const activeRun: ActiveRun = {
|
||||
key,
|
||||
ptyId: context.ptyId,
|
||||
run,
|
||||
launchMode,
|
||||
owner,
|
||||
hostKinds,
|
||||
factsByHostKind
|
||||
}
|
||||
state.lastAttestationId = attestationId
|
||||
state.activeRun = activeRun
|
||||
this.runsByKey.set(key, activeRun)
|
||||
if (this.scheduler.attestRun(key)) {
|
||||
return true
|
||||
}
|
||||
this.runsByKey.delete(key)
|
||||
state.activeRun = null
|
||||
for (const hostKind of hostKinds) {
|
||||
this.sink.addCoverage(hostKind, 'overflow')
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
observeEvidence(context: PaneAgentIdentityPtyContext, evidence: EvidenceObservation): boolean {
|
||||
const state = this.states.get(context.ptyId)
|
||||
const activeRun = state?.activeRun
|
||||
if (!state || state.incarnationId !== context.incarnationId || !activeRun) {
|
||||
return false
|
||||
}
|
||||
return appendEvidenceToActiveRun(activeRun, evidence)
|
||||
}
|
||||
|
||||
observeTitle(context: PaneAgentIdentityPtyContext, agent: TuiAgent): void {
|
||||
if (this.observeEvidence(context, { source: 'title', agent })) {
|
||||
return
|
||||
}
|
||||
const state = this.getState(context)
|
||||
if (state.ordinal > 0 || state.candidate || state.finalizedCandidateKey) {
|
||||
return
|
||||
}
|
||||
const hostKind = titleHostKind(context.hostKinds)
|
||||
const candidate = {
|
||||
key: `${this.authorityId}:${context.ptyId}:${context.incarnationId ?? 'unknown'}:candidate`,
|
||||
ptyId: context.ptyId,
|
||||
hostKind
|
||||
}
|
||||
if (!this.scheduler.scheduleTitleCandidate(candidate.key, candidate.hostKind)) {
|
||||
this.sink.addCoverage(hostKind, 'overflow')
|
||||
state.finalizedCandidateKey = candidate.key
|
||||
return
|
||||
}
|
||||
state.candidate = candidate
|
||||
this.candidatesByKey.set(candidate.key, candidate)
|
||||
}
|
||||
|
||||
exitOrRebind(ptyId: string, incarnationId?: PtyIncarnationId | null): void {
|
||||
const state = this.states.get(ptyId)
|
||||
if (!state || (incarnationId !== undefined && state.incarnationId !== incarnationId)) {
|
||||
return
|
||||
}
|
||||
if (state.activeRun && this.scheduler.isPendingRun(state.activeRun.key)) {
|
||||
if (!this.scheduler.freezeRun(state.activeRun.key)) {
|
||||
for (const hostKind of state.activeRun.hostKinds) {
|
||||
this.sink.addCoverage(hostKind, 'overflow')
|
||||
}
|
||||
}
|
||||
}
|
||||
if (state.candidate && this.scheduler.isPendingCandidate(state.candidate.key)) {
|
||||
if (!this.scheduler.freezeTitleCandidate(state.candidate.key, state.candidate.hostKind)) {
|
||||
this.sink.addCoverage(state.candidate.hostKind, 'overflow')
|
||||
}
|
||||
}
|
||||
if (state.activeRun) {
|
||||
this.scheduler.exitOrRebind(state.activeRun.key)
|
||||
this.runsByKey.delete(state.activeRun.key)
|
||||
}
|
||||
if (state.finalizedRunKey) {
|
||||
this.scheduler.exitOrRebind(state.finalizedRunKey)
|
||||
}
|
||||
if (state.candidate) {
|
||||
this.scheduler.exitOrRebind(state.candidate.key)
|
||||
this.candidatesByKey.delete(state.candidate.key)
|
||||
}
|
||||
if (state.finalizedCandidateKey) {
|
||||
this.scheduler.exitOrRebind(state.finalizedCandidateKey)
|
||||
}
|
||||
this.states.delete(ptyId)
|
||||
}
|
||||
|
||||
shutdown(): void {
|
||||
for (const state of this.states.values()) {
|
||||
if (state.activeRun && this.scheduler.isPendingRun(state.activeRun.key)) {
|
||||
for (const hostKind of state.activeRun.hostKinds) {
|
||||
this.sink.addCoverage(hostKind, 'truncated')
|
||||
}
|
||||
}
|
||||
if (state.candidate && this.scheduler.isPendingCandidate(state.candidate.key)) {
|
||||
this.sink.addCoverage(state.candidate.hostKind, 'truncated')
|
||||
}
|
||||
}
|
||||
this.scheduler.shutdown()
|
||||
this.states.clear()
|
||||
this.runsByKey.clear()
|
||||
this.candidatesByKey.clear()
|
||||
}
|
||||
|
||||
private getState(context: PaneAgentIdentityPtyContext): PtyObservationState {
|
||||
const existing = this.states.get(context.ptyId)
|
||||
if (existing && existing.incarnationId === context.incarnationId) {
|
||||
return existing
|
||||
}
|
||||
if (existing) {
|
||||
this.exitOrRebind(context.ptyId, existing.incarnationId)
|
||||
}
|
||||
const state: PtyObservationState = {
|
||||
incarnationId: context.incarnationId,
|
||||
ordinal: 0,
|
||||
lastAttestationId: null,
|
||||
activeRun: null,
|
||||
finalizedRunKey: null,
|
||||
candidate: null,
|
||||
finalizedCandidateKey: null
|
||||
}
|
||||
this.states.set(context.ptyId, state)
|
||||
return state
|
||||
}
|
||||
|
||||
private freezeRun(key: string): void {
|
||||
const run = this.runsByKey.get(key)
|
||||
if (!run) {
|
||||
return
|
||||
}
|
||||
for (const hostKind of run.hostKinds) {
|
||||
this.sink.record(
|
||||
reducePaneAgentIdentityAvailability(hostKind, run.launchMode, {
|
||||
facts: run.factsByHostKind.get(hostKind) ?? [],
|
||||
currentRun: run.run,
|
||||
owner: { agent: run.owner, run: run.run }
|
||||
})
|
||||
)
|
||||
}
|
||||
this.runsByKey.delete(key)
|
||||
const state = this.states.get(run.ptyId)
|
||||
if (state?.activeRun?.key === key) {
|
||||
state.activeRun = null
|
||||
state.finalizedRunKey = key
|
||||
}
|
||||
}
|
||||
|
||||
private freezeCandidate(hostKind: PaneAgentIdentityHostKind, key: string): void {
|
||||
this.sink.addCoverage(hostKind, 'candidate')
|
||||
const candidate = this.candidatesByKey.get(key)
|
||||
this.candidatesByKey.delete(key)
|
||||
const candidateState = candidate ? this.states.get(candidate.ptyId) : undefined
|
||||
if (candidateState) {
|
||||
candidateState.candidate = null
|
||||
candidateState.finalizedCandidateKey = key
|
||||
}
|
||||
}
|
||||
|
||||
private recordFinalizedOverflow(
|
||||
kind: 'run' | 'title-candidate',
|
||||
hostKind: PaneAgentIdentityHostKind | undefined,
|
||||
key: string
|
||||
): void {
|
||||
if (kind === 'title-candidate' && hostKind) {
|
||||
this.sink.addCoverage(hostKind, 'overflow')
|
||||
const candidate = this.candidatesByKey.get(key)
|
||||
this.candidatesByKey.delete(key)
|
||||
const state = candidate ? this.states.get(candidate.ptyId) : undefined
|
||||
if (state?.candidate?.key === key) {
|
||||
state.candidate = null
|
||||
state.finalizedCandidateKey = key
|
||||
}
|
||||
return
|
||||
}
|
||||
const run = this.runsByKey.get(key)
|
||||
if (!run) {
|
||||
return
|
||||
}
|
||||
for (const runHostKind of run.hostKinds) {
|
||||
this.sink.addCoverage(runHostKind, 'overflow')
|
||||
}
|
||||
this.runsByKey.delete(key)
|
||||
const state = this.states.get(run.ptyId)
|
||||
if (state?.activeRun?.key === key) {
|
||||
state.activeRun = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type { PtyIncarnationId } from '../../shared/pty-incarnation'
|
||||
import type { TuiAgent } from '../../shared/tui-agent'
|
||||
import type {
|
||||
PaneAgentIdentityEvidenceFact,
|
||||
PaneAgentIdentityHostKind,
|
||||
PaneAgentIdentityLaunchMode
|
||||
} from '../../shared/pane-agent-identity-evidence'
|
||||
import type {
|
||||
PaneAgentEvidenceSource,
|
||||
PaneAgentRunKey
|
||||
} from '../../shared/pane-agent-identity-resolver'
|
||||
|
||||
export type ActiveRun = {
|
||||
key: string
|
||||
ptyId: string
|
||||
run: PaneAgentRunKey
|
||||
launchMode: PaneAgentIdentityLaunchMode
|
||||
owner: TuiAgent
|
||||
hostKinds: readonly PaneAgentIdentityHostKind[]
|
||||
factsByHostKind: Map<PaneAgentIdentityHostKind, PaneAgentIdentityEvidenceFact[]>
|
||||
}
|
||||
|
||||
export type Candidate = {
|
||||
key: string
|
||||
ptyId: string
|
||||
hostKind: PaneAgentIdentityHostKind
|
||||
}
|
||||
|
||||
export type PtyObservationState = {
|
||||
incarnationId: PtyIncarnationId | null
|
||||
ordinal: number
|
||||
lastAttestationId: string | null
|
||||
activeRun: ActiveRun | null
|
||||
finalizedRunKey: string | null
|
||||
candidate: Candidate | null
|
||||
finalizedCandidateKey: string | null
|
||||
}
|
||||
|
||||
export type EvidenceObservation =
|
||||
| {
|
||||
source: 'process'
|
||||
agent: TuiAgent
|
||||
processProvenance: 'execution-host' | 'distro-origin' | 'target-origin'
|
||||
}
|
||||
| {
|
||||
source: Exclude<
|
||||
PaneAgentEvidenceSource,
|
||||
'launch' | 'sleeping-session' | 'sibling' | 'process'
|
||||
>
|
||||
agent: TuiAgent
|
||||
processProvenance?: never
|
||||
}
|
||||
|
||||
export const uniqueHostKinds = (
|
||||
hostKinds: readonly PaneAgentIdentityHostKind[]
|
||||
): PaneAgentIdentityHostKind[] => [...new Set(hostKinds)]
|
||||
|
||||
export function appendEvidenceToActiveRun(
|
||||
activeRun: ActiveRun,
|
||||
evidence: EvidenceObservation
|
||||
): boolean {
|
||||
for (const hostKind of activeRun.hostKinds) {
|
||||
if (!evidenceBelongsToHost(hostKind, evidence)) {
|
||||
continue
|
||||
}
|
||||
const facts = activeRun.factsByHostKind.get(hostKind)
|
||||
if (!facts) {
|
||||
continue
|
||||
}
|
||||
const fact: PaneAgentIdentityEvidenceFact =
|
||||
evidence.source === 'process'
|
||||
? {
|
||||
source: evidence.source,
|
||||
agent: evidence.agent,
|
||||
run: activeRun.run,
|
||||
executionHostProvenance: evidence.processProvenance
|
||||
}
|
||||
: { source: evidence.source, agent: evidence.agent, run: activeRun.run }
|
||||
if (
|
||||
!facts.some((existing) => existing.source === fact.source && existing.agent === fact.agent)
|
||||
) {
|
||||
facts.push(fact)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function titleHostKind(
|
||||
hostKinds: readonly PaneAgentIdentityHostKind[]
|
||||
): PaneAgentIdentityHostKind {
|
||||
if (hostKinds.includes('ssh')) {
|
||||
return 'ssh'
|
||||
}
|
||||
if (hostKinds.includes('wsl-distro')) {
|
||||
return 'wsl-distro'
|
||||
}
|
||||
if (hostKinds.includes('relay')) {
|
||||
return 'relay'
|
||||
}
|
||||
return 'native'
|
||||
}
|
||||
|
||||
function evidenceBelongsToHost(
|
||||
hostKind: PaneAgentIdentityHostKind,
|
||||
evidence: EvidenceObservation
|
||||
): boolean {
|
||||
if (hostKind === 'wsl-host') {
|
||||
return false
|
||||
}
|
||||
if (evidence.source !== 'process') {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
(hostKind === 'native' && evidence.processProvenance === 'execution-host') ||
|
||||
(hostKind === 'wsl-distro' && evidence.processProvenance === 'distro-origin') ||
|
||||
(hostKind === 'ssh' && evidence.processProvenance === 'target-origin') ||
|
||||
hostKind === 'relay'
|
||||
)
|
||||
}
|
||||
@@ -177,6 +177,27 @@ describe('sendTerminal absence gate for leaf-branch writes', () => {
|
||||
expect(write).toHaveBeenCalledWith(STALE_PTY_ID, 'ping')
|
||||
})
|
||||
|
||||
it('observes a recognized terminal.send command only after its accepted provider write', async () => {
|
||||
vi.useFakeTimers()
|
||||
try {
|
||||
const { runtime, handle } = await makeRuntimeWithLeafHandle({
|
||||
hasPty: (ptyId) => ptyId === STALE_PTY_ID
|
||||
})
|
||||
|
||||
const send = runtime.sendTerminal(handle, { text: 'codex', enter: true })
|
||||
await vi.advanceTimersByTimeAsync(500)
|
||||
await send
|
||||
vi.advanceTimersByTime(5_000)
|
||||
|
||||
expect((await runtime.listTerminals()).agentIdentityAvailability?.rows).toEqual([
|
||||
['native', 'typed', 1, 0, 0, 0, 0]
|
||||
])
|
||||
runtime.shutdownPaneAgentIdentityCensus(false)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
})
|
||||
|
||||
it('reuses a proven-absent verdict across repeated sends instead of re-probing', async () => {
|
||||
const probe = vi.fn(async () => false)
|
||||
const { runtime, handle } = await makeRuntimeWithLeafHandle({ probePtyLiveness: probe })
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { PaneAgentIdentityAuthoritySnapshot } from '../../shared/pane-agent-identity-availability'
|
||||
import type {
|
||||
PaneAgentIdentityHostKind,
|
||||
PaneAgentIdentityLaunchMode
|
||||
} from '../../shared/pane-agent-identity-evidence'
|
||||
import { track } from './client'
|
||||
import {
|
||||
createDesktopPaneAgentIdentityCensus,
|
||||
createHeadlessPaneAgentIdentityCensus
|
||||
} from './pane-agent-identity-census-composition'
|
||||
|
||||
vi.mock('./client', () => ({ track: vi.fn() }))
|
||||
|
||||
describe('pane identity census composition', () => {
|
||||
it('constructs the desktop census as a telemetry emitter', () => {
|
||||
const census = createDesktopPaneAgentIdentityCensus()
|
||||
census.record({
|
||||
hostKind: 'native',
|
||||
launchMode: 'typed',
|
||||
sourceMask: 4,
|
||||
identityNull: false,
|
||||
ambiguousTopRank: false
|
||||
})
|
||||
census.flush()
|
||||
|
||||
expect(track).toHaveBeenCalledWith('pane_agent_identity_availability', {
|
||||
rows: [expect.objectContaining({ kind: 'aggregate', host_kind: 'native' })]
|
||||
})
|
||||
census.shutdown()
|
||||
})
|
||||
|
||||
it('constructs the headless census as one snapshot-only relay authority', () => {
|
||||
vi.mocked(track).mockClear()
|
||||
const census = createHeadlessPaneAgentIdentityCensus()
|
||||
census.record({
|
||||
hostKind: 'wsl-distro',
|
||||
launchMode: 'resume',
|
||||
sourceMask: 20,
|
||||
identityNull: false,
|
||||
ambiguousTopRank: false
|
||||
})
|
||||
census.flush()
|
||||
|
||||
expect(census.snapshot().rows).toEqual([['relay', 'resume', 1, 0, 0, 0, 0]])
|
||||
expect(track).not.toHaveBeenCalled()
|
||||
census.shutdown()
|
||||
})
|
||||
|
||||
it('keeps the maximum authority snapshot below the terminal-list wire budget', () => {
|
||||
const snapshot = new PaneAgentIdentityAuthoritySnapshot('e'.repeat(128))
|
||||
const hostKinds: PaneAgentIdentityHostKind[] = [
|
||||
'native',
|
||||
'wsl-host',
|
||||
'wsl-distro',
|
||||
'ssh',
|
||||
'relay'
|
||||
]
|
||||
const launchModes: PaneAgentIdentityLaunchMode[] = ['typed', 'orca-launch', 'resume']
|
||||
for (const hostKind of hostKinds) {
|
||||
for (const launchMode of launchModes) {
|
||||
snapshot.record({
|
||||
hostKind,
|
||||
launchMode,
|
||||
sourceMask: 64,
|
||||
identityNull: true,
|
||||
ambiguousTopRank: true
|
||||
})
|
||||
}
|
||||
snapshot.recordCandidateCoverage(hostKind)
|
||||
}
|
||||
|
||||
expect(Buffer.byteLength(JSON.stringify(snapshot.snapshot()), 'utf8')).toBeLessThan(2_048)
|
||||
})
|
||||
|
||||
it('installs the census and primary hook facts in both production entry points', () => {
|
||||
const desktop = readFileSync(resolve(process.cwd(), 'src/main/index.ts'), 'utf8')
|
||||
const headless = readFileSync(resolve(process.cwd(), 'src/main/orcad/orcad-entry.ts'), 'utf8')
|
||||
|
||||
expect(desktop).toContain(
|
||||
'const paneAgentIdentityCensus = isServeMode\n ? createHeadlessPaneAgentIdentityCensus()\n : createDesktopPaneAgentIdentityCensus()'
|
||||
)
|
||||
expect(headless).toContain(
|
||||
'const paneAgentIdentityCensus = createHeadlessPaneAgentIdentityCensus()'
|
||||
)
|
||||
for (const source of [desktop, headless]) {
|
||||
expect(source).toMatch(
|
||||
/new OrcaRuntimeService\([\s\S]*?\{[\s\S]*?paneAgentIdentityCensus[,\s]/
|
||||
)
|
||||
}
|
||||
expect(desktop).toContain('runtime?.observeAgentHookStatus(enriched)')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import { PaneAgentIdentityCensus } from './pane-agent-identity-census'
|
||||
|
||||
export function createDesktopPaneAgentIdentityCensus(): PaneAgentIdentityCensus {
|
||||
return new PaneAgentIdentityCensus()
|
||||
}
|
||||
|
||||
export function createHeadlessPaneAgentIdentityCensus(): PaneAgentIdentityCensus {
|
||||
return new PaneAgentIdentityCensus({ emit: null, snapshotHostKind: 'relay' })
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PaneAgentIdentityCensus } from './pane-agent-identity-census'
|
||||
|
||||
describe('PaneAgentIdentityCensus', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('schedules relay-only deltas and preserves relay coverage host attribution', () => {
|
||||
vi.useFakeTimers()
|
||||
const emit = vi.fn()
|
||||
const census = new PaneAgentIdentityCensus(emit)
|
||||
const row = ['relay', 'typed', 1, 0, 0, 0, 0] as const
|
||||
|
||||
census.ingestRelaySnapshot('env', { epoch: 'e', revision: 1, rows: [row] })
|
||||
vi.advanceTimersByTime(300_000)
|
||||
expect(emit).toHaveBeenCalledTimes(1)
|
||||
expect(emit).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: 'coverage',
|
||||
host_kind: 'relay',
|
||||
reason: 'baseline',
|
||||
count: 1
|
||||
})
|
||||
])
|
||||
)
|
||||
emit.mockClear()
|
||||
|
||||
census.ingestRelaySnapshot('env', {
|
||||
epoch: 'e',
|
||||
revision: 2,
|
||||
rows: [['relay', 'typed', 2, 0, 0, 0, 0]],
|
||||
candidateCoverage: [['relay', 2]]
|
||||
})
|
||||
expect(emit).not.toHaveBeenCalled()
|
||||
expect(census.snapshot().candidateCoverage).toBeUndefined()
|
||||
|
||||
vi.advanceTimersByTime(300_000)
|
||||
|
||||
expect(emit).toHaveBeenCalledTimes(1)
|
||||
expect(emit).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
kind: 'aggregate',
|
||||
host_kind: 'relay',
|
||||
attested_runs: 1
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: 'coverage',
|
||||
host_kind: 'relay',
|
||||
reason: 'snapshot',
|
||||
count: 1
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: 'coverage',
|
||||
host_kind: 'relay',
|
||||
reason: 'candidate',
|
||||
count: 2
|
||||
})
|
||||
])
|
||||
)
|
||||
census.shutdown()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,240 @@
|
||||
import {
|
||||
aggregatePaneAgentIdentityAvailability,
|
||||
emptyPaneAgentIdentityAggregate,
|
||||
PaneAgentIdentityRelaySnapshotMerger,
|
||||
PaneAgentIdentityAuthoritySnapshot,
|
||||
PANE_AGENT_IDENTITY_MAX_BATCH_ROWS,
|
||||
type PaneAgentIdentityAggregate,
|
||||
type PaneAgentIdentityAvailabilitySnapshot
|
||||
} from '../../shared/pane-agent-identity-availability'
|
||||
import type {
|
||||
PaneAgentIdentityAvailability,
|
||||
PaneAgentIdentityHostKind
|
||||
} from '../../shared/pane-agent-identity-evidence'
|
||||
import { track } from './client'
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
type CoverageReason =
|
||||
| 'snapshot'
|
||||
| 'baseline'
|
||||
| 'epoch_changed'
|
||||
| 'counter_regressed'
|
||||
| 'capacity'
|
||||
| 'candidate'
|
||||
| 'overflow'
|
||||
| 'truncated'
|
||||
type AggregateRow = {
|
||||
kind: 'aggregate'
|
||||
host_kind: PaneAgentIdentityAggregate['hostKind']
|
||||
launch_mode: PaneAgentIdentityAggregate['launchMode']
|
||||
attested_runs: number
|
||||
no_evidence: number
|
||||
title_only: number
|
||||
identity_null: number
|
||||
ambiguous_top_rank: number
|
||||
}
|
||||
type CoverageRow = {
|
||||
kind: 'coverage'
|
||||
host_kind: PaneAgentIdentityAggregate['hostKind']
|
||||
reason: CoverageReason
|
||||
count: number
|
||||
}
|
||||
type TelemetryRow = AggregateRow | CoverageRow
|
||||
type TelemetryEmitter = (rows: readonly TelemetryRow[]) => void
|
||||
type PaneAgentIdentityCensusOptions = {
|
||||
emit: TelemetryEmitter | null
|
||||
snapshotHostKind?: PaneAgentIdentityHostKind
|
||||
epoch?: string
|
||||
}
|
||||
|
||||
const keyOf = (hostKind: string, launchMode: string): string => `${hostKind}:${launchMode}`
|
||||
|
||||
export class PaneAgentIdentityCensus {
|
||||
private readonly aggregate = new Map<string, PaneAgentIdentityAggregate>()
|
||||
private readonly relay = new PaneAgentIdentityRelaySnapshotMerger()
|
||||
private readonly coverage = new Map<string, CoverageRow>()
|
||||
private timer: ReturnType<typeof setTimeout> | undefined
|
||||
private closed = false
|
||||
private readonly emit: TelemetryEmitter | null
|
||||
private readonly snapshotHostKind: PaneAgentIdentityHostKind | undefined
|
||||
private readonly authoritySnapshot: PaneAgentIdentityAuthoritySnapshot
|
||||
|
||||
constructor(input: TelemetryEmitter | PaneAgentIdentityCensusOptions = defaultOptions()) {
|
||||
const options = typeof input === 'function' ? { emit: input } : input
|
||||
this.emit = options.emit
|
||||
this.snapshotHostKind = options.snapshotHostKind
|
||||
this.authoritySnapshot = new PaneAgentIdentityAuthoritySnapshot(options.epoch ?? randomUUID())
|
||||
}
|
||||
|
||||
record(row: PaneAgentIdentityAvailability): void {
|
||||
if (this.closed) {
|
||||
return
|
||||
}
|
||||
const normalized = this.normalizeObservation(row)
|
||||
this.authoritySnapshot.record(normalized)
|
||||
if (!this.emit) {
|
||||
return
|
||||
}
|
||||
const key = keyOf(normalized.hostKind, normalized.launchMode)
|
||||
this.aggregate.set(
|
||||
key,
|
||||
aggregatePaneAgentIdentityAvailability(
|
||||
normalized,
|
||||
this.aggregate.get(key) ??
|
||||
emptyPaneAgentIdentityAggregate(normalized.hostKind, normalized.launchMode)
|
||||
)
|
||||
)
|
||||
this.scheduleFlush()
|
||||
}
|
||||
|
||||
ingestRelaySnapshot(
|
||||
environmentKey: string,
|
||||
snapshot: PaneAgentIdentityAvailabilitySnapshot | undefined
|
||||
): void {
|
||||
if (this.closed) {
|
||||
return
|
||||
}
|
||||
const result = this.relay.merge(environmentKey, snapshot)
|
||||
if (result.kind !== 'unavailable') {
|
||||
this.addCoverage('relay', 'snapshot')
|
||||
}
|
||||
if (result.kind === 'delta') {
|
||||
for (const row of result.rows) {
|
||||
// Apply cumulative deltas without reclassifying every run.
|
||||
const key = keyOf(row.hostKind, row.launchMode)
|
||||
const previous =
|
||||
this.aggregate.get(key) ?? emptyPaneAgentIdentityAggregate(row.hostKind, row.launchMode)
|
||||
this.aggregate.set(key, {
|
||||
...previous,
|
||||
attestedRuns: Math.min(1_000_000_000, previous.attestedRuns + row.attestedRuns),
|
||||
noEvidence: Math.min(1_000_000_000, previous.noEvidence + row.noEvidence),
|
||||
titleOnly: Math.min(1_000_000_000, previous.titleOnly + row.titleOnly),
|
||||
identityNull: Math.min(1_000_000_000, previous.identityNull + row.identityNull),
|
||||
ambiguousTopRank: Math.min(
|
||||
1_000_000_000,
|
||||
previous.ambiguousTopRank + row.ambiguousTopRank
|
||||
)
|
||||
})
|
||||
}
|
||||
for (const candidate of result.candidateCoverage) {
|
||||
this.addTelemetryCoverage(candidate.hostKind, 'candidate', candidate.exposures)
|
||||
}
|
||||
} else if (result.kind === 'baseline') {
|
||||
this.addCoverage('relay', 'baseline')
|
||||
} else if (result.kind === 'failed-closed') {
|
||||
this.addCoverage(
|
||||
'relay',
|
||||
result.reason === 'capacity'
|
||||
? 'capacity'
|
||||
: result.reason === 'counter-regressed'
|
||||
? 'counter_regressed'
|
||||
: 'epoch_changed'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
addCoverage(
|
||||
hostKind: PaneAgentIdentityAggregate['hostKind'],
|
||||
reason: CoverageReason,
|
||||
amount = 1
|
||||
): void {
|
||||
if (this.closed) {
|
||||
return
|
||||
}
|
||||
const normalizedHostKind = this.snapshotHostKind ?? hostKind
|
||||
if (reason === 'candidate') {
|
||||
this.authoritySnapshot.recordCandidateCoverage(normalizedHostKind, amount)
|
||||
}
|
||||
this.addTelemetryCoverage(normalizedHostKind, reason, amount)
|
||||
}
|
||||
|
||||
private addTelemetryCoverage(
|
||||
hostKind: PaneAgentIdentityAggregate['hostKind'],
|
||||
reason: CoverageReason,
|
||||
amount: number
|
||||
): void {
|
||||
if (!this.emit) {
|
||||
return
|
||||
}
|
||||
const key = `${hostKind}:${reason}`
|
||||
const previous = this.coverage.get(key)
|
||||
this.coverage.set(key, {
|
||||
kind: 'coverage',
|
||||
host_kind: hostKind,
|
||||
reason,
|
||||
count: Math.min(1_000_000_000, (previous?.count ?? 0) + Math.max(0, Math.floor(amount)))
|
||||
})
|
||||
this.scheduleFlush()
|
||||
}
|
||||
|
||||
scheduleFlush(delayMs = 300_000): void {
|
||||
if (this.closed || !this.emit || this.timer) {
|
||||
return
|
||||
}
|
||||
this.timer = setTimeout(() => {
|
||||
this.timer = undefined
|
||||
this.flush()
|
||||
}, delayMs)
|
||||
this.timer.unref?.()
|
||||
}
|
||||
|
||||
flush(): void {
|
||||
if (this.closed) {
|
||||
return
|
||||
}
|
||||
const rows: TelemetryRow[] = [...this.aggregate.values()].map((row) => ({
|
||||
kind: 'aggregate',
|
||||
host_kind: row.hostKind,
|
||||
launch_mode: row.launchMode,
|
||||
attested_runs: row.attestedRuns,
|
||||
no_evidence: row.noEvidence,
|
||||
title_only: row.titleOnly,
|
||||
identity_null: row.identityNull,
|
||||
ambiguous_top_rank: row.ambiguousTopRank
|
||||
}))
|
||||
for (const coverage of this.coverage.values()) {
|
||||
rows.push(coverage)
|
||||
}
|
||||
if (rows.length === 0) {
|
||||
return
|
||||
}
|
||||
for (let offset = 0; offset < rows.length; offset += PANE_AGENT_IDENTITY_MAX_BATCH_ROWS) {
|
||||
this.emit?.(rows.slice(offset, offset + PANE_AGENT_IDENTITY_MAX_BATCH_ROWS))
|
||||
}
|
||||
this.aggregate.clear()
|
||||
this.coverage.clear()
|
||||
}
|
||||
|
||||
shutdown(): void {
|
||||
this.closed = true
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
}
|
||||
this.timer = undefined
|
||||
this.aggregate.clear()
|
||||
this.coverage.clear()
|
||||
this.relay.clear()
|
||||
}
|
||||
|
||||
snapshot(): PaneAgentIdentityAvailabilitySnapshot {
|
||||
return this.authoritySnapshot.snapshot()
|
||||
}
|
||||
|
||||
hostKindsForObservation(
|
||||
hostKinds: readonly PaneAgentIdentityHostKind[]
|
||||
): readonly PaneAgentIdentityHostKind[] {
|
||||
return this.snapshotHostKind ? [this.snapshotHostKind] : hostKinds
|
||||
}
|
||||
|
||||
private normalizeObservation(row: PaneAgentIdentityAvailability): PaneAgentIdentityAvailability {
|
||||
return this.snapshotHostKind ? { ...row, hostKind: this.snapshotHostKind } : row
|
||||
}
|
||||
}
|
||||
|
||||
function defaultOptions(): PaneAgentIdentityCensusOptions {
|
||||
return {
|
||||
emit: (rows) => track('pane_agent_identity_availability', { rows: [...rows] })
|
||||
}
|
||||
}
|
||||
|
||||
export type PaneAgentIdentityTelemetryRow = TelemetryRow
|
||||
+13
-4
@@ -326,7 +326,10 @@ import type {
|
||||
LocalLogTailReadResult,
|
||||
LocalLogTailWatchArgs
|
||||
} from '../shared/local-log-tail-types'
|
||||
import { subscribeRuntimeEnvironmentFromPreload } from './runtime-environment-subscriptions'
|
||||
import {
|
||||
observeRuntimeEnvironmentTerminalListResponse,
|
||||
subscribeRuntimeEnvironmentFromPreload
|
||||
} from './runtime-environment-subscriptions'
|
||||
import type { RuntimeEnvironmentSubscriptionHandle } from './runtime-environment-subscriptions'
|
||||
import type { HostedReviewForBranchArgs } from '../shared/hosted-review'
|
||||
import type { ReadClipboardTextOptions } from '../shared/clipboard-text'
|
||||
@@ -4700,14 +4703,20 @@ const api = {
|
||||
ipcRenderer.invoke('runtimeEnvironments:prepareBrowserClientHostPlacement', args),
|
||||
retryConnectionsNow: (): Promise<void> =>
|
||||
ipcRenderer.invoke('runtimeEnvironments:retryConnectionsNow'),
|
||||
call: (args: {
|
||||
call: async (args: {
|
||||
selector: string
|
||||
method: string
|
||||
params?: unknown
|
||||
timeoutMs?: number
|
||||
expectedEnvironmentPairingRevision?: number
|
||||
}): Promise<RuntimeRpcResponse<unknown>> =>
|
||||
ipcRenderer.invoke('runtimeEnvironments:call', args),
|
||||
}): Promise<RuntimeRpcResponse<unknown>> => {
|
||||
const response = (await ipcRenderer.invoke(
|
||||
'runtimeEnvironments:call',
|
||||
args
|
||||
)) as RuntimeRpcResponse<unknown>
|
||||
observeRuntimeEnvironmentTerminalListResponse(ipcRenderer, args, response)
|
||||
return response
|
||||
},
|
||||
subscribe: async (
|
||||
args: {
|
||||
selector: string
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { subscribeRuntimeEnvironmentFromPreload } from './runtime-environment-subscriptions'
|
||||
import {
|
||||
observeRuntimeEnvironmentTerminalListResponse,
|
||||
subscribeRuntimeEnvironmentFromPreload
|
||||
} from './runtime-environment-subscriptions'
|
||||
|
||||
function deferred<T>(): {
|
||||
promise: Promise<T>
|
||||
@@ -55,6 +58,96 @@ function dispatch(ipc: ReturnType<typeof createIpc>, event: SubscriptionEvent):
|
||||
}
|
||||
|
||||
describe('subscribeRuntimeEnvironmentFromPreload', () => {
|
||||
it('observes one-shot terminal lists at the preload boundary and skips old peers', () => {
|
||||
const ipc = createIpc()
|
||||
const snapshot = { epoch: 'epoch', revision: 1, rows: [] }
|
||||
|
||||
observeRuntimeEnvironmentTerminalListResponse(
|
||||
ipc,
|
||||
{ selector: 'desk', method: 'terminal.list' },
|
||||
{
|
||||
id: 'rpc-list',
|
||||
ok: true,
|
||||
result: { terminals: [], agentIdentityAvailability: snapshot },
|
||||
_meta: { runtimeId: 'rt' }
|
||||
}
|
||||
)
|
||||
|
||||
expect(ipc.invoke).toHaveBeenCalledWith('telemetry:ingestPaneAgentIdentityAvailability', {
|
||||
environmentKey: 'desk',
|
||||
snapshot
|
||||
})
|
||||
|
||||
ipc.invoke.mockClear()
|
||||
observeRuntimeEnvironmentTerminalListResponse(
|
||||
ipc,
|
||||
{ selector: 'desk', method: 'terminal.list' },
|
||||
{
|
||||
id: 'rpc-old-peer',
|
||||
ok: true,
|
||||
result: { terminals: [] },
|
||||
_meta: { runtimeId: 'rt' }
|
||||
}
|
||||
)
|
||||
observeRuntimeEnvironmentTerminalListResponse(
|
||||
ipc,
|
||||
{ selector: 'desk', method: 'terminal.show' },
|
||||
{
|
||||
id: 'rpc-other',
|
||||
ok: true,
|
||||
result: { agentIdentityAvailability: snapshot },
|
||||
_meta: { runtimeId: 'rt' }
|
||||
}
|
||||
)
|
||||
observeRuntimeEnvironmentTerminalListResponse(
|
||||
ipc,
|
||||
{ selector: 'desk', method: 'terminal.list' },
|
||||
{
|
||||
id: 'rpc-private-extra',
|
||||
ok: true,
|
||||
result: { agentIdentityAvailability: { ...snapshot, rawTitle: 'private' } },
|
||||
_meta: { runtimeId: 'rt' }
|
||||
}
|
||||
)
|
||||
expect(ipc.invoke).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('observes each successful terminal-list subscription response once', async () => {
|
||||
const ipc = createIpc()
|
||||
ipc.invoke.mockImplementation((channel: string) =>
|
||||
channel === 'runtimeEnvironments:subscribe'
|
||||
? Promise.resolve({ subscriptionId: 'sub-list', requestId: 'rpc-list' })
|
||||
: (Promise.resolve({}) as Promise<unknown>)
|
||||
)
|
||||
const onResponse = vi.fn()
|
||||
const cleanup = await subscribeRuntimeEnvironmentFromPreload(
|
||||
ipc,
|
||||
{ selector: 'desk', method: 'terminal.list' },
|
||||
{ onResponse },
|
||||
() => 'sub-list'
|
||||
)
|
||||
const snapshot = { epoch: 'epoch', revision: 1, rows: [] }
|
||||
|
||||
dispatch(ipc, {
|
||||
subscriptionId: 'sub-list',
|
||||
type: 'response',
|
||||
response: {
|
||||
id: 'rpc-list',
|
||||
ok: true,
|
||||
result: { terminals: [], agentIdentityAvailability: snapshot },
|
||||
_meta: { runtimeId: 'rt' }
|
||||
}
|
||||
})
|
||||
|
||||
expect(onResponse).toHaveBeenCalledTimes(1)
|
||||
expect(ipc.invoke).toHaveBeenCalledTimes(2)
|
||||
expect(ipc.invoke).toHaveBeenLastCalledWith('telemetry:ingestPaneAgentIdentityAvailability', {
|
||||
environmentKey: 'desk',
|
||||
snapshot
|
||||
})
|
||||
cleanup.unsubscribe()
|
||||
})
|
||||
|
||||
it('registers the subscription event listener before invoking main', async () => {
|
||||
const subscription = deferred<{ subscriptionId: string; requestId: string }>()
|
||||
const ipc = createIpc()
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RuntimeRpcResponse } from '../shared/runtime-rpc-envelope'
|
||||
import { isPaneAgentIdentityAvailabilitySnapshot } from '../shared/pane-agent-identity-availability-validator'
|
||||
|
||||
type RuntimeEnvironmentSubscribeArgs = {
|
||||
selector: string
|
||||
@@ -38,6 +39,8 @@ type RuntimeEnvironmentSubscriptionIpc = {
|
||||
) => void
|
||||
}
|
||||
|
||||
type RuntimeEnvironmentTerminalListObserverIpc = Pick<RuntimeEnvironmentSubscriptionIpc, 'invoke'>
|
||||
|
||||
const SUBSCRIPTION_EVENT_CHANNEL = 'runtimeEnvironments:subscriptionEvent'
|
||||
|
||||
// Why: each subscribe() previously attached its own channel listener that the
|
||||
@@ -55,6 +58,28 @@ const subscriptionDispatchers = new WeakMap<
|
||||
RuntimeEnvironmentSubscriptionDispatcher
|
||||
>()
|
||||
|
||||
export function observeRuntimeEnvironmentTerminalListResponse(
|
||||
ipc: RuntimeEnvironmentTerminalListObserverIpc,
|
||||
args: Pick<RuntimeEnvironmentSubscribeArgs, 'selector' | 'method'>,
|
||||
response: RuntimeRpcResponse<unknown>
|
||||
): void {
|
||||
if (args.method !== 'terminal.list' || !response.ok || !response.result) {
|
||||
return
|
||||
}
|
||||
if (typeof response.result !== 'object') {
|
||||
return
|
||||
}
|
||||
const snapshot = (response.result as { agentIdentityAvailability?: unknown })
|
||||
.agentIdentityAvailability
|
||||
if (snapshot === undefined || !isPaneAgentIdentityAvailabilitySnapshot(snapshot)) {
|
||||
return
|
||||
}
|
||||
void ipc.invoke('telemetry:ingestPaneAgentIdentityAvailability', {
|
||||
environmentKey: args.selector,
|
||||
snapshot
|
||||
})
|
||||
}
|
||||
|
||||
function releaseIdleDispatcher(
|
||||
ipc: RuntimeEnvironmentSubscriptionIpc,
|
||||
callbacks: Map<string, RuntimeEnvironmentSubscriptionCallbacks>,
|
||||
@@ -132,7 +157,17 @@ export async function subscribeRuntimeEnvironmentFromPreload(
|
||||
// Why: streaming RPCs can emit their first frame before ipcMain.handle()
|
||||
// resolves, so the dispatcher must be routing this id before invoking.
|
||||
const dispatcher = getOrCreateDispatcher(ipc)
|
||||
dispatcher.callbacks.set(subscriptionId, callbacks)
|
||||
const observedCallbacks =
|
||||
args.method === 'terminal.list'
|
||||
? {
|
||||
...callbacks,
|
||||
onResponse: (response: RuntimeRpcResponse<unknown>): void => {
|
||||
observeRuntimeEnvironmentTerminalListResponse(ipc, args, response)
|
||||
callbacks.onResponse(response)
|
||||
}
|
||||
}
|
||||
: callbacks
|
||||
dispatcher.callbacks.set(subscriptionId, observedCallbacks)
|
||||
const releaseCurrentSubscription = (): void => {
|
||||
releaseSubscription(ipc, dispatcher, subscriptionId)
|
||||
}
|
||||
|
||||
+7
-101
@@ -9,12 +9,10 @@ import { registerAgentHookTerminalLifecycleHandler } from '../agent-hook-termina
|
||||
import type { AgentCompletionStatusSnapshot } from '../agent-completion-coordinator-types'
|
||||
import { resolveCompatibleAgentTypeForOwner } from '../../../../../shared/agent-title-owner'
|
||||
import { resolveCommittedTitleAgentType } from '@/lib/pane-agent-evidence'
|
||||
import { recognizeAgentProcessFromCommandLine } from '../../../../../shared/agent-process-recognition'
|
||||
import { ManualAgentCommandTracker } from '../../../../../shared/manual-agent-command-tracker'
|
||||
import type { TuiAgent } from '../../../../../shared/tui-agent'
|
||||
import { isTuiAgent } from '../../../../../shared/tui-agent-config'
|
||||
|
||||
import { MANUAL_AGENT_COMMAND_MAX_CHARS } from './pty-connect-limits'
|
||||
|
||||
import type { ConnectPanePtySession } from './connect-pane-pty-session'
|
||||
|
||||
export function installCommandInferredPaneAgent(session: ConnectPanePtySession): void {
|
||||
@@ -22,22 +20,17 @@ export function installCommandInferredPaneAgent(session: ConnectPanePtySession):
|
||||
// shadowing the shell's current command line, for generic terminals where no
|
||||
// launch metadata exists. Consumed by getAuthoritativePaneAgent below.
|
||||
session.commandInferredPaneAgent = null
|
||||
session.pendingShellCommandLine = ''
|
||||
session.pendingShellCommandCursor = 0
|
||||
session.manualAgentCommandTracker = new ManualAgentCommandTracker()
|
||||
session.commandInferredPaneAgentGeneration = 0
|
||||
session.shellCommandInferenceSuspendedUntilCommandEnd = false
|
||||
session.startAcceptedInferredCommand = (_agent: TuiAgent): void => {}
|
||||
session.requestKnownWindowsShiftEnterReconfirmation = (): void => {}
|
||||
session.resetPendingShellCommandLine = (): void => {
|
||||
session.pendingShellCommandLine = ''
|
||||
session.pendingShellCommandCursor = 0
|
||||
session.manualAgentCommandTracker.reset()
|
||||
}
|
||||
session.rememberCommandInferredPaneAgent = (): void => {
|
||||
const commandLine = session.pendingShellCommandLine.trim()
|
||||
session.resetPendingShellCommandLine()
|
||||
const candidateAgent = commandLine
|
||||
? (recognizeAgentProcessFromCommandLine(commandLine)?.agent ?? null)
|
||||
: null
|
||||
session.cancelSuspendedShellCommandInference = (): void => {
|
||||
session.manualAgentCommandTracker.cancelSuspendedInference()
|
||||
}
|
||||
session.rememberCommandInferredPaneAgent = (candidateAgent: TuiAgent): void => {
|
||||
const state = useAppStore.getState()
|
||||
const registeredLaunchAgent =
|
||||
state.agentLaunchConfigByPaneKey[session.cacheKey]?.identity.agentType
|
||||
@@ -69,93 +62,6 @@ export function installCommandInferredPaneAgent(session: ConnectPanePtySession):
|
||||
}, 0)
|
||||
})
|
||||
}
|
||||
session.appendPendingShellCommandInput = (text: string): void => {
|
||||
const available = MANUAL_AGENT_COMMAND_MAX_CHARS - session.pendingShellCommandLine.length
|
||||
if (available <= 0) {
|
||||
session.shellCommandInferenceSuspendedUntilCommandEnd = true
|
||||
return
|
||||
}
|
||||
const inserted = text.slice(0, available)
|
||||
session.pendingShellCommandLine =
|
||||
session.pendingShellCommandLine.slice(0, session.pendingShellCommandCursor) +
|
||||
inserted +
|
||||
session.pendingShellCommandLine.slice(session.pendingShellCommandCursor)
|
||||
session.pendingShellCommandCursor += inserted.length
|
||||
if (inserted.length < text.length) {
|
||||
session.shellCommandInferenceSuspendedUntilCommandEnd = true
|
||||
}
|
||||
}
|
||||
session.deletePendingShellCommandWord = (): void => {
|
||||
const beforeCursor = session.pendingShellCommandLine.slice(0, session.pendingShellCommandCursor)
|
||||
const afterCursor = session.pendingShellCommandLine.slice(session.pendingShellCommandCursor)
|
||||
const nextBeforeCursor = beforeCursor.replace(/[^\S\r\n]*\S+[^\S\r\n]*$/, '')
|
||||
session.pendingShellCommandLine = nextBeforeCursor + afterCursor
|
||||
session.pendingShellCommandCursor = nextBeforeCursor.length
|
||||
}
|
||||
session.cancelSuspendedShellCommandInference = (): void => {
|
||||
if (!session.shellCommandInferenceSuspendedUntilCommandEnd) {
|
||||
return
|
||||
}
|
||||
session.shellCommandInferenceSuspendedUntilCommandEnd = false
|
||||
session.resetPendingShellCommandLine()
|
||||
}
|
||||
session.deletePendingShellCommandCharacter = (): void => {
|
||||
if (session.pendingShellCommandCursor === 0) {
|
||||
return
|
||||
}
|
||||
session.pendingShellCommandLine =
|
||||
session.pendingShellCommandLine.slice(0, session.pendingShellCommandCursor - 1) +
|
||||
session.pendingShellCommandLine.slice(session.pendingShellCommandCursor)
|
||||
session.pendingShellCommandCursor -= 1
|
||||
}
|
||||
session.deletePendingShellCommandCharacterAtCursor = (): void => {
|
||||
if (session.pendingShellCommandCursor >= session.pendingShellCommandLine.length) {
|
||||
return
|
||||
}
|
||||
session.pendingShellCommandLine =
|
||||
session.pendingShellCommandLine.slice(0, session.pendingShellCommandCursor) +
|
||||
session.pendingShellCommandLine.slice(session.pendingShellCommandCursor + 1)
|
||||
}
|
||||
session.movePendingShellCommandCursor = (delta: number): void => {
|
||||
session.pendingShellCommandCursor = Math.min(
|
||||
session.pendingShellCommandLine.length,
|
||||
Math.max(0, session.pendingShellCommandCursor + delta)
|
||||
)
|
||||
}
|
||||
session.consumeShellCommandCsiSequence = (data: string, index: number): number | null => {
|
||||
if (data.charCodeAt(index) !== 0x1b || data[index + 1] !== '[') {
|
||||
return null
|
||||
}
|
||||
let cursor = index + 2
|
||||
while (cursor < data.length && /[0-9;?]/.test(data[cursor]!)) {
|
||||
cursor += 1
|
||||
}
|
||||
const final = data[cursor]
|
||||
if (!final || !/[~A-Za-z]/.test(final)) {
|
||||
return null
|
||||
}
|
||||
const params = data.slice(index + 2, cursor)
|
||||
// Why: only emulate a bare one-column move. Parameterized/modified cursor keys
|
||||
// (e.g. Ctrl+Left `\x1b[1;5D` = word-jump) move the real cursor by more than
|
||||
// one, so tracking them as ±1 would desync the shadow line — fall through to
|
||||
// reset instead of silently corrupting the sampled command.
|
||||
if (final === 'D' && params === '') {
|
||||
session.movePendingShellCommandCursor(-1)
|
||||
} else if (final === 'C' && params === '') {
|
||||
session.movePendingShellCommandCursor(1)
|
||||
} else if (final === 'H' || (final === '~' && params === '1')) {
|
||||
session.pendingShellCommandCursor = 0
|
||||
} else if (final === 'F' || (final === '~' && params === '4')) {
|
||||
session.pendingShellCommandCursor = session.pendingShellCommandLine.length
|
||||
} else if (final === '~' && params === '3') {
|
||||
session.deletePendingShellCommandCharacterAtCursor()
|
||||
} else if (final === '~' && (params === '200' || params === '201')) {
|
||||
// Bracketed paste wrappers are terminal framing, not shell command text.
|
||||
} else {
|
||||
session.resetPendingShellCommandLine()
|
||||
}
|
||||
return cursor + 1
|
||||
}
|
||||
session.getLivePaneAgentTitle = (): string | null => {
|
||||
const state = useAppStore.getState()
|
||||
const runtimeTitle = state.runtimePaneTitlesByTabId?.[session.deps.tabId]?.[session.pane.id]
|
||||
|
||||
+4
-57
@@ -4,7 +4,6 @@ import type { AgentType } from '../../../../../shared/agent-status-types'
|
||||
import { AGENT_INTERRUPT_SETTLE_MS } from '../../../../../shared/agent-interrupt-intent'
|
||||
import { resolvePaneAgentOwner } from '../../../../../shared/pane-agent-owner'
|
||||
|
||||
import { MANUAL_AGENT_COMMAND_MAX_CHARS } from './pty-connect-limits'
|
||||
import {
|
||||
CURSOR_AGENT_REATTACH_HEADER,
|
||||
terminalOwnsDomFocus,
|
||||
@@ -34,62 +33,10 @@ export function installShellCommandInference(session: ConnectPanePtySession): vo
|
||||
session.resetPendingShellCommandLine()
|
||||
return
|
||||
}
|
||||
if (session.shellCommandInferenceSuspendedUntilCommandEnd) {
|
||||
if (data.includes('\x03') || data.includes('\x15')) {
|
||||
session.shellCommandInferenceSuspendedUntilCommandEnd = false
|
||||
session.resetPendingShellCommandLine()
|
||||
}
|
||||
if (data.includes('\r') || data.includes('\n')) {
|
||||
session.shellCommandInferenceSuspendedUntilCommandEnd = false
|
||||
}
|
||||
return
|
||||
}
|
||||
if (data.length > MANUAL_AGENT_COMMAND_MAX_CHARS) {
|
||||
session.resetPendingShellCommandLine()
|
||||
session.shellCommandInferenceSuspendedUntilCommandEnd =
|
||||
!data.includes('\r') && !data.includes('\n')
|
||||
return
|
||||
}
|
||||
for (let index = 0; index < data.length; index += 1) {
|
||||
const char = data[index]!
|
||||
if (char === '\r' || char === '\n') {
|
||||
session.shellCommandInferenceSuspendedUntilCommandEnd = false
|
||||
session.rememberCommandInferredPaneAgent()
|
||||
if (session.commandInferredPaneAgent) {
|
||||
return
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (char === '\x7f' || char === '\b') {
|
||||
session.deletePendingShellCommandCharacter()
|
||||
continue
|
||||
}
|
||||
if (char === '\x17') {
|
||||
session.deletePendingShellCommandWord()
|
||||
continue
|
||||
}
|
||||
if (char === '\x03' || char === '\x15') {
|
||||
session.resetPendingShellCommandLine()
|
||||
continue
|
||||
}
|
||||
if (char === '\x1b') {
|
||||
const nextIndex = session.consumeShellCommandCsiSequence(data, index)
|
||||
if (nextIndex !== null) {
|
||||
index = nextIndex - 1
|
||||
continue
|
||||
}
|
||||
session.resetPendingShellCommandLine()
|
||||
continue
|
||||
}
|
||||
if (char < ' ') {
|
||||
session.resetPendingShellCommandLine()
|
||||
continue
|
||||
}
|
||||
if (char >= ' ') {
|
||||
session.appendPendingShellCommandInput(char)
|
||||
if (session.shellCommandInferenceSuspendedUntilCommandEnd) {
|
||||
return
|
||||
}
|
||||
for (const candidate of session.manualAgentCommandTracker.ingest(data)) {
|
||||
session.rememberCommandInferredPaneAgent(candidate)
|
||||
if (session.commandInferredPaneAgent) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { recognizeAgentProcessFromCommandLine } from './agent-process-recognition'
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
|
||||
export const MANUAL_AGENT_COMMAND_MAX_CHARS = 4_096
|
||||
|
||||
/** Bounded shell-line shadow used by both UI ownership inference and execution authorities. */
|
||||
export class ManualAgentCommandTracker {
|
||||
private line = ''
|
||||
private cursor = 0
|
||||
private suspended = false
|
||||
private acceptedCount = 0
|
||||
|
||||
ingest(data: string): TuiAgent[] {
|
||||
const accepted: TuiAgent[] = []
|
||||
if (this.suspended) {
|
||||
if (data.includes('\x03') || data.includes('\x15')) {
|
||||
this.reset()
|
||||
} else if (data.includes('\r') || data.includes('\n')) {
|
||||
this.suspended = false
|
||||
}
|
||||
return accepted
|
||||
}
|
||||
if (data.length > MANUAL_AGENT_COMMAND_MAX_CHARS) {
|
||||
this.reset()
|
||||
this.suspended = !data.includes('\r') && !data.includes('\n')
|
||||
return accepted
|
||||
}
|
||||
for (let index = 0; index < data.length; index += 1) {
|
||||
const char = data[index]!
|
||||
if (char === '\r' || char === '\n') {
|
||||
if (!this.suspended) {
|
||||
const recognized = recognizeAgentProcessFromCommandLine(this.line.trim())
|
||||
if (recognized) {
|
||||
accepted.push(recognized.agent)
|
||||
this.acceptedCount += 1
|
||||
}
|
||||
}
|
||||
this.reset()
|
||||
continue
|
||||
}
|
||||
if (char === '\x7f' || char === '\b') {
|
||||
this.backspace()
|
||||
continue
|
||||
}
|
||||
if (char === '\x03' || char === '\x15') {
|
||||
this.reset()
|
||||
continue
|
||||
}
|
||||
if (char === '\x17') {
|
||||
this.deleteWord()
|
||||
continue
|
||||
}
|
||||
if (char === '\x1b' && data[index + 1] === '[') {
|
||||
const nextIndex = this.consumeCsi(data, index)
|
||||
if (nextIndex === null) {
|
||||
this.reset()
|
||||
} else {
|
||||
index = nextIndex - 1
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (char < ' ') {
|
||||
this.reset()
|
||||
continue
|
||||
}
|
||||
if (this.line.length >= MANUAL_AGENT_COMMAND_MAX_CHARS) {
|
||||
this.suspended = true
|
||||
return accepted
|
||||
}
|
||||
this.line = this.line.slice(0, this.cursor) + char + this.line.slice(this.cursor)
|
||||
this.cursor += 1
|
||||
}
|
||||
return accepted
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.line = ''
|
||||
this.cursor = 0
|
||||
this.suspended = false
|
||||
}
|
||||
cancelSuspendedInference(): void {
|
||||
if (this.suspended) {
|
||||
this.reset()
|
||||
}
|
||||
}
|
||||
get acceptedCommandCount(): number {
|
||||
return this.acceptedCount
|
||||
}
|
||||
private backspace(): void {
|
||||
if (this.cursor > 0) {
|
||||
this.line = this.line.slice(0, this.cursor - 1) + this.line.slice(this.cursor)
|
||||
this.cursor -= 1
|
||||
}
|
||||
}
|
||||
private deleteWord(): void {
|
||||
const before = this.line.slice(0, this.cursor).replace(/[^\S\r\n]*\S+[^\S\r\n]*$/, '')
|
||||
this.line = before + this.line.slice(this.cursor)
|
||||
this.cursor = before.length
|
||||
}
|
||||
private consumeCsi(data: string, index: number): number | null {
|
||||
let cursor = index + 2
|
||||
while (cursor < data.length && /[0-9;?]/.test(data[cursor]!)) {
|
||||
cursor += 1
|
||||
}
|
||||
const final = data[cursor]
|
||||
if (!final || !/[~A-Za-z]/.test(final)) {
|
||||
return null
|
||||
}
|
||||
const params = data.slice(index + 2, cursor)
|
||||
if (final === 'D' && params === '') {
|
||||
this.cursor = Math.max(0, this.cursor - 1)
|
||||
} else if (final === 'C' && params === '') {
|
||||
this.cursor = Math.min(this.line.length, this.cursor + 1)
|
||||
} else if (final === 'H' || (final === '~' && params === '1')) {
|
||||
this.cursor = 0
|
||||
} else if (final === 'F' || (final === '~' && params === '4')) {
|
||||
this.cursor = this.line.length
|
||||
} else if (final === '~' && params === '3' && this.cursor < this.line.length) {
|
||||
this.line = this.line.slice(0, this.cursor) + this.line.slice(this.cursor + 1)
|
||||
} else if (final !== '~' || (params !== '200' && params !== '201')) {
|
||||
this.reset()
|
||||
}
|
||||
return cursor + 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
PANE_AGENT_IDENTITY_MAX_COUNT,
|
||||
type PaneAgentIdentityAvailabilitySnapshot
|
||||
} from './pane-agent-identity-availability'
|
||||
|
||||
export function isPaneAgentIdentityAvailabilitySnapshot(
|
||||
value: unknown
|
||||
): value is PaneAgentIdentityAvailabilitySnapshot {
|
||||
if (!value || typeof value !== 'object') {
|
||||
return false
|
||||
}
|
||||
const candidate = value as Record<string, unknown>
|
||||
const snapshotKeys = new Set(['epoch', 'revision', 'rows', 'candidateCoverage'])
|
||||
if (Object.keys(candidate).some((key) => !snapshotKeys.has(key))) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
typeof candidate.epoch !== 'string' ||
|
||||
candidate.epoch.length === 0 ||
|
||||
candidate.epoch.length > 128
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (!Number.isSafeInteger(candidate.revision) || (candidate.revision as number) < 0) {
|
||||
return false
|
||||
}
|
||||
if (!Array.isArray(candidate.rows) || candidate.rows.length > 15) {
|
||||
return false
|
||||
}
|
||||
const rowKeys = new Set<string>()
|
||||
const rowsValid = candidate.rows.every((row) => {
|
||||
if (!Array.isArray(row) || row.length !== 7) {
|
||||
return false
|
||||
}
|
||||
const [hostKind, launchMode, attestedRuns, ...counters] = row
|
||||
if (
|
||||
!['native', 'wsl-host', 'wsl-distro', 'ssh', 'relay'].includes(hostKind as string) ||
|
||||
!['typed', 'orca-launch', 'resume'].includes(launchMode as string)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
const key = `${hostKind as string}:${launchMode as string}`
|
||||
if (rowKeys.has(key)) {
|
||||
return false
|
||||
}
|
||||
rowKeys.add(key)
|
||||
if (
|
||||
!Number.isSafeInteger(attestedRuns) ||
|
||||
(attestedRuns as number) < 1 ||
|
||||
(attestedRuns as number) > PANE_AGENT_IDENTITY_MAX_COUNT
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return counters.every(
|
||||
(count) =>
|
||||
Number.isSafeInteger(count) &&
|
||||
(count as number) >= 0 &&
|
||||
(count as number) <= (attestedRuns as number)
|
||||
)
|
||||
})
|
||||
if (!rowsValid) {
|
||||
return false
|
||||
}
|
||||
if (candidate.candidateCoverage === undefined) {
|
||||
return true
|
||||
}
|
||||
if (!Array.isArray(candidate.candidateCoverage) || candidate.candidateCoverage.length > 5) {
|
||||
return false
|
||||
}
|
||||
const coverageHosts = new Set<string>()
|
||||
return candidate.candidateCoverage.every((row) => {
|
||||
if (!Array.isArray(row) || row.length !== 2) {
|
||||
return false
|
||||
}
|
||||
const [hostKind, exposures] = row
|
||||
if (
|
||||
!['native', 'wsl-host', 'wsl-distro', 'ssh', 'relay'].includes(hostKind as string) ||
|
||||
!Number.isSafeInteger(exposures) ||
|
||||
(exposures as number) < 1 ||
|
||||
(exposures as number) > PANE_AGENT_IDENTITY_MAX_COUNT ||
|
||||
coverageHosts.has(hostKind as string)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
coverageHosts.add(hostKind as string)
|
||||
return true
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
import type {
|
||||
PaneAgentIdentityAvailability,
|
||||
PaneAgentIdentityHostKind,
|
||||
PaneAgentIdentityLaunchMode
|
||||
} from './pane-agent-identity-evidence'
|
||||
|
||||
export const PANE_AGENT_IDENTITY_MAX_COUNT = 1_000_000_000
|
||||
export const PANE_AGENT_IDENTITY_MAX_BATCH_ROWS = 64
|
||||
export const PANE_AGENT_IDENTITY_MAX_REMOTE_ENVIRONMENTS = 256
|
||||
export {
|
||||
MAX_LIVE_FINALIZED_RUN_KEYS,
|
||||
MAX_PENDING_IDENTITY_OBSERVATIONS,
|
||||
PaneAgentIdentityObservationScheduler,
|
||||
RUN_SETTLE_MS,
|
||||
TITLE_CANDIDATE_WINDOW_MS,
|
||||
type PaneAgentIdentityObservationSchedulerDeps
|
||||
} from './pane-agent-identity-observation-scheduler'
|
||||
|
||||
export type PaneAgentIdentityAggregate = {
|
||||
hostKind: PaneAgentIdentityHostKind
|
||||
launchMode: PaneAgentIdentityLaunchMode
|
||||
attestedRuns: number
|
||||
noEvidence: number
|
||||
titleOnly: number
|
||||
identityNull: number
|
||||
ambiguousTopRank: number
|
||||
}
|
||||
|
||||
export type PaneAgentIdentityCandidateCoverage = {
|
||||
hostKind: PaneAgentIdentityHostKind
|
||||
exposures: number
|
||||
}
|
||||
|
||||
export type PaneAgentIdentityAggregateWireRow = readonly [
|
||||
hostKind: PaneAgentIdentityHostKind,
|
||||
launchMode: PaneAgentIdentityLaunchMode,
|
||||
attestedRuns: number,
|
||||
noEvidence: number,
|
||||
titleOnly: number,
|
||||
identityNull: number,
|
||||
ambiguousTopRank: number
|
||||
]
|
||||
|
||||
export type PaneAgentIdentityCandidateCoverageWireRow = readonly [
|
||||
hostKind: PaneAgentIdentityHostKind,
|
||||
exposures: number
|
||||
]
|
||||
|
||||
export type PaneAgentIdentityAvailabilitySnapshot = {
|
||||
epoch: string
|
||||
revision: number
|
||||
/** Compact fixed tuples keep the cumulative field below the terminal-list wire budget. */
|
||||
rows: readonly PaneAgentIdentityAggregateWireRow[]
|
||||
candidateCoverage?: readonly PaneAgentIdentityCandidateCoverageWireRow[]
|
||||
}
|
||||
|
||||
export function emptyPaneAgentIdentityAggregate(
|
||||
hostKind: PaneAgentIdentityHostKind,
|
||||
launchMode: PaneAgentIdentityLaunchMode
|
||||
): PaneAgentIdentityAggregate {
|
||||
return {
|
||||
hostKind,
|
||||
launchMode,
|
||||
attestedRuns: 0,
|
||||
noEvidence: 0,
|
||||
titleOnly: 0,
|
||||
identityNull: 0,
|
||||
ambiguousTopRank: 0
|
||||
}
|
||||
}
|
||||
|
||||
function addCount(value: number, amount: number): number {
|
||||
return Math.min(PANE_AGENT_IDENTITY_MAX_COUNT, value + Math.max(0, Math.floor(amount)))
|
||||
}
|
||||
|
||||
export function aggregatePaneAgentIdentityAvailability(
|
||||
row: PaneAgentIdentityAvailability,
|
||||
aggregate = emptyPaneAgentIdentityAggregate(row.hostKind, row.launchMode)
|
||||
): PaneAgentIdentityAggregate {
|
||||
const titleOnly = row.sourceMask === 64
|
||||
return {
|
||||
...aggregate,
|
||||
attestedRuns: addCount(aggregate.attestedRuns, 1),
|
||||
noEvidence: addCount(aggregate.noEvidence, row.sourceMask === 0 ? 1 : 0),
|
||||
titleOnly: addCount(aggregate.titleOnly, titleOnly ? 1 : 0),
|
||||
identityNull: addCount(aggregate.identityNull, row.identityNull ? 1 : 0),
|
||||
ambiguousTopRank: addCount(aggregate.ambiguousTopRank, row.ambiguousTopRank ? 1 : 0)
|
||||
}
|
||||
}
|
||||
|
||||
export type RelayMergeResult =
|
||||
| { kind: 'baseline' }
|
||||
| {
|
||||
kind: 'delta'
|
||||
rows: PaneAgentIdentityAggregate[]
|
||||
candidateCoverage: PaneAgentIdentityCandidateCoverage[]
|
||||
}
|
||||
| { kind: 'stale' }
|
||||
| {
|
||||
kind: 'failed-closed'
|
||||
reason: 'epoch-changed' | 'counter-regressed' | 'capacity'
|
||||
}
|
||||
| { kind: 'unavailable' }
|
||||
|
||||
type RelayState = {
|
||||
epoch: string
|
||||
revision: number
|
||||
rows: Map<string, PaneAgentIdentityAggregate>
|
||||
candidates: Map<PaneAgentIdentityHostKind, number>
|
||||
failedClosedReason: 'epoch-changed' | 'counter-regressed' | null
|
||||
}
|
||||
|
||||
const rowKey = (row: PaneAgentIdentityAggregate): string => `${row.hostKind}:${row.launchMode}`
|
||||
const COUNTER_KEYS = [
|
||||
'attestedRuns',
|
||||
'noEvidence',
|
||||
'titleOnly',
|
||||
'identityNull',
|
||||
'ambiguousTopRank'
|
||||
] as const
|
||||
|
||||
const decodeAggregateRow = (
|
||||
row: PaneAgentIdentityAggregateWireRow
|
||||
): PaneAgentIdentityAggregate => ({
|
||||
hostKind: row[0],
|
||||
launchMode: row[1],
|
||||
attestedRuns: row[2],
|
||||
noEvidence: row[3],
|
||||
titleOnly: row[4],
|
||||
identityNull: row[5],
|
||||
ambiguousTopRank: row[6]
|
||||
})
|
||||
|
||||
/** Session-scoped cumulative snapshot merger. State is intentionally not evicted. */
|
||||
export class PaneAgentIdentityRelaySnapshotMerger {
|
||||
private readonly states = new Map<string, RelayState>()
|
||||
private readonly maxEnvironments: number
|
||||
constructor(maxEnvironments = PANE_AGENT_IDENTITY_MAX_REMOTE_ENVIRONMENTS) {
|
||||
this.maxEnvironments = maxEnvironments
|
||||
}
|
||||
|
||||
merge(
|
||||
environmentKey: string,
|
||||
snapshot: PaneAgentIdentityAvailabilitySnapshot | undefined
|
||||
): RelayMergeResult {
|
||||
if (
|
||||
!snapshot ||
|
||||
typeof snapshot.epoch !== 'string' ||
|
||||
!Number.isSafeInteger(snapshot.revision) ||
|
||||
snapshot.revision < 0
|
||||
) {
|
||||
return { kind: 'unavailable' }
|
||||
}
|
||||
let state = this.states.get(environmentKey)
|
||||
const decodedRows = snapshot.rows.map(decodeAggregateRow)
|
||||
if (!state) {
|
||||
if (this.states.size >= this.maxEnvironments) {
|
||||
return { kind: 'failed-closed', reason: 'capacity' }
|
||||
}
|
||||
state = {
|
||||
epoch: snapshot.epoch,
|
||||
revision: snapshot.revision,
|
||||
rows: new Map(decodedRows.map((row) => [rowKey(row), row])),
|
||||
candidates: new Map((snapshot.candidateCoverage ?? []).map((row) => [row[0], row[1]])),
|
||||
failedClosedReason: null
|
||||
}
|
||||
this.states.set(environmentKey, state)
|
||||
return { kind: 'baseline' }
|
||||
}
|
||||
if (state.failedClosedReason) {
|
||||
return { kind: 'failed-closed', reason: state.failedClosedReason }
|
||||
}
|
||||
if (state.epoch !== snapshot.epoch) {
|
||||
state.failedClosedReason = 'epoch-changed'
|
||||
return { kind: 'failed-closed', reason: 'epoch-changed' }
|
||||
}
|
||||
if (snapshot.revision <= state.revision) {
|
||||
return { kind: 'stale' }
|
||||
}
|
||||
const incomingRows = new Map(decodedRows.map((row) => [rowKey(row), row]))
|
||||
const incomingCandidates = new Map(
|
||||
(snapshot.candidateCoverage ?? []).map((row) => [row[0], row[1]])
|
||||
)
|
||||
const counterRegressed =
|
||||
[...state.rows].some(([key, previous]) => {
|
||||
const incoming = incomingRows.get(key)
|
||||
return !incoming || COUNTER_KEYS.some((counter) => incoming[counter] < previous[counter])
|
||||
}) ||
|
||||
[...state.candidates].some(
|
||||
([hostKind, previous]) => (incomingCandidates.get(hostKind) ?? -1) < previous
|
||||
)
|
||||
if (counterRegressed) {
|
||||
state.failedClosedReason = 'counter-regressed'
|
||||
return { kind: 'failed-closed', reason: 'counter-regressed' }
|
||||
}
|
||||
const deltas: PaneAgentIdentityAggregate[] = []
|
||||
for (const incoming of decodedRows) {
|
||||
const previous =
|
||||
state.rows.get(rowKey(incoming)) ??
|
||||
emptyPaneAgentIdentityAggregate(incoming.hostKind, incoming.launchMode)
|
||||
const delta = {
|
||||
...incoming,
|
||||
...Object.fromEntries(
|
||||
COUNTER_KEYS.map((key) => [key, Math.max(0, incoming[key] - previous[key])])
|
||||
)
|
||||
} as PaneAgentIdentityAggregate
|
||||
if (COUNTER_KEYS.some((key) => delta[key] > 0)) {
|
||||
deltas.push(delta)
|
||||
}
|
||||
state.rows.set(rowKey(incoming), { ...incoming })
|
||||
}
|
||||
const candidateCoverage: PaneAgentIdentityCandidateCoverage[] = []
|
||||
for (const [hostKind, exposures] of incomingCandidates) {
|
||||
const delta = exposures - (state.candidates.get(hostKind) ?? 0)
|
||||
if (delta > 0) {
|
||||
candidateCoverage.push({ hostKind, exposures: delta })
|
||||
}
|
||||
state.candidates.set(hostKind, exposures)
|
||||
}
|
||||
state.revision = snapshot.revision
|
||||
return { kind: 'delta', rows: deltas, candidateCoverage }
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
this.states.clear()
|
||||
}
|
||||
get size(): number {
|
||||
return this.states.size
|
||||
}
|
||||
}
|
||||
|
||||
export class PaneAgentIdentityAuthoritySnapshot {
|
||||
private readonly rows = new Map<string, PaneAgentIdentityAggregate>()
|
||||
private readonly candidates = new Map<PaneAgentIdentityHostKind, number>()
|
||||
private revision = 0
|
||||
constructor(private readonly epoch: string) {}
|
||||
|
||||
record(observation: PaneAgentIdentityAvailability): void {
|
||||
const key = `${observation.hostKind}:${observation.launchMode}`
|
||||
this.rows.set(
|
||||
key,
|
||||
aggregatePaneAgentIdentityAvailability(
|
||||
observation,
|
||||
this.rows.get(key) ??
|
||||
emptyPaneAgentIdentityAggregate(observation.hostKind, observation.launchMode)
|
||||
)
|
||||
)
|
||||
this.revision += 1
|
||||
}
|
||||
|
||||
recordCandidateCoverage(hostKind: PaneAgentIdentityHostKind, amount = 1): void {
|
||||
this.candidates.set(hostKind, addCount(this.candidates.get(hostKind) ?? 0, amount))
|
||||
this.revision += 1
|
||||
}
|
||||
|
||||
snapshot(): PaneAgentIdentityAvailabilitySnapshot {
|
||||
const candidateCoverage: PaneAgentIdentityCandidateCoverageWireRow[] = [...this.candidates]
|
||||
return {
|
||||
epoch: this.epoch,
|
||||
revision: this.revision,
|
||||
rows: [...this.rows.values()].map(
|
||||
(row): PaneAgentIdentityAggregateWireRow => [
|
||||
row.hostKind,
|
||||
row.launchMode,
|
||||
row.attestedRuns,
|
||||
row.noEvidence,
|
||||
row.titleOnly,
|
||||
row.identityNull,
|
||||
row.ambiguousTopRank
|
||||
]
|
||||
),
|
||||
...(candidateCoverage.length > 0 ? { candidateCoverage } : {})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
assemblePaneAgentIdentityEvidence,
|
||||
reducePaneAgentIdentityAvailability
|
||||
} from './pane-agent-identity-evidence'
|
||||
import {
|
||||
aggregatePaneAgentIdentityAvailability,
|
||||
PaneAgentIdentityRelaySnapshotMerger
|
||||
} from './pane-agent-identity-availability'
|
||||
import { isPaneAgentIdentityAvailabilitySnapshot } from './pane-agent-identity-availability-validator'
|
||||
import {
|
||||
MANUAL_AGENT_COMMAND_MAX_CHARS,
|
||||
ManualAgentCommandTracker
|
||||
} from './manual-agent-command-tracker'
|
||||
import { eventSchemas } from './telemetry-events'
|
||||
|
||||
describe('pane identity evidence census', () => {
|
||||
it('returns eligible evidence in the canonical resolver order', () => {
|
||||
expect(
|
||||
assemblePaneAgentIdentityEvidence({
|
||||
facts: [
|
||||
{ source: 'title', agent: 'codex' },
|
||||
{ source: 'completed-hook', agent: 'codex' },
|
||||
{
|
||||
source: 'process',
|
||||
agent: 'codex',
|
||||
executionHostProvenance: 'execution-host'
|
||||
},
|
||||
{ source: 'live-hook', agent: 'codex' }
|
||||
]
|
||||
}).evidence.map((fact) => fact.source)
|
||||
).toEqual(['live-hook', 'process', 'completed-hook', 'title'])
|
||||
})
|
||||
|
||||
it('drops process facts without positive provenance and emits fixed source bits', () => {
|
||||
const result = assemblePaneAgentIdentityEvidence({
|
||||
facts: [
|
||||
{ source: 'process', agent: 'codex', executionHostProvenance: undefined } as never,
|
||||
{ source: 'title', agent: 'codex' }
|
||||
]
|
||||
})
|
||||
expect(result.sourceMask).toBe(64)
|
||||
expect(result.identityNull).toBe(false)
|
||||
})
|
||||
|
||||
it('reduces no evidence and ambiguity without exposing the winning agent', () => {
|
||||
expect(reducePaneAgentIdentityAvailability('native', 'typed', { facts: [] })).toMatchObject({
|
||||
sourceMask: 0,
|
||||
identityNull: true
|
||||
})
|
||||
const ambiguous = reducePaneAgentIdentityAvailability('native', 'orca-launch', {
|
||||
facts: [
|
||||
{ source: 'live-hook', agent: 'codex' },
|
||||
{ source: 'live-hook', agent: 'claude' }
|
||||
]
|
||||
})
|
||||
expect(ambiguous.ambiguousTopRank).toBe(true)
|
||||
expect(ambiguous).not.toHaveProperty('resolvedAgent')
|
||||
expect(
|
||||
aggregatePaneAgentIdentityAvailability({
|
||||
hostKind: 'native',
|
||||
launchMode: 'typed',
|
||||
sourceMask: 64,
|
||||
identityNull: false,
|
||||
ambiguousTopRank: false
|
||||
}).titleOnly
|
||||
).toBe(1)
|
||||
})
|
||||
|
||||
it('excludes superseded and pane-ineligible evidence from the availability mask', () => {
|
||||
const currentRun = { authorityId: 'authority', incarnation: 2 }
|
||||
const result = reducePaneAgentIdentityAvailability('native', 'orca-launch', {
|
||||
currentRun,
|
||||
facts: [
|
||||
{
|
||||
source: 'live-hook',
|
||||
agent: 'claude',
|
||||
run: { authorityId: 'authority', incarnation: 1 }
|
||||
},
|
||||
{ source: 'sibling', agent: 'claude', run: currentRun },
|
||||
{ source: 'title', agent: 'codex', run: currentRun }
|
||||
]
|
||||
})
|
||||
|
||||
expect(result).toEqual({
|
||||
hostKind: 'native',
|
||||
launchMode: 'orca-launch',
|
||||
sourceMask: 64,
|
||||
identityNull: false,
|
||||
ambiguousTopRank: false
|
||||
})
|
||||
})
|
||||
|
||||
it('uses same-run owner compatibility and preserves relay baselines', () => {
|
||||
const key = { authorityId: 'a', incarnation: 1 }
|
||||
expect(
|
||||
assemblePaneAgentIdentityEvidence({
|
||||
currentRun: key,
|
||||
owner: { agent: 'omp', run: key },
|
||||
facts: [{ source: 'launch', agent: 'pi', run: key }]
|
||||
}).resolvedAgent
|
||||
).toBe('omp')
|
||||
expect(
|
||||
assemblePaneAgentIdentityEvidence({
|
||||
currentRun: key,
|
||||
owner: { agent: 'claude', run: key },
|
||||
facts: [{ source: 'launch', agent: 'codex', run: key }]
|
||||
}).resolvedAgent
|
||||
).toBe('codex')
|
||||
const merger = new PaneAgentIdentityRelaySnapshotMerger()
|
||||
const snapshot = {
|
||||
epoch: 'e',
|
||||
revision: 1,
|
||||
rows: [['relay', 'typed', 1, 1, 0, 1, 0] as const]
|
||||
}
|
||||
expect(merger.merge('env', snapshot)).toEqual({ kind: 'baseline' })
|
||||
expect(merger.merge('env', { ...snapshot, revision: 0 })).toEqual({ kind: 'stale' })
|
||||
expect(
|
||||
merger.merge('env', {
|
||||
...snapshot,
|
||||
revision: 2,
|
||||
rows: [['relay', 'typed', 2, 1, 0, 1, 0] as const]
|
||||
})
|
||||
).toMatchObject({ kind: 'delta' })
|
||||
expect(merger.merge('env', { ...snapshot, revision: 3, epoch: 'new' })).toEqual({
|
||||
kind: 'failed-closed',
|
||||
reason: 'epoch-changed'
|
||||
})
|
||||
})
|
||||
|
||||
it('fail-closes cumulative counter regressions without moving the baseline backward', () => {
|
||||
const merger = new PaneAgentIdentityRelaySnapshotMerger()
|
||||
const row = ['relay', 'typed', 5, 1, 0, 1, 0] as const
|
||||
expect(merger.merge('env', { epoch: 'e', revision: 1, rows: [row] })).toEqual({
|
||||
kind: 'baseline'
|
||||
})
|
||||
expect(
|
||||
merger.merge('env', {
|
||||
epoch: 'e',
|
||||
revision: 2,
|
||||
rows: [['relay', 'typed', 4, 1, 0, 1, 0]]
|
||||
})
|
||||
).toEqual({ kind: 'failed-closed', reason: 'counter-regressed' })
|
||||
expect(
|
||||
merger.merge('env', {
|
||||
epoch: 'e',
|
||||
revision: 3,
|
||||
rows: [['relay', 'typed', 6, 1, 0, 1, 0]]
|
||||
})
|
||||
).toEqual({ kind: 'failed-closed', reason: 'counter-regressed' })
|
||||
})
|
||||
|
||||
it('merges candidate coverage as a cumulative delta and fail-closes regressions', () => {
|
||||
const merger = new PaneAgentIdentityRelaySnapshotMerger()
|
||||
const baseline = {
|
||||
epoch: 'e',
|
||||
revision: 1,
|
||||
rows: [],
|
||||
candidateCoverage: [['relay', 2] as const]
|
||||
}
|
||||
expect(merger.merge('env', baseline)).toEqual({ kind: 'baseline' })
|
||||
expect(
|
||||
merger.merge('env', {
|
||||
...baseline,
|
||||
revision: 2,
|
||||
candidateCoverage: [['relay', 5] as const]
|
||||
})
|
||||
).toEqual({
|
||||
kind: 'delta',
|
||||
rows: [],
|
||||
candidateCoverage: [{ hostKind: 'relay', exposures: 3 }]
|
||||
})
|
||||
expect(
|
||||
merger.merge('env', {
|
||||
...baseline,
|
||||
revision: 3,
|
||||
candidateCoverage: [['relay', 4] as const]
|
||||
})
|
||||
).toEqual({ kind: 'failed-closed', reason: 'counter-regressed' })
|
||||
})
|
||||
|
||||
it('strictly validates privacy-reduced snapshots and optional candidate coverage', () => {
|
||||
const snapshot = {
|
||||
epoch: 'e',
|
||||
revision: 1,
|
||||
rows: [['relay', 'typed', 1, 0, 0, 0, 0] as const],
|
||||
candidateCoverage: [['ssh', 1] as const]
|
||||
}
|
||||
expect(isPaneAgentIdentityAvailabilitySnapshot(snapshot)).toBe(true)
|
||||
expect(
|
||||
isPaneAgentIdentityAvailabilitySnapshot({ ...snapshot, rawTitle: 'private title' })
|
||||
).toBe(false)
|
||||
expect(
|
||||
isPaneAgentIdentityAvailabilitySnapshot({
|
||||
...snapshot,
|
||||
candidateCoverage: [['ssh', 1, '/private']]
|
||||
})
|
||||
).toBe(false)
|
||||
expect(
|
||||
isPaneAgentIdentityAvailabilitySnapshot({
|
||||
...snapshot,
|
||||
rows: [['relay', 'typed', 1, 2, 0, 0, 0]]
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts only bounded enum-and-count telemetry rows', () => {
|
||||
const valid = {
|
||||
rows: [
|
||||
{
|
||||
kind: 'coverage' as const,
|
||||
host_kind: 'relay' as const,
|
||||
reason: 'counter_regressed' as const,
|
||||
count: 1
|
||||
}
|
||||
]
|
||||
}
|
||||
expect(eventSchemas.pane_agent_identity_availability.safeParse(valid).success).toBe(true)
|
||||
expect(
|
||||
eventSchemas.pane_agent_identity_availability.safeParse({
|
||||
rows: [{ ...valid.rows[0], title: 'private' }]
|
||||
}).success
|
||||
).toBe(false)
|
||||
expect(
|
||||
eventSchemas.pane_agent_identity_availability.safeParse({
|
||||
rows: [{ ...valid.rows[0], count: '1' }]
|
||||
}).success
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('retains bounded command edit semantics and discards input after classification', () => {
|
||||
const tracker = new ManualAgentCommandTracker()
|
||||
expect(tracker.ingest('codex\r')).toEqual(['codex'])
|
||||
expect(tracker.ingest('cod\u0008dex\r')).toEqual(['codex'])
|
||||
expect(tracker.ingest('claude\rcodex\r')).toEqual(['claude', 'codex'])
|
||||
|
||||
tracker.ingest('x'.repeat(MANUAL_AGENT_COMMAND_MAX_CHARS))
|
||||
expect(tracker.ingest('y\rcodex\r')).toEqual([])
|
||||
expect(tracker.ingest('\r')).toEqual([])
|
||||
expect(tracker.ingest('codex\r')).toEqual([])
|
||||
tracker.ingest('\x15')
|
||||
expect(tracker.ingest('codex\r')).toEqual(['codex'])
|
||||
})
|
||||
|
||||
it('only clears a pending command when cancelling suspended inference', () => {
|
||||
const tracker = new ManualAgentCommandTracker()
|
||||
tracker.ingest('co')
|
||||
tracker.cancelSuspendedInference()
|
||||
expect(tracker.ingest('dex\r')).toEqual(['codex'])
|
||||
|
||||
tracker.ingest('x'.repeat(MANUAL_AGENT_COMMAND_MAX_CHARS + 1))
|
||||
tracker.cancelSuspendedInference()
|
||||
expect(tracker.ingest('claude\r')).toEqual(['claude'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,139 @@
|
||||
import {
|
||||
PANE_AGENT_EVIDENCE_SOURCES,
|
||||
type PaneAgentEvidence,
|
||||
type PaneAgentEvidenceSource,
|
||||
type PaneAgentRunKey,
|
||||
resolvePaneAgentIdentity
|
||||
} from './pane-agent-identity-resolver'
|
||||
import { resolveCompatibleAgentTypeForOwner } from './agent-title-owner'
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
|
||||
export type PaneAgentIdentityHostKind = 'native' | 'wsl-host' | 'wsl-distro' | 'ssh' | 'relay'
|
||||
export type PaneAgentIdentityLaunchMode = 'typed' | 'orca-launch' | 'resume'
|
||||
|
||||
type PaneAgentIdentityNonProcessFact = Omit<PaneAgentEvidence<TuiAgent>, 'source'> & {
|
||||
source: Exclude<PaneAgentEvidenceSource, 'process'>
|
||||
executionHostProvenance?: never
|
||||
}
|
||||
type PaneAgentIdentityProcessFact = Omit<PaneAgentEvidence<TuiAgent>, 'source'> & {
|
||||
source: 'process'
|
||||
/** Process observations must identify the execution host that produced them. */
|
||||
executionHostProvenance: 'execution-host' | 'distro-origin' | 'target-origin'
|
||||
}
|
||||
export type PaneAgentIdentityEvidenceFact =
|
||||
| PaneAgentIdentityNonProcessFact
|
||||
| PaneAgentIdentityProcessFact
|
||||
|
||||
export type PaneAgentIdentityOwnerFact = {
|
||||
agent: TuiAgent
|
||||
run: PaneAgentRunKey
|
||||
}
|
||||
|
||||
export type PaneAgentIdentityEvidenceInput = {
|
||||
facts: readonly PaneAgentIdentityEvidenceFact[]
|
||||
currentRun?: PaneAgentRunKey
|
||||
owner?: PaneAgentIdentityOwnerFact
|
||||
}
|
||||
|
||||
/** Stable encoding. Do not derive telemetry bits from array indexes. */
|
||||
export const PANE_AGENT_IDENTITY_SOURCE_BITS: Readonly<Record<PaneAgentEvidenceSource, number>> = {
|
||||
'live-hook': 1,
|
||||
process: 2,
|
||||
launch: 4,
|
||||
'completed-hook': 8,
|
||||
'sleeping-session': 16,
|
||||
sibling: 32,
|
||||
title: 64
|
||||
}
|
||||
|
||||
export type PaneAgentIdentityEvidence = {
|
||||
evidence: readonly PaneAgentEvidence<TuiAgent>[]
|
||||
resolvedAgent: TuiAgent | null
|
||||
sourceMask: number
|
||||
identityNull: boolean
|
||||
ambiguousTopRank: boolean
|
||||
}
|
||||
|
||||
/** Assemble already-redacted authority facts; raw titles/commands never enter this layer. */
|
||||
export function assemblePaneAgentIdentityEvidence(
|
||||
input: PaneAgentIdentityEvidenceInput
|
||||
): PaneAgentIdentityEvidence {
|
||||
const evidence = input.facts
|
||||
.filter((fact) => {
|
||||
if (fact.source === 'process' && fact.executionHostProvenance === undefined) {
|
||||
return false
|
||||
}
|
||||
if (fact.source === 'sibling') {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
input.currentRun &&
|
||||
fact.run?.authorityId === input.currentRun.authorityId &&
|
||||
fact.run.incarnation !== input.currentRun.incarnation
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
.sort(
|
||||
(left, right) =>
|
||||
PANE_AGENT_EVIDENCE_SOURCES.indexOf(left.source) -
|
||||
PANE_AGENT_EVIDENCE_SOURCES.indexOf(right.source)
|
||||
)
|
||||
const resolved = resolvePaneAgentIdentity({
|
||||
evidence,
|
||||
currentRun: input.currentRun
|
||||
})
|
||||
let resolvedAgent = resolved.agent
|
||||
if (
|
||||
resolvedAgent &&
|
||||
input.owner &&
|
||||
input.currentRun &&
|
||||
input.owner.run.authorityId === input.currentRun.authorityId &&
|
||||
input.owner.run.incarnation === input.currentRun.incarnation
|
||||
) {
|
||||
// Owner compatibility is deliberately same-run only; incomparable owners do not rewrite.
|
||||
if (
|
||||
input.owner.agent !== resolvedAgent &&
|
||||
resolveCompatibleAgentTypeForOwner(resolvedAgent, input.owner.agent) === input.owner.agent
|
||||
) {
|
||||
resolvedAgent = input.owner.agent
|
||||
}
|
||||
}
|
||||
return {
|
||||
evidence,
|
||||
resolvedAgent,
|
||||
sourceMask: evidence.reduce(
|
||||
(mask, item) => mask | PANE_AGENT_IDENTITY_SOURCE_BITS[item.source],
|
||||
0
|
||||
),
|
||||
identityNull: resolved.agent === null,
|
||||
ambiguousTopRank: resolved.ambiguousAt !== undefined
|
||||
}
|
||||
}
|
||||
|
||||
export type PaneAgentIdentityAvailability = {
|
||||
hostKind: PaneAgentIdentityHostKind
|
||||
launchMode: PaneAgentIdentityLaunchMode
|
||||
sourceMask: number
|
||||
identityNull: boolean
|
||||
ambiguousTopRank: boolean
|
||||
}
|
||||
|
||||
export function reducePaneAgentIdentityAvailability(
|
||||
hostKind: PaneAgentIdentityHostKind,
|
||||
launchMode: PaneAgentIdentityLaunchMode,
|
||||
input: PaneAgentIdentityEvidenceInput
|
||||
): PaneAgentIdentityAvailability {
|
||||
const assembled = assemblePaneAgentIdentityEvidence(input)
|
||||
return {
|
||||
hostKind,
|
||||
launchMode,
|
||||
sourceMask: assembled.sourceMask,
|
||||
identityNull: assembled.identityNull,
|
||||
ambiguousTopRank: assembled.ambiguousTopRank
|
||||
}
|
||||
}
|
||||
|
||||
export const PANE_AGENT_TITLE_SOURCE_BIT = PANE_AGENT_IDENTITY_SOURCE_BITS.title
|
||||
export const PANE_AGENT_IDENTITY_SOURCE_ORDER = PANE_AGENT_EVIDENCE_SOURCES
|
||||
@@ -0,0 +1,145 @@
|
||||
import type { PaneAgentIdentityHostKind } from './pane-agent-identity-evidence'
|
||||
|
||||
export const RUN_SETTLE_MS = 5_000
|
||||
export const TITLE_CANDIDATE_WINDOW_MS = 30_000
|
||||
export const MAX_PENDING_IDENTITY_OBSERVATIONS = 4_096
|
||||
export const MAX_LIVE_FINALIZED_RUN_KEYS = 4_096
|
||||
|
||||
export type PaneAgentIdentityObservationSchedulerDeps = {
|
||||
now?: () => number
|
||||
onRunFreeze: (key: string) => void
|
||||
onTitleCandidateFreeze: (hostKind: PaneAgentIdentityHostKind, key: string) => void
|
||||
onFinalizedOverflow?: (
|
||||
kind: 'run' | 'title-candidate',
|
||||
hostKind: PaneAgentIdentityHostKind | undefined,
|
||||
key: string
|
||||
) => void
|
||||
}
|
||||
|
||||
/** Event-driven settle/deadline scheduler; it never probes or polls a host. */
|
||||
export class PaneAgentIdentityObservationScheduler {
|
||||
private readonly pending = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
private readonly candidates = new Map<string, ReturnType<typeof setTimeout>>()
|
||||
private readonly finalized = new Set<string>()
|
||||
private readonly now: () => number
|
||||
constructor(private readonly deps: PaneAgentIdentityObservationSchedulerDeps) {
|
||||
this.now = deps.now ?? Date.now
|
||||
}
|
||||
scheduleRun(key: string, settleMs = RUN_SETTLE_MS): boolean {
|
||||
if (
|
||||
this.pending.size + this.candidates.size >= MAX_PENDING_IDENTITY_OBSERVATIONS &&
|
||||
!this.pending.has(key)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (this.finalized.has(key)) {
|
||||
return false
|
||||
}
|
||||
this.cancelRun(key)
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
if (!this.freezeRun(key)) {
|
||||
this.deps.onFinalizedOverflow?.('run', undefined, key)
|
||||
}
|
||||
},
|
||||
Math.max(0, settleMs)
|
||||
)
|
||||
timer.unref?.()
|
||||
this.pending.set(key, timer)
|
||||
return true
|
||||
}
|
||||
scheduleTitleCandidate(
|
||||
key: string,
|
||||
hostKind: PaneAgentIdentityHostKind,
|
||||
windowMs = TITLE_CANDIDATE_WINDOW_MS
|
||||
): boolean {
|
||||
if (
|
||||
this.pending.size + this.candidates.size >= MAX_PENDING_IDENTITY_OBSERVATIONS &&
|
||||
!this.candidates.has(key)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (this.candidates.has(key)) {
|
||||
return true
|
||||
}
|
||||
const timer = setTimeout(
|
||||
() => {
|
||||
if (!this.freezeTitleCandidate(key, hostKind)) {
|
||||
this.deps.onFinalizedOverflow?.('title-candidate', hostKind, key)
|
||||
}
|
||||
},
|
||||
Math.max(0, windowMs)
|
||||
)
|
||||
timer.unref?.()
|
||||
this.candidates.set(key, timer)
|
||||
return true
|
||||
}
|
||||
attestRun(key: string): boolean {
|
||||
return this.scheduleRun(key)
|
||||
}
|
||||
freezeRun(key: string): boolean {
|
||||
if (this.finalized.has(key)) {
|
||||
return false
|
||||
}
|
||||
if (this.finalized.size >= MAX_LIVE_FINALIZED_RUN_KEYS) {
|
||||
this.cancelRun(key)
|
||||
return false
|
||||
}
|
||||
this.cancelRun(key)
|
||||
this.finalized.add(key)
|
||||
this.deps.onRunFreeze(key)
|
||||
return true
|
||||
}
|
||||
freezeTitleCandidate(key: string, hostKind: PaneAgentIdentityHostKind): boolean {
|
||||
if (this.finalized.has(key)) {
|
||||
return false
|
||||
}
|
||||
if (this.finalized.size >= MAX_LIVE_FINALIZED_RUN_KEYS) {
|
||||
this.cancelCandidate(key)
|
||||
return false
|
||||
}
|
||||
this.cancelCandidate(key)
|
||||
this.finalized.add(key)
|
||||
this.deps.onTitleCandidateFreeze(hostKind, key)
|
||||
return true
|
||||
}
|
||||
cancelCandidate(key: string): void {
|
||||
const timer = this.candidates.get(key)
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
this.candidates.delete(key)
|
||||
}
|
||||
cancelRun(key: string): void {
|
||||
const timer = this.pending.get(key)
|
||||
if (timer) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
this.pending.delete(key)
|
||||
}
|
||||
exitOrRebind(key: string): void {
|
||||
this.cancelRun(key)
|
||||
this.cancelCandidate(key)
|
||||
this.finalized.delete(key)
|
||||
}
|
||||
shutdown(): void {
|
||||
for (const timer of this.pending.values()) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
for (const timer of this.candidates.values()) {
|
||||
clearTimeout(timer)
|
||||
}
|
||||
this.pending.clear()
|
||||
this.candidates.clear()
|
||||
this.finalized.clear()
|
||||
}
|
||||
isPendingRun(key: string): boolean {
|
||||
return this.pending.has(key)
|
||||
}
|
||||
isPendingCandidate(key: string): boolean {
|
||||
return this.candidates.has(key)
|
||||
}
|
||||
get clock(): number {
|
||||
return this.now()
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import type { TabGroupLayoutNode } from './tab-types'
|
||||
import type { TerminalExitCause } from './terminal-exit-cause'
|
||||
import type { TerminalPaneLayoutNode } from './terminal-tab-types'
|
||||
import type { TuiAgent } from './tui-agent'
|
||||
import type { PaneAgentIdentityAvailabilitySnapshot } from './pane-agent-identity-availability'
|
||||
|
||||
export type RuntimeTerminalSummary = {
|
||||
handle: string
|
||||
@@ -93,6 +94,8 @@ export type RuntimeTerminalListResult = {
|
||||
truncated: boolean
|
||||
/** Absent from hosts that predate the field; treat that scope as unverifiable. */
|
||||
hostScope?: RuntimeTerminalListHostScope
|
||||
/** Optional cumulative, privacy-reduced pane identity census from the authority. */
|
||||
agentIdentityAvailability?: PaneAgentIdentityAvailabilitySnapshot
|
||||
}
|
||||
|
||||
export type RuntimeTerminalOrphanAdoptionClaim = {
|
||||
|
||||
@@ -1351,6 +1351,45 @@ const terminalPaneSplitSchema = z
|
||||
})
|
||||
.strict()
|
||||
|
||||
const paneAgentIdentityHostKindSchema = z.enum(['native', 'wsl-host', 'wsl-distro', 'ssh', 'relay'])
|
||||
const paneAgentIdentityLaunchModeSchema = z.enum(['typed', 'orca-launch', 'resume'])
|
||||
const paneAgentIdentityAggregateRowSchema = z
|
||||
.object({
|
||||
kind: z.literal('aggregate'),
|
||||
host_kind: paneAgentIdentityHostKindSchema,
|
||||
launch_mode: paneAgentIdentityLaunchModeSchema,
|
||||
attested_runs: z.number().int().min(1).max(1_000_000_000),
|
||||
no_evidence: z.number().int().min(0).max(1_000_000_000),
|
||||
title_only: z.number().int().min(0).max(1_000_000_000),
|
||||
identity_null: z.number().int().min(0).max(1_000_000_000),
|
||||
ambiguous_top_rank: z.number().int().min(0).max(1_000_000_000)
|
||||
})
|
||||
.strict()
|
||||
const paneAgentIdentityCoverageRowSchema = z
|
||||
.object({
|
||||
kind: z.literal('coverage'),
|
||||
host_kind: paneAgentIdentityHostKindSchema,
|
||||
reason: z.enum([
|
||||
'snapshot',
|
||||
'baseline',
|
||||
'epoch_changed',
|
||||
'counter_regressed',
|
||||
'capacity',
|
||||
'candidate',
|
||||
'overflow',
|
||||
'truncated'
|
||||
]),
|
||||
count: z.number().int().min(1).max(1_000_000_000)
|
||||
})
|
||||
.strict()
|
||||
const paneAgentIdentityAvailabilitySchema = z
|
||||
.object({
|
||||
rows: z
|
||||
.array(z.union([paneAgentIdentityAggregateRowSchema, paneAgentIdentityCoverageRowSchema]))
|
||||
.max(64)
|
||||
})
|
||||
.strict()
|
||||
|
||||
// Why: measures the changed-on-disk conflict flow (issue #7265) per transport; deliberately path-free.
|
||||
const editorExternalChangeConflictShownSchema = z
|
||||
.object({
|
||||
@@ -1518,6 +1557,7 @@ export const eventSchemas = {
|
||||
editor_external_change_conflict_action: editorExternalChangeConflictActionSchema,
|
||||
|
||||
direct_ssh_reconnect_operation: directSshReconnectOperationSchema,
|
||||
pane_agent_identity_availability: paneAgentIdentityAvailabilitySchema,
|
||||
|
||||
smart_sort_class_distribution: smartSortClassDistributionSchema,
|
||||
smart_sort_class_1_promotion: smartSortClass1PromotionSchema,
|
||||
|
||||
Reference in New Issue
Block a user