Repository navigation
fix(server): keep pending questions open for goal steering - #173
Conversation
From v0.1.3 a follow-up steers the running turn by default (pingdotgg#24718). A steer into a turn that waits on a pending runtime request aborts the provider's pending callback: Claude steers with priority "now", a pending AskUserQuestion resolves as cancelled and an approval as cancel, and the person never sees the question (#139). #119 guarded only sendToThread's auto mode; the composer's explicit steer, the auto branch of the delivery intent and promoting a queued message still steered through it. The guard now lives in one place, CommandPolicy.resolveMessageDispatchIntent: while the thread has a pending runtime request, any steer_active becomes queue_after_active. It covers a steer requested as a delivery intent (auto or steer) and one requested as a dispatch mode (sendToThread, older clients). The orchestrator reads the thread with its runtime requests on every message.dispatch, so no caller has to load them. Promoting a queued message to a steer checks the same predicate (waitsOnPendingRequest) and puts the message first in the queue instead, so it runs right after the answer. Restart is unchanged. sendToThread's own check from #119 is gone, and ThreadManagementService.ts is upstream's again; its test resolves the dispatched mode through the guard, and an explicit steer with a pending request now queues too. The allowlist trades ThreadManagementService.ts for CommandPolicy.ts, Orchestrator.ts and their tests, and the user guides for the composer and the agent CLI say a steer queues while a question waits. On v0.1.4: upstream renamed stopRunForGoalCommand to stopRunForQueuedCommand with a required reason (pingdotgg#25837); the guard passes 'Queued behind a pending request' and stays after the compaction and /goal branches. pingdotgg#25846 renamed the queued message action to Send now, and composer.md names it. A delegated task's completion wake, which v0.1.4's remote delegation can also produce (RemoteDelegation.ts), set steer_active after the guard, as it did on the v0.1.3 line; it now steers only while waitsOnPendingRequest is false, and SteeringCompletion.integration.test.ts queues such a wake behind a pending request. On v0.1.5 pingdotgg#26715 queues a steer for a provider that can neither steer its running turn nor restart it. The overlay's pending-request check runs before that branch and still holds; the row's ThreadManagementService test gives its projection empty providerThreads and providerSessions, which the new branch reads. On v0.1.6 upstream still steers into a turn that waits on a pending runtime request, and the guard still reads the thread's runtimeRequests on every message.dispatch. Claude's MCP elicitations now wait on the approval card (pingdotgg#21903) as pending mcp-elicitation runtime requests (ClaudeAdapterV2.ts), and a steer would cancel them like any other request, so CommandPolicy.test.ts covers that kind as well. Upstream's Force stop cases (pingdotgg#27252) and this row's sendToThread cases are two separate it.effect.each blocks in ThreadManagementService.test.ts. On v0.1.7: upstream lets a delegated task's question steer a busy parent turn as well (pingdotgg#29909), on the same path as a completion wake. A steer into a turn that waits on the person would cancel that person's question, so the guard now covers both: `waitsOnPendingRequest` comes before the completion-wake and task-question conditions. The pending-request test also sends a task question and checks that it stays queued; without the guard it steers. Also on v0.1.7, a follow-up sent before the run's provider turn started waited for a turn of its own (#170). resolveDeliveryIntent queues automatic input while the active run is preparing or starting, or has no running provider turn of its attempt, and upstream's web composer turns a send made while the run is preparing or starting into Queue (planComposerTurn in ChatView.logic.ts). A cold start, such as Buddy's agent over ACP, leaves that gap open for seconds, so the message ran after the turn instead of steering it. Such a queued run now notes the run it was sent during (steersRunId, optional in orchestrationV2.ts, from runAwaitingProviderTurn in CommandPolicy.ts). When that run's provider turn reports running, the orchestrator promotes those runs to a steer under the thread lock, only if upstream's resolveMessageDispatchIntent would steer auto input into that run now (pending request, maintenance and compaction targets, steering capability) and stopReachedRun allows it; held runs, /goal commands, notifications and delegation wakes are left alone. A provider that cannot steer, or a turn that never starts, keeps upstream's queue. SteeringCompletion.integration.test.ts sends a follow-up before the turn starts and expects it steered in; without the reaction it waits in the queue. composer.md follows. A running provider-turn update repeats with every usage tick. The first running update of each provider turn takes the thread lock (makeFirstRunningUpdate in CommandPolicy.ts), because a follow-up committed alongside it is visible only under the lock; a later one first reads the thread's queued runs without the lock and takes it only while a follow-up waits for its turn. Under the lock the reaction reads the queued runs again and reads the whole projection only while a follow-up waits. CommandPolicy.test.ts covers runAwaitingProviderTurn and makeFirstRunningUpdate, and the contract test pins the lock's order; no test drives the race itself, because nothing between message.dispatch's read and its commit can be held from a test. The web composer now keeps auto for a send made while the run is set up, which the server steers in; planComposerTurn still turns the other choices and a send that compacts first into Queue, and ChatView.logic.test.ts fails without the change. A send the person makes as Queue (Queue as the default action, or the alternate key over a steer default) stays queued. resolveComposerDispatchMode takes turnStarting, which ChatComposer.tsx and composerCommandKey.ts set while the phase is connecting, and composerDispatch.test.ts fails without it. Row 5's allowlist names ChatView.logic.ts and its test, composerCommandKey.ts, composerDispatch.ts and its test, and orchestrationV2.ts. The official mobile app is upstream's and still sends queue while a turn starts. Each queued follow-up gets one promotion, under its own command id (command:steer-awaiting-turn:<runId>). A refused promotion leaves its message queued, logs a warning and does not hold back the next follow-up, and a later usage tick does not retry it. Two integration tests cover this: a /compact sent during the start no longer blocks the follow-up after it, and a refused promotion is tried once and the next follow-up still steers in. A /goal the person sends into a turn that waits on a question still interrupts that turn, as on main and upstream; #173 carries the fix to upstream's pull request. A follow-up sent to Buddy during a cold start now steers, so row 33 gives the steer Buddy's page as the Czech block (#150). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From v0.1.3 a follow-up steers the running turn by default (pingdotgg#24718). A steer into a turn that waits on a pending runtime request aborts the provider's pending callback: Claude steers with priority "now", a pending AskUserQuestion resolves as cancelled and an approval as cancel, and the person never sees the question (#139). #119 guarded only sendToThread's auto mode; the composer's explicit steer, the auto branch of the delivery intent and promoting a queued message still steered through it. The guard now lives in one place, CommandPolicy.resolveMessageDispatchIntent: while the thread has a pending runtime request, any steer_active becomes queue_after_active. It covers a steer requested as a delivery intent (auto or steer) and one requested as a dispatch mode (sendToThread, older clients). The orchestrator reads the thread with its runtime requests on every message.dispatch, so no caller has to load them. Promoting a queued message to a steer checks the same predicate (waitsOnPendingRequest) and puts the message first in the queue instead, so it runs right after the answer. Restart is unchanged. sendToThread's own check from #119 is gone, and ThreadManagementService.ts is upstream's again; its test resolves the dispatched mode through the guard, and an explicit steer with a pending request now queues too. The allowlist trades ThreadManagementService.ts for CommandPolicy.ts, Orchestrator.ts and their tests, and the user guides for the composer and the agent CLI say a steer queues while a question waits. On v0.1.4: upstream renamed stopRunForGoalCommand to stopRunForQueuedCommand with a required reason (pingdotgg#25837); the guard passes 'Queued behind a pending request' and stays after the compaction and /goal branches. pingdotgg#25846 renamed the queued message action to Send now, and composer.md names it. A delegated task's completion wake, which v0.1.4's remote delegation can also produce (RemoteDelegation.ts), set steer_active after the guard, as it did on the v0.1.3 line; it now steers only while waitsOnPendingRequest is false, and SteeringCompletion.integration.test.ts queues such a wake behind a pending request. On v0.1.5 pingdotgg#26715 queues a steer for a provider that can neither steer its running turn nor restart it. The overlay's pending-request check runs before that branch and still holds; the row's ThreadManagementService test gives its projection empty providerThreads and providerSessions, which the new branch reads. On v0.1.6 upstream still steers into a turn that waits on a pending runtime request, and the guard still reads the thread's runtimeRequests on every message.dispatch. Claude's MCP elicitations now wait on the approval card (pingdotgg#21903) as pending mcp-elicitation runtime requests (ClaudeAdapterV2.ts), and a steer would cancel them like any other request, so CommandPolicy.test.ts covers that kind as well. Upstream's Force stop cases (pingdotgg#27252) and this row's sendToThread cases are two separate it.effect.each blocks in ThreadManagementService.test.ts. On v0.1.7: upstream lets a delegated task's question steer a busy parent turn as well (pingdotgg#29909), on the same path as a completion wake. A steer into a turn that waits on the person would cancel that person's question, so the guard now covers both: `waitsOnPendingRequest` comes before the completion-wake and task-question conditions. The pending-request test also sends a task question and checks that it stays queued; without the guard it steers. Also on v0.1.7, a follow-up sent before the run's provider turn started waited for a turn of its own (#170). resolveDeliveryIntent queues automatic input while the active run is preparing or starting, or has no running provider turn of its attempt, and upstream's web composer turns a send made while the run is preparing or starting into Queue (planComposerTurn in ChatView.logic.ts). A cold start, such as Buddy's agent over ACP, leaves that gap open for seconds, so the message ran after the turn instead of steering it. Such a queued run now notes the run it was sent during (steersRunId, optional in orchestrationV2.ts, from runAwaitingProviderTurn in CommandPolicy.ts). When that run's provider turn reports running, the orchestrator promotes those runs to a steer under the thread lock, only if upstream's resolveMessageDispatchIntent would steer auto input into that run now (pending request, maintenance and compaction targets, steering capability) and stopReachedRun allows it; held runs, /goal commands, notifications and delegation wakes are left alone. A provider that cannot steer, or a turn that never starts, keeps upstream's queue. SteeringCompletion.integration.test.ts sends a follow-up before the turn starts and expects it steered in; without the reaction it waits in the queue. composer.md follows. A running provider-turn update repeats with every usage tick. The first running update of each provider turn takes the thread lock (makeFirstRunningUpdate in CommandPolicy.ts), because a follow-up committed alongside it is visible only under the lock; a later one first reads the thread's queued runs without the lock and takes it only while a follow-up waits for its turn. Under the lock the reaction reads the queued runs again and reads the whole projection only while a follow-up waits. CommandPolicy.test.ts covers runAwaitingProviderTurn and makeFirstRunningUpdate, and the contract test pins the lock's order; no test drives the race itself, because nothing between message.dispatch's read and its commit can be held from a test. The web composer now keeps auto for a send made while the run is set up, which the server steers in; planComposerTurn still turns the other choices and a send that compacts first into Queue, and ChatView.logic.test.ts fails without the change. A send the person makes as Queue (Queue as the default action, or the alternate key over a steer default) stays queued. resolveComposerDispatchMode takes turnStarting, which ChatComposer.tsx and composerCommandKey.ts set while the phase is connecting, and composerDispatch.test.ts fails without it. Row 5's allowlist names ChatView.logic.ts and its test, composerCommandKey.ts, composerDispatch.ts and its test, and orchestrationV2.ts. The official mobile app is upstream's and still sends queue while a turn starts. Each queued follow-up gets one promotion, under its own command id (command:steer-awaiting-turn:<runId>). A refused promotion leaves its message queued, logs a warning and does not hold back the next follow-up, and a later usage tick does not retry it. Two integration tests cover this: a /compact sent during the start no longer blocks the follow-up after it, and a refused promotion is tried once and the next follow-up still steers in. A /goal the person sends into a turn that waits on a question still interrupts that turn, as on main and upstream; #173 carries the fix to upstream's pull request. A follow-up sent to Buddy during a cold start now steers, so row 33 gives the steer Buddy's page as the Czech block (#150). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From v0.1.3 a follow-up steers the running turn by default (pingdotgg#24718). A steer into a turn that waits on a pending runtime request aborts the provider's pending callback: Claude steers with priority "now", a pending AskUserQuestion resolves as cancelled and an approval as cancel, and the person never sees the question (#139). #119 guarded only sendToThread's auto mode; the composer's explicit steer, the auto branch of the delivery intent and promoting a queued message still steered through it. The guard now lives in one place, CommandPolicy.resolveMessageDispatchIntent: while the thread has a pending runtime request, any steer_active becomes queue_after_active. It covers a steer requested as a delivery intent (auto or steer) and one requested as a dispatch mode (sendToThread, older clients). The orchestrator reads the thread with its runtime requests on every message.dispatch, so no caller has to load them. Promoting a queued message to a steer checks the same predicate (waitsOnPendingRequest) and puts the message first in the queue instead, so it runs right after the answer. Restart is unchanged. sendToThread's own check from #119 is gone, and ThreadManagementService.ts is upstream's again; its test resolves the dispatched mode through the guard, and an explicit steer with a pending request now queues too. The allowlist trades ThreadManagementService.ts for CommandPolicy.ts, Orchestrator.ts and their tests, and the user guides for the composer and the agent CLI say a steer queues while a question waits. On v0.1.4: upstream renamed stopRunForGoalCommand to stopRunForQueuedCommand with a required reason (pingdotgg#25837); the guard passes 'Queued behind a pending request' and stays after the compaction and /goal branches. pingdotgg#25846 renamed the queued message action to Send now, and composer.md names it. A delegated task's completion wake, which v0.1.4's remote delegation can also produce (RemoteDelegation.ts), set steer_active after the guard, as it did on the v0.1.3 line; it now steers only while waitsOnPendingRequest is false, and SteeringCompletion.integration.test.ts queues such a wake behind a pending request. On v0.1.5 pingdotgg#26715 queues a steer for a provider that can neither steer its running turn nor restart it. The overlay's pending-request check runs before that branch and still holds; the row's ThreadManagementService test gives its projection empty providerThreads and providerSessions, which the new branch reads. On v0.1.6 upstream still steers into a turn that waits on a pending runtime request, and the guard still reads the thread's runtimeRequests on every message.dispatch. Claude's MCP elicitations now wait on the approval card (pingdotgg#21903) as pending mcp-elicitation runtime requests (ClaudeAdapterV2.ts), and a steer would cancel them like any other request, so CommandPolicy.test.ts covers that kind as well. Upstream's Force stop cases (pingdotgg#27252) and this row's sendToThread cases are two separate it.effect.each blocks in ThreadManagementService.test.ts. On v0.1.7: upstream lets a delegated task's question steer a busy parent turn as well (pingdotgg#29909), on the same path as a completion wake. A steer into a turn that waits on the person would cancel that person's question, so the guard now covers both: `waitsOnPendingRequest` comes before the completion-wake and task-question conditions. The pending-request test also sends a task question and checks that it stays queued; without the guard it steers. Also on v0.1.7, a follow-up sent before the run's provider turn started waited for a turn of its own (#170). resolveDeliveryIntent queues automatic input while the active run is preparing or starting, or has no running provider turn of its attempt, and upstream's web composer turns a send made while the run is preparing or starting into Queue (planComposerTurn in ChatView.logic.ts). A cold start, such as Buddy's agent over ACP, leaves that gap open for seconds, so the message ran after the turn instead of steering it. Such a queued run now notes the run it was sent during (steersRunId, optional in orchestrationV2.ts, from runAwaitingProviderTurn in CommandPolicy.ts). When that run's provider turn reports running, the orchestrator promotes those runs to a steer under the thread lock, only if upstream's resolveMessageDispatchIntent would steer auto input into that run now (pending request, maintenance and compaction targets, steering capability) and stopReachedRun allows it; held runs, /goal commands, notifications and delegation wakes are left alone. A provider that cannot steer, or a turn that never starts, keeps upstream's queue. SteeringCompletion.integration.test.ts sends a follow-up before the turn starts and expects it steered in; without the reaction it waits in the queue. composer.md follows. A running provider-turn update repeats with every usage tick. The first running update of each provider turn takes the thread lock (makeFirstRunningUpdate in CommandPolicy.ts), because a follow-up committed alongside it is visible only under the lock; a later one first reads the thread's queued runs without the lock and takes it only while a follow-up waits for its turn. Under the lock the reaction reads the queued runs again and reads the whole projection only while a follow-up waits. CommandPolicy.test.ts covers runAwaitingProviderTurn and makeFirstRunningUpdate, and the contract test pins the lock's order; no test drives the race itself, because nothing between message.dispatch's read and its commit can be held from a test. The web composer now keeps auto for a send made while the run is set up, which the server steers in; planComposerTurn still turns the other choices and a send that compacts first into Queue, and ChatView.logic.test.ts fails without the change. A send the person makes as Queue (Queue as the default action, or the alternate key over a steer default) stays queued. resolveComposerDispatchMode takes turnStarting, which ChatComposer.tsx and composerCommandKey.ts set while the phase is connecting, and composerDispatch.test.ts fails without it. Row 5's allowlist names ChatView.logic.ts and its test, composerCommandKey.ts, composerDispatch.ts and its test, and orchestrationV2.ts. The official mobile app is upstream's and still sends queue while a turn starts. Each queued follow-up gets one promotion, under its own command id (command:steer-awaiting-turn:<runId>). A refused promotion leaves its message queued, logs a warning and does not hold back the next follow-up, and a later usage tick does not retry it. Two integration tests cover this: a /compact sent during the start no longer blocks the follow-up after it, and a refused promotion is tried once and the next follow-up still steers in. A /goal the person sends into a turn that waits on a question still interrupts that turn, as on main and upstream; #173 carries the fix to upstream's pull request. A follow-up sent to Buddy during a cold start now steers, so row 33 gives the steer Buddy's page as the Czech block (#150). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From v0.1.3 a follow-up steers the running turn by default (pingdotgg#24718). A steer into a turn that waits on a pending runtime request aborts the provider's pending callback: Claude steers with priority "now", a pending AskUserQuestion resolves as cancelled and an approval as cancel, and the person never sees the question (#139). #119 guarded only sendToThread's auto mode; the composer's explicit steer, the auto branch of the delivery intent and promoting a queued message still steered through it. The guard now lives in one place, CommandPolicy.resolveMessageDispatchIntent: while the thread has a pending runtime request, any steer_active becomes queue_after_active. It covers a steer requested as a delivery intent (auto or steer) and one requested as a dispatch mode (sendToThread, older clients). The orchestrator reads the thread with its runtime requests on every message.dispatch, so no caller has to load them. Promoting a queued message to a steer checks the same predicate (waitsOnPendingRequest) and puts the message first in the queue instead, so it runs right after the answer. Restart is unchanged. sendToThread's own check from #119 is gone, and ThreadManagementService.ts is upstream's again; its test resolves the dispatched mode through the guard, and an explicit steer with a pending request now queues too. The allowlist trades ThreadManagementService.ts for CommandPolicy.ts, Orchestrator.ts and their tests, and the user guides for the composer and the agent CLI say a steer queues while a question waits. On v0.1.4: upstream renamed stopRunForGoalCommand to stopRunForQueuedCommand with a required reason (pingdotgg#25837); the guard passes 'Queued behind a pending request' and stays after the compaction and /goal branches. pingdotgg#25846 renamed the queued message action to Send now, and composer.md names it. A delegated task's completion wake, which v0.1.4's remote delegation can also produce (RemoteDelegation.ts), set steer_active after the guard, as it did on the v0.1.3 line; it now steers only while waitsOnPendingRequest is false, and SteeringCompletion.integration.test.ts queues such a wake behind a pending request. On v0.1.5 pingdotgg#26715 queues a steer for a provider that can neither steer its running turn nor restart it. The overlay's pending-request check runs before that branch and still holds; the row's ThreadManagementService test gives its projection empty providerThreads and providerSessions, which the new branch reads. On v0.1.6 upstream still steers into a turn that waits on a pending runtime request, and the guard still reads the thread's runtimeRequests on every message.dispatch. Claude's MCP elicitations now wait on the approval card (pingdotgg#21903) as pending mcp-elicitation runtime requests (ClaudeAdapterV2.ts), and a steer would cancel them like any other request, so CommandPolicy.test.ts covers that kind as well. Upstream's Force stop cases (pingdotgg#27252) and this row's sendToThread cases are two separate it.effect.each blocks in ThreadManagementService.test.ts. On v0.1.7: upstream lets a delegated task's question steer a busy parent turn as well (pingdotgg#29909), on the same path as a completion wake. A steer into a turn that waits on the person would cancel that person's question, so the guard now covers both: `waitsOnPendingRequest` comes before the completion-wake and task-question conditions. The pending-request test also sends a task question and checks that it stays queued; without the guard it steers. Also on v0.1.7, a follow-up sent before the run's provider turn started waited for a turn of its own (#170). resolveDeliveryIntent queues automatic input while the active run is preparing or starting, or has no running provider turn of its attempt, and upstream's web composer turns a send made while the run is preparing or starting into Queue (planComposerTurn in ChatView.logic.ts). A cold start, such as Buddy's agent over ACP, leaves that gap open for seconds, so the message ran after the turn instead of steering it. Such a queued run now notes the run it was sent during (steersRunId, optional in orchestrationV2.ts, from runAwaitingProviderTurn in CommandPolicy.ts). When that run's provider turn reports running, the orchestrator promotes those runs to a steer under the thread lock, only if upstream's resolveMessageDispatchIntent would steer auto input into that run now (pending request, maintenance and compaction targets, steering capability) and stopReachedRun allows it; held runs, /goal commands, notifications and delegation wakes are left alone. A provider that cannot steer, or a turn that never starts, keeps upstream's queue. SteeringCompletion.integration.test.ts sends a follow-up before the turn starts and expects it steered in; without the reaction it waits in the queue. composer.md follows. A running provider-turn update repeats with every usage tick. The first running update of each provider turn takes the thread lock (makeFirstRunningUpdate in CommandPolicy.ts), because a follow-up committed alongside it is visible only under the lock; a later one first reads the thread's queued runs without the lock and takes it only while a follow-up waits for its turn. Under the lock the reaction reads the queued runs again and reads the whole projection only while a follow-up waits. CommandPolicy.test.ts covers runAwaitingProviderTurn and makeFirstRunningUpdate, and the contract test pins the lock's order; no test drives the race itself, because nothing between message.dispatch's read and its commit can be held from a test. The web composer now keeps auto for a send made while the run is set up, which the server steers in; planComposerTurn still turns the other choices and a send that compacts first into Queue, and ChatView.logic.test.ts fails without the change. A send the person makes as Queue (Queue as the default action, or the alternate key over a steer default) stays queued. resolveComposerDispatchMode takes turnStarting, which ChatComposer.tsx and composerCommandKey.ts set while the phase is connecting, and composerDispatch.test.ts fails without it. Row 5's allowlist names ChatView.logic.ts and its test, composerCommandKey.ts, composerDispatch.ts and its test, and orchestrationV2.ts. The official mobile app is upstream's and still sends queue while a turn starts. Each queued follow-up gets one promotion, under its own command id (command:steer-awaiting-turn:<runId>). A refused promotion leaves its message queued, logs a warning and does not hold back the next follow-up, and a later usage tick does not retry it. Two integration tests cover this: a /compact sent during the start no longer blocks the follow-up after it, and a refused promotion is tried once and the next follow-up still steers in. A /goal the person sends into a turn that waits on a question still interrupts that turn, as on main and upstream; #173 carries the fix to upstream's pull request. A follow-up sent to Buddy during a cold start now steers, so row 33 gives the steer Buddy's page as the Czech block (#150). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From v0.1.3 a follow-up steers the running turn by default (pingdotgg#24718). A steer into a turn that waits on a pending runtime request aborts the provider's pending callback: Claude steers with priority "now", a pending AskUserQuestion resolves as cancelled and an approval as cancel, and the person never sees the question (#139). #119 guarded only sendToThread's auto mode; the composer's explicit steer, the auto branch of the delivery intent and promoting a queued message still steered through it. The guard now lives in one place, CommandPolicy.resolveMessageDispatchIntent: while the thread has a pending runtime request, any steer_active becomes queue_after_active. It covers a steer requested as a delivery intent (auto or steer) and one requested as a dispatch mode (sendToThread, older clients). The orchestrator reads the thread with its runtime requests on every message.dispatch, so no caller has to load them. Promoting a queued message to a steer checks the same predicate (waitsOnPendingRequest) and puts the message first in the queue instead, so it runs right after the answer. Restart is unchanged. sendToThread's own check from #119 is gone, and ThreadManagementService.ts is upstream's again; its test resolves the dispatched mode through the guard, and an explicit steer with a pending request now queues too. The allowlist trades ThreadManagementService.ts for CommandPolicy.ts, Orchestrator.ts and their tests, and the user guides for the composer and the agent CLI say a steer queues while a question waits. On v0.1.4: upstream renamed stopRunForGoalCommand to stopRunForQueuedCommand with a required reason (pingdotgg#25837); the guard passes 'Queued behind a pending request' and stays after the compaction and /goal branches. pingdotgg#25846 renamed the queued message action to Send now, and composer.md names it. A delegated task's completion wake, which v0.1.4's remote delegation can also produce (RemoteDelegation.ts), set steer_active after the guard, as it did on the v0.1.3 line; it now steers only while waitsOnPendingRequest is false, and SteeringCompletion.integration.test.ts queues such a wake behind a pending request. On v0.1.5 pingdotgg#26715 queues a steer for a provider that can neither steer its running turn nor restart it. The overlay's pending-request check runs before that branch and still holds; the row's ThreadManagementService test gives its projection empty providerThreads and providerSessions, which the new branch reads. On v0.1.6 upstream still steers into a turn that waits on a pending runtime request, and the guard still reads the thread's runtimeRequests on every message.dispatch. Claude's MCP elicitations now wait on the approval card (pingdotgg#21903) as pending mcp-elicitation runtime requests (ClaudeAdapterV2.ts), and a steer would cancel them like any other request, so CommandPolicy.test.ts covers that kind as well. Upstream's Force stop cases (pingdotgg#27252) and this row's sendToThread cases are two separate it.effect.each blocks in ThreadManagementService.test.ts. On v0.1.7: upstream lets a delegated task's question steer a busy parent turn as well (pingdotgg#29909), on the same path as a completion wake. A steer into a turn that waits on the person would cancel that person's question, so the guard now covers both: `waitsOnPendingRequest` comes before the completion-wake and task-question conditions. The pending-request test also sends a task question and checks that it stays queued; without the guard it steers. Also on v0.1.7, a follow-up sent before the run's provider turn started waited for a turn of its own (#170). resolveDeliveryIntent queues automatic input while the active run is preparing or starting, or has no running provider turn of its attempt, and upstream's web composer turns a send made while the run is preparing or starting into Queue (planComposerTurn in ChatView.logic.ts). A cold start, such as Buddy's agent over ACP, leaves that gap open for seconds, so the message ran after the turn instead of steering it. Such a queued run now notes the run it was sent during (steersRunId, optional in orchestrationV2.ts, from runAwaitingProviderTurn in CommandPolicy.ts). When that run's provider turn reports running, the orchestrator promotes those runs to a steer under the thread lock, only if upstream's resolveMessageDispatchIntent would steer auto input into that run now (pending request, maintenance and compaction targets, steering capability) and stopReachedRun allows it; held runs, /goal commands, notifications and delegation wakes are left alone. A provider that cannot steer, or a turn that never starts, keeps upstream's queue. SteeringCompletion.integration.test.ts sends a follow-up before the turn starts and expects it steered in; without the reaction it waits in the queue. composer.md follows. A running provider-turn update repeats with every usage tick. The first running update of each provider turn takes the thread lock (makeFirstRunningUpdate in CommandPolicy.ts), because a follow-up committed alongside it is visible only under the lock; a later one first reads the thread's queued runs without the lock and takes it only while a follow-up waits for its turn. Under the lock the reaction reads the queued runs again and reads the whole projection only while a follow-up waits. CommandPolicy.test.ts covers runAwaitingProviderTurn and makeFirstRunningUpdate, and the contract test pins the lock's order; no test drives the race itself, because nothing between message.dispatch's read and its commit can be held from a test. The web composer now keeps auto for a send made while the run is set up, which the server steers in; planComposerTurn still turns the other choices and a send that compacts first into Queue, and ChatView.logic.test.ts fails without the change. A send the person makes as Queue (Queue as the default action, or the alternate key over a steer default) stays queued. resolveComposerDispatchMode takes turnStarting, which ChatComposer.tsx and composerCommandKey.ts set while the phase is connecting, and composerDispatch.test.ts fails without it. Row 5's allowlist names ChatView.logic.ts and its test, composerCommandKey.ts, composerDispatch.ts and its test, and orchestrationV2.ts. The official mobile app is upstream's and still sends queue while a turn starts. Each queued follow-up gets one promotion, under its own command id (command:steer-awaiting-turn:<runId>). A refused promotion leaves its message queued, logs a warning and does not hold back the next follow-up, and a later usage tick does not retry it. Two integration tests cover this: a /compact sent during the start no longer blocks the follow-up after it, and a refused promotion is tried once and the next follow-up still steers in. A /goal the person sends into a turn that waits on a question still interrupts that turn, as on main and upstream; #173 carries the fix to upstream's pull request. A follow-up sent to Buddy during a cold start now steers, so row 33 gives the steer Buddy's page as the Czech block (#150). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
agentrozjedemeai
left a comment
There was a problem hiding this comment.
Approved. The pending-request guards cover goal steering and promotion while preserving explicit restart. vp run check:changed 40104be17baa53bc8b8047897264b3e7ec14eafb passed, including 39 tests; full suites and build remain for CI.
— Pablo
agentrozjedemeai
left a comment
There was a problem hiding this comment.
Approved. No blocking findings.
Verified the pending-request guards and explicit restart against the upstream finding. The focused tests passed (38); check:changed passed formatting, lint, both knip checks, GraphQL, typecheck and 39 tests. Full suites and build remain for CI.
— Pablo
|
Thank you, and sorry for the wait. I'm merging this into the branch of pingdotgg#26842, so the upstream PR guards both Agent: environment a480495c-25e0-4e51-828b-1dbf1b77d62f · T3 thread f1b261fa-fde5-4bdf-8df3-6c4d6fca7060 |
163789a
into
Lazurio:upstream/steer-waits-on-pending-request
Problem
This supplements pingdotgg#26842 on its existing author branch. The ordinary pending-question guard works, but a
/goalsent with steering intent or promoted from the queue still reaches a separate stop decision and cancels the question the person is answering. This independently reproduces the existing review finding on40104be17baa53bc8b8047897264b3e7ec14eafb.Change
Guard both
/goalstop decisions while a runtime request is pending. An explicitrestart_activestill interrupts intentionally. Extend the existing pending-question integration case with goal steering, goal promotion and the explicit-restart control; preserve the pending request and queue order.This PR targets
upstream/steer-waits-on-pending-requestin the author's fork so it can be merged directly into the existing upstream PR. Its diff is only two files relative to that branch. It addresses the same pending-input cancellation defect and does not change the installed application.Verification
CommandPolicy.test.tsandSteeringCompletion.integration.test.tsrun passes 38 tests, including intentional restart.vp run check:changed origin/pr-26842: format, lint, both knip passes, GraphQL check, typecheck of the three affected packages, and 39 tests in three files pass. The unused-patch check is skipped because no dependency files changed; full suites and build remain for CI.ee0548d3fdbaf9fee088da5b0129cdeebcc5b072: isolated macOS arm64 source server, Node 26.12.1, locked Claude Agent SDK 0.3.276, native Claude Haiku 4.5. One real Claude thread asks a native question; a second sends/goal Return exactly TRIAGE_GOAL_DONE once and finish immediately.throught3_thread_send,mode: auto.UTC, 2026-11-04:
The isolated server was stopped before answering that goal test, so native goal execution was not started. Separately, on the unchanged upstream PR head, a real normal agent update stayed queued, the synthetic answer Blue reached the original Claude turn, and the queued update ran afterwards. That evidence is in the upstream review.
Not checked: live goal promotion, live explicit restart, mobile UI, remote transport or crash recovery. Goal promotion and restart have deterministic integration coverage. No modified build was installed and no live user thread or database was changed.
Prepared by Codex (GPT-6). Claude Haiku 4.5 was used only as the real test receiver and sender.


Confidence Score: 5/5
The PR appears safe to merge; goal steering and promotion preserve pending questions without disabling intentional restart.Summary
Keeps pending questions open when
/goalis sent as steering input or promoted from the queue.Diagram
%%{init: {'theme': 'neutral'}}%% flowchart TD A["Goal steering or promotion"] --> B{"Request pending?"} B -->|No| C["Existing goal stop rules"] B -->|Yes| D{"Explicit restart?"} D -->|Yes| C D -->|No| E["Keep question open"] E --> F["Queue goal; promotion moves it first"]Reviews (1) · Last reviewed commit: "fix(server): keep pending questions open..." · Reviewed by Greptile