diff --git a/src/main/runtime/agent-prompt-submission-verification.test.ts b/src/main/runtime/agent-prompt-submission-verification.test.ts index 1d3d1791f24..ec7c2b85378 100644 --- a/src/main/runtime/agent-prompt-submission-verification.test.ts +++ b/src/main/runtime/agent-prompt-submission-verification.test.ts @@ -4,6 +4,7 @@ import { AGENT_PROMPT_HOOK_EFFECT_TIMEOUT_MS, type AgentPromptActivity, isAgentPromptStalledError, + readAgentPromptWaitText, resolveAgentPromptEffectTimeoutMs, verifyAgentPromptSubmission } from './agent-prompt-submission-verification' @@ -23,6 +24,17 @@ function activity(overrides: Partial = {}): AgentPromptActi describe('agent prompt submission verification', () => { afterEach(() => vi.useRealTimers()) + it('reuses wait text while the PTY output sequence is unchanged', () => { + const cache: { outputSequence?: number; waitText?: string } = {} + const readWaitText = vi.fn(() => 'retained terminal tail') + + expect(readAgentPromptWaitText(cache, 7, readWaitText)).toBe('retained terminal tail') + expect(readAgentPromptWaitText(cache, 7, readWaitText)).toBe('retained terminal tail') + expect(readAgentPromptWaitText(cache, 8, readWaitText)).toBe('retained terminal tail') + + expect(readWaitText).toHaveBeenCalledTimes(2) + }) + it('accepts an observed working transition', async () => { vi.useFakeTimers() let current = activity() diff --git a/src/main/runtime/agent-prompt-submission-verification.ts b/src/main/runtime/agent-prompt-submission-verification.ts index e394d4e8f62..5bd1a622321 100644 --- a/src/main/runtime/agent-prompt-submission-verification.ts +++ b/src/main/runtime/agent-prompt-submission-verification.ts @@ -22,6 +22,11 @@ export type AgentPromptActivity = Readonly<{ status: 'working' | 'permission' | 'idle' | null }> +export type AgentPromptWaitTextCache = { + outputSequence?: number + waitText?: string +} + type AgentPromptVerificationOptions = { baseline: AgentPromptActivity readActivity: () => AgentPromptActivity @@ -47,6 +52,20 @@ export function isAgentPromptStalledError(error: unknown): boolean { ) } +export function readAgentPromptWaitText( + cache: AgentPromptWaitTextCache, + outputSequence: number, + readWaitText: () => string +): string { + if (cache.outputSequence === outputSequence && cache.waitText !== undefined) { + return cache.waitText + } + const waitText = readWaitText() + cache.outputSequence = outputSequence + cache.waitText = waitText + return waitText +} + export async function verifyAgentPromptSubmission( options: AgentPromptVerificationOptions ): Promise { diff --git a/src/main/runtime/orca-runtime.ts b/src/main/runtime/orca-runtime.ts index 63e3d6bd5cf..1e8676d6e99 100644 --- a/src/main/runtime/orca-runtime.ts +++ b/src/main/runtime/orca-runtime.ts @@ -106,6 +106,8 @@ import { } from '../../shared/agent-prompt-injection' import { type AgentPromptActivity, + type AgentPromptWaitTextCache, + readAgentPromptWaitText, resolveAgentPromptEffectTimeoutMs, verifyAgentPromptSubmission } from './agent-prompt-submission-verification' @@ -19366,7 +19368,8 @@ export class OrcaRuntimeService { private getTerminalAgentStatusSnapshot( handle: string, - expectedPtyId: string + expectedPtyId: string, + waitTextOverride?: string ): TerminalAgentStatusSnapshot { const pty = this.getLivePtyForHandle(handle) if (pty) { @@ -19386,11 +19389,9 @@ export class OrcaRuntimeService { { title: pty.pty.title, updatedAt: pty.pty.titleUpdatedAt }, { title: pty.pty.lastOscTitle, updatedAt: pty.pty.lastOscTitleAt } ) - const waitText = buildTerminalWaitText( - pty.pty.tailBuffer, - pty.pty.tailPartialLine, - pty.pty.preview - ) + const waitText = + waitTextOverride ?? + buildTerminalWaitText(pty.pty.tailBuffer, pty.pty.tailPartialLine, pty.pty.preview) return { waitText, waitBlockedAt: pty.pty.waitBlockedAt, @@ -19418,7 +19419,9 @@ export class OrcaRuntimeService { { title: this.tabs.get(leaf.tabId)?.title, updatedAt: 0 } ) return { - waitText: buildTerminalWaitText(leaf.tailBuffer, leaf.tailPartialLine, leaf.preview), + waitText: + waitTextOverride ?? + buildTerminalWaitText(leaf.tailBuffer, leaf.tailPartialLine, leaf.preview), waitBlockedAt: leaf.waitBlockedAt, title: title?.title ?? null, titleStatus: title ? detectAgentStatusFromTitle(title.title) : leaf.lastAgentStatus, @@ -20077,7 +20080,8 @@ export class OrcaRuntimeService { } assertAgentPromptRequestActive(options.signal) this.assertAgentPromptGeneration(ptyId, generation) - const baseline = this.getAgentPromptActivity(handle, ptyId) + const waitTextCache: AgentPromptWaitTextCache = {} + const baseline = this.getAgentPromptActivity(handle, ptyId, waitTextCache) this.assertAgentPromptPermissionSafe(permissionBaseline, baseline) const suffixWrote = this.ptyController?.write(ptyId, AGENT_PROMPT_SUBMIT) ?? false if (!suffixWrote) { @@ -20085,7 +20089,7 @@ export class OrcaRuntimeService { } await verifyAgentPromptSubmission({ baseline, - readActivity: () => this.getAgentPromptActivity(handle, ptyId), + readActivity: () => this.getAgentPromptActivity(handle, ptyId, waitTextCache), timeoutMs: resolveAgentPromptEffectTimeoutMs(this.getPtyAgent(ptyId)), signal: options.signal }) @@ -20114,8 +20118,13 @@ export class OrcaRuntimeService { } } - private getAgentPromptActivity(handle: string, ptyId: string): AgentPromptActivity { + private getAgentPromptActivity( + handle: string, + ptyId: string, + waitTextCache?: AgentPromptWaitTextCache + ): AgentPromptActivity { this.assertLiveTerminalHandleTargetsPty(handle, ptyId) + const outputSequence = this.getPtyOutputSequence(ptyId) const explicitCandidate = this.getFreshExplicitAgentStatusForHandle(handle) const explicitFloor = this.agentPromptExplicitStatusFloorByPtyId.get(ptyId) const explicit = @@ -20133,7 +20142,14 @@ export class OrcaRuntimeService { (!explicit || lifecycle.updatedAt > explicit.updatedAt || (lifecycle.updatedAt === explicit.updatedAt && lifecycle.status === 'permission')) - const terminal = this.getTerminalAgentStatusSnapshot(handle, ptyId) + const waitText = waitTextCache + ? readAgentPromptWaitText( + waitTextCache, + outputSequence, + () => this.getTerminalAgentStatusSnapshot(handle, ptyId).waitText + ) + : undefined + const terminal = this.getTerminalAgentStatusSnapshot(handle, ptyId, waitText) const status = this.hasAuthoritativeTerminalWaitPermission(terminal, explicit, lifecycle) ? 'permission' : lifecycleIsNewer @@ -20148,7 +20164,7 @@ export class OrcaRuntimeService { // stateStartedAt, not updatedAt — same-state tool/prompt pings refresh updatedAt and would // otherwise pass off an in-progress turn as a new one. explicitWorkingStartedAt: explicit?.status === 'working' ? explicit.stateStartedAt : null, - outputSequence: this.getPtyOutputSequence(ptyId), + outputSequence, status } }