mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
fix: harden relay hook cache hydration
This commit is contained in:
@@ -366,6 +366,75 @@ describe('RelayAgentHookServer', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('skips malformed durable entries while hydrating valid status and diagnostics', async () => {
|
||||
writeFileSync(
|
||||
join(dir, 'hook-status-cache.json'),
|
||||
JSON.stringify({
|
||||
version: 1,
|
||||
entries: [
|
||||
{ event: { paneKey: PANE_KEY }, meta: { source: 'claude' } },
|
||||
{
|
||||
event: {
|
||||
paneKey: PANE_KEY,
|
||||
payload: { state: 'working', prompt: 'missing agent type' }
|
||||
},
|
||||
meta: { source: 'claude' }
|
||||
},
|
||||
{
|
||||
event: {
|
||||
paneKey: PANE_KEY,
|
||||
payload: null
|
||||
},
|
||||
meta: { source: 'claude' }
|
||||
},
|
||||
{
|
||||
event: {
|
||||
paneKey: PANE_KEY,
|
||||
payload: {
|
||||
state: 'working',
|
||||
prompt: 'keep me after restart',
|
||||
agentType: 'codex'
|
||||
},
|
||||
reconcileDiagnostic: {
|
||||
kind: 'unverifiable',
|
||||
reason: 'transcript-unreadable',
|
||||
observedAt: 123
|
||||
}
|
||||
},
|
||||
meta: { source: 'codex', env: 'remote', version: '1' }
|
||||
}
|
||||
]
|
||||
}),
|
||||
'utf8'
|
||||
)
|
||||
|
||||
const forward = vi.fn<(envelope: AgentHookRelayEnvelope) => void>()
|
||||
const server = new RelayAgentHookServer({ endpointDir: dir, forward })
|
||||
await expect(server.start()).resolves.toBeUndefined()
|
||||
try {
|
||||
expect(server.replayCachedPayloadsForPanes()).toBe(1)
|
||||
expect(forward).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
source: 'codex',
|
||||
env: 'remote',
|
||||
version: '1',
|
||||
payload: expect.objectContaining({
|
||||
prompt: 'keep me after restart',
|
||||
agentType: 'codex'
|
||||
}),
|
||||
reconcileDiagnostic: {
|
||||
kind: 'unverifiable',
|
||||
reason: 'transcript-unreadable',
|
||||
observedAt: 123
|
||||
},
|
||||
isReplay: true
|
||||
})
|
||||
)
|
||||
} finally {
|
||||
server.stop()
|
||||
}
|
||||
})
|
||||
|
||||
it('forwards and replays Pi session identity as metadata-only', async () => {
|
||||
const forward = vi.fn<(envelope: AgentHookRelayEnvelope) => void>()
|
||||
const server = new RelayAgentHookServer({ endpointDir: dir, forward })
|
||||
|
||||
@@ -6,10 +6,16 @@ import {
|
||||
type AgentHookEventPayload,
|
||||
type HookListenerState
|
||||
} from '../shared/agent-hook-listener'
|
||||
import type { AgentHookRelayEnvelope, AgentHookSource } from '../shared/agent-hook-relay'
|
||||
import { normalizeAgentStatusPayload } from '../shared/agent-status-types'
|
||||
import {
|
||||
isAgentHookSource,
|
||||
type AgentHookRelayEnvelope,
|
||||
type AgentHookSource
|
||||
} from '../shared/agent-hook-relay'
|
||||
import { codexRosterToSnapshots } from '../shared/codex-subagent-roster'
|
||||
import { reconcileCodexSubagentTranscript } from '../shared/codex-subagent-transcript'
|
||||
import { buildRelayHookEnvelope } from './agent-hook-envelope-build'
|
||||
import { parsePaneKey } from '../shared/stable-pane-id'
|
||||
|
||||
export type RelayHookStatusMeta = { source: AgentHookSource; env?: string; version?: string }
|
||||
type PersistedCache = {
|
||||
@@ -22,6 +28,50 @@ const MAX_CACHED_PANES = 256
|
||||
const MAX_RECONCILE_ATTEMPTS = 5
|
||||
const RECONCILE_INTERVAL_MS = 1_000
|
||||
|
||||
function sanitizeHydratedEntry(
|
||||
rawEntry: unknown
|
||||
): { event: AgentHookEventPayload; meta: RelayHookStatusMeta } | null {
|
||||
if (typeof rawEntry !== 'object' || rawEntry === null) {
|
||||
return null
|
||||
}
|
||||
const entry = rawEntry as Record<string, unknown>
|
||||
if (typeof entry.event !== 'object' || entry.event === null) {
|
||||
return null
|
||||
}
|
||||
if (typeof entry.meta !== 'object' || entry.meta === null) {
|
||||
return null
|
||||
}
|
||||
const rawEvent = entry.event as Record<string, unknown>
|
||||
const paneKey = rawEvent.paneKey
|
||||
if (typeof paneKey !== 'string' || !parsePaneKey(paneKey)) {
|
||||
return null
|
||||
}
|
||||
const payload = normalizeAgentStatusPayload(rawEvent.payload)
|
||||
// Every normalized hook event carries its source agent type. Without it,
|
||||
// restart reconciliation and replay would operate on an untrusted shape.
|
||||
if (!payload || typeof payload.agentType !== 'string' || payload.agentType.length === 0) {
|
||||
return null
|
||||
}
|
||||
const rawMeta = entry.meta as Record<string, unknown>
|
||||
if (!isAgentHookSource(rawMeta.source)) {
|
||||
return null
|
||||
}
|
||||
if (
|
||||
(rawMeta.env !== undefined && typeof rawMeta.env !== 'string') ||
|
||||
(rawMeta.version !== undefined && typeof rawMeta.version !== 'string')
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
event: { ...rawEvent, paneKey, payload } as AgentHookEventPayload,
|
||||
meta: {
|
||||
source: rawMeta.source,
|
||||
env: typeof rawMeta.env === 'string' ? rawMeta.env : undefined,
|
||||
version: typeof rawMeta.version === 'string' ? rawMeta.version : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function applyRelayHookEvent(options: {
|
||||
state: HookListenerState
|
||||
event: AgentHookEventPayload
|
||||
@@ -107,8 +157,9 @@ export function hydrateRelayHookStatusCache(
|
||||
if (cache.version !== CACHE_VERSION || !Array.isArray(cache.entries)) {
|
||||
return metadata
|
||||
}
|
||||
for (const entry of cache.entries.slice(-MAX_CACHED_PANES)) {
|
||||
if (!entry?.event || !entry.meta || typeof entry.event.paneKey !== 'string') {
|
||||
for (const rawEntry of cache.entries.slice(-MAX_CACHED_PANES)) {
|
||||
const entry = sanitizeHydratedEntry(rawEntry)
|
||||
if (!entry) {
|
||||
continue
|
||||
}
|
||||
const event =
|
||||
|
||||
Reference in New Issue
Block a user