Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0f292ca
fix: correct managed OMP readiness and lifecycle status
sauravhiremath Sep 30, 2026
426cea0
fix: preserve genuine OMP readiness title evidence
sauravhiremath Sep 30, 2026
39e2d31
fix: keep OMP async completion checks after agent settled
sauravhiremath Sep 30, 2026
38c5d65
fix: reject stale-clear renderer titles as idle evidence
sauravhiremath Sep 30, 2026
bb71fd2
fix: preserve OMP readiness boundaries after completed turns
sauravhiremath Sep 30, 2026
6573145
Merge upstream/main into fix/omp-status-mapping
sauravhiremath Oct 1, 2026
370eb49
fix: require shell evidence before retiring agent launch authority
sauravhiremath Oct 1, 2026
58a7bb7
fix: retain agent status and completed turn duration
sauravhiremath Oct 1, 2026
bee3570
fix: fence stale OMP launch authority and hook retirement
sauravhiremath Oct 1, 2026
0b2b1bb
Merge upstream/main into fix/omp-status-mapping
sauravhiremath Oct 2, 2026
1b79ee7
Merge main and reconcile OMP readiness with native title evidence
sauravhiremath Oct 3, 2026
a1f705a
Merge upstream main and preserve managed OMP readiness
sauravhiremath Oct 5, 2026
2f5e87c
Merge upstream main and simplify OMP status integration
sauravhiremath Oct 7, 2026
e9ed8c4
Keep upstream formatting outside the OMP status fix
sauravhiremath Oct 7, 2026
0dfb9ea
Merge latest upstream main into OMP status fix
sauravhiremath Oct 9, 2026
7f77bd6
docs: record completed OMP agent status reproduction
sauravhiremath Oct 15, 2026
4756879
Merge upstream main and preserve OMP status lifecycle
sauravhiremath Oct 15, 2026
5909949
Merge upstream main and tighten OMP status ownership
sauravhiremath Oct 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions docs/bug-reproductions/omp-completed-agent-stale-status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Completed OMP agents still show active in Orca

Reported: 2026-10-01
Status: Confirmed worker status mismatch. This record describes the original investigation, not the current implementation.
Fix and current verification: [PR #24196](https://github.com/stablyai/orca/pull/24196).
Build observed during the test: `1.4.214-local.1790834225853.65731454c1c6`.
Runtime: `696fd1e3-312f-4b8d-8d92-35f422dbe46e`.

## User-visible failure

The user supplied two screenshots and reported that the main agent and a worker still showed in progress after both had completed. The screenshots are in the conversation, not copied into this repository. They must remain part of the issue evidence; this file does not claim to attach them.

Expected: After a turn and its background jobs complete, the agent status must show done. Sidebar and tab status must agree. Keeping an OMP process and terminal open must not by itself mean the agent is working.

## Live evidence

Run: `run_429bdaf8c24e`.

| Worker | Task | Dispatch | Result | Live status |
| ------ | ------------------- | ------------------ | --------------------------------- | ----------------------------------------------------------------------------------- |
| A | `task_5b92fcefe26b` | `ctx_0fdb3ba46a37` | `succeeded`; dispatch `completed` | `unavailable`; liveness `unverifiable`, reason `missing_status`; activity `unknown` |
| B | `task_b8afdc8d5ccc` | `ctx_1320df23f4dc` | `succeeded`; dispatch `completed` | `fresh`; activity `done`; main-agent outcome `success` |

Worker A terminal: `term_a2263d24-1d7b-4f28-bc6e-b41f22abfa2c`.
Worker B terminal: `term_0a4c7fb2-76e0-470c-bd48-e7ffe7703e2b`.

Worker A's terminal screen showed:

- `LIVE_READY_PASS` after `sleep 20` completed.
- `BACKGROUND_JOB_PENDING` after an asynchronous `sleep 60` started.
- The background job completion notice.
- `BACKGROUND_JOB_FINISHED`, followed by the input prompt.

Thus, the worker had completed its later live probes as well as its original task. Terminal `status: running` means the OMP process remains open, not that it is still performing the task.

`orca worktree ps --json` exposed only worker B in the worktree's `agents` list, with `state: done`. The main agent and worker A were absent. The worktree reported `status: working` and `workspaceStatus: in-progress`.

Important limit: The main agent was running this investigation when that snapshot was taken. Its current working state is expected. That snapshot does not independently prove that the main agent was idle at the time of the user's screenshots. The reported stale main-agent status remains a user-observed failure; worker A's missing live status is independently confirmed.

## Earlier hook-delivery probe

A separate, now-closed diagnostic terminal completed a normal turn. Its canonical status-cache row was absent. `/new` restored its ready row, but the row was absent again after the next completed turn. The diagnostic extension reported HTTP `204` for `before_agent_start`, `agent_start`, `message_end`, and `agent_end`.

This shows that an HTTP acknowledgement does not prove that Orca retained and displayed the status row. It does not identify the exact path that removed or rejected the row.

## Source checks and cause limits

- `src/main/runtime/runtime-hook-agent-row-selection.ts`: fresh explicit status selection excludes restored-unconfirmed, identity-only, and stale observations.
- `src/main/runtime/runtime-worktree-agent-rows.ts`: fresh non-done rows contribute active status; completed worker B alone does not account for an active agent row.
- `src/main/agent-hooks/server/server-status-disposition.ts`: pane retirement and launch authority can suppress or restart status admission.
- `src/main/agent-hooks/server/server-cleanup.ts`: status rows can be removed by dismissal or pane cleanup.

[INFERENCE] The absent explicit status may leave another status source visible in the UI. Neither the removal path nor the UI fallback responsible for the screenshots has been established. Do not label this a renderer-only bug or a failed task.

## Reproduction and verification commands

1. Start two OMP workers through Orca. Complete both tasks and leave their terminals open.
2. In worker A, run a normal turn, then a background `sleep 60` turn. Let the job complete and the worker return to its input prompt.
3. Compare the terminal screen, worker result, live status, sidebar, and tab indicator.
4. Check whether a session reset restores the status row only until the next turn completes.

Commands exercised during this investigation:

```text
orca terminal read --terminal term_a2263d24-1d7b-4f28-bc6e-b41f22abfa2c --screen --json
orca orchestration worker-list --run run_429bdaf8c24e --json
orca worktree ps --json
```

No status was changed by hand. Both worker terminals were retained at the end of this investigation. No implementation changes or test runs were made during that investigation. Earlier automated test passes do not disprove the recorded live failure.

## Fix acceptance checks

- Both retained, completed workers expose explicit done status.
- The main agent shows done after its response ends; check it from a separate terminal to avoid starting a new main-agent turn.
- Sidebar and tab indicators agree with the explicit status.
- Normal turns, session reset, and background-job completion do not lose the live status row.
- Actual active work still shows working, and an open input dialog still shows waiting.
12 changes: 0 additions & 12 deletions src/main/agent-hooks/server-agent-presence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,18 +244,6 @@ describe('host-owned hook presence', () => {
}
)

it('reports a checkable agent process only for an identified, running owner', async () => {
const server = await createServer()
expect(server.hasVerifiableAgentProcess(PANE)).toBe(false)
await hook(server, 'SessionStart', 'unidentified', undefined, null)
expect(server.hasVerifiableAgentProcess(PANE)).toBe(false)
const identified = await createServer()
await hook(identified, 'SessionStart')
expect(identified.hasVerifiableAgentProcess(PANE)).toBe(true)
await hook(identified, 'SessionEnd', 'session-a', 'prompt_input_exit')
expect(identified.hasVerifiableAgentProcess(PANE)).toBe(false)
})

it('checks each pane once after replaying its spooled hooks', async () => {
const userDataPath = mkdtempSync(join(tmpdir(), 'orca-presence-spool-'))
const first = new AgentHookServer()
Expand Down
9 changes: 0 additions & 9 deletions src/main/agent-hooks/server/server-agent-presence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,6 @@ export abstract class AgentHookServerAgentPresence extends AgentHookServerLifecy
// Why: a live process already reports from these panes, so their owner's exit is not the pane's.
private readonly ownerChallengesByLiveSender = new Map<string, number>()

/** Whether this pane's owner carries a process identity that its execution host can check. */
hasVerifiableAgentProcess(paneKey: string): boolean {
const presence = this.state.lastStatusByPaneKey.get(
this.resolvePaneKeyAlias(paneKey)
)?.agentPresence
return presence?.process !== undefined && !presence.ended
}

/** A local inspection keeps the exact hook row, rather than probing the pane's next owner. */
readCliWriterObservation(expected: AgentStatusIpcPayload): CliWriterObservation | null {
const resolved = this.resolvePaneKeyAlias(expected.paneKey)
Expand Down Expand Up @@ -94,7 +86,6 @@ export abstract class AgentHookServerAgentPresence extends AgentHookServerLifecy
this.agentExitedAtByPaneKey.get(paneKey)
return exit !== undefined && exit.at >= exitedSince ? 'exited' : null
}

checkAgentPresence(paneKey: string): Promise<AgentProcessVerdict | null> {
const resolved = this.resolvePaneKeyAlias(paneKey)
const row = this.state.lastStatusByPaneKey.get(resolved)
Expand Down
4 changes: 0 additions & 4 deletions src/main/ipc/agent-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@ export function registerAgentHookHandlers(
ipcMain.removeHandler('agentStatus:getSnapshot')
ipcMain.removeHandler('agentStatus:inferInterrupt')
ipcMain.removeHandler('agentStatus:inferQuestionAnswered')
ipcMain.removeHandler('agentStatus:hasVerifiableAgentProcess')
ipcMain.removeHandler('agentStatus:getMigrationUnsupportedSnapshot')
registerAgentStatusRowTeardownIpcHandlers()
registerAgentPaneAuthorityIpcHandlers({
Expand Down Expand Up @@ -70,9 +69,6 @@ export function registerAgentHookHandlers(
}
return agentHookServer.inferQuestionAnswered(request as AgentQuestionAnsweredInferenceRequest)
})
ipcMain.handle('agentStatus:hasVerifiableAgentProcess', (_event, paneKey: unknown): boolean =>
typeof paneKey === 'string' ? agentHookServer.hasVerifiableAgentProcess(paneKey) : false
)
ipcMain.handle(
'agentStatus:getMigrationUnsupportedSnapshot',
(): MigrationUnsupportedPtyEntry[] => getMigrationUnsupportedPtySnapshot()
Expand Down
53 changes: 45 additions & 8 deletions src/main/pi/agent-status-extension-omp-lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
createAgentStatusExtensionHarness,
type AgentStatusExtensionHarness
} from './agent-status-extension-test-harness'
import { normalizedOmpPosts } from './agent-status-subagent-event-fixtures'

function postedHookNames(fetchMock: ReturnType<typeof vi.fn>): string[] {
return fetchMock.mock.calls.map(
Expand All @@ -18,6 +19,34 @@ const OMP_RUNTIME_CASES = [
] as const

describe('OMP agent_end contract', () => {
it.each(OMP_RUNTIME_CASES)(
'publishes %s startup identity before its first turn',
async (_name, args) => {
const harness = createAgentStatusExtensionHarness(args)
await harness.callHook(
'session_start',
{},
{
...ompSession('resumed-session'),
isIdle: () => true,
hasPendingMessages: () => false,
getAsyncJobSnapshot: () => ({ running: [] }),
model: { provider: 'test-provider', id: 'test-model' }
}
)
await vi.waitFor(() => {
const startup = normalizedOmpPosts(harness).at(-1)
expect(startup).toMatchObject({
providerSession: {
id: 'resumed-session',
transcriptPath: '/sessions/resumed-session.jsonl'
},
payload: { state: 'done', sessionBoundary: true, model: 'test-provider/test-model' }
})
})
}
)

it('keeps a Pi pane working until async subagents finish', async () => {
const harness = createAgentStatusExtensionHarness({ kind: 'pi' })

Expand Down Expand Up @@ -107,7 +136,6 @@ describe('OMP agent_end contract', () => {
await vi.waitFor(() =>
expect(postedHookNames(harness.fetchMock)).toEqual(['agent_start', 'agent_end'])
)
expect(context.isIdle).not.toHaveBeenCalled()
}
}
)
Expand Down Expand Up @@ -220,10 +248,13 @@ describe('OMP subagent settlement', () => {
await hook(harness, 'agent_end')
await hook(harness, 'agent_start')
await hook(harness, 'agent_end')
expect(postedHookNames(harness.fetchMock)).not.toContain('agent_end')
expect(normalizedOmpPosts(harness).at(-1)?.payload).toMatchObject({
state: 'working',
subagents: [{ id: 'helper' }]
})

await lifecycle(harness, 'helper', 'completed')
expect(postedHookNames(harness.fetchMock).at(-1)).toBe('agent_end')
expect(normalizedOmpPosts(harness).at(-1)?.payload.state).toBe('done')
}
)

Expand All @@ -237,10 +268,13 @@ describe('OMP subagent settlement', () => {
await lifecycle(harness, 'wake-1', 'started')
await hook(harness, 'agent_start')
await hook(harness, 'agent_end')
expect(postedHookNames(harness.fetchMock).at(-1)).not.toBe('agent_end')
expect(normalizedOmpPosts(harness).at(-1)?.payload).toMatchObject({
state: 'working',
subagents: [{ id: 'wake-1' }]
})

await lifecycle(harness, 'wake-1', 'completed')
expect(postedHookNames(harness.fetchMock).at(-1)).toBe('agent_end')
expect(normalizedOmpPosts(harness).at(-1)?.payload.state).toBe('done')
}
)

Expand All @@ -261,7 +295,7 @@ describe('OMP subagent settlement', () => {
await lifecycle(harness, 'revived', 'started')
await lifecycle(harness, 'revived', 'completed')

expect(postedHookNames(harness.fetchMock)).toEqual(['agent_start', 'agent_end'])
expect(normalizedOmpPosts(harness).at(-1)?.payload.state).toBe('done')
})

it("ignores children seen by an OMP task session's copy of the extension", async () => {
Expand Down Expand Up @@ -295,10 +329,13 @@ describe('OMP subagent settlement', () => {
await lifecycle(harness, 'child-1', 'started')
await hook(harness, 'agent_end')
harness.reload()
expect(postedHookNames(harness.fetchMock)).not.toContain('agent_end')
expect(normalizedOmpPosts(harness).at(-1)?.payload).toMatchObject({
state: 'working',
subagents: [{ id: 'child-1' }]
})

await lifecycle(harness, 'child-1', 'completed')

expect(postedHookNames(harness.fetchMock).at(-1)).toBe('agent_end')
expect(normalizedOmpPosts(harness).at(-1)?.payload.state).toBe('done')
})
})
1 change: 0 additions & 1 deletion src/main/pi/agent-status-extension-omp-prefill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ describe('OMP prefill through the explicitly loaded status extension', () => {
h.reload()
await h.callHook('session_start', {}, { ui: { setEditorText } })
expect(setEditorText).toHaveBeenCalledTimes(1)
expect(h.fetchMock).not.toHaveBeenCalled()
})

it('preserves the draft until an editor is available', async () => {
Expand Down
Loading