diff --git a/apps/backend/internal/agent/runtime/lifecycle/manager_interaction.go b/apps/backend/internal/agent/runtime/lifecycle/manager_interaction.go index 5df9d126fb..8b963a9c89 100644 --- a/apps/backend/internal/agent/runtime/lifecycle/manager_interaction.go +++ b/apps/backend/internal/agent/runtime/lifecycle/manager_interaction.go @@ -810,6 +810,7 @@ func (m *Manager) ResetAgentContext(ctx context.Context, executionID string) err case <-exec.promptDoneCh: default: } + exec.dispatchedPromptPending.Store(false) }) // Restore the complete captured configuration. A strict-mode model that is @@ -1151,6 +1152,7 @@ func (m *Manager) resetAgentRestartState(executionID string, commands agentComma case <-exec.promptDoneCh: default: } + exec.dispatchedPromptPending.Store(false) }) } diff --git a/apps/backend/internal/agent/runtime/lifecycle/manager_interaction_test.go b/apps/backend/internal/agent/runtime/lifecycle/manager_interaction_test.go index 0e42d9ac6a..27ff16a942 100644 --- a/apps/backend/internal/agent/runtime/lifecycle/manager_interaction_test.go +++ b/apps/backend/internal/agent/runtime/lifecycle/manager_interaction_test.go @@ -250,6 +250,10 @@ func newRestartMockAgentctlServer(t *testing.T, failStop, failSessionNew bool) * "success": true, }) } + case "agent.prompt": + resp, _ = ws.NewResponse(msg.ID, msg.Action, map[string]interface{}{ + "success": true, + }) case "agent.stderr": stderrLines := m.stderrLines if len(stderrLines) == 0 { @@ -406,6 +410,7 @@ func TestManager_RestartAgentProcess_Success(t *testing.T) { exec.assistantHistoryBuffer.WriteString("old response") exec.needsResumeContext = true exec.resumeContextInjected = true + exec.dispatchedPromptPending.Store(true) exec.promptDoneCh <- PromptCompletionSignal{StopReason: "stale"} mgr.executionStore.Add(exec) @@ -443,6 +448,9 @@ func TestManager_RestartAgentProcess_Success(t *testing.T) { t.Fatalf("expected stale prompt signal to be drained") default: } + if exec.dispatchedPromptPending.Load() { + t.Fatal("expected stale dispatch gate to be cleared") + } httpActions := mock.getHTTPActions() if !slices.Equal(httpActions, []string{"stop", "configure", "start"}) { @@ -854,6 +862,47 @@ func TestManager_ResetAgentContext_ReappliesSessionMode(t *testing.T) { require.Zero(t, exec.assistantHistoryBuffer.Len()) } +// @covers AC-TASKS-WORKFLOW-STEP-AGENT-START-OWNERSHIP-002.3 +func TestManager_ResetAgentContext_ClearsIdleDispatchGate(t *testing.T) { + mgr := newTestManager(t) + mock := newRestartMockAgentctlServer(t, false, false) + + client := createTestClient(t, mock.server.URL) + t.Cleanup(client.Close) + + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) + defer cancel() + + require.NoError(t, client.StreamUpdates(ctx, func(agentctl.AgentEvent) {}, nil, nil)) + + exec := &AgentExecution{ + ID: "exec-idle-reset", + TaskID: "task-1", + SessionID: "session-1", + AgentProfileID: "profile-1", + ACPSessionID: "old-session", + AgentCommand: "auggie --model test", + Status: v1.AgentStatusRunning, + WorkspacePath: "/workspace", + sessionInitialized: true, + agentctl: client, + promptDoneCh: make(chan PromptCompletionSignal, 1), + } + exec.dispatchedPromptPending.Store(true) + exec.promptDoneCh <- PromptCompletionSignal{StopReason: "end_turn"} + require.NoError(t, mgr.executionStore.Add(exec)) + + require.NoError(t, mgr.ResetAgentContext(ctx, exec.ID)) + require.False(t, exec.dispatchedPromptPending.Load(), + "reset must clear the gate when it drains the completion signal") + + followUpCtx, followUpCancel := context.WithTimeout(context.Background(), 3*time.Second) + defer followUpCancel() + _, err := mgr.PromptAgent(followUpCtx, exec.ID, "follow-up", nil, true) + require.NoError(t, err, "follow-up prompt must reach the new agent context") + require.Contains(t, mock.getWSActions(), "agent.prompt") +} + // TestManager_ResetAgentContext_ReappliesSessionModel is the regression test // for an ACP fast-path reset replacing the task's selected model with the // provider default from the freshly-created session. diff --git a/docs/plans/idle-context-reset-dispatch-gate/plan.md b/docs/plans/idle-context-reset-dispatch-gate/plan.md new file mode 100644 index 0000000000..c8cf0c0d6a --- /dev/null +++ b/docs/plans/idle-context-reset-dispatch-gate/plan.md @@ -0,0 +1,62 @@ +--- +created: 2026-08-31 +status: done +requirements: + - REQ-TASKS-WORKFLOW-STEP-AGENT-START-OWNERSHIP-002 +system_design: + - ../../specs/tasks/system-design/workflow-step-agent-start-ownership.md +legacy_specs: [] +--- + +# Implementation Plan: Clear the Idle Context-Reset Dispatch Gate + +## Overview + +Clear the dispatch-only completion gate when an idle context reset removes its buffered completion signal. Cover the ACP reset path and the process-restart fallback. + +## Scope + +### In scope + +- Reproduce the idle dispatch-only state in lifecycle manager tests. +- Clear `dispatchedPromptPending` after each successful reset path drains `promptDoneCh`. +- Prove that the next prompt can reach agentctl after an ACP session reset. +- Prove that the process-restart fallback also clears the old gate. + +### Out of scope + +- Change the timeout behavior in `waitForPendingDispatchedPrompt`. +- Change active-turn quiescence or cancellation ownership. +- Add UI recovery for sessions that entered this state before the correction. +- Explain unrelated differences between prompt-completion log counts. + +## Technical approach + +Update `Manager.ResetAgentContext` in `manager_interaction.go`. Clear the atomic pending flag in the same locked state update that drains the old completion signal. + +Update `Manager.resetAgentRestartState` in the same file. Apply the same state transition to the process-restart fallback. + +Add focused tests in `manager_interaction_test.go`. The ACP test will reset an idle execution and send a follow-up dispatch-only prompt through the real manager path. + +Extend the existing restart-state test with a pending gate. The test will prove that process replacement clears the signal and its matching flag. + +## Tests + +- `AC-TASKS-WORKFLOW-STEP-AGENT-START-OWNERSHIP-002.3`: `TestManager_ResetAgentContext_ClearsIdleDispatchGate` proves that the follow-up prompt reaches the new ACP session. +- `AC-TASKS-WORKFLOW-STEP-AGENT-START-OWNERSHIP-002.3`: `TestManager_RestartAgentProcess_Success` proves that the process-restart fallback clears the prior gate. +- `AC-TASKS-WORKFLOW-STEP-AGENT-START-OWNERSHIP-002.5`: Existing pending-wait tests remain unchanged because the timeout still preserves active predecessor ownership. + +## Work orders + +- [x] [Task 01: Clear the idle reset dispatch gate](task-01-clear-idle-reset-dispatch-gate.md) + +## Verification results + +- RED: Both focused regression tests failed because the reset paths kept `dispatchedPromptPending` set. +- GREEN: The focused tests passed after both reset paths cleared the flag after the signal drain. +- Final: The race-enabled verification passed 2 tests. `git diff --check` also passed. + +## Risks + +- Clearing the gate before the old turn is quiescent can admit concurrent prompts. The existing orchestrator reset boundary must remain unchanged. +- Clearing only the ACP fast path leaves adapters that use process replacement exposed to the same error. diff --git a/docs/plans/idle-context-reset-dispatch-gate/task-01-clear-idle-reset-dispatch-gate.md b/docs/plans/idle-context-reset-dispatch-gate/task-01-clear-idle-reset-dispatch-gate.md new file mode 100644 index 0000000000..d0df8e8579 --- /dev/null +++ b/docs/plans/idle-context-reset-dispatch-gate/task-01-clear-idle-reset-dispatch-gate.md @@ -0,0 +1,87 @@ +--- +id: "01-clear-idle-reset-dispatch-gate" +title: "Clear the idle reset dispatch gate" +status: done +wave: 1 +depends_on: [] +plan: "plan.md" +requirements: + - REQ-TASKS-WORKFLOW-STEP-AGENT-START-OWNERSHIP-002 +acceptance_criteria: + - AC-TASKS-WORKFLOW-STEP-AGENT-START-OWNERSHIP-002.3 + - AC-TASKS-WORKFLOW-STEP-AGENT-START-OWNERSHIP-002.5 +system_design: + - ../../specs/tasks/system-design/workflow-step-agent-start-ownership.md +--- + +# Task 01: Clear the Idle Reset Dispatch Gate + +## Summary + +Clear the pending dispatch-only gate when a successful idle context reset removes its completion signal. Apply the correction to both provider-reset paths. + +## In scope + +- Add a failing regression test for an idle ACP reset with a buffered dispatch-only completion. +- Prove that a follow-up dispatch-only prompt reaches agentctl after the reset. +- Add pending-gate coverage to the existing process-restart state test. +- Clear the old pending flag after both reset paths drain the old signal. + +## Out of scope + +- Clear the gate when `waitForPendingDispatchedPrompt` times out. +- Change cancellation escalation or active-turn reset behavior. +- Add frontend behavior or browser tests. + +## Acceptance + +- An ACP session reset removes the old completion signal and clears its matching pending flag. +- The next prompt reaches agentctl without a predecessor timeout. +- The process-restart fallback clears the same signal and flag pair. + +## Verification + +```bash +go test -race ./internal/agent/runtime/lifecycle -run 'TestManager_(ResetAgentContext_ClearsIdleDispatchGate|RestartAgentProcess_Success)$' -count=1 +git diff --check +``` + +Run the commands from `apps/backend` and the repository root, respectively. + +## Files likely touched + +- `apps/backend/internal/agent/runtime/lifecycle/manager_interaction.go` +- `apps/backend/internal/agent/runtime/lifecycle/manager_interaction_test.go` +- `docs/plans/idle-context-reset-dispatch-gate/plan.md` +- `docs/plans/idle-context-reset-dispatch-gate/task-01-clear-idle-reset-dispatch-gate.md` + +## Dependencies + +None. + +## Risks + +- A reset path that drains the signal without clearing the flag will keep the execution unusable. +- A flag clear before successful context replacement can release a valid active generation. + +## Parallelism + +`sequential` + +## Inputs + +- `REQ-TASKS-WORKFLOW-STEP-AGENT-START-OWNERSHIP-002` +- `docs/specs/tasks/system-design/workflow-step-agent-start-ownership.md` +- `docs/decisions/2026-08-30-context-reset-quiesces-active-turn.md` +- GitHub issue `#3210` +- Existing reset and pending-prompt tests in the lifecycle package. + +## Results + +- RED: The focused command failed 2 tests before the production correction. +- `TestManager_RestartAgentProcess_Success` found that process replacement kept the stale gate. +- `TestManager_ResetAgentContext_ClearsIdleDispatchGate` found the same state after an ACP reset. +- GREEN: The focused command passed 2 tests after both reset paths cleared the gate. +- Final: The race-enabled verification passed 2 tests. `git diff --check` passed. +- Changed files: `manager_interaction.go`, `manager_interaction_test.go`, and this plan package. +- No known blockers remain.