mirror of
https://github.com/stablyai/orca.git
synced 2026-11-03 10:37:35 +00:00
* fix(agent-status): preserve WSL Codex questions and child completion * test(agent-status): route remote Codex question test through relay envelopes * fix(agent-status): scope remote Codex roster removals to the reporting relay session - Unlabelled relayed polls retire only children the current relay link listed; each WSL link starts a new session, so a restarted guest's partial roster cannot delete children an earlier guest reported. - One removal rule: an explicit list, a done aggregate, or an unshed absent roster proves removals; a shed roster with live work keeps the children. Drops the old done-main-agent clear that wiped shed rosters. - An unshed poll with no roster now retires the last child while the root works. - Share the Codex hook event-to-state rule between local and relayed paths.
159 lines
4.5 KiB
TypeScript
159 lines
4.5 KiB
TypeScript
import {
|
||
AGENT_MODEL_MAX_LENGTH,
|
||
AGENT_STATUS_TOOL_INPUT_MAX_LENGTH,
|
||
AGENT_TYPE_MAX_LENGTH,
|
||
type AgentSubagentSnapshot
|
||
} from './agent-status-types'
|
||
import { normalizeOptionalField } from './agent-status-field-normalization'
|
||
import { readCodexSubagentIdentity } from './codex-subagent-identity'
|
||
import {
|
||
agentChildWorkLiveness,
|
||
type AgentChildWorkLiveness
|
||
} from './agent-status-child-work-liveness'
|
||
|
||
const CODEX_SUBAGENT_ID_MAX_LENGTH = 64
|
||
|
||
export type CodexSubagentRoster = Map<string, TrackedCodexSubagent>
|
||
|
||
type TrackedCodexSubagent = {
|
||
agentType?: string
|
||
description?: string
|
||
model?: string
|
||
state: 'working' | 'waiting'
|
||
startedAt: number
|
||
identityTranscriptPath?: string
|
||
/** Relay session whose own roster listed this child; only that session may retire it by omission. */
|
||
reportedBy?: string
|
||
}
|
||
|
||
export function upsertCodexSubagent(
|
||
roster: CodexSubagentRoster,
|
||
id: string,
|
||
fields: {
|
||
agentType?: string
|
||
description?: string
|
||
model?: string
|
||
state: 'working' | 'waiting'
|
||
},
|
||
now: number
|
||
): void {
|
||
const normalizedId = id.trim()
|
||
if (normalizedId.length === 0 || normalizedId.length > CODEX_SUBAGENT_ID_MAX_LENGTH) {
|
||
return
|
||
}
|
||
const agentType = normalizeOptionalField(fields.agentType, AGENT_TYPE_MAX_LENGTH)
|
||
const description = normalizeOptionalField(fields.description, AGENT_STATUS_TOOL_INPUT_MAX_LENGTH)
|
||
const model = normalizeOptionalField(fields.model, AGENT_MODEL_MAX_LENGTH)
|
||
const existing = roster.get(normalizedId)
|
||
if (existing) {
|
||
existing.agentType = agentType ?? existing.agentType
|
||
existing.description = description ?? existing.description
|
||
existing.model = model ?? existing.model
|
||
existing.state = fields.state
|
||
return
|
||
}
|
||
roster.set(normalizedId, {
|
||
agentType,
|
||
description,
|
||
model,
|
||
state: fields.state,
|
||
startedAt: now
|
||
})
|
||
}
|
||
|
||
export function finishCodexSubagent(roster: CodexSubagentRoster, id: string): void {
|
||
roster.delete(id.trim())
|
||
}
|
||
|
||
export function enrichCodexSubagentIdentity(
|
||
roster: CodexSubagentRoster,
|
||
id: string,
|
||
transcriptPath: string | undefined
|
||
): void {
|
||
const child = roster.get(id.trim())
|
||
if (!child || !transcriptPath || child.identityTranscriptPath === transcriptPath) {
|
||
return
|
||
}
|
||
const identity = readCodexSubagentIdentity(transcriptPath, id.trim())
|
||
if (!identity) {
|
||
return
|
||
}
|
||
child.identityTranscriptPath = transcriptPath
|
||
child.description =
|
||
normalizeOptionalField(identity.description, AGENT_STATUS_TOOL_INPUT_MAX_LENGTH) ??
|
||
child.description
|
||
}
|
||
|
||
/** Update only an existing child’s model, preserving lifecycle so late discovery cannot resurrect finished children. */
|
||
export function setCodexSubagentModel(
|
||
roster: CodexSubagentRoster,
|
||
id: string,
|
||
model: string | undefined
|
||
): void {
|
||
const normalizedModel = normalizeOptionalField(model, AGENT_MODEL_MAX_LENGTH)
|
||
if (!normalizedModel) {
|
||
return
|
||
}
|
||
const existing = roster.get(id.trim())
|
||
if (!existing) {
|
||
return
|
||
}
|
||
existing.model = normalizedModel
|
||
}
|
||
|
||
export function seedCodexSubagentRoster(
|
||
roster: CodexSubagentRoster,
|
||
snapshots: readonly AgentSubagentSnapshot[],
|
||
reportedBy?: string
|
||
): void {
|
||
for (const snapshot of snapshots) {
|
||
if (snapshot.state !== 'working' && snapshot.state !== 'waiting') {
|
||
continue
|
||
}
|
||
upsertCodexSubagent(
|
||
roster,
|
||
snapshot.id,
|
||
{
|
||
agentType: snapshot.agentType,
|
||
description: snapshot.description,
|
||
model: snapshot.model,
|
||
state: snapshot.state
|
||
},
|
||
snapshot.startedAt
|
||
)
|
||
const tracked = reportedBy ? roster.get(snapshot.id.trim()) : undefined
|
||
if (tracked) {
|
||
tracked.reportedBy = reportedBy
|
||
}
|
||
}
|
||
}
|
||
|
||
export function codexRosterToSnapshots(
|
||
roster: CodexSubagentRoster | undefined
|
||
): AgentSubagentSnapshot[] | undefined {
|
||
if (!roster || roster.size === 0) {
|
||
return undefined
|
||
}
|
||
const snapshots = Array.from(roster, ([id, tracked]) => ({
|
||
id,
|
||
agentType: tracked.agentType,
|
||
description: tracked.description,
|
||
model: tracked.model,
|
||
state: tracked.state,
|
||
startedAt: tracked.startedAt
|
||
}))
|
||
snapshots.sort((a, b) => a.startedAt - b.startedAt || a.id.localeCompare(b.id, 'en'))
|
||
return snapshots
|
||
}
|
||
|
||
/** Project spawned agent threads through the shared kind policy: waiting/working child evidence, never watch loops. */
|
||
export function codexRosterChildWorkLiveness(
|
||
roster: CodexSubagentRoster | undefined
|
||
): AgentChildWorkLiveness {
|
||
return agentChildWorkLiveness(
|
||
roster
|
||
? Array.from(roster.values(), (tracked) => ({ kind: 'agent' as const, state: tracked.state }))
|
||
: undefined
|
||
)
|
||
}
|