🤖 feat: add goal intervention policy#3319
Conversation
|
@codex review |
|
Codex Review: Didn't find any major issues. Delightful! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
4c4e88b to
0aaa360
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. Keep them coming! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Normal manual sends now steer running goals by default, with an explicit pause policy available through the send menu and backend send options. Queued messages preserve sticky pause intent, accepted manual sends clear stale continuation candidates, and rejected/manual pre-stream paths remain safe. --- _Generated with `mux` • Model: `openai:gpt-5.5` • Thinking: `xhigh` • Cost: `$79.45`_
0aaa360 to
ee320c7
Compare
|
@codex review |
|
Codex Review: Didn't find any major issues. Swish! ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Summary
Normal user messages now steer active goals by default instead of auto-pausing them, while keeping an explicit Send and pause goal path for intentional pauses.
Background
Running goals previously treated any manual user message as an interruption and auto-paused the goal. This change lets users steer a goal at the next send boundary without disabling goal continuation, while preserving conservative safety behavior for rejected sends, edits, and explicit pause intent.
Implementation
goalInterventionPolicysend option with"steer"and"pause"policies.Validation
bun test src/node/services/agentSession.goalAutoPause.test.ts src/node/services/messageQueue.test.ts src/node/services/agentSession.budgetGate.test.ts --timeout 30000TEST_INTEGRATION=1 bun x jest tests/ui/chat/sendModeDropdown.test.ts --runInBandmake static-checkDogfooding
Evidence was captured under
dogfood-output/goal-intervention-policy/in this workspace, including:screenshots/mock-default-steering-sent.pngscreenshots/final-explicit-pause-menu-open-2.pngscreenshots/final-explicit-pause-sent-paused.pngvideos/final-explicit-pause-success.webmreport.mdThe mock-AI dogfood verified normal steering keeps the goal active and Send and pause goal transitions the goal to paused.
Risks
Goal continuation control is safety-sensitive. The main mitigations are backend defaults as the source of truth, explicit tests for accepted steering versus rejected sends, stale-candidate clearing, and not propagating the policy into provider/retry metadata.
📋 Implementation Plan
Implementation plan: explicit goal intervention policy
Goal
Let a user steer a running goal by sending a normal chat message without auto-pausing the goal, while retaining an explicit way to send a message that intentionally pauses the goal.
This implements Option 2 from the investigation: add an explicit goal-intervention policy to send-message options, with two policies:
"steer"— acknowledge any pending goal gate, send the user's message, and leave an active/running goal active so goal continuation may resume after the message turn."pause"— preserve today's safety behavior: acknowledge the user, then auto-pause an active/running goal before/around the user intervention.Verified context and constraints
AgentSession.applyManualUserMessageGoalSafety()insrc/node/services/agentSession.ts; it callsworkspaceGoalService.acknowledgeUser(...)and then pauses active goals viasetGoal({ status: "paused", initiator: "auto" }).AgentSession.sendMessage(...)calls that hook for accepted manual sends and for pricing-gate rejections after preserving the rejected user message.WorkspaceService.sendMessage(...)(src/node/services/workspaceService.ts) and later dispatch viaAgentSession.sendQueuedMessages(), so a steering message can already wait for a step/turn boundary.MessageQueuestores latest send options and combines queued messages insrc/node/services/messageQueue.ts; any policy that must survive queueing should be part of send options and/or explicitly aggregated by the queue.ChatInputalready has access to the current goal snapshot viauseOptionalWorkspaceSidebarState(workspaceId)and can useisGoalRunning(...)fromsrc/common/types/goal.ts.tool-end(Enter/button, “Send after step”) andturn-end(Ctrl/Cmd+Enter, “Send after turn”) viasrc/browser/features/ChatInput/sendDispatchModes.ts.Important semantic distinction
This plan does not attempt true mid-stream prompt injection into an in-flight model request. The existing queue/step/turn boundary behavior remains the steering delivery mechanism. The change is that a normal user message no longer flips the goal status to paused after it is accepted for sending.
Explicit interrupts/stops remain different from steering messages and should continue to gate future goal continuation for safety.
Recommended approach
Approach A — explicit enum + default steering for normal sends + explicit pause override
Net product LoC estimate: ~140–260 LoC total.
Breakdown:
This is the recommended implementation. It changes normal “send while goal is running” behavior to steering, while preserving an explicit pause path in the send menu and backend API.
Approach B — same backend enum, plus a persistent user setting for default behavior
Net product LoC estimate: ~260–480 LoC total.
This adds a Settings toggle such as “Messages sent during goals: steer / pause”. It is more flexible but adds settings storage, settings UI, copy, tests, and one more behavior axis. Defer unless users actively need to make old auto-pause the default.
Approach C — backend-only policy support, no UI affordance
Net product LoC estimate: ~70–140 LoC total.
This is smaller but weak: normal users would get changed behavior without an obvious way to intentionally pause by sending a message, and the explicit policy would mostly exist for internal callers. Not recommended because the user explicitly liked Option 2.
Design decisions
Use an enum, not a boolean.
goalInterventionPolicy?: "steer" | "pause"to send-message options.pauseActiveGoal?: boolean; the negative/optional boolean is harder to reason about and less extensible.Accepted normal manual sends default to
"steer"."steer"when it detects a running goal so transcript/UI intent is easy to reason about."steer"for non-edit manual sends so direct frontend callsites do not accidentally retain the old pause behavior.Explicit
"pause"remains available.goalInterventionPolicy: "pause".Rejected/unsent manual interventions should stay safe.
Edits are not ordinary steering.
"pause"unless the product explicitly decides otherwise later."steer"merely because a goal is running.Explicit user interrupts/stops are unchanged.
interruptStream(...)andWorkspaceGoalService.recordUserStoppedStream(...)should continue to require acknowledgment / suppress continuation.goalInterventionPolicyonly applies to sending a chat message, not hard stop semantics.Queued policy aggregation should be conservative.
"pause", the combined queued message should dispatch with"pause"."steer"when appropriate.Non-obvious correctness constraints from advisor review
Backend default is the source of truth.
"steer"for clarity, but correctness must not depend on it.Separate attempted sends from accepted sends.
Block stale pending goal continuations from racing ahead of steering.
WorkspaceGoalServicemethod that clears/defer pending continuation candidates for an accepted manual steering turn.Define accepted pre-stream failure behavior.
Policy is control metadata, not model/retry metadata.
goalInterventionPolicyshould affect only the current manual send's goal-safety behavior.retrySendOptions, startup-recovery options, and synthetic goal-continuation send options.Steering accounting becomes normal, not rare.
originKind === "user"behavior.budget_limitedwithoriginKind: "user", budget wrap-up suppression must be intentional and covered by tests or comments.Implementation phases
Phase 1 — Shared schema and type plumbing
Files:
src/common/orpc/schemas/stream.tssrc/common/orpc/types.ts(likely type inference only)Steps:
Add the option to
SendMessageOptionsSchema:Match the surrounding schema style. If this schema is consumed as provider/tool input anywhere strict-null compatibility matters, use the repo convention for nullable optionals.
Derive or export a type where needed:
Add a tiny resolver/helper if it clarifies semantics:
Quality gate:
Phase 2 — Backend session behavior
Files:
src/node/services/agentSession.tssrc/node/services/workspaceService.tsif policy resolution should be centralized before queueing.src/node/services/messageQueue.tsSteps:
Replace
applyManualUserMessageGoalSafety()with a policy-aware form, e.g.:Keep the existing try/catch/logging around the pause mutation.
In
AgentSession.sendMessage(...), resolve policy separately for accepted manual sends:options.goalInterventionPolicy ?? "steer".options.goalInterventionPolicy ?? "pause"."pause"always pauses if a goal is active."steer"acknowledges but does not pause.Move/apply the accepted-send policy at the durable acceptance boundary:
Add a deterministic continuation-race mitigation for accepted steering:
WorkspaceGoalServicemethod or equivalent session bridge hook to clear/defer pending continuation candidates for an accepted manual steering turn without changing goal status.On pricing-gate rejection paths where
preserveRejectedManualSend(...)returns true:"pause", because the steering message was not accepted by the model."steer"sends.Define terminal accepted-pre-stream failure handling:
Ensure queued sends preserve/aggregate policy:
Add a queue-level field in
MessageQueuesuch asgoalInterventionPolicy?: GoalInterventionPolicy.Only manual entries should contribute policy; synthetic entries should not accidentally pause a goal.
On
addInternal, compute sticky pause:In
produceMessage(), reattachgoalInterventionPolicyto produced options if present.In
clear(), reset it.Treat policy as control metadata:
goalInterventionPolicyfrom provider/model request options.retrySendOptionsor startup recovery state.Confirm
streamWithHistory(...)andaiService.streamMessage(...)do not need to consume this option. It is a send/session policy, not a model provider option.Quality gate:
bun test src/node/services/agentSession.goalAutoPause.test.tsbun test src/node/services/messageQueue.test.tsworkspaceService.test.tsslices if touched.Phase 3 — Frontend policy selection and UI
Files:
src/browser/features/ChatInput/index.tsxsrc/browser/features/ChatInput/types.tssrc/browser/features/ChatInput/sendDispatchModes.tsor a sibling goal-policy config if needed.src/browser/features/Messages/QueuedMessage.tsxsrc/common/types/message.tssrc/common/orpc/schemas/stream.tsqueued-message event schemasrc/node/services/agentSession.tsemitQueuedMessageChanged()payloadSteps:
Import/use goal helpers in
ChatInput:Compute default policy only for workspace, running-goal, non-edit sends:
Extend
handleSendoverrides:Add policy to constructed
sendOptions:Thread the same override shape through
executeParsedCommand(...)only for command paths that behave like user-message sends (model one-shots, skills, normal message-producing commands). Do not blindly apply it to explicit goal-management commands or compaction commands unless their product semantics are intentionally user-steering.Add a goal-aware send-menu action visible only when
runningGoalActive && !editingMessageForUi:handleSend({ goalInterventionPolicy: "pause" }).Optional/deferable: if reviewers want queued pause intent visible, include policy in queued-message event data and display a small label such as “Will pause goal” for queued pause messages. Defer this if the core behavior is already clear enough; it expands event/schema/UI scope.
Quality gate:
ChatInput.Phase 4 — Tests
Backend tests:
Update
src/node/services/agentSession.goalAutoPause.test.ts:activerecordGoalLifecycleEvent("goal_paused", ...)is not called{ goalInterventionPolicy: "pause" }pausedinitiator: "auto"requireUserAcknowledgmentSinceMswhile keeping statusactive.Update
src/node/services/messageQueue.test.ts:Update/add
src/node/services/workspaceService.test.tsonly if WorkspaceService gains policy-specific logic:Additional backend regression cases from advisor review:
goalInterventionPolicydoes not leak into synthetic goal-continuation sends or persisted retry/startup-recovery send options.Frontend tests/stories:
If a
ChatInputtest harness exists or is practical:goalInterventionPolicy: "steer""steer""pause"If no harness exists, add/update a Storybook story for the send menu with a running goal and rely on dogfooding plus type/lint validation rather than creating a large new UI harness.
Test quality guardrails:
Phase 5 — Documentation/comments cleanup
Files:
src/common/types/goal.tsAgentSession/ goal tests.Steps:
Do not add standalone docs unless requested; keep rationale near the code.
Acceptance criteria
goalInterventionPolicydoes not leak into provider requests, persisted retry options, startup recovery, or synthetic continuation options.tool-end,turn-end) continue to work.Dogfooding and quality gates
Dogfooding must follow the
dogfood,agent-browser, anddev-server-sandboxskills: use the directagent-browserbinary (nevernpx), gather screenshots/video evidence, write findings incrementally, and run the app in an isolated sandbox when possible.Setup
./dogfood-output/goal-intervention-policy/, withscreenshots/,videos/, and a report copied from the dogfood report template.agent-browser skills get coreagent-browser skills get electron.MUX_ROOT:make dev-server-sandboxmake dev-server-sandbox DEV_SERVER_SANDBOX_ARGS="--clean-projects"KEEP_SANDBOX=1only when debugging and remember provider config may contain API keys; the sandbox intentionally does not copysecrets.json.VITE_PORT/target URL from the sandbox output as theagent-browsertarget.agent-browser --session goal-intervention open <target-url>agent-browser --session goal-intervention wait --load networkidleagent-browser --session goal-intervention screenshot --annotate dogfood-output/goal-intervention-policy/screenshots/initial.pngagent-browser --session goal-intervention snapshot -isnapshot -ibefore interactions, re-snapshot after every page-changing action, captureagent-browser console/agent-browser errorsperiodically, and usetyperather thanfillwhile recording videos so repros are watchable.Gate 1 — default steering
/goal <objective>.agent-browser --session goal-intervention record start dogfood-output/goal-intervention-policy/videos/default-steering.webmGate 2 — explicit pause override
Gate 3 — queued policy behavior
Gate 4 — regression checks
Run:
make typecheckmake lintormake static-checkif the change is broad enough and runtime allows.Gate 5 — wrap-up evidence
agent-browser --session goal-intervention close.Risks and mitigations
MessageQueue."steer", and implementation should audit directapi.workspace.sendMessagecallsites.Implementation order
applyManualUserMessageGoalSafetyrefactor.Follow-up plan: commit, PR, and readiness loop
Mode constraint: this workspace is currently in Plan Mode, so I must not run mutating git/GitHub commands yet. Switch to Exec mode to execute this plan.
Net product LoC estimate: 0 LoC. This follow-up should only commit the already-implemented work, create/update a pull request, and wait for gates.
Required preflight
pull-requestsskill before any commit or PR action (done for this plan; repeat in Exec if the session has changed).git status --shortgit diff --statprintf '%s\n' "$MUX_MODEL_STRING" "$MUX_THINKING_LEVEL" "$MUX_COSTS_USD"Commit plan
src/browser/features/ChatInput/index.tsx,src/browser/features/ChatInput/types.ts,src/common/orpc/schemas/stream.ts,src/common/types/goal.ts,src/node/services/agentSession.ts,src/node/services/messageQueue.ts,src/node/services/workspaceGoalService.ts, and tests.dogfood-output/goal-intervention-policy/; include it only if the repository convention allows generated dogfood artifacts in commits. If not, keep it untracked/out of commit and reference paths in the PR body.bun test src/node/services/agentSession.goalAutoPause.test.ts src/node/services/messageQueue.test.ts src/node/services/agentSession.budgetGate.test.ts --timeout 30000bun test ./tests/ui/chat/sendModeDropdown.test.ts --timeout 120000make static-checkgit add <intended files>git commit -m "feat: steer active goals from normal user sends"git push --set-upstream origin HEADif no upstream exists, otherwisegit push.Pull request plan
gh pr view --json number,url,title,state.🤖 feat: steer active goals from normal user sendsmktemp; do not use a hard-coded/tmp/pr-body.mdpath.make static-check, dogfood evidence paths.<details><summary>📋 Implementation Plan</summary>containing this plan file’s contents, per repo preference.pull-requestsskill using$MUX_MODEL_STRING,$MUX_THINKING_LEVEL, and$MUX_COSTS_USD.gh pr new --body-file "$PR_BODY"orgh pr edit --body-file "$PR_BODY"as appropriate.PR readiness loop
gh pr comment <number> --body-file - <<'EOF' ... EOF./scripts/wait_pr_ready.sh <pr_number>../scripts/resolve_pr_comment.sh <thread_id>;@codex reviewagain.Dogfooding / quality gate for this follow-up
No additional product dogfooding is needed for the commit/PR mechanics. Preserve the existing dogfood report and evidence paths in the PR body:
dogfood-output/goal-intervention-policy/report.mddogfood-output/goal-intervention-policy/screenshots/mock-default-steering-sent.pngdogfood-output/goal-intervention-policy/screenshots/final-explicit-pause-menu-open-2.pngdogfood-output/goal-intervention-policy/screenshots/final-explicit-pause-sent-paused.pngdogfood-output/goal-intervention-policy/videos/final-explicit-pause-success.webmAcceptance criteria for this follow-up
Generated with
mux• Model:openai:gpt-5.5• Thinking:xhigh• Cost:$79.45