| ... | @@ -1739,6 +1739,13 @@ function isClaudeBackgroundTasksChangedMessage(message: SDKMessage): boolean { | ... | @@ -1739,6 +1739,13 @@ function isClaudeBackgroundTasksChangedMessage(message: SDKMessage): boolean { |
| 1739 | ); | 1739 | ); |
| 1740 | } | 1740 | } |
| 1741 | | 1741 | |
| | 1742 | // Claude opens every turn it runs with a root `init` frame. Outside a T3 turn |
| | 1743 | // that turn is a wake, and `init` comes 20-110 ms after the notification that |
| | 1744 | // caused it but seconds before its first output (model thinking time). |
| | 1745 | function isClaudeTurnStartMessage(message: SDKMessage): boolean { |
| | 1746 | return message.type === "system" && message.subtype === "init"; |
| | 1747 | } |
| | 1748 | |
| 1742 | function claudePendingBackgroundTasksFromRoster( | 1749 | function claudePendingBackgroundTasksFromRoster( |
| 1743 | roster: ReadonlyMap<string, OrchestrationV2PendingBackgroundTask>, | 1750 | roster: ReadonlyMap<string, OrchestrationV2PendingBackgroundTask>, |
| 1744 | ): ReadonlyArray<OrchestrationV2PendingBackgroundTask> { | 1751 | ): ReadonlyArray<OrchestrationV2PendingBackgroundTask> { |
| ... | @@ -5095,12 +5102,14 @@ export function makeClaudeAdapterV2( | ... | @@ -5095,12 +5102,14 @@ export function makeClaudeAdapterV2( |
| 5095 | // replay them to the turn that was still starting when they | 5102 | // replay them to the turn that was still starting when they |
| 5096 | // arrived; the offer gate below keeps them from requesting a | 5103 | // arrived; the offer gate below keeps them from requesting a |
| 5097 | // continuation on their own. | 5104 | // continuation on their own. |
| | 5105 | const isWakeTurnStart = isClaudeTurnStartMessage(message); |
| 5098 | const isWakeEvidence = | 5106 | const isWakeEvidence = |
| 5099 | isPendingTaskNotification || | 5107 | isPendingTaskNotification || |
| 5100 | isPendingSubagentNotification || | 5108 | isPendingSubagentNotification || |
| 5101 | isNestedSubagentNotification || | 5109 | isNestedSubagentNotification || |
| 5102 | isKnownSubagentTaskStarted || | 5110 | isKnownSubagentTaskStarted || |
| 5103 | isNewSubagentTaskStarted || | 5111 | isNewSubagentTaskStarted || |
| | 5112 | isWakeTurnStart || |
| 5104 | message.type === "assistant" || | 5113 | message.type === "assistant" || |
| 5105 | message.type === "user" || | 5114 | message.type === "user" || |
| 5106 | message.type === "result" || | 5115 | message.type === "result" || |
| ... | @@ -5157,12 +5166,14 @@ export function makeClaudeAdapterV2( | ... | @@ -5157,12 +5166,14 @@ export function makeClaudeAdapterV2( |
| 5157 | } | 5166 | } |
| 5158 | // A terminal task notification can clear the Waiting roster without | 5167 | // A terminal task notification can clear the Waiting roster without |
| 5159 | // Claude dequeuing it into a native model turn. Buffer it for replay, | 5168 | // Claude dequeuing it into a native model turn. Buffer it for replay, |
| 5160 | // but do not open an opaque-task continuation until native user, | 5169 | // but do not open an opaque-task continuation until the turn's `init`, |
| 5161 | // assistant, or result output proves that Claude actually began the | 5170 | // or native user, assistant, or result output, proves that Claude |
| 5162 | // wake turn. Subagent notifications retain their existing immediate | 5171 | // actually began the wake turn. `init` comes first, so the thread |
| 5163 | // offer because their projected lifecycle owns the continuation. | 5172 | // shows working while Claude thinks instead of looking finished. |
| 5164 | // Only root output proves it: a background subagent keeps streaming | 5173 | // Subagent notifications retain their existing immediate offer |
| 5165 | // its own frames while the root is idle. | 5174 | // because their projected lifecycle owns the continuation. Only |
| | 5175 | // root frames prove it: a background subagent keeps streaming its |
| | 5176 | // own frames while the root is idle. |
| 5166 | const buffered = (yield* Ref.get(wakeBuffers)).get(wakeInput.nativeThreadId); | 5177 | const buffered = (yield* Ref.get(wakeBuffers)).get(wakeInput.nativeThreadId); |
| 5167 | const hasBufferedNotification = | 5178 | const hasBufferedNotification = |
| 5168 | buffered?.messages.some( | 5179 | buffered?.messages.some( |
| ... | @@ -5175,6 +5186,7 @@ export function makeClaudeAdapterV2( | ... | @@ -5175,6 +5186,7 @@ export function makeClaudeAdapterV2( |
| 5175 | if ( | 5186 | if ( |
| 5176 | !isPendingSubagentNotification && | 5187 | !isPendingSubagentNotification && |
| 5177 | !isNativeOpaqueWakeFrame && | 5188 | !isNativeOpaqueWakeFrame && |
| | 5189 | !isWakeTurnStart && |
| 5178 | message.type !== "result" | 5190 | message.type !== "result" |
| 5179 | ) { | 5191 | ) { |
| 5180 | return; | 5192 | return; |
| ... | @@ -7135,8 +7147,13 @@ export function makeClaudeAdapterV2( | ... | @@ -7135,8 +7147,13 @@ export function makeClaudeAdapterV2( |
| 7135 | yield* handleSdkMessage({ query: querySession.query, message: lastResult }); | 7147 | yield* handleSdkMessage({ query: querySession.query, message: lastResult }); |
| 7136 | return; | 7148 | return; |
| 7137 | } | 7149 | } |
| | 7150 | // A drained `init` means Claude began the wake turn, so its output |
| | 7151 | // may still be on the way: stay open for it. |
| 7138 | const hasNativeWakeFrame = drained.some( | 7152 | const hasNativeWakeFrame = drained.some( |
| 7139 | (entry) => entry.type === "user" || entry.type === "assistant", | 7153 | (entry) => |
| | 7154 | entry.type === "user" || |
| | 7155 | entry.type === "assistant" || |
| | 7156 | isClaudeTurnStartMessage(entry), |
| 7140 | ); | 7157 | ); |
| 7141 | if (hasOpaqueTaskNotification && !hasNativeWakeFrame) { | 7158 | if (hasOpaqueTaskNotification && !hasNativeWakeFrame) { |
| 7142 | const completedAt = yield* DateTime.now; | 7159 | const completedAt = yield* DateTime.now; |