mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
Nest chat-UI orchestration workers under the agent that started them (#34601)
* Nest chat-UI orchestration workers under the agent that started them Main's lineage projection only walked terminal panes, so workers running in the chat UI never got a parent link and listed flat in the sidebar. It now also projects chat workers, keyed by their visible chat row, and resolves a chat creator or coordinator to that chat's visible row instead of its handle or private mailbox pane key. * Fix public worker lineage across assignment and row identities * Complete terminal lineage provenance and bound active session selection * Repair lineage test runtime boundaries and SQLite counters * Batch public lineage verification after exact recency selection * Use checked runtime values in lineage integration tests * Document the trusted Dispatch SQL row boundary * Keep lineage integration imports within their test programs * Load native lineage fixtures before test execution * test: complete lineage inputs in runtime and sidebar fixtures * test: seed benchmark facts from active dispatches * test: replace partial orchestration DB casts with typed fixtures * test: complete stale-leaf mailbox fixture session input * fix(test): render worker lineage fixture with production list
This commit is contained in:
@@ -3,6 +3,7 @@ import original from '../mobile/vitest.config.ts'
|
||||
|
||||
// Keep real process, native, and runtime-specific contracts on Node.
|
||||
const nodeFiles = [
|
||||
'src/components/WorktreeAgentList.runtime-lineage.test.tsx',
|
||||
'scripts/check-tests-typecheck-incremental-cache.test.ts',
|
||||
'src/tasks/mobile-agent-launch-architecture.test.ts',
|
||||
'src/tasks/agent-launch-mobile-replay.test.ts',
|
||||
|
||||
@@ -13,6 +13,10 @@ const ERROR_LINE = /^(\S.*?)\(\d+,\d+\): error TS\d+:/
|
||||
|
||||
// Require every on-disk test in program except these explicit exclusions: error-only diff cannot see excluded/shadowed tests.
|
||||
export const TESTS_OUTSIDE_PROGRAM = new Map([
|
||||
[
|
||||
'src/components/WorktreeAgentList.runtime-lineage.test.tsx',
|
||||
'Node-side: renders the mobile consumer of real desktop SQLite assignment projection, against Node rather than RN libs'
|
||||
],
|
||||
[
|
||||
'src/session/mobile-native-command-discovery.test.tsx',
|
||||
'Node-side: imports the desktop ACP command discovery source, checked against @types/node rather than RN libs'
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import { createElement } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { expect, it, vi } from 'vitest'
|
||||
import type { RuntimeWorktreeAgentRow } from '../../../src/shared/runtime-types'
|
||||
import { WorktreeAgentSummary } from './WorktreeAgentSummary'
|
||||
import { WorktreeAgentList } from './WorktreeAgentList'
|
||||
|
||||
vi.mock('react-native', () => ({
|
||||
Pressable: 'Pressable',
|
||||
StyleSheet: { create: <T,>(styles: T) => styles },
|
||||
Text: 'Text',
|
||||
View: 'View'
|
||||
}))
|
||||
vi.mock('lucide-react-native', () => ({
|
||||
ChevronDown: 'ChevronDown',
|
||||
ChevronRight: 'ChevronRight'
|
||||
}))
|
||||
vi.mock('./AgentStateDot', () => ({ AgentStateDot: () => null }))
|
||||
vi.mock('./MobileAgentIcon', () => ({ MobileAgentIcon: () => null }))
|
||||
|
||||
it('renders production-built canonical assignments as nested rows and groups only roots', async () => {
|
||||
const clock = vi.spyOn(Date, 'now').mockReturnValue(1_800_000_000_000)
|
||||
const { publicLineageFixture, canonicalChatRow, LINEAGE_PARENT, LINEAGE_CHILD } =
|
||||
await import('../../../src/main/runtime/runtime-agent-lineage.test-fixture')
|
||||
const { flattenAgentRowLineage } = await import('../worktree/agent-row-lineage')
|
||||
const f = publicLineageFixture()
|
||||
let tree: ReactTestRenderer | undefined
|
||||
try {
|
||||
const agents = f.build().agents
|
||||
expect(flattenAgentRowLineage(agents).map((node) => [node.row.paneKey, node.depth])).toEqual([
|
||||
[canonicalChatRow(LINEAGE_PARENT), 0],
|
||||
[canonicalChatRow(LINEAGE_CHILD), 1]
|
||||
])
|
||||
await act(async () => {
|
||||
tree = create(
|
||||
createElement(WorktreeAgentList, {
|
||||
agents,
|
||||
now: 1_800_000_000_000,
|
||||
unvisited: false,
|
||||
statusLive: true
|
||||
})
|
||||
)
|
||||
})
|
||||
expect(
|
||||
tree?.root
|
||||
.findAll((node) => node.props.agent !== undefined)
|
||||
.map((node) => [node.props.agent.paneKey, node.props.depth])
|
||||
).toEqual([
|
||||
[canonicalChatRow(LINEAGE_PARENT), 0],
|
||||
[canonicalChatRow(LINEAGE_CHILD), 1]
|
||||
])
|
||||
expect(tree?.root.findAllByType(WorktreeAgentSummary)).toHaveLength(0)
|
||||
expect(agents[1].taskTitle).toBe('Build the worker result')
|
||||
expect(JSON.stringify(tree?.toJSON())).toContain('Coordinate work')
|
||||
expect(JSON.stringify(tree?.toJSON())).toContain('Implement work')
|
||||
expect(JSON.stringify(tree?.toJSON())).not.toContain('2 agents')
|
||||
expect(JSON.stringify(tree?.toJSON())).not.toContain(f.privateHandle)
|
||||
expect(JSON.stringify(tree?.toJSON())).not.toContain(f.privatePane)
|
||||
const orphan = {
|
||||
...agents[1],
|
||||
paneKey: 'missing-parent-worker',
|
||||
parentPaneKey: 'missing-parent'
|
||||
}
|
||||
await act(async () =>
|
||||
tree?.update(
|
||||
createElement(WorktreeAgentList, {
|
||||
agents: [...agents, orphan],
|
||||
now: 1_800_000_000_000,
|
||||
unvisited: false
|
||||
})
|
||||
)
|
||||
)
|
||||
expect(
|
||||
tree?.root
|
||||
.findByType(WorktreeAgentSummary)
|
||||
.props.agents.map((row: RuntimeWorktreeAgentRow) => row.paneKey)
|
||||
).toEqual([canonicalChatRow(LINEAGE_PARENT), orphan.paneKey])
|
||||
} finally {
|
||||
await act(async () => tree?.unmount())
|
||||
f.db.close()
|
||||
clock.mockRestore()
|
||||
}
|
||||
})
|
||||
@@ -10,6 +10,7 @@
|
||||
// are React Native's, where `setTimeout` answers a number rather than a NodeJS.Timeout, so
|
||||
// pulling that graph in reports ~280 errors about the desktop rather than about mobile. Their
|
||||
// own runtime coverage is vitest, which runs them under Node.
|
||||
"src/components/WorktreeAgentList.runtime-lineage.test.tsx",
|
||||
"src/session/mobile-native-command-discovery.test.tsx",
|
||||
"src/tasks/agent-launch-mobile-replay.test.ts",
|
||||
"src/test-support/rpc-recording/recorded-request-params.test.ts",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import type * as ChildProcess from 'node:child_process'
|
||||
|
||||
const {
|
||||
callMock,
|
||||
@@ -35,16 +36,38 @@ vi.mock('./runtime/environments', () => ({
|
||||
resolveEnvironment: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('child_process', async () => {
|
||||
vi.mock('child_process', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof ChildProcess>()
|
||||
const { createChildProcessModuleMock } = await import('./index-test-harness.js')
|
||||
return createChildProcessModuleMock(spawnMock)
|
||||
return { ...actual, ...createChildProcessModuleMock(spawnMock) }
|
||||
})
|
||||
|
||||
import { main } from './index'
|
||||
import type {
|
||||
RuntimeAgentLineageTestModule,
|
||||
RuntimeAgentLineageCatalogTestModule
|
||||
} from '../shared/runtime-agent-lineage-consumer.test-fixture'
|
||||
import { COMMAND_SPECS } from './specs'
|
||||
import { okFixture, queueFixtures } from './test-fixtures'
|
||||
import { pairRuntimeEnvironment, useWorktreeAwarenessEnvironment } from './index-test-harness'
|
||||
|
||||
let lineage: RuntimeAgentLineageTestModule
|
||||
let catalog: RuntimeAgentLineageCatalogTestModule
|
||||
|
||||
// The CLI's Node16 program needs a hook; each test still creates its own DB and runtime.
|
||||
beforeAll(async () => {
|
||||
lineage = (
|
||||
await vi.importActual<{ default: RuntimeAgentLineageTestModule }>(
|
||||
'../main/runtime/runtime-agent-lineage.test-fixture'
|
||||
)
|
||||
).default
|
||||
catalog = (
|
||||
await vi.importActual<{ default: RuntimeAgentLineageCatalogTestModule }>(
|
||||
'../main/runtime/runtime-agent-lineage-catalog.test-fixture'
|
||||
)
|
||||
).default
|
||||
})
|
||||
|
||||
const TERMINAL_ROW = {
|
||||
handle: 'term_1',
|
||||
ptyId: 'pty-1',
|
||||
@@ -71,6 +94,64 @@ describe('omittedHostIds selector annotation', () => {
|
||||
spawnMock
|
||||
})
|
||||
|
||||
it.each(['repo::/workspace', 'folder:lineage'])(
|
||||
'prints canonical production-built lineage through actual CLI and worktree RPC for %s',
|
||||
async (worktreeId) => {
|
||||
const { publicLineageFixture, canonicalChatRow, LINEAGE_PARENT, LINEAGE_CHILD } = lineage
|
||||
const f = publicLineageFixture(worktreeId)
|
||||
try {
|
||||
const dispatcher = catalog.createLineageCatalogDispatcher(f.build)
|
||||
const resultOf = (response: unknown) => JSON.parse(JSON.stringify(response)).result
|
||||
const request = (params = {}) =>
|
||||
dispatcher.dispatch({
|
||||
id: 'req_worktree_ps',
|
||||
authToken: 'test',
|
||||
method: 'worktree.ps',
|
||||
params
|
||||
})
|
||||
const legacy = await request()
|
||||
expect(legacy).toMatchObject({
|
||||
ok: true,
|
||||
result: {
|
||||
worktrees: [
|
||||
{
|
||||
agents: [
|
||||
{ parentPaneKey: null },
|
||||
{
|
||||
parentPaneKey: canonicalChatRow(LINEAGE_PARENT),
|
||||
taskTitle: 'Build the worker result'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
callMock.mockResolvedValueOnce(legacy)
|
||||
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
|
||||
await main(['worktree', 'ps', '--json'], '/workspace')
|
||||
const printed = JSON.parse(String(log.mock.calls[0]?.[0]))
|
||||
expect(printed.result.worktrees[0].agents[1].paneKey).toBe(canonicalChatRow(LINEAGE_CHILD))
|
||||
expect(JSON.stringify(printed)).not.toContain(f.privateHandle)
|
||||
expect(JSON.stringify(printed)).not.toContain(f.privatePane)
|
||||
const first = resultOf(await request({ afterSnapshotId: null }))
|
||||
expect(resultOf(await request({ afterSnapshotId: first.snapshotId }))).toMatchObject({
|
||||
unchanged: true
|
||||
})
|
||||
f.db.db
|
||||
.prepare(
|
||||
'UPDATE runs SET coordinator_orca_session_id = NULL, coordinator_orca_session_id_generation = NULL WHERE id = ?'
|
||||
)
|
||||
.run(f.run.id)
|
||||
expect(resultOf(await request({ afterSnapshotId: first.snapshotId })).snapshotId).not.toBe(
|
||||
first.snapshotId
|
||||
)
|
||||
log.mockRestore()
|
||||
} finally {
|
||||
f.db.close()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('marks a stale runtime host that no caller can select', async () => {
|
||||
pairRuntimeEnvironment(listEnvironmentsMock, 'env-paired', 'm4air')
|
||||
queueFixtures(
|
||||
|
||||
@@ -44,6 +44,9 @@ import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
|
||||
import { mainListsEveryPane } from '../terminal/surface/main-terminal-surfaces'
|
||||
import { TUI_IDLE_QUIESCENCE_MS } from '../orca-runtime-postlude'
|
||||
import { screenClearsBlockedTail } from '../blocked-tail-screen-clear'
|
||||
import { conversationRecordReader } from '../orchestration/structured-session-lineage'
|
||||
import { getStructuredAgentSessionHost } from '../../native-chat/agent-session-wire/structured-agent-session-registry'
|
||||
import { peekOpenedAgentSessionRecordStore } from '../agent-session-record-store-slot'
|
||||
|
||||
export abstract class OrcaRuntimeWithStopRequestedPtyIds extends OrcaRuntimeWithRuntimeId {
|
||||
protected readonly stopRequestedPtyIds = new Set<string>()
|
||||
@@ -124,7 +127,12 @@ export abstract class OrcaRuntimeWithStopRequestedPtyIds extends OrcaRuntimeWith
|
||||
getHandleForPaneKey: (paneKey) => this.getTerminalHandleForPaneKey(paneKey),
|
||||
getPaneKey: (handle) => this.getPaneKeyForTerminalHandle(handle),
|
||||
getDispatchAuthority: (handle) => this.getOrchestrationDispatchAuthority(handle),
|
||||
getAgentStatusSnapshot: () => this.getOrchestrationFleetAgentStatusSnapshot()
|
||||
getAgentStatusSnapshot: () => this.getOrchestrationFleetAgentStatusSnapshot(),
|
||||
getSessionRecords: () => {
|
||||
const store =
|
||||
getStructuredAgentSessionHost()?.deps.store ?? peekOpenedAgentSessionRecordStore()
|
||||
return store ? conversationRecordReader(store) : null
|
||||
}
|
||||
})
|
||||
|
||||
protected readonly terminalList = new RuntimeTerminalList({
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { DatabaseSync } from 'node:sqlite'
|
||||
import { vi } from 'vitest'
|
||||
import type SyncDatabase from '../sqlite/sync-database'
|
||||
import type { SqliteBindings } from '../sqlite/sqlite-statement'
|
||||
import type { OrchestrationDb } from './orchestration/db'
|
||||
|
||||
// SQL predicate visits complement range plans/probe counts; they are not total SQLite VM steps.
|
||||
export function installSqlVisitCounters(counters: Record<string, () => number>) {
|
||||
const initialized = new WeakSet<DatabaseSync>()
|
||||
const prepare = DatabaseSync.prototype.prepare
|
||||
return vi.spyOn(DatabaseSync.prototype, 'prepare').mockImplementation(function (
|
||||
this: DatabaseSync,
|
||||
sql
|
||||
) {
|
||||
if (!initialized.has(this)) {
|
||||
for (const [name, visit] of Object.entries(counters)) {
|
||||
this.function(name, {}, (_rowid: unknown) => visit())
|
||||
}
|
||||
initialized.add(this)
|
||||
}
|
||||
return prepare.call(this, sql)
|
||||
})
|
||||
}
|
||||
|
||||
export function traceDispatchPredicateVisits(
|
||||
db: SyncDatabase,
|
||||
functions: { candidate: string; latest?: string; observation?: string }
|
||||
) {
|
||||
const prepare = db.prepare.bind(db)
|
||||
const executions: { query: string; parameters: SqliteBindings; rows: number }[] = []
|
||||
const observed = new WeakSet<object>()
|
||||
const trace = vi.spyOn(db, 'prepare').mockImplementation((query) => {
|
||||
let instrumented = query.replaceAll(
|
||||
'WHERE assignee_orca_session_id IS NOT NULL',
|
||||
`WHERE assignee_orca_session_id IS NOT NULL AND ${functions.candidate}(rowid)`
|
||||
)
|
||||
if (functions.latest && query.includes('rowid AS n')) {
|
||||
instrumented = instrumented.replace(
|
||||
'ORDER BY rowid DESC LIMIT 1',
|
||||
`AND ${functions.latest}(rowid) ORDER BY rowid DESC LIMIT 1`
|
||||
)
|
||||
}
|
||||
if (functions.observation) {
|
||||
instrumented = instrumented.replace(
|
||||
'WHERE dispatch_id IN',
|
||||
`WHERE ${functions.observation}(rowid) AND dispatch_id IN`
|
||||
)
|
||||
}
|
||||
const statement = prepare(instrumented)
|
||||
if (!observed.has(statement)) {
|
||||
observed.add(statement)
|
||||
const all = statement.all.bind(statement)
|
||||
const get = statement.get.bind(statement)
|
||||
vi.spyOn(statement, 'all').mockImplementation((...args) => {
|
||||
const rows = all(...args)
|
||||
executions.push({ query, parameters: args, rows: rows.length })
|
||||
return rows
|
||||
})
|
||||
vi.spyOn(statement, 'get').mockImplementation((...args) => {
|
||||
const row = get(...args)
|
||||
executions.push({ query, parameters: args, rows: row ? 1 : 0 })
|
||||
return row
|
||||
})
|
||||
}
|
||||
return statement
|
||||
})
|
||||
return Object.assign(trace, { executions })
|
||||
}
|
||||
|
||||
export function latestStatementMetrics(
|
||||
trace: ReturnType<typeof traceDispatchPredicateVisits>,
|
||||
native: ReturnType<typeof installSqlVisitCounters>
|
||||
) {
|
||||
const latest = (query: string) => query.includes('rowid AS n FROM dispatch_contexts')
|
||||
const executions = trace.executions.filter(({ query }) => latest(query))
|
||||
return {
|
||||
cachedPrepareCalls: trace.mock.calls.filter(([query]) => latest(query)).length,
|
||||
executedStatements: executions.length,
|
||||
nativeCompilations: native.mock.calls.filter(([query]) => latest(query)).length,
|
||||
addressEntries: executions.reduce((sum, { parameters }) => {
|
||||
const addresses: unknown =
|
||||
typeof parameters[0] === 'string' ? JSON.parse(parameters[0]) : null
|
||||
if (!Array.isArray(addresses) || !addresses.every((value) => typeof value === 'string')) {
|
||||
throw new Error('Expected actual JSON address batch')
|
||||
}
|
||||
return sum + addresses.length
|
||||
}, 0),
|
||||
returnedVerificationRows: executions.reduce((sum, { rows }) => sum + rows, 0)
|
||||
}
|
||||
}
|
||||
|
||||
export function dispatchQueryPlans(
|
||||
db: SyncDatabase,
|
||||
trace: ReturnType<typeof traceDispatchPredicateVisits>
|
||||
) {
|
||||
const queries = new Map(
|
||||
trace.executions
|
||||
.filter(
|
||||
({ query }) =>
|
||||
query.includes('idx_dispatch_session_presentation') ||
|
||||
query.includes('rowid AS n FROM dispatch_contexts')
|
||||
)
|
||||
.map(({ query, parameters }) => [query, parameters])
|
||||
)
|
||||
return [...queries].map(([query, parameters]) => ({
|
||||
query,
|
||||
parameters,
|
||||
plan: db.prepare(`EXPLAIN QUERY PLAN ${query}`).all(...parameters)
|
||||
}))
|
||||
}
|
||||
|
||||
export function seedCoarsePresentationDispatches(
|
||||
db: OrchestrationDb,
|
||||
runId: string,
|
||||
count: number,
|
||||
now: number,
|
||||
prefix = 'coarse'
|
||||
) {
|
||||
if (!count) {
|
||||
return
|
||||
}
|
||||
const task = db.createTask({ runId, spec: 'coarse-only history' })
|
||||
db.db
|
||||
.prepare(`INSERT INTO dispatch_contexts
|
||||
(id, run_id, task_id, assignee_handle, assignee_orca_session_id, status, completed_at)
|
||||
WITH RECURSIVE seq(n) AS (VALUES(1) UNION ALL SELECT n+1 FROM seq WHERE n < ?)
|
||||
SELECT ? || n, ?, ?, CASE WHEN n % 2 = 1 THEN ? || n WHEN n % 4 = 0 THEN '' ELSE NULL END,
|
||||
? || n, 'completed', ? FROM seq`)
|
||||
.run(
|
||||
count,
|
||||
`${prefix}-id-`,
|
||||
runId,
|
||||
task.id,
|
||||
`${prefix}-handle-`,
|
||||
`${prefix}-session-`,
|
||||
new Date(now - 7_200_000).toISOString()
|
||||
)
|
||||
}
|
||||
@@ -227,6 +227,10 @@ export class InMemoryOrchestrationMessages {
|
||||
return [...this.runs.values()].find((run) => run.coordinator_pane_key === paneKey)
|
||||
}
|
||||
|
||||
listSessionDispatchesForPresentation(): never[] {
|
||||
return []
|
||||
}
|
||||
|
||||
listWorkerTerminalReleaseBacklog(): never[] {
|
||||
return []
|
||||
}
|
||||
|
||||
@@ -215,6 +215,7 @@ function makePostRevealWorkerRecoveryHarness(
|
||||
)
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This recovery fixture reaches only the listed planner, terminal settlement and authority lookup methods.
|
||||
runtime.setOrchestrationDb({
|
||||
listSessionDispatchesForPresentation: () => [],
|
||||
reconcileMissingWorkerTerminal: vi.fn(),
|
||||
getActiveDispatchForTerminal: () => undefined,
|
||||
listLegacyWorkerTerminalRecoveryRows: () => [
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import {
|
||||
makeRuntimeOrchestrationDb,
|
||||
dispatchContextRow,
|
||||
coordinatorRunRow,
|
||||
taskLineageRow,
|
||||
runRow
|
||||
} from '../runtime-orchestration-db.test-fixture'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
MOCK_GIT_WORKTREES,
|
||||
@@ -208,15 +215,20 @@ describe('OrcaRuntimeService', () => {
|
||||
const runtime = new OrcaRuntimeService(runtimeStore as never)
|
||||
const workerHandle = runtime.preAllocateHandleForPty('pty-worker')
|
||||
const coordinatorHandle = runtime.preAllocateHandleForPty('pty-coordinator')
|
||||
runtime.setOrchestrationDb({
|
||||
getActiveDispatchForTerminal: vi.fn(() => ({
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'getActiveDispatchForTerminal').mockImplementation(() =>
|
||||
dispatchContextRow({
|
||||
task_id: 'task-1'
|
||||
})),
|
||||
getActiveCoordinatorRun: vi.fn(() => ({
|
||||
})
|
||||
)
|
||||
vi.spyOn(orchestrationDb, 'getActiveCoordinatorRun').mockImplementation(() =>
|
||||
coordinatorRunRow({
|
||||
id: 'run-1',
|
||||
coordinator_handle: coordinatorHandle
|
||||
}))
|
||||
} as never)
|
||||
})
|
||||
)
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
runtime.attachWindow(1)
|
||||
runtime.syncWindowGraph(1, {
|
||||
tabs: [
|
||||
@@ -310,44 +322,54 @@ describe('OrcaRuntimeService', () => {
|
||||
const coordinatorPaneKey = makePaneKey('tab-coordinator', coordinatorLeafId)
|
||||
const workerHandle = runtime.preAllocateHandleForPty('pty-worker')
|
||||
const coordinatorHandle = runtime.preAllocateHandleForPty('pty-coordinator')
|
||||
runtime.setOrchestrationDb({
|
||||
getActiveDispatchForTerminal: vi.fn((handle: string) =>
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'getActiveDispatchForTerminal').mockImplementation(
|
||||
(handle: string) =>
|
||||
handle === workerHandle
|
||||
? {
|
||||
? dispatchContextRow({
|
||||
id: 'ctx-1',
|
||||
run_id: 'run-1',
|
||||
task_id: 'task-1',
|
||||
assignee_handle: workerHandle,
|
||||
status: 'dispatched'
|
||||
}
|
||||
})
|
||||
: undefined
|
||||
),
|
||||
getLatestDispatchForTerminal: vi.fn((handle: string) =>
|
||||
)
|
||||
vi.spyOn(orchestrationDb, 'getLatestDispatchForTerminal').mockImplementation(
|
||||
(handle: string) =>
|
||||
handle === workerHandle
|
||||
? {
|
||||
? dispatchContextRow({
|
||||
id: 'ctx-done',
|
||||
run_id: 'run-1',
|
||||
task_id: 'task-done',
|
||||
assignee_handle: workerHandle,
|
||||
status: 'completed',
|
||||
completed_at: new Date(Date.now()).toISOString()
|
||||
}
|
||||
})
|
||||
: undefined
|
||||
),
|
||||
getTask: vi.fn(() => ({
|
||||
)
|
||||
vi.spyOn(orchestrationDb, 'getTask').mockImplementation(() =>
|
||||
taskLineageRow({
|
||||
id: 'task-1',
|
||||
run_id: 'run-1',
|
||||
task_title: 'Dispatch prompt work',
|
||||
display_name: 'Review dispatch prompts and make worker labels distinct',
|
||||
spec: 'Review dispatch prompts\n\nand make worker labels distinct',
|
||||
created_by_terminal_handle: coordinatorHandle
|
||||
})),
|
||||
getRun: vi.fn(() => ({
|
||||
})
|
||||
)
|
||||
vi.spyOn(orchestrationDb, 'getRun').mockImplementation(() =>
|
||||
runRow({
|
||||
id: 'run-1',
|
||||
coordinator_handle: coordinatorHandle,
|
||||
coordinator_orca_session_id: null,
|
||||
coordinator_orca_session_id_generation: null,
|
||||
consumer_generation: 0,
|
||||
legacy: 0
|
||||
}))
|
||||
} as never)
|
||||
})
|
||||
)
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
runtime.attachWindow(1)
|
||||
|
||||
const result = runtime.syncWindowGraph(1, {
|
||||
@@ -410,32 +432,38 @@ describe('OrcaRuntimeService', () => {
|
||||
const workerPaneKey = makePaneKey('tab-worker', workerLeafId)
|
||||
const workerHandle = runtime.preAllocateHandleForPty('pty-worker')
|
||||
const coordinatorHandle = runtime.preAllocateHandleForPty('pty-coordinator')
|
||||
const getActiveCoordinatorRun = vi.fn(() => ({
|
||||
id: 'run-legacy-unrelated',
|
||||
coordinator_handle: coordinatorHandle
|
||||
}))
|
||||
runtime.setOrchestrationDb({
|
||||
getActiveDispatchForTerminal: vi.fn((handle: string) =>
|
||||
const getActiveCoordinatorRun = vi.fn(() =>
|
||||
coordinatorRunRow({
|
||||
id: 'run-legacy-unrelated',
|
||||
coordinator_handle: coordinatorHandle
|
||||
})
|
||||
)
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'getActiveDispatchForTerminal').mockImplementation(
|
||||
(handle: string) =>
|
||||
handle === workerHandle
|
||||
? {
|
||||
? dispatchContextRow({
|
||||
id: 'ctx-missing-run',
|
||||
run_id: dispatchRunId,
|
||||
task_id: 'task-missing-run',
|
||||
assignee_handle: workerHandle,
|
||||
status: 'dispatched'
|
||||
}
|
||||
})
|
||||
: undefined
|
||||
),
|
||||
getLatestDispatchForTerminal: vi.fn(() => undefined),
|
||||
getTask: vi.fn(() => ({
|
||||
)
|
||||
vi.spyOn(orchestrationDb, 'getLatestDispatchForTerminal').mockImplementation(() => undefined)
|
||||
vi.spyOn(orchestrationDb, 'getTask').mockImplementation(() =>
|
||||
taskLineageRow({
|
||||
id: 'task-missing-run',
|
||||
run_id: taskRunId,
|
||||
spec: 'modern task without proven Run',
|
||||
created_by_terminal_handle: coordinatorHandle
|
||||
})),
|
||||
getRun: vi.fn(() => undefined),
|
||||
getActiveCoordinatorRun
|
||||
} as never)
|
||||
})
|
||||
)
|
||||
vi.spyOn(orchestrationDb, 'getRun').mockImplementation(() => undefined)
|
||||
vi.spyOn(orchestrationDb, 'getActiveCoordinatorRun').mockImplementation(getActiveCoordinatorRun)
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
runtime.attachWindow(1)
|
||||
|
||||
const result = runtime.syncWindowGraph(1, {
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
import {
|
||||
makeRuntimeOrchestrationDb,
|
||||
dispatchContextRow,
|
||||
taskLineageRow,
|
||||
runRow,
|
||||
coordinatorRunRow
|
||||
} from '../runtime-orchestration-db.test-fixture'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
AGENT_STATUS_STALE_AFTER_MS,
|
||||
@@ -460,31 +467,40 @@ describe('OrcaRuntimeService', () => {
|
||||
const coordinatorPaneKey = makePaneKey('tab-coordinator', coordinatorLeafId)
|
||||
const workerHandle = runtime.preAllocateHandleForPty('pty-worker')
|
||||
const coordinatorHandle = runtime.preAllocateHandleForPty('pty-coordinator')
|
||||
runtime.setOrchestrationDb({
|
||||
getActiveDispatchForTerminal: vi.fn(() => undefined),
|
||||
getLatestDispatchForTerminal: vi.fn((handle: string) =>
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'getActiveDispatchForTerminal').mockImplementation(() => undefined)
|
||||
vi.spyOn(orchestrationDb, 'getLatestDispatchForTerminal').mockImplementation(
|
||||
(handle: string) =>
|
||||
handle === workerHandle
|
||||
? {
|
||||
? dispatchContextRow({
|
||||
id: 'ctx-done',
|
||||
run_id: 'run-1',
|
||||
task_id: 'task-done',
|
||||
assignee_handle: workerHandle,
|
||||
status: 'completed',
|
||||
completed_at: new Date(Date.now()).toISOString()
|
||||
}
|
||||
})
|
||||
: undefined
|
||||
),
|
||||
getTask: vi.fn(() => ({
|
||||
)
|
||||
vi.spyOn(orchestrationDb, 'getTask').mockImplementation(() =>
|
||||
taskLineageRow({
|
||||
id: 'task-done',
|
||||
run_id: 'run-1',
|
||||
created_by_terminal_handle: coordinatorHandle
|
||||
})),
|
||||
getRun: vi.fn(() => ({
|
||||
})
|
||||
)
|
||||
vi.spyOn(orchestrationDb, 'getRun').mockImplementation(() =>
|
||||
runRow({
|
||||
id: 'run-1',
|
||||
coordinator_handle: coordinatorHandle,
|
||||
coordinator_orca_session_id: null,
|
||||
coordinator_orca_session_id_generation: null,
|
||||
consumer_generation: 0,
|
||||
legacy: 0
|
||||
}))
|
||||
} as never)
|
||||
})
|
||||
)
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
runtime.attachWindow(1)
|
||||
|
||||
const result = runtime.syncWindowGraph(1, {
|
||||
@@ -538,27 +554,33 @@ describe('OrcaRuntimeService', () => {
|
||||
const workerLeafId = '55555555-5555-4555-8555-555555555555'
|
||||
const workerPaneKey = makePaneKey('tab-worker', workerLeafId)
|
||||
const workerHandle = runtime.preAllocateHandleForPty('pty-worker')
|
||||
runtime.setOrchestrationDb({
|
||||
getActiveDispatchForTerminal: vi.fn(() => undefined),
|
||||
getLatestDispatchForTerminal: vi.fn((handle: string) =>
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'getActiveDispatchForTerminal').mockImplementation(() => undefined)
|
||||
vi.spyOn(orchestrationDb, 'getLatestDispatchForTerminal').mockImplementation(
|
||||
(handle: string) =>
|
||||
handle === workerHandle
|
||||
? {
|
||||
? dispatchContextRow({
|
||||
id: 'ctx-done',
|
||||
task_id: 'task-done',
|
||||
assignee_handle: workerHandle,
|
||||
status: 'completed',
|
||||
completed_at: new Date(Date.now()).toISOString()
|
||||
}
|
||||
})
|
||||
: undefined
|
||||
),
|
||||
getTask: vi.fn(() => ({
|
||||
)
|
||||
vi.spyOn(orchestrationDb, 'getTask').mockImplementation(() =>
|
||||
taskLineageRow({
|
||||
id: 'task-done'
|
||||
})),
|
||||
getActiveCoordinatorRun: vi.fn(() => ({
|
||||
})
|
||||
)
|
||||
vi.spyOn(orchestrationDb, 'getActiveCoordinatorRun').mockImplementation(() =>
|
||||
coordinatorRunRow({
|
||||
id: 'run-unrelated',
|
||||
coordinator_handle: 'term_unrelated'
|
||||
}))
|
||||
} as never)
|
||||
})
|
||||
)
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
runtime.attachWindow(1)
|
||||
|
||||
const result = runtime.syncWindowGraph(1, {
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
makeRuntimeOrchestrationDb,
|
||||
dispatchContextRow,
|
||||
taskLineageRow,
|
||||
coordinatorRunRow
|
||||
} from '../runtime-orchestration-db.test-fixture'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
AGENT_STATUS_STALE_AFTER_MS,
|
||||
@@ -28,21 +34,28 @@ describe('OrcaRuntimeService', () => {
|
||||
const workerLeafId = '66666666-6666-4666-8666-666666666666'
|
||||
const workerPaneKey = makePaneKey('tab-worker', workerLeafId)
|
||||
const workerHandle = runtime.preAllocateHandleForPty('pty-worker')
|
||||
const getActiveCoordinatorRun = vi.fn(() => ({
|
||||
id: 'run-unrelated',
|
||||
coordinator_handle: 'term_unrelated'
|
||||
}))
|
||||
runtime.setOrchestrationDb({
|
||||
getActiveDispatchForTerminal: vi.fn(() => undefined),
|
||||
getLatestDispatchForTerminal: vi.fn(() => ({
|
||||
const getActiveCoordinatorRun = vi.fn(() =>
|
||||
coordinatorRunRow({
|
||||
id: 'run-unrelated',
|
||||
coordinator_handle: 'term_unrelated'
|
||||
})
|
||||
)
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'getActiveDispatchForTerminal').mockImplementation(() => undefined)
|
||||
vi.spyOn(orchestrationDb, 'getLatestDispatchForTerminal').mockImplementation(() =>
|
||||
dispatchContextRow({
|
||||
id: 'ctx-settled',
|
||||
task_id: 'task-settled',
|
||||
assignee_handle: workerHandle,
|
||||
status: dispatchStatus,
|
||||
completed_at: new Date(Date.now()).toISOString()
|
||||
})),
|
||||
})
|
||||
)
|
||||
vi.spyOn(orchestrationDb, 'getActiveCoordinatorRun').mockImplementation(
|
||||
getActiveCoordinatorRun
|
||||
} as never)
|
||||
)
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
runtime.attachWindow(1)
|
||||
|
||||
const result = runtime.syncWindowGraph(1, {
|
||||
@@ -80,20 +93,22 @@ describe('OrcaRuntimeService', () => {
|
||||
const runtime = new OrcaRuntimeService(store)
|
||||
const workerLeafId = '77777777-7777-4777-8777-777777777777'
|
||||
const workerHandle = runtime.preAllocateHandleForPty('pty-worker')
|
||||
runtime.setOrchestrationDb({
|
||||
getActiveDispatchForTerminal: vi.fn(() => undefined),
|
||||
getLatestDispatchForTerminal: vi.fn((handle: string) =>
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'getActiveDispatchForTerminal').mockImplementation(() => undefined)
|
||||
vi.spyOn(orchestrationDb, 'getLatestDispatchForTerminal').mockImplementation(
|
||||
(handle: string) =>
|
||||
handle === workerHandle
|
||||
? {
|
||||
? dispatchContextRow({
|
||||
id: 'ctx-stale',
|
||||
task_id: 'task-stale',
|
||||
assignee_handle: workerHandle,
|
||||
status: 'completed',
|
||||
completed_at: new Date(Date.now() - AGENT_STATUS_STALE_AFTER_MS - 1).toISOString()
|
||||
}
|
||||
})
|
||||
: undefined
|
||||
)
|
||||
} as never)
|
||||
)
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
runtime.attachWindow(1)
|
||||
|
||||
const result = runtime.syncWindowGraph(1, {
|
||||
@@ -257,13 +272,16 @@ describe('OrcaRuntimeService', () => {
|
||||
}
|
||||
const runtime = new OrcaRuntimeService(runtimeStore as never)
|
||||
const workerHandle = runtime.preAllocateHandleForPty('pty-worker')
|
||||
runtime.setOrchestrationDb({
|
||||
getDispatchContext: vi.fn(() => ({
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'getDispatchContext').mockImplementation(() =>
|
||||
dispatchContextRow({
|
||||
task_id: 'task_abc123',
|
||||
assignee_handle: workerHandle,
|
||||
status: 'completed'
|
||||
}))
|
||||
} as never)
|
||||
})
|
||||
)
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
runtime.attachWindow(1)
|
||||
runtime.syncWindowGraph(1, {
|
||||
tabs: [
|
||||
@@ -355,13 +373,16 @@ describe('OrcaRuntimeService', () => {
|
||||
}
|
||||
const runtime = new OrcaRuntimeService(runtimeStore as never)
|
||||
const parentHandle = runtime.preAllocateHandleForPty('pty-parent')
|
||||
runtime.setOrchestrationDb({
|
||||
getDispatchContext: vi.fn(() => undefined),
|
||||
getTask: vi.fn(() => ({
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'getDispatchContext').mockImplementation(() => undefined)
|
||||
vi.spyOn(orchestrationDb, 'getTask').mockImplementation(() =>
|
||||
taskLineageRow({
|
||||
id: 'task_creator123',
|
||||
created_by_terminal_handle: parentHandle
|
||||
}))
|
||||
} as never)
|
||||
})
|
||||
)
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
runtime.attachWindow(1)
|
||||
runtime.syncWindowGraph(1, {
|
||||
tabs: [
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { makeRuntimeOrchestrationDb } from '../runtime-orchestration-db.test-fixture'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { makeAgentStatusStoreWiring } from '../agent-status-store-wiring.test-fixture'
|
||||
import { OrcaRuntimeService, listWorktrees } from '../orca-runtime-test-mocks.spec'
|
||||
import {
|
||||
OrcaRuntimeService,
|
||||
createRootDispatch,
|
||||
listWorktrees
|
||||
} from '../orca-runtime-test-mocks.spec'
|
||||
import {
|
||||
HEADLESS_LEAF_ID,
|
||||
TEST_FOLDER_PROJECT_GROUP_ID,
|
||||
@@ -456,26 +461,20 @@ describe('OrcaRuntimeService', () => {
|
||||
]
|
||||
})
|
||||
const workerHandle = runtime.preAllocateHandleForPty('pty-1')
|
||||
runtime.setOrchestrationDb({
|
||||
getActiveDispatchForTerminal: vi.fn((handle: string) =>
|
||||
handle === workerHandle
|
||||
? {
|
||||
id: 'ctx-1',
|
||||
task_id: 'task-1',
|
||||
assignee_handle: workerHandle,
|
||||
status: 'dispatched'
|
||||
}
|
||||
: undefined
|
||||
),
|
||||
getLatestDispatchForTerminal: vi.fn(() => undefined),
|
||||
getTask: vi.fn(() => ({
|
||||
id: 'task-1',
|
||||
task_title: 'Dispatch prompt work',
|
||||
display_name: 'Review dispatch prompts and make worker labels distinct',
|
||||
spec: 'Review dispatch prompts\n\nand make worker labels distinct'
|
||||
})),
|
||||
getActiveCoordinatorRun: vi.fn(() => undefined)
|
||||
} as never)
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
const run = orchestrationDb.createRun({
|
||||
objective: 'Hook summary projection',
|
||||
coordinatorHandle: null,
|
||||
coordinatorPaneKey: null
|
||||
})
|
||||
const task = orchestrationDb.createTask({
|
||||
runId: run.id,
|
||||
taskTitle: 'Dispatch prompt work',
|
||||
displayName: 'Review dispatch prompts and make worker labels distinct',
|
||||
spec: 'Review dispatch prompts\n\nand make worker labels distinct'
|
||||
})
|
||||
createRootDispatch(orchestrationDb, task.id, workerHandle, paneKey)
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
runtime.attachWindow(1)
|
||||
runtime.syncWindowGraph(1, {
|
||||
tabs: [
|
||||
|
||||
+12
-8
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
makeRuntimeOrchestrationDb,
|
||||
dispatchContextRow
|
||||
} from '../runtime-orchestration-db.test-fixture'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
OrcaRuntimeService,
|
||||
@@ -9,7 +13,6 @@ import {
|
||||
setTerminalViewAttributes,
|
||||
tmpdir
|
||||
} from '../orca-runtime-test-mocks.spec'
|
||||
import type { OrchestrationDb } from '../orchestration/db'
|
||||
import { getTerminalViewerColors } from '../terminal-view-attribute-store'
|
||||
import type {
|
||||
TerminalViewAttributes,
|
||||
@@ -112,14 +115,15 @@ describe('OrcaRuntimeService', () => {
|
||||
retireAgentHookCompatibilityAuthority: retireAuthority
|
||||
}
|
||||
)
|
||||
runtime.setOrchestrationDb({
|
||||
getActiveDispatchForTerminal: (handle: string) =>
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'getActiveDispatchForTerminal').mockImplementation(
|
||||
(handle: string) =>
|
||||
handle === 'term_ssh_retained'
|
||||
? { id: 'dispatch-ssh', task_id: 'task-ssh', status: 'dispatched' }
|
||||
: undefined,
|
||||
failDispatch,
|
||||
getActiveCoordinatorRun: () => undefined
|
||||
} as unknown as OrchestrationDb)
|
||||
? dispatchContextRow({ id: 'dispatch-ssh', task_id: 'task-ssh', status: 'dispatched' })
|
||||
: undefined
|
||||
)
|
||||
vi.spyOn(orchestrationDb, 'failDispatch').mockImplementation(failDispatch)
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
runtime.attachWindow(TEST_WINDOW_ID)
|
||||
runtime.syncWindowGraph(TEST_WINDOW_ID, {
|
||||
tabs: [
|
||||
|
||||
+19
-18
@@ -1,3 +1,4 @@
|
||||
import { makeRuntimeOrchestrationDb } from '../runtime-orchestration-db.test-fixture'
|
||||
import { withDurableRuntimeStore } from '../runtime-durable-store-fixture'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
|
||||
@@ -6,7 +7,6 @@ import {
|
||||
getDefaultWorkspaceSession,
|
||||
makePaneKey
|
||||
} from '../orca-runtime-test-mocks.spec'
|
||||
import type { OrchestrationDb } from '../orchestration/db'
|
||||
import type { WorkspaceSessionState } from '../orca-runtime-test-mocks.spec'
|
||||
import {
|
||||
HEADLESS_LEAF_ID,
|
||||
@@ -469,23 +469,24 @@ describe('OrcaRuntimeService', () => {
|
||||
: null
|
||||
}
|
||||
)
|
||||
runtime.setOrchestrationDb({
|
||||
getActiveDispatchForTerminal: () => undefined,
|
||||
listLegacyWorkerTerminalRecoveryRows: () => [
|
||||
{
|
||||
dispatch_id: 'dispatch-legacy',
|
||||
task_id: 'task-legacy',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_legacy',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `pty-legacy:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_WORKTREE_ID,
|
||||
agent_terminal_handle: 'term_legacy'
|
||||
}
|
||||
]
|
||||
} as unknown as OrchestrationDb)
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'getActiveDispatchForTerminal').mockImplementation(() => undefined)
|
||||
vi.spyOn(orchestrationDb, 'listLegacyWorkerTerminalRecoveryRows').mockImplementation(() => [
|
||||
{
|
||||
dispatch_id: 'dispatch-legacy',
|
||||
task_id: 'task-legacy',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_legacy',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `pty-legacy:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_WORKTREE_ID,
|
||||
agent_terminal_handle: 'term_legacy'
|
||||
}
|
||||
])
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
const write = vi.fn(() => true)
|
||||
const kill = vi.fn(() => true)
|
||||
const READY_SCREEN =
|
||||
|
||||
+36
-34
@@ -1,8 +1,8 @@
|
||||
import { makeRuntimeOrchestrationDb } from '../runtime-orchestration-db.test-fixture'
|
||||
import { withDurableRuntimeStore } from '../runtime-durable-store-fixture'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
|
||||
import { OrcaRuntimeService, getDefaultWorkspaceSession } from '../orca-runtime-test-mocks.spec'
|
||||
import type { OrchestrationDb } from '../orchestration/db'
|
||||
import type { WorkspaceSessionState } from '../orca-runtime-test-mocks.spec'
|
||||
import {
|
||||
HEADLESS_LEAF_ID,
|
||||
@@ -46,23 +46,24 @@ describe('OrcaRuntimeService', () => {
|
||||
undefined,
|
||||
{ canRecoverPersistentLocalPtys: () => true }
|
||||
)
|
||||
runtime.setOrchestrationDb({
|
||||
getActiveDispatchForTerminal: () => undefined,
|
||||
listLegacyWorkerTerminalRecoveryRows: () => [
|
||||
{
|
||||
dispatch_id: 'dispatch-reveal-retry',
|
||||
task_id: 'task-reveal-retry',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_reveal_retry',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `pty-reveal-retry:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_WORKTREE_ID,
|
||||
agent_terminal_handle: 'term_reveal_retry'
|
||||
}
|
||||
]
|
||||
} as unknown as OrchestrationDb)
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'getActiveDispatchForTerminal').mockImplementation(() => undefined)
|
||||
vi.spyOn(orchestrationDb, 'listLegacyWorkerTerminalRecoveryRows').mockImplementation(() => [
|
||||
{
|
||||
dispatch_id: 'dispatch-reveal-retry',
|
||||
task_id: 'task-reveal-retry',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_reveal_retry',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `pty-reveal-retry:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_WORKTREE_ID,
|
||||
agent_terminal_handle: 'term_reveal_retry'
|
||||
}
|
||||
])
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
runtime.setPtyController({
|
||||
write: vi.fn(() => true),
|
||||
kill: vi.fn(() => true),
|
||||
@@ -461,22 +462,23 @@ describe('OrcaRuntimeService', () => {
|
||||
undefined,
|
||||
{ canRecoverPersistentLocalPtys: () => true }
|
||||
)
|
||||
runtime.setOrchestrationDb({
|
||||
listLegacyWorkerTerminalRecoveryRows: () => [
|
||||
{
|
||||
dispatch_id: 'dispatch-persistence-failure',
|
||||
task_id: 'task-persistence-failure',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_persistence_failure',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `pty-persistence-failure:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_WORKTREE_ID,
|
||||
agent_terminal_handle: 'term_persistence_failure'
|
||||
}
|
||||
]
|
||||
} as unknown as OrchestrationDb)
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'listLegacyWorkerTerminalRecoveryRows').mockImplementation(() => [
|
||||
{
|
||||
dispatch_id: 'dispatch-persistence-failure',
|
||||
task_id: 'task-persistence-failure',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_persistence_failure',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `pty-persistence-failure:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_WORKTREE_ID,
|
||||
agent_terminal_handle: 'term_persistence_failure'
|
||||
}
|
||||
])
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
runtime.setPtyController({
|
||||
write: vi.fn(() => true),
|
||||
kill: vi.fn(() => true),
|
||||
|
||||
+19
-16
@@ -1,3 +1,4 @@
|
||||
import { makeRuntimeOrchestrationDb } from '../runtime-orchestration-db.test-fixture'
|
||||
import { withDurableRuntimeStore } from '../runtime-durable-store-fixture'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
@@ -279,6 +280,7 @@ describe('OrcaRuntimeService', () => {
|
||||
)
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This isolated recovery path reaches only the supplied planner and missing-terminal settlement methods.
|
||||
runtime.setOrchestrationDb({
|
||||
listSessionDispatchesForPresentation: () => [],
|
||||
reconcileMissingWorkerTerminal: vi.fn(),
|
||||
listLegacyWorkerTerminalRecoveryRows: () => [
|
||||
{
|
||||
@@ -390,22 +392,23 @@ describe('OrcaRuntimeService', () => {
|
||||
undefined,
|
||||
{ canRecoverPersistentLocalPtys: () => true }
|
||||
)
|
||||
runtime.setOrchestrationDb({
|
||||
listLegacyWorkerTerminalRecoveryRows: () => [
|
||||
{
|
||||
dispatch_id: 'dispatch-inventory-unavailable',
|
||||
task_id: 'task-inventory-unavailable',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_inventory_unavailable',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `pty-inventory-unavailable:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_WORKTREE_ID,
|
||||
agent_terminal_handle: 'term_inventory_unavailable'
|
||||
}
|
||||
]
|
||||
} as unknown as OrchestrationDb)
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'listLegacyWorkerTerminalRecoveryRows').mockImplementation(() => [
|
||||
{
|
||||
dispatch_id: 'dispatch-inventory-unavailable',
|
||||
task_id: 'task-inventory-unavailable',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_inventory_unavailable',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `pty-inventory-unavailable:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_WORKTREE_ID,
|
||||
agent_terminal_handle: 'term_inventory_unavailable'
|
||||
}
|
||||
])
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
const listProcesses = vi
|
||||
.fn()
|
||||
.mockRejectedValueOnce(new Error('local provider unavailable'))
|
||||
|
||||
+36
-32
@@ -1,3 +1,4 @@
|
||||
import { makeRuntimeOrchestrationDb } from '../runtime-orchestration-db.test-fixture'
|
||||
import { withDurableRuntimeStore } from '../runtime-durable-store-fixture'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
@@ -79,6 +80,7 @@ describe('OrcaRuntimeService', () => {
|
||||
)
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This isolated recovery path reaches only the supplied planner and missing-terminal settlement methods.
|
||||
runtime.setOrchestrationDb({
|
||||
listSessionDispatchesForPresentation: () => [],
|
||||
reconcileMissingWorkerTerminal: vi.fn(),
|
||||
listLegacyWorkerTerminalRecoveryRows: () =>
|
||||
cases.map(({ name, leafId, terminalHandle }) => ({
|
||||
@@ -201,22 +203,23 @@ describe('OrcaRuntimeService', () => {
|
||||
undefined,
|
||||
{ canRecoverPersistentLocalPtys: () => true }
|
||||
)
|
||||
runtime.setOrchestrationDb({
|
||||
listLegacyWorkerTerminalRecoveryRows: () => [
|
||||
{
|
||||
dispatch_id: 'dispatch-folder',
|
||||
task_id: 'task-folder',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_folder',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `pty-folder-legacy:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_FOLDER_WORKSPACE_KEY,
|
||||
agent_terminal_handle: 'term_folder'
|
||||
}
|
||||
]
|
||||
} as unknown as OrchestrationDb)
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'listLegacyWorkerTerminalRecoveryRows').mockImplementation(() => [
|
||||
{
|
||||
dispatch_id: 'dispatch-folder',
|
||||
task_id: 'task-folder',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_folder',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `pty-folder-legacy:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_FOLDER_WORKSPACE_KEY,
|
||||
agent_terminal_handle: 'term_folder'
|
||||
}
|
||||
])
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
const listProcesses = vi.fn(async () => [
|
||||
{
|
||||
id: 'pty-folder-legacy',
|
||||
@@ -329,22 +332,23 @@ describe('OrcaRuntimeService', () => {
|
||||
undefined,
|
||||
{ canRecoverPersistentLocalPtys: () => true }
|
||||
)
|
||||
runtime.setOrchestrationDb({
|
||||
listLegacyWorkerTerminalRecoveryRows: () => [
|
||||
{
|
||||
dispatch_id: 'dispatch-ssh-folder',
|
||||
task_id: 'task-ssh-folder',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_ssh_folder',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `${ptyId}:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_FOLDER_WORKSPACE_KEY,
|
||||
agent_terminal_handle: 'term_ssh_folder'
|
||||
}
|
||||
]
|
||||
} as unknown as OrchestrationDb)
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'listLegacyWorkerTerminalRecoveryRows').mockImplementation(() => [
|
||||
{
|
||||
dispatch_id: 'dispatch-ssh-folder',
|
||||
task_id: 'task-ssh-folder',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_ssh_folder',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `${ptyId}:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_FOLDER_WORKSPACE_KEY,
|
||||
agent_terminal_handle: 'term_ssh_folder'
|
||||
}
|
||||
])
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
const listProcesses = vi.fn(async () => [
|
||||
{
|
||||
id: ptyId,
|
||||
|
||||
+35
-33
@@ -1,3 +1,4 @@
|
||||
import { makeRuntimeOrchestrationDb } from '../runtime-orchestration-db.test-fixture'
|
||||
import { withDurableRuntimeStore } from '../runtime-durable-store-fixture'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
@@ -5,7 +6,6 @@ import {
|
||||
getDefaultWorkspaceSession,
|
||||
setPlatform
|
||||
} from '../orca-runtime-test-mocks.spec'
|
||||
import type { OrchestrationDb } from '../orchestration/db'
|
||||
import type { WorkspaceSessionState } from '../orca-runtime-test-mocks.spec'
|
||||
import {
|
||||
HEADLESS_LEAF_ID,
|
||||
@@ -95,22 +95,23 @@ describe('OrcaRuntimeService', () => {
|
||||
undefined,
|
||||
{ canRecoverPersistentLocalPtys: () => true }
|
||||
)
|
||||
runtime.setOrchestrationDb({
|
||||
listLegacyWorkerTerminalRecoveryRows: () => [
|
||||
{
|
||||
dispatch_id: 'dispatch-ssh',
|
||||
task_id: 'task-ssh',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_ssh_legacy',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `${ptyId}:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_WORKTREE_ID,
|
||||
agent_terminal_handle: 'term_ssh_legacy'
|
||||
}
|
||||
]
|
||||
} as unknown as OrchestrationDb)
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'listLegacyWorkerTerminalRecoveryRows').mockImplementation(() => [
|
||||
{
|
||||
dispatch_id: 'dispatch-ssh',
|
||||
task_id: 'task-ssh',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_ssh_legacy',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `${ptyId}:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_WORKTREE_ID,
|
||||
agent_terminal_handle: 'term_ssh_legacy'
|
||||
}
|
||||
])
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
runtime.setPtyController({
|
||||
write: vi.fn(() => true),
|
||||
kill: vi.fn(() => true),
|
||||
@@ -228,22 +229,23 @@ describe('OrcaRuntimeService', () => {
|
||||
undefined,
|
||||
{ canRecoverPersistentLocalPtys: () => true }
|
||||
)
|
||||
runtime.setOrchestrationDb({
|
||||
listLegacyWorkerTerminalRecoveryRows: () => [
|
||||
{
|
||||
dispatch_id: 'dispatch-wsl',
|
||||
task_id: 'task-wsl',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_wsl_legacy',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `pty-wsl-legacy:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_WORKTREE_ID,
|
||||
agent_terminal_handle: 'term_wsl_legacy'
|
||||
}
|
||||
]
|
||||
} as unknown as OrchestrationDb)
|
||||
const orchestrationDb = makeRuntimeOrchestrationDb()
|
||||
vi.spyOn(orchestrationDb, 'listSessionDispatchesForPresentation').mockImplementation(() => [])
|
||||
vi.spyOn(orchestrationDb, 'listLegacyWorkerTerminalRecoveryRows').mockImplementation(() => [
|
||||
{
|
||||
dispatch_id: 'dispatch-wsl',
|
||||
task_id: 'task-wsl',
|
||||
dispatch_status: 'completed',
|
||||
contract_version: 0,
|
||||
assignee_handle: 'term_wsl_legacy',
|
||||
assignee_pane_key: workerPaneKey,
|
||||
process_incarnation: `pty-wsl-legacy:${incarnationId}`,
|
||||
worker_state: 'ready',
|
||||
worktree_id: TEST_WORKTREE_ID,
|
||||
agent_terminal_handle: 'term_wsl_legacy'
|
||||
}
|
||||
])
|
||||
runtime.setOrchestrationDb(orchestrationDb)
|
||||
// Declare the scope parameter so mock.calls retains its type alongside the runtime deadline.
|
||||
const listProcesses = vi.fn(async (_connectionId?: string | null) => [
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AGENT_STATUS_STALE_AFTER_MS } from '../../../../../shared/agent-status-types'
|
||||
import { parsePaneKey } from '../../../../../shared/stable-pane-id'
|
||||
import type { DispatchContextRow } from '../../types'
|
||||
import {
|
||||
@@ -24,6 +25,122 @@ const ACTIVE_DISPATCH_BY_PANE_SUFFIX_SQL = `SELECT ${DISPATCH_CONTEXT_COLUMN_LIS
|
||||
ORDER BY rowid DESC LIMIT 1`
|
||||
const LATEST_DISPATCH_BY_HANDLE_SQL = `SELECT ${DISPATCH_CONTEXT_COLUMN_LIST} FROM dispatch_contexts WHERE assignee_handle = ? ORDER BY rowid DESC LIMIT 1`
|
||||
|
||||
function presentationAddressKey(column: string, address: string | null): string {
|
||||
return `${column}:${address}`
|
||||
}
|
||||
|
||||
/** Preserve the terminal projection's exact ISO/SQLite recency contract, including future dates. */
|
||||
export function isRecentPresentationDispatch(dispatch: DispatchContextRow, now: number): boolean {
|
||||
if (!dispatch.completed_at || dispatch.status === 'pending' || dispatch.status === 'dispatched') {
|
||||
return false
|
||||
}
|
||||
const completedAt = Date.parse(
|
||||
dispatch.completed_at.includes('T')
|
||||
? dispatch.completed_at
|
||||
: `${dispatch.completed_at.replace(' ', 'T')}Z`
|
||||
)
|
||||
return Number.isFinite(completedAt) && now - completedAt <= AGENT_STATUS_STALE_AFTER_MS
|
||||
}
|
||||
|
||||
/** Active first, otherwise latest per address; an older eligible completion never resurfaces. */
|
||||
export function listSessionDispatchesForPresentation(
|
||||
this: OrchestrationDb,
|
||||
now: number
|
||||
): { dispatch: DispatchContextRow; order: number }[] {
|
||||
const lowerBound = new Date(now - AGENT_STATUS_STALE_AFTER_MS - 86_400_000).toISOString()
|
||||
const rawCandidates = this.db
|
||||
.prepare(`
|
||||
SELECT rowid AS presentation_order, ${DISPATCH_CONTEXT_COLUMN_LIST} FROM dispatch_contexts
|
||||
INDEXED BY idx_dispatch_session_presentation
|
||||
WHERE assignee_orca_session_id IS NOT NULL AND status IN ('pending', 'dispatched')
|
||||
UNION ALL
|
||||
SELECT rowid AS presentation_order, ${DISPATCH_CONTEXT_COLUMN_LIST} FROM dispatch_contexts
|
||||
INDEXED BY idx_dispatch_session_presentation
|
||||
WHERE assignee_orca_session_id IS NOT NULL
|
||||
AND status IN ('completed', 'failed', 'circuit_broken') AND completed_at >= ?
|
||||
`)
|
||||
.all(lowerBound)
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The complete schema-pinned Dispatch projection restores the persisted-row contract; the default integer wrapper normalizes rowid to a safe number.
|
||||
const candidates = rawCandidates as (DispatchContextRow & { presentation_order: number })[]
|
||||
const newestActiveOrderBySession = new Map<string, number>()
|
||||
const selected = new Map<string, (typeof candidates)[number]>()
|
||||
for (const candidate of candidates) {
|
||||
// Include handled rows even when a newer non-public row later suppresses their address.
|
||||
if (candidate.status === 'pending' || candidate.status === 'dispatched') {
|
||||
const session = candidate.assignee_orca_session_id
|
||||
if (session) {
|
||||
newestActiveOrderBySession.set(
|
||||
session,
|
||||
Math.max(newestActiveOrderBySession.get(session) ?? 0, candidate.presentation_order)
|
||||
)
|
||||
}
|
||||
}
|
||||
const address = presentationAddressKey(
|
||||
candidate.assignee_handle ? 'assignee_handle' : 'assignee_orca_session_id',
|
||||
candidate.assignee_handle || candidate.assignee_orca_session_id
|
||||
)
|
||||
const previous = selected.get(address)
|
||||
const active = candidate.status === 'pending' || candidate.status === 'dispatched'
|
||||
const previousActive = previous?.status === 'pending' || previous?.status === 'dispatched'
|
||||
if (
|
||||
!previous ||
|
||||
(active && !previousActive) ||
|
||||
(active === previousActive && candidate.presentation_order > previous.presentation_order)
|
||||
) {
|
||||
selected.set(address, candidate)
|
||||
}
|
||||
}
|
||||
const eligible = [...selected].filter(
|
||||
([, candidate]) =>
|
||||
candidate.status === 'pending' ||
|
||||
candidate.status === 'dispatched' ||
|
||||
isRecentPresentationDispatch(candidate, now)
|
||||
)
|
||||
const latestOrderByAddress = new Map<string, number>()
|
||||
for (const [column, active] of [
|
||||
['assignee_handle', true],
|
||||
['assignee_handle', false],
|
||||
['assignee_orca_session_id', false]
|
||||
] as const) {
|
||||
const addresses = eligible
|
||||
.filter(
|
||||
([, candidate]) =>
|
||||
Boolean(candidate.assignee_handle) === (column === 'assignee_handle') &&
|
||||
(candidate.status === 'pending' || candidate.status === 'dispatched') === active
|
||||
)
|
||||
.map(([, candidate]) => candidate[column])
|
||||
if (addresses.length === 0) {
|
||||
continue
|
||||
}
|
||||
// Batch native calls, retaining one equality-index seek per selected address.
|
||||
const latest = this.db
|
||||
.prepare(`SELECT requested.value AS address, (SELECT rowid AS n FROM dispatch_contexts
|
||||
WHERE ${column} = requested.value
|
||||
${active ? "AND status IN ('pending', 'dispatched')" : ''}
|
||||
ORDER BY rowid DESC LIMIT 1) AS n FROM json_each(?) requested`)
|
||||
.all(JSON.stringify(addresses))
|
||||
for (const row of latest) {
|
||||
if (typeof row.address === 'string' && typeof row.n === 'number') {
|
||||
latestOrderByAddress.set(presentationAddressKey(column, row.address), row.n)
|
||||
}
|
||||
}
|
||||
}
|
||||
const result: { dispatch: DispatchContextRow; order: number }[] = []
|
||||
for (const [address, candidate] of eligible) {
|
||||
const { presentation_order, ...dispatch } = candidate
|
||||
const active = dispatch.status === 'pending' || dispatch.status === 'dispatched'
|
||||
const latest =
|
||||
active && !candidate.assignee_handle
|
||||
? newestActiveOrderBySession.get(candidate.assignee_orca_session_id ?? '')
|
||||
: latestOrderByAddress.get(address)
|
||||
if (latest !== presentation_order) {
|
||||
continue
|
||||
}
|
||||
result.push({ dispatch, order: presentation_order })
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export function getActiveDispatchForTerminal(
|
||||
this: OrchestrationDb,
|
||||
handle: string,
|
||||
@@ -168,6 +285,7 @@ export type DispatchLookupMethods = {
|
||||
getActiveDispatchMailboxOwners: typeof getActiveDispatchMailboxOwners
|
||||
isDispatchMessageSender: typeof isDispatchMessageSender
|
||||
findActiveDispatchForAssignee: typeof findActiveDispatchForAssignee
|
||||
listSessionDispatchesForPresentation: typeof listSessionDispatchesForPresentation
|
||||
getLatestDispatchForTerminal: typeof getLatestDispatchForTerminal
|
||||
}
|
||||
|
||||
@@ -179,6 +297,7 @@ export function attachDispatchLookup(ctor: { prototype: object }): void {
|
||||
getActiveDispatchMailboxOwners,
|
||||
isDispatchMessageSender,
|
||||
findActiveDispatchForAssignee,
|
||||
getLatestDispatchForTerminal
|
||||
getLatestDispatchForTerminal,
|
||||
listSessionDispatchesForPresentation
|
||||
})
|
||||
}
|
||||
|
||||
@@ -103,7 +103,8 @@ function buildProjection(db: OrchestrationDb): RuntimeAgentOrchestrationProjecti
|
||||
processIncarnation: 'inc_1'
|
||||
} as OrchestrationCompatibilityTerminalAuthority)
|
||||
: null,
|
||||
getAgentStatusSnapshot: () => []
|
||||
getAgentStatusSnapshot: () => [],
|
||||
getSessionRecords: () => null
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ function fixture(count = 1, version: 'fresh' | 'v41' = 'fresh') {
|
||||
${DROP_REMOVED_WORK_TRIGGERS_SQL}
|
||||
DROP INDEX idx_runs_coordinator_orca_session_id;
|
||||
DROP INDEX idx_dispatch_assignee_orca_session_id;
|
||||
DROP INDEX idx_dispatch_session_presentation;
|
||||
DROP TRIGGER trg_runs_remember_coordinator_insert;
|
||||
DROP TRIGGER trg_runs_remember_coordinator_update;
|
||||
DROP TRIGGER trg_messages_route_coordinator_mail;
|
||||
@@ -92,7 +93,19 @@ function fixture(count = 1, version: 'fresh' | 'v41' = 'fresh') {
|
||||
.map((row) => row.name)
|
||||
.toSorted()
|
||||
).toEqual(DISPATCH_CONTEXT_COLUMNS.toSorted())
|
||||
const priorVersion = writer.db.pragma('user_version', { simple: true })
|
||||
expect(
|
||||
writer.db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE name = 'idx_dispatch_session_presentation'")
|
||||
.get()
|
||||
).toBeDefined()
|
||||
const reader = new OrchestrationDb(path)
|
||||
expect(reader.db.pragma('user_version', { simple: true })).toBe(priorVersion)
|
||||
expect(
|
||||
reader.db
|
||||
.prepare("SELECT name FROM sqlite_master WHERE name = 'idx_dispatch_session_presentation'")
|
||||
.get()
|
||||
).toBeDefined()
|
||||
databases.push(reader)
|
||||
return { writer, reader, run, entries }
|
||||
}
|
||||
|
||||
@@ -7,6 +7,14 @@ export function hasColumn(this: OrchestrationDb, table: string, column: string):
|
||||
}
|
||||
|
||||
export function createMailboxDeliveryIndexesIfPossible(this: OrchestrationDb): void {
|
||||
if (
|
||||
['assignee_orca_session_id', 'status', 'completed_at'].every((column) =>
|
||||
this.hasColumn('dispatch_contexts', column)
|
||||
)
|
||||
) {
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idx_dispatch_session_presentation
|
||||
ON dispatch_contexts(status, completed_at) WHERE assignee_orca_session_id IS NOT NULL`)
|
||||
}
|
||||
if (this.hasColumn('deliveries', 'mailbox_handle')) {
|
||||
this.db.exec(OUTSTANDING_MAILBOX_INDEX_SQL)
|
||||
}
|
||||
|
||||
@@ -136,6 +136,7 @@ function stripOrcaSessionSchema(path: string, version: number): void {
|
||||
raw.exec(`
|
||||
${DROP_REMOVED_WORK_TRIGGERS_SQL}
|
||||
DROP INDEX idx_runs_coordinator_orca_session_id;
|
||||
DROP INDEX IF EXISTS idx_dispatch_session_presentation;
|
||||
DROP INDEX idx_dispatch_assignee_orca_session_id;
|
||||
DROP TRIGGER trg_runs_remember_coordinator_insert;
|
||||
DROP TRIGGER trg_runs_remember_coordinator_update;
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
import type { AgentSessionConversationView } from '../../../shared/agent-session-conversation-view'
|
||||
import { getStructuredAgentSessionHost } from '../../native-chat/agent-session-wire/structured-agent-session-registry'
|
||||
import {
|
||||
structuredAgentSessionPaneKey,
|
||||
structuredAgentSessionTabId
|
||||
} from '../../../shared/structured-agent-session-projection'
|
||||
import { structuredWorkerHostScope } from '../structured-worker-identity'
|
||||
|
||||
export type AgentSessionRecordReader = {
|
||||
@@ -56,6 +60,7 @@ export function resolveLineageRunningSession(
|
||||
sessionId: string
|
||||
): LineageRunningSession {
|
||||
const lineage = [sessionId]
|
||||
const visited = new Set(lineage)
|
||||
if (!store) {
|
||||
return {
|
||||
kind: 'unverifiable',
|
||||
@@ -86,13 +91,14 @@ export function resolveLineageRunningSession(
|
||||
? { kind: 'here', ...running }
|
||||
: { kind: 'other-host', ...running }
|
||||
}
|
||||
if (lineage.includes(next)) {
|
||||
if (visited.has(next)) {
|
||||
return {
|
||||
kind: 'unverifiable',
|
||||
reason: `The /clear lineage of structured session ${sessionId} loops back on itself.`,
|
||||
lineage
|
||||
}
|
||||
}
|
||||
visited.add(next)
|
||||
lineage.push(next)
|
||||
current = next
|
||||
}
|
||||
@@ -106,3 +112,45 @@ export function lineageLiveSession(
|
||||
const running = resolveLineageRunningSession(store, sessionId)
|
||||
return running.kind === 'unverifiable' ? null : running.record
|
||||
}
|
||||
|
||||
/** Canonical host status keys; this build-local resolver shares successful suffixes and failed verdicts. */
|
||||
export function createCanonicalSessionPaneResolver(
|
||||
store: AgentSessionRecordReader | null
|
||||
): (sessionId: string) => string {
|
||||
const resolved = new Map<string, string | null>()
|
||||
return (sessionId) => {
|
||||
let current = sessionId
|
||||
const path = new Set<string>()
|
||||
let destination: string | null = null
|
||||
for (;;) {
|
||||
if (resolved.has(current)) {
|
||||
destination = resolved.get(current) ?? null
|
||||
break
|
||||
}
|
||||
if (path.has(current)) {
|
||||
break
|
||||
}
|
||||
path.add(current)
|
||||
let record: AgentSessionConversationView | null
|
||||
try {
|
||||
record = store?.getRecord(current) ?? null
|
||||
} catch {
|
||||
record = null
|
||||
}
|
||||
if (!record) {
|
||||
break
|
||||
}
|
||||
const next = clearedInto(record)
|
||||
if (!next) {
|
||||
destination = current
|
||||
break
|
||||
}
|
||||
current = next
|
||||
}
|
||||
for (const member of path) {
|
||||
resolved.set(member, destination)
|
||||
}
|
||||
const id = destination ?? sessionId
|
||||
return structuredAgentSessionPaneKey(structuredAgentSessionTabId(id), id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { vi } from 'vitest'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
import { RpcDispatcher } from './rpc/dispatcher'
|
||||
import { WORKTREE_CATALOG_METHODS } from './rpc/methods/worktree-catalog-methods'
|
||||
import type { RuntimeWorktreePsSummary } from '../../shared/runtime-types'
|
||||
import type { RuntimeAgentLineageCatalogTestModule } from '../../shared/runtime-agent-lineage-consumer.test-fixture'
|
||||
|
||||
export function createLineageCatalogDispatcher(build: () => RuntimeWorktreePsSummary) {
|
||||
const runtime = new OrcaRuntimeService()
|
||||
vi.spyOn(runtime, 'getRuntimeId').mockReturnValue('lineage-test')
|
||||
vi.spyOn(runtime, 'getWorktreePs').mockImplementation(async () => ({
|
||||
worktrees: [build()],
|
||||
totalCount: 1,
|
||||
truncated: false
|
||||
}))
|
||||
return new RpcDispatcher({ runtime, methods: WORKTREE_CATALOG_METHODS })
|
||||
}
|
||||
|
||||
export default { createLineageCatalogDispatcher } satisfies RuntimeAgentLineageCatalogTestModule
|
||||
@@ -0,0 +1,128 @@
|
||||
import { OrchestrationDb } from './orchestration/db'
|
||||
import type { RuntimeAgentLineageTestModule } from '../../shared/runtime-agent-lineage-consumer.test-fixture'
|
||||
import { RuntimeAgentOrchestrationProjection } from './runtime-agent-orchestration-projection'
|
||||
import { attachRuntimeWorktreeAgentRows } from './runtime-worktree-agent-rows'
|
||||
import type { RuntimeWorktreePsSummary } from '../../shared/runtime-types'
|
||||
import type { RuntimeWorktreeAgentSource } from './runtime-worktree-agent-source'
|
||||
import { testOrcaSessionId } from '../../shared/orca-session-address-test-fixture'
|
||||
import {
|
||||
structuredAgentSessionPaneKey,
|
||||
structuredAgentSessionTabId
|
||||
} from '../../shared/structured-agent-session-projection'
|
||||
import {
|
||||
mintStructuredWorkerHandle,
|
||||
mintStructuredWorkerPaneKey,
|
||||
structuredWorkerProcessIncarnation
|
||||
} from './structured-worker-identity'
|
||||
|
||||
export const LINEAGE_PARENT = testOrcaSessionId('ca1f6c2e-8b3d-4e7a-9c15-0d2b6e8f1a37')
|
||||
export const LINEAGE_CHILD = testOrcaSessionId('ce3b9d15-2c4a-4f86-a0b1-5c9e2d7f3b64')
|
||||
export const canonicalChatRow = (id: string): string =>
|
||||
structuredAgentSessionPaneKey(structuredAgentSessionTabId(id), id)
|
||||
|
||||
/** Production assignment projection and host row attachment; no preassembled agent output. */
|
||||
export function publicLineageFixture(worktreeId = 'repo::/workspace') {
|
||||
const db = new OrchestrationDb(':memory:')
|
||||
const privateHandle = mintStructuredWorkerHandle()
|
||||
const privatePane = mintStructuredWorkerPaneKey(LINEAGE_CHILD)
|
||||
const run = db.createRun({
|
||||
objective: 'lineage',
|
||||
coordinatorHandle: null,
|
||||
coordinatorPaneKey: null,
|
||||
coordinatorOrcaSessionId: LINEAGE_PARENT
|
||||
})
|
||||
db.createDispatchContext({
|
||||
taskId: db.createTask({ runId: run.id, spec: 'Build the worker result' }).id,
|
||||
assigneeHandle: privateHandle,
|
||||
assigneePaneKey: privatePane,
|
||||
processIncarnation: structuredWorkerProcessIncarnation(LINEAGE_CHILD),
|
||||
creator: { kind: 'system' },
|
||||
maxDepth: 99
|
||||
})
|
||||
const projection = new RuntimeAgentOrchestrationProjection({
|
||||
getDb: () => db,
|
||||
getLeaves: () => [],
|
||||
getPtys: () => [],
|
||||
getSessionRecords: () => null,
|
||||
paneOf: () => ({ tabId: null, paneKey: null, recordedAtGraphSequence: 0 }),
|
||||
issueLeafHandle: () => '',
|
||||
issuePtyHandle: () => '',
|
||||
makePaneKey: () => '',
|
||||
getWorktreeId: () => null,
|
||||
getHandleForPaneKey: () => null,
|
||||
getPaneKey: () => null,
|
||||
getDispatchAuthority: () => null,
|
||||
getAgentStatusSnapshot: () => []
|
||||
})
|
||||
const updatedAt = Date.now()
|
||||
const build = (): RuntimeWorktreePsSummary => {
|
||||
const row: RuntimeWorktreePsSummary = {
|
||||
worktreeId,
|
||||
workspaceKind: worktreeId.startsWith('folder:') ? 'folder-workspace' : 'git',
|
||||
repoId: 'repo',
|
||||
repo: 'repo',
|
||||
path: '/workspace',
|
||||
branch: 'main',
|
||||
isArchived: false,
|
||||
isMainWorktree: true,
|
||||
hasHostSidebarActivity: false,
|
||||
parentWorktreeId: null,
|
||||
childWorktreeIds: [],
|
||||
displayName: 'Workspace',
|
||||
workspaceStatus: '',
|
||||
sortOrder: 0,
|
||||
linkedIssue: null,
|
||||
linkedPR: null,
|
||||
linkedLinearIssue: null,
|
||||
linkedGitLabMR: null,
|
||||
linkedGitLabIssue: null,
|
||||
comment: '',
|
||||
isPinned: false,
|
||||
isActive: false,
|
||||
unread: false,
|
||||
liveTerminalCount: 0,
|
||||
hasAttachedPty: false,
|
||||
lastOutputAt: null,
|
||||
preview: '',
|
||||
status: 'inactive',
|
||||
agents: []
|
||||
}
|
||||
const sources = new Map<string, RuntimeWorktreeAgentSource>(
|
||||
[LINEAGE_PARENT, LINEAGE_CHILD].map((id) => [
|
||||
canonicalChatRow(id),
|
||||
{
|
||||
paneKey: canonicalChatRow(id),
|
||||
connectionId: null,
|
||||
worktreeId,
|
||||
state: 'working',
|
||||
agentType: 'codex',
|
||||
prompt: id === LINEAGE_PARENT ? 'Coordinate work' : 'Implement work',
|
||||
lastAssistantMessage: null,
|
||||
toolName: null,
|
||||
toolInput: null,
|
||||
interrupted: false,
|
||||
stateStartedAt: 1,
|
||||
updatedAt,
|
||||
structuredHost: 'owned'
|
||||
}
|
||||
])
|
||||
)
|
||||
const summaries = new Map([[worktreeId, row]])
|
||||
attachRuntimeWorktreeAgentRows({
|
||||
summaries,
|
||||
rowSources: sources,
|
||||
terminalTitleEvidenceByWorktreeId: new Map(),
|
||||
orchestrationByPaneKey: projection.buildByPaneKey(),
|
||||
getSummary: (id) => summaries.get(id) ?? null
|
||||
})
|
||||
return row
|
||||
}
|
||||
return { db, projection, build, privateHandle, privatePane, run }
|
||||
}
|
||||
|
||||
export default {
|
||||
publicLineageFixture,
|
||||
canonicalChatRow,
|
||||
LINEAGE_PARENT,
|
||||
LINEAGE_CHILD
|
||||
} satisfies RuntimeAgentLineageTestModule
|
||||
@@ -0,0 +1,240 @@
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { hostname, tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { expect, vi } from 'vitest'
|
||||
import { OrchestrationDb } from './orchestration/db'
|
||||
import { formatOrcaSessionAddress, type OrcaSessionId } from '../../shared/orca-session-address'
|
||||
import { testOrcaSessionId } from '../../shared/orca-session-address-test-fixture'
|
||||
import {
|
||||
structuredAgentSessionPaneKey,
|
||||
structuredAgentSessionTabId
|
||||
} from '../../shared/structured-agent-session-projection'
|
||||
import {
|
||||
installSqlVisitCounters,
|
||||
traceDispatchPredicateVisits,
|
||||
latestStatementMetrics,
|
||||
dispatchQueryPlans,
|
||||
seedCoarsePresentationDispatches
|
||||
} from './dispatch-predicate-visits.test-fixture'
|
||||
|
||||
export function benchmarkPresentationCorpus(
|
||||
corpus: {
|
||||
nullAddress: boolean
|
||||
history: number
|
||||
terminalHistory: number
|
||||
attentionFacts: number
|
||||
parentSessionId: OrcaSessionId
|
||||
},
|
||||
build: (
|
||||
db: OrchestrationDb
|
||||
) => Record<string, { parentPaneKey?: string | null; dispatchId: string }> | undefined
|
||||
) {
|
||||
const { nullAddress, history, terminalHistory, attentionFacts, parentSessionId } = corpus
|
||||
const revision = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8' }).trim()
|
||||
const now = Date.parse('2026-10-27T12:00:00Z')
|
||||
const clock = vi.spyOn(Date, 'now').mockReturnValue(now)
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-lineage-benchmark-'))
|
||||
const path = join(directory, 'orchestration.sqlite')
|
||||
let db = new OrchestrationDb(path)
|
||||
let native: ReturnType<typeof installSqlVisitCounters> | undefined
|
||||
let trace: ReturnType<typeof traceDispatchPredicateVisits> | undefined
|
||||
try {
|
||||
const run = db.createRun({
|
||||
objective: 'benchmark',
|
||||
coordinatorHandle: null,
|
||||
coordinatorPaneKey: null,
|
||||
coordinatorOrcaSessionId: parentSessionId
|
||||
})
|
||||
const expected = new Map<string, string>()
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const session = testOrcaSessionId(`00000000-0000-4000-8000-${String(i).padStart(12, '0')}`)
|
||||
const dispatch = db.createDispatchContext({
|
||||
taskId: db.createTask({ runId: run.id, spec: 'active' }).id,
|
||||
assigneeHandle: formatOrcaSessionAddress(session),
|
||||
creator: { kind: 'system' },
|
||||
maxDepth: 99
|
||||
})
|
||||
expected.set(
|
||||
structuredAgentSessionPaneKey(structuredAgentSessionTabId(session), session),
|
||||
dispatch.id
|
||||
)
|
||||
}
|
||||
if (nullAddress) {
|
||||
db.db.exec(
|
||||
"UPDATE dispatch_contexts SET assignee_handle = NULL, assignee_pane_key = NULL WHERE status IN ('pending', 'dispatched')"
|
||||
)
|
||||
}
|
||||
const task = db.createTask({ runId: run.id, spec: 'history' })
|
||||
const insert = db.db.prepare(`INSERT INTO dispatch_contexts
|
||||
(id, run_id, task_id, assignee_handle, assignee_orca_session_id, status, completed_at)
|
||||
WITH RECURSIVE seq(n) AS (VALUES(1) UNION ALL SELECT n+1 FROM seq WHERE n < ?)
|
||||
SELECT ? || n, ?, ?, CASE WHEN ? THEN printf('orca_session_id:00000000-0000-4000-8000-%012d', (n-1) % 20) ELSE 'terminal-address-' || n END,
|
||||
CASE WHEN ? THEN CASE WHEN ? THEN printf('00000000-0000-4000-8000-%012d', (n-1) % 20) ELSE 'session-' || n END ELSE NULL END,
|
||||
'completed', '2020-01-01T00:00:00Z' FROM seq`)
|
||||
insert.run(history, 'session-history-', run.id, task.id, 1, 1, nullAddress ? 1 : 0)
|
||||
if (terminalHistory) {
|
||||
insert.run(terminalHistory, 'terminal-history-', run.id, task.id, 0, 0, 0)
|
||||
}
|
||||
const active = db.db
|
||||
.prepare(
|
||||
"SELECT id, task_id FROM dispatch_contexts WHERE status IN ('pending', 'dispatched') ORDER BY rowid"
|
||||
)
|
||||
.all()
|
||||
expect(active.map((row) => row.id)).toEqual([...expected.values()])
|
||||
const insertFact = db.db.prepare(
|
||||
`INSERT INTO attempt_observation_facts(id, dispatch_id, task_id, sequence, authority_id, authority_clock, facet, payload, home_received_at) VALUES (?, ?, ?, ?, 'home', 'home', 'process_turn', '{"process":"running","turn":"working"}', ?)`
|
||||
)
|
||||
for (let i = 0; i < attentionFacts; i++) {
|
||||
const row = active[i % active.length]
|
||||
if (typeof row?.id !== 'string' || typeof row.task_id !== 'string') {
|
||||
throw new Error('Missing benchmark Dispatch')
|
||||
}
|
||||
insertFact.run(
|
||||
`observation-${i}`,
|
||||
row.id,
|
||||
row.task_id,
|
||||
Math.floor(i / active.length),
|
||||
1_800_000_000_000 + i
|
||||
)
|
||||
}
|
||||
const sqlite = db.db.prepare('SELECT sqlite_version() AS version').get()?.version
|
||||
db.db.exec('DROP INDEX idx_dispatch_session_presentation')
|
||||
const first = performance.now()
|
||||
db.createMailboxDeliveryIndexesIfPossible()
|
||||
const indexHelperMs = performance.now() - first
|
||||
db.db.exec('DROP INDEX idx_dispatch_session_presentation')
|
||||
db.close()
|
||||
const openStarted = performance.now()
|
||||
db = new OrchestrationDb(path)
|
||||
const reopenWithIndexCreationMs = performance.now() - openStarted
|
||||
let candidateVisits = 0,
|
||||
latestVisits = 0,
|
||||
observationVisits = 0
|
||||
native = installSqlVisitCounters({
|
||||
lineage_bench_visit: () => {
|
||||
candidateVisits++
|
||||
return 1
|
||||
},
|
||||
lineage_latest_visit: () => {
|
||||
latestVisits++
|
||||
return 1
|
||||
},
|
||||
lineage_observation_visit: () => {
|
||||
observationVisits++
|
||||
return 1
|
||||
}
|
||||
})
|
||||
trace = traceDispatchPredicateVisits(db.db, {
|
||||
candidate: 'lineage_bench_visit',
|
||||
latest: 'lineage_latest_visit',
|
||||
observation: 'lineage_observation_visit'
|
||||
})
|
||||
let plans: ReturnType<typeof dispatchQueryPlans> | undefined
|
||||
let previousCoarse = 0
|
||||
const sweep =
|
||||
!nullAddress && history === 10_000 && terminalHistory === 0 && attentionFacts === 0
|
||||
for (const coarseOnly of sweep ? [0, 1_000, 10_000] : [0]) {
|
||||
seedCoarsePresentationDispatches(
|
||||
db,
|
||||
run.id,
|
||||
coarseOnly - previousCoarse,
|
||||
now,
|
||||
`coarse-${coarseOnly}`
|
||||
)
|
||||
previousCoarse = coarseOnly
|
||||
trace.mockClear()
|
||||
trace.executions.length = 0
|
||||
native.mockClear()
|
||||
candidateVisits = 0
|
||||
latestVisits = 0
|
||||
observationVisits = 0
|
||||
const start = performance.now()
|
||||
const output = build(db)
|
||||
const buildMs = performance.now() - start
|
||||
const production = {
|
||||
cachedPrepareCalls: trace.mock.calls.length,
|
||||
executedStatements: trace.executions.length,
|
||||
nativeCompilations: native.mock.calls.length,
|
||||
latest: latestStatementMetrics(trace, native),
|
||||
candidateVisits,
|
||||
latestVisits,
|
||||
observationVisits
|
||||
}
|
||||
expect(production.latest).toEqual({
|
||||
cachedPrepareCalls: nullAddress ? 0 : 1,
|
||||
executedStatements: nullAddress ? 0 : 1,
|
||||
nativeCompilations: nullAddress || coarseOnly ? 0 : 1,
|
||||
addressEntries: nullAddress ? 0 : 20,
|
||||
returnedVerificationRows: nullAddress ? 0 : 20
|
||||
})
|
||||
expect(
|
||||
trace.executions.filter(
|
||||
({ query }) =>
|
||||
query.includes('rowid AS n') &&
|
||||
query.includes('assignee_orca_session_id') &&
|
||||
query.includes("AND status IN ('pending', 'dispatched')")
|
||||
)
|
||||
).toHaveLength(0)
|
||||
expect(Object.keys(output ?? {}).sort()).toEqual([...expected.keys()].sort())
|
||||
for (const [key, dispatchId] of expected) {
|
||||
expect(output?.[key]).toMatchObject({
|
||||
dispatchId,
|
||||
parentPaneKey: structuredAgentSessionPaneKey(
|
||||
structuredAgentSessionTabId(parentSessionId),
|
||||
parentSessionId
|
||||
)
|
||||
})
|
||||
}
|
||||
expect(candidateVisits).toBe(20 + coarseOnly)
|
||||
expect(latestVisits).toBe(nullAddress ? 0 : 20)
|
||||
expect(observationVisits).toBe(attentionFacts)
|
||||
if (!plans) {
|
||||
plans = dispatchQueryPlans(db.db, trace)
|
||||
}
|
||||
const planText = JSON.stringify(plans)
|
||||
expect(planText).not.toMatch(/SCAN dispatch_contexts|USE TEMP B-TREE/)
|
||||
expect(planText).toContain('idx_dispatch_session_presentation (status=? AND completed_at>?)')
|
||||
if (!nullAddress) {
|
||||
expect(planText).toContain('idx_dispatch_active_assignee_handle (assignee_handle=?)')
|
||||
}
|
||||
console.log(
|
||||
'LINEAGE_BENCH_ENVIRONMENT_EVIDENCE',
|
||||
JSON.stringify({
|
||||
host: hostname(),
|
||||
node: process.version,
|
||||
sqlite,
|
||||
platform: process.platform,
|
||||
arch: process.arch,
|
||||
checkout: process.cwd(),
|
||||
revision,
|
||||
timingScope: 'instrumented production build; includes predicate/tracing overhead',
|
||||
corpus,
|
||||
coarseOnly,
|
||||
Q: 20 + coarseOnly,
|
||||
E: nullAddress ? 0 : 20,
|
||||
B: nullAddress ? 0 : 1,
|
||||
cachePhase: coarseOnly
|
||||
? 'warm statements after coarse-only inserts'
|
||||
: 'first instrumented build after constructor reopen',
|
||||
indexHelperMs,
|
||||
reopenWithIndexCreationMs,
|
||||
buildMs,
|
||||
production,
|
||||
diagnostic: {
|
||||
cachedPrepareCalls: trace.mock.calls.length - production.cachedPrepareCalls,
|
||||
executedStatements: trace.executions.length - production.executedStatements,
|
||||
nativeCompilations: native.mock.calls.length - production.nativeCompilations
|
||||
},
|
||||
plans
|
||||
})
|
||||
)
|
||||
}
|
||||
} finally {
|
||||
trace?.mockRestore()
|
||||
native?.mockRestore()
|
||||
clock.mockRestore()
|
||||
db.close()
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,778 @@
|
||||
import { mkdtempSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { seedTestAgentSessionRecordStore } from './agent-session-record-store-test-harness'
|
||||
import { closeTestJournalHostDatabase } from '../native-chat/agent-session-journal/journal-host-database-test-support'
|
||||
import {
|
||||
openAgentSessionRecordStoreOnce,
|
||||
releaseAgentSessionRecordStore
|
||||
} from './agent-session-record-store-slot'
|
||||
import { recordingStructuredAgentSessionLogger } from '../native-chat/agent-session-wire/structured-agent-session-logger-test-support'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
import './rpc/unused-default-rpc-methods.test-fixture'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { agentSessionConversationView } from '../../shared/agent-session-conversation-view'
|
||||
import {
|
||||
createSessionCallerHarness,
|
||||
orchestrationRequest,
|
||||
resultOf,
|
||||
idOf,
|
||||
SESSION_X,
|
||||
SESSION_Y,
|
||||
sessionRecord
|
||||
} from './rpc/orchestration-session-caller-test-fixture'
|
||||
import { formatOrcaSessionAddress } from '../../shared/orca-session-address'
|
||||
import type { AgentSessionRecordReader } from './orchestration/structured-session-lineage'
|
||||
|
||||
const hostRef = vi.hoisted((): { current: unknown } => ({ current: null }))
|
||||
vi.mock('../native-chat/agent-session-wire/structured-agent-session-registry', () => ({
|
||||
getStructuredAgentSessionHost: () => hostRef.current
|
||||
}))
|
||||
import { testOrcaSessionId } from '../../shared/orca-session-address-test-fixture'
|
||||
import {
|
||||
structuredAgentSessionPaneKey,
|
||||
structuredAgentSessionTabId
|
||||
} from '../../shared/structured-agent-session-projection'
|
||||
import { RuntimeAgentOrchestrationProjection } from './runtime-agent-orchestration-projection'
|
||||
import {
|
||||
installSqlVisitCounters,
|
||||
latestStatementMetrics,
|
||||
dispatchQueryPlans,
|
||||
seedCoarsePresentationDispatches,
|
||||
traceDispatchPredicateVisits
|
||||
} from './dispatch-predicate-visits.test-fixture'
|
||||
import { benchmarkPresentationCorpus } from './runtime-agent-orchestration-benchmark.test-fixture'
|
||||
import { MIRROR_LEAVES_STEP_1 } from './runtime-leaf-record-projection.test-fixture'
|
||||
import type { RuntimeLeafRecord } from './runtime-terminal-state-records'
|
||||
import { OrchestrationDb } from './orchestration/db'
|
||||
import {
|
||||
mintStructuredWorkerHandle,
|
||||
mintStructuredWorkerPaneKey,
|
||||
structuredWorkerProcessIncarnation
|
||||
} from './structured-worker-identity'
|
||||
|
||||
const COORDINATOR_CHAT = testOrcaSessionId('1b6f0c3a-7d2e-4a91-8c55-2e9d4b7a0f13')
|
||||
const WORKER_CHAT = testOrcaSessionId('9c3e5a17-4b2d-4f60-8e91-0d7a6c2b5e48')
|
||||
const TERMINAL_WORKER_HANDLE = 'term_worker'
|
||||
const TERMINAL_WORKER_PANE = 'tab_w:11111111-1111-4111-8111-111111111111'
|
||||
|
||||
afterEach(() => vi.restoreAllMocks())
|
||||
|
||||
function chatRow(sessionId: string): string {
|
||||
return structuredAgentSessionPaneKey(structuredAgentSessionTabId(sessionId), sessionId)
|
||||
}
|
||||
|
||||
describe('RuntimeAgentOrchestrationProjection chat lineage', () => {
|
||||
let db: OrchestrationDb
|
||||
|
||||
afterEach(() => {
|
||||
db?.close()
|
||||
hostRef.current = null
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('nests chat and terminal workers under the chat that dispatched them', () => {
|
||||
db = new OrchestrationDb(':memory:')
|
||||
const run = db.createRun({
|
||||
objective: 'chat run',
|
||||
coordinatorHandle: null,
|
||||
coordinatorPaneKey: null,
|
||||
coordinatorOrcaSessionId: COORDINATOR_CHAT
|
||||
})
|
||||
const chatWorker = {
|
||||
handle: mintStructuredWorkerHandle(),
|
||||
sessionId: WORKER_CHAT,
|
||||
paneKey: mintStructuredWorkerPaneKey(WORKER_CHAT)
|
||||
}
|
||||
const creator = { kind: 'session', orcaSessionId: COORDINATOR_CHAT } as const
|
||||
db.createDispatchContext({
|
||||
taskId: db.createTask({ runId: run.id, spec: 'chat worker' }).id,
|
||||
assigneeHandle: chatWorker.handle,
|
||||
assigneePaneKey: chatWorker.paneKey,
|
||||
processIncarnation: structuredWorkerProcessIncarnation(WORKER_CHAT),
|
||||
creator,
|
||||
maxDepth: Number.MAX_SAFE_INTEGER
|
||||
})
|
||||
db.createDispatchContext({
|
||||
taskId: db.createTask({ runId: run.id, spec: 'terminal worker' }).id,
|
||||
assigneeHandle: TERMINAL_WORKER_HANDLE,
|
||||
assigneePaneKey: TERMINAL_WORKER_PANE,
|
||||
creator,
|
||||
maxDepth: Number.MAX_SAFE_INTEGER
|
||||
})
|
||||
const leaf: RuntimeLeafRecord = { ...MIRROR_LEAVES_STEP_1[0], ptyId: 'pty_w' }
|
||||
const projection = new RuntimeAgentOrchestrationProjection({
|
||||
getDb: () => db,
|
||||
getLeaves: () => [leaf],
|
||||
getPtys: () => [],
|
||||
paneOf: () => ({ tabId: null, paneKey: null, recordedAtGraphSequence: 0 }),
|
||||
issueLeafHandle: () => TERMINAL_WORKER_HANDLE,
|
||||
issuePtyHandle: () => '',
|
||||
makePaneKey: () => TERMINAL_WORKER_PANE,
|
||||
getWorktreeId: () => null,
|
||||
getHandleForPaneKey: () => null,
|
||||
getPaneKey: () => null,
|
||||
getDispatchAuthority: () => null,
|
||||
getAgentStatusSnapshot: () => [],
|
||||
getSessionRecords: () => null
|
||||
})
|
||||
|
||||
const contexts = projection.buildByPaneKey()
|
||||
|
||||
expect(contexts?.[chatRow(WORKER_CHAT)]?.parentPaneKey).toBe(chatRow(COORDINATOR_CHAT))
|
||||
expect(contexts?.[TERMINAL_WORKER_PANE]?.parentPaneKey).toBe(chatRow(COORDINATOR_CHAT))
|
||||
// The minted pane key authorizes the worker's mailbox; it must never reach a renderer.
|
||||
expect(JSON.stringify(contexts)).not.toContain(chatWorker.paneKey)
|
||||
expect(JSON.stringify(contexts)).not.toContain(chatWorker.handle)
|
||||
})
|
||||
})
|
||||
|
||||
function projectionFor(db: OrchestrationDb, records: AgentSessionRecordReader | null = null) {
|
||||
return new RuntimeAgentOrchestrationProjection({
|
||||
getDb: () => db,
|
||||
getLeaves: () => [],
|
||||
getPtys: () => [],
|
||||
paneOf: () => ({ tabId: null, paneKey: null, recordedAtGraphSequence: 0 }),
|
||||
issueLeafHandle: () => '',
|
||||
issuePtyHandle: () => '',
|
||||
makePaneKey: () => '',
|
||||
getWorktreeId: () => null,
|
||||
getHandleForPaneKey: () => null,
|
||||
getPaneKey: () => null,
|
||||
getDispatchAuthority: () => null,
|
||||
getAgentStatusSnapshot: () => [],
|
||||
getSessionRecords: () => records
|
||||
})
|
||||
}
|
||||
|
||||
it('publishes ordinary RPC assignments without registering display authority and honors current Run generations', async () => {
|
||||
const h = createSessionCallerHarness(hostRef)
|
||||
try {
|
||||
const runId = idOf(
|
||||
resultOf(
|
||||
await h.dispatch(
|
||||
orchestrationRequest(
|
||||
'orchestration.runCreate',
|
||||
{ objective: 'public lineage' },
|
||||
{ sessionId: SESSION_X }
|
||||
)
|
||||
)
|
||||
).run
|
||||
)
|
||||
const task = h.db.createTask({ runId, spec: 'existing chat task' })
|
||||
const response = await h.dispatch(
|
||||
orchestrationRequest(
|
||||
'orchestration.dispatch',
|
||||
{ task: task.id, to: formatOrcaSessionAddress(SESSION_Y) },
|
||||
{ sessionId: SESSION_X }
|
||||
)
|
||||
)
|
||||
const dispatchId = idOf(resultOf(response).dispatch)
|
||||
const projection = h.runtime['agentOrchestrationProjection']
|
||||
expect(projection.buildByPaneKey()?.[chatRow(SESSION_Y)]).toMatchObject({
|
||||
dispatchId,
|
||||
parentPaneKey: chatRow(SESSION_X),
|
||||
taskTitle: 'existing chat task'
|
||||
})
|
||||
h.db.db
|
||||
.prepare('UPDATE runs SET coordinator_orca_session_id = ? WHERE id = ?')
|
||||
.run(COORDINATOR_CHAT, runId)
|
||||
expect(projection.buildByPaneKey()?.[chatRow(SESSION_Y)]?.parentPaneKey).toBe(
|
||||
chatRow(SESSION_X)
|
||||
)
|
||||
h.db.db
|
||||
.prepare('UPDATE runs SET coordinator_orca_session_id = ? WHERE id = ?')
|
||||
.run(SESSION_X, runId)
|
||||
// A callback does not have to populate mailbox authority before the public relationship exists.
|
||||
h.runtime.onStructuredSessionStatusForMail({ sessionId: SESSION_Y, status: 'attention' })
|
||||
expect(projection.buildByPaneKey()?.[chatRow(SESSION_Y)]?.parentPaneKey).toBe(
|
||||
chatRow(SESSION_X)
|
||||
)
|
||||
h.db.db
|
||||
.prepare('UPDATE dispatch_contexts SET creator_orca_session_id = NULL WHERE id = ?')
|
||||
.run(dispatchId)
|
||||
const terminal = 'offline:11111111-1111-4111-8111-111111111111'
|
||||
h.db.db
|
||||
.prepare(
|
||||
'UPDATE runs SET coordinator_handle = ?, coordinator_pane_key = ?, consumer_generation = consumer_generation + 1 WHERE id = ?'
|
||||
)
|
||||
.run('term_old_writer', terminal, runId)
|
||||
expect(projection.buildByPaneKey()?.[chatRow(SESSION_Y)]?.parentPaneKey).toBe(terminal)
|
||||
h.db.db
|
||||
.prepare(
|
||||
'UPDATE runs SET coordinator_orca_session_id_generation = consumer_generation WHERE id = ?'
|
||||
)
|
||||
.run(runId)
|
||||
expect(projection.buildByPaneKey()?.[chatRow(SESSION_Y)]?.parentPaneKey).toBe(
|
||||
chatRow(SESSION_X)
|
||||
)
|
||||
h.db.db
|
||||
.prepare(
|
||||
'UPDATE runs SET coordinator_orca_session_id_generation = NULL, coordinator_handle = ?, coordinator_pane_key = ? WHERE id = ?'
|
||||
)
|
||||
.run(mintStructuredWorkerHandle(), mintStructuredWorkerPaneKey(SESSION_X), runId)
|
||||
expect(projection.buildByPaneKey()?.[chatRow(SESSION_Y)]?.parentPaneKey).toBeUndefined()
|
||||
expect(JSON.stringify(projection.buildByPaneKey())).not.toContain('structworker_')
|
||||
} finally {
|
||||
h.close()
|
||||
}
|
||||
})
|
||||
|
||||
it.each([20, 200])(
|
||||
'shares forward lineage and DB/attention work across %i workers; each build rederives facts',
|
||||
(n) => {
|
||||
const now = Date.parse('2026-10-27T12:00:00Z')
|
||||
vi.spyOn(Date, 'now').mockReturnValue(now)
|
||||
const db = new OrchestrationDb(':memory:')
|
||||
try {
|
||||
const run = db.createRun({
|
||||
objective: 'shared ancestry',
|
||||
coordinatorHandle: null,
|
||||
coordinatorPaneKey: null,
|
||||
coordinatorOrcaSessionId: SESSION_X
|
||||
})
|
||||
for (let i = 0; i < n; i++) {
|
||||
db.createDispatchContext({
|
||||
taskId: db.createTask({ runId: run.id, spec: 'shared task' }).id,
|
||||
assigneeHandle: `ordinary-${i}`,
|
||||
creator: { kind: 'session', orcaSessionId: SESSION_X },
|
||||
maxDepth: 9999
|
||||
})
|
||||
db.db
|
||||
.prepare(
|
||||
'UPDATE dispatch_contexts SET assignee_orca_session_id = ? WHERE assignee_handle = ?'
|
||||
)
|
||||
.run(`worker-${i}`, `ordinary-${i}`)
|
||||
}
|
||||
seedCoarsePresentationDispatches(db, run.id, 2 * n, now)
|
||||
const historyTask = db.createTask({ runId: run.id, spec: 'newer inactive handle history' })
|
||||
const insertHistory = db.db.prepare(`INSERT INTO dispatch_contexts
|
||||
(id, run_id, task_id, assignee_handle, status, completed_at)
|
||||
WITH RECURSIVE seq(n) AS (VALUES(1) UNION ALL SELECT n+1 FROM seq WHERE n < ?)
|
||||
SELECT ? || n, ?, ?, ?, 'completed', '2020-01-01T00:00:00Z' FROM seq`)
|
||||
for (let i = 0; i < n; i++) {
|
||||
insertHistory.run(n, `expired-handled-${i}-`, run.id, historyTask.id, `ordinary-${i}`)
|
||||
}
|
||||
let endpoint = n
|
||||
const read = vi.fn((id: string): ReturnType<typeof agentSessionConversationView> | null => {
|
||||
const record = sessionRecord(id)
|
||||
const index = id === SESSION_X ? 0 : id.startsWith('edge-') ? Number(id.slice(5)) : endpoint
|
||||
return agentSessionConversationView(
|
||||
index < endpoint
|
||||
? {
|
||||
...record,
|
||||
conversationCommand: {
|
||||
command: 'clear',
|
||||
runtimeFence: record.lease.runtimeFence,
|
||||
operationId: 'clear',
|
||||
callerKey: 'test',
|
||||
phase: 'committed',
|
||||
state: 'completed',
|
||||
replacementSessionId: `edge-${index + 1}`
|
||||
}
|
||||
}
|
||||
: record
|
||||
)
|
||||
})
|
||||
const projection = projectionFor(db, { getRecord: read, listRecords: () => [] })
|
||||
const tasks = vi.spyOn(db, 'getTask')
|
||||
const runs = vi.spyOn(db, 'getRun')
|
||||
const batch = vi.spyOn(db, 'getWorkerAttentionFactsForDispatches')
|
||||
const individual = vi.spyOn(db, 'getWorkerAttentionFacts')
|
||||
const roster = vi.spyOn(db, 'listSessionDispatchesForPresentation')
|
||||
let candidateVisits = 0
|
||||
let latestVisits = 0
|
||||
const native = installSqlVisitCounters({
|
||||
lineage_candidate_visit: () => {
|
||||
candidateVisits++
|
||||
return 1
|
||||
},
|
||||
lineage_latest_visit: () => {
|
||||
latestVisits++
|
||||
return 1
|
||||
}
|
||||
})
|
||||
const sql = traceDispatchPredicateVisits(db.db, {
|
||||
candidate: 'lineage_candidate_visit',
|
||||
latest: 'lineage_latest_visit'
|
||||
})
|
||||
const output = projection.buildByPaneKey()
|
||||
expect(Object.keys(output ?? {})).toHaveLength(n)
|
||||
expect(
|
||||
Object.values(output ?? {}).every(
|
||||
(context) => context.parentPaneKey === chatRow(`edge-${n}`)
|
||||
)
|
||||
).toBe(true)
|
||||
expect(read).toHaveBeenCalledTimes(n + n + 1)
|
||||
expect(tasks).toHaveBeenCalledTimes(n)
|
||||
expect(runs).toHaveBeenCalledTimes(1)
|
||||
expect(roster).toHaveBeenCalledTimes(1)
|
||||
expect(candidateVisits).toBe(3 * n)
|
||||
expect(latestVisits).toBe(n)
|
||||
expect(latestStatementMetrics(sql, native)).toEqual({
|
||||
cachedPrepareCalls: 1,
|
||||
executedStatements: 1,
|
||||
nativeCompilations: 1,
|
||||
addressEntries: n,
|
||||
returnedVerificationRows: n
|
||||
})
|
||||
expect(
|
||||
sql.mock.calls.filter(([query]) => query.includes('d.id AS dispatch_id'))
|
||||
).toHaveLength(1)
|
||||
expect(
|
||||
sql.mock.calls.filter(([query]) => query.includes('FROM attempt_observation_facts'))
|
||||
).toHaveLength(1)
|
||||
expect(batch).toHaveBeenCalledTimes(1)
|
||||
expect(individual).not.toHaveBeenCalled()
|
||||
endpoint++
|
||||
read.mockClear()
|
||||
expect(
|
||||
Object.values(projection.buildByPaneKey() ?? {}).every(
|
||||
(context) => context.parentPaneKey === chatRow(`edge-${endpoint}`)
|
||||
)
|
||||
).toBe(true)
|
||||
expect(read.mock.calls.length).toBeLessThanOrEqual(n + endpoint + 1)
|
||||
expect(latestStatementMetrics(sql, native)).toEqual({
|
||||
cachedPrepareCalls: 2,
|
||||
executedStatements: 2,
|
||||
nativeCompilations: 1,
|
||||
addressEntries: 2 * n,
|
||||
returnedVerificationRows: 2 * n
|
||||
})
|
||||
expect(candidateVisits).toBe(6 * n)
|
||||
expect(latestVisits).toBe(2 * n)
|
||||
const plans = dispatchQueryPlans(db.db, sql)
|
||||
expect(JSON.stringify(plans)).toContain(
|
||||
'idx_dispatch_active_assignee_handle (assignee_handle=?)'
|
||||
)
|
||||
expect(JSON.stringify(plans)).not.toMatch(/SCAN dispatch_contexts|USE TEMP B-TREE/)
|
||||
for (const failure of ['missing', 'error', 'cycle', 'prepared']) {
|
||||
read.mockImplementation((id) => {
|
||||
if (failure === 'error') {
|
||||
throw new Error('unreadable')
|
||||
}
|
||||
if (failure === 'missing') {
|
||||
return null
|
||||
}
|
||||
const record = sessionRecord(id)
|
||||
return agentSessionConversationView({
|
||||
...record,
|
||||
conversationCommand: {
|
||||
command: 'clear',
|
||||
runtimeFence: record.lease.runtimeFence,
|
||||
operationId: 'cycle',
|
||||
callerKey: 'test',
|
||||
phase: failure === 'prepared' ? 'prepared' : 'committed',
|
||||
state: 'completed',
|
||||
replacementSessionId: SESSION_X
|
||||
}
|
||||
})
|
||||
})
|
||||
expect(
|
||||
Object.values(projection.buildByPaneKey() ?? {}).every(
|
||||
(context) => context.parentPaneKey === chatRow(SESSION_X)
|
||||
)
|
||||
).toBe(true)
|
||||
}
|
||||
db.db
|
||||
.prepare(
|
||||
"UPDATE dispatch_contexts SET status = 'completed', completed_at = ? WHERE status IN ('pending', 'dispatched')"
|
||||
)
|
||||
.run(new Date(now - 7_200_000).toISOString())
|
||||
sql.mockClear()
|
||||
sql.executions.length = 0
|
||||
native.mockClear()
|
||||
candidateVisits = 0
|
||||
latestVisits = 0
|
||||
read.mockClear()
|
||||
expect(projection.buildByPaneKey()).toBeUndefined()
|
||||
expect(candidateVisits).toBe(3 * n)
|
||||
expect(latestVisits).toBe(0)
|
||||
expect(read).not.toHaveBeenCalled()
|
||||
expect(latestStatementMetrics(sql, native)).toEqual({
|
||||
cachedPrepareCalls: 0,
|
||||
executedStatements: 0,
|
||||
nativeCompilations: 0,
|
||||
addressEntries: 0,
|
||||
returnedVerificationRows: 0
|
||||
})
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it.each([20, 200])(
|
||||
'derives newest active null-address sessions from one complete batch at %i identities',
|
||||
(n) => {
|
||||
const db = new OrchestrationDb(':memory:')
|
||||
try {
|
||||
const run = db.createRun({
|
||||
objective: 'null-address history',
|
||||
coordinatorHandle: null,
|
||||
coordinatorPaneKey: null,
|
||||
coordinatorOrcaSessionId: SESSION_X
|
||||
})
|
||||
const task = db.createTask({ runId: run.id, spec: 'null-address selection' })
|
||||
const insert = db.db.prepare(`INSERT INTO dispatch_contexts
|
||||
(id, run_id, task_id, assignee_orca_session_id, status, completed_at)
|
||||
WITH RECURSIVE seq(n) AS (VALUES(1) UNION ALL SELECT n+1 FROM seq WHERE n < ?)
|
||||
SELECT ? || n, ?, ?, ?, 'completed', '2020-01-01T00:00:00Z' FROM seq`)
|
||||
for (let i = 0; i < n; i++) {
|
||||
const dispatch = db.createDispatchContext({
|
||||
taskId: db.createTask({ runId: run.id, spec: 'active selection' }).id,
|
||||
assigneeHandle: `old-${i}`,
|
||||
creator: { kind: 'system' },
|
||||
maxDepth: 99
|
||||
})
|
||||
db.db
|
||||
.prepare(
|
||||
'UPDATE dispatch_contexts SET assignee_handle = NULL, assignee_pane_key = NULL, assignee_orca_session_id = ? WHERE id = ?'
|
||||
)
|
||||
.run(`null-session-${i}`, dispatch.id)
|
||||
insert.run(n, `expired-${i}-`, run.id, task.id, `null-session-${i}`)
|
||||
}
|
||||
let candidates = 0
|
||||
installSqlVisitCounters({
|
||||
null_candidate_visit: () => {
|
||||
candidates++
|
||||
return 1
|
||||
}
|
||||
})
|
||||
const trace = traceDispatchPredicateVisits(db.db, { candidate: 'null_candidate_visit' })
|
||||
const roster = vi.spyOn(db, 'listSessionDispatchesForPresentation')
|
||||
const projection = projectionFor(db)
|
||||
const output = projection.buildByPaneKey() ?? {}
|
||||
expect(Object.keys(output)).toHaveLength(n)
|
||||
for (let i = 0; i < n; i++) {
|
||||
expect(output[chatRow(`null-session-${i}`)]?.parentPaneKey).toBe(chatRow(SESSION_X))
|
||||
}
|
||||
expect(roster).toHaveBeenCalledTimes(1)
|
||||
expect(candidates).toBe(n)
|
||||
expect(
|
||||
trace.mock.calls.filter(
|
||||
([q]) =>
|
||||
q.includes('rowid AS n') &&
|
||||
q.includes('assignee_orca_session_id') &&
|
||||
q.includes("AND status IN ('pending', 'dispatched')")
|
||||
)
|
||||
).toHaveLength(0)
|
||||
expect(
|
||||
trace.mock.calls.filter(([q]) => q.includes('idx_dispatch_session_presentation'))
|
||||
).toHaveLength(1)
|
||||
trace.mockRestore()
|
||||
// The newer handled active row contributes to the session maximum even if its address is suppressed.
|
||||
const handled = db.createDispatchContext({
|
||||
taskId: db.createTask({ runId: run.id, spec: 'active selection' }).id,
|
||||
assigneeHandle: 'new-handled',
|
||||
creator: { kind: 'system' },
|
||||
maxDepth: 99
|
||||
})
|
||||
db.db
|
||||
.prepare('UPDATE dispatch_contexts SET assignee_orca_session_id = ? WHERE id = ?')
|
||||
.run('null-session-0', handled.id)
|
||||
const newer = db.createDispatchContext({
|
||||
taskId: db.createTask({ runId: run.id, spec: 'active selection' }).id,
|
||||
assigneeHandle: 'newer-suppression',
|
||||
creator: { kind: 'system' },
|
||||
maxDepth: 99
|
||||
})
|
||||
db.db
|
||||
.prepare('UPDATE dispatch_contexts SET assignee_handle = ? WHERE id = ?')
|
||||
.run('new-handled', newer.id)
|
||||
const suppressed = projection.buildByPaneKey() ?? {}
|
||||
expect(suppressed[chatRow('null-session-0')]).toBeUndefined()
|
||||
expect(Object.keys(suppressed)).toHaveLength(n - 1)
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('selects indexed active/latest per address with exact ISO/SQLite recency and deterministic public collisions', () => {
|
||||
const db = new OrchestrationDb(':memory:')
|
||||
const now = Date.parse('2026-10-27T00:00:10.000Z')
|
||||
try {
|
||||
const emptyRoster = vi.spyOn(db, 'listSessionDispatchesForPresentation')
|
||||
const emptyRead = vi.fn(() => null)
|
||||
expect(
|
||||
projectionFor(db, { getRecord: emptyRead, listRecords: () => [] }).buildByPaneKey()
|
||||
).toBeUndefined()
|
||||
expect(emptyRoster).not.toHaveBeenCalled()
|
||||
expect(emptyRead).not.toHaveBeenCalled()
|
||||
const version = db.db.pragma('user_version', { simple: true })
|
||||
const run = db.createRun({
|
||||
objective: 'selection',
|
||||
coordinatorHandle: 'term_c',
|
||||
coordinatorPaneKey: 'c:11111111-1111-4111-8111-111111111111'
|
||||
})
|
||||
let counter = 0
|
||||
const add = (
|
||||
handle: string | null,
|
||||
session: string | null,
|
||||
status: string,
|
||||
completed: string | null
|
||||
) => {
|
||||
const row = db.createDispatchContext({
|
||||
taskId: db.createTask({ runId: run.id, spec: 'selection' }).id,
|
||||
assigneeHandle: `${handle}-${counter++}`,
|
||||
creator: { kind: 'system' },
|
||||
maxDepth: 99
|
||||
})
|
||||
db.db
|
||||
.prepare(
|
||||
'UPDATE dispatch_contexts SET assignee_handle = ?, assignee_orca_session_id = ?, status = ?, completed_at = ? WHERE id = ?'
|
||||
)
|
||||
.run(handle, session, status, completed, row.id)
|
||||
return row.id
|
||||
}
|
||||
add('suppressed', SESSION_X, 'completed', '2026-10-27T00:00:00Z')
|
||||
add('suppressed', null, 'failed', null)
|
||||
add('active-hidden', 'hidden', 'pending', null)
|
||||
add('active-hidden', null, 'pending', null)
|
||||
add('newer-expired', 'expired-hidden', 'completed', '2026-10-27T00:00:00Z')
|
||||
add('newer-expired', 'expired-hidden', 'completed', '2020-01-01T00:00:00Z')
|
||||
add('newer-invalid', 'invalid-hidden', 'completed', '2026-10-27T00:00:00Z')
|
||||
add('newer-invalid', 'invalid-hidden', 'completed', 'zzzz')
|
||||
const unhandledRecent = add('', 'session-recent', 'completed', '2026-10-27T00:00:00Z')
|
||||
const collisionHandle = add(
|
||||
'shared-address',
|
||||
'handle-namespace-session',
|
||||
'completed',
|
||||
'2026-10-27T00:00:00Z'
|
||||
)
|
||||
const collisionSession = add(null, 'shared-address', 'completed', '2026-10-27T00:00:00Z')
|
||||
const nullRecent = add(null, 'null-recent', 'completed', '2026-10-27T00:00:00Z')
|
||||
const boundary = add(
|
||||
'exact-boundary',
|
||||
'boundary',
|
||||
'completed',
|
||||
new Date(now - 1_800_000).toISOString()
|
||||
)
|
||||
add('stale-boundary', 'stale-boundary', 'completed', new Date(now - 1_800_001).toISOString())
|
||||
add(null, 'suppressed-session', 'completed', '2026-10-27T00:00:00Z')
|
||||
add('suppressed-session-handle', 'suppressed-session', 'completed', '2026-10-27T00:00:00Z')
|
||||
add('suppressed-session-handle', null, 'completed', '2026-10-27T00:00:00Z')
|
||||
const sqlite = add('sqlite', SESSION_Y, 'completed', '2026-10-26 23:59:59')
|
||||
add('expired', 'expired', 'completed', '2020-01-01T00:00:00Z')
|
||||
add('invalid', 'invalid', 'completed', 'zzzz')
|
||||
add('missing', 'missing', 'failed', null)
|
||||
const future = add('future', 'future', 'failed', '2099-01-01T00:00:00Z')
|
||||
const active = add('active', SESSION_Y, 'dispatched', null)
|
||||
add('same-address', 'active-old', 'pending', null)
|
||||
const newest = add('same-address', 'active-new', 'pending', null)
|
||||
const native = installSqlVisitCounters({
|
||||
selection_candidate: () => 1,
|
||||
selection_latest: () => 1
|
||||
})
|
||||
const trace = traceDispatchPredicateVisits(db.db, {
|
||||
candidate: 'selection_candidate',
|
||||
latest: 'selection_latest'
|
||||
})
|
||||
expect(db.listSessionDispatchesForPresentation(now).map((row) => row.dispatch.id)).toEqual([
|
||||
active,
|
||||
newest,
|
||||
sqlite,
|
||||
boundary,
|
||||
unhandledRecent,
|
||||
collisionHandle,
|
||||
collisionSession,
|
||||
nullRecent,
|
||||
future
|
||||
])
|
||||
const metrics = latestStatementMetrics(trace, native)
|
||||
expect(metrics.cachedPrepareCalls).toBe(3)
|
||||
expect(metrics.executedStatements).toBe(3)
|
||||
expect(metrics.nativeCompilations).toBe(3)
|
||||
expect(metrics.addressEntries).toBe(metrics.returnedVerificationRows)
|
||||
const latestExecutions = trace.executions.filter(({ query }) =>
|
||||
query.includes('rowid AS n FROM dispatch_contexts')
|
||||
)
|
||||
expect(new Set(latestExecutions.map(({ query }) => query)).size).toBe(3)
|
||||
expect(latestExecutions.map(({ parameters }) => JSON.parse(String(parameters[0])))).toEqual([
|
||||
['active', 'active-hidden', 'same-address'],
|
||||
[
|
||||
'sqlite',
|
||||
'exact-boundary',
|
||||
'suppressed',
|
||||
'newer-expired',
|
||||
'shared-address',
|
||||
'suppressed-session-handle',
|
||||
'future'
|
||||
],
|
||||
['session-recent', 'shared-address', 'null-recent', 'suppressed-session']
|
||||
])
|
||||
const plans = dispatchQueryPlans(db.db, trace)
|
||||
const planText = JSON.stringify(plans)
|
||||
for (const index of [
|
||||
'idx_dispatch_active_assignee_handle (assignee_handle=?)',
|
||||
'idx_dispatch_assignee_handle (assignee_handle=?)',
|
||||
'idx_dispatch_assignee_orca_session_id (assignee_orca_session_id=?)',
|
||||
'idx_dispatch_session_presentation (status=? AND completed_at>?)'
|
||||
]) {
|
||||
expect(planText).toContain(index)
|
||||
}
|
||||
expect(planText).not.toMatch(/SCAN dispatch_contexts|USE TEMP B-TREE/)
|
||||
trace.mockRestore()
|
||||
add('', 'session-recent', 'completed', 'zzzz')
|
||||
expect(
|
||||
db
|
||||
.listSessionDispatchesForPresentation(now)
|
||||
.some((row) => row.dispatch.id === unhandledRecent)
|
||||
).toBe(false)
|
||||
const noHandle = add('null-handle', 'null-address', 'dispatched', null)
|
||||
db.db.prepare('UPDATE dispatch_contexts SET assignee_handle = NULL WHERE id = ?').run(noHandle)
|
||||
expect(
|
||||
db.listSessionDispatchesForPresentation(now).some((row) => row.dispatch.id === noHandle)
|
||||
).toBe(true)
|
||||
const columns = db.db.prepare('PRAGMA index_info(idx_dispatch_session_presentation)').all()
|
||||
expect(columns).toMatchObject([{ name: 'status' }, { name: 'completed_at' }])
|
||||
db.createMailboxDeliveryIndexesIfPossible()
|
||||
expect(db.db.pragma('user_version', { simple: true })).toBe(version)
|
||||
const plan = JSON.stringify(
|
||||
db.db
|
||||
.prepare(
|
||||
"EXPLAIN QUERY PLAN SELECT rowid FROM dispatch_contexts INDEXED BY idx_dispatch_session_presentation WHERE assignee_orca_session_id IS NOT NULL AND status = 'completed' AND completed_at >= ?"
|
||||
)
|
||||
.all('2026-10-26')
|
||||
)
|
||||
expect(plan).toContain('idx_dispatch_session_presentation (status=? AND completed_at>?)')
|
||||
expect(projectionFor(db).buildByPaneKey()?.[chatRow(SESSION_Y)]?.dispatchId).toBe(active)
|
||||
const latestActive = add('cleared-address', SESSION_X, 'pending', null)
|
||||
add('newer-recent-address', SESSION_Y, 'completed', '2099-01-01T00:00:00Z')
|
||||
const read = (id: string) => {
|
||||
const record = sessionRecord(id)
|
||||
return agentSessionConversationView(
|
||||
id === SESSION_X
|
||||
? {
|
||||
...record,
|
||||
conversationCommand: {
|
||||
command: 'clear',
|
||||
runtimeFence: record.lease.runtimeFence,
|
||||
operationId: 'destination',
|
||||
callerKey: 'test',
|
||||
phase: 'committed',
|
||||
state: 'completed',
|
||||
replacementSessionId: SESSION_Y
|
||||
}
|
||||
}
|
||||
: record
|
||||
)
|
||||
}
|
||||
const sharedDestination = projectionFor(db, {
|
||||
getRecord: read,
|
||||
listRecords: () => []
|
||||
}).buildByPaneKey()
|
||||
expect(sharedDestination?.[chatRow(SESSION_Y)]?.dispatchId).toBe(latestActive)
|
||||
expect(sharedDestination?.[chatRow(SESSION_X)]).toBeUndefined()
|
||||
} finally {
|
||||
db.close()
|
||||
}
|
||||
})
|
||||
|
||||
it.skipIf(process.env.ORCA_LINEAGE_BENCH !== '1')(
|
||||
'benchmarks retained-history ranges and first derived-index creation (admitted machines only)',
|
||||
() => {
|
||||
for (const nullAddress of [false, true]) {
|
||||
for (const history of [10_000, 100_000]) {
|
||||
for (const terminalHistory of [0, 100_000]) {
|
||||
for (const attentionFacts of [0, 2_000]) {
|
||||
benchmarkPresentationCorpus(
|
||||
{ nullAddress, history, terminalHistory, attentionFacts, parentSessionId: SESSION_X },
|
||||
(db) => projectionFor(db).buildByPaneKey()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('reads reopened/backfilled assignments and committed clear facts from the already opened store before host installation', async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), 'orca-lineage-contract-'))
|
||||
const root = sessionRecord(SESSION_X)
|
||||
const successor = COORDINATOR_CHAT
|
||||
await seedTestAgentSessionRecordStore(directory, {
|
||||
records: [
|
||||
{
|
||||
...root,
|
||||
conversationCommand: {
|
||||
command: 'clear',
|
||||
runtimeFence: root.lease.runtimeFence,
|
||||
operationId: 'clear',
|
||||
callerKey: 'test',
|
||||
phase: 'committed',
|
||||
state: 'completed',
|
||||
replacementSessionId: successor
|
||||
}
|
||||
},
|
||||
sessionRecord(successor),
|
||||
sessionRecord(SESSION_Y)
|
||||
],
|
||||
sessionTabs: [
|
||||
{ tabId: 'reserved-successor', sessionId: successor },
|
||||
{ tabId: 'reserved-worker', sessionId: SESSION_Y }
|
||||
]
|
||||
})
|
||||
closeTestJournalHostDatabase(directory)
|
||||
const path = join(directory, 'orchestration.sqlite')
|
||||
let db = new OrchestrationDb(path)
|
||||
let opened = false
|
||||
try {
|
||||
const run = db.createRun({
|
||||
objective: 'cold public display',
|
||||
coordinatorHandle: null,
|
||||
coordinatorPaneKey: null,
|
||||
coordinatorOrcaSessionId: SESSION_X
|
||||
})
|
||||
const privateHandles: string[] = []
|
||||
const privatePanes: string[] = []
|
||||
for (const id of [SESSION_Y, successor]) {
|
||||
const handle = mintStructuredWorkerHandle()
|
||||
const pane = mintStructuredWorkerPaneKey(id)
|
||||
privateHandles.push(handle)
|
||||
privatePanes.push(pane)
|
||||
db.createDispatchContext({
|
||||
taskId: db.createTask({ runId: run.id, spec: 'persisted worker' }).id,
|
||||
assigneeHandle: handle,
|
||||
assigneePaneKey: pane,
|
||||
processIncarnation: structuredWorkerProcessIncarnation(id),
|
||||
creator: { kind: 'session', orcaSessionId: SESSION_X },
|
||||
maxDepth: 99
|
||||
})
|
||||
}
|
||||
db.db.exec('UPDATE dispatch_contexts SET assignee_orca_session_id = NULL')
|
||||
db.close()
|
||||
db = new OrchestrationDb(path)
|
||||
const runtime = new OrcaRuntimeService()
|
||||
runtime.setOrchestrationDb(db)
|
||||
hostRef.current = null
|
||||
const before = runtime['agentOrchestrationProjection'].buildByPaneKey()
|
||||
expect(before?.[chatRow(SESSION_Y)]?.parentPaneKey).toBe(chatRow(SESSION_X))
|
||||
await openAgentSessionRecordStoreOnce({
|
||||
stateDirectory: directory,
|
||||
hostId: 'local',
|
||||
logger: recordingStructuredAgentSessionLogger().logger
|
||||
})
|
||||
opened = true
|
||||
const result = runtime['agentOrchestrationProjection'].buildByPaneKey()
|
||||
expect(result?.[chatRow(SESSION_Y)]?.parentPaneKey).toBe(chatRow(successor))
|
||||
expect(result?.[chatRow(successor)]?.parentPaneKey).toBeUndefined()
|
||||
for (const privateValue of [...privateHandles, ...privatePanes]) {
|
||||
expect(JSON.stringify(result)).not.toContain(privateValue)
|
||||
}
|
||||
for (const status of ['idle', 'working', 'attention'] as const) {
|
||||
runtime.onStructuredSessionStatusForMail({ sessionId: SESSION_Y, status })
|
||||
expect(
|
||||
runtime['agentOrchestrationProjection'].buildByPaneKey()?.[chatRow(SESSION_Y)]
|
||||
?.parentPaneKey
|
||||
).toBe(chatRow(successor))
|
||||
}
|
||||
} finally {
|
||||
if (opened) {
|
||||
;(await releaseAgentSessionRecordStore())?.journalDatabase.close()
|
||||
}
|
||||
db.close()
|
||||
rmSync(directory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
AGENT_STATUS_STALE_AFTER_MS,
|
||||
type AgentStatusOrchestrationContext
|
||||
} from '../../shared/agent-status-types'
|
||||
import type { AgentStatusOrchestrationContext } from '../../shared/agent-status-types'
|
||||
import type { FleetAgentStatusEvidence } from '../../shared/orchestration-fleet-agent-status-evidence'
|
||||
import { buildOrchestrationTaskDisplayMetadata } from '../../shared/orchestration-task-display'
|
||||
import { parsePaneKey } from '../../shared/stable-pane-id'
|
||||
@@ -13,9 +10,14 @@ import {
|
||||
projectWorkerAttentionContext
|
||||
} from './orchestration/worker-attention-context'
|
||||
import { worktreeIdsEqual } from '../../shared/worktree/id'
|
||||
import type { AgentSessionRecordReader } from './orchestration/structured-session-lineage'
|
||||
import { createOrchestrationProjectionScope } from './runtime-agent-orchestration-scope'
|
||||
import { isRecentPresentationDispatch } from './orchestration/db/dispatch-context/dispatch-lookup'
|
||||
import { currentRunCoordinatorOrcaSessionId } from './orchestration/db/runs/run-coordinator-orca-session'
|
||||
import type { DispatchContextRow } from './orchestration/types'
|
||||
import type { PtyPaneOf } from './terminal/surface/pty-pane-query'
|
||||
|
||||
type RuntimeAgentOrchestrationDependencies = {
|
||||
export type RuntimeAgentOrchestrationDependencies = {
|
||||
getDb(): OrchestrationDb | null
|
||||
getLeaves(): Iterable<RuntimeLeafRecord>
|
||||
getPtys(): Iterable<RuntimePtyWorktreeRecord>
|
||||
@@ -28,6 +30,7 @@ type RuntimeAgentOrchestrationDependencies = {
|
||||
getPaneKey(handle: string): string | null
|
||||
getDispatchAuthority(handle: string): OrchestrationCompatibilityTerminalAuthority | null
|
||||
getAgentStatusSnapshot(): readonly FleetAgentStatusEvidence[]
|
||||
getSessionRecords(): AgentSessionRecordReader | null
|
||||
}
|
||||
|
||||
export class RuntimeAgentOrchestrationProjection {
|
||||
@@ -38,6 +41,7 @@ export class RuntimeAgentOrchestrationProjection {
|
||||
if (!db || db.hasAnyDispatchContexts?.() === false) {
|
||||
return undefined
|
||||
}
|
||||
const scope = createOrchestrationProjectionScope(db, this.deps)
|
||||
const contexts: Record<string, AgentStatusOrchestrationContext> = {}
|
||||
const evidenceByPaneKey = new Map(
|
||||
this.deps.getAgentStatusSnapshot().map((evidence) => [evidence.activity.paneKey, evidence])
|
||||
@@ -52,11 +56,16 @@ export class RuntimeAgentOrchestrationProjection {
|
||||
const handle = this.deps.issueLeafHandle(leaf)
|
||||
queriedHandles.add(handle)
|
||||
const paneKey = this.deps.makePaneKey(leaf)
|
||||
const context = this.getForHandle(handle, db, {
|
||||
paneKey,
|
||||
evidence: evidenceByPaneKey.get(paneKey),
|
||||
deferAttention: batchAttention
|
||||
})
|
||||
const context = this.getForHandle(
|
||||
handle,
|
||||
db,
|
||||
{
|
||||
paneKey,
|
||||
evidence: evidenceByPaneKey.get(paneKey),
|
||||
deferAttention: batchAttention
|
||||
},
|
||||
scope
|
||||
)
|
||||
if (context) {
|
||||
contexts[paneKey] = context
|
||||
}
|
||||
@@ -71,21 +80,41 @@ export class RuntimeAgentOrchestrationProjection {
|
||||
continue
|
||||
}
|
||||
queriedHandles.add(handle)
|
||||
const context = this.getForHandle(handle, db, {
|
||||
paneKey,
|
||||
evidence: evidenceByPaneKey.get(paneKey),
|
||||
deferAttention: batchAttention
|
||||
})
|
||||
const context = this.getForHandle(
|
||||
handle,
|
||||
db,
|
||||
{
|
||||
paneKey,
|
||||
evidence: evidenceByPaneKey.get(paneKey),
|
||||
deferAttention: batchAttention
|
||||
},
|
||||
scope
|
||||
)
|
||||
if (context) {
|
||||
contexts[paneKey] = context
|
||||
}
|
||||
}
|
||||
for (const [paneKey, { dispatch }] of scope.sessionDispatches()) {
|
||||
if (!contexts[paneKey]) {
|
||||
contexts[paneKey] = this.projectDispatch(
|
||||
dispatch,
|
||||
dispatch.assignee_handle ?? '',
|
||||
db,
|
||||
{
|
||||
paneKey,
|
||||
evidence: evidenceByPaneKey.get(paneKey),
|
||||
deferAttention: batchAttention
|
||||
},
|
||||
scope
|
||||
)
|
||||
}
|
||||
}
|
||||
const entries = Object.entries(contexts)
|
||||
if (entries.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
if (batchAttention) {
|
||||
const now = Date.now()
|
||||
const now = scope.now
|
||||
const factsByDispatch = db.getWorkerAttentionFactsForDispatches(
|
||||
entries.map(([, context]) => context.dispatchId),
|
||||
now
|
||||
@@ -116,15 +145,27 @@ export class RuntimeAgentOrchestrationProjection {
|
||||
paneKey?: string
|
||||
evidence?: FleetAgentStatusEvidence
|
||||
deferAttention?: boolean
|
||||
} = {}
|
||||
} = {},
|
||||
scope = createOrchestrationProjectionScope(db, this.deps)
|
||||
): AgentStatusOrchestrationContext | undefined {
|
||||
const { paneKey, evidence, deferAttention = false } = options
|
||||
const { paneKey } = options
|
||||
const dispatch =
|
||||
db?.getActiveDispatchForTerminal?.(handle, paneKey) ?? this.getRecent(handle, db)
|
||||
db?.getActiveDispatchForTerminal?.(handle, paneKey) ?? this.getRecent(handle, db, scope.now)
|
||||
if (!dispatch) {
|
||||
return undefined
|
||||
}
|
||||
const task = db?.getTask?.(dispatch.task_id, dispatch.run_id)
|
||||
return this.projectDispatch(dispatch, handle, db, options, scope)
|
||||
}
|
||||
|
||||
private projectDispatch(
|
||||
dispatch: DispatchContextRow,
|
||||
handle: string,
|
||||
db: OrchestrationDb | null,
|
||||
options: { paneKey?: string; evidence?: FleetAgentStatusEvidence; deferAttention?: boolean },
|
||||
scope: ReturnType<typeof createOrchestrationProjectionScope>
|
||||
): AgentStatusOrchestrationContext {
|
||||
const { paneKey, evidence, deferAttention = false } = options
|
||||
const task = scope.task(dispatch.task_id, dispatch.run_id)
|
||||
const display =
|
||||
typeof task?.spec === 'string'
|
||||
? buildOrchestrationTaskDisplayMetadata({
|
||||
@@ -134,7 +175,7 @@ export class RuntimeAgentOrchestrationProjection {
|
||||
})
|
||||
: { taskTitle: '', displayName: '' }
|
||||
const owningRun =
|
||||
task?.run_id && task.run_id === dispatch.run_id ? db?.getRun?.(dispatch.run_id) : undefined
|
||||
task?.run_id && task.run_id === dispatch.run_id ? scope.run(dispatch.run_id) : undefined
|
||||
const runCoordinatorHandle = owningRun?.coordinator_handle ?? undefined
|
||||
const legacyActiveRun =
|
||||
owningRun?.legacy === 1 && (dispatch.status === 'pending' || dispatch.status === 'dispatched')
|
||||
@@ -170,7 +211,7 @@ export class RuntimeAgentOrchestrationProjection {
|
||||
currentCreatorPane &&
|
||||
storedCreatorPane.leafId === currentCreatorPane.leafId))
|
||||
)
|
||||
const paneRun = creatorPaneKey ? db?.getCurrentRunForPane?.(creatorPaneKey) : undefined
|
||||
const paneRun = creatorPaneKey ? scope.pane(creatorPaneKey) : undefined
|
||||
const sameRunCreatorDispatch = Boolean(
|
||||
task?.creator_dispatch_id &&
|
||||
task.creator_dispatch_run_id === owningRun?.id &&
|
||||
@@ -194,28 +235,48 @@ export class RuntimeAgentOrchestrationProjection {
|
||||
sameCreatorPane
|
||||
? (creatorPaneHandle ?? undefined)
|
||||
: undefined
|
||||
const coordinator = this.resolveLivePane(
|
||||
coordinatorHandle,
|
||||
owningRun?.legacy === 0 ? owningRun.coordinator_pane_key : null
|
||||
)
|
||||
const creator = currentCreatorHandle
|
||||
? {
|
||||
handle: currentCreatorHandle,
|
||||
paneKey: this.deps.getPaneKey(currentCreatorHandle) ?? undefined
|
||||
}
|
||||
: creatorLineageInRun
|
||||
? this.resolveLivePane(creatorPaneHandle, creatorPaneKey ?? null)
|
||||
: undefined
|
||||
const selfSession = dispatch.assignee_orca_session_id
|
||||
const selfPane = selfSession ? scope.resolve(selfSession) : paneKey
|
||||
const coordinatorSession =
|
||||
owningRun?.legacy === 0 ? currentRunCoordinatorOrcaSessionId(owningRun) : null
|
||||
// Why: a chat party nests by its visible row; its handle and minted pane key match no row.
|
||||
const coordinator: { handle?: string; paneKey?: string } = coordinatorSession
|
||||
? { paneKey: scope.resolve(coordinatorSession) }
|
||||
: scope.resolveTerminal(
|
||||
coordinatorHandle,
|
||||
owningRun?.legacy === 0 ? owningRun.coordinator_pane_key : null
|
||||
)
|
||||
const creatorSession =
|
||||
dispatch.creator_orca_session_id && dispatch.creator_orca_session_id !== selfSession
|
||||
? dispatch.creator_orca_session_id
|
||||
: null
|
||||
const creator: { handle?: string; paneKey?: string } | undefined = creatorSession
|
||||
? { paneKey: scope.resolve(creatorSession) }
|
||||
: currentCreatorHandle
|
||||
? scope.resolveTerminal(currentCreatorHandle, creatorAuthority?.paneKey ?? null)
|
||||
: creatorLineageInRun
|
||||
? scope.resolveTerminal(creatorPaneHandle, creatorPaneKey ?? null)
|
||||
: undefined
|
||||
const coordinatorIsSelf =
|
||||
coordinator.handle === handle ||
|
||||
(coordinatorSession !== null && coordinatorSession === selfSession) ||
|
||||
(paneKey !== undefined &&
|
||||
coordinator.paneKey !== undefined &&
|
||||
coordinator.paneKey === paneKey)
|
||||
// Why: a creator whose pane is gone still has a coordinator to nest under.
|
||||
const parent = creator?.handle ? creator : coordinatorIsSelf ? {} : coordinator
|
||||
const parent =
|
||||
(creator?.handle || (creatorSession && creator?.paneKey)) &&
|
||||
(!selfPane || creator?.paneKey !== selfPane) &&
|
||||
creator?.handle !== handle
|
||||
? creator
|
||||
: coordinatorIsSelf
|
||||
? {}
|
||||
: coordinator.paneKey === selfPane
|
||||
? {}
|
||||
: coordinator
|
||||
const attention =
|
||||
!deferAttention && db && typeof db.getWorkerAttentionFacts === 'function'
|
||||
? buildWorkerAttentionContext({ db, dispatch, task, evidence })
|
||||
? buildWorkerAttentionContext({ db, dispatch, task, evidence, now: scope.now })
|
||||
: undefined
|
||||
return {
|
||||
taskId: dispatch.task_id,
|
||||
@@ -231,43 +292,8 @@ export class RuntimeAgentOrchestrationProjection {
|
||||
}
|
||||
}
|
||||
|
||||
/** Never publish stale process-local handles; re-resolve the durable pane key, retaining it while offline so restored rows can nest again. */
|
||||
private resolveLivePane(
|
||||
storedHandle: string | null | undefined,
|
||||
storedPaneKey: string | null
|
||||
): { handle?: string; paneKey?: string } {
|
||||
if (storedHandle && this.deps.getWorktreeId(storedHandle) !== null) {
|
||||
return {
|
||||
handle: storedHandle,
|
||||
paneKey: this.deps.getPaneKey(storedHandle) ?? storedPaneKey ?? undefined
|
||||
}
|
||||
}
|
||||
if (!storedPaneKey) {
|
||||
return {}
|
||||
}
|
||||
const liveHandle = this.deps.getHandleForPaneKey(storedPaneKey)
|
||||
if (!liveHandle) {
|
||||
return { paneKey: storedPaneKey }
|
||||
}
|
||||
return { handle: liveHandle, paneKey: this.deps.getPaneKey(liveHandle) ?? storedPaneKey }
|
||||
}
|
||||
|
||||
private getRecent(handle: string, db: OrchestrationDb | null) {
|
||||
private getRecent(handle: string, db: OrchestrationDb | null, now: number) {
|
||||
const dispatch = db?.getLatestDispatchForTerminal?.(handle)
|
||||
if (
|
||||
!dispatch?.completed_at ||
|
||||
dispatch.status === 'pending' ||
|
||||
dispatch.status === 'dispatched'
|
||||
) {
|
||||
return undefined
|
||||
}
|
||||
const completedAt = Date.parse(
|
||||
dispatch.completed_at.includes('T')
|
||||
? dispatch.completed_at
|
||||
: `${dispatch.completed_at.replace(' ', 'T')}Z`
|
||||
)
|
||||
return Number.isFinite(completedAt) && Date.now() - completedAt <= AGENT_STATUS_STALE_AFTER_MS
|
||||
? dispatch
|
||||
: undefined
|
||||
return dispatch && isRecentPresentationDispatch(dispatch, now) ? dispatch : undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import type { OrchestrationDb } from './orchestration/db'
|
||||
import type { RuntimeAgentOrchestrationDependencies } from './runtime-agent-orchestration-projection'
|
||||
import { createCanonicalSessionPaneResolver } from './orchestration/structured-session-lineage'
|
||||
import { isStructuredWorkerHandle } from '../../shared/structured-worker-handle'
|
||||
|
||||
export function createOrchestrationProjectionScope(
|
||||
db: OrchestrationDb | null,
|
||||
deps: RuntimeAgentOrchestrationDependencies
|
||||
) {
|
||||
const tasks = new Map<string, ReturnType<OrchestrationDb['getTask']>>()
|
||||
const runs = new Map<string, ReturnType<OrchestrationDb['getRun']>>()
|
||||
const panes = new Map<string, ReturnType<OrchestrationDb['getCurrentRunForPane']>>()
|
||||
const now = Date.now()
|
||||
const resolve = createCanonicalSessionPaneResolver(deps.getSessionRecords())
|
||||
return {
|
||||
now,
|
||||
resolve,
|
||||
sessionDispatches: () => {
|
||||
const destinations = new Map<
|
||||
string,
|
||||
ReturnType<OrchestrationDb['listSessionDispatchesForPresentation']>[number]
|
||||
>()
|
||||
for (const candidate of db?.listSessionDispatchesForPresentation(now) ?? []) {
|
||||
if (!candidate.dispatch.assignee_orca_session_id) {
|
||||
continue
|
||||
}
|
||||
const paneKey = resolve(candidate.dispatch.assignee_orca_session_id)
|
||||
const previous = destinations.get(paneKey)
|
||||
const active =
|
||||
candidate.dispatch.status === 'pending' || candidate.dispatch.status === 'dispatched'
|
||||
const previousActive =
|
||||
previous?.dispatch.status === 'pending' || previous?.dispatch.status === 'dispatched'
|
||||
if (
|
||||
!previous ||
|
||||
(active && !previousActive) ||
|
||||
(active === previousActive && candidate.order > previous.order)
|
||||
) {
|
||||
destinations.set(paneKey, candidate)
|
||||
}
|
||||
}
|
||||
return destinations
|
||||
},
|
||||
resolveTerminal: (storedHandle: string | null | undefined, storedPaneKey: string | null) =>
|
||||
resolveLivePane(deps, storedHandle, storedPaneKey),
|
||||
task: (id: string, run: string) => {
|
||||
const key = `${run}:${id}`
|
||||
if (!tasks.has(key)) {
|
||||
tasks.set(key, db?.getTask?.(id, run))
|
||||
}
|
||||
return tasks.get(key)
|
||||
},
|
||||
run: (id: string) => {
|
||||
if (!runs.has(id)) {
|
||||
runs.set(id, db?.getRun?.(id))
|
||||
}
|
||||
return runs.get(id)
|
||||
},
|
||||
pane: (id: string) => {
|
||||
if (!panes.has(id)) {
|
||||
panes.set(id, db?.getCurrentRunForPane?.(id))
|
||||
}
|
||||
return panes.get(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Never publish stale process-local handles; re-resolve the durable pane key, retaining it while offline so restored rows can nest again. */
|
||||
function resolveLivePane(
|
||||
deps: RuntimeAgentOrchestrationDependencies,
|
||||
storedHandle: string | null | undefined,
|
||||
storedPaneKey: string | null
|
||||
): { handle?: string; paneKey?: string } {
|
||||
// Structured mailbox panes are credentials, including stale rows with no current public proof.
|
||||
if (isStructuredWorkerHandle(storedHandle)) {
|
||||
return {}
|
||||
}
|
||||
if (storedHandle && deps.getWorktreeId(storedHandle) !== null) {
|
||||
return {
|
||||
handle: storedHandle,
|
||||
paneKey: deps.getPaneKey(storedHandle) ?? storedPaneKey ?? undefined
|
||||
}
|
||||
}
|
||||
if (storedPaneKey?.startsWith('structured-agent-session-')) {
|
||||
return {}
|
||||
}
|
||||
if (!storedPaneKey) {
|
||||
return {}
|
||||
}
|
||||
const liveHandle = deps.getHandleForPaneKey(storedPaneKey)
|
||||
if (isStructuredWorkerHandle(liveHandle)) {
|
||||
return {}
|
||||
}
|
||||
if (!liveHandle) {
|
||||
return { paneKey: storedPaneKey }
|
||||
}
|
||||
return { handle: liveHandle, paneKey: deps.getPaneKey(liveHandle) ?? storedPaneKey }
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
import { afterEach, vi } from 'vitest'
|
||||
import { OrchestrationDb } from './orchestration/db'
|
||||
import type { TaskRuntimeLineageRow } from './orchestration/db/run-list-page'
|
||||
import type { CoordinatorRun, DispatchContextRow, RunRow } from './orchestration/types'
|
||||
|
||||
const databases = new Set<OrchestrationDb>()
|
||||
const createdAt = '2026-10-27T12:00:00Z'
|
||||
|
||||
afterEach(() => {
|
||||
for (const db of databases) {
|
||||
db.close()
|
||||
}
|
||||
databases.clear()
|
||||
})
|
||||
|
||||
export function makeRuntimeOrchestrationDb(): OrchestrationDb {
|
||||
const db = new OrchestrationDb(':memory:')
|
||||
databases.add(db)
|
||||
// These scenarios project supplied terminal contexts even without persisted Dispatches.
|
||||
vi.spyOn(db, 'hasAnyDispatchContexts').mockReturnValue(true)
|
||||
return db
|
||||
}
|
||||
|
||||
export function dispatchContextRow(fields: Partial<DispatchContextRow>): DispatchContextRow {
|
||||
return {
|
||||
id: 'fixture-dispatch',
|
||||
run_id: 'fixture-run',
|
||||
task_id: 'fixture-task',
|
||||
contract_version: 0,
|
||||
launch_token_hash: null,
|
||||
assignee_handle: null,
|
||||
assignee_pane_key: null,
|
||||
assignee_orca_session_id: null,
|
||||
capability_hash: null,
|
||||
process_incarnation: null,
|
||||
capability_revoked_at: null,
|
||||
retry_of_dispatch_id: null,
|
||||
creator_dispatch_id: null,
|
||||
creator_handle: null,
|
||||
creator_pane_key: null,
|
||||
creator_orca_session_id: null,
|
||||
host_scope: null,
|
||||
status: 'dispatched',
|
||||
failure_count: 0,
|
||||
last_failure: null,
|
||||
termination_reason: null,
|
||||
depth: 1,
|
||||
consumer_generation: 0,
|
||||
dispatched_at: createdAt,
|
||||
completed_at: null,
|
||||
created_at: createdAt,
|
||||
last_heartbeat_at: null,
|
||||
...fields
|
||||
}
|
||||
}
|
||||
|
||||
export function taskLineageRow(fields: Partial<TaskRuntimeLineageRow>): TaskRuntimeLineageRow {
|
||||
return {
|
||||
id: 'fixture-task',
|
||||
run_id: 'fixture-run',
|
||||
parent_id: null,
|
||||
created_by_terminal_handle: null,
|
||||
created_by_pane_key: null,
|
||||
created_by_process_incarnation: null,
|
||||
created_by_run_generation: null,
|
||||
task_title: null,
|
||||
display_name: null,
|
||||
spec: '',
|
||||
status: 'ready',
|
||||
deps: '[]',
|
||||
result: null,
|
||||
created_at: createdAt,
|
||||
completed_at: null,
|
||||
creator_dispatch_id: null,
|
||||
creator_dispatch_run_id: null,
|
||||
creator_dispatch_pane_key: null,
|
||||
creator_dispatch_process_incarnation: null,
|
||||
...fields
|
||||
}
|
||||
}
|
||||
|
||||
export function runRow(fields: Partial<RunRow>): RunRow {
|
||||
return {
|
||||
id: 'fixture-run',
|
||||
objective: '',
|
||||
home_database: 'this_database',
|
||||
coordinator_handle: null,
|
||||
coordinator_pane_key: null,
|
||||
coordinator_orca_session_id: null,
|
||||
coordinator_orca_session_id_generation: null,
|
||||
consumer_generation: 0,
|
||||
legacy: 0,
|
||||
created_at: createdAt,
|
||||
updated_at: createdAt,
|
||||
...fields
|
||||
}
|
||||
}
|
||||
|
||||
export function coordinatorRunRow(fields: Partial<CoordinatorRun>): CoordinatorRun {
|
||||
return {
|
||||
id: 'fixture-coordinator',
|
||||
spec: '',
|
||||
status: 'running',
|
||||
coordinator_handle: 'term_fixture',
|
||||
poll_interval_ms: 2000,
|
||||
created_at: createdAt,
|
||||
completed_at: null,
|
||||
scheduler_lost_at: null,
|
||||
...fields
|
||||
}
|
||||
}
|
||||
@@ -418,6 +418,7 @@ function makeOrchestrationDbStub(toHandle: () => string) {
|
||||
})
|
||||
},
|
||||
db: {
|
||||
listSessionDispatchesForPresentation: () => [],
|
||||
// Mirrors the real query: `read = 0 AND delivered_at IS NULL`.
|
||||
getUndeliveredUnreadMessages: (handle: string) =>
|
||||
rows.filter((row) => row.to_handle === handle && row.read === 0 && !row.delivered_at),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getStructuredAgentSessionTabsByPaneKey } from '@/components/native-chat/structured-agent-session-tabs'
|
||||
import { parseAgentStatusPaneIdentity } from '../../../../shared/agent-status-worktree-attribution'
|
||||
import { migrationUnsupportedToAgentStatusEntry } from '../../../../shared/migration-unsupported-agent-entry'
|
||||
import { parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import type { TerminalTab } from '../../../../shared/terminal-tab-types'
|
||||
@@ -75,7 +77,14 @@ export function appendUnsupportedAndRetainedEvents(context: {
|
||||
}
|
||||
|
||||
for (const [paneKey, retained] of Object.entries(args.retainedAgentsByPaneKey)) {
|
||||
if (!parsePaneKey(paneKey)) {
|
||||
if (
|
||||
!parseAgentStatusPaneIdentity(
|
||||
paneKey,
|
||||
args.unifiedTabsByWorktree
|
||||
? getStructuredAgentSessionTabsByPaneKey(args.unifiedTabsByWorktree).get(paneKey)
|
||||
: undefined
|
||||
)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
const owner = resolveOwner(
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { getStructuredAgentSessionTabsByPaneKey } from '@/components/native-chat/structured-agent-session-tabs'
|
||||
import { parseAgentStatusPaneIdentity } from '../../../../shared/agent-status-worktree-attribution'
|
||||
import { freshActivityLiveAgentState } from './activity-event-state'
|
||||
import type { RetainedAgentEntry } from '@/store/slices/agent-status/agent-status-contract'
|
||||
import type {
|
||||
@@ -8,7 +10,6 @@ import type {
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import type { StructuredChatTabNames } from '@/runtime/structured-chat-tab-name'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import { parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import type { Tab } from '../../../../shared/tab-types'
|
||||
import type { TerminalTab } from '../../../../shared/terminal-tab-types'
|
||||
import type { Worktree } from '../../../../shared/worktree/types'
|
||||
@@ -83,7 +84,12 @@ export function buildActivityEvents(
|
||||
}
|
||||
|
||||
for (const [paneKey, entry] of Object.entries(args.agentStatusByPaneKey)) {
|
||||
const parsed = parsePaneKey(paneKey)
|
||||
const parsed = parseAgentStatusPaneIdentity(
|
||||
paneKey,
|
||||
args.unifiedTabsByWorktree
|
||||
? getStructuredAgentSessionTabsByPaneKey(args.unifiedTabsByWorktree).get(paneKey)
|
||||
: undefined
|
||||
)
|
||||
if (!parsed) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { selectVisibleAgentOrchestration } from '@/runtime/visible-agent-orchestration'
|
||||
import { useDeferredValue, useMemo, useRef } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { useAppStore } from '@/store'
|
||||
@@ -111,7 +112,7 @@ export function useAgentPaneThreads(args: {
|
||||
const storeData = useAppStore(
|
||||
useShallow((s) => ({
|
||||
agentStatusByPaneKey: s.agentStatusByPaneKey,
|
||||
runtimeAgentOrchestrationByPaneKey: s.runtimeAgentOrchestrationByPaneKey,
|
||||
runtimeAgentOrchestrationByPaneKey: selectVisibleAgentOrchestration(s),
|
||||
migrationUnsupportedByPtyId: s.migrationUnsupportedByPtyId,
|
||||
retainedAgentsByPaneKey: s.retainedAgentsByPaneKey,
|
||||
tabsByWorktree: s.tabsByWorktree,
|
||||
|
||||
@@ -70,7 +70,14 @@ export type DashboardSnapshotState = Pick<
|
||||
Partial<
|
||||
DashboardCardTerminalInputState &
|
||||
DashboardLaunchDetectionState &
|
||||
Pick<AppState, 'runtimeEnvironments' | 'sshTargetLabels' | 'unifiedTabsByWorktree'>
|
||||
Pick<
|
||||
AppState,
|
||||
| 'runtimeEnvironments'
|
||||
| 'sshTargetLabels'
|
||||
| 'unifiedTabsByWorktree'
|
||||
| 'activeWorktreeId'
|
||||
| 'activeWorkspaceExecutionHostId'
|
||||
>
|
||||
>
|
||||
|
||||
export type DashboardSnapshotInput = DashboardSnapshotState & { chatNames: StructuredChatTabNames }
|
||||
|
||||
@@ -133,10 +133,10 @@ describe('agent bucket count input gate', () => {
|
||||
expect(second.agentStatusEpoch).toBe(1)
|
||||
})
|
||||
|
||||
it('carries every slice the counts read, and settings pinned to null', () => {
|
||||
it('carries canonical contexts and every row-owner input the counts read', () => {
|
||||
const state = storeState()
|
||||
const inputs = selectAgentBucketCountState(state)
|
||||
expect(inputs.settings).toBeNull()
|
||||
expect(inputs.settings).toBe(state.settings)
|
||||
for (const key of [
|
||||
'repos',
|
||||
'worktreesByRepo',
|
||||
|
||||
@@ -6,7 +6,11 @@ import {
|
||||
buildDashboardBucketCounts,
|
||||
createDashboardBucketCountsCache
|
||||
} from './build-dashboard-bucket-counts'
|
||||
import { selectFolderWorkspaces, selectWorktreesByRepo } from '@/store/workspace-record-selectors'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectFolderWorkspaces,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
|
||||
export type AgentBucketCounts = Record<DashboardBucket, number>
|
||||
|
||||
@@ -28,10 +32,16 @@ export type AgentBucketCountState = Pick<
|
||||
| 'folderWorkspaces'
|
||||
| 'acknowledgedAgentsByPaneKey'
|
||||
| 'agentStatusEpoch'
|
||||
> & {
|
||||
// Counts never render generated conversation names; keep this path off settings subscriptions.
|
||||
settings: null
|
||||
}
|
||||
| 'settings'
|
||||
| 'detectedWorktreesByRepo'
|
||||
| 'projectGroups'
|
||||
| 'restoredRuntimeHostIdByWorkspaceSessionKey'
|
||||
| 'activeWorktreeId'
|
||||
| 'activeWorkspaceExecutionHostId'
|
||||
| 'runtimeEnvironments'
|
||||
| 'runtimeEnvironmentCatalogHydrated'
|
||||
| 'removedRuntimeEnvironmentIds'
|
||||
>
|
||||
|
||||
// Compare slice identities directly: useShallow allocates entries/Maps per store write. This pure state projection shares one allocation-free unchanged path across consumers.
|
||||
let previousState: AgentBucketCountState | null = null
|
||||
@@ -59,7 +69,17 @@ export function selectAgentBucketCountState(s: AppState): AgentBucketCountState
|
||||
previous.paneForegroundAgentByPaneKey === s.paneForegroundAgentByPaneKey &&
|
||||
selectFolderWorkspaces(previous) === selectFolderWorkspaces(s) &&
|
||||
previous.acknowledgedAgentsByPaneKey === s.acknowledgedAgentsByPaneKey &&
|
||||
previous.agentStatusEpoch === s.agentStatusEpoch
|
||||
previous.agentStatusEpoch === s.agentStatusEpoch &&
|
||||
previous.settings === s.settings &&
|
||||
selectDetectedWorktreesByRepo(previous) === selectDetectedWorktreesByRepo(s) &&
|
||||
previous.projectGroups === s.projectGroups &&
|
||||
previous.restoredRuntimeHostIdByWorkspaceSessionKey ===
|
||||
s.restoredRuntimeHostIdByWorkspaceSessionKey &&
|
||||
previous.activeWorktreeId === s.activeWorktreeId &&
|
||||
previous.activeWorkspaceExecutionHostId === s.activeWorkspaceExecutionHostId &&
|
||||
previous.runtimeEnvironments === s.runtimeEnvironments &&
|
||||
previous.runtimeEnvironmentCatalogHydrated === s.runtimeEnvironmentCatalogHydrated &&
|
||||
previous.removedRuntimeEnvironmentIds === s.removedRuntimeEnvironmentIds
|
||||
) {
|
||||
return previous
|
||||
}
|
||||
@@ -79,7 +99,15 @@ export function selectAgentBucketCountState(s: AppState): AgentBucketCountState
|
||||
folderWorkspaces: selectFolderWorkspaces(s),
|
||||
acknowledgedAgentsByPaneKey: s.acknowledgedAgentsByPaneKey,
|
||||
agentStatusEpoch: s.agentStatusEpoch,
|
||||
settings: null
|
||||
settings: s.settings,
|
||||
detectedWorktreesByRepo: selectDetectedWorktreesByRepo(s),
|
||||
projectGroups: s.projectGroups,
|
||||
restoredRuntimeHostIdByWorkspaceSessionKey: s.restoredRuntimeHostIdByWorkspaceSessionKey,
|
||||
activeWorktreeId: s.activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId: s.activeWorkspaceExecutionHostId,
|
||||
runtimeEnvironments: s.runtimeEnvironments,
|
||||
runtimeEnvironmentCatalogHydrated: s.runtimeEnvironmentCatalogHydrated,
|
||||
removedRuntimeEnvironmentIds: s.removedRuntimeEnvironmentIds
|
||||
}
|
||||
return previousState
|
||||
}
|
||||
|
||||
@@ -79,6 +79,8 @@ export function useLiveDashboardSnapshot(): DashboardSnapshot {
|
||||
buildDashboardSnapshot(
|
||||
{
|
||||
chatNames,
|
||||
activeWorktreeId: launchRouteStore.activeWorktreeId,
|
||||
activeWorkspaceExecutionHostId: launchRouteStore.activeWorkspaceExecutionHostId,
|
||||
repos,
|
||||
worktreesByRepo,
|
||||
tabsByWorktree,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getStructuredAgentSessionTabsByPaneKey } from '@/components/native-chat/structured-agent-session-tabs'
|
||||
import type {
|
||||
AgentStatusEntry,
|
||||
AgentStatusOrchestrationContext,
|
||||
@@ -146,6 +147,9 @@ export function selectWorktreeAgentRowsCached(args: {
|
||||
const rows = applyAgentRowLineage(
|
||||
buildWorktreeAgentRows({
|
||||
tabs: tabs ?? [],
|
||||
representedTabs: state.unifiedTabsByWorktree
|
||||
? getStructuredAgentSessionTabsByPaneKey(state.unifiedTabsByWorktree)
|
||||
: undefined,
|
||||
entries,
|
||||
retained,
|
||||
runtimePaneTitlesByTabId: paneTitlesByTabId,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { structuredAgentSessionPaneKey } from '../../../../shared/structured-agent-session-projection'
|
||||
import { isStructuredTab } from '../../../../shared/structured-agent-session-tab'
|
||||
import type { Tab } from '../../../../shared/tab-types'
|
||||
|
||||
@@ -28,3 +29,21 @@ export function getStructuredAgentSessionTabs(
|
||||
structuredTabsByUnifiedTabsSnapshot.set(unifiedTabsByWorktree, tabs)
|
||||
return tabs
|
||||
}
|
||||
|
||||
const paneTabs = new WeakMap<Record<string, Tab[]>, ReadonlyMap<string, StructuredTab>>()
|
||||
export function getStructuredAgentSessionTabsByPaneKey(
|
||||
tabs: Record<string, Tab[]>
|
||||
): ReadonlyMap<string, StructuredTab> {
|
||||
const cached = paneTabs.get(tabs)
|
||||
if (cached) {
|
||||
return cached
|
||||
}
|
||||
const result = new Map(
|
||||
getStructuredAgentSessionTabs(tabs).map((tab) => [
|
||||
structuredAgentSessionPaneKey(tab.id, tab.entityId),
|
||||
tab
|
||||
])
|
||||
)
|
||||
paneTabs.set(tabs, result)
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import { defineFixture } from '../../component-fixtures/component-fixture'
|
||||
import WorktreeCardAgents from './WorktreeCardAgents'
|
||||
import { applyAgentRowLineage } from '../dashboard/agent-row-lineage'
|
||||
import { buildWorktreeAgentRows } from './worktree-agent-rows'
|
||||
import { structuredAgentSessionPaneKey } from '../../../../shared/structured-agent-session-projection'
|
||||
import type { Tab } from '../../../../shared/tab-types'
|
||||
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
|
||||
|
||||
const now = 1_792_983_600_000
|
||||
const tabs: Tab[] = [
|
||||
'11111111-1111-4111-8111-111111111111',
|
||||
'22222222-2222-4222-8222-222222222222'
|
||||
].map((entityId, index) => ({
|
||||
id: index === 0 ? 'reserved-chat-tab' : 'worker-chat-tab:history-1',
|
||||
entityId,
|
||||
contentType: 'agent-session',
|
||||
agentSessionAgent: 'codex',
|
||||
worktreeId: 'fixture-workspace',
|
||||
groupId: 'fixture-group',
|
||||
executionHostId: 'local',
|
||||
label: 'Chat',
|
||||
customLabel: null,
|
||||
color: null,
|
||||
sortOrder: index,
|
||||
createdAt: now
|
||||
}))
|
||||
const [parent, child] = tabs.map((tab) => structuredAgentSessionPaneKey(tab.id, tab.entityId))
|
||||
const entries: AgentStatusEntry[] = [parent, child].map((paneKey, index) => ({
|
||||
paneKey,
|
||||
worktreeId: 'fixture-workspace',
|
||||
agentType: 'codex',
|
||||
state: 'working',
|
||||
prompt: index === 0 ? 'Coordinate the release checks' : 'Verify the worker result',
|
||||
updatedAt: now,
|
||||
stateStartedAt: now,
|
||||
stateHistory: [],
|
||||
...(index === 1
|
||||
? { orchestration: { taskId: 'task', dispatchId: 'dispatch', parentPaneKey: parent } }
|
||||
: {})
|
||||
}))
|
||||
const rows = applyAgentRowLineage(
|
||||
buildWorktreeAgentRows({
|
||||
tabs: [],
|
||||
representedTabs: new Map(
|
||||
tabs.map((tab) => [structuredAgentSessionPaneKey(tab.id, tab.entityId), tab])
|
||||
),
|
||||
entries,
|
||||
retained: [],
|
||||
now
|
||||
})
|
||||
)
|
||||
|
||||
export const nestedChatWorker = defineFixture({
|
||||
width: 380,
|
||||
store: { agentActivityDisplayMode: 'compact' },
|
||||
render: () => (
|
||||
<div className="flex flex-col gap-0.5 bg-sidebar p-2 text-sidebar-foreground">
|
||||
<WorktreeCardAgents worktreeId="fixture-workspace" agents={rows} />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
@@ -46,6 +46,7 @@ vi.mock('@/store', () => {
|
||||
sshConnectionStates: new Map(),
|
||||
sshTargetLabels: new Map(),
|
||||
tabsByWorktree: {},
|
||||
unifiedTabsByWorktree: {},
|
||||
updateWorktreeMeta,
|
||||
workspacePortScan: null,
|
||||
worktreeCardProperties
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
import { act } from 'react'
|
||||
import { expect, vi } from 'vitest'
|
||||
import type { RuntimeAgentLineageTestModule } from '../../../../shared/runtime-agent-lineage-consumer.test-fixture'
|
||||
import { isStructuredTab } from '../../../../shared/structured-agent-session-tab'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { AppState } from '@/store/types'
|
||||
import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
|
||||
import { makePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
|
||||
import { makeTab } from '@/store/slices/store-test-helpers'
|
||||
|
||||
// Load the native graph during collection; fixture state stays per test.
|
||||
const {
|
||||
default: { publicLineageFixture, canonicalChatRow, LINEAGE_PARENT, LINEAGE_CHILD }
|
||||
} = await vi.importActual<{ default: RuntimeAgentLineageTestModule }>(
|
||||
'../../../../main/runtime/runtime-agent-lineage.test-fixture'
|
||||
)
|
||||
|
||||
const LEAF = '11111111-1111-4111-8111-111111111111'
|
||||
|
||||
export async function verifyMirroredTerminalLineage(
|
||||
kind: 'default' | 'reserved' | 'collision',
|
||||
{
|
||||
patch,
|
||||
entry,
|
||||
mount,
|
||||
getRows
|
||||
}: {
|
||||
patch: (update: Partial<AppState>) => void
|
||||
entry: (key: string, worktree: string) => AgentStatusEntry
|
||||
mount: () => void
|
||||
getRows: () => DashboardAgentRow[]
|
||||
}
|
||||
): Promise<void> {
|
||||
const { makeUnifiedTab, makeWorktree, TEST_REPO } =
|
||||
await import('@/store/slices/store-test-helpers')
|
||||
const { selectDashboardOrchestration } =
|
||||
await import('@/components/dashboard/dashboard-orchestration-selection')
|
||||
const { selectWorktreeAgentRowsCached } =
|
||||
await import('@/components/dashboard/worktree-agent-rows-cache')
|
||||
const { buildActivityEvents } = await import('@/components/activity/activity-event-builder')
|
||||
const { buildMirroredAgentTabs } =
|
||||
await import('@/runtime/web-session-tabs-sync/terminal-surfaces')
|
||||
const { buildMirroredAgentStatusPatch } =
|
||||
await import('@/runtime/web-session-tabs-sync/agent-status-patch')
|
||||
const { buildTerminalUnifiedTab } = await import('@/runtime/web-session-tabs-sync/tab-builders')
|
||||
const { toRemoteRuntimePtyId } =
|
||||
await import('../../../../shared/remote-runtime/remote-runtime-pty-id')
|
||||
const { toWebTerminalSurfaceTabId } = await import('../../../../shared/terminal-surface-id')
|
||||
const { projectStatus } =
|
||||
await import('@/components/native-chat/structured-agent-session-status-projection')
|
||||
const { structuredAgentSessionPaneKey: pane, structuredAgentSessionTabId: tabId } =
|
||||
await import('../../../../shared/structured-agent-session-projection')
|
||||
const { selectVisibleAgentOrchestration } = await import('@/runtime/visible-agent-orchestration')
|
||||
const worktree = makeWorktree({ id: 'folder-a', repoId: TEST_REPO.id })
|
||||
patch({ repos: [TEST_REPO], worktreesByRepo: { [TEST_REPO.id]: [worktree] } })
|
||||
const f = publicLineageFixture('folder-a')
|
||||
try {
|
||||
const context = f.projection.buildByPaneKey()?.[canonicalChatRow(LINEAGE_CHILD)]
|
||||
if (!context) {
|
||||
throw new Error('Missing production context')
|
||||
}
|
||||
const native = buildMirroredAgentTabs(
|
||||
{
|
||||
worktree: 'folder-a',
|
||||
publicationEpoch: 'test',
|
||||
snapshotVersion: 1,
|
||||
activeGroupId: 'g',
|
||||
activeTabId: null,
|
||||
activeTabType: 'agent-session',
|
||||
tabs: [
|
||||
{
|
||||
type: 'agent-session',
|
||||
id: 'host-parent',
|
||||
title: 'Parent',
|
||||
sessionId: LINEAGE_PARENT,
|
||||
replacesSessionId: 'previous',
|
||||
agent: 'codex',
|
||||
isActive: false
|
||||
}
|
||||
]
|
||||
},
|
||||
'runtime:paired',
|
||||
new Map(),
|
||||
'g',
|
||||
0,
|
||||
kind === 'default'
|
||||
? []
|
||||
: [
|
||||
makeUnifiedTab({
|
||||
id: kind === 'reserved' ? 'reserved-parent' : tabId(LINEAGE_PARENT),
|
||||
entityId: kind === 'reserved' ? 'previous' : 'occupied',
|
||||
worktreeId: 'folder-a',
|
||||
groupId: 'g',
|
||||
contentType: 'agent-session'
|
||||
})
|
||||
],
|
||||
Date.now()
|
||||
)[0].unifiedTab
|
||||
if (!isStructuredTab(native)) {
|
||||
throw new Error('Expected the mirrored native session tab')
|
||||
}
|
||||
const actualParent = pane(native.id, LINEAGE_PARENT)
|
||||
const tab = makeTab({
|
||||
id: toWebTerminalSurfaceTabId('host-terminal'),
|
||||
worktreeId: 'folder-a',
|
||||
ptyId: toRemoteRuntimePtyId('terminal', 'paired')
|
||||
})
|
||||
const worker = makePaneKey(tab.id, LEAF)
|
||||
const terminal = buildTerminalUnifiedTab(tab, 'g', 'paired')
|
||||
patch({
|
||||
tabsByWorktree: { 'folder-a': [tab] },
|
||||
unifiedTabsByWorktree: {},
|
||||
ptyIdsByTabId: {},
|
||||
agentStatusByPaneKey: {},
|
||||
retainedAgentsByPaneKey: {},
|
||||
runtimeAgentOrchestrationByPaneKey: {}
|
||||
})
|
||||
const produced = buildMirroredAgentStatusPatch(
|
||||
useAppStore.getState(),
|
||||
[],
|
||||
[
|
||||
{
|
||||
type: 'terminal',
|
||||
id: 'surface',
|
||||
title: 'Worker',
|
||||
parentTabId: 'host-terminal',
|
||||
leafId: LEAF,
|
||||
terminal: 'terminal',
|
||||
status: 'ready',
|
||||
isActive: false,
|
||||
agentStatus: {
|
||||
...entry(makePaneKey('host-terminal', LEAF), 'folder-a'),
|
||||
orchestration: context
|
||||
}
|
||||
}
|
||||
],
|
||||
[],
|
||||
'paired',
|
||||
'folder-a',
|
||||
new Set(),
|
||||
Date.now()
|
||||
)
|
||||
if (!produced) {
|
||||
throw new Error('Missing mirrored terminal status')
|
||||
}
|
||||
patch(produced)
|
||||
expect(useAppStore.getState().agentStatusByPaneKey[worker].connectionId).toBe('paired')
|
||||
// Status before graph: the missing parent stays rooted, then tab hydration alone joins it.
|
||||
expect(selectVisibleAgentOrchestration(useAppStore.getState())[worker]?.parentPaneKey).not.toBe(
|
||||
actualParent
|
||||
)
|
||||
patch({ unifiedTabsByWorktree: { 'folder-a': [native, terminal] } })
|
||||
act(() =>
|
||||
useAppStore.getState().transactAgentStatuses((transaction) =>
|
||||
projectStatus(
|
||||
transaction,
|
||||
native,
|
||||
{
|
||||
sessionId: LINEAGE_PARENT,
|
||||
workspaceId: 'folder-a',
|
||||
agent: 'codex',
|
||||
status: 'working',
|
||||
latestPrompt: 'Parent',
|
||||
updatedAt: Date.now(),
|
||||
hostExecutionOwned: true
|
||||
},
|
||||
'live',
|
||||
null
|
||||
)
|
||||
)
|
||||
)
|
||||
mount()
|
||||
const assertNested = (parent = actualParent) => {
|
||||
expect(getRows().map((row) => row.paneKey)).toEqual([parent, worker])
|
||||
expect(getRows().map((row) => row.lineage?.depth)).toEqual([0, 1])
|
||||
expect(getRows()[1].entry.orchestration?.parentPaneKey).toBe(parent)
|
||||
expect(
|
||||
useAppStore.getState().agentStatusByPaneKey[worker]?.orchestration?.parentPaneKey ??
|
||||
context.parentPaneKey
|
||||
).toBe(canonicalChatRow(LINEAGE_PARENT))
|
||||
const state = useAppStore.getState()
|
||||
const orchestration =
|
||||
selectDashboardOrchestration(state, [{ worktree }]).singletonOrchestration ?? {}
|
||||
const dashboard = selectWorktreeAgentRowsCached({
|
||||
state,
|
||||
worktreeId: 'folder-a',
|
||||
orchestration,
|
||||
now: Date.now(),
|
||||
generation: 1
|
||||
})
|
||||
expect(dashboard.map((row) => [row.paneKey, row.lineage.depth])).toEqual([
|
||||
[parent, 0],
|
||||
[worker, 1]
|
||||
])
|
||||
const activity = buildActivityEvents({
|
||||
...state,
|
||||
chatNames: new Map(),
|
||||
worktreeMap: new Map([[worktree.id, worktree]]),
|
||||
repoMap: new Map([[TEST_REPO.id, TEST_REPO]]),
|
||||
runtimeAgentOrchestrationByPaneKey: selectVisibleAgentOrchestration(state),
|
||||
now: Date.now()
|
||||
})
|
||||
expect(activity.paneEntryByPaneKey[worker]?.orchestration?.parentPaneKey).toBe(parent)
|
||||
expect(activity.paneEntryByPaneKey[parent]).toBeDefined()
|
||||
}
|
||||
assertNested()
|
||||
// A host-supplied legacy connection spelling does not override actual producer proof.
|
||||
patch({
|
||||
agentStatusByPaneKey: {
|
||||
...useAppStore.getState().agentStatusByPaneKey,
|
||||
[worker]: {
|
||||
...useAppStore.getState().agentStatusByPaneKey[worker],
|
||||
connectionId: 'ssh-legacy'
|
||||
}
|
||||
}
|
||||
})
|
||||
assertNested()
|
||||
const canonical = useAppStore.getState().agentStatusByPaneKey[worker]
|
||||
const epoch = useAppStore.getState().agentStatusEpoch
|
||||
// Graph before parent hydration; matching task/Dispatch producer remains valid with nonempty connectionId.
|
||||
const parentEntry = useAppStore.getState().agentStatusByPaneKey[actualParent]
|
||||
patch({
|
||||
unifiedTabsByWorktree: { 'folder-a': [terminal] },
|
||||
agentStatusByPaneKey: { [worker]: canonical }
|
||||
})
|
||||
act(() =>
|
||||
useAppStore
|
||||
.getState()
|
||||
.setRuntimeAgentOrchestrationByPaneKey(
|
||||
{ [worker]: structuredClone(context) },
|
||||
'runtime:paired'
|
||||
)
|
||||
)
|
||||
expect(getRows().find((row) => row.paneKey === worker)?.lineage?.depth).toBe(0)
|
||||
patch({
|
||||
unifiedTabsByWorktree: { 'folder-a': [native, terminal] },
|
||||
agentStatusByPaneKey: { [worker]: canonical, [actualParent]: parentEntry }
|
||||
})
|
||||
assertNested()
|
||||
act(() => useAppStore.getState().setRuntimeAgentOrchestrationByPaneKey({}))
|
||||
// PTY-only hydration is an independent cache input; disagreeing tab/PTY facts must not join.
|
||||
patch({
|
||||
unifiedTabsByWorktree: { 'folder-a': [native] },
|
||||
tabsByWorktree: { 'folder-a': [{ ...tab, ptyId: null }] }
|
||||
})
|
||||
expect(getRows().find((row) => row.paneKey === worker)?.lineage?.depth).toBe(0)
|
||||
patch({ ptyIdsByTabId: { [tab.id]: [tab.ptyId ?? ''] } })
|
||||
assertNested()
|
||||
patch({
|
||||
unifiedTabsByWorktree: {
|
||||
'folder-a': [native, { ...terminal, executionHostId: 'runtime:other' }]
|
||||
}
|
||||
})
|
||||
expect(getRows().find((row) => row.paneKey === worker)?.lineage?.depth).toBe(0)
|
||||
patch({
|
||||
unifiedTabsByWorktree: {
|
||||
'folder-a': [{ ...native, executionHostId: 'runtime:other' }, terminal]
|
||||
},
|
||||
ptyIdsByTabId: {}
|
||||
})
|
||||
expect(getRows().find((row) => row.paneKey === worker)?.lineage?.depth).toBe(0)
|
||||
patch({ unifiedTabsByWorktree: { 'folder-a': [native, terminal] } })
|
||||
assertNested()
|
||||
// Retained snapshot proves the runtime after the current terminal tab/PTY/unified row disappears.
|
||||
act(() =>
|
||||
useAppStore.getState().retainAgents([
|
||||
{
|
||||
entry: { ...canonical, state: 'done' },
|
||||
worktreeId: 'folder-a',
|
||||
tab,
|
||||
agentType: 'claude',
|
||||
startedAt: Date.now()
|
||||
}
|
||||
])
|
||||
)
|
||||
patch({
|
||||
agentStatusByPaneKey: {
|
||||
[actualParent]: useAppStore.getState().agentStatusByPaneKey[actualParent]
|
||||
},
|
||||
tabsByWorktree: {},
|
||||
unifiedTabsByWorktree: { 'folder-a': [native] },
|
||||
ptyIdsByTabId: {}
|
||||
})
|
||||
assertNested()
|
||||
patch({ unifiedTabsByWorktree: { 'folder-a': [{ ...native, id: 'remapped-parent' }] } })
|
||||
// The parent status arrives independently; canonical retained context did not change or need an RPC.
|
||||
patch({
|
||||
agentStatusByPaneKey: {
|
||||
[pane('remapped-parent', LINEAGE_PARENT)]: {
|
||||
...useAppStore.getState().agentStatusByPaneKey[actualParent],
|
||||
paneKey: pane('remapped-parent', LINEAGE_PARENT),
|
||||
tabId: 'remapped-parent'
|
||||
}
|
||||
}
|
||||
})
|
||||
assertNested(pane('remapped-parent', LINEAGE_PARENT))
|
||||
expect(
|
||||
useAppStore.getState().retainedAgentsByPaneKey[worker].entry.orchestration?.parentPaneKey
|
||||
).toBe(canonicalChatRow(LINEAGE_PARENT))
|
||||
expect(useAppStore.getState().agentStatusEpoch).toBeGreaterThanOrEqual(epoch)
|
||||
// Removing the only retained PTY proof cannot infer a runtime from its connection spelling.
|
||||
patch({
|
||||
retainedAgentsByPaneKey: {
|
||||
[worker]: {
|
||||
...useAppStore.getState().retainedAgentsByPaneKey[worker],
|
||||
tab: { ...tab, ptyId: null }
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(getRows().find((row) => row.paneKey === worker)?.lineage?.depth).toBe(0)
|
||||
expect(new Set(getRows().map((row) => row.paneKey)).size).toBe(getRows().length)
|
||||
// Matching graph context identifies its producer even when terminal routing names another host.
|
||||
const renamedParent = pane('remapped-parent', LINEAGE_PARENT)
|
||||
patch({
|
||||
retainedAgentsByPaneKey: {},
|
||||
tabsByWorktree: { 'folder-a': [tab] },
|
||||
unifiedTabsByWorktree: {
|
||||
'folder-a': [{ ...native, id: 'remapped-parent', executionHostId: 'local' }, terminal]
|
||||
},
|
||||
agentStatusByPaneKey: {
|
||||
[renamedParent]: useAppStore.getState().agentStatusByPaneKey[renamedParent],
|
||||
[worker]: canonical
|
||||
}
|
||||
})
|
||||
act(() =>
|
||||
useAppStore
|
||||
.getState()
|
||||
.setRuntimeAgentOrchestrationByPaneKey({ [worker]: structuredClone(context) })
|
||||
)
|
||||
assertNested(renamedParent)
|
||||
act(() =>
|
||||
useAppStore
|
||||
.getState()
|
||||
.setRuntimeAgentOrchestrationByPaneKey(
|
||||
{ [worker]: structuredClone(context) },
|
||||
'runtime:other'
|
||||
)
|
||||
)
|
||||
expect(getRows().find((row) => row.paneKey === worker)?.lineage?.depth).toBe(0)
|
||||
act(() => useAppStore.getState().setRuntimeAgentOrchestrationByPaneKey({}))
|
||||
patch({
|
||||
tabsByWorktree: { 'folder-a': [{ ...tab, ptyId: 'remote:%zz@@terminal' }] },
|
||||
unifiedTabsByWorktree: {
|
||||
'folder-a': [{ ...native, id: 'remapped-parent', executionHostId: 'local' }]
|
||||
},
|
||||
agentStatusByPaneKey: {
|
||||
...useAppStore.getState().agentStatusByPaneKey,
|
||||
[worker]: { ...canonical, connectionId: undefined }
|
||||
}
|
||||
})
|
||||
expect(getRows().find((row) => row.paneKey === worker)?.lineage?.depth).toBe(0)
|
||||
} finally {
|
||||
f.db.close()
|
||||
}
|
||||
}
|
||||
@@ -13,9 +13,19 @@ import {
|
||||
type AgentStatusEntry
|
||||
} from '../../../../shared/agent-status-types'
|
||||
import { useWorktreeAgentRows } from './useWorktreeAgentRows'
|
||||
import { verifyMirroredTerminalLineage } from './mirrored-terminal-lineage.test-fixture'
|
||||
import type { RuntimeAgentLineageTestModule } from '../../../../shared/runtime-agent-lineage-consumer.test-fixture'
|
||||
import { isStructuredTab } from '../../../../shared/structured-agent-session-tab'
|
||||
|
||||
Reflect.set(globalThis, 'IS_REACT_ACT_ENVIRONMENT', true)
|
||||
|
||||
// Load the native graph during collection; fixture state stays per test.
|
||||
const {
|
||||
default: { publicLineageFixture, canonicalChatRow, LINEAGE_PARENT, LINEAGE_CHILD }
|
||||
} = await vi.importActual<{ default: RuntimeAgentLineageTestModule }>(
|
||||
'../../../../main/runtime/runtime-agent-lineage.test-fixture'
|
||||
)
|
||||
|
||||
const LEAF = '11111111-1111-4111-8111-111111111111'
|
||||
const KEY_A = makePaneKey('tab-a', LEAF)
|
||||
const KEY_B = makePaneKey('tab-b', LEAF)
|
||||
@@ -220,3 +230,535 @@ describe('mounted worktree agent row subscriptions', () => {
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
it.each([20, 200])(
|
||||
'shares canonical receiving/alias work and preserves unaffected mounted rows at %i worktrees',
|
||||
async (n) => {
|
||||
const { structuredAgentSessionPaneKey: pane, structuredAgentSessionTabId: tabId } =
|
||||
await import('../../../../shared/structured-agent-session-projection')
|
||||
const { makeUnifiedTab } = await import('@/store/slices/store-test-helpers')
|
||||
const { visibleOrchestrationWorkForTest } =
|
||||
await import('@/runtime/visible-agent-orchestration')
|
||||
const { _getWorktreeAgentOrchestrationIndexBuildCountForTest: indexBuilds } =
|
||||
await import('./worktree-agent-orchestration-index')
|
||||
const live: AppState['agentStatusByPaneKey'] = {}
|
||||
const tabs: AppState['unifiedTabsByWorktree'] = {}
|
||||
const raw: AppState['runtimeAgentOrchestrationByPaneKey'] = {}
|
||||
const observed = new Map<number, DashboardAgentRow[]>()
|
||||
const counts = new Map<number, number>()
|
||||
for (let i = 0; i < n; i++) {
|
||||
const worktreeId = `lineage-${i}`
|
||||
tabs[worktreeId] = ['parent', 'child'].map((role) =>
|
||||
makeUnifiedTab({
|
||||
id: `${role}-actual-${i}`,
|
||||
entityId: `${role}-${i}`,
|
||||
groupId: 'group',
|
||||
worktreeId,
|
||||
contentType: 'agent-session',
|
||||
agentSessionAgent: 'codex',
|
||||
executionHostId: i === n - 1 ? 'runtime:other' : 'local'
|
||||
})
|
||||
)
|
||||
for (const tab of tabs[worktreeId]) {
|
||||
live[pane(tab.id, tab.entityId)] = {
|
||||
...entry(pane(tab.id, tab.entityId), worktreeId),
|
||||
agentType: 'codex',
|
||||
tabId: tab.id
|
||||
}
|
||||
}
|
||||
raw[pane(tabId(`child-${i}`), `child-${i}`)] = {
|
||||
taskId: `task-${i}`,
|
||||
dispatchId: `dispatch-${i}`,
|
||||
dispatchStatus: 'dispatched',
|
||||
parentPaneKey: pane(tabId(`parent-${i}`), `parent-${i}`)
|
||||
}
|
||||
}
|
||||
patch({
|
||||
tabsByWorktree: {},
|
||||
unifiedTabsByWorktree: tabs,
|
||||
agentStatusByPaneKey: live,
|
||||
runtimeAgentOrchestrationByPaneKey: {},
|
||||
retainedAgentsByPaneKey: {}
|
||||
})
|
||||
act(() => useAppStore.getState().setRuntimeAgentOrchestrationByPaneKey(raw))
|
||||
function ManyProbe({ index }: { index: number }) {
|
||||
observed.set(index, useWorktreeAgentRows(`lineage-${index}`))
|
||||
counts.set(index, (counts.get(index) ?? 0) + 1)
|
||||
return null
|
||||
}
|
||||
act(() =>
|
||||
root.render(Array.from({ length: n }, (_, index) => <ManyProbe key={index} index={index} />))
|
||||
)
|
||||
const initial = new Map(observed)
|
||||
counts.clear()
|
||||
const beforeEqual = visibleOrchestrationWorkForTest()
|
||||
const canonicalBefore = useAppStore.getState().runtimeAgentOrchestrationByPaneKey
|
||||
act(() => useAppStore.getState().setRuntimeAgentOrchestrationByPaneKey(structuredClone(raw)))
|
||||
expect(useAppStore.getState().runtimeAgentOrchestrationByPaneKey).toBe(canonicalBefore)
|
||||
expect(counts.size).toBe(0)
|
||||
expect(visibleOrchestrationWorkForTest()).toEqual(beforeEqual)
|
||||
const changedKey = pane(tabId('child-0'), 'child-0')
|
||||
for (const change of [
|
||||
{ taskTitle: 'A real new title' },
|
||||
{ dispatchStatus: 'completed' as const },
|
||||
{ attention: { categories: ['input' as const], requiresAction: true } },
|
||||
{ parentPaneKey: 'missing-parent' }
|
||||
]) {
|
||||
const before = visibleOrchestrationWorkForTest()
|
||||
const beforeIndex = indexBuilds()
|
||||
counts.clear()
|
||||
act(() =>
|
||||
useAppStore.getState().setRuntimeAgentOrchestrationByPaneKey({
|
||||
...useAppStore.getState().runtimeAgentOrchestrationByPaneKey,
|
||||
[changedKey]: { ...raw[changedKey], ...change }
|
||||
})
|
||||
)
|
||||
expect(counts.get(0)).toBe(1)
|
||||
expect(
|
||||
observed.get(0)?.find((row) => row.paneKey === pane('child-actual-0', 'child-0'))?.entry
|
||||
.orchestration
|
||||
).toMatchObject(change)
|
||||
for (let i = 1; i < n; i++) {
|
||||
expect(counts.has(i)).toBe(false)
|
||||
expect(observed.get(i)).toBe(initial.get(i))
|
||||
}
|
||||
const after = visibleOrchestrationWorkForTest()
|
||||
expect(after.builds - before.builds).toBeLessThanOrEqual(1)
|
||||
expect(after.aliasBuilds).toBe(before.aliasBuilds)
|
||||
expect(indexBuilds() - beforeIndex).toBeLessThanOrEqual(1)
|
||||
expect(after.visits - before.visits).toBeLessThanOrEqual(n * 14)
|
||||
}
|
||||
act(() => useAppStore.getState().setRuntimeAgentOrchestrationByPaneKey(raw))
|
||||
// Releasing one relationship preserves unrelated arrays even while its embedded completion remains.
|
||||
counts.clear()
|
||||
const remaining = { ...useAppStore.getState().runtimeAgentOrchestrationByPaneKey }
|
||||
delete remaining[changedKey]
|
||||
act(() => useAppStore.getState().setRuntimeAgentOrchestrationByPaneKey(remaining))
|
||||
for (let i = 1; i < n; i++) {
|
||||
expect(observed.get(i)).toBe(initial.get(i))
|
||||
}
|
||||
const epoch = useAppStore.getState().agentStatusEpoch
|
||||
const beforeAlias = visibleOrchestrationWorkForTest()
|
||||
patch({
|
||||
unifiedTabsByWorktree: {
|
||||
...tabs,
|
||||
'lineage-0': tabs['lineage-0'].map((tab) =>
|
||||
tab.entityId === 'parent-0' ? { ...tab, id: 'remapped-parent' } : tab
|
||||
)
|
||||
}
|
||||
})
|
||||
expect(
|
||||
observed.get(0)?.find((row) => row.paneKey === pane('child-actual-0', 'child-0'))?.entry
|
||||
.orchestration?.parentPaneKey
|
||||
).toBe(pane('remapped-parent', 'parent-0'))
|
||||
expect(useAppStore.getState().agentStatusEpoch).toBe(epoch)
|
||||
for (let i = 1; i < n; i++) {
|
||||
expect(observed.get(i)).toBe(initial.get(i))
|
||||
}
|
||||
expect(visibleOrchestrationWorkForTest().builds - beforeAlias.builds).toBe(1)
|
||||
// A host change cannot attach a local worker to an identically named foreign parent.
|
||||
patch({
|
||||
unifiedTabsByWorktree: {
|
||||
...tabs,
|
||||
'lineage-0': tabs['lineage-0'].map((tab) =>
|
||||
tab.entityId === 'parent-0' ? { ...tab, executionHostId: 'runtime:other' } : tab
|
||||
)
|
||||
}
|
||||
})
|
||||
expect(
|
||||
observed.get(0)?.find((row) => row.paneKey === pane('child-actual-0', 'child-0'))?.entry
|
||||
.orchestration?.parentPaneKey
|
||||
).not.toBe(pane('parent-actual-0', 'parent-0'))
|
||||
for (let i = 1; i < n; i++) {
|
||||
expect(observed.get(i)).toBe(initial.get(i))
|
||||
}
|
||||
counts.clear()
|
||||
const foreignRow = pane(`child-actual-${n - 1}`, `child-${n - 1}`)
|
||||
const beforeForeign = visibleOrchestrationWorkForTest()
|
||||
const beforeForeignIndex = indexBuilds()
|
||||
const { projectStatus } =
|
||||
await import('@/components/native-chat/structured-agent-session-status-projection')
|
||||
const foreignTab = tabs[`lineage-${n - 1}`][1]
|
||||
if (!isStructuredTab(foreignTab)) {
|
||||
throw new Error('Expected the native session tab')
|
||||
}
|
||||
act(() =>
|
||||
useAppStore.getState().transactAgentStatuses((transaction) =>
|
||||
projectStatus(
|
||||
transaction,
|
||||
foreignTab,
|
||||
{
|
||||
sessionId: foreignTab.entityId,
|
||||
workspaceId: foreignTab.worktreeId,
|
||||
agent: 'codex',
|
||||
status: 'working',
|
||||
latestPrompt: 'Another host update',
|
||||
updatedAt: Date.now() + 1,
|
||||
hostExecutionOwned: true
|
||||
},
|
||||
'live',
|
||||
null
|
||||
)
|
||||
)
|
||||
)
|
||||
expect(counts.get(n - 1)).toBe(1)
|
||||
expect(observed.get(n - 1)?.find((row) => row.paneKey === foreignRow)?.entry.prompt).toBe(
|
||||
'Another host update'
|
||||
)
|
||||
for (let i = 0; i < n - 1; i++) {
|
||||
expect(counts.has(i)).toBe(false)
|
||||
}
|
||||
const afterForeign = visibleOrchestrationWorkForTest()
|
||||
expect(afterForeign.builds - beforeForeign.builds).toBeLessThanOrEqual(1)
|
||||
expect(afterForeign.aliasBuilds).toBe(beforeForeign.aliasBuilds)
|
||||
expect(indexBuilds() - beforeForeignIndex).toBeLessThanOrEqual(1)
|
||||
expect(afterForeign.visits - beforeForeign.visits).toBeLessThanOrEqual(n * 14)
|
||||
}
|
||||
)
|
||||
|
||||
it('joins production mirrored custom/collision rows locally and keeps canonical initial, graph, retained and consumer contexts', async () => {
|
||||
const { makeUnifiedTab, makeWorktree, TEST_REPO } =
|
||||
await import('@/store/slices/store-test-helpers')
|
||||
const { buildMirroredAgentTabs } =
|
||||
await import('@/runtime/web-session-tabs-sync/terminal-surfaces')
|
||||
const { projectStatus } =
|
||||
await import('@/components/native-chat/structured-agent-session-status-projection')
|
||||
const { getStructuredAgentSessionTabs } =
|
||||
await import('@/components/native-chat/structured-agent-session-tabs')
|
||||
const { selectVisibleAgentOrchestration } = await import('@/runtime/visible-agent-orchestration')
|
||||
const { structuredAgentSessionPaneKey: pane, structuredAgentSessionTabId: tabId } =
|
||||
await import('../../../../shared/structured-agent-session-projection')
|
||||
const { selectWorktreeAgentActivitySummary } = await import('./worktree-agent-activity-summary')
|
||||
const { selectDashboardOrchestration } =
|
||||
await import('@/components/dashboard/dashboard-orchestration-selection')
|
||||
const { selectWorktreeAgentRowsCached } =
|
||||
await import('@/components/dashboard/worktree-agent-rows-cache')
|
||||
const { buildActivityEvents } = await import('@/components/activity/activity-event-builder')
|
||||
const f = publicLineageFixture('folder-a')
|
||||
try {
|
||||
const predecessor = makeUnifiedTab({
|
||||
id: 'reserved-parent',
|
||||
entityId: 'old-parent',
|
||||
groupId: 'g',
|
||||
worktreeId: 'folder-a',
|
||||
contentType: 'agent-session',
|
||||
agentSessionAgent: 'codex',
|
||||
executionHostId: 'local'
|
||||
})
|
||||
const occupied = makeUnifiedTab({
|
||||
id: tabId(LINEAGE_CHILD),
|
||||
entityId: 'unrelated',
|
||||
groupId: 'g',
|
||||
worktreeId: 'folder-a'
|
||||
})
|
||||
const mirrored = buildMirroredAgentTabs(
|
||||
{
|
||||
worktree: 'folder-a',
|
||||
publicationEpoch: 'test',
|
||||
snapshotVersion: 1,
|
||||
activeGroupId: 'g',
|
||||
activeTabId: null,
|
||||
activeTabType: 'agent-session',
|
||||
tabs: [
|
||||
{
|
||||
type: 'agent-session',
|
||||
id: 'host-parent',
|
||||
title: 'Parent',
|
||||
sessionId: LINEAGE_PARENT,
|
||||
replacesSessionId: 'old-parent',
|
||||
agent: 'codex',
|
||||
isActive: false
|
||||
},
|
||||
{
|
||||
type: 'agent-session',
|
||||
id: 'host-child',
|
||||
title: 'Child',
|
||||
sessionId: LINEAGE_CHILD,
|
||||
agent: 'codex',
|
||||
isActive: false
|
||||
}
|
||||
]
|
||||
},
|
||||
'local',
|
||||
new Map(),
|
||||
'g',
|
||||
0,
|
||||
[predecessor, occupied],
|
||||
Date.now()
|
||||
).map((item) => item.unifiedTab)
|
||||
expect(mirrored[0].id).toBe('reserved-parent')
|
||||
expect(mirrored[1].id).toContain(':history-1')
|
||||
const raw = f.projection.buildByPaneKey() ?? {}
|
||||
patch({
|
||||
tabsByWorktree: {},
|
||||
unifiedTabsByWorktree: {},
|
||||
agentStatusByPaneKey: {},
|
||||
retainedAgentsByPaneKey: {},
|
||||
runtimeAgentOrchestrationByPaneKey: {}
|
||||
})
|
||||
act(() => useAppStore.getState().setRuntimeAgentOrchestrationByPaneKey(raw))
|
||||
const canonical = useAppStore.getState().runtimeAgentOrchestrationByPaneKey
|
||||
patch({ unifiedTabsByWorktree: { 'folder-a': mirrored } })
|
||||
const actualParent = pane(mirrored[0].id, LINEAGE_PARENT)
|
||||
const actualChild = pane(mirrored[1].id, LINEAGE_CHILD)
|
||||
expect(selectVisibleAgentOrchestration(useAppStore.getState())[actualChild].parentPaneKey).toBe(
|
||||
actualParent
|
||||
)
|
||||
const emitStatuses = () =>
|
||||
act(() =>
|
||||
useAppStore.getState().transactAgentStatuses((transaction) => {
|
||||
for (const tab of getStructuredAgentSessionTabs(
|
||||
useAppStore.getState().unifiedTabsByWorktree
|
||||
)) {
|
||||
projectStatus(
|
||||
transaction,
|
||||
tab,
|
||||
{
|
||||
sessionId: tab.entityId,
|
||||
workspaceId: tab.worktreeId,
|
||||
agent: 'codex',
|
||||
status: 'working',
|
||||
latestPrompt: tab.label,
|
||||
updatedAt: Date.now(),
|
||||
hostExecutionOwned: true
|
||||
},
|
||||
'live',
|
||||
null
|
||||
)
|
||||
}
|
||||
})
|
||||
)
|
||||
emitStatuses()
|
||||
expect(
|
||||
useAppStore.getState().agentStatusByPaneKey[actualChild].orchestration?.parentPaneKey
|
||||
).toBe(canonicalChatRow(LINEAGE_PARENT))
|
||||
expect(useAppStore.getState().runtimeAgentOrchestrationByPaneKey).toBe(canonical)
|
||||
act(() => root.render(<Probe />))
|
||||
expect(rows.map((row) => row.paneKey)).toEqual([actualParent, actualChild])
|
||||
expect(rows[1].entry.orchestration?.parentPaneKey).toBe(actualParent)
|
||||
const worktree = makeWorktree({ id: 'folder-a', repoId: TEST_REPO.id })
|
||||
patch({ repos: [TEST_REPO], worktreesByRepo: { [TEST_REPO.id]: [worktree] } })
|
||||
const state = useAppStore.getState()
|
||||
const singleton = selectDashboardOrchestration(state, [{ worktree }]).singletonOrchestration
|
||||
const batch = selectDashboardOrchestration(state, [
|
||||
{ worktree },
|
||||
{ worktree: { ...worktree, id: 'other' } }
|
||||
]).orchestrationByWorktree?.get('folder-a')
|
||||
expect(batch).toBe(singleton)
|
||||
const dashboard = selectWorktreeAgentRowsCached({
|
||||
state,
|
||||
worktreeId: 'folder-a',
|
||||
orchestration: singleton ?? {},
|
||||
now: Date.now(),
|
||||
generation: 1
|
||||
})
|
||||
expect(dashboard.map((row) => row.lineage?.depth)).toEqual([0, 1])
|
||||
expect(selectWorktreeAgentActivitySummary(state, 'folder-a').hasLiveWorking).toBe(true)
|
||||
const activity = buildActivityEvents({
|
||||
...state,
|
||||
chatNames: new Map(),
|
||||
tabsByWorktree: state.tabsByWorktree,
|
||||
unifiedTabsByWorktree: state.unifiedTabsByWorktree,
|
||||
worktreeMap: new Map([[worktree.id, worktree]]),
|
||||
repoMap: new Map([[TEST_REPO.id, TEST_REPO]]),
|
||||
runtimeAgentOrchestrationByPaneKey: selectVisibleAgentOrchestration(state),
|
||||
now: Date.now()
|
||||
})
|
||||
expect(activity.paneEntryByPaneKey[actualChild]?.orchestration?.parentPaneKey).toBe(
|
||||
actualParent
|
||||
)
|
||||
expect(activity.paneEntryByPaneKey[actualParent]).toBeDefined()
|
||||
const { getWorkspaceCleanupAgentEvidence } =
|
||||
await import('@/store/slices/workspace-cleanup-agent-evidence')
|
||||
const { makeCandidate } = await import('@/store/slices/workspace-cleanup-slice-test-harness')
|
||||
expect([
|
||||
...getWorkspaceCleanupAgentEvidence(state, [
|
||||
makeCandidate({ worktreeId: 'folder-a', executionHostId: 'local' })
|
||||
]).values()
|
||||
]).toEqual(['live'])
|
||||
const { buildDashboardSnapshot } =
|
||||
await import('@/components/dashboard/build-dashboard-snapshot')
|
||||
const snapshot = buildDashboardSnapshot({ ...state, chatNames: new Map() }, Date.now())
|
||||
expect(JSON.stringify(snapshot)).toContain(actualChild)
|
||||
expect(JSON.stringify(snapshot)).toContain(actualParent)
|
||||
|
||||
// Payload-before-graph and a retained terminal child's embedded parent use that same read-side join.
|
||||
act(() => useAppStore.getState().setRuntimeAgentOrchestrationByPaneKey({}))
|
||||
expect(selectVisibleAgentOrchestration(useAppStore.getState())[actualChild].parentPaneKey).toBe(
|
||||
actualParent
|
||||
)
|
||||
const terminal = {
|
||||
...entry(KEY_B, 'folder-a'),
|
||||
state: 'done' as const,
|
||||
orchestration: raw[canonicalChatRow(LINEAGE_CHILD)]
|
||||
}
|
||||
act(() =>
|
||||
useAppStore.getState().retainAgents([
|
||||
{
|
||||
entry: terminal,
|
||||
worktreeId: 'folder-a',
|
||||
tab: makeTab({ id: 'tab-b', worktreeId: 'folder-a' }),
|
||||
agentType: 'codex',
|
||||
startedAt: Date.now()
|
||||
}
|
||||
])
|
||||
)
|
||||
expect(selectVisibleAgentOrchestration(useAppStore.getState())[KEY_B].parentPaneKey).toBe(
|
||||
actualParent
|
||||
)
|
||||
// Alias rederivation is immediate and writes neither canonical storage nor an epoch.
|
||||
const epoch = useAppStore.getState().agentStatusEpoch
|
||||
patch({
|
||||
unifiedTabsByWorktree: {
|
||||
'folder-a': mirrored.map((tab, index) => (index === 0 ? { ...tab, id: 'new-parent' } : tab))
|
||||
}
|
||||
})
|
||||
expect(selectVisibleAgentOrchestration(useAppStore.getState())[actualChild].parentPaneKey).toBe(
|
||||
pane('new-parent', LINEAGE_PARENT)
|
||||
)
|
||||
expect(useAppStore.getState().agentStatusEpoch).toBe(epoch)
|
||||
patch({
|
||||
unifiedTabsByWorktree: {
|
||||
'folder-a': mirrored.map((tab, index) =>
|
||||
index === 0 ? { ...tab, executionHostId: 'runtime:foreign' } : tab
|
||||
)
|
||||
}
|
||||
})
|
||||
expect(
|
||||
selectVisibleAgentOrchestration(useAppStore.getState())[actualChild]?.parentPaneKey
|
||||
).not.toBe(actualParent)
|
||||
} finally {
|
||||
f.db.close()
|
||||
}
|
||||
})
|
||||
|
||||
it.each(['default', 'reserved', 'collision'] as const)(
|
||||
'joins mirrored live/retained terminal contexts with positive provenance and a %s parent',
|
||||
async (kind) => {
|
||||
await verifyMirroredTerminalLineage(kind, {
|
||||
patch,
|
||||
entry,
|
||||
mount: () => act(() => root.render(<Probe />)),
|
||||
getRows: () => rows
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('settles the production graph subscription/scheduler/publication/receiver loop with at most one feedback flight', async () => {
|
||||
const { useRuntimeGraphSync } = await import('@/app-shell/use-runtime-graph-sync')
|
||||
const { setRuntimeGraphSyncEnabled, setRuntimeGraphStoreStateGetter } =
|
||||
await import('@/runtime/sync-runtime-graph')
|
||||
const { graphState } = await import('@/runtime/sync-runtime-graph/graph-state')
|
||||
const { makeUnifiedTab } = await import('@/store/slices/store-test-helpers')
|
||||
const { selectVisibleAgentOrchestration } = await import('@/runtime/visible-agent-orchestration')
|
||||
const { structuredAgentSessionPaneKey: pane } =
|
||||
await import('../../../../shared/structured-agent-session-projection')
|
||||
const f = publicLineageFixture('folder-a')
|
||||
const response = {
|
||||
runtimeId: 'lineage',
|
||||
rendererGraphEpoch: 1,
|
||||
graphStatus: 'ready' as const,
|
||||
authoritativeWindowId: null,
|
||||
liveTabCount: 0,
|
||||
liveLeafCount: 0,
|
||||
agentOrchestrationByPaneKey: f.projection.buildByPaneKey()
|
||||
}
|
||||
let release: (() => void) | undefined
|
||||
let fail = false
|
||||
let hold = true
|
||||
const previousApi = window.api
|
||||
const sync = vi.fn(async () => {
|
||||
if (hold) {
|
||||
await new Promise<void>((resolve) => {
|
||||
release = resolve
|
||||
})
|
||||
}
|
||||
if (fail) {
|
||||
throw new Error('expected transport failure')
|
||||
}
|
||||
return structuredClone(response)
|
||||
})
|
||||
Reflect.set(window, 'api', {
|
||||
...previousApi,
|
||||
runtime: { ...previousApi?.runtime, syncWindowGraph: sync }
|
||||
})
|
||||
const error = vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
function SyncProbe() {
|
||||
useRuntimeGraphSync()
|
||||
return null
|
||||
}
|
||||
const flush = () =>
|
||||
act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(20)
|
||||
})
|
||||
try {
|
||||
patch({
|
||||
workspaceSessionReady: true,
|
||||
tabsByWorktree: {},
|
||||
unifiedTabsByWorktree: {},
|
||||
retainedAgentsByPaneKey: {},
|
||||
runtimeAgentOrchestrationByPaneKey: {},
|
||||
agentStatusByPaneKey: {
|
||||
[canonicalChatRow(LINEAGE_CHILD)]: entry(canonicalChatRow(LINEAGE_CHILD), 'folder-a')
|
||||
}
|
||||
})
|
||||
act(() => root.render(<SyncProbe />))
|
||||
await flush()
|
||||
expect(sync).toHaveBeenCalledTimes(1)
|
||||
const parent = makeUnifiedTab({
|
||||
id: 'late-parent',
|
||||
entityId: LINEAGE_PARENT,
|
||||
groupId: 'g',
|
||||
worktreeId: 'folder-a',
|
||||
contentType: 'agent-session',
|
||||
agentSessionAgent: 'codex',
|
||||
executionHostId: 'local'
|
||||
})
|
||||
patch({ unifiedTabsByWorktree: { 'folder-a': [parent] } })
|
||||
hold = false
|
||||
await act(async () => {
|
||||
release?.()
|
||||
await Promise.resolve()
|
||||
})
|
||||
await flush()
|
||||
expect(sync).toHaveBeenCalledTimes(2)
|
||||
const stable = useAppStore.getState()
|
||||
expect(
|
||||
selectVisibleAgentOrchestration(stable)[canonicalChatRow(LINEAGE_CHILD)].parentPaneKey
|
||||
).toBe(pane(parent.id, LINEAGE_PARENT))
|
||||
await flush()
|
||||
expect(sync).toHaveBeenCalledTimes(2)
|
||||
expect(useAppStore.getState().runtimeAgentOrchestrationByPaneKey).toBe(
|
||||
stable.runtimeAgentOrchestrationByPaneKey
|
||||
)
|
||||
expect(useAppStore.getState().agentStatusByPaneKey).toBe(stable.agentStatusByPaneKey)
|
||||
expect(useAppStore.getState().agentStatusEpoch).toBe(stable.agentStatusEpoch)
|
||||
fail = true
|
||||
patch({ unifiedTabsByWorktree: { 'folder-a': [{ ...parent, label: 'legitimate wake' }] } })
|
||||
await flush()
|
||||
expect(sync).toHaveBeenCalledTimes(3)
|
||||
expect(graphState.syncInFlight).toBe(false)
|
||||
await flush()
|
||||
expect(sync).toHaveBeenCalledTimes(3)
|
||||
setRuntimeGraphSyncEnabled(false)
|
||||
patch({ unifiedTabsByWorktree: { 'folder-a': [{ ...parent, id: 'disabled-local-alias' }] } })
|
||||
expect(
|
||||
selectVisibleAgentOrchestration(useAppStore.getState())[canonicalChatRow(LINEAGE_CHILD)]
|
||||
.parentPaneKey
|
||||
).toBe(pane('disabled-local-alias', LINEAGE_PARENT))
|
||||
await flush()
|
||||
expect(sync).toHaveBeenCalledTimes(3)
|
||||
fail = false
|
||||
setRuntimeGraphSyncEnabled(true)
|
||||
await flush()
|
||||
expect(sync).toHaveBeenCalledTimes(4)
|
||||
await flush()
|
||||
expect(sync).toHaveBeenCalledTimes(4)
|
||||
} finally {
|
||||
setRuntimeGraphSyncEnabled(false)
|
||||
setRuntimeGraphStoreStateGetter(null)
|
||||
Reflect.set(window, 'api', previousApi)
|
||||
error.mockRestore()
|
||||
f.db.close()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getStructuredAgentSessionTabsByPaneKey } from '@/components/native-chat/structured-agent-session-tabs'
|
||||
import { useCallback, useMemo, useSyncExternalStore } from 'react'
|
||||
import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
|
||||
import { applyAgentRowLineage } from '@/components/dashboard/agent-row-lineage'
|
||||
@@ -117,6 +118,9 @@ export function useWorktreeAgentRows(worktreeId: string, active = true): Dashboa
|
||||
return applyAgentRowLineage(
|
||||
buildWorktreeAgentRows({
|
||||
tabs: tabs ?? EMPTY_TABS,
|
||||
representedTabs: getStructuredAgentSessionTabsByPaneKey(
|
||||
useAppStore.getState().unifiedTabsByWorktree
|
||||
),
|
||||
entries,
|
||||
retained,
|
||||
runtimePaneTitlesByTabId,
|
||||
|
||||
@@ -64,6 +64,33 @@ describe('selectWorktreeAgentActivitySummary', () => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('attributes migration-blocked UUID and legacy terminal panes without affecting other worktrees', () => {
|
||||
for (const paneKey of [makePaneKey('tab-1', LEAF_ID), 'tab-1:0', undefined, 'malformed']) {
|
||||
const state: AgentActivityInput = {
|
||||
tabsByWorktree: { 'repo::/wt-1': [makeTab('tab-1', 'repo::/wt-1')] },
|
||||
agentStatusEpoch: 0,
|
||||
agentStatusByPaneKey: {},
|
||||
retainedAgentsByPaneKey: {},
|
||||
runtimeAgentOrchestrationByPaneKey: {},
|
||||
migrationUnsupportedByPtyId: {
|
||||
legacy: {
|
||||
ptyId: 'legacy',
|
||||
paneKey,
|
||||
worktreeId: 'repo::/wt-1',
|
||||
tabId: 'tab-1',
|
||||
reason: 'legacy-numeric-pane-key',
|
||||
source: 'local',
|
||||
updatedAt: 1_000
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(selectWorktreeAgentActivitySummary(state, 'repo::/wt-1').hasPermission).toBe(
|
||||
paneKey !== undefined && paneKey !== 'malformed'
|
||||
)
|
||||
expect(selectWorktreeAgentActivitySummary(state, 'other').hasPermission).toBe(false)
|
||||
}
|
||||
})
|
||||
|
||||
it('builds one cached agent summary index for multiple worktree lookups', () => {
|
||||
const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(2_000)
|
||||
const firstPaneKey = makePaneKey('tab-1', LEAF_ID)
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import { getStructuredAgentSessionTabsByPaneKey } from '@/components/native-chat/structured-agent-session-tabs'
|
||||
import {
|
||||
selectVisibleAgentOrchestration,
|
||||
type VisibleOrchestrationState
|
||||
} from '@/runtime/visible-agent-orchestration'
|
||||
import type { AppState } from '@/store'
|
||||
import { isExplicitAgentStatusFresh } from '@/lib/agent-status'
|
||||
import { migrationUnsupportedToAgentStatusEntry } from '../../../../shared/migration-unsupported-agent-entry'
|
||||
@@ -56,10 +61,11 @@ export type AgentActivityInput = Pick<
|
||||
| 'agentStatusByPaneKey'
|
||||
| 'migrationUnsupportedByPtyId'
|
||||
| 'retainedAgentsByPaneKey'
|
||||
> & {
|
||||
tabsByWorktree: AgentActivityTabsByWorktree
|
||||
runtimeAgentOrchestrationByPaneKey?: AppState['runtimeAgentOrchestrationByPaneKey']
|
||||
}
|
||||
> &
|
||||
VisibleOrchestrationState & {
|
||||
tabsByWorktree: AgentActivityTabsByWorktree
|
||||
runtimeAgentOrchestrationByPaneKey?: AppState['runtimeAgentOrchestrationByPaneKey']
|
||||
}
|
||||
|
||||
type AgentActivityCache = {
|
||||
tabsByWorktree: AgentActivityTabsByWorktree
|
||||
@@ -82,7 +88,7 @@ export function selectWorktreeAgentActivitySummary(
|
||||
function getWorktreeAgentActivitySummaries(
|
||||
state: AgentActivityInput
|
||||
): Map<string, WorktreeAgentActivitySummary> {
|
||||
const runtimeAgentOrchestrationByPaneKey = state.runtimeAgentOrchestrationByPaneKey
|
||||
const runtimeAgentOrchestrationByPaneKey = selectVisibleAgentOrchestration(state)
|
||||
if (
|
||||
agentActivityCache &&
|
||||
agentActivityCache.tabsByWorktree === state.tabsByWorktree &&
|
||||
@@ -114,7 +120,12 @@ function getWorktreeAgentActivitySummaries(
|
||||
|
||||
const now = Date.now()
|
||||
for (const [paneKey, entry] of Object.entries(state.agentStatusByPaneKey)) {
|
||||
const paneIdentity = parseAgentStatusPaneIdentity(paneKey)
|
||||
const paneIdentity = parseAgentStatusPaneIdentity(
|
||||
paneKey,
|
||||
state.unifiedTabsByWorktree
|
||||
? getStructuredAgentSessionTabsByPaneKey(state.unifiedTabsByWorktree).get(paneKey)
|
||||
: undefined
|
||||
)
|
||||
if (!paneIdentity) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { resolveAgentStatusWorktreeId } from '../../../../shared/agent-status-worktree-attribution'
|
||||
import type { Tab } from '../../../../shared/tab-types'
|
||||
import {
|
||||
parseAgentStatusPaneIdentity,
|
||||
resolveAgentStatusWorktreeId
|
||||
} from '../../../../shared/agent-status-worktree-attribution'
|
||||
import type { AppState } from '@/store/types'
|
||||
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
|
||||
import { parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import { isWebTerminalSurfaceTabId } from '../../../../shared/terminal-surface-id'
|
||||
|
||||
export type LiveEntriesByWorktreeCache = {
|
||||
@@ -24,9 +27,10 @@ export function recordLiveEntriesFullRebuild(): void {
|
||||
export function liveEntryWorktreeId(
|
||||
paneKey: string,
|
||||
entry: AgentStatusEntry,
|
||||
tabIdToWorktreeId: Map<string, string>
|
||||
tabIdToWorktreeId: Map<string, string>,
|
||||
representedTabs?: ReadonlyMap<string, Tab>
|
||||
): string | undefined {
|
||||
const parsed = parsePaneKey(paneKey)
|
||||
const parsed = parseAgentStatusPaneIdentity(paneKey, representedTabs?.get(paneKey))
|
||||
if (!parsed) {
|
||||
return undefined
|
||||
}
|
||||
@@ -42,7 +46,8 @@ export function liveEntryWorktreeId(
|
||||
export function patchLiveEntriesByWorktree(
|
||||
cache: LiveEntriesByWorktreeCache,
|
||||
agentStatusByPaneKey: AppState['agentStatusByPaneKey'],
|
||||
tabIdToWorktreeId: Map<string, string>
|
||||
tabIdToWorktreeId: Map<string, string>,
|
||||
representedTabs?: ReadonlyMap<string, Tab>
|
||||
): Map<string, AgentStatusEntry[]> | null {
|
||||
const previousMap = cache.agentStatusByPaneKey
|
||||
const changed: { paneKey: string; entry: AgentStatusEntry }[] = []
|
||||
@@ -76,7 +81,7 @@ export function patchLiveEntriesByWorktree(
|
||||
const entriesByWorktree = new Map(cache.entriesByWorktree)
|
||||
const clonedBuckets = new Set<string>()
|
||||
for (const { paneKey, entry } of changed) {
|
||||
const worktreeId = liveEntryWorktreeId(paneKey, entry, tabIdToWorktreeId)
|
||||
const worktreeId = liveEntryWorktreeId(paneKey, entry, tabIdToWorktreeId, representedTabs)
|
||||
if (!worktreeId) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
selectVisibleAgentOrchestration,
|
||||
representedOrchestrationWorktrees,
|
||||
type VisibleOrchestrationState
|
||||
} from '@/runtime/visible-agent-orchestration'
|
||||
import type { AppState } from '@/store/types'
|
||||
import type { AgentStatusOrchestrationContext } from '../../../../shared/agent-status-types'
|
||||
import { parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
@@ -8,7 +13,8 @@ type OrchestrationIndexState = Pick<
|
||||
| 'retainedAgentsByPaneKey'
|
||||
| 'runtimeAgentOrchestrationByPaneKey'
|
||||
| 'tabsByWorktree'
|
||||
>
|
||||
> &
|
||||
VisibleOrchestrationState
|
||||
|
||||
type RuntimeOrchestrationRecord = Record<string, AgentStatusOrchestrationContext>
|
||||
|
||||
@@ -26,6 +32,7 @@ type PaneWorktreeProjectionCache = {
|
||||
runtimeSource: OrchestrationIndexState['runtimeAgentOrchestrationByPaneKey']
|
||||
liveSource: OrchestrationIndexState['agentStatusByPaneKey']
|
||||
retainedSource: OrchestrationIndexState['retainedAgentsByPaneKey']
|
||||
representedSource: OrchestrationIndexState['unifiedTabsByWorktree']
|
||||
paneWorktreeIds: readonly (string | undefined)[]
|
||||
}
|
||||
|
||||
@@ -76,27 +83,37 @@ function projectPaneWorktreeIds(
|
||||
runtimeSource: OrchestrationIndexState['runtimeAgentOrchestrationByPaneKey'],
|
||||
runtimeEntries: readonly [string, AgentStatusOrchestrationContext][],
|
||||
agentStatusByPaneKey: OrchestrationIndexState['agentStatusByPaneKey'],
|
||||
retainedAgentsByPaneKey: OrchestrationIndexState['retainedAgentsByPaneKey']
|
||||
retainedAgentsByPaneKey: OrchestrationIndexState['retainedAgentsByPaneKey'],
|
||||
state: OrchestrationIndexState
|
||||
): readonly (string | undefined)[] {
|
||||
// Cache by map identity so all cards share one attribution scan per publication.
|
||||
if (
|
||||
paneWorktreeProjectionCache?.runtimeSource === runtimeSource &&
|
||||
paneWorktreeProjectionCache.liveSource === agentStatusByPaneKey &&
|
||||
paneWorktreeProjectionCache.retainedSource === retainedAgentsByPaneKey
|
||||
paneWorktreeProjectionCache.retainedSource === retainedAgentsByPaneKey &&
|
||||
paneWorktreeProjectionCache.representedSource === state.unifiedTabsByWorktree
|
||||
) {
|
||||
return paneWorktreeProjectionCache.paneWorktreeIds
|
||||
}
|
||||
const paneWorktreeIds: (string | undefined)[] = []
|
||||
for (const [paneKey] of runtimeEntries) {
|
||||
const represented = state.unifiedTabsByWorktree
|
||||
? representedOrchestrationWorktrees(state)
|
||||
: new Map<string, string>()
|
||||
for (const [paneKey, context] of runtimeEntries) {
|
||||
paneWorktreeIds.push(
|
||||
agentStatusByPaneKey[paneKey]?.worktreeId,
|
||||
retainedAgentsByPaneKey[paneKey]?.worktreeId
|
||||
retainedAgentsByPaneKey[paneKey]?.worktreeId,
|
||||
represented.get(paneKey),
|
||||
represented.size > 0 && context.parentPaneKey
|
||||
? represented.get(context.parentPaneKey)
|
||||
: undefined
|
||||
)
|
||||
}
|
||||
paneWorktreeProjectionCache = {
|
||||
runtimeSource,
|
||||
liveSource: agentStatusByPaneKey,
|
||||
retainedSource: retainedAgentsByPaneKey,
|
||||
representedSource: state.unifiedTabsByWorktree,
|
||||
paneWorktreeIds
|
||||
}
|
||||
return paneWorktreeIds
|
||||
@@ -195,7 +212,15 @@ function buildIndex(
|
||||
}
|
||||
const liveWorktreeId = paneWorktreeIds[projectionCursor]
|
||||
const retainedWorktreeId = paneWorktreeIds[projectionCursor + 1]
|
||||
projectionCursor += 2
|
||||
const representedWorktreeId = paneWorktreeIds[projectionCursor + 2]
|
||||
const parentWorktreeId = paneWorktreeIds[projectionCursor + 3]
|
||||
if (representedWorktreeId) {
|
||||
targets.add(representedWorktreeId)
|
||||
}
|
||||
if (parentWorktreeId) {
|
||||
targets.add(parentWorktreeId)
|
||||
}
|
||||
projectionCursor += 4
|
||||
if (typeof liveWorktreeId === 'string') {
|
||||
targets.add(liveWorktreeId)
|
||||
}
|
||||
@@ -227,8 +252,7 @@ function buildIndex(
|
||||
export function selectWorktreeAgentOrchestrationIndex(
|
||||
state: OrchestrationIndexState
|
||||
): ReadonlyMap<string, RuntimeOrchestrationRecord> {
|
||||
const runtimeAgentOrchestrationByPaneKey =
|
||||
state.runtimeAgentOrchestrationByPaneKey ?? EMPTY_SOURCE
|
||||
const runtimeAgentOrchestrationByPaneKey = selectVisibleAgentOrchestration(state)
|
||||
// Cache context entries separately so unrelated live/retained churn cannot force enumeration.
|
||||
if (runtimeEntriesCache?.source !== runtimeAgentOrchestrationByPaneKey) {
|
||||
runtimeEntriesCache = {
|
||||
@@ -251,7 +275,8 @@ export function selectWorktreeAgentOrchestrationIndex(
|
||||
runtimeAgentOrchestrationByPaneKey,
|
||||
runtimeEntries,
|
||||
state.agentStatusByPaneKey ?? EMPTY_SOURCE,
|
||||
state.retainedAgentsByPaneKey ?? EMPTY_SOURCE
|
||||
state.retainedAgentsByPaneKey ?? EMPTY_SOURCE,
|
||||
state
|
||||
)
|
||||
if (
|
||||
orchestrationIndexCache?.runtimeSource === runtimeAgentOrchestrationByPaneKey &&
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AgentStatusEntry } from '../../../../shared/agent-status-types'
|
||||
import { parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
import { parseAgentStatusPaneIdentity } from '../../../../shared/agent-status-worktree-attribution'
|
||||
import type { Tab } from '../../../../shared/tab-types'
|
||||
import type { TerminalTab } from '../../../../shared/terminal-tab-types'
|
||||
|
||||
export function effectiveWorktreeAgentRowStartedAt(entry: AgentStatusEntry): number {
|
||||
@@ -8,9 +9,10 @@ export function effectiveWorktreeAgentRowStartedAt(entry: AgentStatusEntry): num
|
||||
|
||||
export function tabFromWorktreeAttributedStatusEntry(
|
||||
entry: AgentStatusEntry,
|
||||
effectiveStartedAt: number
|
||||
effectiveStartedAt: number,
|
||||
representedTab?: Tab
|
||||
): TerminalTab | null {
|
||||
const parsed = parsePaneKey(entry.paneKey)
|
||||
const parsed = parseAgentStatusPaneIdentity(entry.paneKey, representedTab)
|
||||
if (!parsed || !entry.worktreeId) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -3,21 +3,7 @@ import type {
|
||||
AgentStatusOrchestrationContext
|
||||
} from '../../../../shared/agent-status-types'
|
||||
|
||||
function orchestrationContextsEqual(
|
||||
a: AgentStatusOrchestrationContext,
|
||||
b: AgentStatusOrchestrationContext
|
||||
): boolean {
|
||||
return (
|
||||
a.taskId === b.taskId &&
|
||||
a.dispatchId === b.dispatchId &&
|
||||
a.taskTitle === b.taskTitle &&
|
||||
a.displayName === b.displayName &&
|
||||
a.parentTerminalHandle === b.parentTerminalHandle &&
|
||||
a.parentPaneKey === b.parentPaneKey &&
|
||||
a.coordinatorHandle === b.coordinatorHandle &&
|
||||
a.orchestrationRunId === b.orchestrationRunId
|
||||
)
|
||||
}
|
||||
import { orchestrationContextsEqual } from '@/store/slices/agent-status/agent-status-orchestration-context'
|
||||
|
||||
export function entryWithRuntimeOrchestration(
|
||||
entry: AgentStatusEntry,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getStructuredAgentSessionTabsByPaneKey } from '@/components/native-chat/structured-agent-session-tabs'
|
||||
import type { RetainedAgentEntry } from '@/store/slices/agent-status/agent-status-contract'
|
||||
import type { AppState } from '@/store/types'
|
||||
import type {
|
||||
@@ -129,6 +130,9 @@ function getLiveEntriesByWorktree(state: WorktreeAgentRowsState): Map<string, Ag
|
||||
}
|
||||
|
||||
const tabIdToWorktreeId = getLiveTabIdToWorktreeId(tabsByWorktree, unifiedTabsByWorktree)
|
||||
const represented = unifiedTabsByWorktree
|
||||
? getStructuredAgentSessionTabsByPaneKey(unifiedTabsByWorktree)
|
||||
: undefined
|
||||
if (
|
||||
liveEntriesByWorktreeCache?.tabsByWorktree === tabsByWorktree &&
|
||||
liveEntriesByWorktreeCache.unifiedTabsByWorktree === unifiedTabsByWorktree
|
||||
@@ -136,7 +140,8 @@ function getLiveEntriesByWorktree(state: WorktreeAgentRowsState): Map<string, Ag
|
||||
const patched = patchLiveEntriesByWorktree(
|
||||
liveEntriesByWorktreeCache,
|
||||
agentStatusByPaneKey,
|
||||
tabIdToWorktreeId
|
||||
tabIdToWorktreeId,
|
||||
represented
|
||||
)
|
||||
if (patched) {
|
||||
liveEntriesByWorktreeCache = {
|
||||
@@ -152,7 +157,7 @@ function getLiveEntriesByWorktree(state: WorktreeAgentRowsState): Map<string, Ag
|
||||
const previous = liveEntriesByWorktreeCache?.entriesByWorktree
|
||||
const entriesByWorktree = new Map<string, AgentStatusEntry[]>()
|
||||
for (const [paneKey, entry] of Object.entries(agentStatusByPaneKey)) {
|
||||
const worktreeId = liveEntryWorktreeId(paneKey, entry, tabIdToWorktreeId)
|
||||
const worktreeId = liveEntryWorktreeId(paneKey, entry, tabIdToWorktreeId, represented)
|
||||
if (!worktreeId) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Tab } from '../../../../shared/tab-types'
|
||||
import { parseAgentStatusPaneIdentity } from '../../../../shared/agent-status-worktree-attribution'
|
||||
import type { DashboardAgentRow } from '@/components/dashboard/useDashboardData'
|
||||
import { isExplicitAgentStatusFresh } from '@/lib/agent-status'
|
||||
import type { RetainedAgentEntry } from '@/store/slices/agent-status/agent-status-contract'
|
||||
@@ -138,6 +140,7 @@ function markCompletedWorkerParentPaneKeysSeen(args: {
|
||||
|
||||
export function buildWorktreeAgentRows(args: {
|
||||
tabs: TerminalTab[]
|
||||
representedTabs?: ReadonlyMap<string, Tab>
|
||||
entries: AgentStatusEntry[]
|
||||
retained: RetainedAgentEntry[]
|
||||
runtimePaneTitlesByTabId?: Record<string, Record<number, string>>
|
||||
@@ -153,7 +156,10 @@ export function buildWorktreeAgentRows(args: {
|
||||
|
||||
const entriesByTabId = new Map<string, AgentStatusEntry[]>()
|
||||
for (const entry of args.entries) {
|
||||
const parsed = parsePaneKey(entry.paneKey)
|
||||
const parsed = parseAgentStatusPaneIdentity(
|
||||
entry.paneKey,
|
||||
args.representedTabs?.get(entry.paneKey)
|
||||
)
|
||||
if (!parsed) {
|
||||
continue
|
||||
}
|
||||
@@ -211,7 +217,11 @@ export function buildWorktreeAgentRows(args: {
|
||||
}
|
||||
const rowEntry = entryWithRuntimeOrchestration(entry, args.runtimeAgentOrchestrationByPaneKey)
|
||||
const startedAt = effectiveWorktreeAgentRowStartedAt(rowEntry)
|
||||
const tab = tabFromWorktreeAttributedStatusEntry(rowEntry, startedAt)
|
||||
const tab = tabFromWorktreeAttributedStatusEntry(
|
||||
rowEntry,
|
||||
startedAt,
|
||||
args.representedTabs?.get(rowEntry.paneKey)
|
||||
)
|
||||
if (!tab) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectFolderWorkspaces,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
import {
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
parseExecutionHostId,
|
||||
@@ -7,12 +12,6 @@ import {
|
||||
import type { Tab } from '../../../shared/tab-types'
|
||||
import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
|
||||
import type { WorktreeRuntimeOwnerState } from '@/lib/worktree-runtime-owner-state'
|
||||
import type { AppState } from '@/store/types'
|
||||
import {
|
||||
selectDetectedWorktreesByRepo,
|
||||
selectFolderWorkspaces,
|
||||
selectWorktreesByRepo
|
||||
} from '@/store/workspace-record-selectors'
|
||||
import { isTwinWorkspace, namedHostForOwnerRef, resolveOwner } from '@/lib/resolve-owner'
|
||||
import { authorityKey } from '../../../shared/host-authority'
|
||||
import { LOCAL_STRUCTURED_SESSION_OWNER } from './local-structured-session-owner'
|
||||
@@ -92,7 +91,9 @@ export function legacyStructuredChatRenameOwner(
|
||||
}
|
||||
|
||||
/** Catalog inputs independent of the user's current workspace selection. */
|
||||
export function structuredAgentSessionCatalogOwnerInputs(state: AppState): unknown[] {
|
||||
export function structuredAgentSessionCatalogOwnerInputs(
|
||||
state: WorktreeRuntimeOwnerState
|
||||
): unknown[] {
|
||||
return [
|
||||
state.repos,
|
||||
selectWorktreesByRepo(state),
|
||||
@@ -105,7 +106,7 @@ export function structuredAgentSessionCatalogOwnerInputs(state: AppState): unkno
|
||||
}
|
||||
|
||||
/** Inputs to the legacy workspace-owner resolver, compared before scanning chat tabs. */
|
||||
export function structuredAgentSessionOwnerInputs(state: AppState): unknown[] {
|
||||
export function structuredAgentSessionOwnerInputs(state: WorktreeRuntimeOwnerState): unknown[] {
|
||||
return [
|
||||
...structuredAgentSessionCatalogOwnerInputs(state),
|
||||
state.settings,
|
||||
|
||||
@@ -0,0 +1,284 @@
|
||||
import type { AppState } from '@/store/types'
|
||||
import type { WorktreeRuntimeOwnerState } from '@/lib/worktree-runtime-owner-state'
|
||||
import { getStructuredAgentSessionTabs } from '@/components/native-chat/structured-agent-session-tabs'
|
||||
import { LOCAL_EXECUTION_HOST_ID, toRuntimeExecutionHostId } from '../../../shared/execution-host'
|
||||
import {
|
||||
isRemoteRuntimePtyId,
|
||||
parseRemoteRuntimePtyId
|
||||
} from '../../../shared/remote-runtime/remote-runtime-pty-id'
|
||||
import { parseAgentStatusPaneIdentity } from '../../../shared/agent-status-worktree-attribution'
|
||||
import {
|
||||
structuredAgentSessionPaneKey,
|
||||
structuredAgentSessionTabId
|
||||
} from '../../../shared/structured-agent-session-projection'
|
||||
import type { AgentStatusOrchestrationContext } from '../../../shared/agent-status-types'
|
||||
import { orchestrationContextsEqual } from '@/store/slices/agent-status/agent-status-orchestration-context'
|
||||
import {
|
||||
resolveStructuredAgentSessionOwner,
|
||||
structuredAgentSessionOwnerForTab,
|
||||
structuredAgentSessionOwnerInputs
|
||||
} from './structured-agent-session-owner'
|
||||
|
||||
type Contexts = Record<string, AgentStatusOrchestrationContext>
|
||||
export type VisibleOrchestrationState = WorktreeRuntimeOwnerState &
|
||||
Partial<
|
||||
Pick<
|
||||
AppState,
|
||||
| 'unifiedTabsByWorktree'
|
||||
| 'agentStatusByPaneKey'
|
||||
| 'retainedAgentsByPaneKey'
|
||||
| 'runtimeAgentOrchestrationByPaneKey'
|
||||
| 'ptyIdsByTabId'
|
||||
>
|
||||
> & {
|
||||
tabsByWorktree?: Record<string, readonly { id: string; ptyId?: string | null }[]>
|
||||
}
|
||||
const EMPTY: Record<string, never> = {}
|
||||
const producerByContext = new WeakMap<AgentStatusOrchestrationContext, string>()
|
||||
export function orchestrationProducer(context: AgentStatusOrchestrationContext): string {
|
||||
return producerByContext.get(context) ?? LOCAL_EXECUTION_HOST_ID
|
||||
}
|
||||
export function recordOrchestrationProducer(
|
||||
context: AgentStatusOrchestrationContext,
|
||||
host: string
|
||||
): void {
|
||||
producerByContext.set(context, host)
|
||||
}
|
||||
type RowIdentities = {
|
||||
inputs: unknown[]
|
||||
aliases: Map<string, string>
|
||||
canonical: Map<string, { key: string; host: string | null }>
|
||||
worktrees: Map<string, string>
|
||||
}
|
||||
let rowCache: RowIdentities | undefined
|
||||
let cache: { inputs: unknown[]; contexts: Contexts } | undefined
|
||||
let builds = 0
|
||||
let aliasBuilds = 0
|
||||
let visits = 0
|
||||
export function visibleOrchestrationWorkForTest(): {
|
||||
builds: number
|
||||
aliasBuilds: number
|
||||
visits: number
|
||||
} {
|
||||
return { builds, aliasBuilds, visits }
|
||||
}
|
||||
const aliasKey = (host: string, key: string) => `${host}\0${key}`
|
||||
|
||||
function rowIdentities(state: VisibleOrchestrationState): RowIdentities {
|
||||
const inputs = [state.unifiedTabsByWorktree, ...structuredAgentSessionOwnerInputs(state)]
|
||||
if (rowCache && inputs.every((value, index) => value === rowCache?.inputs[index])) {
|
||||
return rowCache
|
||||
}
|
||||
aliasBuilds++
|
||||
const aliases = new Map<string, string>()
|
||||
const worktrees = new Map<string, string>()
|
||||
const canonical = new Map<string, { key: string; host: string | null }>()
|
||||
for (const tab of getStructuredAgentSessionTabs(state.unifiedTabsByWorktree ?? EMPTY)) {
|
||||
visits++
|
||||
if (!tab.entityId) {
|
||||
continue
|
||||
}
|
||||
const key = structuredAgentSessionPaneKey(
|
||||
structuredAgentSessionTabId(tab.entityId),
|
||||
tab.entityId
|
||||
)
|
||||
const actual = structuredAgentSessionPaneKey(tab.id, tab.entityId)
|
||||
const host = structuredAgentSessionOwnerForTab(state, tab)
|
||||
canonical.set(actual, { key, host })
|
||||
worktrees.set(actual, tab.worktreeId)
|
||||
if (host && !aliases.has(aliasKey(host, key))) {
|
||||
aliases.set(aliasKey(host, key), actual)
|
||||
}
|
||||
}
|
||||
rowCache = { inputs, aliases, canonical, worktrees }
|
||||
return rowCache
|
||||
}
|
||||
|
||||
let terminalCache:
|
||||
| { inputs: unknown[]; hosts: Map<string, string | null>; ptys: Map<string, string | null> }
|
||||
| undefined
|
||||
function terminalProvenance(state: VisibleOrchestrationState) {
|
||||
const inputs = [
|
||||
state.unifiedTabsByWorktree,
|
||||
state.tabsByWorktree,
|
||||
state.ptyIdsByTabId,
|
||||
state.retainedAgentsByPaneKey
|
||||
]
|
||||
if (terminalCache && inputs.every((value, index) => value === terminalCache?.inputs[index])) {
|
||||
return terminalCache
|
||||
}
|
||||
const hosts = new Map<string, string | null>()
|
||||
const ptys = new Map<string, string | null>()
|
||||
const merge = (map: Map<string, string | null>, key: string, host: string | null) => {
|
||||
map.set(key, map.has(key) && map.get(key) !== host ? null : host)
|
||||
}
|
||||
const ptyHost = (id?: string | null): string | null | undefined => {
|
||||
if (!id) {
|
||||
return undefined
|
||||
}
|
||||
const remote = parseRemoteRuntimePtyId(id)
|
||||
return remote?.environmentId
|
||||
? toRuntimeExecutionHostId(remote.environmentId)
|
||||
: isRemoteRuntimePtyId(id)
|
||||
? null
|
||||
: undefined
|
||||
}
|
||||
for (const [tab, ids] of Object.entries(state.ptyIdsByTabId ?? EMPTY)) {
|
||||
for (const id of ids) {
|
||||
visits++
|
||||
const host = ptyHost(id)
|
||||
if (host !== undefined) {
|
||||
merge(ptys, tab, host)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const tabs of Object.values(state.unifiedTabsByWorktree ?? EMPTY)) {
|
||||
for (const tab of tabs) {
|
||||
visits++
|
||||
if (tab.contentType === 'terminal' && tab.executionHostId) {
|
||||
merge(hosts, aliasKey(tab.worktreeId, tab.entityId), tab.executionHostId)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const [worktree, tabs] of Object.entries(state.tabsByWorktree ?? EMPTY)) {
|
||||
for (const tab of tabs) {
|
||||
visits++
|
||||
const host = ptyHost(tab.ptyId)
|
||||
if (host !== undefined) {
|
||||
merge(hosts, aliasKey(worktree, tab.id), host)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const retained of Object.values(state.retainedAgentsByPaneKey ?? EMPTY)) {
|
||||
visits++
|
||||
const host = ptyHost(retained.tab.ptyId)
|
||||
if (host !== undefined) {
|
||||
merge(hosts, aliasKey(retained.worktreeId, retained.tab.id), host)
|
||||
}
|
||||
}
|
||||
terminalCache = { inputs, hosts, ptys }
|
||||
return terminalCache
|
||||
}
|
||||
|
||||
function projection(state: VisibleOrchestrationState): NonNullable<typeof cache> {
|
||||
const rows = rowIdentities(state)
|
||||
const terminals = terminalProvenance(state)
|
||||
const inputs = [
|
||||
state.runtimeAgentOrchestrationByPaneKey,
|
||||
state.agentStatusByPaneKey,
|
||||
state.retainedAgentsByPaneKey,
|
||||
terminals,
|
||||
rows
|
||||
]
|
||||
if (cache && inputs.every((value, index) => value === cache?.inputs[index])) {
|
||||
return cache
|
||||
}
|
||||
builds++
|
||||
const { aliases, canonical } = rows
|
||||
const contexts: Contexts = Object.fromEntries([])
|
||||
const visibleKey = (key: string, host: string | null) => {
|
||||
if (!host) {
|
||||
return canonical.has(key) ? aliasKey('unknown', key) : key
|
||||
}
|
||||
const alias = aliases.get(aliasKey(host, key))
|
||||
// An identically spelled row from another runtime cannot satisfy this relationship.
|
||||
return (
|
||||
alias ?? (canonical.has(key) && canonical.get(key)?.host !== host ? aliasKey(host, key) : key)
|
||||
)
|
||||
}
|
||||
const adapt = (key: string, context: AgentStatusOrchestrationContext, host: string | null) => {
|
||||
visits++
|
||||
const actual = visibleKey(key, host)
|
||||
const parent = context.parentPaneKey ? visibleKey(context.parentPaneKey, host) : undefined
|
||||
const next = parent === context.parentPaneKey ? context : { ...context, parentPaneKey: parent }
|
||||
const previous = cache?.contexts[actual]
|
||||
contexts[actual] =
|
||||
next !== context && previous && orchestrationContextsEqual(previous, next) ? previous : next
|
||||
}
|
||||
for (const [key, context] of Object.entries(state.runtimeAgentOrchestrationByPaneKey ?? EMPTY)) {
|
||||
adapt(key, context, producerByContext.get(context) ?? LOCAL_EXECUTION_HOST_ID)
|
||||
}
|
||||
const embedded = (
|
||||
entry: AppState['agentStatusByPaneKey'][string],
|
||||
worktreeId = entry.worktreeId
|
||||
) => {
|
||||
visits++
|
||||
if (!entry?.orchestration) {
|
||||
return
|
||||
}
|
||||
const row = canonical.get(entry.paneKey)
|
||||
const runtime = state.runtimeAgentOrchestrationByPaneKey?.[row?.key ?? entry.paneKey]
|
||||
if (
|
||||
runtime &&
|
||||
(!row || row.host === orchestrationProducer(runtime)) &&
|
||||
runtime.taskId === entry.orchestration.taskId &&
|
||||
runtime.dispatchId === entry.orchestration.dispatchId
|
||||
) {
|
||||
// Matching graph facts already carry the context's producer, independently of execution routing.
|
||||
return
|
||||
}
|
||||
const tabId = parseAgentStatusPaneIdentity(entry.paneKey)?.tabId
|
||||
const terminalKey = worktreeId && tabId ? aliasKey(worktreeId, tabId) : null
|
||||
const tabHost = terminalKey ? terminals.hosts.get(terminalKey) : undefined
|
||||
const ptyHost = tabId ? terminals.ptys.get(tabId) : undefined
|
||||
const terminalHost =
|
||||
tabHost === undefined
|
||||
? ptyHost
|
||||
: ptyHost === undefined || tabHost === ptyHost
|
||||
? tabHost
|
||||
: null
|
||||
const host = row
|
||||
? row.host
|
||||
: terminalHost !== undefined
|
||||
? terminalHost
|
||||
: !entry.connectionId && worktreeId
|
||||
? resolveStructuredAgentSessionOwner(state, worktreeId)
|
||||
: null
|
||||
if (
|
||||
entry.orchestration.parentPaneKey &&
|
||||
visibleKey(entry.orchestration.parentPaneKey, host) !== entry.orchestration.parentPaneKey
|
||||
) {
|
||||
adapt(entry.paneKey, entry.orchestration, host)
|
||||
}
|
||||
}
|
||||
for (const entry of Object.values(state.agentStatusByPaneKey ?? EMPTY)) {
|
||||
embedded(entry)
|
||||
}
|
||||
for (const retained of Object.values(state.retainedAgentsByPaneKey ?? EMPTY)) {
|
||||
embedded(retained.entry, retained.worktreeId)
|
||||
}
|
||||
const previous = cache?.contexts
|
||||
const equal =
|
||||
previous &&
|
||||
Object.keys(previous).length === Object.keys(contexts).length &&
|
||||
Object.keys(contexts).every((key) => previous[key] === contexts[key])
|
||||
cache = { inputs, contexts: equal ? previous : contexts }
|
||||
return cache
|
||||
}
|
||||
|
||||
/** Canonical contexts stay in storage; actual rows and parent aliases are a shared read-only view. */
|
||||
export function selectVisibleAgentOrchestration(state: VisibleOrchestrationState): Contexts {
|
||||
if (!state.unifiedTabsByWorktree) {
|
||||
return state.runtimeAgentOrchestrationByPaneKey ?? EMPTY
|
||||
}
|
||||
return projection(state).contexts
|
||||
}
|
||||
|
||||
/** The same row identity joins graph enrichment and first status paint, without translating stored parents. */
|
||||
export function canonicalOrchestrationForRow(
|
||||
state: VisibleOrchestrationState,
|
||||
paneKey: string
|
||||
): AgentStatusOrchestrationContext | undefined {
|
||||
const row = rowIdentities(state).canonical.get(paneKey)
|
||||
const context = state.runtimeAgentOrchestrationByPaneKey?.[row?.key ?? paneKey]
|
||||
return context &&
|
||||
(!row || row.host === (producerByContext.get(context) ?? LOCAL_EXECUTION_HOST_ID))
|
||||
? context
|
||||
: undefined
|
||||
}
|
||||
|
||||
export function representedOrchestrationWorktrees(
|
||||
state: VisibleOrchestrationState
|
||||
): ReadonlyMap<string, string> {
|
||||
return rowIdentities(state).worktrees
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { canonicalOrchestrationForRow } from '@/runtime/visible-agent-orchestration'
|
||||
import { sameAgentStatusTerminalOwner } from '../../../../../shared/agent-status-terminal-owner'
|
||||
import type { AppState } from '../../types'
|
||||
import { resolveAgentStatusLiveEntryMainAgent } from './agent-status-live-entry-main-agent'
|
||||
@@ -141,7 +142,7 @@ export function buildAgentStatusLiveEntry(
|
||||
) {
|
||||
return { entry: null, reason: 'suppressed-inherited-terminal' }
|
||||
}
|
||||
const runtimeOrchestration = state.runtimeAgentOrchestrationByPaneKey[paneKey]
|
||||
const runtimeOrchestration = canonicalOrchestrationForRow(state, paneKey)
|
||||
const runtimeMergedOrchestration = runtimeOrchestration
|
||||
? mergeCurrentOrchestrationContext(existing?.orchestration, runtimeOrchestration)
|
||||
: undefined
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
canonicalOrchestrationForRow,
|
||||
recordOrchestrationProducer,
|
||||
orchestrationProducer
|
||||
} from '@/runtime/visible-agent-orchestration'
|
||||
import { LOCAL_EXECUTION_HOST_ID } from '../../../../../shared/execution-host'
|
||||
import type {
|
||||
AgentStatusEntry,
|
||||
AgentStatusOrchestrationContext
|
||||
@@ -10,7 +16,7 @@ import {
|
||||
} from '../../../../../shared/agent-row-primary-text'
|
||||
import {
|
||||
mergeCurrentOrchestrationContext,
|
||||
orchestrationMapsEqual
|
||||
orchestrationContextsEqual
|
||||
} from './agent-status-orchestration-context'
|
||||
|
||||
export function createAgentStatusOrchestrationActions(
|
||||
@@ -19,20 +25,42 @@ export function createAgentStatusOrchestrationActions(
|
||||
const { get, set } = runtime
|
||||
return {
|
||||
setRuntimeAgentOrchestrationByPaneKey: (
|
||||
entries: Record<string, AgentStatusOrchestrationContext>
|
||||
entries: Record<string, AgentStatusOrchestrationContext>,
|
||||
producerHost = LOCAL_EXECUTION_HOST_ID
|
||||
) => {
|
||||
const generatedTitleUpdates: AgentStatusEntry[] = []
|
||||
set((s, changedPaneKeys) => {
|
||||
const runtimeMapChanged = !orchestrationMapsEqual(
|
||||
s.runtimeAgentOrchestrationByPaneKey,
|
||||
entries
|
||||
)
|
||||
const previous = s.runtimeAgentOrchestrationByPaneKey
|
||||
const reconciled: Record<string, AgentStatusOrchestrationContext> = Object.fromEntries([])
|
||||
let runtimeMapChanged = Object.keys(previous).length !== Object.keys(entries).length
|
||||
for (const [key, context] of Object.entries(entries)) {
|
||||
const old = previous[key]
|
||||
const value =
|
||||
old &&
|
||||
orchestrationProducer(old) === producerHost &&
|
||||
orchestrationContextsEqual(old, context)
|
||||
? old
|
||||
: context
|
||||
reconciled[key] = value
|
||||
recordOrchestrationProducer(value, producerHost)
|
||||
if (value !== old) {
|
||||
runtimeMapChanged = true
|
||||
}
|
||||
}
|
||||
const canonicalState = {
|
||||
...s,
|
||||
runtimeAgentOrchestrationByPaneKey: runtimeMapChanged ? reconciled : previous
|
||||
}
|
||||
let nextLive = s.agentStatusByPaneKey
|
||||
let liveChanged = false
|
||||
let nextRetained = s.retainedAgentsByPaneKey
|
||||
let retainedChanged = false
|
||||
|
||||
for (const [paneKey, runtimeOrchestration] of Object.entries(entries)) {
|
||||
for (const paneKey of new Set([...Object.keys(nextLive), ...Object.keys(nextRetained)])) {
|
||||
const runtimeOrchestration = canonicalOrchestrationForRow(canonicalState, paneKey)
|
||||
if (!runtimeOrchestration) {
|
||||
continue
|
||||
}
|
||||
const liveEntry = nextLive[paneKey]
|
||||
if (liveEntry) {
|
||||
const merged = mergeCurrentOrchestrationContext(
|
||||
@@ -84,7 +112,7 @@ export function createAgentStatusOrchestrationActions(
|
||||
}
|
||||
|
||||
return {
|
||||
...(runtimeMapChanged ? { runtimeAgentOrchestrationByPaneKey: entries } : {}),
|
||||
...(runtimeMapChanged ? { runtimeAgentOrchestrationByPaneKey: reconciled } : {}),
|
||||
...(liveChanged ? { agentStatusByPaneKey: nextLive } : {}),
|
||||
...(retainedChanged ? { retainedAgentsByPaneKey: nextRetained } : {}),
|
||||
...(liveChanged ? { agentStatusEpoch: s.agentStatusEpoch + 1 } : {})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { canonicalOrchestrationForRow } from '@/runtime/visible-agent-orchestration'
|
||||
import type { RetainedAgentEntry } from './agent-status-contract'
|
||||
import type { AgentStatusSlice } from './agent-status-slice-contract'
|
||||
import type { AgentStatusRuntime } from './agent-status-runtime'
|
||||
@@ -34,7 +35,7 @@ export function createAgentStatusRetentionActions(
|
||||
}
|
||||
const next = { ...s.retainedAgentsByPaneKey }
|
||||
for (const retained of entries) {
|
||||
const runtimeOrchestration = s.runtimeAgentOrchestrationByPaneKey[retained.entry.paneKey]
|
||||
const runtimeOrchestration = canonicalOrchestrationForRow(s, retained.entry.paneKey)
|
||||
const mergedOrchestration = runtimeOrchestration
|
||||
? mergeCurrentOrchestrationContext(retained.entry.orchestration, runtimeOrchestration)
|
||||
: retained.entry.orchestration
|
||||
|
||||
@@ -121,7 +121,8 @@ export type AgentStatusSlice = {
|
||||
clearAgentLaunchConfig: (paneKey: string) => void
|
||||
|
||||
setRuntimeAgentOrchestrationByPaneKey: (
|
||||
entries: Record<string, AgentStatusOrchestrationContext>
|
||||
entries: Record<string, AgentStatusOrchestrationContext>,
|
||||
producerHost?: string
|
||||
) => void
|
||||
|
||||
setMigrationUnsupportedPty: (entry: MigrationUnsupportedPtyEntry) => void
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getStructuredAgentSessionTabsByPaneKey } from '@/components/native-chat/structured-agent-session-tabs'
|
||||
import type { AppState } from '../types'
|
||||
import type { WorkspaceCleanupCandidate } from '../../../../shared/workspace-cleanup'
|
||||
import { parsePaneKey } from '../../../../shared/stable-pane-id'
|
||||
@@ -68,6 +69,9 @@ export function getWorkspaceCleanupAgentEvidence(
|
||||
for (const [worktreeId, worktreeCandidates] of candidatesByWorktreeId) {
|
||||
const rows = buildWorktreeAgentRows({
|
||||
tabs: state.tabsByWorktree[worktreeId] ?? [],
|
||||
representedTabs: state.unifiedTabsByWorktree
|
||||
? getStructuredAgentSessionTabsByPaneKey(state.unifiedTabsByWorktree)
|
||||
: undefined,
|
||||
entries: selectLiveAgentStatusEntriesForWorktree(state, worktreeId),
|
||||
retained: selectRetainedAgentEntriesForWorktree(state, worktreeId),
|
||||
runtimePaneTitlesByTabId: state.runtimePaneTitlesByTabId,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Tab } from './tab-types'
|
||||
import { structuredAgentSessionPaneKey } from './structured-agent-session-projection'
|
||||
import type { AgentStatusEntry, AgentStatusOrchestrationContext } from './agent-status-types'
|
||||
import { isWebTerminalSurfaceTabId } from './terminal-surface-id'
|
||||
import { parseLegacyNumericPaneKey, parsePaneKey } from './stable-pane-id'
|
||||
@@ -5,11 +7,18 @@ import { parseLegacyNumericPaneKey, parsePaneKey } from './stable-pane-id'
|
||||
export type AgentStatusPaneIdentity = { tabId: string; paneId: string }
|
||||
|
||||
export function parseAgentStatusPaneIdentity(
|
||||
paneKey: string | undefined
|
||||
paneKey: string | undefined,
|
||||
representedTab?: Pick<Tab, 'id' | 'entityId' | 'contentType'>
|
||||
): AgentStatusPaneIdentity | null {
|
||||
if (!paneKey) {
|
||||
return null
|
||||
}
|
||||
if (
|
||||
representedTab?.contentType === 'agent-session' &&
|
||||
structuredAgentSessionPaneKey(representedTab.id, representedTab.entityId) === paneKey
|
||||
) {
|
||||
return { tabId: representedTab.id, paneId: paneKey.slice(representedTab.id.length + 1) }
|
||||
}
|
||||
const parsed = parsePaneKey(paneKey)
|
||||
if (parsed) {
|
||||
return { tabId: parsed.tabId, paneId: parsed.leafId }
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { AgentStatusOrchestrationContext } from './agent-status-types'
|
||||
import type { OrcaSessionId } from './orca-session-address'
|
||||
import type { RuntimeRpcResponse } from './runtime-rpc-envelope'
|
||||
import type { RuntimeWorktreePsSummary } from './runtime-types'
|
||||
|
||||
// Keep desktop-only imports in the native fixture; its export checks this consumer contract.
|
||||
export type RuntimeAgentLineageTestModule = {
|
||||
LINEAGE_PARENT: OrcaSessionId
|
||||
LINEAGE_CHILD: OrcaSessionId
|
||||
canonicalChatRow(id: string): string
|
||||
publicLineageFixture(worktreeId?: string): {
|
||||
db: {
|
||||
db: { prepare(sql: string): { run(...values: string[]): unknown } }
|
||||
close(): void
|
||||
}
|
||||
projection: {
|
||||
buildByPaneKey(): Record<string, AgentStatusOrchestrationContext> | undefined
|
||||
}
|
||||
build(): RuntimeWorktreePsSummary
|
||||
privateHandle: string
|
||||
privatePane: string
|
||||
run: { id: string }
|
||||
}
|
||||
}
|
||||
|
||||
export type RuntimeAgentLineageCatalogTestModule = {
|
||||
createLineageCatalogDispatcher(build: () => RuntimeWorktreePsSummary): {
|
||||
dispatch(request: {
|
||||
id: string
|
||||
authToken: string
|
||||
method: string
|
||||
params?: Record<string, unknown>
|
||||
}): Promise<RuntimeRpcResponse<unknown>>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user