mirror of
https://github.com/stablyai/orca.git
synced 2026-09-24 16:02:41 +00:00
feat(agent-status): add canonical shared store contract
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createAgentChildWorkAdmission } from '../shared/agent-status-child-work-admission'
|
||||
import { createAgentStatusStore } from '../shared/agent-status-store'
|
||||
import { makeStructuredAgentStatusSubject } from '../shared/agent-status-subject'
|
||||
|
||||
const SHARED_CORE_FILES = [
|
||||
'agent-status-child-work.ts',
|
||||
'agent-status-child-work-codec.ts',
|
||||
'agent-status-child-work-admission.ts',
|
||||
'agent-status-child-work-admission-core.ts',
|
||||
'agent-status-child-work-admission-operations.ts',
|
||||
'agent-status-child-work-alias.ts',
|
||||
'agent-status-child-work-freshness.ts',
|
||||
'agent-status-child-work-projection.ts',
|
||||
'agent-status-store.ts',
|
||||
'agent-status-store-codec.ts',
|
||||
'agent-status-store-mutation.ts',
|
||||
'agent-status-store-contract.ts',
|
||||
'agent-status-store-fact-codec.ts',
|
||||
'agent-status-store-parent.ts',
|
||||
'agent-status-store-persistence.ts',
|
||||
'agent-status-store-state.ts',
|
||||
'agent-status-store-status-codec.ts',
|
||||
'agent-status-transport-envelope.ts'
|
||||
]
|
||||
|
||||
const trustedSubject = makeStructuredAgentStatusSubject(
|
||||
{
|
||||
executionHostId: 'ssh:relay-host-a',
|
||||
wslDistro: null,
|
||||
workspaceId: 'folder-workspace-a',
|
||||
workspaceKind: 'folder'
|
||||
},
|
||||
'session_11111111-1111-4111-8111-111111111111'
|
||||
)
|
||||
|
||||
describe('agent status store relay context', () => {
|
||||
it('instantiates the same shared core and completes an admission/snapshot round-trip', () => {
|
||||
const authority = createAgentStatusStore({ epoch: 'relay-epoch-a', mode: 'authority' })
|
||||
expect(
|
||||
authority.applyMutation({ parent: { subject: trustedSubject, firstObservedAt: 10 } })
|
||||
).not.toBeNull()
|
||||
const admission = createAgentChildWorkAdmission(authority, {
|
||||
mintChildWorkId: () => 'relay-child-1'
|
||||
})
|
||||
|
||||
expect(
|
||||
admission.announce({
|
||||
parent: trustedSubject,
|
||||
provider: 'claude',
|
||||
aliases: [{ segmentId: 'segment-1', aliasKind: 'task_id', alias: 'task-1' }],
|
||||
fence: { invocationId: 'invocation-1', generation: 1 },
|
||||
lifetime: 'current',
|
||||
kind: 'agent',
|
||||
state: 'working',
|
||||
membership: 'live',
|
||||
observedAt: 20,
|
||||
stoppable: true,
|
||||
provenance: { source: 'transport', producerId: 'relay-fixture' }
|
||||
})
|
||||
).toMatchObject({ accepted: true, childWorkId: 'relay-child-1' })
|
||||
|
||||
const replica = createAgentStatusStore({ epoch: 'replica-placeholder', mode: 'replica' })
|
||||
expect(replica.applySnapshot(authority.getSnapshot())).toBe(true)
|
||||
expect(replica.getParent(trustedSubject)?.firstObservedAt).toBe(10)
|
||||
expect(replica.getChildren(trustedSubject)[0]?.childWorkId).toBe('relay-child-1')
|
||||
})
|
||||
|
||||
it('keeps the relay-consumed core free of main, renderer and Electron imports', () => {
|
||||
for (const filename of SHARED_CORE_FILES) {
|
||||
const source = readFileSync(new URL(`../shared/${filename}`, import.meta.url), 'utf8')
|
||||
expect(source, filename).not.toMatch(
|
||||
/from\s+['"](?:electron|\.\.\/(?:main|renderer))(?:\/|['"])/
|
||||
)
|
||||
expect(source, filename).not.toMatch(/require\(['"]electron['"]\)/)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createAgentChildWorkAdmission } from './agent-status-child-work-admission'
|
||||
import { createAgentStatusStore } from './agent-status-store'
|
||||
import {
|
||||
makeStructuredAgentStatusSubject,
|
||||
type AgentStatusExecutionScope,
|
||||
type AgentStatusSubject
|
||||
} from './agent-status-subject'
|
||||
|
||||
const SESSION_ID = 'session_11111111-1111-4111-8111-111111111111'
|
||||
|
||||
function subject(overrides: Partial<AgentStatusExecutionScope> = {}): AgentStatusSubject {
|
||||
return makeStructuredAgentStatusSubject(
|
||||
{
|
||||
executionHostId: 'local',
|
||||
wslDistro: null,
|
||||
workspaceId: 'workspace-1',
|
||||
workspaceKind: 'git-worktree',
|
||||
...overrides
|
||||
},
|
||||
SESSION_ID
|
||||
)
|
||||
}
|
||||
|
||||
function request(parent: AgentStatusSubject, kind: 'agent' | 'unknown') {
|
||||
return {
|
||||
parent,
|
||||
provider: 'claude',
|
||||
aliases: [{ segmentId: 'segment-1', aliasKind: 'task_id' as const, alias: 'task-1' }],
|
||||
fence: { invocationId: `invocation-${kind}`, generation: kind === 'unknown' ? 1 : 2 },
|
||||
lifetime: 'current' as const,
|
||||
kind,
|
||||
state: 'working' as const,
|
||||
membership: 'live' as const,
|
||||
observedAt: 10,
|
||||
stoppable: true,
|
||||
provenance: { source: 'structured-session' as const, producerId: 'journal-1' }
|
||||
}
|
||||
}
|
||||
|
||||
describe('agent child-work admission collisions', () => {
|
||||
it('rejects an adopt whose alias reclassification would capture another child', () => {
|
||||
const parent = subject()
|
||||
const store = createAgentStatusStore({ epoch: 'epoch-a', mode: 'authority' })
|
||||
store.applyMutation({ parent: { subject: parent } })
|
||||
const ids = ['child-1', 'child-2']
|
||||
const admission = createAgentChildWorkAdmission(store, {
|
||||
mintChildWorkId: () => ids.shift() ?? 'unexpected-child'
|
||||
})
|
||||
admission.announce(request(parent, 'unknown'))
|
||||
admission.announce(request(parent, 'agent'))
|
||||
const before = store.getSnapshot()
|
||||
|
||||
expect(
|
||||
admission.adopt({
|
||||
...request(parent, 'unknown'),
|
||||
childWorkId: 'child-1',
|
||||
expectedFence: { invocationId: 'invocation-unknown', generation: 1 },
|
||||
kind: 'agent',
|
||||
observedAt: 20
|
||||
})
|
||||
).toEqual({ accepted: false, reason: 'ambiguous' })
|
||||
expect(store.getSnapshot()).toEqual(before)
|
||||
})
|
||||
|
||||
it('rejects reparenting into an occupied alias scope without moving the child', () => {
|
||||
const firstParent = subject()
|
||||
const nextParent = subject({ workspaceId: 'workspace-2' })
|
||||
const store = createAgentStatusStore({ epoch: 'epoch-a', mode: 'authority' })
|
||||
store.applyMutation({ parent: { subject: firstParent } })
|
||||
store.applyMutation({ parent: { subject: nextParent } })
|
||||
const ids = ['child-1', 'child-2']
|
||||
const admission = createAgentChildWorkAdmission(store, {
|
||||
mintChildWorkId: () => ids.shift() ?? 'unexpected-child'
|
||||
})
|
||||
admission.announce(request(firstParent, 'agent'))
|
||||
admission.announce(request(nextParent, 'agent'))
|
||||
const before = store.getSnapshot()
|
||||
|
||||
expect(
|
||||
admission.reparent({
|
||||
childWorkId: 'child-1',
|
||||
fromParent: firstParent,
|
||||
toParent: nextParent,
|
||||
expectedFence: { invocationId: 'invocation-agent', generation: 2 },
|
||||
observedAt: 20
|
||||
})
|
||||
).toEqual({ accepted: false, reason: 'ambiguous' })
|
||||
expect(store.getSnapshot()).toEqual(before)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,171 @@
|
||||
import {
|
||||
serializeAgentChildWorkAliasKey,
|
||||
type AgentChildWorkAliasInput,
|
||||
type AgentChildWorkAliasRecord
|
||||
} from './agent-status-child-work-alias'
|
||||
import {
|
||||
agentChildWorkFencesEqual,
|
||||
type AgentChildWorkId,
|
||||
type AgentChildWorkInput,
|
||||
type AgentChildWorkInvocationFence,
|
||||
type AgentChildWorkKind,
|
||||
type AgentChildWorkRecord
|
||||
} from './agent-status-child-work'
|
||||
import { parseAgentChildWorkInput } from './agent-status-child-work-codec'
|
||||
import type {
|
||||
AgentChildWorkAdmissionResult,
|
||||
AgentChildWorkAdoptRequest,
|
||||
AgentChildWorkAnnounceRequest,
|
||||
AgentChildWorkObservationAlias,
|
||||
AgentChildWorkObservationFields
|
||||
} from './agent-status-child-work-admission'
|
||||
import type { AgentStatusStore } from './agent-status-store'
|
||||
import { agentStatusSubjectsEqual, type AgentStatusSubject } from './agent-status-subject'
|
||||
|
||||
const MAX_ALIASES_PER_ADMISSION = 32
|
||||
|
||||
export function rejectAgentChildWorkAdmission(
|
||||
reason: Extract<AgentChildWorkAdmissionResult, { accepted: false }>['reason']
|
||||
) {
|
||||
return { accepted: false, reason } as const
|
||||
}
|
||||
|
||||
export function findAgentChildWork(
|
||||
store: AgentStatusStore,
|
||||
childWorkId: string
|
||||
): AgentChildWorkRecord | null {
|
||||
return store.getSnapshot().children.find((child) => child.childWorkId === childWorkId) ?? null
|
||||
}
|
||||
|
||||
export function agentChildWorkAliasesForChild(
|
||||
store: AgentStatusStore,
|
||||
childWorkId: string
|
||||
): AgentChildWorkAliasRecord[] {
|
||||
return store.getSnapshot().aliases.filter((alias) => alias.childWorkId === childWorkId)
|
||||
}
|
||||
|
||||
export function buildAgentChildWorkAliases(
|
||||
parent: AgentStatusSubject,
|
||||
provider: string,
|
||||
kind: AgentChildWorkKind,
|
||||
aliases: AgentChildWorkObservationAlias[],
|
||||
childWorkId: AgentChildWorkId,
|
||||
fence: AgentChildWorkInvocationFence
|
||||
): AgentChildWorkAliasInput[] | null {
|
||||
if (aliases.length === 0 || aliases.length > MAX_ALIASES_PER_ADMISSION) {
|
||||
return null
|
||||
}
|
||||
const built: AgentChildWorkAliasInput[] = []
|
||||
const keys = new Set<string>()
|
||||
try {
|
||||
for (const alias of aliases) {
|
||||
const candidate = { parent, provider, kind, ...alias, childWorkId, fence }
|
||||
const key = serializeAgentChildWorkAliasKey(candidate)
|
||||
if (keys.has(key)) {
|
||||
return null
|
||||
}
|
||||
keys.add(key)
|
||||
built.push(candidate)
|
||||
}
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
return built
|
||||
}
|
||||
|
||||
export function buildAgentChildWork(
|
||||
request: AgentChildWorkObservationFields & {
|
||||
parent: AgentStatusSubject
|
||||
provider: string
|
||||
},
|
||||
childWorkId: string,
|
||||
firstObservedAt: number,
|
||||
invocation: AgentChildWorkInvocationFence,
|
||||
previousInvocations?: AgentChildWorkInput['previousInvocations']
|
||||
): AgentChildWorkInput | null {
|
||||
return parseAgentChildWorkInput({
|
||||
childWorkId,
|
||||
parent: request.parent,
|
||||
provider: request.provider,
|
||||
kind: request.kind,
|
||||
state: request.state,
|
||||
membership: request.membership,
|
||||
...(request.outcome !== undefined ? { outcome: request.outcome } : {}),
|
||||
...(request.name !== undefined ? { name: request.name } : {}),
|
||||
...(request.description !== undefined ? { description: request.description } : {}),
|
||||
...(request.agentType !== undefined ? { agentType: request.agentType } : {}),
|
||||
...(request.model !== undefined ? { model: request.model } : {}),
|
||||
...(request.totalTokens !== undefined ? { totalTokens: request.totalTokens } : {}),
|
||||
...(request.providerTiming !== undefined ? { providerTiming: request.providerTiming } : {}),
|
||||
firstObservedAt,
|
||||
observedAt: request.observedAt,
|
||||
stoppable: request.stoppable,
|
||||
invocation,
|
||||
...(previousInvocations !== undefined ? { previousInvocations } : {}),
|
||||
provenance: request.provenance
|
||||
})
|
||||
}
|
||||
|
||||
export function commitAgentChildWork(
|
||||
store: AgentStatusStore,
|
||||
child: AgentChildWorkInput,
|
||||
aliases: AgentChildWorkAliasInput[],
|
||||
created: boolean,
|
||||
removeAliases: string[] = []
|
||||
): AgentChildWorkAdmissionResult {
|
||||
const envelope = store.applyMutation({
|
||||
children: [child],
|
||||
aliases,
|
||||
...(removeAliases.length > 0 ? { removeAliases } : {})
|
||||
})
|
||||
return envelope
|
||||
? { accepted: true, childWorkId: child.childWorkId, revision: envelope.revision, created }
|
||||
: rejectAgentChildWorkAdmission('store-rejected')
|
||||
}
|
||||
|
||||
export function updateExistingAgentChildWork(
|
||||
store: AgentStatusStore,
|
||||
request: AgentChildWorkAnnounceRequest | AgentChildWorkAdoptRequest,
|
||||
child: AgentChildWorkRecord,
|
||||
aliases: AgentChildWorkAliasInput[],
|
||||
removeAliases: string[] = []
|
||||
): AgentChildWorkAdmissionResult {
|
||||
const updated = buildAgentChildWork(
|
||||
request,
|
||||
child.childWorkId,
|
||||
child.firstObservedAt,
|
||||
child.invocation,
|
||||
child.previousInvocations
|
||||
)
|
||||
return updated
|
||||
? commitAgentChildWork(store, updated, aliases, false, removeAliases)
|
||||
: rejectAgentChildWorkAdmission('invalid')
|
||||
}
|
||||
|
||||
export function resolveAgentChildWorkAliasRecords(
|
||||
store: AgentStatusStore,
|
||||
aliases: AgentChildWorkAliasInput[]
|
||||
): AgentChildWorkAliasRecord[] {
|
||||
const keys = new Set(aliases.map(serializeAgentChildWorkAliasKey))
|
||||
return store
|
||||
.getSnapshot()
|
||||
.aliases.filter((alias) => keys.has(serializeAgentChildWorkAliasKey(alias)))
|
||||
}
|
||||
|
||||
export function validateExistingAgentChildWork(
|
||||
child: AgentChildWorkRecord | null,
|
||||
parent: AgentStatusSubject,
|
||||
provider: string,
|
||||
expectedFence: AgentChildWorkInvocationFence
|
||||
): AgentChildWorkAdmissionResult | null {
|
||||
if (!child) {
|
||||
return rejectAgentChildWorkAdmission('unknown-child')
|
||||
}
|
||||
if (!agentStatusSubjectsEqual(child.parent, parent) || child.provider !== provider) {
|
||||
return rejectAgentChildWorkAdmission('ambiguous')
|
||||
}
|
||||
if (!agentChildWorkFencesEqual(child.invocation, expectedFence)) {
|
||||
return rejectAgentChildWorkAdmission('stale-invocation')
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
import { serializeAgentChildWorkAliasKey } from './agent-status-child-work-alias'
|
||||
import {
|
||||
AGENT_CHILD_WORK_INVOCATION_HISTORY_MAX,
|
||||
agentChildWorkFencesEqual,
|
||||
type AgentChildWorkId,
|
||||
type AgentChildWorkRecord
|
||||
} from './agent-status-child-work'
|
||||
import {
|
||||
agentChildWorkAliasesForChild,
|
||||
buildAgentChildWork,
|
||||
buildAgentChildWorkAliases,
|
||||
commitAgentChildWork,
|
||||
findAgentChildWork,
|
||||
rejectAgentChildWorkAdmission,
|
||||
resolveAgentChildWorkAliasRecords,
|
||||
updateExistingAgentChildWork,
|
||||
validateExistingAgentChildWork
|
||||
} from './agent-status-child-work-admission-core'
|
||||
import type {
|
||||
AgentChildWorkAdmissionResult,
|
||||
AgentChildWorkAdoptRequest,
|
||||
AgentChildWorkAnnounceRequest,
|
||||
AgentChildWorkReparentRequest,
|
||||
AgentChildWorkResumeRequest,
|
||||
AgentChildWorkStopRequest
|
||||
} from './agent-status-child-work-admission'
|
||||
import {
|
||||
parseAgentChildWorkInput,
|
||||
parseAgentChildWorkInvocationFence
|
||||
} from './agent-status-child-work-codec'
|
||||
import type { AgentStatusStore } from './agent-status-store'
|
||||
import { agentStatusSubjectsEqual, parseAgentStatusSubject } from './agent-status-subject'
|
||||
|
||||
export function announceAgentChildWork(
|
||||
store: AgentStatusStore,
|
||||
mintChildWorkId: () => AgentChildWorkId,
|
||||
request: AgentChildWorkAnnounceRequest
|
||||
): AgentChildWorkAdmissionResult {
|
||||
const parent = parseAgentStatusSubject(request.parent)
|
||||
const fence = parseAgentChildWorkInvocationFence(request.fence)
|
||||
if (!parent || !fence || !store.getParent(parent)) {
|
||||
return rejectAgentChildWorkAdmission('invalid')
|
||||
}
|
||||
const lookupAliases = buildAgentChildWorkAliases(
|
||||
parent,
|
||||
request.provider,
|
||||
request.kind,
|
||||
request.aliases,
|
||||
'unresolved-child',
|
||||
fence
|
||||
)
|
||||
if (!lookupAliases) {
|
||||
return rejectAgentChildWorkAdmission('invalid')
|
||||
}
|
||||
const bindings = resolveAgentChildWorkAliasRecords(store, lookupAliases)
|
||||
const exact = bindings.filter((binding) => agentChildWorkFencesEqual(binding.fence, fence))
|
||||
const exactIds = new Set(exact.map((binding) => binding.childWorkId))
|
||||
if (request.lifetime === 'proven-new') {
|
||||
if (exact.length > 0) {
|
||||
return rejectAgentChildWorkAdmission('id-collision')
|
||||
}
|
||||
const candidateId = mintChildWorkId()
|
||||
const aliases = buildAgentChildWorkAliases(
|
||||
parent,
|
||||
request.provider,
|
||||
request.kind,
|
||||
request.aliases,
|
||||
candidateId,
|
||||
fence
|
||||
)
|
||||
if (!aliases || findAgentChildWork(store, candidateId)) {
|
||||
return rejectAgentChildWorkAdmission(aliases ? 'id-collision' : 'invalid')
|
||||
}
|
||||
const child = buildAgentChildWork(request, candidateId, request.observedAt, fence)
|
||||
return child
|
||||
? commitAgentChildWork(store, child, aliases, true)
|
||||
: rejectAgentChildWorkAdmission('invalid')
|
||||
}
|
||||
if (bindings.length !== exact.length || exactIds.size > 1) {
|
||||
return rejectAgentChildWorkAdmission(exactIds.size > 1 ? 'ambiguous' : 'stale-invocation')
|
||||
}
|
||||
const existingId = exact[0]?.childWorkId
|
||||
if (existingId) {
|
||||
const child = findAgentChildWork(store, existingId)
|
||||
if (!child) {
|
||||
return rejectAgentChildWorkAdmission('ambiguous')
|
||||
}
|
||||
const aliases = buildAgentChildWorkAliases(
|
||||
parent,
|
||||
request.provider,
|
||||
request.kind,
|
||||
request.aliases,
|
||||
child.childWorkId,
|
||||
fence
|
||||
)
|
||||
return aliases
|
||||
? updateExistingAgentChildWork(store, request, child, aliases)
|
||||
: rejectAgentChildWorkAdmission('invalid')
|
||||
}
|
||||
const candidateId = mintChildWorkId()
|
||||
if (findAgentChildWork(store, candidateId)) {
|
||||
return rejectAgentChildWorkAdmission('id-collision')
|
||||
}
|
||||
const aliases = buildAgentChildWorkAliases(
|
||||
parent,
|
||||
request.provider,
|
||||
request.kind,
|
||||
request.aliases,
|
||||
candidateId,
|
||||
fence
|
||||
)
|
||||
const child = buildAgentChildWork(request, candidateId, request.observedAt, fence)
|
||||
return aliases && child
|
||||
? commitAgentChildWork(store, child, aliases, true)
|
||||
: rejectAgentChildWorkAdmission('invalid')
|
||||
}
|
||||
|
||||
export function adoptAgentChildWork(
|
||||
store: AgentStatusStore,
|
||||
request: AgentChildWorkAdoptRequest
|
||||
): AgentChildWorkAdmissionResult {
|
||||
const child = findAgentChildWork(store, request.childWorkId)
|
||||
const invalid = validateExistingAgentChildWork(
|
||||
child,
|
||||
request.parent,
|
||||
request.provider,
|
||||
request.expectedFence
|
||||
)
|
||||
if (invalid || !child) {
|
||||
return invalid ?? rejectAgentChildWorkAdmission('unknown-child')
|
||||
}
|
||||
const aliases = buildAgentChildWorkAliases(
|
||||
request.parent,
|
||||
request.provider,
|
||||
request.kind,
|
||||
request.aliases,
|
||||
child.childWorkId,
|
||||
child.invocation
|
||||
)
|
||||
if (!aliases) {
|
||||
return rejectAgentChildWorkAdmission('invalid')
|
||||
}
|
||||
const collisions = resolveAgentChildWorkAliasRecords(store, aliases).filter(
|
||||
(binding) => binding.childWorkId !== child.childWorkId
|
||||
)
|
||||
if (collisions.length > 0) {
|
||||
return rejectAgentChildWorkAdmission('ambiguous')
|
||||
}
|
||||
const oldAliases = agentChildWorkAliasesForChild(store, child.childWorkId)
|
||||
const removeAliases = oldAliases
|
||||
.filter((alias) => alias.kind !== request.kind)
|
||||
.map(serializeAgentChildWorkAliasKey)
|
||||
const reclassified = oldAliases.map((alias) => ({
|
||||
parent: alias.parent,
|
||||
provider: alias.provider,
|
||||
segmentId: alias.segmentId,
|
||||
kind: request.kind,
|
||||
aliasKind: alias.aliasKind,
|
||||
alias: alias.alias,
|
||||
childWorkId: alias.childWorkId,
|
||||
fence: alias.fence
|
||||
}))
|
||||
const unique = new Map(
|
||||
[...reclassified, ...aliases].map((alias) => [serializeAgentChildWorkAliasKey(alias), alias])
|
||||
)
|
||||
const reclassifiedCollisions = resolveAgentChildWorkAliasRecords(store, [
|
||||
...unique.values()
|
||||
]).filter((binding) => binding.childWorkId !== child.childWorkId)
|
||||
if (reclassifiedCollisions.length > 0) {
|
||||
return rejectAgentChildWorkAdmission('ambiguous')
|
||||
}
|
||||
return updateExistingAgentChildWork(store, request, child, [...unique.values()], removeAliases)
|
||||
}
|
||||
|
||||
export function resumeAgentChildWork(
|
||||
store: AgentStatusStore,
|
||||
request: AgentChildWorkResumeRequest
|
||||
): AgentChildWorkAdmissionResult {
|
||||
const child = findAgentChildWork(store, request.childWorkId)
|
||||
const invalid = validateExistingAgentChildWork(
|
||||
child,
|
||||
request.parent,
|
||||
request.provider,
|
||||
request.expectedFence
|
||||
)
|
||||
const nextFence = parseAgentChildWorkInvocationFence(request.nextFence)
|
||||
if (invalid || !child) {
|
||||
return invalid ?? rejectAgentChildWorkAdmission('unknown-child')
|
||||
}
|
||||
if (!nextFence || agentChildWorkFencesEqual(child.invocation, nextFence)) {
|
||||
return rejectAgentChildWorkAdmission('invalid')
|
||||
}
|
||||
const aliases = buildAgentChildWorkAliases(
|
||||
request.parent,
|
||||
request.provider,
|
||||
request.kind,
|
||||
request.aliases,
|
||||
child.childWorkId,
|
||||
nextFence
|
||||
)
|
||||
if (!aliases) {
|
||||
return rejectAgentChildWorkAdmission('invalid')
|
||||
}
|
||||
const collisions = resolveAgentChildWorkAliasRecords(store, aliases).filter(
|
||||
(binding) => binding.childWorkId !== child.childWorkId
|
||||
)
|
||||
if (collisions.length > 0) {
|
||||
return rejectAgentChildWorkAdmission('ambiguous')
|
||||
}
|
||||
const previousInvocations = [
|
||||
...(child.previousInvocations ?? []),
|
||||
{
|
||||
fence: child.invocation,
|
||||
...(child.outcome !== undefined ? { outcome: child.outcome } : {}),
|
||||
...(child.membership === 'settled' ? { settledAt: child.observedAt } : {})
|
||||
}
|
||||
].slice(-AGENT_CHILD_WORK_INVOCATION_HISTORY_MAX)
|
||||
const resumed = buildAgentChildWork(
|
||||
request,
|
||||
child.childWorkId,
|
||||
child.firstObservedAt,
|
||||
nextFence,
|
||||
previousInvocations
|
||||
)
|
||||
return resumed
|
||||
? commitAgentChildWork(store, resumed, aliases, false)
|
||||
: rejectAgentChildWorkAdmission('invalid')
|
||||
}
|
||||
|
||||
export function reparentAgentChildWork(
|
||||
store: AgentStatusStore,
|
||||
request: AgentChildWorkReparentRequest
|
||||
): AgentChildWorkAdmissionResult {
|
||||
const child = findAgentChildWork(store, request.childWorkId)
|
||||
if (
|
||||
!child ||
|
||||
!agentStatusSubjectsEqual(child.parent, request.fromParent) ||
|
||||
!agentChildWorkFencesEqual(child.invocation, request.expectedFence) ||
|
||||
!store.getParent(request.toParent) ||
|
||||
request.observedAt < child.observedAt
|
||||
) {
|
||||
return rejectAgentChildWorkAdmission(child ? 'stale-invocation' : 'unknown-child')
|
||||
}
|
||||
const oldAliases = agentChildWorkAliasesForChild(store, child.childWorkId)
|
||||
const aliases = oldAliases.map((alias) => ({
|
||||
parent: request.toParent,
|
||||
provider: alias.provider,
|
||||
segmentId: alias.segmentId,
|
||||
kind: alias.kind,
|
||||
aliasKind: alias.aliasKind,
|
||||
alias: alias.alias,
|
||||
childWorkId: alias.childWorkId,
|
||||
fence: alias.fence
|
||||
}))
|
||||
const collisions = resolveAgentChildWorkAliasRecords(store, aliases).filter(
|
||||
(binding) => binding.childWorkId !== child.childWorkId
|
||||
)
|
||||
if (collisions.length > 0) {
|
||||
return rejectAgentChildWorkAdmission('ambiguous')
|
||||
}
|
||||
const { revision: _revision, ...childInput } = child
|
||||
const moved = parseAgentChildWorkInput({
|
||||
...childInput,
|
||||
parent: request.toParent,
|
||||
observedAt: request.observedAt
|
||||
})
|
||||
return moved
|
||||
? commitAgentChildWork(
|
||||
store,
|
||||
moved,
|
||||
aliases,
|
||||
false,
|
||||
oldAliases.map(serializeAgentChildWorkAliasKey)
|
||||
)
|
||||
: rejectAgentChildWorkAdmission('invalid')
|
||||
}
|
||||
|
||||
export function authorizeAgentChildWorkStop(
|
||||
store: AgentStatusStore,
|
||||
request: AgentChildWorkStopRequest
|
||||
): AgentChildWorkRecord | null {
|
||||
const child = findAgentChildWork(store, request.childWorkId)
|
||||
if (
|
||||
!child ||
|
||||
!agentStatusSubjectsEqual(child.parent, request.parent) ||
|
||||
!agentChildWorkFencesEqual(child.invocation, request.expectedFence) ||
|
||||
child.membership !== 'live' ||
|
||||
child.stoppable !== true
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return child
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
createAgentChildWorkAdmission,
|
||||
type AgentChildWorkAnnounceRequest
|
||||
} from './agent-status-child-work-admission'
|
||||
import { serializeAgentChildWorkAliasKey } from './agent-status-child-work-alias'
|
||||
import { createAgentStatusStore } from './agent-status-store'
|
||||
import {
|
||||
makeStructuredAgentStatusSubject,
|
||||
type AgentStatusExecutionScope,
|
||||
type AgentStatusSubject
|
||||
} from './agent-status-subject'
|
||||
|
||||
const SESSION_ID = 'session_11111111-1111-4111-8111-111111111111'
|
||||
|
||||
function subject(overrides: Partial<AgentStatusExecutionScope> = {}): AgentStatusSubject {
|
||||
return makeStructuredAgentStatusSubject(
|
||||
{
|
||||
executionHostId: 'local',
|
||||
wslDistro: null,
|
||||
workspaceId: 'workspace-1',
|
||||
workspaceKind: 'git-worktree',
|
||||
...overrides
|
||||
},
|
||||
SESSION_ID
|
||||
)
|
||||
}
|
||||
|
||||
function announce(
|
||||
parent: AgentStatusSubject,
|
||||
overrides: Partial<AgentChildWorkAnnounceRequest> = {}
|
||||
): AgentChildWorkAnnounceRequest {
|
||||
return {
|
||||
parent,
|
||||
provider: 'claude',
|
||||
aliases: [{ segmentId: 'segment-1', aliasKind: 'task_id', alias: 'task-1' }],
|
||||
fence: { invocationId: 'invocation-1', generation: 1 },
|
||||
lifetime: 'current',
|
||||
kind: 'agent',
|
||||
state: 'working',
|
||||
membership: 'live',
|
||||
observedAt: 10,
|
||||
stoppable: true,
|
||||
provenance: { source: 'structured-session', producerId: 'journal-1' },
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function setup(parents: AgentStatusSubject[] = [subject()]) {
|
||||
const ids = ['child-1', 'child-2', 'child-3', 'child-4']
|
||||
const mintChildWorkId = vi.fn(() => ids.shift() ?? 'child-overflow')
|
||||
const store = createAgentStatusStore({ epoch: 'epoch-a', mode: 'authority' })
|
||||
for (const parent of parents) {
|
||||
expect(store.applyMutation({ parent: { subject: parent } })).not.toBeNull()
|
||||
}
|
||||
return {
|
||||
store,
|
||||
mintChildWorkId,
|
||||
admission: createAgentChildWorkAdmission(store, { mintChildWorkId })
|
||||
}
|
||||
}
|
||||
|
||||
describe('agent child-work admission', () => {
|
||||
it('keeps one host id across re-announcement with another tool-use alias', () => {
|
||||
const parent = subject()
|
||||
const { admission, mintChildWorkId, store } = setup([parent])
|
||||
|
||||
expect(admission.announce(announce(parent))).toMatchObject({
|
||||
accepted: true,
|
||||
childWorkId: 'child-1',
|
||||
created: true
|
||||
})
|
||||
expect(
|
||||
admission.announce(
|
||||
announce(parent, {
|
||||
aliases: [
|
||||
{ segmentId: 'segment-1', aliasKind: 'task_id', alias: 'task-1' },
|
||||
{ segmentId: 'segment-1', aliasKind: 'tool_use_id', alias: 'tool-2' }
|
||||
],
|
||||
observedAt: 11
|
||||
})
|
||||
)
|
||||
).toMatchObject({ accepted: true, childWorkId: 'child-1', created: false })
|
||||
expect(mintChildWorkId).toHaveBeenCalledTimes(1)
|
||||
expect(store.getChildren(parent)).toHaveLength(1)
|
||||
expect(store.getSnapshot().aliases).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('adopts and reclassifies a provisional child without changing its id', () => {
|
||||
const parent = subject()
|
||||
const { admission, store } = setup([parent])
|
||||
const first = admission.announce(
|
||||
announce(parent, {
|
||||
kind: 'unknown',
|
||||
aliases: [{ segmentId: 'segment-1', aliasKind: 'tool_use_id', alias: 'tool-1' }]
|
||||
})
|
||||
)
|
||||
expect(first).toMatchObject({ accepted: true, childWorkId: 'child-1' })
|
||||
|
||||
expect(
|
||||
admission.adopt({
|
||||
...announce(parent, { kind: 'agent', observedAt: 12 }),
|
||||
childWorkId: 'child-1',
|
||||
expectedFence: { invocationId: 'invocation-1', generation: 1 },
|
||||
aliases: [{ segmentId: 'segment-1', aliasKind: 'task_id', alias: 'task-1' }]
|
||||
})
|
||||
).toMatchObject({ accepted: true, childWorkId: 'child-1', created: false })
|
||||
expect(store.getChildren(parent)).toMatchObject([{ childWorkId: 'child-1', kind: 'agent' }])
|
||||
expect(store.getSnapshot().aliases.every((alias) => alias.kind === 'agent')).toBe(true)
|
||||
})
|
||||
|
||||
it('preserves the logical id and prior outcome across an explicit resume fence', () => {
|
||||
const parent = subject()
|
||||
const { admission, store } = setup([parent])
|
||||
admission.announce(
|
||||
announce(parent, {
|
||||
state: 'done',
|
||||
membership: 'settled',
|
||||
outcome: 'failed',
|
||||
observedAt: 20
|
||||
})
|
||||
)
|
||||
|
||||
expect(
|
||||
admission.resume({
|
||||
...announce(parent, {
|
||||
aliases: [{ segmentId: 'segment-2', aliasKind: 'task_id', alias: 'task-1' }],
|
||||
observedAt: 30
|
||||
}),
|
||||
childWorkId: 'child-1',
|
||||
expectedFence: { invocationId: 'invocation-1', generation: 1 },
|
||||
nextFence: { invocationId: 'invocation-2', generation: 2 }
|
||||
})
|
||||
).toMatchObject({ accepted: true, childWorkId: 'child-1', created: false })
|
||||
|
||||
expect(store.getChildren(parent)[0]).toMatchObject({
|
||||
childWorkId: 'child-1',
|
||||
firstObservedAt: 20,
|
||||
invocation: { invocationId: 'invocation-2', generation: 2 },
|
||||
previousInvocations: [
|
||||
{
|
||||
fence: { invocationId: 'invocation-1', generation: 1 },
|
||||
outcome: 'failed',
|
||||
settledAt: 20
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it('mints a distinct id for proven reuse and rejects delayed predecessor updates and stops', () => {
|
||||
const parent = subject()
|
||||
const { admission, store } = setup([parent])
|
||||
admission.announce(
|
||||
announce(parent, {
|
||||
state: 'done',
|
||||
membership: 'settled',
|
||||
outcome: 'succeeded',
|
||||
observedAt: 20
|
||||
})
|
||||
)
|
||||
const successor = announce(parent, {
|
||||
fence: { invocationId: 'invocation-2', generation: 2 },
|
||||
lifetime: 'proven-new',
|
||||
observedAt: 30
|
||||
})
|
||||
expect(admission.announce(successor)).toMatchObject({
|
||||
accepted: true,
|
||||
childWorkId: 'child-2',
|
||||
created: true
|
||||
})
|
||||
expect(store.getChildren(parent).map((child) => child.childWorkId)).toEqual([
|
||||
'child-1',
|
||||
'child-2'
|
||||
])
|
||||
|
||||
expect(admission.announce(announce(parent, { observedAt: 40 }))).toEqual({
|
||||
accepted: false,
|
||||
reason: 'stale-invocation'
|
||||
})
|
||||
expect(
|
||||
admission.authorizeStop({
|
||||
parent,
|
||||
childWorkId: 'child-2',
|
||||
expectedFence: { invocationId: 'invocation-1', generation: 1 }
|
||||
})
|
||||
).toBeNull()
|
||||
expect(
|
||||
admission.authorizeStop({
|
||||
parent,
|
||||
childWorkId: 'child-2',
|
||||
expectedFence: { invocationId: 'invocation-2', generation: 2 }
|
||||
})?.childWorkId
|
||||
).toBe('child-2')
|
||||
})
|
||||
|
||||
it('fails closed for settled or non-stoppable stop targets', () => {
|
||||
const parent = subject()
|
||||
const { admission } = setup([parent])
|
||||
admission.announce(announce(parent, { stoppable: false }))
|
||||
const stop = {
|
||||
parent,
|
||||
childWorkId: 'child-1',
|
||||
expectedFence: { invocationId: 'invocation-1', generation: 1 }
|
||||
}
|
||||
expect(admission.authorizeStop(stop)).toBeNull()
|
||||
|
||||
const second = setup([parent])
|
||||
second.admission.announce(
|
||||
announce(parent, { state: 'done', membership: 'settled', outcome: 'succeeded' })
|
||||
)
|
||||
expect(second.admission.authorizeStop(stop)).toBeNull()
|
||||
})
|
||||
|
||||
it('scopes identical aliases by parent, provider, segment and kind', () => {
|
||||
const firstParent = subject()
|
||||
const secondParent = subject({ executionHostId: 'ssh:host-a' })
|
||||
const { admission, store } = setup([firstParent, secondParent])
|
||||
|
||||
const requests = [
|
||||
announce(firstParent),
|
||||
announce(secondParent),
|
||||
announce(firstParent, { provider: 'codex' }),
|
||||
announce(firstParent, {
|
||||
aliases: [{ segmentId: 'segment-2', aliasKind: 'task_id', alias: 'task-1' }]
|
||||
}),
|
||||
announce(firstParent, { kind: 'workflow' })
|
||||
]
|
||||
for (const request of requests) {
|
||||
expect(admission.announce(request).accepted).toBe(true)
|
||||
}
|
||||
|
||||
expect(store.getSnapshot().children).toHaveLength(5)
|
||||
expect(new Set(store.getSnapshot().aliases.map(serializeAgentChildWorkAliasKey)).size).toBe(5)
|
||||
})
|
||||
|
||||
it('reparents a child and all aliases without reminting identity', () => {
|
||||
const firstParent = subject()
|
||||
const nextParent = subject({ workspaceId: 'workspace-2' })
|
||||
const { admission, store, mintChildWorkId } = setup([firstParent, nextParent])
|
||||
admission.announce(announce(firstParent))
|
||||
|
||||
expect(
|
||||
admission.reparent({
|
||||
childWorkId: 'child-1',
|
||||
fromParent: firstParent,
|
||||
toParent: nextParent,
|
||||
expectedFence: { invocationId: 'invocation-1', generation: 1 },
|
||||
observedAt: 20
|
||||
})
|
||||
).toMatchObject({ accepted: true, childWorkId: 'child-1', created: false })
|
||||
expect(store.getChildren(firstParent)).toEqual([])
|
||||
expect(store.getChildren(nextParent)[0]?.childWorkId).toBe('child-1')
|
||||
expect(store.getSnapshot().aliases[0]?.parent).toEqual(nextParent)
|
||||
expect(mintChildWorkId).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,116 @@
|
||||
import type { AgentChildWorkAliasKind } from './agent-status-child-work-alias'
|
||||
import type {
|
||||
AgentChildWorkId,
|
||||
AgentChildWorkInvocationFence,
|
||||
AgentChildWorkKind,
|
||||
AgentChildWorkMembership,
|
||||
AgentChildWorkOutcome,
|
||||
AgentChildWorkProviderTiming,
|
||||
AgentChildWorkProvenance,
|
||||
AgentChildWorkRecord,
|
||||
AgentChildWorkState
|
||||
} from './agent-status-child-work'
|
||||
import {
|
||||
adoptAgentChildWork,
|
||||
announceAgentChildWork,
|
||||
authorizeAgentChildWorkStop,
|
||||
reparentAgentChildWork,
|
||||
resumeAgentChildWork
|
||||
} from './agent-status-child-work-admission-operations'
|
||||
import type { AgentStatusStore } from './agent-status-store'
|
||||
import type { AgentStatusSubject } from './agent-status-subject'
|
||||
|
||||
export type AgentChildWorkObservationAlias = {
|
||||
segmentId: string
|
||||
aliasKind: AgentChildWorkAliasKind
|
||||
alias: string
|
||||
}
|
||||
|
||||
export type AgentChildWorkObservationFields = {
|
||||
kind: AgentChildWorkKind
|
||||
state: AgentChildWorkState
|
||||
membership: AgentChildWorkMembership
|
||||
outcome?: AgentChildWorkOutcome
|
||||
name?: string
|
||||
description?: string
|
||||
agentType?: string
|
||||
model?: string
|
||||
totalTokens?: number
|
||||
providerTiming?: AgentChildWorkProviderTiming
|
||||
observedAt: number
|
||||
stoppable: boolean
|
||||
provenance: AgentChildWorkProvenance
|
||||
}
|
||||
|
||||
export type AgentChildWorkAnnounceRequest = AgentChildWorkObservationFields & {
|
||||
parent: AgentStatusSubject
|
||||
provider: string
|
||||
aliases: AgentChildWorkObservationAlias[]
|
||||
fence: AgentChildWorkInvocationFence
|
||||
lifetime: 'current' | 'proven-new'
|
||||
}
|
||||
|
||||
export type AgentChildWorkAdoptRequest = AgentChildWorkObservationFields & {
|
||||
parent: AgentStatusSubject
|
||||
provider: string
|
||||
childWorkId: AgentChildWorkId
|
||||
expectedFence: AgentChildWorkInvocationFence
|
||||
aliases: AgentChildWorkObservationAlias[]
|
||||
}
|
||||
|
||||
export type AgentChildWorkResumeRequest = AgentChildWorkObservationFields & {
|
||||
parent: AgentStatusSubject
|
||||
provider: string
|
||||
childWorkId: AgentChildWorkId
|
||||
expectedFence: AgentChildWorkInvocationFence
|
||||
nextFence: AgentChildWorkInvocationFence
|
||||
aliases: AgentChildWorkObservationAlias[]
|
||||
}
|
||||
|
||||
export type AgentChildWorkReparentRequest = {
|
||||
childWorkId: AgentChildWorkId
|
||||
fromParent: AgentStatusSubject
|
||||
toParent: AgentStatusSubject
|
||||
expectedFence: AgentChildWorkInvocationFence
|
||||
observedAt: number
|
||||
}
|
||||
|
||||
export type AgentChildWorkStopRequest = {
|
||||
parent: AgentStatusSubject
|
||||
childWorkId: AgentChildWorkId
|
||||
expectedFence: AgentChildWorkInvocationFence
|
||||
}
|
||||
|
||||
export type AgentChildWorkAdmissionResult =
|
||||
| { accepted: true; childWorkId: AgentChildWorkId; revision: number; created: boolean }
|
||||
| {
|
||||
accepted: false
|
||||
reason:
|
||||
| 'invalid'
|
||||
| 'ambiguous'
|
||||
| 'stale-invocation'
|
||||
| 'unknown-child'
|
||||
| 'id-collision'
|
||||
| 'store-rejected'
|
||||
}
|
||||
|
||||
export type AgentChildWorkAdmission = {
|
||||
announce(request: AgentChildWorkAnnounceRequest): AgentChildWorkAdmissionResult
|
||||
adopt(request: AgentChildWorkAdoptRequest): AgentChildWorkAdmissionResult
|
||||
resume(request: AgentChildWorkResumeRequest): AgentChildWorkAdmissionResult
|
||||
reparent(request: AgentChildWorkReparentRequest): AgentChildWorkAdmissionResult
|
||||
authorizeStop(request: AgentChildWorkStopRequest): AgentChildWorkRecord | null
|
||||
}
|
||||
|
||||
export function createAgentChildWorkAdmission(
|
||||
store: AgentStatusStore,
|
||||
options: { mintChildWorkId: () => AgentChildWorkId }
|
||||
): AgentChildWorkAdmission {
|
||||
return {
|
||||
announce: (request) => announceAgentChildWork(store, options.mintChildWorkId, request),
|
||||
adopt: (request) => adoptAgentChildWork(store, request),
|
||||
resume: (request) => resumeAgentChildWork(store, request),
|
||||
reparent: (request) => reparentAgentChildWork(store, request),
|
||||
authorizeStop: (request) => authorizeAgentChildWorkStop(store, request)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
import {
|
||||
agentChildWorkFencesEqual,
|
||||
type AgentChildWorkId,
|
||||
type AgentChildWorkInvocationFence,
|
||||
type AgentChildWorkKind
|
||||
} from './agent-status-child-work'
|
||||
import { parseAgentChildWorkInvocationFence } from './agent-status-child-work-codec'
|
||||
import {
|
||||
deserializeAgentStatusSubject,
|
||||
parseAgentStatusSubject,
|
||||
serializeAgentStatusSubject,
|
||||
type AgentStatusSubject
|
||||
} from './agent-status-subject'
|
||||
|
||||
const CHILD_ALIAS_KEY_PREFIX = 'agent-child-work-alias-v1:'
|
||||
const MAX_ALIAS_PART_LENGTH = 512
|
||||
|
||||
export type AgentChildWorkAliasKind = 'task_id' | 'tool_use_id'
|
||||
|
||||
export type AgentChildWorkAliasIdentity = Pick<
|
||||
AgentChildWorkAliasInput,
|
||||
'parent' | 'provider' | 'segmentId' | 'kind' | 'aliasKind' | 'alias'
|
||||
>
|
||||
|
||||
export type AgentChildWorkAliasInput = {
|
||||
parent: AgentStatusSubject
|
||||
provider: string
|
||||
segmentId: string
|
||||
kind: AgentChildWorkKind
|
||||
aliasKind: AgentChildWorkAliasKind
|
||||
alias: string
|
||||
childWorkId: AgentChildWorkId
|
||||
fence: AgentChildWorkInvocationFence
|
||||
}
|
||||
|
||||
export type AgentChildWorkAliasRecord = AgentChildWorkAliasInput & {
|
||||
revision: number
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function hasOnlyKeys(
|
||||
record: Record<string, unknown>,
|
||||
required: readonly string[],
|
||||
optional: readonly string[] = []
|
||||
): boolean {
|
||||
const keys = Object.keys(record)
|
||||
return (
|
||||
required.every((key) => Object.hasOwn(record, key)) &&
|
||||
keys.every((key) => required.includes(key) || optional.includes(key))
|
||||
)
|
||||
}
|
||||
|
||||
function isBoundedString(value: unknown): value is string {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
value.length === 0 ||
|
||||
value.length > MAX_ALIAS_PART_LENGTH ||
|
||||
value !== value.trim()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const code = value.charCodeAt(index)
|
||||
if (code <= 0x1f || code === 0x7f) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function isRevision(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0
|
||||
}
|
||||
|
||||
function isKind(value: unknown): value is AgentChildWorkKind {
|
||||
return (
|
||||
value === 'agent' ||
|
||||
value === 'workflow' ||
|
||||
value === 'command' ||
|
||||
value === 'monitor' ||
|
||||
value === 'unknown'
|
||||
)
|
||||
}
|
||||
|
||||
export function parseAgentChildWorkAliasInput(value: unknown): AgentChildWorkAliasInput | null {
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
!hasOnlyKeys(value, [
|
||||
'parent',
|
||||
'provider',
|
||||
'segmentId',
|
||||
'kind',
|
||||
'aliasKind',
|
||||
'alias',
|
||||
'childWorkId',
|
||||
'fence'
|
||||
]) ||
|
||||
!isBoundedString(value.provider) ||
|
||||
!isBoundedString(value.segmentId) ||
|
||||
!isKind(value.kind) ||
|
||||
(value.aliasKind !== 'task_id' && value.aliasKind !== 'tool_use_id') ||
|
||||
!isBoundedString(value.alias) ||
|
||||
!isBoundedString(value.childWorkId)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const parent = parseAgentStatusSubject(value.parent)
|
||||
const fence = parseAgentChildWorkInvocationFence(value.fence)
|
||||
if (!parent || !fence) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
parent,
|
||||
provider: value.provider,
|
||||
segmentId: value.segmentId,
|
||||
kind: value.kind,
|
||||
aliasKind: value.aliasKind,
|
||||
alias: value.alias,
|
||||
childWorkId: value.childWorkId,
|
||||
fence
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAgentChildWorkAliasRecord(value: unknown): AgentChildWorkAliasRecord | null {
|
||||
if (!isRecord(value) || !isRevision(value.revision)) {
|
||||
return null
|
||||
}
|
||||
const input = { ...value }
|
||||
delete input.revision
|
||||
const parsed = parseAgentChildWorkAliasInput(input)
|
||||
return parsed ? { ...parsed, revision: value.revision } : null
|
||||
}
|
||||
|
||||
export function serializeAgentChildWorkAliasKey(alias: AgentChildWorkAliasIdentity): string {
|
||||
const parsed = parseAgentChildWorkAliasInput({
|
||||
parent: alias.parent,
|
||||
provider: alias.provider,
|
||||
segmentId: alias.segmentId,
|
||||
kind: alias.kind,
|
||||
aliasKind: alias.aliasKind,
|
||||
alias: alias.alias,
|
||||
childWorkId: 'key-only',
|
||||
fence: { invocationId: 'key-only', generation: 0 }
|
||||
})
|
||||
if (!parsed) {
|
||||
throw new Error('Invalid agent child-work alias')
|
||||
}
|
||||
return `${CHILD_ALIAS_KEY_PREFIX}${JSON.stringify([
|
||||
serializeAgentStatusSubject(parsed.parent),
|
||||
parsed.provider,
|
||||
parsed.segmentId,
|
||||
parsed.kind,
|
||||
parsed.aliasKind,
|
||||
parsed.alias
|
||||
])}`
|
||||
}
|
||||
|
||||
export function deserializeAgentChildWorkAliasKey(
|
||||
value: string
|
||||
): AgentChildWorkAliasIdentity | null {
|
||||
if (!value.startsWith(CHILD_ALIAS_KEY_PREFIX)) {
|
||||
return null
|
||||
}
|
||||
let tuple: unknown
|
||||
try {
|
||||
tuple = JSON.parse(value.slice(CHILD_ALIAS_KEY_PREFIX.length))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
if (!Array.isArray(tuple) || tuple.length !== 6) {
|
||||
return null
|
||||
}
|
||||
const [parentKey, provider, segmentId, kind, aliasKind, alias] = tuple
|
||||
if (typeof parentKey !== 'string') {
|
||||
return null
|
||||
}
|
||||
const parent = deserializeAgentStatusSubject(parentKey)
|
||||
const parsed = parseAgentChildWorkAliasInput({
|
||||
parent,
|
||||
provider,
|
||||
segmentId,
|
||||
kind,
|
||||
aliasKind,
|
||||
alias,
|
||||
childWorkId: 'key-only',
|
||||
fence: { invocationId: 'key-only', generation: 0 }
|
||||
})
|
||||
if (!parsed) {
|
||||
return null
|
||||
}
|
||||
const identity = {
|
||||
parent: parsed.parent,
|
||||
provider: parsed.provider,
|
||||
segmentId: parsed.segmentId,
|
||||
kind: parsed.kind,
|
||||
aliasKind: parsed.aliasKind,
|
||||
alias: parsed.alias
|
||||
}
|
||||
return serializeAgentChildWorkAliasKey(identity) === value ? identity : null
|
||||
}
|
||||
|
||||
export function agentChildWorkAliasesMatch(
|
||||
left: AgentChildWorkAliasInput,
|
||||
right: AgentChildWorkAliasInput
|
||||
): boolean {
|
||||
return (
|
||||
serializeAgentChildWorkAliasKey(left) === serializeAgentChildWorkAliasKey(right) &&
|
||||
left.childWorkId === right.childWorkId &&
|
||||
agentChildWorkFencesEqual(left.fence, right.fence)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
import {
|
||||
AGENT_CHILD_WORK_INVOCATION_HISTORY_MAX,
|
||||
AGENT_CHILD_WORK_KINDS,
|
||||
AGENT_CHILD_WORK_MEMBERSHIPS,
|
||||
AGENT_CHILD_WORK_OUTCOMES,
|
||||
AGENT_CHILD_WORK_STATES,
|
||||
agentChildWorkFencesEqual,
|
||||
type AgentChildWorkInput,
|
||||
type AgentChildWorkInvocationFence,
|
||||
type AgentChildWorkInvocationHistory,
|
||||
type AgentChildWorkKind,
|
||||
type AgentChildWorkMembership,
|
||||
type AgentChildWorkOutcome,
|
||||
type AgentChildWorkProviderTiming,
|
||||
type AgentChildWorkProvenance,
|
||||
type AgentChildWorkRecord,
|
||||
type AgentChildWorkState
|
||||
} from './agent-status-child-work'
|
||||
import { parseAgentStatusSubject } from './agent-status-subject'
|
||||
|
||||
const MAX_ID_LENGTH = 256
|
||||
const MAX_LABEL_LENGTH = 512
|
||||
const MAX_DESCRIPTION_LENGTH = 8_000
|
||||
const CHILD_WORK_KIND_SET: ReadonlySet<string> = new Set(AGENT_CHILD_WORK_KINDS)
|
||||
const CHILD_WORK_STATE_SET: ReadonlySet<string> = new Set(AGENT_CHILD_WORK_STATES)
|
||||
const CHILD_WORK_MEMBERSHIP_SET: ReadonlySet<string> = new Set(AGENT_CHILD_WORK_MEMBERSHIPS)
|
||||
const CHILD_WORK_OUTCOME_SET: ReadonlySet<string> = new Set(AGENT_CHILD_WORK_OUTCOMES)
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function hasOnlyKeys(
|
||||
record: Record<string, unknown>,
|
||||
required: readonly string[],
|
||||
optional: readonly string[] = []
|
||||
): boolean {
|
||||
const keys = Object.keys(record)
|
||||
return (
|
||||
required.every((key) => Object.hasOwn(record, key)) &&
|
||||
keys.every((key) => required.includes(key) || optional.includes(key))
|
||||
)
|
||||
}
|
||||
|
||||
function isBoundedString(value: unknown, maxLength = MAX_ID_LENGTH): value is string {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
value.length === 0 ||
|
||||
value.length > maxLength ||
|
||||
value !== value.trim()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const code = value.charCodeAt(index)
|
||||
if (code <= 0x1f || code === 0x7f) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function isTimestamp(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
||||
}
|
||||
|
||||
function isRevision(value: unknown): value is number {
|
||||
return Number.isSafeInteger(value) && typeof value === 'number' && value >= 0
|
||||
}
|
||||
|
||||
function isKind(value: unknown): value is AgentChildWorkKind {
|
||||
return typeof value === 'string' && CHILD_WORK_KIND_SET.has(value)
|
||||
}
|
||||
|
||||
function isState(value: unknown): value is AgentChildWorkState {
|
||||
return typeof value === 'string' && CHILD_WORK_STATE_SET.has(value)
|
||||
}
|
||||
|
||||
function isMembership(value: unknown): value is AgentChildWorkMembership {
|
||||
return typeof value === 'string' && CHILD_WORK_MEMBERSHIP_SET.has(value)
|
||||
}
|
||||
|
||||
function isOutcome(value: unknown): value is AgentChildWorkOutcome {
|
||||
return typeof value === 'string' && CHILD_WORK_OUTCOME_SET.has(value)
|
||||
}
|
||||
|
||||
export function parseAgentChildWorkInvocationFence(
|
||||
value: unknown
|
||||
): AgentChildWorkInvocationFence | null {
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
!hasOnlyKeys(value, ['invocationId', 'generation']) ||
|
||||
!isBoundedString(value.invocationId) ||
|
||||
!isRevision(value.generation)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return { invocationId: value.invocationId, generation: value.generation }
|
||||
}
|
||||
|
||||
function parseProviderTiming(value: unknown): AgentChildWorkProviderTiming | null {
|
||||
if (!isRecord(value) || !hasOnlyKeys(value, [], ['startedAt', 'completedAt'])) {
|
||||
return null
|
||||
}
|
||||
if (
|
||||
(value.startedAt !== undefined && !isTimestamp(value.startedAt)) ||
|
||||
(value.completedAt !== undefined && !isTimestamp(value.completedAt))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
...(isTimestamp(value.startedAt) ? { startedAt: value.startedAt } : {}),
|
||||
...(isTimestamp(value.completedAt) ? { completedAt: value.completedAt } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function parseProvenance(value: unknown): AgentChildWorkProvenance | null {
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
!hasOnlyKeys(value, ['source', 'producerId']) ||
|
||||
(value.source !== 'hook' &&
|
||||
value.source !== 'structured-session' &&
|
||||
value.source !== 'restore' &&
|
||||
value.source !== 'transport') ||
|
||||
!isBoundedString(value.producerId)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return { source: value.source, producerId: value.producerId }
|
||||
}
|
||||
|
||||
function parseInvocationHistory(value: unknown): AgentChildWorkInvocationHistory[] | null {
|
||||
if (!Array.isArray(value) || value.length > AGENT_CHILD_WORK_INVOCATION_HISTORY_MAX) {
|
||||
return null
|
||||
}
|
||||
const history: AgentChildWorkInvocationHistory[] = []
|
||||
for (const candidate of value) {
|
||||
if (
|
||||
!isRecord(candidate) ||
|
||||
!hasOnlyKeys(candidate, ['fence'], ['outcome', 'settledAt']) ||
|
||||
(candidate.outcome !== undefined && !isOutcome(candidate.outcome)) ||
|
||||
(candidate.settledAt !== undefined && !isTimestamp(candidate.settledAt))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const fence = parseAgentChildWorkInvocationFence(candidate.fence)
|
||||
if (!fence) {
|
||||
return null
|
||||
}
|
||||
history.push({
|
||||
fence,
|
||||
...(isOutcome(candidate.outcome) ? { outcome: candidate.outcome } : {}),
|
||||
...(isTimestamp(candidate.settledAt) ? { settledAt: candidate.settledAt } : {})
|
||||
})
|
||||
}
|
||||
return history
|
||||
}
|
||||
|
||||
function parseOptionalLabel(value: unknown, maxLength = MAX_LABEL_LENGTH): string | null {
|
||||
return value === undefined ? '' : isBoundedString(value, maxLength) ? value : null
|
||||
}
|
||||
|
||||
export function parseAgentChildWorkInput(value: unknown): AgentChildWorkInput | null {
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
!hasOnlyKeys(
|
||||
value,
|
||||
[
|
||||
'childWorkId',
|
||||
'parent',
|
||||
'provider',
|
||||
'kind',
|
||||
'state',
|
||||
'membership',
|
||||
'firstObservedAt',
|
||||
'observedAt',
|
||||
'stoppable',
|
||||
'invocation',
|
||||
'provenance'
|
||||
],
|
||||
[
|
||||
'outcome',
|
||||
'name',
|
||||
'description',
|
||||
'agentType',
|
||||
'model',
|
||||
'totalTokens',
|
||||
'providerTiming',
|
||||
'previousInvocations'
|
||||
]
|
||||
) ||
|
||||
!isBoundedString(value.childWorkId) ||
|
||||
!isBoundedString(value.provider) ||
|
||||
!isKind(value.kind) ||
|
||||
!isState(value.state) ||
|
||||
!isMembership(value.membership) ||
|
||||
(value.outcome !== undefined && !isOutcome(value.outcome)) ||
|
||||
(value.outcome !== undefined && value.membership !== 'settled') ||
|
||||
!isTimestamp(value.firstObservedAt) ||
|
||||
!isTimestamp(value.observedAt) ||
|
||||
value.firstObservedAt > value.observedAt ||
|
||||
typeof value.stoppable !== 'boolean' ||
|
||||
(value.totalTokens !== undefined &&
|
||||
(typeof value.totalTokens !== 'number' ||
|
||||
!Number.isSafeInteger(value.totalTokens) ||
|
||||
value.totalTokens < 0))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const parent = parseAgentStatusSubject(value.parent)
|
||||
const invocation = parseAgentChildWorkInvocationFence(value.invocation)
|
||||
const provenance = parseProvenance(value.provenance)
|
||||
const timing =
|
||||
value.providerTiming === undefined ? undefined : parseProviderTiming(value.providerTiming)
|
||||
const history =
|
||||
value.previousInvocations === undefined
|
||||
? undefined
|
||||
: parseInvocationHistory(value.previousInvocations)
|
||||
const labels = {
|
||||
name: parseOptionalLabel(value.name),
|
||||
description: parseOptionalLabel(value.description, MAX_DESCRIPTION_LENGTH),
|
||||
agentType: parseOptionalLabel(value.agentType),
|
||||
model: parseOptionalLabel(value.model)
|
||||
}
|
||||
if (!parent || !invocation || !provenance || timing === null || history === null) {
|
||||
return null
|
||||
}
|
||||
if (Object.values(labels).includes(null)) {
|
||||
return null
|
||||
}
|
||||
const historyFenceKeys = history?.map(
|
||||
(entry) => `${entry.fence.invocationId}\0${entry.fence.generation}`
|
||||
)
|
||||
if (
|
||||
history &&
|
||||
historyFenceKeys &&
|
||||
(new Set(historyFenceKeys).size !== historyFenceKeys.length ||
|
||||
history.some((entry) => agentChildWorkFencesEqual(entry.fence, invocation)))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
childWorkId: value.childWorkId,
|
||||
parent,
|
||||
provider: value.provider,
|
||||
kind: value.kind,
|
||||
state: value.state,
|
||||
membership: value.membership,
|
||||
...(isOutcome(value.outcome) ? { outcome: value.outcome } : {}),
|
||||
...(labels.name ? { name: labels.name } : {}),
|
||||
...(labels.description ? { description: labels.description } : {}),
|
||||
...(labels.agentType ? { agentType: labels.agentType } : {}),
|
||||
...(labels.model ? { model: labels.model } : {}),
|
||||
...(typeof value.totalTokens === 'number' ? { totalTokens: value.totalTokens } : {}),
|
||||
...(timing ? { providerTiming: timing } : {}),
|
||||
firstObservedAt: value.firstObservedAt,
|
||||
observedAt: value.observedAt,
|
||||
stoppable: value.stoppable,
|
||||
invocation,
|
||||
...(history ? { previousInvocations: history } : {}),
|
||||
provenance
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAgentChildWorkRecord(value: unknown): AgentChildWorkRecord | null {
|
||||
if (!isRecord(value) || !isRevision(value.revision)) {
|
||||
return null
|
||||
}
|
||||
const input = { ...value }
|
||||
delete input.revision
|
||||
const parsed = parseAgentChildWorkInput(input)
|
||||
return parsed ? { ...parsed, revision: value.revision } : null
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { AgentChildWorkMembership, AgentChildWorkState } from './agent-status-child-work'
|
||||
|
||||
export type AgentChildWorkFreshnessInput = {
|
||||
state: AgentChildWorkState
|
||||
membership: AgentChildWorkMembership
|
||||
parentEvidenceFresh: boolean
|
||||
transportObservation: 'live' | 'unverifiable'
|
||||
}
|
||||
|
||||
/** One decay rule for CLI and structured children; freshness never rewrites settled history. */
|
||||
export function resolveAgentChildWorkFreshness(
|
||||
input: AgentChildWorkFreshnessInput
|
||||
): AgentChildWorkState {
|
||||
if (input.membership === 'settled') {
|
||||
return input.state
|
||||
}
|
||||
return input.parentEvidenceFresh && input.transportObservation === 'live'
|
||||
? input.state
|
||||
: 'unverifiable'
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { AGENT_STATUS_MAX_SUBAGENTS } from './agent-status-types'
|
||||
import { resolveAgentChildWorkFreshness } from './agent-status-child-work-freshness'
|
||||
import {
|
||||
projectAgentChildWorkLegacyBackgroundTasks,
|
||||
projectAgentChildWorkLegacySubagents,
|
||||
type AgentChildWorkLegacyProjectionCandidate
|
||||
} from './agent-status-child-work-projection'
|
||||
import type { AgentChildWorkState } from './agent-status-child-work'
|
||||
|
||||
function candidate(
|
||||
providerId: string,
|
||||
overrides: Partial<AgentChildWorkLegacyProjectionCandidate['child']> = {}
|
||||
): AgentChildWorkLegacyProjectionCandidate {
|
||||
return {
|
||||
providerId,
|
||||
child: {
|
||||
kind: 'agent',
|
||||
state: 'working',
|
||||
membership: 'live',
|
||||
firstObservedAt: 123,
|
||||
description: 'Investigate',
|
||||
agentType: 'researcher',
|
||||
model: 'model-a',
|
||||
stoppable: true,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('agent child-work legacy projection', () => {
|
||||
it('preserves the bridge state mapping and stable host first-observation time', () => {
|
||||
const states: (AgentChildWorkState | undefined)[] = [
|
||||
undefined,
|
||||
'working',
|
||||
'monitoring',
|
||||
'done',
|
||||
'idle',
|
||||
'waiting',
|
||||
'blocked',
|
||||
'unverifiable'
|
||||
]
|
||||
const projected = projectAgentChildWorkLegacySubagents(
|
||||
states.map((state, index) => candidate(`task-${index}`, { state }))
|
||||
)
|
||||
|
||||
expect(projected?.map((item) => item.state)).toEqual([
|
||||
'working',
|
||||
'working',
|
||||
'working',
|
||||
'idle',
|
||||
'idle',
|
||||
'waiting',
|
||||
'blocked',
|
||||
'unverifiable'
|
||||
])
|
||||
expect(projected?.every((item) => item.startedAt === 123)).toBe(true)
|
||||
})
|
||||
|
||||
it('admits only agent kind and trims bounded nonempty provider ids', () => {
|
||||
const projected = projectAgentChildWorkLegacySubagents([
|
||||
candidate(' valid-id '),
|
||||
candidate(''),
|
||||
candidate(' '.repeat(10)),
|
||||
candidate('x'.repeat(65)),
|
||||
candidate('workflow-id', { kind: 'workflow' }),
|
||||
candidate('command-id', { kind: 'command' }),
|
||||
candidate('monitor-id', { kind: 'monitor' }),
|
||||
candidate('unknown-id', { kind: 'unknown' })
|
||||
])
|
||||
|
||||
expect(projected).toEqual([
|
||||
{
|
||||
id: 'valid-id',
|
||||
state: 'working',
|
||||
startedAt: 123,
|
||||
agentType: 'researcher',
|
||||
model: 'model-a',
|
||||
description: 'Investigate'
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it.each([31, 32, 33])('caps after accepting %i valid rows in source order', (count) => {
|
||||
const interleaved = Array.from({ length: count }, (_, index) => [
|
||||
candidate('', { kind: 'agent' }),
|
||||
candidate(`task-${index}`)
|
||||
]).flat()
|
||||
const projected = projectAgentChildWorkLegacySubagents(interleaved)
|
||||
|
||||
expect(projected).toHaveLength(Math.min(count, AGENT_STATUS_MAX_SUBAGENTS))
|
||||
expect(projected?.at(-1)?.id).toBe(`task-${Math.min(count, AGENT_STATUS_MAX_SUBAGENTS) - 1}`)
|
||||
})
|
||||
|
||||
it('rejects missing host first-observation time instead of inventing zero', () => {
|
||||
expect(
|
||||
projectAgentChildWorkLegacySubagents([
|
||||
candidate('task-invalid', { firstObservedAt: Number.NaN }),
|
||||
candidate('task-valid', { firstObservedAt: 55 })
|
||||
])
|
||||
).toMatchObject([{ id: 'task-valid', startedAt: 55 }])
|
||||
})
|
||||
|
||||
it('projects every kind into separate live and settled background lists', () => {
|
||||
const projection = projectAgentChildWorkLegacyBackgroundTasks([
|
||||
candidate('agent-live', { kind: 'agent', totalTokens: 10 }),
|
||||
candidate('workflow-settled', {
|
||||
kind: 'workflow',
|
||||
state: 'done',
|
||||
membership: 'settled'
|
||||
}),
|
||||
candidate('command-live', { kind: 'command' }),
|
||||
candidate('monitor-live', { kind: 'monitor', state: 'monitoring' }),
|
||||
candidate('unknown-settled', { kind: 'unknown', state: 'idle', membership: 'settled' })
|
||||
])
|
||||
|
||||
expect(projection.tasks?.map((item) => item.kind)).toEqual(['agent', 'command', 'monitor'])
|
||||
expect(projection.settledTasks?.map((item) => item.kind)).toEqual(['workflow', 'unknown'])
|
||||
expect(projection.tasks?.[0]).toMatchObject({
|
||||
id: 'agent-live',
|
||||
startedAt: 123,
|
||||
totalTokens: 10,
|
||||
stoppable: true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveAgentChildWorkFreshness', () => {
|
||||
it.each([
|
||||
[true, 'live', 'working'],
|
||||
[false, 'live', 'unverifiable'],
|
||||
[true, 'unverifiable', 'unverifiable'],
|
||||
[false, 'unverifiable', 'unverifiable']
|
||||
] as const)(
|
||||
'maps parentFresh=%s transport=%s to %s for live work',
|
||||
(parentEvidenceFresh, transportObservation, expected) => {
|
||||
expect(
|
||||
resolveAgentChildWorkFreshness({
|
||||
state: 'working',
|
||||
membership: 'live',
|
||||
parentEvidenceFresh,
|
||||
transportObservation
|
||||
})
|
||||
).toBe(expected)
|
||||
}
|
||||
)
|
||||
|
||||
it('does not turn live idle into settled or rewrite settled history on contact loss', () => {
|
||||
expect(
|
||||
resolveAgentChildWorkFreshness({
|
||||
state: 'idle',
|
||||
membership: 'live',
|
||||
parentEvidenceFresh: false,
|
||||
transportObservation: 'live'
|
||||
})
|
||||
).toBe('unverifiable')
|
||||
expect(
|
||||
resolveAgentChildWorkFreshness({
|
||||
state: 'done',
|
||||
membership: 'settled',
|
||||
parentEvidenceFresh: false,
|
||||
transportObservation: 'unverifiable'
|
||||
})
|
||||
).toBe('done')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,146 @@
|
||||
import type { AgentSessionBackgroundTask } from './agent-session-background-task-wire'
|
||||
import { AGENT_STATUS_MAX_SUBAGENTS, type AgentSubagentSnapshot } from './agent-status-types'
|
||||
import type {
|
||||
AgentChildWorkKind,
|
||||
AgentChildWorkMembership,
|
||||
AgentChildWorkState
|
||||
} from './agent-status-child-work'
|
||||
|
||||
const LEGACY_PROVIDER_ID_MAX_LENGTH = 64
|
||||
const BACKGROUND_PROVIDER_ID_MAX_LENGTH = 512
|
||||
|
||||
export type AgentChildWorkLegacyProjectionCandidate = {
|
||||
providerId: string
|
||||
child: {
|
||||
kind: AgentChildWorkKind
|
||||
state?: AgentChildWorkState
|
||||
membership: AgentChildWorkMembership
|
||||
firstObservedAt: number
|
||||
name?: string
|
||||
description?: string
|
||||
agentType?: string
|
||||
model?: string
|
||||
totalTokens?: number
|
||||
stoppable: boolean
|
||||
}
|
||||
}
|
||||
|
||||
function legacyProviderId(value: unknown): string | null {
|
||||
if (typeof value !== 'string') {
|
||||
return null
|
||||
}
|
||||
const trimmed = value.trim()
|
||||
return trimmed.length > 0 && trimmed.length <= LEGACY_PROVIDER_ID_MAX_LENGTH ? trimmed : null
|
||||
}
|
||||
|
||||
function backgroundProviderId(value: unknown): string | null {
|
||||
if (typeof value !== 'string') {
|
||||
return null
|
||||
}
|
||||
const trimmed = value.trim()
|
||||
return trimmed.length > 0 && trimmed.length <= BACKGROUND_PROVIDER_ID_MAX_LENGTH ? trimmed : null
|
||||
}
|
||||
|
||||
function legacySubagentState(
|
||||
state: AgentChildWorkState | undefined
|
||||
): AgentSubagentSnapshot['state'] | null {
|
||||
if (state === undefined || state === 'working' || state === 'monitoring') {
|
||||
return 'working'
|
||||
}
|
||||
if (state === 'done' || state === 'idle') {
|
||||
return 'idle'
|
||||
}
|
||||
if (state === 'waiting' || state === 'blocked' || state === 'unverifiable') {
|
||||
return state
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export function projectAgentChildWorkLegacySubagents(
|
||||
candidates: readonly AgentChildWorkLegacyProjectionCandidate[]
|
||||
): AgentSubagentSnapshot[] | undefined {
|
||||
const projected: AgentSubagentSnapshot[] = []
|
||||
for (const candidate of candidates) {
|
||||
if (candidate.child.kind !== 'agent') {
|
||||
continue
|
||||
}
|
||||
const id = legacyProviderId(candidate.providerId)
|
||||
const state = legacySubagentState(candidate.child.state)
|
||||
if (
|
||||
!id ||
|
||||
!state ||
|
||||
!Number.isFinite(candidate.child.firstObservedAt) ||
|
||||
candidate.child.firstObservedAt < 0
|
||||
) {
|
||||
continue
|
||||
}
|
||||
projected.push({
|
||||
id,
|
||||
state,
|
||||
startedAt: candidate.child.firstObservedAt,
|
||||
...(candidate.child.agentType !== undefined ? { agentType: candidate.child.agentType } : {}),
|
||||
...(candidate.child.model !== undefined ? { model: candidate.child.model } : {}),
|
||||
...(candidate.child.description !== undefined
|
||||
? { description: candidate.child.description }
|
||||
: {})
|
||||
})
|
||||
if (projected.length === AGENT_STATUS_MAX_SUBAGENTS) {
|
||||
break
|
||||
}
|
||||
}
|
||||
return projected.length > 0 ? projected : undefined
|
||||
}
|
||||
|
||||
export type AgentChildWorkLegacyBackgroundProjection = {
|
||||
tasks?: AgentSessionBackgroundTask[]
|
||||
settledTasks?: AgentSessionBackgroundTask[]
|
||||
}
|
||||
|
||||
function projectBackgroundTask(
|
||||
candidate: AgentChildWorkLegacyProjectionCandidate
|
||||
): AgentSessionBackgroundTask | null {
|
||||
const id = backgroundProviderId(candidate.providerId)
|
||||
if (
|
||||
!id ||
|
||||
!Number.isFinite(candidate.child.firstObservedAt) ||
|
||||
candidate.child.firstObservedAt < 0
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
id,
|
||||
kind: candidate.child.kind,
|
||||
...(candidate.child.description !== undefined
|
||||
? { description: candidate.child.description }
|
||||
: {}),
|
||||
...(candidate.child.name !== undefined ? { name: candidate.child.name } : {}),
|
||||
...(candidate.child.state !== undefined ? { state: candidate.child.state } : {}),
|
||||
startedAt: candidate.child.firstObservedAt,
|
||||
...(candidate.child.totalTokens !== undefined
|
||||
? { totalTokens: candidate.child.totalTokens }
|
||||
: {}),
|
||||
stoppable: candidate.child.stoppable
|
||||
}
|
||||
}
|
||||
|
||||
export function projectAgentChildWorkLegacyBackgroundTasks(
|
||||
candidates: readonly AgentChildWorkLegacyProjectionCandidate[]
|
||||
): AgentChildWorkLegacyBackgroundProjection {
|
||||
const tasks: AgentSessionBackgroundTask[] = []
|
||||
const settledTasks: AgentSessionBackgroundTask[] = []
|
||||
for (const candidate of candidates) {
|
||||
const projected = projectBackgroundTask(candidate)
|
||||
if (!projected) {
|
||||
continue
|
||||
}
|
||||
if (candidate.child.membership === 'live') {
|
||||
tasks.push(projected)
|
||||
} else {
|
||||
settledTasks.push(projected)
|
||||
}
|
||||
}
|
||||
return {
|
||||
...(tasks.length > 0 ? { tasks } : {}),
|
||||
...(settledTasks.length > 0 ? { settledTasks } : {})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
AGENT_CHILD_WORK_KINDS,
|
||||
AGENT_CHILD_WORK_STATES,
|
||||
type AgentChildWorkInput
|
||||
} from './agent-status-child-work'
|
||||
import {
|
||||
parseAgentChildWorkInput,
|
||||
parseAgentChildWorkRecord
|
||||
} from './agent-status-child-work-codec'
|
||||
import { createAgentStatusStore } from './agent-status-store'
|
||||
import { makeStructuredAgentStatusSubject } from './agent-status-subject'
|
||||
|
||||
const parent = makeStructuredAgentStatusSubject(
|
||||
{
|
||||
executionHostId: 'local',
|
||||
wslDistro: null,
|
||||
workspaceId: 'folder-1',
|
||||
workspaceKind: 'folder'
|
||||
},
|
||||
'session_11111111-1111-4111-8111-111111111111'
|
||||
)
|
||||
|
||||
function child(overrides: Partial<AgentChildWorkInput> = {}): AgentChildWorkInput {
|
||||
return {
|
||||
childWorkId: 'child-1',
|
||||
parent,
|
||||
provider: 'claude',
|
||||
kind: 'agent',
|
||||
state: 'working',
|
||||
membership: 'live',
|
||||
name: 'Research',
|
||||
description: 'Investigate the contract',
|
||||
agentType: 'researcher',
|
||||
model: 'model-a',
|
||||
totalTokens: 42,
|
||||
providerTiming: { startedAt: 5, completedAt: 8 },
|
||||
firstObservedAt: 10,
|
||||
observedAt: 20,
|
||||
stoppable: true,
|
||||
invocation: { invocationId: 'invocation-1', generation: 1 },
|
||||
previousInvocations: [
|
||||
{
|
||||
fence: { invocationId: 'invocation-0', generation: 0 },
|
||||
outcome: 'cancelled',
|
||||
settledAt: 9
|
||||
}
|
||||
],
|
||||
provenance: { source: 'structured-session', producerId: 'journal-1' },
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('AgentChildWorkRecord', () => {
|
||||
it('round-trips the complete canonical vocabulary through the store snapshot', () => {
|
||||
const store = createAgentStatusStore({ epoch: 'epoch-a', mode: 'authority' })
|
||||
expect(store.applyMutation({ parent: { subject: parent } })).not.toBeNull()
|
||||
const children = AGENT_CHILD_WORK_STATES.map((state, index) =>
|
||||
child({
|
||||
childWorkId: `child-${index}`,
|
||||
kind: AGENT_CHILD_WORK_KINDS[index % AGENT_CHILD_WORK_KINDS.length],
|
||||
state,
|
||||
membership: state === 'done' ? 'settled' : 'live',
|
||||
...(state === 'done' ? { outcome: 'failed' } : {}),
|
||||
invocation: { invocationId: `invocation-${index}`, generation: index },
|
||||
previousInvocations: undefined
|
||||
})
|
||||
)
|
||||
|
||||
expect(store.applyMutation({ children })).not.toBeNull()
|
||||
const snapshot = store.getSnapshot()
|
||||
expect(snapshot.children.map((item) => item.kind)).toEqual([
|
||||
'agent',
|
||||
'workflow',
|
||||
'command',
|
||||
'monitor',
|
||||
'unknown',
|
||||
'agent',
|
||||
'workflow'
|
||||
])
|
||||
expect(snapshot.children.map((item) => item.state)).toEqual(AGENT_CHILD_WORK_STATES)
|
||||
expect(snapshot.children.find((item) => item.state === 'done')).toMatchObject({
|
||||
membership: 'settled',
|
||||
outcome: 'failed',
|
||||
totalTokens: 42,
|
||||
providerTiming: { startedAt: 5, completedAt: 8 },
|
||||
firstObservedAt: 10
|
||||
})
|
||||
})
|
||||
|
||||
it('parses a copied record with outcome, tokens, timing and bounded invocation history', () => {
|
||||
const parsed = parseAgentChildWorkRecord({ ...child(), revision: 7 })
|
||||
|
||||
expect(parsed).toEqual({ ...child(), revision: 7 })
|
||||
expect(parsed).not.toBe(child())
|
||||
expect(parsed?.parent).not.toBe(parent)
|
||||
})
|
||||
|
||||
it.each([
|
||||
child({ childWorkId: 'bad\nid' }),
|
||||
child({ provider: 'claude\0forged' }),
|
||||
child({ firstObservedAt: 21 }),
|
||||
child({ totalTokens: -1 }),
|
||||
child({ membership: 'live', outcome: 'succeeded' }),
|
||||
child({
|
||||
previousInvocations: [
|
||||
{ fence: { invocationId: 'invocation-1', generation: 1 }, outcome: 'failed' }
|
||||
]
|
||||
}),
|
||||
child({
|
||||
previousInvocations: [
|
||||
{ fence: { invocationId: 'old', generation: 1 } },
|
||||
{ fence: { invocationId: 'old', generation: 1 } }
|
||||
]
|
||||
})
|
||||
])('rejects malformed canonical child input %#', (value) => {
|
||||
expect(parseAgentChildWorkInput(value)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,88 @@
|
||||
import { agentStatusSubjectsEqual, type AgentStatusSubject } from './agent-status-subject'
|
||||
|
||||
export const AGENT_CHILD_WORK_KINDS = [
|
||||
'agent',
|
||||
'workflow',
|
||||
'command',
|
||||
'monitor',
|
||||
'unknown'
|
||||
] as const
|
||||
export const AGENT_CHILD_WORK_STATES = [
|
||||
'working',
|
||||
'monitoring',
|
||||
'waiting',
|
||||
'blocked',
|
||||
'done',
|
||||
'idle',
|
||||
'unverifiable'
|
||||
] as const
|
||||
export const AGENT_CHILD_WORK_MEMBERSHIPS = ['live', 'settled'] as const
|
||||
export const AGENT_CHILD_WORK_OUTCOMES = ['succeeded', 'failed', 'cancelled', 'unknown'] as const
|
||||
export const AGENT_CHILD_WORK_INVOCATION_HISTORY_MAX = 32
|
||||
|
||||
export type AgentChildWorkId = string
|
||||
export type AgentChildWorkKind = (typeof AGENT_CHILD_WORK_KINDS)[number]
|
||||
export type AgentChildWorkState = (typeof AGENT_CHILD_WORK_STATES)[number]
|
||||
export type AgentChildWorkMembership = (typeof AGENT_CHILD_WORK_MEMBERSHIPS)[number]
|
||||
export type AgentChildWorkOutcome = (typeof AGENT_CHILD_WORK_OUTCOMES)[number]
|
||||
|
||||
export type AgentChildWorkInvocationFence = {
|
||||
invocationId: string
|
||||
generation: number
|
||||
}
|
||||
|
||||
export type AgentChildWorkInvocationHistory = {
|
||||
fence: AgentChildWorkInvocationFence
|
||||
outcome?: AgentChildWorkOutcome
|
||||
settledAt?: number
|
||||
}
|
||||
|
||||
export type AgentChildWorkProviderTiming = {
|
||||
startedAt?: number
|
||||
completedAt?: number
|
||||
}
|
||||
|
||||
export type AgentChildWorkProvenance = {
|
||||
source: 'hook' | 'structured-session' | 'restore' | 'transport'
|
||||
producerId: string
|
||||
}
|
||||
|
||||
export type AgentChildWorkInput = {
|
||||
childWorkId: AgentChildWorkId
|
||||
parent: AgentStatusSubject
|
||||
provider: string
|
||||
kind: AgentChildWorkKind
|
||||
state: AgentChildWorkState
|
||||
membership: AgentChildWorkMembership
|
||||
outcome?: AgentChildWorkOutcome
|
||||
name?: string
|
||||
description?: string
|
||||
agentType?: string
|
||||
model?: string
|
||||
totalTokens?: number
|
||||
providerTiming?: AgentChildWorkProviderTiming
|
||||
firstObservedAt: number
|
||||
observedAt: number
|
||||
stoppable: boolean
|
||||
invocation: AgentChildWorkInvocationFence
|
||||
previousInvocations?: AgentChildWorkInvocationHistory[]
|
||||
provenance: AgentChildWorkProvenance
|
||||
}
|
||||
|
||||
export type AgentChildWorkRecord = AgentChildWorkInput & {
|
||||
revision: number
|
||||
}
|
||||
|
||||
export function agentChildWorkFencesEqual(
|
||||
left: AgentChildWorkInvocationFence,
|
||||
right: AgentChildWorkInvocationFence
|
||||
): boolean {
|
||||
return left.invocationId === right.invocationId && left.generation === right.generation
|
||||
}
|
||||
|
||||
export function agentChildWorkBelongsTo(
|
||||
child: Pick<AgentChildWorkInput, 'parent'>,
|
||||
parent: AgentStatusSubject
|
||||
): boolean {
|
||||
return agentStatusSubjectsEqual(child.parent, parent)
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import {
|
||||
parseAgentChildWorkAliasInput,
|
||||
parseAgentChildWorkAliasRecord
|
||||
} from './agent-status-child-work-alias'
|
||||
import {
|
||||
parseAgentChildWorkInput,
|
||||
parseAgentChildWorkRecord
|
||||
} from './agent-status-child-work-codec'
|
||||
import {
|
||||
AGENT_STATUS_STORE_LIMITS,
|
||||
AGENT_STATUS_STORE_SNAPSHOT_VERSION,
|
||||
type AgentStatusStoreMutation,
|
||||
type AgentStatusStoreSnapshot,
|
||||
type AgentStatusTombstoneEntity,
|
||||
type AgentStatusTombstoneInput,
|
||||
type AgentStatusTombstoneRecord
|
||||
} from './agent-status-store-contract'
|
||||
import {
|
||||
parseAgentStatusFactIdentity,
|
||||
parseAgentStatusFactInput,
|
||||
parseAgentStatusFactRecord
|
||||
} from './agent-status-store-fact-codec'
|
||||
import {
|
||||
parseAgentStatusParentInput,
|
||||
parseAgentStatusParentRecord
|
||||
} from './agent-status-store-parent'
|
||||
import { parseAgentStatusSubject } from './agent-status-subject'
|
||||
import { measureUtf8ByteLength } from './utf8-byte-limits'
|
||||
|
||||
const MAX_EPOCH_LENGTH = 256
|
||||
const MAX_TOMBSTONE_KEY_LENGTH = 4_096
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function hasOnlyKeys(
|
||||
record: Record<string, unknown>,
|
||||
required: readonly string[],
|
||||
optional: readonly string[] = []
|
||||
): boolean {
|
||||
const keys = Object.keys(record)
|
||||
return (
|
||||
required.every((key) => Object.hasOwn(record, key)) &&
|
||||
keys.every((key) => required.includes(key) || optional.includes(key))
|
||||
)
|
||||
}
|
||||
|
||||
function isRevision(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0
|
||||
}
|
||||
|
||||
function isWithinSerializedLimit(value: unknown): boolean {
|
||||
try {
|
||||
return !measureUtf8ByteLength(JSON.stringify(value), {
|
||||
stopAfterBytes: AGENT_STATUS_STORE_LIMITS.serializedBytes
|
||||
}).exceededLimit
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function isBoundedKey(value: unknown, maxLength: number): value is string {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
value.length === 0 ||
|
||||
value.length > maxLength ||
|
||||
value !== value.trim()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const code = value.charCodeAt(index)
|
||||
if (code <= 0x1f || code === 0x7f) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function isAgentStatusStoreEpoch(value: unknown): value is string {
|
||||
return isBoundedKey(value, MAX_EPOCH_LENGTH)
|
||||
}
|
||||
|
||||
function isTombstoneEntity(value: unknown): value is AgentStatusTombstoneEntity {
|
||||
return value === 'parent' || value === 'child' || value === 'alias' || value === 'fact'
|
||||
}
|
||||
|
||||
export function parseAgentStatusTombstoneInput(value: unknown): AgentStatusTombstoneInput | null {
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
!hasOnlyKeys(value, ['entity', 'key']) ||
|
||||
!isTombstoneEntity(value.entity) ||
|
||||
!isBoundedKey(value.key, MAX_TOMBSTONE_KEY_LENGTH)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return { entity: value.entity, key: value.key }
|
||||
}
|
||||
|
||||
export function parseAgentStatusTombstoneRecord(value: unknown): AgentStatusTombstoneRecord | null {
|
||||
if (!isRecord(value) || !isRevision(value.revision)) {
|
||||
return null
|
||||
}
|
||||
const input = { ...value }
|
||||
delete input.revision
|
||||
const tombstone = parseAgentStatusTombstoneInput(input)
|
||||
return tombstone ? { ...tombstone, revision: value.revision } : null
|
||||
}
|
||||
|
||||
function parseArray<T>(
|
||||
value: unknown,
|
||||
parser: (candidate: unknown) => T | null,
|
||||
maxLength: number
|
||||
): T[] | null {
|
||||
if (!Array.isArray(value) || value.length > maxLength) {
|
||||
return null
|
||||
}
|
||||
const parsed: T[] = []
|
||||
for (const candidate of value) {
|
||||
const item = parser(candidate)
|
||||
if (!item) {
|
||||
return null
|
||||
}
|
||||
parsed.push(item)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
function parseMutationArray<T>(
|
||||
value: unknown,
|
||||
parser: (candidate: unknown) => T | null
|
||||
): T[] | null {
|
||||
return parseArray(value, parser, AGENT_STATUS_STORE_LIMITS.mutationEntries)
|
||||
}
|
||||
|
||||
function parseStringArray(value: unknown): string[] | null {
|
||||
return parseMutationArray(value, (candidate) =>
|
||||
isBoundedKey(candidate, MAX_TOMBSTONE_KEY_LENGTH) ? candidate : null
|
||||
)
|
||||
}
|
||||
|
||||
export function parseAgentStatusStoreMutation(value: unknown): AgentStatusStoreMutation | null {
|
||||
const optionalKeys = [
|
||||
'parent',
|
||||
'removeParent',
|
||||
'children',
|
||||
'removeChildren',
|
||||
'aliases',
|
||||
'removeAliases',
|
||||
'facts',
|
||||
'removeFacts',
|
||||
'tombstones'
|
||||
]
|
||||
if (
|
||||
!isWithinSerializedLimit(value) ||
|
||||
!isRecord(value) ||
|
||||
!hasOnlyKeys(value, [], optionalKeys) ||
|
||||
Object.keys(value).length === 0
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const parent = value.parent === undefined ? undefined : parseAgentStatusParentInput(value.parent)
|
||||
const removeParent =
|
||||
value.removeParent === undefined ? undefined : parseAgentStatusSubject(value.removeParent)
|
||||
const children =
|
||||
value.children === undefined
|
||||
? undefined
|
||||
: parseMutationArray(value.children, parseAgentChildWorkInput)
|
||||
const removeChildren =
|
||||
value.removeChildren === undefined ? undefined : parseStringArray(value.removeChildren)
|
||||
const aliases =
|
||||
value.aliases === undefined
|
||||
? undefined
|
||||
: parseMutationArray(value.aliases, parseAgentChildWorkAliasInput)
|
||||
const removeAliases =
|
||||
value.removeAliases === undefined ? undefined : parseStringArray(value.removeAliases)
|
||||
const facts =
|
||||
value.facts === undefined
|
||||
? undefined
|
||||
: parseMutationArray(value.facts, parseAgentStatusFactInput)
|
||||
const removeFacts =
|
||||
value.removeFacts === undefined
|
||||
? undefined
|
||||
: parseMutationArray(value.removeFacts, parseAgentStatusFactIdentity)
|
||||
const tombstones =
|
||||
value.tombstones === undefined
|
||||
? undefined
|
||||
: parseMutationArray(value.tombstones, parseAgentStatusTombstoneInput)
|
||||
const parsedValues = [
|
||||
parent,
|
||||
removeParent,
|
||||
children,
|
||||
removeChildren,
|
||||
aliases,
|
||||
removeAliases,
|
||||
facts,
|
||||
removeFacts,
|
||||
tombstones
|
||||
]
|
||||
const sourceValues = optionalKeys.map((key) => value[key])
|
||||
if (sourceValues.some((item, index) => item !== undefined && !parsedValues[index])) {
|
||||
return null
|
||||
}
|
||||
const mutationEntryCount = [
|
||||
children,
|
||||
removeChildren,
|
||||
aliases,
|
||||
removeAliases,
|
||||
facts,
|
||||
removeFacts,
|
||||
tombstones
|
||||
].reduce((sum, items) => sum + (items?.length ?? 0), 0)
|
||||
if (mutationEntryCount > AGENT_STATUS_STORE_LIMITS.mutationEntries) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
...(parent ? { parent } : {}),
|
||||
...(removeParent ? { removeParent } : {}),
|
||||
...(children ? { children } : {}),
|
||||
...(removeChildren ? { removeChildren } : {}),
|
||||
...(aliases ? { aliases } : {}),
|
||||
...(removeAliases ? { removeAliases } : {}),
|
||||
...(facts ? { facts } : {}),
|
||||
...(removeFacts ? { removeFacts } : {}),
|
||||
...(tombstones ? { tombstones } : {})
|
||||
}
|
||||
}
|
||||
|
||||
export function parseAgentStatusStoreSnapshot(value: unknown): AgentStatusStoreSnapshot | null {
|
||||
const keys = [
|
||||
'version',
|
||||
'epoch',
|
||||
'revision',
|
||||
'parents',
|
||||
'children',
|
||||
'aliases',
|
||||
'facts',
|
||||
'tombstones'
|
||||
]
|
||||
if (
|
||||
!isWithinSerializedLimit(value) ||
|
||||
!isRecord(value) ||
|
||||
!hasOnlyKeys(value, keys) ||
|
||||
value.version !== AGENT_STATUS_STORE_SNAPSHOT_VERSION ||
|
||||
!isAgentStatusStoreEpoch(value.epoch) ||
|
||||
!isRevision(value.revision)
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const parents = parseArray(
|
||||
value.parents,
|
||||
parseAgentStatusParentRecord,
|
||||
AGENT_STATUS_STORE_LIMITS.parents
|
||||
)
|
||||
const children = parseArray(
|
||||
value.children,
|
||||
parseAgentChildWorkRecord,
|
||||
AGENT_STATUS_STORE_LIMITS.children
|
||||
)
|
||||
const aliases = parseArray(
|
||||
value.aliases,
|
||||
parseAgentChildWorkAliasRecord,
|
||||
AGENT_STATUS_STORE_LIMITS.aliases
|
||||
)
|
||||
const facts = parseArray(value.facts, parseAgentStatusFactRecord, AGENT_STATUS_STORE_LIMITS.facts)
|
||||
const tombstones = parseArray(
|
||||
value.tombstones,
|
||||
parseAgentStatusTombstoneRecord,
|
||||
AGENT_STATUS_STORE_LIMITS.tombstones
|
||||
)
|
||||
return parents && children && aliases && facts && tombstones
|
||||
? {
|
||||
version: AGENT_STATUS_STORE_SNAPSHOT_VERSION,
|
||||
epoch: value.epoch,
|
||||
revision: value.revision,
|
||||
parents,
|
||||
children,
|
||||
aliases,
|
||||
facts,
|
||||
tombstones
|
||||
}
|
||||
: null
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import type {
|
||||
AgentChildWorkAliasInput,
|
||||
AgentChildWorkAliasRecord
|
||||
} from './agent-status-child-work-alias'
|
||||
import type { AgentChildWorkInput, AgentChildWorkRecord } from './agent-status-child-work'
|
||||
import type { AgentStatusParentInput, AgentStatusParentRecord } from './agent-status-store-parent'
|
||||
import type { AgentStatusSubject } from './agent-status-subject'
|
||||
|
||||
export const AGENT_STATUS_STORE_SNAPSHOT_VERSION = 1 as const
|
||||
export const AGENT_STATUS_STORE_LIMITS = {
|
||||
parents: 2_048,
|
||||
children: 8_192,
|
||||
aliases: 16_384,
|
||||
facts: 16_384,
|
||||
tombstones: 16_384,
|
||||
mutationEntries: 2_048,
|
||||
serializedBytes: 16 * 1024 * 1024
|
||||
} as const
|
||||
|
||||
export type AgentStatusFactValue = string | number | boolean | null
|
||||
|
||||
export type AgentStatusFactInput = {
|
||||
subject: AgentStatusSubject
|
||||
key: string
|
||||
value: AgentStatusFactValue
|
||||
}
|
||||
|
||||
export type AgentStatusFactRecord = AgentStatusFactInput & { revision: number }
|
||||
export type AgentStatusFactIdentity = Pick<AgentStatusFactInput, 'subject' | 'key'>
|
||||
export type AgentStatusTombstoneEntity = 'parent' | 'child' | 'alias' | 'fact'
|
||||
|
||||
export type AgentStatusTombstoneInput = {
|
||||
entity: AgentStatusTombstoneEntity
|
||||
key: string
|
||||
}
|
||||
|
||||
export type AgentStatusTombstoneRecord = AgentStatusTombstoneInput & { revision: number }
|
||||
|
||||
export type AgentStatusStoreMutation = {
|
||||
parent?: AgentStatusParentInput
|
||||
removeParent?: AgentStatusSubject
|
||||
children?: AgentChildWorkInput[]
|
||||
removeChildren?: string[]
|
||||
aliases?: AgentChildWorkAliasInput[]
|
||||
removeAliases?: string[]
|
||||
facts?: AgentStatusFactInput[]
|
||||
removeFacts?: AgentStatusFactIdentity[]
|
||||
tombstones?: AgentStatusTombstoneInput[]
|
||||
}
|
||||
|
||||
export type AgentStatusStoreSnapshot = {
|
||||
version: typeof AGENT_STATUS_STORE_SNAPSHOT_VERSION
|
||||
epoch: string
|
||||
revision: number
|
||||
parents: AgentStatusParentRecord[]
|
||||
children: AgentChildWorkRecord[]
|
||||
aliases: AgentChildWorkAliasRecord[]
|
||||
facts: AgentStatusFactRecord[]
|
||||
tombstones: AgentStatusTombstoneRecord[]
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import type {
|
||||
AgentStatusFactIdentity,
|
||||
AgentStatusFactInput,
|
||||
AgentStatusFactRecord,
|
||||
AgentStatusFactValue
|
||||
} from './agent-status-store-contract'
|
||||
import {
|
||||
deserializeAgentStatusSubject,
|
||||
parseAgentStatusSubject,
|
||||
serializeAgentStatusSubject
|
||||
} from './agent-status-subject'
|
||||
|
||||
const MAX_FACT_KEY_LENGTH = 256
|
||||
const MAX_FACT_STRING_LENGTH = 4_096
|
||||
const FACT_KEY_PREFIX = 'agent-status-fact-v1:'
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function hasExactKeys(record: Record<string, unknown>, expected: readonly string[]): boolean {
|
||||
const keys = Object.keys(record)
|
||||
return keys.length === expected.length && keys.every((key) => expected.includes(key))
|
||||
}
|
||||
|
||||
function isRevision(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0
|
||||
}
|
||||
|
||||
function isFactKey(value: unknown): value is string {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
value.length === 0 ||
|
||||
value.length > MAX_FACT_KEY_LENGTH ||
|
||||
value !== value.trim()
|
||||
) {
|
||||
return false
|
||||
}
|
||||
for (let index = 0; index < value.length; index += 1) {
|
||||
const code = value.charCodeAt(index)
|
||||
if (code <= 0x1f || code === 0x7f) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function parseFactValue(value: unknown): AgentStatusFactValue | undefined {
|
||||
if (value === null || typeof value === 'boolean') {
|
||||
return value
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value.length <= MAX_FACT_STRING_LENGTH ? value : undefined
|
||||
}
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
||||
}
|
||||
|
||||
export function parseAgentStatusFactInput(value: unknown): AgentStatusFactInput | null {
|
||||
if (!isRecord(value) || !hasExactKeys(value, ['subject', 'key', 'value'])) {
|
||||
return null
|
||||
}
|
||||
const subject = parseAgentStatusSubject(value.subject)
|
||||
const factValue = parseFactValue(value.value)
|
||||
if (!subject || !isFactKey(value.key) || factValue === undefined) {
|
||||
return null
|
||||
}
|
||||
return { subject, key: value.key, value: factValue }
|
||||
}
|
||||
|
||||
export function parseAgentStatusFactRecord(value: unknown): AgentStatusFactRecord | null {
|
||||
if (!isRecord(value) || !isRevision(value.revision)) {
|
||||
return null
|
||||
}
|
||||
const input = { ...value }
|
||||
delete input.revision
|
||||
const fact = parseAgentStatusFactInput(input)
|
||||
return fact ? { ...fact, revision: value.revision } : null
|
||||
}
|
||||
|
||||
export function parseAgentStatusFactIdentity(value: unknown): AgentStatusFactIdentity | null {
|
||||
if (!isRecord(value) || !hasExactKeys(value, ['subject', 'key'])) {
|
||||
return null
|
||||
}
|
||||
const subject = parseAgentStatusSubject(value.subject)
|
||||
return subject && isFactKey(value.key) ? { subject, key: value.key } : null
|
||||
}
|
||||
|
||||
export function serializeAgentStatusFactKey(fact: AgentStatusFactIdentity): string {
|
||||
const parsed = parseAgentStatusFactIdentity({ subject: fact.subject, key: fact.key })
|
||||
if (!parsed) {
|
||||
throw new Error('Invalid agent status fact identity')
|
||||
}
|
||||
return `${FACT_KEY_PREFIX}${JSON.stringify([
|
||||
serializeAgentStatusSubject(parsed.subject),
|
||||
parsed.key
|
||||
])}`
|
||||
}
|
||||
|
||||
export function deserializeAgentStatusFactKey(value: string): AgentStatusFactIdentity | null {
|
||||
if (!value.startsWith(FACT_KEY_PREFIX)) {
|
||||
return null
|
||||
}
|
||||
let tuple: unknown
|
||||
try {
|
||||
tuple = JSON.parse(value.slice(FACT_KEY_PREFIX.length))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
if (!Array.isArray(tuple) || tuple.length !== 2) {
|
||||
return null
|
||||
}
|
||||
const [subjectKey, key] = tuple
|
||||
if (typeof subjectKey !== 'string') {
|
||||
return null
|
||||
}
|
||||
const subject = deserializeAgentStatusSubject(subjectKey)
|
||||
const parsed = parseAgentStatusFactIdentity({ subject, key })
|
||||
return parsed && serializeAgentStatusFactKey(parsed) === value ? parsed : null
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
import {
|
||||
deserializeAgentChildWorkAliasKey,
|
||||
parseAgentChildWorkAliasRecord,
|
||||
serializeAgentChildWorkAliasKey
|
||||
} from './agent-status-child-work-alias'
|
||||
import { parseAgentChildWorkRecord } from './agent-status-child-work-codec'
|
||||
import type {
|
||||
AgentStatusStoreMutation,
|
||||
AgentStatusTombstoneEntity
|
||||
} from './agent-status-store-contract'
|
||||
import { parseAgentStatusFactRecord } from './agent-status-store-fact-codec'
|
||||
import { parseAgentStatusParentRecord } from './agent-status-store-parent'
|
||||
import {
|
||||
agentStatusFactMapKey,
|
||||
agentStatusTombstoneMapKey,
|
||||
cloneAgentStatusStoreState,
|
||||
deepFreezeAgentStatusStoreValue,
|
||||
validateAgentStatusStoreState,
|
||||
type AgentStatusStoreState
|
||||
} from './agent-status-store-state'
|
||||
import {
|
||||
agentStatusSubjectsEqual,
|
||||
deserializeAgentStatusSubject,
|
||||
serializeAgentStatusSubject,
|
||||
type AgentStatusSubject
|
||||
} from './agent-status-subject'
|
||||
|
||||
function addTombstone(
|
||||
state: AgentStatusStoreState,
|
||||
entity: AgentStatusTombstoneEntity,
|
||||
key: string,
|
||||
revision: number
|
||||
): void {
|
||||
const record = deepFreezeAgentStatusStoreValue({ entity, key, revision })
|
||||
state.tombstones.set(agentStatusTombstoneMapKey(entity, key), record)
|
||||
}
|
||||
|
||||
function removeAlias(state: AgentStatusStoreState, key: string, revision: number): void {
|
||||
state.aliases.delete(key)
|
||||
addTombstone(state, 'alias', key, revision)
|
||||
}
|
||||
|
||||
function removeFact(state: AgentStatusStoreState, key: string, revision: number): void {
|
||||
state.facts.delete(key)
|
||||
addTombstone(state, 'fact', key, revision)
|
||||
}
|
||||
|
||||
function removeChild(state: AgentStatusStoreState, childWorkId: string, revision: number): void {
|
||||
state.children.delete(childWorkId)
|
||||
addTombstone(state, 'child', childWorkId, revision)
|
||||
for (const [key, alias] of state.aliases) {
|
||||
if (alias.childWorkId === childWorkId) {
|
||||
removeAlias(state, key, revision)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeParent(
|
||||
state: AgentStatusStoreState,
|
||||
subject: AgentStatusSubject,
|
||||
revision: number
|
||||
): void {
|
||||
const key = serializeAgentStatusSubject(subject)
|
||||
state.parents.delete(key)
|
||||
addTombstone(state, 'parent', key, revision)
|
||||
for (const child of state.children.values()) {
|
||||
if (agentStatusSubjectsEqual(child.parent, subject)) {
|
||||
removeChild(state, child.childWorkId, revision)
|
||||
}
|
||||
}
|
||||
for (const [factMapKey, fact] of state.facts) {
|
||||
if (agentStatusSubjectsEqual(fact.subject, subject)) {
|
||||
removeFact(state, factMapKey, revision)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function applyExplicitTombstone(
|
||||
state: AgentStatusStoreState,
|
||||
tombstone: { entity: AgentStatusTombstoneEntity; key: string },
|
||||
revision: number
|
||||
): boolean {
|
||||
if (tombstone.entity === 'parent') {
|
||||
const subject = deserializeAgentStatusSubject(tombstone.key)
|
||||
if (!subject) {
|
||||
return false
|
||||
}
|
||||
removeParent(state, subject, revision)
|
||||
return true
|
||||
}
|
||||
if (tombstone.entity === 'child') {
|
||||
removeChild(state, tombstone.key, revision)
|
||||
} else if (tombstone.entity === 'alias') {
|
||||
if (!deserializeAgentChildWorkAliasKey(tombstone.key)) {
|
||||
return false
|
||||
}
|
||||
removeAlias(state, tombstone.key, revision)
|
||||
} else {
|
||||
removeFact(state, tombstone.key, revision)
|
||||
}
|
||||
addTombstone(state, tombstone.entity, tombstone.key, revision)
|
||||
return true
|
||||
}
|
||||
|
||||
function upsertParent(
|
||||
state: AgentStatusStoreState,
|
||||
input: NonNullable<AgentStatusStoreMutation['parent']>,
|
||||
revision: number
|
||||
): boolean {
|
||||
const key = serializeAgentStatusSubject(input.subject)
|
||||
if (state.tombstones.has(agentStatusTombstoneMapKey('parent', key))) {
|
||||
return false
|
||||
}
|
||||
const previous = state.parents.get(key)
|
||||
const record = parseAgentStatusParentRecord({
|
||||
...input,
|
||||
...(input.firstObservedAt === undefined && previous?.firstObservedAt !== undefined
|
||||
? { firstObservedAt: previous.firstObservedAt }
|
||||
: {}),
|
||||
revision
|
||||
})
|
||||
if (!record) {
|
||||
return false
|
||||
}
|
||||
state.parents.set(key, deepFreezeAgentStatusStoreValue(record))
|
||||
return true
|
||||
}
|
||||
|
||||
function upsertChildren(
|
||||
state: AgentStatusStoreState,
|
||||
children: NonNullable<AgentStatusStoreMutation['children']>,
|
||||
revision: number
|
||||
): boolean {
|
||||
for (const input of children) {
|
||||
const previous = state.children.get(input.childWorkId)
|
||||
if (
|
||||
state.tombstones.has(agentStatusTombstoneMapKey('child', input.childWorkId)) ||
|
||||
!state.parents.has(serializeAgentStatusSubject(input.parent)) ||
|
||||
(previous !== undefined && previous.firstObservedAt !== input.firstObservedAt) ||
|
||||
(previous !== undefined && input.observedAt < previous.observedAt)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
const record = parseAgentChildWorkRecord({ ...input, revision })
|
||||
if (!record) {
|
||||
return false
|
||||
}
|
||||
state.children.set(input.childWorkId, deepFreezeAgentStatusStoreValue(record))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function upsertAliases(
|
||||
state: AgentStatusStoreState,
|
||||
aliases: NonNullable<AgentStatusStoreMutation['aliases']>,
|
||||
revision: number
|
||||
): boolean {
|
||||
for (const input of aliases) {
|
||||
const record = parseAgentChildWorkAliasRecord({ ...input, revision })
|
||||
if (!record) {
|
||||
return false
|
||||
}
|
||||
state.aliases.set(
|
||||
serializeAgentChildWorkAliasKey(record),
|
||||
deepFreezeAgentStatusStoreValue(record)
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function upsertFacts(
|
||||
state: AgentStatusStoreState,
|
||||
facts: NonNullable<AgentStatusStoreMutation['facts']>,
|
||||
revision: number
|
||||
): boolean {
|
||||
for (const input of facts) {
|
||||
const record = parseAgentStatusFactRecord({ ...input, revision })
|
||||
if (!record || !state.parents.has(serializeAgentStatusSubject(record.subject))) {
|
||||
return false
|
||||
}
|
||||
state.facts.set(agentStatusFactMapKey(record), deepFreezeAgentStatusStoreValue(record))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function applyAgentStatusStoreMutation(
|
||||
current: AgentStatusStoreState,
|
||||
mutation: AgentStatusStoreMutation,
|
||||
revision: number
|
||||
): AgentStatusStoreState | null {
|
||||
const next = cloneAgentStatusStoreState(current)
|
||||
next.revision = revision
|
||||
if (mutation.removeParent) {
|
||||
removeParent(next, mutation.removeParent, revision)
|
||||
}
|
||||
for (const childWorkId of mutation.removeChildren ?? []) {
|
||||
removeChild(next, childWorkId, revision)
|
||||
}
|
||||
for (const key of mutation.removeAliases ?? []) {
|
||||
if (!deserializeAgentChildWorkAliasKey(key)) {
|
||||
return null
|
||||
}
|
||||
removeAlias(next, key, revision)
|
||||
}
|
||||
for (const identity of mutation.removeFacts ?? []) {
|
||||
removeFact(next, agentStatusFactMapKey(identity), revision)
|
||||
}
|
||||
for (const tombstone of mutation.tombstones ?? []) {
|
||||
if (!applyExplicitTombstone(next, tombstone, revision)) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (mutation.parent && !upsertParent(next, mutation.parent, revision)) {
|
||||
return null
|
||||
}
|
||||
if (mutation.children && !upsertChildren(next, mutation.children, revision)) {
|
||||
return null
|
||||
}
|
||||
if (mutation.aliases && !upsertAliases(next, mutation.aliases, revision)) {
|
||||
return null
|
||||
}
|
||||
if (mutation.facts && !upsertFacts(next, mutation.facts, revision)) {
|
||||
return null
|
||||
}
|
||||
return validateAgentStatusStoreState(next) ? next : null
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import type { AgentStatusIpcPayload } from './agent-status-ipc-payload'
|
||||
import { parseAgentStatusPtyRunRecord, type AgentStatusPtyRunRecord } from './agent-status-run'
|
||||
import { parseAgentStatusIpcPayloadCopy } from './agent-status-store-status-codec'
|
||||
import { parseAgentStatusSubject, type AgentStatusSubject } from './agent-status-subject'
|
||||
|
||||
export type AgentStatusParentInput = {
|
||||
subject: AgentStatusSubject
|
||||
status?: AgentStatusIpcPayload
|
||||
run?: AgentStatusPtyRunRecord
|
||||
firstObservedAt?: number
|
||||
}
|
||||
|
||||
export type AgentStatusParentRecord = AgentStatusParentInput & { revision: number }
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function hasOnlyKeys(
|
||||
record: Record<string, unknown>,
|
||||
required: readonly string[],
|
||||
optional: readonly string[] = []
|
||||
): boolean {
|
||||
const keys = Object.keys(record)
|
||||
return (
|
||||
required.every((key) => Object.hasOwn(record, key)) &&
|
||||
keys.every((key) => required.includes(key) || optional.includes(key))
|
||||
)
|
||||
}
|
||||
|
||||
function isTimestamp(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
||||
}
|
||||
|
||||
function isRevision(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0
|
||||
}
|
||||
|
||||
function isParentScopeConsistent(parent: AgentStatusParentInput): boolean {
|
||||
const { subject, status, run } = parent
|
||||
if (run && (subject.kind !== 'pty-run' || run.runId !== subject.runId)) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
subject.kind !== 'pty-run' &&
|
||||
(status?.runId !== undefined || status?.executionId !== undefined)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (status?.worktreeId !== undefined && status.worktreeId !== subject.workspaceId) {
|
||||
return false
|
||||
}
|
||||
if (subject.kind === 'pty' && status?.paneKey !== subject.paneKey) {
|
||||
return false
|
||||
}
|
||||
if (subject.kind === 'pty-run' && status?.runId !== undefined && status.runId !== subject.runId) {
|
||||
return false
|
||||
}
|
||||
if (run && status?.paneKey !== undefined && status.paneKey !== run.paneKey) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
run &&
|
||||
status?.executionId !== undefined &&
|
||||
status.executionId !== run.attachment.executionId
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (
|
||||
run &&
|
||||
status?.providerAlias &&
|
||||
run.providerSessions.length > 0 &&
|
||||
!run.providerSessions.some(
|
||||
(session) =>
|
||||
session.provider === status.providerAlias?.provider &&
|
||||
session.sessionKeyKind === status.providerAlias.sessionKeyKind &&
|
||||
session.providerId === status.providerAlias.providerId
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function parseAgentStatusParentInput(value: unknown): AgentStatusParentInput | null {
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
!hasOnlyKeys(value, ['subject'], ['status', 'run', 'firstObservedAt']) ||
|
||||
(value.firstObservedAt !== undefined && !isTimestamp(value.firstObservedAt))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const subject = parseAgentStatusSubject(value.subject)
|
||||
const status =
|
||||
value.status === undefined ? undefined : parseAgentStatusIpcPayloadCopy(value.status)
|
||||
const run = value.run === undefined ? undefined : parseAgentStatusPtyRunRecord(value.run)
|
||||
if (!subject || (value.status !== undefined && !status) || (value.run !== undefined && !run)) {
|
||||
return null
|
||||
}
|
||||
const parent: AgentStatusParentInput = {
|
||||
subject,
|
||||
...(status ? { status } : {}),
|
||||
...(run ? { run } : {}),
|
||||
...(isTimestamp(value.firstObservedAt) ? { firstObservedAt: value.firstObservedAt } : {})
|
||||
}
|
||||
return isParentScopeConsistent(parent) ? parent : null
|
||||
}
|
||||
|
||||
export function parseAgentStatusParentRecord(value: unknown): AgentStatusParentRecord | null {
|
||||
if (!isRecord(value) || !isRevision(value.revision)) {
|
||||
return null
|
||||
}
|
||||
const input = { ...value }
|
||||
delete input.revision
|
||||
const parent = parseAgentStatusParentInput(input)
|
||||
return parent ? { ...parent, revision: value.revision } : null
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
AGENT_STATUS_STORE_LIMITS,
|
||||
type AgentStatusStoreSnapshot
|
||||
} from './agent-status-store-contract'
|
||||
import { parseAgentStatusStoreSnapshot } from './agent-status-store-codec'
|
||||
import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit'
|
||||
import { measureUtf8ByteLength } from './utf8-byte-limits'
|
||||
|
||||
const SNAPSHOT_STRUCTURE_LIMITS = {
|
||||
structuralTokens: 512 * 1024,
|
||||
nestingDepth: 32
|
||||
} as const
|
||||
|
||||
function isWithinByteLimit(value: string): boolean {
|
||||
return !measureUtf8ByteLength(value, {
|
||||
stopAfterBytes: AGENT_STATUS_STORE_LIMITS.serializedBytes
|
||||
}).exceededLimit
|
||||
}
|
||||
|
||||
/** Bounded persistence form; callers write the returned string with their existing owner. */
|
||||
export function serializeAgentStatusStoreSnapshot(snapshot: AgentStatusStoreSnapshot): string {
|
||||
const parsed = parseAgentStatusStoreSnapshot(snapshot)
|
||||
if (!parsed) {
|
||||
throw new Error('Invalid agent status store snapshot')
|
||||
}
|
||||
const serialized = JSON.stringify(parsed)
|
||||
if (!isWithinByteLimit(serialized)) {
|
||||
throw new Error('Agent status store snapshot exceeds its serialized-byte limit')
|
||||
}
|
||||
return serialized
|
||||
}
|
||||
|
||||
export function deserializeAgentStatusStoreSnapshot(
|
||||
serialized: string
|
||||
): AgentStatusStoreSnapshot | null {
|
||||
if (!isWithinByteLimit(serialized)) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
assertJsonTextStructureWithinLimits(serialized, SNAPSHOT_STRUCTURE_LIMITS)
|
||||
return parseAgentStatusStoreSnapshot(JSON.parse(serialized))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
import {
|
||||
deserializeAgentChildWorkAliasKey,
|
||||
parseAgentChildWorkAliasRecord,
|
||||
serializeAgentChildWorkAliasKey,
|
||||
type AgentChildWorkAliasRecord
|
||||
} from './agent-status-child-work-alias'
|
||||
import {
|
||||
agentChildWorkBelongsTo,
|
||||
agentChildWorkFencesEqual,
|
||||
type AgentChildWorkRecord
|
||||
} from './agent-status-child-work'
|
||||
import { parseAgentChildWorkRecord } from './agent-status-child-work-codec'
|
||||
import {
|
||||
AGENT_STATUS_STORE_LIMITS,
|
||||
AGENT_STATUS_STORE_SNAPSHOT_VERSION,
|
||||
type AgentStatusFactIdentity,
|
||||
type AgentStatusFactRecord,
|
||||
type AgentStatusStoreSnapshot,
|
||||
type AgentStatusTombstoneEntity,
|
||||
type AgentStatusTombstoneRecord
|
||||
} from './agent-status-store-contract'
|
||||
import {
|
||||
parseAgentStatusStoreSnapshot,
|
||||
parseAgentStatusTombstoneRecord
|
||||
} from './agent-status-store-codec'
|
||||
import {
|
||||
deserializeAgentStatusFactKey,
|
||||
parseAgentStatusFactRecord,
|
||||
serializeAgentStatusFactKey
|
||||
} from './agent-status-store-fact-codec'
|
||||
import {
|
||||
parseAgentStatusParentRecord,
|
||||
type AgentStatusParentRecord
|
||||
} from './agent-status-store-parent'
|
||||
import { deserializeAgentStatusSubject, serializeAgentStatusSubject } from './agent-status-subject'
|
||||
|
||||
export type AgentStatusStoreState = {
|
||||
epoch: string
|
||||
revision: number
|
||||
parents: Map<string, AgentStatusParentRecord>
|
||||
children: Map<string, AgentChildWorkRecord>
|
||||
aliases: Map<string, AgentChildWorkAliasRecord>
|
||||
facts: Map<string, AgentStatusFactRecord>
|
||||
tombstones: Map<string, AgentStatusTombstoneRecord>
|
||||
}
|
||||
|
||||
export function deepFreezeAgentStatusStoreValue<T>(value: T): T {
|
||||
if (typeof value !== 'object' || value === null || Object.isFrozen(value)) {
|
||||
return value
|
||||
}
|
||||
for (const nested of Object.values(value)) {
|
||||
deepFreezeAgentStatusStoreValue(nested)
|
||||
}
|
||||
return Object.freeze(value)
|
||||
}
|
||||
|
||||
export function agentStatusFactMapKey(fact: AgentStatusFactIdentity): string {
|
||||
return serializeAgentStatusFactKey(fact)
|
||||
}
|
||||
|
||||
export function agentStatusTombstoneMapKey(
|
||||
entity: AgentStatusTombstoneEntity,
|
||||
key: string
|
||||
): string {
|
||||
return `${entity}\0${key}`
|
||||
}
|
||||
|
||||
export function createEmptyAgentStatusStoreState(epoch: string): AgentStatusStoreState {
|
||||
return {
|
||||
epoch,
|
||||
revision: 0,
|
||||
parents: new Map(),
|
||||
children: new Map(),
|
||||
aliases: new Map(),
|
||||
facts: new Map(),
|
||||
tombstones: new Map()
|
||||
}
|
||||
}
|
||||
|
||||
export function cloneAgentStatusStoreState(state: AgentStatusStoreState): AgentStatusStoreState {
|
||||
return {
|
||||
epoch: state.epoch,
|
||||
revision: state.revision,
|
||||
parents: new Map(state.parents),
|
||||
children: new Map(state.children),
|
||||
aliases: new Map(state.aliases),
|
||||
facts: new Map(state.facts),
|
||||
tombstones: new Map(state.tombstones)
|
||||
}
|
||||
}
|
||||
|
||||
function hasMatchingFence(child: AgentChildWorkRecord, alias: AgentChildWorkAliasRecord): boolean {
|
||||
if (agentChildWorkFencesEqual(child.invocation, alias.fence)) {
|
||||
return true
|
||||
}
|
||||
return (
|
||||
child.previousInvocations?.some((entry) =>
|
||||
agentChildWorkFencesEqual(entry.fence, alias.fence)
|
||||
) === true
|
||||
)
|
||||
}
|
||||
|
||||
export function validateAgentStatusStoreState(state: AgentStatusStoreState): boolean {
|
||||
if (
|
||||
state.parents.size > AGENT_STATUS_STORE_LIMITS.parents ||
|
||||
state.children.size > AGENT_STATUS_STORE_LIMITS.children ||
|
||||
state.aliases.size > AGENT_STATUS_STORE_LIMITS.aliases ||
|
||||
state.facts.size > AGENT_STATUS_STORE_LIMITS.facts ||
|
||||
state.tombstones.size > AGENT_STATUS_STORE_LIMITS.tombstones
|
||||
) {
|
||||
return false
|
||||
}
|
||||
for (const [key, parent] of state.parents) {
|
||||
if (
|
||||
key !== serializeAgentStatusSubject(parent.subject) ||
|
||||
parent.revision > state.revision ||
|
||||
state.tombstones.has(agentStatusTombstoneMapKey('parent', key))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (const [childWorkId, child] of state.children) {
|
||||
if (
|
||||
childWorkId !== child.childWorkId ||
|
||||
child.revision > state.revision ||
|
||||
!state.parents.has(serializeAgentStatusSubject(child.parent)) ||
|
||||
state.tombstones.has(agentStatusTombstoneMapKey('child', childWorkId))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (const [key, alias] of state.aliases) {
|
||||
const child = state.children.get(alias.childWorkId)
|
||||
const tombstone = state.tombstones.get(agentStatusTombstoneMapKey('alias', key))
|
||||
if (
|
||||
key !== serializeAgentChildWorkAliasKey(alias) ||
|
||||
alias.revision > state.revision ||
|
||||
!child ||
|
||||
!agentChildWorkBelongsTo(child, alias.parent) ||
|
||||
child.provider !== alias.provider ||
|
||||
child.kind !== alias.kind ||
|
||||
!hasMatchingFence(child, alias) ||
|
||||
(tombstone !== undefined && tombstone.revision >= alias.revision)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (const [key, fact] of state.facts) {
|
||||
const tombstone = state.tombstones.get(agentStatusTombstoneMapKey('fact', key))
|
||||
if (
|
||||
key !== agentStatusFactMapKey(fact) ||
|
||||
fact.revision > state.revision ||
|
||||
!state.parents.has(serializeAgentStatusSubject(fact.subject)) ||
|
||||
(tombstone !== undefined && tombstone.revision >= fact.revision)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
for (const item of state.tombstones.values()) {
|
||||
if (
|
||||
item.revision > state.revision ||
|
||||
(item.entity === 'parent' && !deserializeAgentStatusSubject(item.key)) ||
|
||||
(item.entity === 'alias' && !deserializeAgentChildWorkAliasKey(item.key)) ||
|
||||
(item.entity === 'fact' && !deserializeAgentStatusFactKey(item.key))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function snapshotFromAgentStatusStoreState(
|
||||
state: AgentStatusStoreState
|
||||
): AgentStatusStoreSnapshot {
|
||||
const snapshot = parseAgentStatusStoreSnapshot({
|
||||
version: AGENT_STATUS_STORE_SNAPSHOT_VERSION,
|
||||
epoch: state.epoch,
|
||||
revision: state.revision,
|
||||
parents: [...state.parents.values()],
|
||||
children: [...state.children.values()],
|
||||
aliases: [...state.aliases.values()],
|
||||
facts: [...state.facts.values()],
|
||||
tombstones: [...state.tombstones.values()]
|
||||
})
|
||||
if (!snapshot) {
|
||||
throw new Error('Agent status store produced an invalid snapshot')
|
||||
}
|
||||
return deepFreezeAgentStatusStoreValue(snapshot)
|
||||
}
|
||||
|
||||
export function agentStatusStoreStateFromSnapshot(
|
||||
snapshot: AgentStatusStoreSnapshot,
|
||||
epoch: string
|
||||
): AgentStatusStoreState | null {
|
||||
const state = createEmptyAgentStatusStoreState(epoch)
|
||||
state.revision = snapshot.revision
|
||||
for (const parent of snapshot.parents) {
|
||||
const record = parseAgentStatusParentRecord(parent)
|
||||
if (!record) {
|
||||
return null
|
||||
}
|
||||
const key = serializeAgentStatusSubject(record.subject)
|
||||
if (state.parents.has(key)) {
|
||||
return null
|
||||
}
|
||||
state.parents.set(key, deepFreezeAgentStatusStoreValue(record))
|
||||
}
|
||||
for (const child of snapshot.children) {
|
||||
const record = parseAgentChildWorkRecord(child)
|
||||
if (!record || state.children.has(record.childWorkId)) {
|
||||
return null
|
||||
}
|
||||
state.children.set(record.childWorkId, deepFreezeAgentStatusStoreValue(record))
|
||||
}
|
||||
for (const alias of snapshot.aliases) {
|
||||
const record = parseAgentChildWorkAliasRecord(alias)
|
||||
if (!record) {
|
||||
return null
|
||||
}
|
||||
const key = serializeAgentChildWorkAliasKey(record)
|
||||
if (state.aliases.has(key)) {
|
||||
return null
|
||||
}
|
||||
state.aliases.set(key, deepFreezeAgentStatusStoreValue(record))
|
||||
}
|
||||
for (const fact of snapshot.facts) {
|
||||
const record = parseAgentStatusFactRecord(fact)
|
||||
if (!record) {
|
||||
return null
|
||||
}
|
||||
const key = agentStatusFactMapKey(record)
|
||||
if (state.facts.has(key)) {
|
||||
return null
|
||||
}
|
||||
state.facts.set(key, deepFreezeAgentStatusStoreValue(record))
|
||||
}
|
||||
for (const tombstone of snapshot.tombstones) {
|
||||
const record = parseAgentStatusTombstoneRecord(tombstone)
|
||||
if (!record) {
|
||||
return null
|
||||
}
|
||||
const key = agentStatusTombstoneMapKey(record.entity, record.key)
|
||||
if (state.tombstones.has(key)) {
|
||||
return null
|
||||
}
|
||||
state.tombstones.set(key, deepFreezeAgentStatusStoreValue(record))
|
||||
}
|
||||
return validateAgentStatusStoreState(state) ? state : null
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
import { normalizeAgentProviderSession } from './agent-session-resume'
|
||||
import type { AgentStatusIpcPayload } from './agent-status-ipc-payload'
|
||||
import {
|
||||
isAgentStatusExecutionId,
|
||||
isAgentStatusRunId,
|
||||
parseAgentStatusProviderAlias
|
||||
} from './agent-status-run'
|
||||
import { normalizeAgentStatusPayload } from './agent-status-types'
|
||||
import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit'
|
||||
import { measureUtf8ByteLength } from './utf8-byte-limits'
|
||||
|
||||
const MAX_STATUS_BYTES = 256 * 1024
|
||||
const MAX_ID_LENGTH = 4_096
|
||||
const STATUS_STRUCTURE_LIMITS = { structuralTokens: 16_384, nestingDepth: 24 } as const
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function hasOnlyKeys(
|
||||
record: Record<string, unknown>,
|
||||
required: readonly string[],
|
||||
optional: readonly string[] = []
|
||||
): boolean {
|
||||
const keys = Object.keys(record)
|
||||
return (
|
||||
required.every((key) => Object.hasOwn(record, key)) &&
|
||||
keys.every((key) => required.includes(key) || optional.includes(key))
|
||||
)
|
||||
}
|
||||
|
||||
function isTimestamp(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isFinite(value) && value >= 0
|
||||
}
|
||||
|
||||
function isRevision(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0
|
||||
}
|
||||
|
||||
function isBoundedString(value: unknown): value is string {
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
value.length > 0 &&
|
||||
value.length <= MAX_ID_LENGTH &&
|
||||
!value.includes('\0')
|
||||
)
|
||||
}
|
||||
|
||||
function copyJsonRecord(value: unknown): Record<string, unknown> | null {
|
||||
let serialized: string
|
||||
try {
|
||||
serialized = JSON.stringify(value)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
if (measureUtf8ByteLength(serialized, { stopAfterBytes: MAX_STATUS_BYTES }).exceededLimit) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
assertJsonTextStructureWithinLimits(serialized, STATUS_STRUCTURE_LIMITS)
|
||||
const parsed: unknown = JSON.parse(serialized)
|
||||
return isRecord(parsed) ? parsed : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function parseObservation(value: unknown): Record<string, unknown> | null {
|
||||
if (
|
||||
!isRecord(value) ||
|
||||
!hasOnlyKeys(
|
||||
value,
|
||||
['origin', 'authorityId', 'incarnation', 'revision', 'observedAt'],
|
||||
['boundary', 'kind']
|
||||
) ||
|
||||
(value.origin !== 'hook' &&
|
||||
value.origin !== 'osc' &&
|
||||
value.origin !== 'title' &&
|
||||
value.origin !== 'process' &&
|
||||
value.origin !== 'launch' &&
|
||||
value.origin !== 'orchestration' &&
|
||||
value.origin !== 'structured') ||
|
||||
!isBoundedString(value.authorityId) ||
|
||||
!isRevision(value.incarnation) ||
|
||||
!isRevision(value.revision) ||
|
||||
!isTimestamp(value.observedAt) ||
|
||||
(value.boundary !== undefined && value.boundary !== true) ||
|
||||
(value.kind !== undefined &&
|
||||
value.kind !== 'transition' &&
|
||||
value.kind !== 'snapshot' &&
|
||||
value.kind !== 'identity-only')
|
||||
) {
|
||||
return null
|
||||
}
|
||||
return {
|
||||
origin: value.origin,
|
||||
authorityId: value.authorityId,
|
||||
incarnation: value.incarnation,
|
||||
revision: value.revision,
|
||||
observedAt: value.observedAt,
|
||||
...(value.boundary === true ? { boundary: true } : {}),
|
||||
...(value.kind !== undefined ? { kind: value.kind } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function parseOrchestration(value: unknown): Record<string, unknown> | null {
|
||||
if (!isRecord(value) || !isBoundedString(value.taskId) || !isBoundedString(value.dispatchId)) {
|
||||
return null
|
||||
}
|
||||
const optionalStrings = [
|
||||
'taskTitle',
|
||||
'displayName',
|
||||
'parentTerminalHandle',
|
||||
'parentPaneKey',
|
||||
'coordinatorHandle',
|
||||
'orchestrationRunId'
|
||||
]
|
||||
if (optionalStrings.some((key) => value[key] !== undefined && !isBoundedString(value[key]))) {
|
||||
return null
|
||||
}
|
||||
if (
|
||||
value.dispatchStatus !== undefined &&
|
||||
value.dispatchStatus !== 'pending' &&
|
||||
value.dispatchStatus !== 'dispatched' &&
|
||||
value.dispatchStatus !== 'completed' &&
|
||||
value.dispatchStatus !== 'failed' &&
|
||||
value.dispatchStatus !== 'circuit_broken'
|
||||
) {
|
||||
return null
|
||||
}
|
||||
if (value.attention !== undefined && !isRecord(value.attention)) {
|
||||
return null
|
||||
}
|
||||
return { ...value }
|
||||
}
|
||||
|
||||
function copyOptionalString(
|
||||
source: Record<string, unknown>,
|
||||
target: Record<string, unknown>,
|
||||
key: string
|
||||
): boolean {
|
||||
const value = source[key]
|
||||
if (value === undefined) {
|
||||
return true
|
||||
}
|
||||
if (!isBoundedString(value)) {
|
||||
return false
|
||||
}
|
||||
target[key] = value
|
||||
return true
|
||||
}
|
||||
|
||||
export function parseAgentStatusIpcPayloadCopy(value: unknown): AgentStatusIpcPayload | null {
|
||||
const copied = copyJsonRecord(value)
|
||||
const payload = normalizeAgentStatusPayload(copied)
|
||||
if (
|
||||
!copied ||
|
||||
!payload ||
|
||||
!isBoundedString(copied.paneKey) ||
|
||||
(copied.connectionId !== null && !isBoundedString(copied.connectionId)) ||
|
||||
!isTimestamp(copied.receivedAt) ||
|
||||
!isTimestamp(copied.stateStartedAt) ||
|
||||
(copied.evidenceObservedAt !== undefined && !isTimestamp(copied.evidenceObservedAt))
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const parsed: Record<string, unknown> = {
|
||||
...payload,
|
||||
paneKey: copied.paneKey,
|
||||
connectionId: copied.connectionId,
|
||||
receivedAt: copied.receivedAt,
|
||||
stateStartedAt: copied.stateStartedAt
|
||||
}
|
||||
for (const key of [
|
||||
'launchToken',
|
||||
'terminalHandle',
|
||||
'tabId',
|
||||
'worktreeId',
|
||||
'promptInteractionKey'
|
||||
]) {
|
||||
if (!copyOptionalString(copied, parsed, key)) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
if (copied.runId !== undefined) {
|
||||
if (!isAgentStatusRunId(copied.runId)) {
|
||||
return null
|
||||
}
|
||||
parsed.runId = copied.runId
|
||||
}
|
||||
if (copied.executionId !== undefined) {
|
||||
if (!isAgentStatusExecutionId(copied.executionId)) {
|
||||
return null
|
||||
}
|
||||
parsed.executionId = copied.executionId
|
||||
}
|
||||
if (copied.providerAlias !== undefined) {
|
||||
const providerAlias = parseAgentStatusProviderAlias(copied.providerAlias)
|
||||
if (!providerAlias) {
|
||||
return null
|
||||
}
|
||||
parsed.providerAlias = providerAlias
|
||||
}
|
||||
if (isTimestamp(copied.evidenceObservedAt)) {
|
||||
parsed.evidenceObservedAt = copied.evidenceObservedAt
|
||||
}
|
||||
if (copied.providerSession !== undefined) {
|
||||
const providerSession = normalizeAgentProviderSession(copied.providerSession)
|
||||
if (!providerSession) {
|
||||
return null
|
||||
}
|
||||
parsed.providerSession = providerSession
|
||||
}
|
||||
if (copied.orchestration !== undefined) {
|
||||
const orchestration = parseOrchestration(copied.orchestration)
|
||||
if (!orchestration) {
|
||||
return null
|
||||
}
|
||||
parsed.orchestration = orchestration
|
||||
}
|
||||
if (copied.observation !== undefined) {
|
||||
const observation = parseObservation(copied.observation)
|
||||
if (!observation) {
|
||||
return null
|
||||
}
|
||||
parsed.observation = observation
|
||||
}
|
||||
if (copied.providerSessionOnly === true) {
|
||||
parsed.providerSessionOnly = true
|
||||
} else if (copied.providerSessionOnly !== undefined && copied.providerSessionOnly !== false) {
|
||||
return null
|
||||
}
|
||||
if (copied.restoredUnconfirmed === true) {
|
||||
parsed.restoredUnconfirmed = true
|
||||
} else if (copied.restoredUnconfirmed !== undefined && copied.restoredUnconfirmed !== false) {
|
||||
return null
|
||||
}
|
||||
if (copied.structuredHost === 'held' || copied.structuredHost === 'owned') {
|
||||
parsed.structuredHost = copied.structuredHost
|
||||
} else if (copied.structuredHost !== undefined) {
|
||||
return null
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Every required and optional field is rebuilt from its canonical parser above.
|
||||
return parsed as AgentStatusIpcPayload
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { AgentStatusIpcPayload } from './agent-status-ipc-payload'
|
||||
import type { AgentChildWorkAliasInput } from './agent-status-child-work-alias'
|
||||
import type { AgentChildWorkInput } from './agent-status-child-work'
|
||||
import { createAgentStatusStore } from './agent-status-store'
|
||||
import {
|
||||
AGENT_STATUS_STORE_LIMITS,
|
||||
type AgentStatusStoreSnapshot
|
||||
} from './agent-status-store-contract'
|
||||
import {
|
||||
deserializeAgentStatusStoreSnapshot,
|
||||
serializeAgentStatusStoreSnapshot
|
||||
} from './agent-status-store-persistence'
|
||||
import {
|
||||
makePtyAgentStatusSubject,
|
||||
makePtyRunAgentStatusSubject,
|
||||
makeStructuredAgentStatusSubject,
|
||||
serializeAgentStatusSubject,
|
||||
type AgentStatusExecutionScope,
|
||||
type AgentStatusSubject
|
||||
} from './agent-status-subject'
|
||||
|
||||
const SESSION_ID = 'session_11111111-1111-4111-8111-111111111111'
|
||||
|
||||
function scope(overrides: Partial<AgentStatusExecutionScope> = {}): AgentStatusExecutionScope {
|
||||
return {
|
||||
executionHostId: 'local',
|
||||
wslDistro: null,
|
||||
workspaceId: 'workspace-1',
|
||||
workspaceKind: 'git-worktree',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
function subject(overrides: Partial<AgentStatusExecutionScope> = {}): AgentStatusSubject {
|
||||
return makeStructuredAgentStatusSubject(scope(overrides), SESSION_ID)
|
||||
}
|
||||
|
||||
function status(parent: AgentStatusSubject, overrides: Partial<AgentStatusIpcPayload> = {}) {
|
||||
return {
|
||||
state: 'working',
|
||||
prompt: 'Ship it',
|
||||
paneKey: 'structured-pane-key',
|
||||
connectionId: null,
|
||||
receivedAt: 20,
|
||||
evidenceObservedAt: 18,
|
||||
stateStartedAt: 10,
|
||||
worktreeId: parent.workspaceId,
|
||||
structuredHost: 'owned',
|
||||
...overrides
|
||||
} satisfies AgentStatusIpcPayload
|
||||
}
|
||||
|
||||
function child(parent: AgentStatusSubject, overrides: Partial<AgentChildWorkInput> = {}) {
|
||||
return {
|
||||
childWorkId: 'child-1',
|
||||
parent,
|
||||
provider: 'claude',
|
||||
kind: 'agent',
|
||||
state: 'working',
|
||||
membership: 'live',
|
||||
firstObservedAt: 12,
|
||||
observedAt: 20,
|
||||
stoppable: true,
|
||||
invocation: { invocationId: 'invocation-1', generation: 1 },
|
||||
provenance: { source: 'structured-session', producerId: 'journal-1' },
|
||||
...overrides
|
||||
} satisfies AgentChildWorkInput
|
||||
}
|
||||
|
||||
function alias(parent: AgentStatusSubject): AgentChildWorkAliasInput {
|
||||
return {
|
||||
parent,
|
||||
provider: 'claude',
|
||||
segmentId: 'segment-1',
|
||||
kind: 'agent',
|
||||
aliasKind: 'task_id',
|
||||
alias: 'provider-task-1',
|
||||
childWorkId: 'child-1',
|
||||
fence: { invocationId: 'invocation-1', generation: 1 }
|
||||
}
|
||||
}
|
||||
|
||||
function populatedStore() {
|
||||
const parent = subject()
|
||||
const store = createAgentStatusStore({ epoch: 'epoch-a', mode: 'authority' })
|
||||
const committed = store.applyMutation({
|
||||
parent: { subject: parent, status: status(parent), firstObservedAt: 5 },
|
||||
children: [child(parent)],
|
||||
aliases: [alias(parent)],
|
||||
facts: [{ subject: parent, key: 'acknowledged', value: true }]
|
||||
})
|
||||
expect(committed?.revision).toBe(1)
|
||||
return { parent, store }
|
||||
}
|
||||
|
||||
describe('AgentStatusStore', () => {
|
||||
it('stores structured status on the parent record and isolates colliding scoped subjects', () => {
|
||||
const store = createAgentStatusStore({ epoch: 'epoch-a', mode: 'authority' })
|
||||
const subjects = [
|
||||
subject(),
|
||||
subject({ wslDistro: 'Ubuntu' }),
|
||||
subject({ executionHostId: 'ssh:host-a' }),
|
||||
subject({ executionHostId: 'runtime:peer-a' }),
|
||||
subject({ workspaceId: 'folder-1', workspaceKind: 'folder' })
|
||||
]
|
||||
|
||||
for (const [index, scopedSubject] of subjects.entries()) {
|
||||
expect(
|
||||
store.applyMutation({
|
||||
parent: {
|
||||
subject: scopedSubject,
|
||||
status: status(scopedSubject, { prompt: `prompt-${index}` }),
|
||||
firstObservedAt: index + 1
|
||||
}
|
||||
})
|
||||
).not.toBeNull()
|
||||
}
|
||||
|
||||
expect(store.getSnapshot().parents).toHaveLength(subjects.length)
|
||||
expect(subjects.map((item) => store.getParent(item)?.status?.prompt)).toEqual([
|
||||
'prompt-0',
|
||||
'prompt-1',
|
||||
'prompt-2',
|
||||
'prompt-3',
|
||||
'prompt-4'
|
||||
])
|
||||
})
|
||||
|
||||
it('accepts trusted PTY fixtures while enforcing run and scope consistency', () => {
|
||||
const store = createAgentStatusStore({ epoch: 'epoch-a', mode: 'authority' })
|
||||
const pty = makePtyAgentStatusSubject(scope(), 'pane-1')
|
||||
const runSubject = makePtyRunAgentStatusSubject(scope(), 'run-1')
|
||||
|
||||
expect(
|
||||
store.applyMutation({
|
||||
parent: { subject: pty, status: status(pty, { paneKey: 'pane-1' }) }
|
||||
})
|
||||
).not.toBeNull()
|
||||
expect(
|
||||
store.applyMutation({
|
||||
parent: { subject: pty, status: status(pty, { paneKey: 'wrong-pane' }) }
|
||||
})
|
||||
).toBeNull()
|
||||
const run = {
|
||||
runId: 'run-1',
|
||||
paneKey: 'pane-2',
|
||||
attachment: { executionId: 'execution-1' },
|
||||
attribution: 'token',
|
||||
providerSessions: [
|
||||
{ provider: 'claude', sessionKeyKind: 'session_id', providerId: 'provider-1' }
|
||||
],
|
||||
role: 'root',
|
||||
verdict: 'live'
|
||||
} as const
|
||||
expect(
|
||||
store.applyMutation({
|
||||
parent: {
|
||||
subject: runSubject,
|
||||
run,
|
||||
status: status(runSubject, {
|
||||
paneKey: 'pane-2',
|
||||
runId: 'run-1',
|
||||
executionId: 'execution-1',
|
||||
providerAlias: {
|
||||
provider: 'claude',
|
||||
sessionKeyKind: 'session_id',
|
||||
providerId: 'provider-1'
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
).not.toBeNull()
|
||||
expect(store.getParent(runSubject)?.run).toEqual(run)
|
||||
expect(
|
||||
store.applyMutation({
|
||||
parent: {
|
||||
subject: runSubject,
|
||||
status: status(runSubject, { paneKey: 'pane-2', runId: 'wrong-run' })
|
||||
}
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('uses parsed immutable copies instead of caller-owned objects', () => {
|
||||
const parent = subject()
|
||||
const store = createAgentStatusStore({ epoch: 'epoch-a', mode: 'authority' })
|
||||
const inputStatus = status(parent, {
|
||||
subagents: [{ id: 'provider-child', state: 'working', startedAt: 1, description: 'before' }]
|
||||
})
|
||||
expect(store.applyMutation({ parent: { subject: parent, status: inputStatus } })).not.toBeNull()
|
||||
|
||||
inputStatus.prompt = 'mutated input'
|
||||
inputStatus.subagents?.splice(0)
|
||||
const first = store.getParent(parent)
|
||||
expect(first?.status?.prompt).toBe('Ship it')
|
||||
expect(first?.status?.subagents).toHaveLength(1)
|
||||
expect(Object.isFrozen(first)).toBe(true)
|
||||
expect(Object.isFrozen(first?.status?.subagents)).toBe(true)
|
||||
expect(() => first?.status?.subagents?.splice(0)).toThrow()
|
||||
expect(store.getParent(parent)?.status?.subagents).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('commits parent, child, alias, fact and tombstone state atomically', () => {
|
||||
const { parent, store } = populatedStore()
|
||||
const before = store.getSnapshot()
|
||||
const invalidAlias = { ...alias(parent), provider: 'codex' }
|
||||
|
||||
expect(
|
||||
store.applyMutation({
|
||||
children: [child(parent, { state: 'waiting', observedAt: 21 })],
|
||||
aliases: [invalidAlias],
|
||||
facts: [{ subject: parent, key: 'unread', value: true }]
|
||||
})
|
||||
).toBeNull()
|
||||
expect(store.getSnapshot()).toEqual(before)
|
||||
|
||||
const removal = store.applyMutation({ removeParent: parent })
|
||||
const removed = store.getSnapshot()
|
||||
expect(removal?.revision).toBe(2)
|
||||
expect(removed.parents).toEqual([])
|
||||
expect(removed.children).toEqual([])
|
||||
expect(removed.aliases).toEqual([])
|
||||
expect(removed.facts).toEqual([])
|
||||
expect(new Set(removed.tombstones.map((item) => item.entity))).toEqual(
|
||||
new Set(['parent', 'child', 'alias', 'fact'])
|
||||
)
|
||||
expect(new Set(removed.tombstones.map((item) => item.revision))).toEqual(new Set([2]))
|
||||
})
|
||||
|
||||
it('never resurrects an exactly removed parent or child id', () => {
|
||||
const { parent, store } = populatedStore()
|
||||
expect(store.applyMutation({ removeChildren: ['child-1'] })).not.toBeNull()
|
||||
expect(store.applyMutation({ children: [child(parent, { observedAt: 30 })] })).toBeNull()
|
||||
expect(store.applyMutation({ removeParent: parent })).not.toBeNull()
|
||||
expect(store.applyMutation({ parent: { subject: parent, status: status(parent) } })).toBeNull()
|
||||
})
|
||||
|
||||
it('persists a bounded snapshot and restores child identity under a new epoch', () => {
|
||||
const { parent, store } = populatedStore()
|
||||
expect(
|
||||
store.applyMutation({ removeFacts: [{ subject: parent, key: 'acknowledged' }] })
|
||||
).not.toBeNull()
|
||||
expect(
|
||||
store.applyMutation({ facts: [{ subject: parent, key: 'acknowledged', value: true }] })
|
||||
).not.toBeNull()
|
||||
const serialized = serializeAgentStatusStoreSnapshot(store.getSnapshot())
|
||||
const persisted = deserializeAgentStatusStoreSnapshot(serialized)
|
||||
const restarted = createAgentStatusStore({ epoch: 'epoch-b', mode: 'authority' })
|
||||
|
||||
expect(persisted).not.toBeNull()
|
||||
expect(restarted.applySnapshot(persisted)).toBe(true)
|
||||
expect(restarted.getSnapshot().epoch).toBe('epoch-b')
|
||||
expect(restarted.getSnapshot().revision).toBe(3)
|
||||
expect(restarted.getChildren(parent)[0]?.childWorkId).toBe('child-1')
|
||||
expect(restarted.getChildren(parent)[0]?.firstObservedAt).toBe(12)
|
||||
})
|
||||
|
||||
it('fails closed before parsing oversized persistence payloads or allocating huge mutations', () => {
|
||||
const parse = vi.spyOn(JSON, 'parse')
|
||||
const oversized = ' '.repeat(AGENT_STATUS_STORE_LIMITS.serializedBytes + 1)
|
||||
|
||||
expect(deserializeAgentStatusStoreSnapshot(oversized)).toBeNull()
|
||||
expect(parse).not.toHaveBeenCalled()
|
||||
parse.mockRestore()
|
||||
|
||||
const store = createAgentStatusStore({ epoch: 'epoch-a', mode: 'authority' })
|
||||
expect(
|
||||
store.applyMutation({
|
||||
removeChildren: Array.from(
|
||||
{ length: AGENT_STATUS_STORE_LIMITS.mutationEntries + 1 },
|
||||
(_, index) => `child-${index}`
|
||||
)
|
||||
})
|
||||
).toBeNull()
|
||||
expect(store.getSnapshot().revision).toBe(0)
|
||||
const scopedSubject = subject()
|
||||
expect(
|
||||
store.applyMutation({
|
||||
parent: {
|
||||
subject: scopedSubject,
|
||||
status: {
|
||||
...status(scopedSubject),
|
||||
orchestration: {
|
||||
taskId: 'task-1',
|
||||
dispatchId: 'dispatch-1',
|
||||
attention: { oversized: 'x'.repeat(300 * 1024) }
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('rejects malformed and scope-mismatched snapshots without changing state', () => {
|
||||
const { store } = populatedStore()
|
||||
const before = store.getSnapshot()
|
||||
const malformed: AgentStatusStoreSnapshot = {
|
||||
...before,
|
||||
children: [
|
||||
{
|
||||
...before.children[0],
|
||||
parent: subject({ workspaceId: 'other-workspace' })
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
expect(store.applySnapshot(malformed)).toBe(false)
|
||||
expect(store.getSnapshot()).toEqual(before)
|
||||
})
|
||||
|
||||
it('serializes subject removal keys exactly in tombstones', () => {
|
||||
const { parent, store } = populatedStore()
|
||||
expect(store.applyMutation({ removeParent: parent })).not.toBeNull()
|
||||
expect(store.getSnapshot().tombstones).toContainEqual({
|
||||
entity: 'parent',
|
||||
key: serializeAgentStatusSubject(parent),
|
||||
revision: 2
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,158 @@
|
||||
import { agentChildWorkBelongsTo, type AgentChildWorkRecord } from './agent-status-child-work'
|
||||
import { parseAgentChildWorkRecord } from './agent-status-child-work-codec'
|
||||
import type { AgentStatusStoreSnapshot } from './agent-status-store-contract'
|
||||
import {
|
||||
isAgentStatusStoreEpoch,
|
||||
parseAgentStatusStoreMutation,
|
||||
parseAgentStatusStoreSnapshot
|
||||
} from './agent-status-store-codec'
|
||||
import { applyAgentStatusStoreMutation } from './agent-status-store-mutation'
|
||||
import {
|
||||
parseAgentStatusParentRecord,
|
||||
type AgentStatusParentRecord
|
||||
} from './agent-status-store-parent'
|
||||
import {
|
||||
agentStatusStoreStateFromSnapshot,
|
||||
createEmptyAgentStatusStoreState,
|
||||
deepFreezeAgentStatusStoreValue,
|
||||
snapshotFromAgentStatusStoreState
|
||||
} from './agent-status-store-state'
|
||||
import {
|
||||
parseAgentStatusSubject,
|
||||
serializeAgentStatusSubject,
|
||||
type AgentStatusSubject
|
||||
} from './agent-status-subject'
|
||||
import {
|
||||
parseAgentStatusTransportEnvelope,
|
||||
type AgentStatusMutationEnvelope
|
||||
} from './agent-status-transport-envelope'
|
||||
|
||||
export type AgentStatusStoreMode = 'authority' | 'replica'
|
||||
|
||||
export type AgentStatusStore = {
|
||||
getParent(subject: AgentStatusSubject): AgentStatusParentRecord | null
|
||||
getChildren(subject: AgentStatusSubject): AgentChildWorkRecord[]
|
||||
getSnapshot(): AgentStatusStoreSnapshot
|
||||
applyMutation(mutation: unknown): AgentStatusMutationEnvelope | null
|
||||
applySnapshot(snapshot: unknown): boolean
|
||||
applyTransportEnvelope(envelope: unknown): boolean
|
||||
}
|
||||
|
||||
export type CreateAgentStatusStoreOptions = {
|
||||
epoch: string
|
||||
mode: AgentStatusStoreMode
|
||||
}
|
||||
|
||||
export function createAgentStatusStore(options: CreateAgentStatusStoreOptions): AgentStatusStore {
|
||||
if (!isAgentStatusStoreEpoch(options.epoch)) {
|
||||
throw new Error('Invalid agent status store epoch')
|
||||
}
|
||||
let state = createEmptyAgentStatusStoreState(options.epoch)
|
||||
let snapshotApplied = options.mode === 'authority'
|
||||
|
||||
const store: AgentStatusStore = {
|
||||
getParent(subject) {
|
||||
const parsed = parseAgentStatusSubject(subject)
|
||||
if (!parsed) {
|
||||
return null
|
||||
}
|
||||
const record = state.parents.get(serializeAgentStatusSubject(parsed))
|
||||
return record ? deepFreezeAgentStatusStoreValue(parseAgentStatusParentRecord(record)) : null
|
||||
},
|
||||
getChildren(subject) {
|
||||
const parsed = parseAgentStatusSubject(subject)
|
||||
if (!parsed) {
|
||||
return []
|
||||
}
|
||||
const children = [...state.children.values()]
|
||||
.filter((child) => agentChildWorkBelongsTo(child, parsed))
|
||||
.map((child) => parseAgentChildWorkRecord(child))
|
||||
.filter((child): child is AgentChildWorkRecord => child !== null)
|
||||
return deepFreezeAgentStatusStoreValue(children)
|
||||
},
|
||||
getSnapshot() {
|
||||
return snapshotFromAgentStatusStoreState(state)
|
||||
},
|
||||
applyMutation(value) {
|
||||
if (options.mode !== 'authority') {
|
||||
return null
|
||||
}
|
||||
const mutation = parseAgentStatusStoreMutation(value)
|
||||
if (!mutation || state.revision === Number.MAX_SAFE_INTEGER) {
|
||||
return null
|
||||
}
|
||||
const previousRevision = state.revision
|
||||
const next = applyAgentStatusStoreMutation(state, mutation, previousRevision + 1)
|
||||
if (!next) {
|
||||
return null
|
||||
}
|
||||
state = next
|
||||
return deepFreezeAgentStatusStoreValue({
|
||||
type: 'mutation',
|
||||
epoch: state.epoch,
|
||||
previousRevision,
|
||||
revision: state.revision,
|
||||
mutation
|
||||
})
|
||||
},
|
||||
applySnapshot(value) {
|
||||
const snapshot = parseAgentStatusStoreSnapshot(value)
|
||||
if (!snapshot) {
|
||||
return false
|
||||
}
|
||||
if (options.mode === 'authority') {
|
||||
if (state.revision !== 0) {
|
||||
return false
|
||||
}
|
||||
const restored = agentStatusStoreStateFromSnapshot(snapshot, options.epoch)
|
||||
if (!restored) {
|
||||
return false
|
||||
}
|
||||
state = restored
|
||||
snapshotApplied = true
|
||||
return true
|
||||
}
|
||||
if (
|
||||
snapshotApplied &&
|
||||
snapshot.epoch === state.epoch &&
|
||||
snapshot.revision <= state.revision
|
||||
) {
|
||||
return false
|
||||
}
|
||||
const mirrored = agentStatusStoreStateFromSnapshot(snapshot, snapshot.epoch)
|
||||
if (!mirrored) {
|
||||
return false
|
||||
}
|
||||
state = mirrored
|
||||
snapshotApplied = true
|
||||
return true
|
||||
},
|
||||
applyTransportEnvelope(value) {
|
||||
if (options.mode !== 'replica') {
|
||||
return false
|
||||
}
|
||||
const envelope = parseAgentStatusTransportEnvelope(value)
|
||||
if (!envelope) {
|
||||
return false
|
||||
}
|
||||
if (envelope.type === 'snapshot') {
|
||||
return store.applySnapshot(envelope.snapshot)
|
||||
}
|
||||
if (
|
||||
!snapshotApplied ||
|
||||
envelope.epoch !== state.epoch ||
|
||||
envelope.previousRevision !== state.revision ||
|
||||
envelope.revision !== state.revision + 1
|
||||
) {
|
||||
return false
|
||||
}
|
||||
const next = applyAgentStatusStoreMutation(state, envelope.mutation, envelope.revision)
|
||||
if (!next) {
|
||||
return false
|
||||
}
|
||||
state = next
|
||||
return true
|
||||
}
|
||||
}
|
||||
return store
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createAgentStatusStore } from './agent-status-store'
|
||||
import { AGENT_STATUS_STORE_LIMITS } from './agent-status-store-contract'
|
||||
import { makeStructuredAgentStatusSubject } from './agent-status-subject'
|
||||
import {
|
||||
deserializeAgentStatusTransportEnvelope,
|
||||
serializeAgentStatusTransportEnvelope
|
||||
} from './agent-status-transport-envelope'
|
||||
|
||||
const parent = makeStructuredAgentStatusSubject(
|
||||
{
|
||||
executionHostId: 'local',
|
||||
wslDistro: null,
|
||||
workspaceId: 'workspace-1',
|
||||
workspaceKind: 'git-worktree'
|
||||
},
|
||||
'session_11111111-1111-4111-8111-111111111111'
|
||||
)
|
||||
|
||||
describe('agent status transport envelope', () => {
|
||||
it('requires a snapshot before replay and then accepts only contiguous same-epoch mutations', () => {
|
||||
const authority = createAgentStatusStore({ epoch: 'epoch-a', mode: 'authority' })
|
||||
const first = authority.applyMutation({ parent: { subject: parent, firstObservedAt: 10 } })
|
||||
expect(first).not.toBeNull()
|
||||
|
||||
const replica = createAgentStatusStore({ epoch: 'replica-placeholder', mode: 'replica' })
|
||||
expect(replica.applyTransportEnvelope(first)).toBe(false)
|
||||
expect(
|
||||
replica.applyTransportEnvelope({ type: 'snapshot', snapshot: authority.getSnapshot() })
|
||||
).toBe(true)
|
||||
|
||||
const second = authority.applyMutation({
|
||||
facts: [{ subject: parent, key: 'acknowledged', value: true }]
|
||||
})
|
||||
expect(replica.applyTransportEnvelope(second)).toBe(true)
|
||||
expect(replica.getSnapshot()).toEqual(authority.getSnapshot())
|
||||
expect(replica.applyTransportEnvelope(second)).toBe(false)
|
||||
expect(
|
||||
replica.applyTransportEnvelope({
|
||||
...second,
|
||||
previousRevision: 9,
|
||||
revision: 10
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('adopts a restart snapshot before replay and rejects the predecessor epoch', () => {
|
||||
const firstAuthority = createAgentStatusStore({ epoch: 'epoch-a', mode: 'authority' })
|
||||
firstAuthority.applyMutation({ parent: { subject: parent } })
|
||||
const persisted = firstAuthority.getSnapshot()
|
||||
const replica = createAgentStatusStore({ epoch: 'replica-placeholder', mode: 'replica' })
|
||||
replica.applySnapshot(persisted)
|
||||
const stale = firstAuthority.applyMutation({
|
||||
facts: [{ subject: parent, key: 'unread', value: true }]
|
||||
})
|
||||
|
||||
const restarted = createAgentStatusStore({ epoch: 'epoch-b', mode: 'authority' })
|
||||
expect(restarted.applySnapshot(persisted)).toBe(true)
|
||||
const restartedSnapshot = restarted.getSnapshot()
|
||||
expect(restartedSnapshot.epoch).toBe('epoch-b')
|
||||
expect(replica.applySnapshot(restartedSnapshot)).toBe(true)
|
||||
expect(replica.applyTransportEnvelope(stale)).toBe(false)
|
||||
|
||||
const fresh = restarted.applyMutation({
|
||||
facts: [{ subject: parent, key: 'retained', value: true }]
|
||||
})
|
||||
expect(replica.applyTransportEnvelope(fresh)).toBe(true)
|
||||
expect(replica.getSnapshot().facts.map((fact) => fact.key)).toEqual(['retained'])
|
||||
})
|
||||
|
||||
it('round-trips a mutation envelope and fails closed on malformed or oversized input', () => {
|
||||
const authority = createAgentStatusStore({ epoch: 'epoch-a', mode: 'authority' })
|
||||
const mutation = authority.applyMutation({ parent: { subject: parent } })
|
||||
expect(mutation).not.toBeNull()
|
||||
if (!mutation) {
|
||||
return
|
||||
}
|
||||
|
||||
expect(
|
||||
deserializeAgentStatusTransportEnvelope(serializeAgentStatusTransportEnvelope(mutation))
|
||||
).toEqual(mutation)
|
||||
expect(
|
||||
deserializeAgentStatusTransportEnvelope(
|
||||
JSON.stringify({ ...mutation, revision: mutation.revision + 2 })
|
||||
)
|
||||
).toBeNull()
|
||||
|
||||
const parse = vi.spyOn(JSON, 'parse')
|
||||
expect(
|
||||
deserializeAgentStatusTransportEnvelope(
|
||||
' '.repeat(AGENT_STATUS_STORE_LIMITS.serializedBytes + 1)
|
||||
)
|
||||
).toBeNull()
|
||||
expect(parse).not.toHaveBeenCalled()
|
||||
parse.mockRestore()
|
||||
})
|
||||
|
||||
it('does not resurrect an exact removal through stale replay', () => {
|
||||
const authority = createAgentStatusStore({ epoch: 'epoch-a', mode: 'authority' })
|
||||
const insertion = authority.applyMutation({ parent: { subject: parent } })
|
||||
const replica = createAgentStatusStore({ epoch: 'replica-placeholder', mode: 'replica' })
|
||||
expect(replica.applySnapshot(authority.getSnapshot())).toBe(true)
|
||||
const removal = authority.applyMutation({ removeParent: parent })
|
||||
|
||||
expect(replica.applyTransportEnvelope(removal)).toBe(true)
|
||||
expect(replica.getParent(parent)).toBeNull()
|
||||
expect(replica.applyTransportEnvelope(insertion)).toBe(false)
|
||||
expect(replica.getParent(parent)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
AGENT_STATUS_STORE_LIMITS,
|
||||
type AgentStatusStoreMutation,
|
||||
type AgentStatusStoreSnapshot
|
||||
} from './agent-status-store-contract'
|
||||
import {
|
||||
isAgentStatusStoreEpoch,
|
||||
parseAgentStatusStoreMutation,
|
||||
parseAgentStatusStoreSnapshot
|
||||
} from './agent-status-store-codec'
|
||||
import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit'
|
||||
import { measureUtf8ByteLength } from './utf8-byte-limits'
|
||||
|
||||
const ENVELOPE_STRUCTURE_LIMITS = {
|
||||
structuralTokens: 512 * 1024,
|
||||
nestingDepth: 40
|
||||
} as const
|
||||
|
||||
export type AgentStatusSnapshotEnvelope = {
|
||||
type: 'snapshot'
|
||||
snapshot: AgentStatusStoreSnapshot
|
||||
}
|
||||
|
||||
export type AgentStatusMutationEnvelope = {
|
||||
type: 'mutation'
|
||||
epoch: string
|
||||
previousRevision: number
|
||||
revision: number
|
||||
mutation: AgentStatusStoreMutation
|
||||
}
|
||||
|
||||
export type AgentStatusTransportEnvelope = AgentStatusSnapshotEnvelope | AgentStatusMutationEnvelope
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
function hasExactKeys(record: Record<string, unknown>, expected: readonly string[]): boolean {
|
||||
const keys = Object.keys(record)
|
||||
return keys.length === expected.length && keys.every((key) => expected.includes(key))
|
||||
}
|
||||
|
||||
function isRevision(value: unknown): value is number {
|
||||
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0
|
||||
}
|
||||
|
||||
export function parseAgentStatusTransportEnvelope(
|
||||
value: unknown
|
||||
): AgentStatusTransportEnvelope | null {
|
||||
if (!isRecord(value)) {
|
||||
return null
|
||||
}
|
||||
if (value.type === 'snapshot' && hasExactKeys(value, ['type', 'snapshot'])) {
|
||||
const snapshot = parseAgentStatusStoreSnapshot(value.snapshot)
|
||||
return snapshot ? { type: 'snapshot', snapshot } : null
|
||||
}
|
||||
if (
|
||||
value.type !== 'mutation' ||
|
||||
!hasExactKeys(value, ['type', 'epoch', 'previousRevision', 'revision', 'mutation']) ||
|
||||
!isAgentStatusStoreEpoch(value.epoch) ||
|
||||
!isRevision(value.previousRevision) ||
|
||||
!isRevision(value.revision) ||
|
||||
value.revision !== value.previousRevision + 1
|
||||
) {
|
||||
return null
|
||||
}
|
||||
const mutation = parseAgentStatusStoreMutation(value.mutation)
|
||||
return mutation
|
||||
? {
|
||||
type: 'mutation',
|
||||
epoch: value.epoch,
|
||||
previousRevision: value.previousRevision,
|
||||
revision: value.revision,
|
||||
mutation
|
||||
}
|
||||
: null
|
||||
}
|
||||
|
||||
export function serializeAgentStatusTransportEnvelope(
|
||||
envelope: AgentStatusTransportEnvelope
|
||||
): string {
|
||||
const parsed = parseAgentStatusTransportEnvelope(envelope)
|
||||
if (!parsed) {
|
||||
throw new Error('Invalid agent status transport envelope')
|
||||
}
|
||||
const serialized = JSON.stringify(parsed)
|
||||
if (
|
||||
measureUtf8ByteLength(serialized, {
|
||||
stopAfterBytes: AGENT_STATUS_STORE_LIMITS.serializedBytes
|
||||
}).exceededLimit
|
||||
) {
|
||||
throw new Error('Agent status transport envelope exceeds its serialized-byte limit')
|
||||
}
|
||||
return serialized
|
||||
}
|
||||
|
||||
export function deserializeAgentStatusTransportEnvelope(
|
||||
serialized: string
|
||||
): AgentStatusTransportEnvelope | null {
|
||||
if (
|
||||
measureUtf8ByteLength(serialized, {
|
||||
stopAfterBytes: AGENT_STATUS_STORE_LIMITS.serializedBytes
|
||||
}).exceededLimit
|
||||
) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
assertJsonTextStructureWithinLimits(serialized, ENVELOPE_STRUCTURE_LIMITS)
|
||||
return parseAgentStatusTransportEnvelope(JSON.parse(serialized))
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user