mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 16:03:47 +00:00
Preserve paired completion tail ownership
This commit is contained in:
@@ -1449,6 +1449,131 @@ describe('agent completion coordinator', () => {
|
||||
expect(dispatchCompletion).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('keeps sibling tail suppression after the host stamped all-clear arrives first', () => {
|
||||
const dispatchCompletion = vi.fn()
|
||||
const localCoordinator = createAgentCompletionCoordinator({
|
||||
paneKey: 'tab-1:leaf-1',
|
||||
getPtyId: () => 'pty-1',
|
||||
getSettings: () => null,
|
||||
inspectProcess: vi.fn(),
|
||||
dispatchCompletion,
|
||||
isLive: () => true
|
||||
})
|
||||
const hostCoordinator = createAgentCompletionCoordinator({
|
||||
paneKey: 'tab-1:leaf-1',
|
||||
getPtyId: () => 'pty-1',
|
||||
getSettings: () => null,
|
||||
inspectProcess: vi.fn(),
|
||||
dispatchCompletion,
|
||||
isLive: () => true
|
||||
})
|
||||
|
||||
localCoordinator.observeHookStatus({
|
||||
state: 'working',
|
||||
prompt: 'first turn',
|
||||
agentType: 'claude',
|
||||
stateStartedAt: 5_000
|
||||
})
|
||||
hostCoordinator.observeHookStatus({
|
||||
state: 'working',
|
||||
prompt: 'first turn',
|
||||
agentType: 'claude',
|
||||
stateStartedAt: 2_000
|
||||
})
|
||||
hostCoordinator.observeHookStatus({
|
||||
state: 'working',
|
||||
prompt: 'first turn',
|
||||
agentType: 'claude',
|
||||
stateStartedAt: 2_000,
|
||||
turnCompletedAt: 2_500
|
||||
})
|
||||
hostCoordinator.observeHookStatus({
|
||||
state: 'done',
|
||||
prompt: 'first turn',
|
||||
agentType: 'claude',
|
||||
stateStartedAt: 3_000,
|
||||
turnCompletedAt: 2_500
|
||||
})
|
||||
localCoordinator.observeHookStatus({
|
||||
state: 'done',
|
||||
prompt: 'first turn',
|
||||
agentType: 'claude',
|
||||
stateStartedAt: 5_500
|
||||
})
|
||||
localCoordinator.observeHookStatus({
|
||||
state: 'done',
|
||||
prompt: 'first turn',
|
||||
agentType: 'claude',
|
||||
stateStartedAt: 5_500
|
||||
})
|
||||
vi.advanceTimersByTime(HOOK_DONE_QUIET_MS)
|
||||
|
||||
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('releases a stamped tail when its host reports a new same-state turn', () => {
|
||||
const dispatchCompletion = vi.fn()
|
||||
const localCoordinator = createAgentCompletionCoordinator({
|
||||
paneKey: 'tab-1:leaf-1',
|
||||
getPtyId: () => 'pty-1',
|
||||
getSettings: () => null,
|
||||
inspectProcess: vi.fn(),
|
||||
dispatchCompletion,
|
||||
isLive: () => true
|
||||
})
|
||||
const hostCoordinator = createAgentCompletionCoordinator({
|
||||
paneKey: 'tab-1:leaf-1',
|
||||
getPtyId: () => 'pty-1',
|
||||
getSettings: () => null,
|
||||
inspectProcess: vi.fn(),
|
||||
dispatchCompletion,
|
||||
isLive: () => true
|
||||
})
|
||||
|
||||
localCoordinator.observeHookStatus({
|
||||
state: 'working',
|
||||
prompt: 'first turn',
|
||||
agentType: 'claude',
|
||||
stateStartedAt: 5_000
|
||||
})
|
||||
hostCoordinator.observeHookStatus({
|
||||
state: 'working',
|
||||
prompt: 'first turn',
|
||||
agentType: 'claude',
|
||||
stateStartedAt: 2_000
|
||||
})
|
||||
hostCoordinator.observeHookStatus({
|
||||
state: 'working',
|
||||
prompt: 'first turn',
|
||||
agentType: 'claude',
|
||||
stateStartedAt: 2_000,
|
||||
turnCompletedAt: 2_500
|
||||
})
|
||||
expect(dispatchCompletion).toHaveBeenCalledTimes(1)
|
||||
|
||||
hostCoordinator.observeHookStatus({
|
||||
state: 'working',
|
||||
prompt: 'second turn',
|
||||
agentType: 'claude',
|
||||
stateStartedAt: 2_000
|
||||
})
|
||||
localCoordinator.observeHookStatus({
|
||||
state: 'working',
|
||||
prompt: 'second turn',
|
||||
agentType: 'claude',
|
||||
stateStartedAt: 5_000
|
||||
})
|
||||
localCoordinator.observeHookStatus({
|
||||
state: 'done',
|
||||
prompt: 'second turn',
|
||||
agentType: 'claude',
|
||||
stateStartedAt: 5_500
|
||||
})
|
||||
vi.advanceTimersByTime(HOOK_DONE_QUIET_MS)
|
||||
|
||||
expect(dispatchCompletion).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('does not replay a stamped turn or its all-clear after a working title blip', () => {
|
||||
const dispatchCompletion = vi.fn()
|
||||
const dispatchHookLifecycle = vi.fn()
|
||||
|
||||
@@ -44,6 +44,7 @@ const pendingStampedTailByPaneKey = new Map<
|
||||
string,
|
||||
{
|
||||
turnCompletedAt: number
|
||||
originCoordinatorId: number
|
||||
eligibleWorkingBoundaryByCoordinator: Map<number, number>
|
||||
}
|
||||
>()
|
||||
@@ -260,10 +261,24 @@ export function createAgentCompletionCoordinator(
|
||||
) {
|
||||
return false
|
||||
}
|
||||
pendingStampedTailByPaneKey.delete(options.paneKey)
|
||||
pendingStampedTail.eligibleWorkingBoundaryByCoordinator.delete(coordinatorId)
|
||||
if (pendingStampedTail.eligibleWorkingBoundaryByCoordinator.size === 0) {
|
||||
pendingStampedTailByPaneKey.delete(options.paneKey)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function consumeStampedTailForCurrentCoordinator(turnCompletedAt: number): void {
|
||||
const pendingStampedTail = pendingStampedTailByPaneKey.get(options.paneKey)
|
||||
if (pendingStampedTail?.turnCompletedAt !== turnCompletedAt) {
|
||||
return
|
||||
}
|
||||
pendingStampedTail.eligibleWorkingBoundaryByCoordinator.delete(coordinatorId)
|
||||
if (pendingStampedTail.eligibleWorkingBoundaryByCoordinator.size === 0) {
|
||||
pendingStampedTailByPaneKey.delete(options.paneKey)
|
||||
}
|
||||
}
|
||||
|
||||
function hookCompletionAgentIdentity(payload: AgentCompletionStatusSnapshot): string | null {
|
||||
return payload.agentType?.trim().toLowerCase() || null
|
||||
}
|
||||
@@ -933,6 +948,7 @@ export function createAgentCompletionCoordinator(
|
||||
if (!alreadyHandled) {
|
||||
pendingStampedTailByPaneKey.set(options.paneKey, {
|
||||
turnCompletedAt,
|
||||
originCoordinatorId: coordinatorId,
|
||||
eligibleWorkingBoundaryByCoordinator: new Map(
|
||||
workingBoundaryByPaneKey.get(options.paneKey)
|
||||
)
|
||||
@@ -970,6 +986,10 @@ export function createAgentCompletionCoordinator(
|
||||
options.dispatchHookLifecycle?.(payload)
|
||||
return
|
||||
}
|
||||
const pendingStampedTail = pendingStampedTailByPaneKey.get(options.paneKey)
|
||||
if (pendingStampedTail?.originCoordinatorId === coordinatorId) {
|
||||
pendingStampedTailByPaneKey.delete(options.paneKey)
|
||||
}
|
||||
recordWorkingBoundary(payload.stateStartedAt)
|
||||
clearPendingHookDone()
|
||||
// Why: resumed work cancels the debounced attention so a self-resolving pause never notifies.
|
||||
@@ -1011,6 +1031,13 @@ export function createAgentCompletionCoordinator(
|
||||
consumePendingStampedTailForAgent(hookCompletionAgentIdentity(payload))
|
||||
) {
|
||||
// Why: paired host hooks carry the stamp while the sibling OSC coordinator sees only the matching all-clear.
|
||||
lastCompletionIdentity = hookIdentity
|
||||
? {
|
||||
source: 'hook',
|
||||
identity: hookIdentity,
|
||||
agentIdentity: hookCompletionAgentIdentity(payload)
|
||||
}
|
||||
: null
|
||||
options.dispatchHookLifecycle?.(payload)
|
||||
return
|
||||
}
|
||||
@@ -1021,9 +1048,7 @@ export function createAgentCompletionCoordinator(
|
||||
lastCompletedTurn === currentTurn)
|
||||
) {
|
||||
// Why: this `done` is the background all-clear of a turn already announced (or completed from another source). Keep pane lifecycle; do not raise a second banner.
|
||||
if (pendingStampedTailByPaneKey.get(options.paneKey)?.turnCompletedAt === turnCompletedAt) {
|
||||
pendingStampedTailByPaneKey.delete(options.paneKey)
|
||||
}
|
||||
consumeStampedTailForCurrentCoordinator(turnCompletedAt)
|
||||
options.dispatchHookLifecycle?.(payload)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user