mirror of
https://github.com/stablyai/orca.git
synced 2026-09-27 08:02:35 +00:00
refactor(agent-status): isolate legacy status ingress behind one admission point (#20716)
* refactor(agent-status): isolate legacy status ingress * fix(agent-hooks): move advertised-capability source onto the ingest envelope ingestRemote() gained a third positional argument in this PR (advertisedAgentStatusCapabilities) to satisfy a new ratchet requiring every legacy-ingress call site to name its capability source. Both production callers pass the same constant every time, so the argument carries zero runtime information — but Vitest's toHaveBeenCalledWith matches argument count exactly, so the pre-existing SSH relay integration test (which asserts a 2-argument call) started failing even though nothing about the actual admission decision changed. Capabilities are a property of the producing peer/connection, not an orthogonal call parameter, so move the field onto the envelope object instead of adding a third positional argument: ingestRemote reads envelope.advertisedAgentStatusCapabilities (defaulting to the unadvertised-legacy-peer set), and both call sites stamp the constant onto their envelope literal. Call arity stays at two arguments, so the pre-existing evidence test needs no change. The envelope never crosses the wire in either caller: SSH rebuilds it field-by-field from the RPC params, and the WSL path copies (never mutates) the wire-deserialized notification before stamping the field on, so this is purely an internal main-process shape change. Also strengthens the ingress ratchet test that required this: it previously only checked that the capability constant's name appeared somewhere in each caller's source, which a stray unused import could satisfy. It now asserts the actual `advertisedAgentStatusCapabilities: AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES` key:value binding is present.
This commit is contained in:
@@ -4,8 +4,11 @@ import { join } from 'node:path'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { RelayAgentHookServer } from '../../relay/agent-hook-server'
|
||||
import { seedLegacyAgentStatusForTests } from '../../shared/agent-hook-listener/listener-state'
|
||||
import { seedClaudeSubagentRosterFromSnapshots } from '../../shared/agent-hook-listener/providers/claude-roster-state'
|
||||
import type { AgentHookEventPayload } from '../../shared/agent-hook-listener/listener-event'
|
||||
import type { AgentHookRelayEnvelope } from '../../shared/agent-hook-relay'
|
||||
import type { AgentSubagentSnapshot } from '../../shared/agent-status-types'
|
||||
import { makePaneKey } from '../../shared/stable-pane-id'
|
||||
import { AgentHookServer } from './server'
|
||||
|
||||
@@ -71,8 +74,10 @@ function legacyRelayCompactEnvelope(
|
||||
* the turn had spawned — restored from disk, so proof of nothing. */
|
||||
function seedHydratedStuckPane(server: AgentHookServer, receivedAt: number) {
|
||||
const state = server._getStateForTests()
|
||||
const subagents = [{ id: 'child-1', state: 'working', startedAt: 0, agentType: 'general' }]
|
||||
state.lastStatusByPaneKey.set(PANE_KEY, {
|
||||
const subagents: AgentSubagentSnapshot[] = [
|
||||
{ id: 'child-1', state: 'working', startedAt: 0, agentType: 'general' }
|
||||
]
|
||||
const status = {
|
||||
paneKey: PANE_KEY,
|
||||
source: 'claude',
|
||||
connectionId: null,
|
||||
@@ -82,8 +87,9 @@ function seedHydratedStuckPane(server: AgentHookServer, receivedAt: number) {
|
||||
restoredUnconfirmed: true,
|
||||
receivedAt,
|
||||
payload: { state: 'working', prompt: 'work before the restart', agentType: 'claude', subagents }
|
||||
} as never)
|
||||
seedClaudeSubagentRosterFromSnapshots(state, PANE_KEY, subagents as never)
|
||||
} satisfies AgentHookEventPayload & { receivedAt: number }
|
||||
seedLegacyAgentStatusForTests(state, status)
|
||||
seedClaudeSubagentRosterFromSnapshots(state, PANE_KEY, subagents)
|
||||
}
|
||||
|
||||
describe('manual Claude compact hook stream', () => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import type { AgentHookEventPayload } from '../../shared/agent-hook-listener/listener-event'
|
||||
import { seedLegacyAgentStatusForTests } from '../../shared/agent-hook-listener/listener-state'
|
||||
import { makePaneKey } from '../../shared/stable-pane-id'
|
||||
import { AgentHookServer } from './server'
|
||||
|
||||
@@ -30,7 +31,7 @@ describe('AgentHookServer authority evidence', () => {
|
||||
receivedAt: 100,
|
||||
stateStartedAt: 100
|
||||
} satisfies AgentHookEventPayload & { receivedAt: number; stateStartedAt: number }
|
||||
server._getStateForTests().lastStatusByPaneKey.set(PANE_KEY, hydrated)
|
||||
seedLegacyAgentStatusForTests(server._getStateForTests(), hydrated)
|
||||
|
||||
await server.start()
|
||||
const commitments = server.getHydratedAuthorityCommitments()
|
||||
@@ -195,7 +196,7 @@ describe('AgentHookServer authority evidence', () => {
|
||||
receivedAt: 100,
|
||||
stateStartedAt: 100
|
||||
} satisfies AgentHookEventPayload & { receivedAt: number; stateStartedAt: number }
|
||||
server._getStateForTests().lastStatusByPaneKey.set(PANE_KEY, hydrated)
|
||||
seedLegacyAgentStatusForTests(server._getStateForTests(), hydrated)
|
||||
server.registerPaneKeyAlias('tab-authority:0', PANE_KEY, 'old-pty')
|
||||
await server.start()
|
||||
server.ingestRemote(
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
parseAgentStatusPayload
|
||||
} from '../../shared/agent-status-types'
|
||||
import { PANE } from './server.test-fixtures'
|
||||
import { AGENT_STATUS_RUNS_RUNTIME_CAPABILITY } from '../../shared/agent-status-run-capability'
|
||||
|
||||
const { getCohortAtEmitMock, trackMock } = vi.hoisted(() => ({
|
||||
getCohortAtEmitMock: vi.fn(),
|
||||
@@ -644,4 +645,32 @@ describe('AgentHookServer ingestRemote', () => {
|
||||
const event = listener.mock.calls[0][0] as { payload: { prompt: string } }
|
||||
expect(event.payload.prompt.length).toBe(200)
|
||||
})
|
||||
|
||||
it('never falls back to the legacy writer for a run-capable peer', () => {
|
||||
const server = new AgentHookServer()
|
||||
server.ingestRemote(
|
||||
{
|
||||
paneKey: PANE,
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'wt-1',
|
||||
advertisedAgentStatusCapabilities: [],
|
||||
payload: { state: 'working', prompt: 'unsupported peer', agentType: 'claude' }
|
||||
},
|
||||
'conn-1'
|
||||
)
|
||||
const olderPeerRow = server.getStatusSnapshot()[0]
|
||||
|
||||
server.ingestRemote(
|
||||
{
|
||||
paneKey: PANE,
|
||||
tabId: 'tab-1',
|
||||
worktreeId: 'wt-1',
|
||||
advertisedAgentStatusCapabilities: [AGENT_STATUS_RUNS_RUNTIME_CAPABILITY],
|
||||
payload: { state: 'done', prompt: 'capable peer', agentType: 'claude' }
|
||||
},
|
||||
'conn-1'
|
||||
)
|
||||
|
||||
expect(server.getStatusSnapshot()).toEqual([olderPeerRow])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { movePaneCacheState } from '../../../shared/agent-hook-listener/listener-state'
|
||||
import {
|
||||
admitLegacyAgentStatus,
|
||||
movePaneCacheState
|
||||
} from '../../../shared/agent-hook-listener/listener-state'
|
||||
import { AGENT_STATUS_2A_CURRENT_PRODUCER_MODE } from '../../../shared/agent-status-legacy-adapter'
|
||||
import { canRegisterPaneKeyAlias, isOpaqueRemintedPaneKey } from '../../../shared/pane-key-alias'
|
||||
import { parsePaneKey } from '../../../shared/stable-pane-id'
|
||||
import { PANE_KEY_ALIASES_MAX } from './server-constants'
|
||||
@@ -152,11 +156,16 @@ export abstract class AgentHookServerAuthorityAliases extends AgentHookServerAut
|
||||
| undefined
|
||||
if (movedStatus) {
|
||||
const owner = parsePaneKey(toPaneKey)
|
||||
this.state.lastStatusByPaneKey.set(toPaneKey, {
|
||||
...movedStatus,
|
||||
paneKey: toPaneKey,
|
||||
tabId: owner?.tabId
|
||||
})
|
||||
admitLegacyAgentStatus(
|
||||
this.state,
|
||||
'main-pane-alias-transfer',
|
||||
{
|
||||
...movedStatus,
|
||||
paneKey: toPaneKey,
|
||||
tabId: owner?.tabId
|
||||
},
|
||||
AGENT_STATUS_2A_CURRENT_PRODUCER_MODE
|
||||
)
|
||||
}
|
||||
const transferredStatus = this.state.lastStatusByPaneKey.get(toPaneKey) as
|
||||
| EnrichedAgentHookEventPayload
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { paneHasStateClaims } from '../../../shared/agent-hook-listener/listener-state'
|
||||
import {
|
||||
admitLegacyAgentStatus,
|
||||
deleteLegacyAgentStatus,
|
||||
paneHasStateClaims
|
||||
} from '../../../shared/agent-hook-listener/listener-state'
|
||||
import { AGENT_STATUS_2A_CURRENT_PRODUCER_MODE } from '../../../shared/agent-status-legacy-adapter'
|
||||
import type { AgentStatusCacheIdentity } from '../../../shared/agent-status-types'
|
||||
import type { EnrichedAgentHookEventPayload } from './server-types'
|
||||
import { AgentHookServerAuthorityFences } from './server-authority-fences'
|
||||
@@ -35,7 +40,12 @@ export abstract class AgentHookServerCleanup extends AgentHookServerAuthorityFen
|
||||
const retained =
|
||||
options?.preserveResumeIdentity === false ? null : this.toRetainedProviderSessionRow(deleted)
|
||||
if (retained) {
|
||||
this.state.lastStatusByPaneKey.set(deleted.paneKey, retained)
|
||||
admitLegacyAgentStatus(
|
||||
this.state,
|
||||
'main-status-cleanup',
|
||||
retained,
|
||||
AGENT_STATUS_2A_CURRENT_PRODUCER_MODE
|
||||
)
|
||||
}
|
||||
this.commitStatusRowMutation(deleted, retained)
|
||||
this.scheduleStatusPersist()
|
||||
@@ -73,7 +83,12 @@ export abstract class AgentHookServerCleanup extends AgentHookServerAuthorityFen
|
||||
}
|
||||
const retained = this.toRetainedProviderSessionRow(deleted)
|
||||
if (retained) {
|
||||
this.state.lastStatusByPaneKey.set(deleted.paneKey, retained)
|
||||
admitLegacyAgentStatus(
|
||||
this.state,
|
||||
'main-status-cleanup',
|
||||
retained,
|
||||
AGENT_STATUS_2A_CURRENT_PRODUCER_MODE
|
||||
)
|
||||
}
|
||||
this.commitStatusRowMutation(deleted, retained)
|
||||
evicted.push(deleted.paneKey)
|
||||
@@ -126,7 +141,12 @@ export abstract class AgentHookServerCleanup extends AgentHookServerAuthorityFen
|
||||
| undefined
|
||||
this.clearPaneState(resolvedPaneKey, { emitStatusRowMutation: false })
|
||||
if (retained) {
|
||||
this.state.lastStatusByPaneKey.set(resolvedPaneKey, retained)
|
||||
admitLegacyAgentStatus(
|
||||
this.state,
|
||||
'main-status-cleanup',
|
||||
retained,
|
||||
AGENT_STATUS_2A_CURRENT_PRODUCER_MODE
|
||||
)
|
||||
this.scheduleStatusPersist()
|
||||
this.notifyStatusChangeListeners()
|
||||
}
|
||||
@@ -208,7 +228,7 @@ export abstract class AgentHookServerCleanup extends AgentHookServerAuthorityFen
|
||||
if (!existing) {
|
||||
return null
|
||||
}
|
||||
this.state.lastStatusByPaneKey.delete(resolvedPaneKey)
|
||||
deleteLegacyAgentStatus(this.state, resolvedPaneKey)
|
||||
this.activeHookTurnCompletedAtByPaneKey.delete(resolvedPaneKey)
|
||||
if (!options?.preserveAuthority) {
|
||||
this.hydratedLaunchTokenHashByPaneKey.delete(resolvedPaneKey)
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import {
|
||||
admitLegacyAgentStatus,
|
||||
clearLegacyAgentStatuses
|
||||
} from '../../../shared/agent-hook-listener/listener-state'
|
||||
import {
|
||||
seedClaudeLeadTurnFromPersistedStatus,
|
||||
seedClaudeSubagentRosterFromSnapshots
|
||||
} from '../../../shared/agent-hook-listener/providers/claude-roster-state'
|
||||
import { seedCodexStateFromSnapshot } from '../../../shared/agent-hook-listener/providers/codex-state'
|
||||
import { AGENT_STATUS_PERSISTED_HYDRATION_MODE } from '../../../shared/agent-status-legacy-adapter'
|
||||
import { HYDRATE_MAX_AGE_MS, LAST_STATUS_FILE_VERSION } from './server-constants'
|
||||
import type { LastStatusFile } from './server-types'
|
||||
import {
|
||||
@@ -23,7 +28,7 @@ export abstract class AgentHookServerHydration extends AgentHookServerReaping {
|
||||
return
|
||||
}
|
||||
// Why: keep hydrate idempotent so a future re-start path can't merge prior-session state.
|
||||
this.state.lastStatusByPaneKey.clear()
|
||||
clearLegacyAgentStatuses(this.state)
|
||||
this.hydratedLaunchTokenHashByPaneKey.clear()
|
||||
this.persistedAuthorityCommitmentsByPaneKey.clear()
|
||||
let raw: string
|
||||
@@ -100,7 +105,12 @@ export abstract class AgentHookServerHydration extends AgentHookServerReaping {
|
||||
// Why: the terminal transition may have fired while no receiver was up; restore as unconfirmed, never as live truth.
|
||||
entry.restoredUnconfirmed = true
|
||||
}
|
||||
this.state.lastStatusByPaneKey.set(resolvedPaneKey, entry)
|
||||
admitLegacyAgentStatus(
|
||||
this.state,
|
||||
'main-status-hydration',
|
||||
entry,
|
||||
AGENT_STATUS_PERSISTED_HYDRATION_MODE
|
||||
)
|
||||
if (entry.connectionId) {
|
||||
// Why: a restart can see an earlier wall clock; seed ordering so new events stay after disk state.
|
||||
const previousWatermark = this.connectionTimestampWatermarkById.get(entry.connectionId)
|
||||
|
||||
@@ -17,6 +17,11 @@ import {
|
||||
import { launchTokenHash } from '../../../shared/agent-hook-spool'
|
||||
import { parsePaneKey } from '../../../shared/stable-pane-id'
|
||||
import type { AgentHookEventPayload } from '../../../shared/agent-hook-listener/listener-event'
|
||||
import {
|
||||
AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES,
|
||||
canAdmitLegacyAgentStatus,
|
||||
olderPeerAgentStatusLegacyMode
|
||||
} from '../../../shared/agent-status-legacy-adapter'
|
||||
import { isValidPiProviderSessionOnly } from './server-status-identity'
|
||||
import { AgentHookServerIngestStructured } from './server-ingest-structured'
|
||||
|
||||
@@ -47,10 +52,23 @@ export abstract class AgentHookServerIngestRemote extends AgentHookServerIngestS
|
||||
/** Payload fields the relay dropped to fit an oversized frame; validated below. */
|
||||
shedFields?: unknown
|
||||
claudeRunningNonAgentTask?: unknown
|
||||
/** The producing peer's advertised run-capability set — a property of the peer/connection that built this envelope, not an orthogonal call parameter. Absent (older relay/HTTP paths) defaults to the unadvertised-legacy-peer set. */
|
||||
advertisedAgentStatusCapabilities?: readonly string[]
|
||||
payload: unknown
|
||||
},
|
||||
connectionId: string | null
|
||||
): void {
|
||||
if (
|
||||
!canAdmitLegacyAgentStatus(
|
||||
'main-status-update',
|
||||
olderPeerAgentStatusLegacyMode(
|
||||
envelope?.advertisedAgentStatusCapabilities ??
|
||||
AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES
|
||||
)
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
// Why: wire crosses a trust boundary — re-check/trim so an empty connectionId can't poison caches.
|
||||
if (connectionId !== null && typeof connectionId !== 'string') {
|
||||
return
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
claudeRosterHasWorkingSubagent,
|
||||
claudeRosterToSnapshots
|
||||
} from '../../../shared/claude-subagent-roster'
|
||||
import { admitLegacyAgentStatus } from '../../../shared/agent-hook-listener/listener-state'
|
||||
import { reapRestoredClaudeSubagentsForDeadPane } from '../../../shared/agent-hook-listener/providers/claude-roster-state'
|
||||
import { AGENT_STATUS_PERSISTED_HYDRATION_MODE } from '../../../shared/agent-status-legacy-adapter'
|
||||
import { AgentHookServerTabCleanup } from './server-tab-cleanup'
|
||||
import type { EnrichedAgentHookEventPayload } from './server-types'
|
||||
|
||||
@@ -113,7 +115,12 @@ export abstract class AgentHookServerReaping extends AgentHookServerTabCleanup {
|
||||
subagents
|
||||
}
|
||||
}
|
||||
this.state.lastStatusByPaneKey.set(paneKey, reconciled)
|
||||
admitLegacyAgentStatus(
|
||||
this.state,
|
||||
'main-restored-status-reaping',
|
||||
reconciled,
|
||||
AGENT_STATUS_PERSISTED_HYDRATION_MODE
|
||||
)
|
||||
this.commitStatusRowMutation(enriched, reconciled)
|
||||
}
|
||||
if (changedPanes > 0) {
|
||||
|
||||
@@ -10,6 +10,8 @@ import { INTERRUPTED_DONE_LATE_WORKING_SUPPRESSION_MS } from './server-constants
|
||||
import type { EnrichedAgentHookEventPayload } from './server-types'
|
||||
import type { AgentHookEventPayload } from '../../../shared/agent-hook-listener/listener-event'
|
||||
import type { AgentStatusObservationOrigin } from '../../../shared/agent-status-observation'
|
||||
import { AGENT_STATUS_2A_CURRENT_PRODUCER_MODE } from '../../../shared/agent-status-legacy-adapter'
|
||||
import { admitLegacyAgentStatus } from '../../../shared/agent-hook-listener/listener-state'
|
||||
import {
|
||||
attachClaudeChildOnlyBoundary,
|
||||
attachClaudePermissionToolUseId,
|
||||
@@ -70,7 +72,7 @@ export abstract class AgentHookServerStatusUpdate extends AgentHookServerStatusA
|
||||
}
|
||||
this.clearAssistantMessageRetry(enriched.paneKey)
|
||||
this.runtimeObservedStatusPaneKeys.delete(enriched.paneKey)
|
||||
this.state.lastStatusByPaneKey.set(enriched.paneKey, enriched)
|
||||
this.writeLegacyStatusRow(enriched)
|
||||
this.commitStatusRowMutation(rowBefore, enriched)
|
||||
this.scheduleStatusPersist()
|
||||
this.notifyStatusChangeListeners()
|
||||
@@ -123,7 +125,7 @@ export abstract class AgentHookServerStatusUpdate extends AgentHookServerStatusA
|
||||
if (boundaryReconciledPrevious !== previous) {
|
||||
previous = boundaryReconciledPrevious
|
||||
if (previous) {
|
||||
this.state.lastStatusByPaneKey.set(previous.paneKey, previous)
|
||||
this.writeLegacyStatusRow(previous)
|
||||
this.scheduleStatusPersist()
|
||||
}
|
||||
}
|
||||
@@ -222,7 +224,7 @@ export abstract class AgentHookServerStatusUpdate extends AgentHookServerStatusA
|
||||
} else {
|
||||
this.runtimeObservedStatusPaneKeys.add(enriched.paneKey)
|
||||
}
|
||||
this.state.lastStatusByPaneKey.set(enriched.paneKey, enriched)
|
||||
this.writeLegacyStatusRow(enriched)
|
||||
this.commitStatusRowMutation(rowBefore, enriched)
|
||||
// Why skipped for structured rows: the serializer drops them, so the whole walk and stringify
|
||||
// can only ever reproduce the last file — once per debounce window for a streaming chat.
|
||||
@@ -264,7 +266,7 @@ export abstract class AgentHookServerStatusUpdate extends AgentHookServerStatusA
|
||||
}
|
||||
const firstRuntimeObservation = !this.runtimeObservedStatusPaneKeys.has(refreshed.paneKey)
|
||||
this.runtimeObservedStatusPaneKeys.add(refreshed.paneKey)
|
||||
this.state.lastStatusByPaneKey.set(refreshed.paneKey, refreshed)
|
||||
this.writeLegacyStatusRow(refreshed)
|
||||
this.commitStatusRowMutation(mutationBefore ?? previous, refreshed)
|
||||
this.scheduleStatusPersist()
|
||||
// A dismissed row may retain only provider resume identity. Its preserved payload can still
|
||||
@@ -301,4 +303,13 @@ export abstract class AgentHookServerStatusUpdate extends AgentHookServerStatusA
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private writeLegacyStatusRow(entry: EnrichedAgentHookEventPayload): void {
|
||||
admitLegacyAgentStatus(
|
||||
this.state,
|
||||
'main-status-update',
|
||||
entry,
|
||||
AGENT_STATUS_2A_CURRENT_PRODUCER_MODE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AgentHookServer } from './server'
|
||||
import { AGENT_STATUS_STALE_AFTER_MS } from '../../shared/agent-status-types'
|
||||
import { selectFreshExplicitAgentStatus } from '../runtime/runtime-hook-agent-row-selection'
|
||||
import { wslHookRelayConnectionId } from '../../shared/wsl-hook-relay-contract'
|
||||
import { seedLegacyAgentStatusForTests } from '../../shared/agent-hook-listener/listener-state'
|
||||
|
||||
const PANE_KEY = 'tab-handle:33333333-3333-4333-8333-333333333333'
|
||||
const HANDLE = 'term_identity'
|
||||
@@ -205,13 +206,15 @@ describe('the terminal handle a status row is stamped with', () => {
|
||||
server.subscribeStatusRowMutations(mutations)
|
||||
const payload = { state: 'working' as const, prompt: 'ship it', agentType: 'claude' as const }
|
||||
ingest(server, { payload })
|
||||
const row = server._getStateForTests().lastStatusByPaneKey.get(PANE_KEY) as
|
||||
| { claudeLeadBoundaryChildOnly?: true }
|
||||
| undefined
|
||||
const row = server._getStateForTests().lastStatusByPaneKey.get(PANE_KEY)
|
||||
if (!row) {
|
||||
throw new Error('expected seeded status row')
|
||||
}
|
||||
row.claudeLeadBoundaryChildOnly = true
|
||||
const childOnlyRow = {
|
||||
...row,
|
||||
claudeLeadBoundaryChildOnly: true
|
||||
}
|
||||
seedLegacyAgentStatusForTests(server._getStateForTests(), childOnlyRow)
|
||||
enriched.mockClear()
|
||||
mutations.mockClear()
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { createHash } from 'node:crypto'
|
||||
import { readFileSync } from 'node:fs'
|
||||
|
||||
import { isAgentStatusHooksEnabled } from './managed-agent-hook-controls'
|
||||
import { AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES } from '../../shared/agent-status-legacy-adapter'
|
||||
import { agentHookServer } from './server'
|
||||
import type { ManagedHookDetectionSettings } from './managed-hook-detection-commands'
|
||||
import { installRemoteManagedAgentHooks } from './remote-managed-hook-installers'
|
||||
@@ -99,11 +100,17 @@ export const defaultWslHookRelayDeps: WslHookRelayManagerDeps = {
|
||||
spawnRelay: spawnWslRelayProcess,
|
||||
runInstall: runWslInstallProcess,
|
||||
waitForSentinel: waitForWslRelaySentinel,
|
||||
ingest: (envelope, connectionId) =>
|
||||
agentHookServer.ingestRemote(
|
||||
envelope as Parameters<typeof agentHookServer.ingestRemote>[0],
|
||||
connectionId
|
||||
),
|
||||
// Why: the WSL relay protocol advertises no run-serving capability; stamped onto a copy so the
|
||||
// wire-deserialized notification object itself is never mutated.
|
||||
ingest: (envelope, connectionId) => {
|
||||
const capped = {
|
||||
...envelope,
|
||||
advertisedAgentStatusCapabilities: AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES
|
||||
}
|
||||
type IngestEnvelope = Parameters<typeof agentHookServer.ingestRemote>[0]
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: envelope is the wire-deserialized notification; ingestRemote independently re-validates paneKey's type before trusting anything here.
|
||||
return agentHookServer.ingestRemote(capped as IngestEnvelope, connectionId)
|
||||
},
|
||||
installHooks: installRemoteManagedAgentHooks,
|
||||
installCodex: (runtimeHomePath, distro) =>
|
||||
codexHookService.installForRuntimeHomeSerialized(runtimeHomePath, {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
clearPaneCacheState,
|
||||
createHookListenerState,
|
||||
movePaneCacheState,
|
||||
seedLegacyAgentStatusForTests,
|
||||
type HookListenerState
|
||||
} from '../../shared/agent-hook-listener/listener-state'
|
||||
import { seedClaudeSubagentRosterFromSnapshots } from '../../shared/agent-hook-listener/providers/claude-roster-state'
|
||||
@@ -31,7 +32,7 @@ function deliverIfRegistered(
|
||||
}
|
||||
const event = normalizeHookPayload(state, 'claude', { paneKey: PANE_KEY, payload }, 'production')
|
||||
if (event) {
|
||||
state.lastStatusByPaneKey.set(PANE_KEY, event)
|
||||
seedLegacyAgentStatusForTests(state, event)
|
||||
}
|
||||
return event
|
||||
}
|
||||
@@ -89,7 +90,7 @@ function hydrateStuckRow(
|
||||
...(subagents ? { subagents } : {})
|
||||
}
|
||||
} as unknown as AgentHookEventPayload
|
||||
state.lastStatusByPaneKey.set(PANE_KEY, hydrated)
|
||||
seedLegacyAgentStatusForTests(state, hydrated)
|
||||
if (subagents) {
|
||||
seedClaudeSubagentRosterFromSnapshots(state, PANE_KEY, subagents)
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
AGENT_HOOK_REQUEST_REPLAY_METHOD,
|
||||
isRemoteAgentHooksEnabled
|
||||
} from '../../shared/agent-hook-relay'
|
||||
import { AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES } from '../../shared/agent-status-legacy-adapter'
|
||||
import { _internals as openCodeInternals } from '../opencode/hook-service'
|
||||
import { getPiAgentStatusExtensionSource } from '../pi/agent-status-extension-source'
|
||||
import {
|
||||
@@ -1606,6 +1607,8 @@ export class SshRelaySession {
|
||||
typeof envelope.claudeRunningNonAgentTask === 'boolean'
|
||||
? envelope.claudeRunningNonAgentTask
|
||||
: undefined,
|
||||
// Why: the SSH relay protocol advertises no run-serving capability.
|
||||
advertisedAgentStatusCapabilities: AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES,
|
||||
payload: envelope.payload
|
||||
},
|
||||
this.targetId
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
createHookListenerState,
|
||||
type HookListenerState
|
||||
} from '../shared/agent-hook-listener/listener-state'
|
||||
import { cacheRelayLegacyAgentStatus } from '../shared/agent-status-legacy-relay-cache'
|
||||
import {
|
||||
getEndpointFileName,
|
||||
writeEndpointFile
|
||||
@@ -41,10 +42,7 @@ import {
|
||||
import { buildRelayHookPtyEnv, defaultEndpointDir } from './agent-hook-endpoint-coordinates'
|
||||
import { buildRelayHookEnvelope, hookBodyEnv, hookBodyVersion } from './agent-hook-envelope-build'
|
||||
import { AgentHookResultRetryScheduler } from './agent-hook-result-retry-scheduler'
|
||||
import {
|
||||
evictCachedPanesOverCap,
|
||||
selectReplayableCachedPanes
|
||||
} from './agent-hook-cached-pane-status'
|
||||
import { MAX_CACHED_PANES, selectReplayableCachedPanes } from './agent-hook-cached-pane-status'
|
||||
|
||||
export type RelayHookForward = (envelope: AgentHookRelayEnvelope) => void
|
||||
|
||||
@@ -209,8 +207,9 @@ export class RelayAgentHookServer {
|
||||
/** Request-driven replay: re-forwards each cached paneKey payload as a fresh notification. Forwards are
|
||||
* issued before the request handler returns, so the response trails all replayed notifications. */
|
||||
replayCachedPayloadsForPanes(): number {
|
||||
const cachedSnapshot = new Map(this.state.lastStatusByPaneKey)
|
||||
const replayable = selectReplayableCachedPanes({
|
||||
cachedByPaneKey: this.state.lastStatusByPaneKey,
|
||||
cachedByPaneKey: cachedSnapshot,
|
||||
metaByPaneKey: this.lastEnvelopeMetaByPaneKey,
|
||||
isPaneSurfaceRetired: this.isPaneSurfaceRetired,
|
||||
dropPane: (paneKey) => this.clearPaneState(paneKey)
|
||||
@@ -325,13 +324,15 @@ export class RelayAgentHookServer {
|
||||
// Why: keep PostCompact identity in the replay cache so the client can re-run ownership when
|
||||
// it reconnects. Stripping it would let a cold relay replay a completion as an ordinary `done`
|
||||
// row and resurrect a pane that the client had already retired.
|
||||
const cachedEvent = event
|
||||
// Why: delete-then-set makes Map insertion order = recency, so the cap below evicts the longest-idle pane.
|
||||
this.state.lastStatusByPaneKey.delete(event.paneKey)
|
||||
this.state.lastStatusByPaneKey.set(event.paneKey, cachedEvent)
|
||||
if (
|
||||
!cacheRelayLegacyAgentStatus(this.state, event, MAX_CACHED_PANES, (paneKey) =>
|
||||
this.clearPaneState(paneKey)
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.lastEnvelopeMetaByPaneKey.delete(event.paneKey)
|
||||
this.lastEnvelopeMetaByPaneKey.set(event.paneKey, { source, env, version })
|
||||
evictCachedPanesOverCap(this.state.lastStatusByPaneKey, (key) => this.clearPaneState(key))
|
||||
this.forward(buildRelayHookEnvelope(event, source, env, version, options))
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
clearAllListenerCaches,
|
||||
clearPaneCacheState,
|
||||
createHookListenerState,
|
||||
movePaneCacheState
|
||||
movePaneCacheState,
|
||||
seedLegacyAgentStatusForTests
|
||||
} from './agent-hook-listener/listener-state'
|
||||
import { warnOnHookEnvOrVersionMismatch } from './agent-hook-listener/listener-limits'
|
||||
import { resolveHookSource } from './agent-hook-listener/source-routing'
|
||||
@@ -122,7 +123,6 @@ describe('agent hook extraction boundaries', () => {
|
||||
const paneMaps = [
|
||||
state.lastPromptByPaneKey,
|
||||
state.lastToolByPaneKey,
|
||||
state.lastStatusByPaneKey,
|
||||
state.antigravityCompletedTranscriptByPaneKey,
|
||||
state.claudeSubagentRosterByPaneKey,
|
||||
state.claudeLeadStateByPaneKey,
|
||||
@@ -137,6 +137,17 @@ describe('agent hook extraction boundaries', () => {
|
||||
cache.set(scoped, 'scoped')
|
||||
cache.set(sibling, 'sibling')
|
||||
}
|
||||
for (const [paneKey, prompt] of [
|
||||
[PANE, 'exact'],
|
||||
[scoped, 'scoped'],
|
||||
[sibling, 'sibling']
|
||||
]) {
|
||||
seedLegacyAgentStatusForTests(state, {
|
||||
paneKey,
|
||||
connectionId: null,
|
||||
payload: { state: 'working', prompt }
|
||||
})
|
||||
}
|
||||
const paneSets = [
|
||||
state.ampCompletedCacheKeys,
|
||||
state.claudeUnconfirmedRestoredStatusPaneKeys,
|
||||
@@ -158,6 +169,10 @@ describe('agent hook extraction boundaries', () => {
|
||||
expect(cache.get(sibling)).toBe('sibling')
|
||||
expect(cache.has(PANE)).toBe(false)
|
||||
}
|
||||
expect(state.lastStatusByPaneKey.get(MOVED_PANE)?.payload.prompt).toBe('exact')
|
||||
expect(state.lastStatusByPaneKey.get(movedScoped)?.payload.prompt).toBe('scoped')
|
||||
expect(state.lastStatusByPaneKey.get(sibling)?.payload.prompt).toBe('sibling')
|
||||
expect(state.lastStatusByPaneKey.has(PANE)).toBe(false)
|
||||
for (const set of paneSets) {
|
||||
expect(set.has(MOVED_PANE)).toBe(true)
|
||||
expect(set.has(movedScoped)).toBe(true)
|
||||
@@ -176,7 +191,6 @@ describe('agent hook extraction boundaries', () => {
|
||||
const paneMaps = [
|
||||
state.lastPromptByPaneKey,
|
||||
state.lastToolByPaneKey,
|
||||
state.lastStatusByPaneKey,
|
||||
state.antigravityCompletedTranscriptByPaneKey
|
||||
]
|
||||
for (const map of paneMaps) {
|
||||
@@ -185,6 +199,17 @@ describe('agent hook extraction boundaries', () => {
|
||||
cache.set(scoped, 'scoped')
|
||||
cache.set(sibling, 'sibling')
|
||||
}
|
||||
for (const [paneKey, prompt] of [
|
||||
[PANE, 'exact'],
|
||||
[scoped, 'scoped'],
|
||||
[sibling, 'sibling']
|
||||
]) {
|
||||
seedLegacyAgentStatusForTests(state, {
|
||||
paneKey,
|
||||
connectionId: null,
|
||||
payload: { state: 'working', prompt }
|
||||
})
|
||||
}
|
||||
state.ampCompletedCacheKeys.add(PANE)
|
||||
state.ampCompletedCacheKeys.add(scoped)
|
||||
state.ampCompletedCacheKeys.add(sibling)
|
||||
@@ -200,6 +225,9 @@ describe('agent hook extraction boundaries', () => {
|
||||
expect(cache.has(scoped)).toBe(false)
|
||||
expect(cache.get(sibling)).toBe('sibling')
|
||||
}
|
||||
expect(state.lastStatusByPaneKey.has(PANE)).toBe(false)
|
||||
expect(state.lastStatusByPaneKey.has(scoped)).toBe(false)
|
||||
expect(state.lastStatusByPaneKey.get(sibling)?.payload.prompt).toBe('sibling')
|
||||
expect(state.ampCompletedCacheKeys.has(scoped)).toBe(false)
|
||||
expect(state.ampCompletedCacheKeys.has(sibling)).toBe(true)
|
||||
expect(state.claudeLeadStateByPaneKey.has(PANE)).toBe(false)
|
||||
@@ -211,7 +239,11 @@ describe('agent hook extraction boundaries', () => {
|
||||
const state = createHookListenerState()
|
||||
state.lastPromptByPaneKey.set(PANE, 'old prompt')
|
||||
state.lastToolByPaneKey.set(PANE, { toolName: 'old tool' })
|
||||
state.lastStatusByPaneKey.set(PANE, {} as never)
|
||||
seedLegacyAgentStatusForTests(state, {
|
||||
paneKey: PANE,
|
||||
connectionId: null,
|
||||
payload: { state: 'working', prompt: 'old status' }
|
||||
})
|
||||
|
||||
const event = normalizeHookPayload(
|
||||
state,
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { normalizeHookPayload } from './agent-hook-listener'
|
||||
import type { HookListenerState } from './agent-hook-listener/listener-state'
|
||||
import {
|
||||
seedLegacyAgentStatusForTests,
|
||||
type HookListenerState
|
||||
} from './agent-hook-listener/listener-state'
|
||||
import { makePaneKey } from './stable-pane-id'
|
||||
|
||||
const LEAF_ID = '11111111-1111-4111-8111-111111111111'
|
||||
@@ -14,7 +17,7 @@ export function normalizeAndAccept(
|
||||
): ReturnType<typeof normalizeHookPayload> {
|
||||
const event = normalizeHookPayload(state, source, { paneKey: PANE_KEY, payload }, 'production')
|
||||
if (event) {
|
||||
state.lastStatusByPaneKey.set(PANE_KEY, event)
|
||||
seedLegacyAgentStatusForTests(state, event)
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import type { AgentStatusState } from '../agent-status-types'
|
||||
import {
|
||||
AGENT_STATUS_2A_CURRENT_PRODUCER_MODE,
|
||||
createAgentStatusLegacyAdapter,
|
||||
type AgentStatusLegacyAdapter,
|
||||
type AgentStatusLegacyAdapterOptions,
|
||||
type AgentStatusLegacyAdmissionMode
|
||||
} from '../agent-status-legacy-adapter'
|
||||
import type { AgentStatusLegacyIngressCaller } from '../agent-status-legacy-ingress-manifest'
|
||||
import type { ClaudeSubagentRoster } from '../claude-subagent-roster'
|
||||
import type { CodexSubagentRoster } from '../codex-subagent-roster'
|
||||
import type { CodexSubagentTranscriptState } from '../codex-subagent-transcript'
|
||||
@@ -10,7 +18,8 @@ export type HookListenerState = {
|
||||
warnedEnvs: Set<string>
|
||||
lastPromptByPaneKey: Map<string, string>
|
||||
lastToolByPaneKey: Map<string, ToolSnapshot>
|
||||
lastStatusByPaneKey: Map<string, AgentHookEventPayload>
|
||||
/** Read-only compatibility view. All writes pass through the isolated legacy adapter. */
|
||||
lastStatusByPaneKey: ReadonlyMap<string, AgentHookEventPayload>
|
||||
antigravityCompletedTranscriptByPaneKey: Map<string, string>
|
||||
ampCompletedCacheKeys: Set<string>
|
||||
/** Live subagents/teammates per Claude pane; survives turn boundaries since background children outlive the lead turn. */
|
||||
@@ -62,13 +71,26 @@ export type CodexLeadTurnState = {
|
||||
model?: string
|
||||
}
|
||||
|
||||
export function createHookListenerState(): HookListenerState {
|
||||
return {
|
||||
const legacyStatusAdapterByState = new WeakMap<HookListenerState, AgentStatusLegacyAdapter>()
|
||||
|
||||
function legacyStatusAdapter(state: HookListenerState): AgentStatusLegacyAdapter {
|
||||
const adapter = legacyStatusAdapterByState.get(state)
|
||||
if (!adapter) {
|
||||
throw new Error('Hook listener state has no legacy agent-status adapter')
|
||||
}
|
||||
return adapter
|
||||
}
|
||||
|
||||
export function createHookListenerState(
|
||||
options: AgentStatusLegacyAdapterOptions = {}
|
||||
): HookListenerState {
|
||||
const adapter = createAgentStatusLegacyAdapter(options)
|
||||
const state: HookListenerState = {
|
||||
warnedVersions: new Set(),
|
||||
warnedEnvs: new Set(),
|
||||
lastPromptByPaneKey: new Map(),
|
||||
lastToolByPaneKey: new Map(),
|
||||
lastStatusByPaneKey: new Map(),
|
||||
lastStatusByPaneKey: adapter.view,
|
||||
antigravityCompletedTranscriptByPaneKey: new Map(),
|
||||
ampCompletedCacheKeys: new Set(),
|
||||
claudeSubagentRosterByPaneKey: new Map(),
|
||||
@@ -83,12 +105,69 @@ export function createHookListenerState(): HookListenerState {
|
||||
codexLeadStateByPaneKey: new Map(),
|
||||
grokActiveTurnByPaneKey: new Map()
|
||||
}
|
||||
legacyStatusAdapterByState.set(state, adapter)
|
||||
return state
|
||||
}
|
||||
|
||||
export function admitLegacyAgentStatus(
|
||||
state: HookListenerState,
|
||||
caller: AgentStatusLegacyIngressCaller,
|
||||
entry: AgentHookEventPayload,
|
||||
mode: AgentStatusLegacyAdmissionMode,
|
||||
options?: { moveToEnd?: boolean }
|
||||
): boolean {
|
||||
return legacyStatusAdapter(state).admit(caller, mode, entry, options)
|
||||
}
|
||||
|
||||
export function deleteLegacyAgentStatus(state: HookListenerState, paneKey: string): boolean {
|
||||
return legacyStatusAdapter(state).delete(paneKey)
|
||||
}
|
||||
|
||||
export function clearLegacyAgentStatuses(state: HookListenerState): void {
|
||||
legacyStatusAdapter(state).clear()
|
||||
}
|
||||
|
||||
export function moveLegacyAgentStatuses(
|
||||
state: HookListenerState,
|
||||
fromPaneKey: string,
|
||||
toPaneKey: string
|
||||
): void {
|
||||
legacyStatusAdapter(state).move(fromPaneKey, toPaneKey)
|
||||
}
|
||||
|
||||
export function getLegacyStatusListingOrder(
|
||||
state: HookListenerState,
|
||||
paneKey: string
|
||||
): number | undefined {
|
||||
return legacyStatusAdapter(state).listingOrder(paneKey)
|
||||
}
|
||||
|
||||
/** Test harnesses seed the same compatibility region without exposing a mutable Map. */
|
||||
export function seedLegacyAgentStatusForTests(
|
||||
state: HookListenerState,
|
||||
entry: AgentHookEventPayload
|
||||
): void {
|
||||
if (
|
||||
!admitLegacyAgentStatus(
|
||||
state,
|
||||
'main-status-update',
|
||||
entry,
|
||||
AGENT_STATUS_2A_CURRENT_PRODUCER_MODE
|
||||
)
|
||||
) {
|
||||
throw new Error('Test legacy agent-status seed was refused')
|
||||
}
|
||||
}
|
||||
|
||||
export function clearPaneCacheState(state: HookListenerState, paneKey: string): void {
|
||||
deletePaneScopedCacheEntry(state.lastPromptByPaneKey, paneKey)
|
||||
deletePaneScopedCacheEntry(state.lastToolByPaneKey, paneKey)
|
||||
deletePaneScopedCacheEntry(state.lastStatusByPaneKey, paneKey)
|
||||
deleteLegacyAgentStatus(state, paneKey)
|
||||
for (const key of state.lastStatusByPaneKey.keys()) {
|
||||
if (key.startsWith(`${paneKey}\0`)) {
|
||||
deleteLegacyAgentStatus(state, key)
|
||||
}
|
||||
}
|
||||
deletePaneScopedCacheEntry(state.antigravityCompletedTranscriptByPaneKey, paneKey)
|
||||
deletePaneScopedSetEntry(state.ampCompletedCacheKeys, paneKey)
|
||||
deletePaneScopedCacheEntry(state.claudeConsumedCompactPromptIdByPaneKey, paneKey)
|
||||
@@ -162,7 +241,7 @@ export function movePaneCacheState(
|
||||
}
|
||||
movePaneScopedMapEntries(state.lastPromptByPaneKey, fromPaneKey, toPaneKey)
|
||||
movePaneScopedMapEntries(state.lastToolByPaneKey, fromPaneKey, toPaneKey)
|
||||
movePaneScopedMapEntries(state.lastStatusByPaneKey, fromPaneKey, toPaneKey)
|
||||
moveLegacyAgentStatuses(state, fromPaneKey, toPaneKey)
|
||||
movePaneScopedMapEntries(state.antigravityCompletedTranscriptByPaneKey, fromPaneKey, toPaneKey)
|
||||
movePaneScopedSetEntries(state.ampCompletedCacheKeys, fromPaneKey, toPaneKey)
|
||||
movePaneScopedMapEntries(state.claudeConsumedCompactPromptIdByPaneKey, fromPaneKey, toPaneKey)
|
||||
@@ -209,7 +288,7 @@ export function deletePaneScopedSetEntry(set: Set<string>, paneKey: string): voi
|
||||
export function clearAllListenerCaches(state: HookListenerState): void {
|
||||
state.lastPromptByPaneKey.clear()
|
||||
state.lastToolByPaneKey.clear()
|
||||
state.lastStatusByPaneKey.clear()
|
||||
clearLegacyAgentStatuses(state)
|
||||
state.antigravityCompletedTranscriptByPaneKey.clear()
|
||||
state.ampCompletedCacheKeys.clear()
|
||||
state.claudeConsumedCompactPromptIdByPaneKey.clear()
|
||||
|
||||
@@ -40,15 +40,15 @@ describe('bounded agent hook status cache', () => {
|
||||
maxPanes: 3,
|
||||
now
|
||||
})
|
||||
upsertBoundedAgentHookStatus(listener, status('stale', 'working', now), {
|
||||
maxPanes: 3,
|
||||
now
|
||||
})
|
||||
upsertBoundedAgentHookStatus(
|
||||
listener,
|
||||
status('stale', 'working', now - AGENT_STATUS_STALE_AFTER_MS - 1),
|
||||
{
|
||||
maxPanes: 3,
|
||||
now
|
||||
}
|
||||
)
|
||||
upsertBoundedAgentHookStatus(listener, status('done', 'done', now), { maxPanes: 3, now })
|
||||
const stale = listener.lastStatusByPaneKey.get('stale') as AgentHookEventPayload & {
|
||||
receivedAt: number
|
||||
}
|
||||
stale.receivedAt = now - AGENT_STATUS_STALE_AFTER_MS - 1
|
||||
listener.lastPromptByPaneKey.set('stale', 'cached prompt')
|
||||
listener.lastToolByPaneKey.set('stale\0tool', {} as never)
|
||||
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { clearPaneCacheState, type HookListenerState } from './agent-hook-listener/listener-state'
|
||||
import {
|
||||
admitLegacyAgentStatus,
|
||||
clearPaneCacheState,
|
||||
type HookListenerState
|
||||
} from './agent-hook-listener/listener-state'
|
||||
import type { AgentHookEventPayload } from './agent-hook-listener/listener-event'
|
||||
import { AGENT_STATUS_2A_CURRENT_PRODUCER_MODE } from './agent-status-legacy-adapter'
|
||||
import { AGENT_STATUS_STALE_AFTER_MS } from './agent-status-types'
|
||||
|
||||
export const MAX_AGENT_HOOK_STATUS_CACHE_PANES = 500
|
||||
@@ -19,8 +24,17 @@ export function upsertBoundedAgentHookStatus(
|
||||
throw new RangeError('Agent hook status cache limit must be a positive safe integer')
|
||||
}
|
||||
|
||||
state.lastStatusByPaneKey.delete(entry.paneKey)
|
||||
state.lastStatusByPaneKey.set(entry.paneKey, entry)
|
||||
if (
|
||||
!admitLegacyAgentStatus(
|
||||
state,
|
||||
'shared-bounded-status-cache',
|
||||
entry,
|
||||
AGENT_STATUS_2A_CURRENT_PRODUCER_MODE,
|
||||
{ moveToEnd: true }
|
||||
)
|
||||
) {
|
||||
return []
|
||||
}
|
||||
const evicted: AgentHookStatusCacheEviction[] = []
|
||||
const now = options.now ?? Date.now()
|
||||
while (state.lastStatusByPaneKey.size > maxPanes) {
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import type { AgentHookEventPayload } from './agent-hook-listener/listener-event'
|
||||
import {
|
||||
AGENT_STATUS_2A_CURRENT_PRODUCER_MODE,
|
||||
AGENT_STATUS_PERSISTED_HYDRATION_MODE,
|
||||
createAgentStatusLegacyAdapter,
|
||||
olderPeerAgentStatusLegacyMode
|
||||
} from './agent-status-legacy-adapter'
|
||||
import { AGENT_STATUS_RUNS_RUNTIME_CAPABILITY } from './agent-status-run-capability'
|
||||
|
||||
function status(paneKey: string, prompt = 'work'): AgentHookEventPayload {
|
||||
return {
|
||||
paneKey,
|
||||
connectionId: null,
|
||||
payload: { state: 'working', prompt, agentType: 'claude' }
|
||||
}
|
||||
}
|
||||
|
||||
describe('legacy agent-status adapter', () => {
|
||||
it('keeps incomplete PTY scope exclusively in the legacy region', () => {
|
||||
const adapter = createAgentStatusLegacyAdapter()
|
||||
const entry = status('pane-with-no-workspace-or-host-scope')
|
||||
|
||||
expect(adapter.admit('main-status-update', AGENT_STATUS_2A_CURRENT_PRODUCER_MODE, entry)).toBe(
|
||||
true
|
||||
)
|
||||
expect(adapter.view.get(entry.paneKey)).toBe(entry)
|
||||
})
|
||||
|
||||
it('refuses keys already owned by the canonical projection', () => {
|
||||
const canonicalPaneKeys = new Set<string>()
|
||||
const adapter = createAgentStatusLegacyAdapter({
|
||||
isCanonicalPaneKey: (paneKey) => canonicalPaneKeys.has(paneKey)
|
||||
})
|
||||
const prior = status('canonical-pane', 'legacy before canonical publication')
|
||||
expect(adapter.admit('main-status-update', AGENT_STATUS_2A_CURRENT_PRODUCER_MODE, prior)).toBe(
|
||||
true
|
||||
)
|
||||
adapter.delete(prior.paneKey)
|
||||
canonicalPaneKeys.add(prior.paneKey)
|
||||
|
||||
expect(
|
||||
adapter.admit(
|
||||
'main-status-update',
|
||||
AGENT_STATUS_2A_CURRENT_PRODUCER_MODE,
|
||||
status(prior.paneKey, 'late legacy write')
|
||||
)
|
||||
).toBe(false)
|
||||
expect(adapter.view.has(prior.paneKey)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not move an existing legacy row onto a canonical projection key', () => {
|
||||
const canonicalPaneKeys = new Set<string>()
|
||||
const adapter = createAgentStatusLegacyAdapter({
|
||||
isCanonicalPaneKey: (paneKey) => canonicalPaneKeys.has(paneKey)
|
||||
})
|
||||
adapter.admit(
|
||||
'main-status-update',
|
||||
AGENT_STATUS_2A_CURRENT_PRODUCER_MODE,
|
||||
status('legacy-pane')
|
||||
)
|
||||
canonicalPaneKeys.add('canonical-pane')
|
||||
|
||||
adapter.move('legacy-pane', 'canonical-pane')
|
||||
|
||||
expect(adapter.view.has('legacy-pane')).toBe(false)
|
||||
expect(adapter.view.has('canonical-pane')).toBe(false)
|
||||
})
|
||||
|
||||
it('admits an unsupported older peer but never falls back for a capable peer', () => {
|
||||
const adapter = createAgentStatusLegacyAdapter()
|
||||
const older = status('same-pane', 'older peer')
|
||||
expect(adapter.admit('main-status-update', olderPeerAgentStatusLegacyMode([]), older)).toBe(
|
||||
true
|
||||
)
|
||||
|
||||
const capable = status('same-pane', 'capable peer must use canonical serving')
|
||||
expect(
|
||||
adapter.admit(
|
||||
'main-status-update',
|
||||
olderPeerAgentStatusLegacyMode([AGENT_STATUS_RUNS_RUNTIME_CAPABILITY]),
|
||||
capable
|
||||
)
|
||||
).toBe(false)
|
||||
expect(adapter.view.get('same-pane')).toBe(older)
|
||||
})
|
||||
|
||||
it('fails closed on malformed older-peer capability evidence', () => {
|
||||
const adapter = createAgentStatusLegacyAdapter()
|
||||
|
||||
expect(
|
||||
adapter.admit(
|
||||
'main-status-update',
|
||||
olderPeerAgentStatusLegacyMode(Array.from({ length: 257 }, () => 'unknown')),
|
||||
status('malformed-peer')
|
||||
)
|
||||
).toBe(false)
|
||||
expect(adapter.view.size).toBe(0)
|
||||
})
|
||||
|
||||
it('restricts hydration admission to the named quarantine callers', () => {
|
||||
const adapter = createAgentStatusLegacyAdapter()
|
||||
|
||||
expect(
|
||||
adapter.admit(
|
||||
'main-status-hydration',
|
||||
AGENT_STATUS_PERSISTED_HYDRATION_MODE,
|
||||
status('hydrated')
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
adapter.admit(
|
||||
'main-status-update',
|
||||
AGENT_STATUS_PERSISTED_HYDRATION_MODE,
|
||||
status('wrong-caller')
|
||||
)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('exposes Map reads without writable methods or mutable values', () => {
|
||||
const adapter = createAgentStatusLegacyAdapter()
|
||||
const entry = status('immutable')
|
||||
adapter.admit('main-status-update', AGENT_STATUS_2A_CURRENT_PRODUCER_MODE, entry)
|
||||
|
||||
expect(adapter.view.get('immutable')).toBe(entry)
|
||||
expect(adapter.view.has('immutable')).toBe(true)
|
||||
expect(Array.from(adapter.view.keys())).toEqual(['immutable'])
|
||||
expect('set' in adapter.view).toBe(false)
|
||||
expect('delete' in adapter.view).toBe(false)
|
||||
expect(Object.isFrozen(adapter.view)).toBe(true)
|
||||
expect(Object.isFrozen(entry)).toBe(true)
|
||||
expect(Object.isFrozen(entry.payload)).toBe(true)
|
||||
expect(() => {
|
||||
entry.payload.prompt = 'mutated outside the adapter'
|
||||
}).toThrow()
|
||||
expect(adapter.view.get('immutable')?.payload.prompt).toBe('work')
|
||||
})
|
||||
|
||||
it('assigns listing order once per live row and preserves it across refresh and move', () => {
|
||||
let nextOrder = 40
|
||||
const adapter = createAgentStatusLegacyAdapter({ nextListingOrder: () => nextOrder++ })
|
||||
adapter.admit('main-status-update', AGENT_STATUS_2A_CURRENT_PRODUCER_MODE, status('pane'))
|
||||
expect(adapter.listingOrder('pane')).toBe(40)
|
||||
|
||||
adapter.admit(
|
||||
'main-status-update',
|
||||
AGENT_STATUS_2A_CURRENT_PRODUCER_MODE,
|
||||
status('pane', 'refresh'),
|
||||
{ moveToEnd: true }
|
||||
)
|
||||
expect(adapter.listingOrder('pane')).toBe(40)
|
||||
adapter.move('pane', 'moved-pane')
|
||||
expect(adapter.listingOrder('moved-pane')).toBe(40)
|
||||
|
||||
adapter.delete('moved-pane')
|
||||
adapter.admit(
|
||||
'main-status-update',
|
||||
AGENT_STATUS_2A_CURRENT_PRODUCER_MODE,
|
||||
status('moved-pane', 'new lifecycle')
|
||||
)
|
||||
expect(adapter.listingOrder('moved-pane')).toBe(41)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,197 @@
|
||||
import type { AgentHookEventPayload } from './agent-hook-listener/listener-event'
|
||||
import {
|
||||
deserializeAgentStatusCapabilities,
|
||||
hasAgentStatusRunCapability
|
||||
} from './agent-status-run-capability'
|
||||
import {
|
||||
findAgentStatusLegacyIngressManifestEntry,
|
||||
type AgentStatusLegacyIngressCaller
|
||||
} from './agent-status-legacy-ingress-manifest'
|
||||
import {
|
||||
AGENT_STATUS_2A_SERVING_READINESS,
|
||||
isAgentStatusRunServingAdvertised,
|
||||
type AgentStatusServingReadiness
|
||||
} from './agent-status-serving-readiness'
|
||||
|
||||
export type AgentStatusLegacyAdmissionMode =
|
||||
| Readonly<{
|
||||
kind: 'current-producer'
|
||||
servingReadiness: AgentStatusServingReadiness
|
||||
}>
|
||||
| Readonly<{
|
||||
kind: 'older-peer'
|
||||
advertisedCapabilities: readonly string[]
|
||||
}>
|
||||
| Readonly<{ kind: 'persisted-hydration' }>
|
||||
|
||||
export const AGENT_STATUS_2A_CURRENT_PRODUCER_MODE: AgentStatusLegacyAdmissionMode = Object.freeze({
|
||||
kind: 'current-producer',
|
||||
servingReadiness: AGENT_STATUS_2A_SERVING_READINESS
|
||||
})
|
||||
|
||||
export const AGENT_STATUS_PERSISTED_HYDRATION_MODE: AgentStatusLegacyAdmissionMode = Object.freeze({
|
||||
kind: 'persisted-hydration'
|
||||
})
|
||||
|
||||
/** Existing relay protocols advertise no run-serving capability. Production ingress call sites stamp this onto the envelope explicitly. */
|
||||
export const AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES: readonly string[] = Object.freeze(
|
||||
[]
|
||||
)
|
||||
|
||||
export function olderPeerAgentStatusLegacyMode(
|
||||
advertisedCapabilities: readonly string[]
|
||||
): AgentStatusLegacyAdmissionMode {
|
||||
return Object.freeze({
|
||||
kind: 'older-peer',
|
||||
advertisedCapabilities: Object.freeze([...advertisedCapabilities])
|
||||
})
|
||||
}
|
||||
|
||||
export function canAdmitLegacyAgentStatus(
|
||||
caller: AgentStatusLegacyIngressCaller,
|
||||
mode: AgentStatusLegacyAdmissionMode
|
||||
): boolean {
|
||||
const manifestEntry = findAgentStatusLegacyIngressManifestEntry(caller)
|
||||
if (!manifestEntry || !manifestEntry.allowedModes.includes(mode.kind)) {
|
||||
return false
|
||||
}
|
||||
if (mode.kind === 'older-peer') {
|
||||
return (
|
||||
deserializeAgentStatusCapabilities(mode.advertisedCapabilities) !== null &&
|
||||
!hasAgentStatusRunCapability(mode.advertisedCapabilities)
|
||||
)
|
||||
}
|
||||
if (mode.kind === 'persisted-hydration') {
|
||||
return true
|
||||
}
|
||||
return !isAgentStatusRunServingAdvertised(mode.servingReadiness)
|
||||
}
|
||||
|
||||
export type AgentStatusLegacyAdapter = {
|
||||
readonly view: ReadonlyMap<string, AgentHookEventPayload>
|
||||
admit(
|
||||
caller: AgentStatusLegacyIngressCaller,
|
||||
mode: AgentStatusLegacyAdmissionMode,
|
||||
entry: AgentHookEventPayload,
|
||||
options?: { moveToEnd?: boolean }
|
||||
): boolean
|
||||
delete(paneKey: string): boolean
|
||||
clear(): void
|
||||
move(fromPaneKey: string, toPaneKey: string): void
|
||||
listingOrder(paneKey: string): number | undefined
|
||||
}
|
||||
|
||||
export type AgentStatusLegacyAdapterOptions = {
|
||||
nextListingOrder?: () => number
|
||||
isCanonicalPaneKey?: (paneKey: string) => boolean
|
||||
}
|
||||
|
||||
function freezeRecursively(value: unknown, seen: WeakSet<object>): void {
|
||||
if (typeof value !== 'object' || value === null || seen.has(value)) {
|
||||
return
|
||||
}
|
||||
seen.add(value)
|
||||
for (const key of Reflect.ownKeys(value)) {
|
||||
freezeRecursively(Reflect.get(value, key), seen)
|
||||
}
|
||||
Object.freeze(value)
|
||||
}
|
||||
|
||||
function freezeStatusEntry(entry: AgentHookEventPayload): void {
|
||||
freezeRecursively(entry, new WeakSet())
|
||||
}
|
||||
|
||||
function createReadonlyView(
|
||||
entries: Map<string, AgentHookEventPayload>
|
||||
): ReadonlyMap<string, AgentHookEventPayload> {
|
||||
let view: ReadonlyMap<string, AgentHookEventPayload>
|
||||
view = Object.freeze({
|
||||
get size() {
|
||||
return entries.size
|
||||
},
|
||||
get: (key: string) => entries.get(key),
|
||||
has: (key: string) => entries.has(key),
|
||||
entries: () => entries.entries(),
|
||||
keys: () => entries.keys(),
|
||||
values: () => entries.values(),
|
||||
forEach: (
|
||||
callback: (
|
||||
value: AgentHookEventPayload,
|
||||
key: string,
|
||||
map: ReadonlyMap<string, AgentHookEventPayload>
|
||||
) => void,
|
||||
thisArg?: unknown
|
||||
) => entries.forEach((value, key) => callback.call(thisArg, value, key, view)),
|
||||
[Symbol.iterator]: () => entries[Symbol.iterator](),
|
||||
[Symbol.toStringTag]: 'AgentStatusLegacyReadonlyMap'
|
||||
})
|
||||
return view
|
||||
}
|
||||
|
||||
export function createAgentStatusLegacyAdapter(
|
||||
options: AgentStatusLegacyAdapterOptions = {}
|
||||
): AgentStatusLegacyAdapter {
|
||||
const entries = new Map<string, AgentHookEventPayload>()
|
||||
const listingOrderByPaneKey = new Map<string, number>()
|
||||
let nextLocalListingOrder = 0
|
||||
const nextListingOrder = options.nextListingOrder ?? (() => nextLocalListingOrder++)
|
||||
const isCanonicalPaneKey = options.isCanonicalPaneKey ?? (() => false)
|
||||
const view = createReadonlyView(entries)
|
||||
|
||||
return {
|
||||
view,
|
||||
admit: (caller, mode, entry, admitOptions = {}) => {
|
||||
if (isCanonicalPaneKey(entry.paneKey) || !canAdmitLegacyAgentStatus(caller, mode)) {
|
||||
return false
|
||||
}
|
||||
if (!listingOrderByPaneKey.has(entry.paneKey)) {
|
||||
const order = nextListingOrder()
|
||||
if (!Number.isSafeInteger(order) || order < 0) {
|
||||
throw new RangeError(
|
||||
'Legacy agent-status listing order must be a non-negative safe integer'
|
||||
)
|
||||
}
|
||||
listingOrderByPaneKey.set(entry.paneKey, order)
|
||||
}
|
||||
freezeStatusEntry(entry)
|
||||
if (admitOptions.moveToEnd) {
|
||||
entries.delete(entry.paneKey)
|
||||
}
|
||||
entries.set(entry.paneKey, entry)
|
||||
return true
|
||||
},
|
||||
delete: (paneKey) => {
|
||||
listingOrderByPaneKey.delete(paneKey)
|
||||
return entries.delete(paneKey)
|
||||
},
|
||||
clear: () => {
|
||||
entries.clear()
|
||||
listingOrderByPaneKey.clear()
|
||||
},
|
||||
move: (fromPaneKey, toPaneKey) => {
|
||||
if (fromPaneKey === toPaneKey) {
|
||||
return
|
||||
}
|
||||
for (const [key, value] of Array.from(entries)) {
|
||||
if (key !== fromPaneKey && !key.startsWith(`${fromPaneKey}\0`)) {
|
||||
continue
|
||||
}
|
||||
const movedKey = `${toPaneKey}${key.slice(fromPaneKey.length)}`
|
||||
const priorTargetOrder = listingOrderByPaneKey.get(movedKey)
|
||||
const sourceOrder = listingOrderByPaneKey.get(key)
|
||||
entries.delete(key)
|
||||
listingOrderByPaneKey.delete(key)
|
||||
if (isCanonicalPaneKey(movedKey)) {
|
||||
continue
|
||||
}
|
||||
entries.set(movedKey, value)
|
||||
if (priorTargetOrder !== undefined) {
|
||||
listingOrderByPaneKey.set(movedKey, priorTargetOrder)
|
||||
} else if (sourceOrder !== undefined) {
|
||||
listingOrderByPaneKey.set(movedKey, sourceOrder)
|
||||
}
|
||||
}
|
||||
},
|
||||
listingOrder: (paneKey) => listingOrderByPaneKey.get(paneKey)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
export type AgentStatusLegacyIngressDestination = '2B' | '6'
|
||||
|
||||
export type AgentStatusLegacyIngressCaller =
|
||||
| 'main-status-update'
|
||||
| 'main-status-cleanup'
|
||||
| 'main-pane-alias-transfer'
|
||||
| 'main-status-hydration'
|
||||
| 'main-restored-status-reaping'
|
||||
| 'relay-status-cache'
|
||||
| 'shared-bounded-status-cache'
|
||||
|
||||
export type AgentStatusLegacyIngressManifestEntry = {
|
||||
caller: AgentStatusLegacyIngressCaller
|
||||
sourcePath: string
|
||||
reason: string
|
||||
owner: 'main-agent-hooks' | 'relay-agent-hooks' | 'shared-hook-listener'
|
||||
destination: AgentStatusLegacyIngressDestination
|
||||
gate: string
|
||||
allowedModes: readonly AgentStatusLegacyIngressModeKind[]
|
||||
}
|
||||
|
||||
export type AgentStatusLegacyIngressModeKind =
|
||||
| 'current-producer'
|
||||
| 'older-peer'
|
||||
| 'persisted-hydration'
|
||||
|
||||
function entry(
|
||||
value: AgentStatusLegacyIngressManifestEntry
|
||||
): Readonly<AgentStatusLegacyIngressManifestEntry> {
|
||||
return Object.freeze({ ...value, allowedModes: Object.freeze([...value.allowedModes]) })
|
||||
}
|
||||
|
||||
/** Every writable legacy ingress. Entries may only disappear as their destination gate lands. */
|
||||
export const AGENT_STATUS_LEGACY_INGRESS_MANIFEST = Object.freeze([
|
||||
entry({
|
||||
caller: 'main-status-update',
|
||||
sourcePath: 'src/main/agent-hooks/server/server-status-update.ts',
|
||||
reason: 'Hook, OSC, and unsupported-peer observations still use pane ownership in 2A.',
|
||||
owner: 'main-agent-hooks',
|
||||
destination: '2B',
|
||||
gate: 'Trusted PTY scope plus owner-atomic producer handover',
|
||||
allowedModes: ['current-producer', 'older-peer']
|
||||
}),
|
||||
entry({
|
||||
caller: 'main-status-cleanup',
|
||||
sourcePath: 'src/main/agent-hooks/server/server-cleanup.ts',
|
||||
reason: 'Legacy provider-session remnants preserve resume identity during pane cleanup.',
|
||||
owner: 'main-agent-hooks',
|
||||
destination: '2B',
|
||||
gate: 'Canonical run retirement and resume-identity mutation',
|
||||
allowedModes: ['current-producer']
|
||||
}),
|
||||
entry({
|
||||
caller: 'main-pane-alias-transfer',
|
||||
sourcePath: 'src/main/agent-hooks/server/server-authority-aliases.ts',
|
||||
reason: 'A verified pane remint transfers the existing legacy row without minting a run.',
|
||||
owner: 'main-agent-hooks',
|
||||
destination: '2B',
|
||||
gate: 'Scope-preserving canonical pane attachment relocation',
|
||||
allowedModes: ['current-producer']
|
||||
}),
|
||||
entry({
|
||||
caller: 'main-status-hydration',
|
||||
sourcePath: 'src/main/agent-hooks/server/server-hydration.ts',
|
||||
reason: 'Persisted pane evidence is quarantined until host evidence confirms ownership.',
|
||||
owner: 'main-agent-hooks',
|
||||
destination: '6',
|
||||
gate: 'Trusted adoption or bounded unconfirmed-observation retention expiry',
|
||||
allowedModes: ['persisted-hydration']
|
||||
}),
|
||||
entry({
|
||||
caller: 'main-restored-status-reaping',
|
||||
sourcePath: 'src/main/agent-hooks/server/server-reaping.ts',
|
||||
reason: 'Process-probe reconciliation can update a quarantined hydrated pane row.',
|
||||
owner: 'main-agent-hooks',
|
||||
destination: '6',
|
||||
gate: 'Canonical hydration adoption fixtures and compatibility-branch ablation',
|
||||
allowedModes: ['persisted-hydration']
|
||||
}),
|
||||
entry({
|
||||
caller: 'relay-status-cache',
|
||||
sourcePath: 'src/shared/agent-status-legacy-relay-cache.ts',
|
||||
reason: 'The relay retains receiver-fenced replay state until trusted host binding exists.',
|
||||
owner: 'relay-agent-hooks',
|
||||
destination: '2B',
|
||||
gate: 'Relay trusted scope binding plus owner-atomic producer handover',
|
||||
allowedModes: ['current-producer']
|
||||
}),
|
||||
entry({
|
||||
caller: 'shared-bounded-status-cache',
|
||||
sourcePath: 'src/shared/agent-hook-status-cache.ts',
|
||||
reason: 'The bounded legacy cache seam remains available to hook listener owners in 2A.',
|
||||
owner: 'shared-hook-listener',
|
||||
destination: '2B',
|
||||
gate: 'All hook listener owners use the canonical mutation core',
|
||||
allowedModes: ['current-producer']
|
||||
})
|
||||
])
|
||||
|
||||
const LEGACY_INGRESS_BY_CALLER = new Map(
|
||||
AGENT_STATUS_LEGACY_INGRESS_MANIFEST.map((candidate) => [candidate.caller, candidate])
|
||||
)
|
||||
const CURRENT_PRODUCER_LEGACY_INGRESS_MANIFEST = Object.freeze(
|
||||
AGENT_STATUS_LEGACY_INGRESS_MANIFEST.filter((candidate) => candidate.destination === '2B')
|
||||
)
|
||||
|
||||
export function findAgentStatusLegacyIngressManifestEntry(
|
||||
caller: AgentStatusLegacyIngressCaller
|
||||
): Readonly<AgentStatusLegacyIngressManifestEntry> | undefined {
|
||||
return LEGACY_INGRESS_BY_CALLER.get(caller)
|
||||
}
|
||||
|
||||
export function currentProducerAgentStatusLegacyIngressManifest(): readonly Readonly<AgentStatusLegacyIngressManifestEntry>[] {
|
||||
return CURRENT_PRODUCER_LEGACY_INGRESS_MANIFEST
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
AGENT_STATUS_LEGACY_INGRESS_MANIFEST,
|
||||
currentProducerAgentStatusLegacyIngressManifest
|
||||
} from './agent-status-legacy-ingress-manifest'
|
||||
import { findAgentStatusLegacyMutationBypasses } from './agent-status-legacy-source-scan'
|
||||
import { scanSourceTree, stripComments } from './source-scan/source-tree-scan'
|
||||
|
||||
const SOURCE_ROOT = resolve(__dirname, '..')
|
||||
const ADMISSION_CALL = /admitLegacyAgentStatus\(\s*(?:this\.)?state\s*,\s*['"]([^'"]+)['"]/gs
|
||||
|
||||
describe('legacy agent-status ingress ratchet', () => {
|
||||
const productionFiles = scanSourceTree(SOURCE_ROOT)
|
||||
|
||||
it('keeps every production admission call in the explicit manifest', () => {
|
||||
const actual = new Set<string>()
|
||||
let parsedCalls = 0
|
||||
let rawCalls = 0
|
||||
for (const file of productionFiles) {
|
||||
if (file.relativePath === 'shared/agent-hook-listener/listener-state.ts') {
|
||||
continue
|
||||
}
|
||||
const source = stripComments(file.source)
|
||||
rawCalls += source.match(/\badmitLegacyAgentStatus\s*\(/g)?.length ?? 0
|
||||
for (const match of source.matchAll(ADMISSION_CALL)) {
|
||||
parsedCalls += 1
|
||||
actual.add(`src/${file.relativePath}:${match[1]}`)
|
||||
}
|
||||
}
|
||||
expect(parsedCalls, 'Every admission must pass a literal caller id directly.').toBe(rawCalls)
|
||||
|
||||
const declared = new Set(
|
||||
AGENT_STATUS_LEGACY_INGRESS_MANIFEST.map((entry) => `${entry.sourcePath}:${entry.caller}`)
|
||||
)
|
||||
expect([...actual].filter((call) => !declared.has(call))).toEqual([])
|
||||
expect([...declared].filter((call) => !actual.has(call))).toEqual([])
|
||||
|
||||
const indirectAdmissions = productionFiles
|
||||
.filter((file) =>
|
||||
/\badmitLegacyAgentStatus\s+as\s+|=\s*admitLegacyAgentStatus\b|\(\s*admitLegacyAgentStatus\s*[,)]/.test(
|
||||
stripComments(file.source)
|
||||
)
|
||||
)
|
||||
.map((file) => file.relativePath)
|
||||
expect(indirectAdmissions).toEqual([])
|
||||
})
|
||||
|
||||
it('allows no mutable Map path around the adapter', () => {
|
||||
const bypasses = productionFiles
|
||||
.filter((file) => file.source.includes('lastStatusByPaneKey'))
|
||||
.flatMap((file) =>
|
||||
findAgentStatusLegacyMutationBypasses(file.source).map(
|
||||
(bypass) => `${file.relativePath}: ${bypass.kind}: ${bypass.detail}`
|
||||
)
|
||||
)
|
||||
expect(bypasses).toEqual([])
|
||||
})
|
||||
|
||||
it('detects direct, aliased, cast, and passed-map mutation bypasses', () => {
|
||||
const planted = findAgentStatusLegacyMutationBypasses(`
|
||||
state.lastStatusByPaneKey.set('pane', row)
|
||||
const alias = state.lastStatusByPaneKey
|
||||
alias.delete('pane')
|
||||
const { lastStatusByPaneKey } = state
|
||||
lastStatusByPaneKey.clear()
|
||||
;(state.lastStatusByPaneKey as unknown as Map<string, Row>).clear()
|
||||
state['lastStatusByPaneKey'].set('pane', row)
|
||||
mutateStatusMap(state.lastStatusByPaneKey)
|
||||
const { lastStatusByPaneKey: passed } = state
|
||||
mutateStatusMap(passed)
|
||||
`)
|
||||
expect(new Set(planted.map((bypass) => bypass.kind))).toEqual(
|
||||
new Set(['direct-mutation', 'alias-mutation', 'map-cast', 'passed-map'])
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps the manifest immutable, descriptive, and partitioned by its exit gate', () => {
|
||||
expect(Object.isFrozen(AGENT_STATUS_LEGACY_INGRESS_MANIFEST)).toBe(true)
|
||||
const currentProducerManifest = currentProducerAgentStatusLegacyIngressManifest()
|
||||
expect(currentProducerManifest.length).toBeGreaterThan(0)
|
||||
expect(currentProducerAgentStatusLegacyIngressManifest()).toBe(currentProducerManifest)
|
||||
expect(Object.isFrozen(currentProducerManifest)).toBe(true)
|
||||
expect(new Set(AGENT_STATUS_LEGACY_INGRESS_MANIFEST.map((entry) => entry.caller)).size).toBe(
|
||||
AGENT_STATUS_LEGACY_INGRESS_MANIFEST.length
|
||||
)
|
||||
for (const entry of AGENT_STATUS_LEGACY_INGRESS_MANIFEST) {
|
||||
expect(Object.isFrozen(entry)).toBe(true)
|
||||
expect(Object.isFrozen(entry.allowedModes)).toBe(true)
|
||||
expect(entry.reason.length).toBeGreaterThan(20)
|
||||
expect(entry.gate.length).toBeGreaterThan(20)
|
||||
expect(['2B', '6']).toContain(entry.destination)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps adapter construction and test seeding behind listener-state', () => {
|
||||
const forbidden = productionFiles
|
||||
.filter(
|
||||
(file) =>
|
||||
file.relativePath !== 'shared/agent-hook-listener/listener-state.ts' &&
|
||||
file.relativePath !== 'shared/agent-status-legacy-adapter.ts'
|
||||
)
|
||||
.filter(
|
||||
(file) =>
|
||||
stripComments(file.source).includes('createAgentStatusLegacyAdapter') ||
|
||||
stripComments(file.source).includes('seedLegacyAgentStatusForTests')
|
||||
)
|
||||
.map((file) => file.relativePath)
|
||||
expect(forbidden).toEqual([])
|
||||
})
|
||||
|
||||
it('requires every production remote ingress to name the unsupported-peer capability source', () => {
|
||||
const callers = productionFiles.filter((file) => /\.ingestRemote\s*\(/.test(file.source))
|
||||
expect(callers.map((file) => file.relativePath).sort()).toEqual([
|
||||
'main/agent-hooks/wsl-hook-relay-deps.ts',
|
||||
'main/ssh/ssh-relay-session.ts'
|
||||
])
|
||||
// Why: a bare import of the constant (unused elsewhere) would pass a substring check
|
||||
// without ever stamping it onto the envelope — require the actual key:value binding.
|
||||
for (const caller of callers) {
|
||||
expect(stripComments(caller.source)).toMatch(
|
||||
/advertisedAgentStatusCapabilities\s*:\s*AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES\b/
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps run-capability advertisement behind the serving gate', () => {
|
||||
const constantUsers = productionFiles
|
||||
.filter((file) => file.source.includes('AGENT_STATUS_RUNS_RUNTIME_CAPABILITY'))
|
||||
.map((file) => file.relativePath)
|
||||
.sort()
|
||||
expect(constantUsers).toEqual([
|
||||
'shared/agent-status-run-capability.ts',
|
||||
'shared/agent-status-serving-readiness.ts'
|
||||
])
|
||||
const literalUsers = productionFiles
|
||||
.filter((file) => /['"]agent-status\.runs\.v1['"]/.test(file.source))
|
||||
.map((file) => file.relativePath)
|
||||
expect(literalUsers).toEqual(['shared/agent-status-run-capability.ts'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { AgentHookEventPayload } from './agent-hook-listener/listener-event'
|
||||
import {
|
||||
admitLegacyAgentStatus,
|
||||
type HookListenerState
|
||||
} from './agent-hook-listener/listener-state'
|
||||
import { AGENT_STATUS_2A_CURRENT_PRODUCER_MODE } from './agent-status-legacy-adapter'
|
||||
|
||||
export function cacheRelayLegacyAgentStatus(
|
||||
state: HookListenerState,
|
||||
entry: AgentHookEventPayload,
|
||||
maxPanes: number,
|
||||
dropPane: (paneKey: string) => void
|
||||
): boolean {
|
||||
if (
|
||||
!admitLegacyAgentStatus(
|
||||
state,
|
||||
'relay-status-cache',
|
||||
entry,
|
||||
AGENT_STATUS_2A_CURRENT_PRODUCER_MODE,
|
||||
{ moveToEnd: true }
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
while (state.lastStatusByPaneKey.size > maxPanes) {
|
||||
const oldest = state.lastStatusByPaneKey.keys().next().value
|
||||
if (oldest === undefined) {
|
||||
return false
|
||||
}
|
||||
dropPane(oldest)
|
||||
}
|
||||
return true
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import {
|
||||
blankStringContents,
|
||||
blankStringContentsDesynced,
|
||||
stripComments
|
||||
} from './source-scan/source-tree-scan'
|
||||
|
||||
export type AgentStatusLegacyMutationBypass = {
|
||||
kind: 'direct-mutation' | 'alias-mutation' | 'map-cast' | 'passed-map' | 'scan-desync'
|
||||
detail: string
|
||||
}
|
||||
|
||||
const MUTATOR_NAMES = '(?:set|delete|clear)'
|
||||
const IDENTIFIER = '[A-Za-z_$][A-Za-z0-9_$]*'
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
function aliasesForLegacyStatusMap(source: string): Set<string> {
|
||||
const aliases = new Set<string>()
|
||||
const assignment = new RegExp(
|
||||
`\\b(?:const|let|var)\\s+(${IDENTIFIER})\\s*=\\s*(?:this\\.)?state\\.lastStatusByPaneKey\\s*(?:;|\\n|$)`,
|
||||
'g'
|
||||
)
|
||||
for (const match of source.matchAll(assignment)) {
|
||||
aliases.add(match[1]!)
|
||||
}
|
||||
const destructuring = new RegExp(
|
||||
`\\b(?:const|let|var)\\s*\\{[^}]*\\blastStatusByPaneKey(?:\\s*:\\s*(${IDENTIFIER}))?`,
|
||||
'g'
|
||||
)
|
||||
for (const match of source.matchAll(destructuring)) {
|
||||
aliases.add(match[1] ?? 'lastStatusByPaneKey')
|
||||
}
|
||||
return aliases
|
||||
}
|
||||
|
||||
function isReadonlySnapshotCallPrefix(prefix: string): boolean {
|
||||
return /(?:Array\.from|new\s+Map)\(\s*$/.test(prefix)
|
||||
}
|
||||
|
||||
export function findAgentStatusLegacyMutationBypasses(
|
||||
source: string
|
||||
): AgentStatusLegacyMutationBypass[] {
|
||||
const stripped = stripComments(source)
|
||||
if (blankStringContentsDesynced(stripped)) {
|
||||
return [{ kind: 'scan-desync', detail: 'source scanner lost quote or template state' }]
|
||||
}
|
||||
const code = blankStringContents(stripped)
|
||||
const bypasses: AgentStatusLegacyMutationBypass[] = []
|
||||
if (
|
||||
new RegExp(`\\blastStatusByPaneKey\\s*\\.\\s*${MUTATOR_NAMES}\\s*\\(`).test(code) ||
|
||||
/\[['"]lastStatusByPaneKey['"]\]\s*\.\s*(?:set|delete|clear)\s*\(/.test(stripped)
|
||||
) {
|
||||
bypasses.push({ kind: 'direct-mutation', detail: 'lastStatusByPaneKey mutator call' })
|
||||
}
|
||||
if (
|
||||
new RegExp(
|
||||
`\\blastStatusByPaneKey\\b[\\s\\S]{0,100}\\bas\\s+(?:unknown\\s+as\\s+)?(?:Readonly)?Map\\b[\\s\\S]{0,100}\\.\\s*${MUTATOR_NAMES}\\s*\\(`
|
||||
).test(code)
|
||||
) {
|
||||
bypasses.push({ kind: 'map-cast', detail: 'lastStatusByPaneKey cast back to a mutable Map' })
|
||||
}
|
||||
|
||||
const aliases = aliasesForLegacyStatusMap(code)
|
||||
for (const alias of aliases) {
|
||||
const escaped = escapeRegExp(alias)
|
||||
if (new RegExp(`\\b${escaped}\\s*\\.\\s*${MUTATOR_NAMES}\\s*\\(`).test(code)) {
|
||||
bypasses.push({ kind: 'alias-mutation', detail: `${alias} mutates an aliased status map` })
|
||||
}
|
||||
const passed = new RegExp(`\\b${IDENTIFIER}(?:\\.${IDENTIFIER})*\\s*\\(\\s*${escaped}\\b`, 'g')
|
||||
for (const match of code.matchAll(passed)) {
|
||||
const prefix = code.slice(
|
||||
Math.max(0, match.index - 24),
|
||||
match.index + match[0].indexOf('(') + 1
|
||||
)
|
||||
if (!isReadonlySnapshotCallPrefix(prefix)) {
|
||||
bypasses.push({ kind: 'passed-map', detail: `${alias} is passed to another function` })
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const directPass = new RegExp(
|
||||
`\\b(${IDENTIFIER}(?:\\.${IDENTIFIER})*)\\s*\\(\\s*((?:this\\.)?state\\.lastStatusByPaneKey)\\s*[,)]`,
|
||||
'g'
|
||||
)
|
||||
for (const match of code.matchAll(directPass)) {
|
||||
if (match[1] !== 'Array.from' && match[1] !== 'Map') {
|
||||
bypasses.push({
|
||||
kind: 'passed-map',
|
||||
detail: 'lastStatusByPaneKey is passed to another function'
|
||||
})
|
||||
}
|
||||
}
|
||||
return bypasses
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
AGENT_STATUS_2A_SERVING_READINESS,
|
||||
agentStatusRunServingGatePasses,
|
||||
advertisedAgentStatusRunCapabilities,
|
||||
isAgentStatusRunServingAdvertised
|
||||
} from './agent-status-serving-readiness'
|
||||
|
||||
describe('agent-status run serving readiness', () => {
|
||||
it('keeps run serving unadvertised throughout 2A', () => {
|
||||
expect(isAgentStatusRunServingAdvertised(AGENT_STATUS_2A_SERVING_READINESS)).toBe(false)
|
||||
expect(advertisedAgentStatusRunCapabilities(AGENT_STATUS_2A_SERVING_READINESS)).toEqual([])
|
||||
})
|
||||
|
||||
it('requires both serving readiness and an empty current-producer manifest', () => {
|
||||
const ready = { servingReady: true }
|
||||
expect(
|
||||
agentStatusRunServingGatePasses({
|
||||
readiness: ready,
|
||||
currentProducerManifest: [{ caller: 'still-legacy' }]
|
||||
})
|
||||
).toBe(false)
|
||||
expect(agentStatusRunServingGatePasses({ readiness: ready, currentProducerManifest: [] })).toBe(
|
||||
true
|
||||
)
|
||||
expect(advertisedAgentStatusRunCapabilities(ready)).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,35 @@
|
||||
import { AGENT_STATUS_RUNS_RUNTIME_CAPABILITY } from './agent-status-run-capability'
|
||||
import { currentProducerAgentStatusLegacyIngressManifest } from './agent-status-legacy-ingress-manifest'
|
||||
|
||||
export type AgentStatusServingReadiness = Readonly<{
|
||||
servingReady: boolean
|
||||
}>
|
||||
|
||||
export type AgentStatusServingGateEvidence = {
|
||||
readiness: AgentStatusServingReadiness
|
||||
currentProducerManifest: readonly unknown[]
|
||||
}
|
||||
|
||||
/** 2A defines the gate but deliberately does not claim run-serving readiness. */
|
||||
export const AGENT_STATUS_2A_SERVING_READINESS: AgentStatusServingReadiness = Object.freeze({
|
||||
servingReady: false
|
||||
})
|
||||
|
||||
export function agentStatusRunServingGatePasses(evidence: AgentStatusServingGateEvidence): boolean {
|
||||
return evidence.readiness.servingReady && evidence.currentProducerManifest.length === 0
|
||||
}
|
||||
|
||||
export function isAgentStatusRunServingAdvertised(readiness: AgentStatusServingReadiness): boolean {
|
||||
return agentStatusRunServingGatePasses({
|
||||
readiness,
|
||||
currentProducerManifest: currentProducerAgentStatusLegacyIngressManifest()
|
||||
})
|
||||
}
|
||||
|
||||
export function advertisedAgentStatusRunCapabilities(
|
||||
readiness: AgentStatusServingReadiness
|
||||
): readonly string[] {
|
||||
return isAgentStatusRunServingAdvertised(readiness)
|
||||
? Object.freeze([AGENT_STATUS_RUNS_RUNTIME_CAPABILITY])
|
||||
: Object.freeze([])
|
||||
}
|
||||
Reference in New Issue
Block a user