mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 01:57:57 +00:00
A descendant's question is stored under its own sourceSessionID but displayed on the root session it rolls up to. clearAttentionForSession matches only the source, so the root's own idle could never retire it: the blocker outlived the turn that raised it and pinned the pane on "needs input" with nothing left to answer. The lead agent truthfully reports no pending question, because the blocker belongs to a child it does not know about (#22371). Add clearAttentionForTurnEnd, matching the source id or the rolled-up root, and use it only from the idle branch. Scoped to turn end deliberately: a live blocker must still outrank the root going Busy, since a subagent can be waiting on the user while the root keeps working — widening the existing clear broke exactly that test, which is what surfaced the right scope. Shared with the OpenCode family, so mimo-code gets the same fix; both pinned plugin digests move. New test fails without the change: after the root turn ends the last post is still AskUserQuestion instead of SessionIdle.
137 lines
7.0 KiB
TypeScript
137 lines
7.0 KiB
TypeScript
export function getStatusPluginLifecycleSource(): string[] {
|
|
return [
|
|
'async function handleLifecycleEvent(client, event, factoryID) {',
|
|
' const sessionID = event.properties?.sessionID;',
|
|
' const statusType = getStatusType(event);',
|
|
' const isResolutionEvent =',
|
|
' event.type === "permission.replied" ||',
|
|
' event.type === "question.replied" ||',
|
|
' event.type === "question.rejected";',
|
|
' if (isResolutionEvent) {',
|
|
' // Why: the stored owner already identifies the root, so replies clear',
|
|
' // immediately even when OpenCode session lookup is slow or unavailable.',
|
|
' const rootSessionID = clearAttentionForResolution(event, sessionID, factoryID);',
|
|
' if (rootSessionID) await publishOwnershipChange(factoryID, rootSessionID);',
|
|
' return;',
|
|
' }',
|
|
' const isAttentionEvent =',
|
|
' event.type === "permission.asked" ||',
|
|
' event.type === "question.asked";',
|
|
" // Why: attention without OpenCode's required sessionID cannot be",
|
|
' // correlated to a later reply/Idle, so it must not become UI authority.',
|
|
' if (isAttentionEvent && !sessionID) return;',
|
|
' const canFailOpen =',
|
|
' statusType === "busy" || statusType === "retry" || isAttentionEvent;',
|
|
' const rootSessionID = sessionID ? await resolveRootSessionID(client, sessionID) : null;',
|
|
' const childState = rootSessionID === null ? null : rootSessionID !== sessionID;',
|
|
' const isIdleEvent = event.type === "session.idle" || statusType === "idle";',
|
|
' const resolvedProvisionalBusy =',
|
|
' childState === false || (childState === true && isIdleEvent)',
|
|
' ? clearProvisionalBusy(sessionID, factoryID)',
|
|
' : false;',
|
|
' if (resolvedProvisionalBusy && childState === false) {',
|
|
' busyRootOwnerBySessionID.delete(sessionID);',
|
|
' busyRootOwnerBySessionID.set(sessionID, factoryID);',
|
|
' }',
|
|
' // Why: child work rolls up to the pane; ignore its normal lifecycle noise,',
|
|
' // but preserve blockers that still require the pane owner to respond.',
|
|
' if (childState === true && !isAttentionEvent) {',
|
|
' let attentionRootSessionID = null;',
|
|
' let childBusyChanged = false;',
|
|
' if (isIdleEvent) {',
|
|
' attentionRootSessionID = clearAttentionForSession(sessionID, factoryID);',
|
|
' childBusyChanged = clearBusyChild(sessionID, factoryID);',
|
|
' } else if (statusType === "busy" || statusType === "retry") {',
|
|
' childBusyChanged = rememberBusyChild(sessionID, rootSessionID, factoryID);',
|
|
' }',
|
|
' if (resolvedProvisionalBusy || attentionRootSessionID || childBusyChanged) {',
|
|
' await publishOwnershipChange(factoryID, attentionRootSessionID || rootSessionID);',
|
|
' }',
|
|
' return;',
|
|
' }',
|
|
' if (childState === null && !canFailOpen) {',
|
|
' if (isIdleEvent) {',
|
|
' // Why: recorded ownership can safely retire a blocker during an SDK',
|
|
' // outage without granting unknown child Idle authority over root state.',
|
|
' const attentionRootSessionID = clearAttentionForSession(sessionID, factoryID);',
|
|
' const clearedProvisionalBusy = clearProvisionalBusy(sessionID, factoryID);',
|
|
' const clearedKnownBusyRoot = clearKnownBusyRoot(sessionID, factoryID);',
|
|
' // Why: exact-session cleanup, so a child whose finishing Idle lands',
|
|
' // during an SDK outage cannot pin the pane Busy forever.',
|
|
' const clearedBusyChild = clearBusyChild(sessionID, factoryID);',
|
|
' if (',
|
|
' attentionRootSessionID ||',
|
|
' clearedProvisionalBusy ||',
|
|
' clearedKnownBusyRoot ||',
|
|
' clearedBusyChild',
|
|
' ) {',
|
|
' await publishOwnershipChange(factoryID, attentionRootSessionID || sessionID);',
|
|
' }',
|
|
' }',
|
|
' return;',
|
|
' }',
|
|
' if (childState === null && (statusType === "busy" || statusType === "retry")) {',
|
|
' // Unknown lineage may be child work, so keep exact provisional ownership',
|
|
' // only until matching Idle/disposal; other blockers still take priority.',
|
|
' clearAttentionForSession(sessionID, factoryID);',
|
|
' rememberProvisionalBusy(sessionID, factoryID);',
|
|
' await publishAggregateStatus(factoryID, sessionID);',
|
|
' return;',
|
|
' }',
|
|
' if (event.type === "permission.asked" || event.type === "question.asked") {',
|
|
' stateArrivalRevision += 1;',
|
|
' // Why: attention must share the lifecycle FIFO and retry target so a',
|
|
' // delayed Busy post cannot overwrite a newer human blocker.',
|
|
' const hookEventName =',
|
|
' event.type === "permission.asked" ? "PermissionRequest" : "AskUserQuestion";',
|
|
' // Why: show the blocker on the root turn while retaining its real child',
|
|
' // owner for exact reply, tool-completion, and disposal cleanup.',
|
|
' const properties = { ...(event.properties || {}), sessionID: rootSessionID || sessionID };',
|
|
' const requestID = properties.id || sessionID || "";',
|
|
' const key = attentionKey(factoryID, hookEventName, requestID, sessionID);',
|
|
' // Why: unresolved blockers are live UI authority and cannot be evicted;',
|
|
' // reply, exact Idle, tool completion, or factory disposal retires them.',
|
|
' pendingAttentionByKey.set(key, {',
|
|
' hookEventName,',
|
|
' properties,',
|
|
' factoryID,',
|
|
' sourceSessionID: sessionID,',
|
|
' });',
|
|
' await publishAggregateStatus(factoryID, rootSessionID || sessionID);',
|
|
' return;',
|
|
' }',
|
|
' if (isIdleEvent) {',
|
|
' stateArrivalRevision += 1;',
|
|
' const idleKey = "idle:" + (sessionID || "");',
|
|
' // Why: current OpenCode emits canonical idle followed by deprecated',
|
|
' // session.idle; a failed canonical post should keep its backoff.',
|
|
' if (',
|
|
' event.type === "session.idle" &&',
|
|
' desiredStatusKey === idleKey &&',
|
|
' statusDeliveryDirty &&',
|
|
' statusRetryTimer',
|
|
' ) return;',
|
|
' // Why: flush the coalesced final reply snapshot before the idle',
|
|
' // transition so the done-state preview shows the completed message.',
|
|
' await flushPendingAssistantPart(true);',
|
|
' clearAttentionForTurnEnd(sessionID, factoryID);',
|
|
' if (busyRootOwnerBySessionID.get(sessionID) === factoryID) {',
|
|
' busyRootOwnerBySessionID.delete(sessionID);',
|
|
' }',
|
|
' await publishAggregateStatus(factoryID, sessionID);',
|
|
' return;',
|
|
' }',
|
|
' // Why: recoverable compaction failures emit session.error and continue;',
|
|
' // canonical session.status is the authority for actual completion.',
|
|
' if (event.type === "session.error") return;',
|
|
' if (statusType === "busy" || statusType === "retry") {',
|
|
' clearAttentionForSession(sessionID, factoryID);',
|
|
' busyRootOwnerBySessionID.delete(sessionID);',
|
|
' busyRootOwnerBySessionID.set(sessionID, factoryID);',
|
|
' await publishAggregateStatus(factoryID, sessionID);',
|
|
' }',
|
|
'}',
|
|
''
|
|
]
|
|
}
|