mirror of
https://github.com/stablyai/orca.git
synced 2026-10-09 00:02:39 +00:00
* feat(agent-status): route structured status through canonical ownership and fence child lifetimes Restacked onto the canonical store and child-work contract. Completing that restack drops the `reopenStructuredParent` mutation flag this change had carried, along with its contract field, its codec branch, and its single call site in structured ingest, which passed a hardcoded `true`. The flag was a narrow escape hatch from the absolute `tombstones.has(...)` rule that governed parent upserts in this branch's original base. The canonical store replaces that rule with a revision envelope, because a bounded store compacts tombstones away and a presence-based guard silently stops fencing once one is evicted. With the envelope deciding the outcome, the escape hatch has nothing left to escape from, so removing it changes no production behaviour. `agent-status-store-reopen.test.ts` is rewritten against the envelope: the reopen case now pins that an unflagged republication succeeds while replay from before the reopen stays fenced even after the parent tombstone is compacted away, and the second case pins where the guard genuinely bites — a republication inside the removing mutation itself, for every subject kind. * fix(agent-status): re-admit unchanged structured owners after teardown * fix(agent-status): clear anti-slop object-param and Reflect.apply findings - agent-status-store-byte-budget.ts: type the byte-budget helper's record parameter as the union of what its call sites actually pass (the snapshot header plus each store entity record) instead of the broad `object`. - server-structured-canonical-status.test.ts: replace `Reflect.apply` with a typed, explicitly-bound call that models a caller at an untyped boundary omitting the trusted owner subject. * docs(agent-status): drop the 2A progress doc from docs/reference docs/reference/ holds implementation detail, not rollout progress. The canonical-boundary notes move to the effort's working directory; the agent-status-store status section keeps the boundary statement and loses the now-dangling link. * fix(agent-status): mint the canonical epoch on first use, not at construction The hook server's canonical store was built in an instance-member initializer, so constructing AgentHookServer — which happens at import time for the module singleton — demanded a live randomUUID. Any importer that stubs node:crypto threw 'Invalid agent status store epoch' before a single test ran. The store is now created on first canonical access and reset by dropping it, so construction owes nothing to a crypto implementation and the epoch still rotates per authority incarnation. * fix(agent-status): drop the orphaned snapshot budget and a duplicated pane guard Two leftovers from the canonical-store routing change: agent-status-store-snapshot-budget.ts lost its only caller when the store state switched to agentStatusStoreFitsByteBudget. Nothing in the repo imports it now, so the module goes with the caller it existed for. The replacement is not a straight copy: it only memoises a record's measured size once the record is frozen, so a still-mutable record can no longer return a stale byte count. persistedStructuredWorkerPaneKeyIsValid repeated its public-pane-key rejection verbatim three lines below the first one. The tests covering that rejection pass on the first occurrence alone, so the second decided nothing and only obscured which predicate was load-bearing. * fix(agent-status): stop a failed structured publish from latching as owned Three defects found reviewing the structured routing path. combinedStatusEntries defaulted a missing listing order to 0, but the counter it compares against starts at 1, so any unordered row sorted above every ordered one. Unknown order now sorts last. The owner map recorded a session as owned before the sink ran. A publish that threw therefore left matchesLocation reporting an owned location for a row that was never written, and the unchanged-projection path — the only thing that would re-offer it — stopped. The address still has to survive a throw so teardown can forget a row that did land, so the two facts are now separate: the address is recorded up front, and only a publish that returned marks the row as landed. The reopen test claimed the revision envelope rather than the tombstone fences a stale replay. It cannot tell: transport consecutiveness, the parent-revision validator and the tombstone guard each refuse that replay alone, and ablating any two leaves the test green. It now asserts the outcome and says so.
147 lines
5.8 KiB
TypeScript
147 lines
5.8 KiB
TypeScript
import { makeStructuredAgentStatusSubject } from '../../shared/agent-status-subject'
|
|
import { collectRuntimeWorktreeAgentSources } from './runtime-worktree-agent-sources'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { attachRuntimeWorktreeAgentRows } from './runtime-worktree-agent-rows'
|
|
import {
|
|
structuredAgentSessionPaneKey,
|
|
structuredAgentSessionTabId
|
|
} from '../../shared/structured-agent-session-projection'
|
|
import type { AgentSessionStatusSummary } from '../../shared/agent-session-wire'
|
|
import type { RuntimeWorktreePsSummary } from '../../shared/runtime-types'
|
|
import { AgentHookServer, _internals } from '../agent-hooks/server'
|
|
|
|
vi.mock('../telemetry/client', () => ({ track: vi.fn() }))
|
|
vi.mock('../telemetry/cohort-classifier', () => ({
|
|
getCohortAtEmit: vi.fn(() => ({ nth_repo_added: 2 }))
|
|
}))
|
|
|
|
/**
|
|
* A structured session has no PTY and no hook script, so the host publishes its projection into
|
|
* the agent-status store itself. This walks that store into `worktree ps` rows: before it, the CLI
|
|
* reported a worktree running one as idle while the sidebar showed it working.
|
|
*/
|
|
const WORKTREE_ID = 'repo-1::/workspace/app'
|
|
const SESSION = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'
|
|
const SUBJECT = makeStructuredAgentStatusSubject(
|
|
{
|
|
executionHostId: 'local',
|
|
wslDistro: null,
|
|
workspaceId: WORKTREE_ID,
|
|
workspaceKind: 'git-worktree'
|
|
},
|
|
SESSION
|
|
)
|
|
|
|
function summary(over: Partial<AgentSessionStatusSummary> = {}): AgentSessionStatusSummary {
|
|
return {
|
|
sessionId: SESSION,
|
|
workspaceId: WORKTREE_ID,
|
|
agent: 'claude',
|
|
status: 'working',
|
|
latestPrompt: 'ship the thing',
|
|
updatedAt: 1_757_030_400_000,
|
|
hostExecutionOwned: true,
|
|
...over
|
|
} as AgentSessionStatusSummary
|
|
}
|
|
|
|
function attach(summaries: AgentSessionStatusSummary[]): RuntimeWorktreePsSummary {
|
|
const store = new AgentHookServer()
|
|
for (const entry of summaries) {
|
|
store.ingestStructuredStatus(entry, SUBJECT)
|
|
}
|
|
const row = {
|
|
worktreeId: WORKTREE_ID,
|
|
status: 'inactive',
|
|
hasHostSidebarActivity: false,
|
|
agents: []
|
|
} as unknown as RuntimeWorktreePsSummary
|
|
const summariesById = new Map<string, RuntimeWorktreePsSummary>([[WORKTREE_ID, row]])
|
|
attachRuntimeWorktreeAgentRows({
|
|
summaries: summariesById,
|
|
pathIndex: { byPath: new Map(), byRealPath: new Map() } as never,
|
|
missingWorktreeIds: new Set(),
|
|
workingTerminalEvidenceByWorktreeId: new Map(),
|
|
rowSources: collectRuntimeWorktreeAgentSources({
|
|
mirroredWorktreeIdByTabId: new Map(),
|
|
connectedPtyEvidence: {
|
|
tabIds: new Set(),
|
|
paneKeys: new Set(),
|
|
ptyIdByTerminalHandle: new Map()
|
|
},
|
|
hookSnapshots: store.getStatusSnapshot()
|
|
}),
|
|
orchestrationByPaneKey: null,
|
|
getSummary: (map, _p, _m, id) => map.get(id) ?? null
|
|
})
|
|
return row
|
|
}
|
|
|
|
beforeEach(() => {
|
|
_internals.resetCachesForTests()
|
|
})
|
|
|
|
describe('worktree ps reports structured sessions', () => {
|
|
it('a busy structured session is not reported idle', () => {
|
|
const row = attach([summary()])
|
|
expect(row.agents).toHaveLength(1)
|
|
expect(row.agents[0]?.state).toBe('working')
|
|
expect(row.agents[0]?.agentType).toBe('claude')
|
|
expect(row.agents[0]?.prompt).toBe('ship the thing')
|
|
expect(row.status).toBe('working')
|
|
})
|
|
|
|
// The same projection the sidebar applies, so the two surfaces cannot disagree about one session.
|
|
it('maps attention to blocked and idle to done', () => {
|
|
expect(attach([summary({ status: 'attention' })]).agents[0]?.state).toBe('blocked')
|
|
expect(attach([summary({ status: 'idle' })]).agents[0]?.state).toBe('done')
|
|
})
|
|
|
|
it('does not turn a completed host-held session into permission', () => {
|
|
const row = attach([summary({ status: 'idle' })])
|
|
expect(row.status).toBe('inactive')
|
|
expect(row.hasHostSidebarActivity).toBe(false)
|
|
})
|
|
|
|
it('reports the DERIVED pane key, never an orchestration credential', () => {
|
|
const row = attach([summary()])
|
|
expect(row.agents[0]?.paneKey).toBe(
|
|
structuredAgentSessionPaneKey(structuredAgentSessionTabId(SESSION), SESSION)
|
|
)
|
|
})
|
|
|
|
// Null status means no turn has been persisted; the chat itself shows nothing, so neither does this.
|
|
it('omits a session with no projected status', () => {
|
|
expect(attach([summary({ status: null })]).agents).toHaveLength(0)
|
|
})
|
|
|
|
it('keeps the journal clock on the row, so a restart republish is not new activity', () => {
|
|
const row = attach([summary()])
|
|
expect(row.agents[0]?.updatedAt).toBe(1_757_030_400_000)
|
|
expect(row.agents[0]?.stateStartedAt).toBe(1_757_030_400_000)
|
|
})
|
|
})
|
|
|
|
/**
|
|
* The deliberate non-goal. Adding structured rows to `terminal list` was investigated and rejected:
|
|
* mobile mounts a terminal WebView per row that can never receive a frame, a `connected`-keyed
|
|
* refresh check goes permanently true and pins shipped clients to a fast cadence with no exit, and
|
|
* the plugin projection has no field that can carry `writable: false`. Every SAFE consumer of a
|
|
* terminal summary checks `ptyId`; the breaking ones key off `connected` or mere row presence,
|
|
* which no added field can qualify. This pins that the listing never reads the status store.
|
|
*/
|
|
describe('terminal listing is deliberately left alone', () => {
|
|
it('never reads the agent-status store that now carries structured rows', async () => {
|
|
const { readFile } = await import('node:fs/promises')
|
|
// orca-runtime-subscribe-to-terminal-resize.ts owns listTerminals.
|
|
const listing = await readFile(
|
|
new URL('./orca-runtime-subscribe-to-terminal-resize.ts', import.meta.url),
|
|
'utf8'
|
|
)
|
|
// Guard the guard: an empty read would make every assertion below vacuously true.
|
|
expect(listing).toContain('async listTerminals(')
|
|
expect(listing).not.toContain('getAgentStatusSnapshotFn')
|
|
expect(listing).not.toContain('structuredHost')
|
|
})
|
|
})
|