diff --git a/src/main/agent-hooks/server/server-ingest-terminal.ts b/src/main/agent-hooks/server/server-ingest-terminal.ts index 9971bd1e2c1..af40a1955f4 100644 --- a/src/main/agent-hooks/server/server-ingest-terminal.ts +++ b/src/main/agent-hooks/server/server-ingest-terminal.ts @@ -88,6 +88,9 @@ export abstract class AgentHookServerIngestTerminal extends AgentHookServerInges ) { // Why: OSC has no child identity or lead boundary, so it cannot replace a persisted child-only proof before the lifecycle hook arrives. this.commitStatusRowMutation(mutationBefore, previous) + if (mutationBefore !== undefined) { + this.emitEnrichedStatus(previous) + } return } // Why: preserve the hook-completed turn stamp while OSC repaints the current state. diff --git a/src/main/agent-hooks/terminal-handle-row-identity.test.ts b/src/main/agent-hooks/terminal-handle-row-identity.test.ts index 0b22e27c954..35c85d99066 100644 --- a/src/main/agent-hooks/terminal-handle-row-identity.test.ts +++ b/src/main/agent-hooks/terminal-handle-row-identity.test.ts @@ -197,6 +197,31 @@ describe('the terminal handle a status row is stamped with', () => { ) }) + it('publishes only the remint observation for a Claude child-only row', () => { + const server = new AgentHookServer() + const enriched = vi.fn() + server.subscribeEnrichedStatus(enriched) + const payload = { state: 'working' as const, prompt: 'ship it', agentType: 'claude' as const } + ingest(server, { payload }) + const row = server._getStateForTests().lastStatusByPaneKey.get(PANE_KEY) as + | { claudeLeadBoundaryChildOnly?: true } + | undefined + if (!row) { + throw new Error('expected seeded status row') + } + row.claudeLeadBoundaryChildOnly = true + enriched.mockClear() + + ingest(server, { payload }) + expect(enriched).not.toHaveBeenCalled() + + ingest(server, { paneKey: NEW_PANE_KEY, tabId: 'tab-reminted', payload }) + expect(enriched).toHaveBeenCalledOnce() + expect(enriched).toHaveBeenCalledWith( + expect.objectContaining({ paneKey: NEW_PANE_KEY, terminalHandle: HANDLE }) + ) + }) + it('does not renew freshness from a provider-session-only dismissal remnant', () => { const server = new AgentHookServer() const freshness = vi.fn() diff --git a/src/main/orcad/orcad-entry.ts b/src/main/orcad/orcad-entry.ts index fc991866452..137894f87b8 100644 --- a/src/main/orcad/orcad-entry.ts +++ b/src/main/orcad/orcad-entry.ts @@ -190,10 +190,10 @@ async function startOrcadRuntime( // which is safe but silently discards accept records on every launch. initSshHostKeyStoreFile(profile.dataFile) + uninstallObservedStatusIdentity = agentHookServer.subscribeEnrichedStatus((enriched) => + observedStatusCapture.observe(enriched) + ) if (isAgentStatusHooksEnabled(store.getSettings())) { - uninstallObservedStatusIdentity = agentHookServer.subscribeEnrichedStatus((enriched) => - observedStatusCapture.observe(enriched) - ) await agentHookServer.start({ env: 'production', userDataPath: runtimeUserDataPath }) } @@ -258,9 +258,6 @@ async function startOrcadRuntime( // spawning an unauthenticated agent. await registerHeadlessPtyRuntime(runtime, undefined, () => store.getSettings(), undefined, store) - // PTY recovery binds terminal identities; only now can startup spool observations be fenced. - observedStatusCapture.attach(runtime) - // Why: same post-registration reconciliation `--serve` performs. Skipping it leaves // restored orchestration rows claiming an authority this host never took over. // Why before the RPC server binds: a client host attaching first would find no pages to recover. @@ -269,6 +266,9 @@ async function startOrcadRuntime( await runtime.refreshRestoredOrchestrationAuthority() await runtime.reconcileLegacyWorkerTerminals() + // Recovery binds terminal and dispatch identities; only now can startup observations be fenced. + observedStatusCapture.attach(runtime) + const bindHost = resolveOrcadBindHost(options.bind) rpc = new OrcaRuntimeRpcServer({ runtime, diff --git a/src/main/startup/headless-pty-hydration-ordering.test.ts b/src/main/startup/headless-pty-hydration-ordering.test.ts index 8e437be0449..3b661dede99 100644 --- a/src/main/startup/headless-pty-hydration-ordering.test.ts +++ b/src/main/startup/headless-pty-hydration-ordering.test.ts @@ -78,12 +78,14 @@ describe('headless PTY registry hydration ordering', () => { const runtime = source.indexOf('const runtime = new OrcaRuntimeService(') const identityReader = source.indexOf('readObservedAgentStatusPaneIdentity:', runtime) const identitySubscription = source.indexOf('agentHookServer.subscribeEnrichedStatus(') + const hooksEnabled = source.indexOf('if (isAgentStatusHooksEnabled(', identitySubscription) const identityFlush = source.indexOf('observedStatusCapture.attach(runtime)', runtime) expect(runtime).toBeGreaterThanOrEqual(0) expect(identityReader).toBeGreaterThan(runtime) expect(identitySubscription).toBeGreaterThanOrEqual(0) expect(identitySubscription).toBeLessThan(runtime) + expect(hooksEnabled).toBeGreaterThan(identitySubscription) expect(identityFlush).toBeGreaterThan(runtime) expect(source.slice(identitySubscription, runtime)).toContain( 'observedStatusCapture.observe(enriched)' @@ -96,13 +98,17 @@ describe('headless PTY registry hydration ordering', () => { const hookStart = source.indexOf('await agentHookServer.start(', subscription) const runtime = source.indexOf('const runtime = new OrcaRuntimeService(') const handlers = source.indexOf('await registerHeadlessPtyRuntime(', runtime) + const identityRecovery = source.indexOf('await runtime.refreshRestoredOrchestrationAuthority()') + const workerRecovery = source.indexOf('await runtime.reconcileLegacyWorkerTerminals()') const replay = source.indexOf('observedStatusCapture.attach(runtime)', runtime) expect(subscription).toBeGreaterThanOrEqual(0) expect(hookStart).toBeGreaterThan(subscription) expect(runtime).toBeGreaterThan(hookStart) expect(handlers).toBeGreaterThan(runtime) - expect(replay).toBeGreaterThan(handlers) + expect(identityRecovery).toBeGreaterThan(handlers) + expect(workerRecovery).toBeGreaterThan(identityRecovery) + expect(replay).toBeGreaterThan(workerRecovery) expect(source.slice(subscription, runtime)).toContain('observedStatusCapture.observe(enriched)') expect(source.slice(replay)).toContain('observedStatusCapture.attach(runtime)') })