Skip to content

🤖 feat: add goal intervention policy#3319

Open
ThomasK33 wants to merge 1 commit into
mainfrom
agent-control-db8w
Open

🤖 feat: add goal intervention policy#3319
ThomasK33 wants to merge 1 commit into
mainfrom
agent-control-db8w

Conversation

@ThomasK33
Copy link
Copy Markdown
Member

@ThomasK33 ThomasK33 commented May 18, 2026

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

  • Added a typed goalInterventionPolicy send option with "steer" and "pause" policies.
  • Default accepted non-edit manual sends to steering; edit sends and rejected pricing-gate sends remain pause/gated.
  • Clear stale pending continuation candidates when accepting a manual intervention so old continuations cannot race ahead of the steering turn.
  • Preserve sticky explicit pause policy through queued message batching.
  • Added the goal-aware Send and pause goal menu action in ChatInput.
  • Kept policy metadata out of provider/retry stream options.

Validation

  • bun test src/node/services/agentSession.goalAutoPause.test.ts src/node/services/messageQueue.test.ts src/node/services/agentSession.budgetGate.test.ts --timeout 30000
  • TEST_INTEGRATION=1 bun x jest tests/ui/chat/sendModeDropdown.test.ts --runInBand
  • make static-check

Dogfooding

Evidence was captured under dogfood-output/goal-intervention-policy/ in this workspace, including:

  • screenshots/mock-default-steering-sent.png
  • screenshots/final-explicit-pause-menu-open-2.png
  • screenshots/final-explicit-pause-sent-paused.png
  • videos/final-explicit-pause-success.webm
  • report.md

The 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

  • Current auto-pause is centralized in AgentSession.applyManualUserMessageGoalSafety() in src/node/services/agentSession.ts; it calls workspaceGoalService.acknowledgeUser(...) and then pauses active goals via setGoal({ status: "paused", initiator: "auto" }).
  • AgentSession.sendMessage(...) calls that hook for accepted manual sends and for pricing-gate rejections after preserving the rejected user message.
  • Busy workspaces already queue sends in WorkspaceService.sendMessage(...) (src/node/services/workspaceService.ts) and later dispatch via AgentSession.sendQueuedMessages(), so a steering message can already wait for a step/turn boundary.
  • MessageQueue stores latest send options and combines queued messages in src/node/services/messageQueue.ts; any policy that must survive queueing should be part of send options and/or explicitly aggregated by the queue.
  • ChatInput already has access to the current goal snapshot via useOptionalWorkspaceSidebarState(workspaceId) and can use isGoalRunning(...) from src/common/types/goal.ts.
  • Mux already exposes send timing modes: tool-end (Enter/button, “Send after step”) and turn-end (Ctrl/Cmd+Enter, “Send after turn”) via src/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:

  • Shared schema/types: ~5–20 LoC
  • Backend/session/queue: ~60–110 LoC
  • Frontend send-menu/default policy: ~50–100 LoC
  • Queued-message policy label, if included: ~25–50 LoC

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

  1. Use an enum, not a boolean.

    • Add goalInterventionPolicy?: "steer" | "pause" to send-message options.
    • Avoid pauseActiveGoal?: boolean; the negative/optional boolean is harder to reason about and less extensible.
  2. Accepted normal manual sends default to "steer".

    • This satisfies the desired product behavior: “Sending a message while a goal is running should not pause the goal.”
    • The frontend should explicitly pass "steer" when it detects a running goal so transcript/UI intent is easy to reason about.
    • The backend should also resolve omitted policy to "steer" for non-edit manual sends so direct frontend callsites do not accidentally retain the old pause behavior.
  3. Explicit "pause" remains available.

    • Add a goal-aware send-menu action such as Send and pause goal when a goal is currently running.
    • This action sends the same user message but includes goalInterventionPolicy: "pause".
  4. Rejected/unsent manual interventions should stay safe.

    • If a message is rejected before a stream can start (notably pricing-gate rejection under a budgeted goal), the agent never saw the steering content.
    • Preserve the current safety behavior on that rejection path: after preserving the user-visible rejected message/error, pause or gate the active goal rather than letting a pending continuation fire blindly.
    • Tests should make this explicit so it is not confused with accepted steering sends.
  5. Edits are not ordinary steering.

    • Editing/truncating history while a goal is active is a stronger intervention than “steer the next turn.”
    • Default edit sends to "pause" unless the product explicitly decides otherwise later.
    • Frontend should not default edit sends to "steer" merely because a goal is running.
  6. Explicit user interrupts/stops are unchanged.

    • interruptStream(...) and WorkspaceGoalService.recordUserStoppedStream(...) should continue to require acknowledgment / suppress continuation.
    • goalInterventionPolicy only applies to sending a chat message, not hard stop semantics.
  7. Queued policy aggregation should be conservative.

    • If any queued entry explicitly requests "pause", the combined queued message should dispatch with "pause".
    • Otherwise default/steering queued entries dispatch as "steer" when appropriate.
    • This avoids losing an explicit pause request when later queued content is added.

Non-obvious correctness constraints from advisor review

  1. Backend default is the source of truth.

    • The frontend should pass "steer" for clarity, but correctness must not depend on it.
    • Backend resolution must make accepted, non-edit, non-synthetic manual sends steer by default.
  2. Separate attempted sends from accepted sends.

    • Empty-message validation, model-format validation, attachment validation, and other pre-accept failures should not clear acknowledgment gates or pause active goals just because the user tried to submit.
    • Once the user turn is durably accepted into history, apply the policy.
    • If the accepted turn is represented by an on-send compaction container rather than the original user message, treat that persisted compaction/follow-up container as the acceptance boundary.
  3. Block stale pending goal continuations from racing ahead of steering.

    • Today auto-pause naturally disqualifies pending continuation candidates. Steering leaves the goal active, so implementation must explicitly prevent an already-armed candidate from firing before the accepted steering turn.
    • Add or reuse a deterministic gate, for example:
      • set the session busy/preparing early enough once a manual send is accepted, and/or
      • add a WorkspaceGoalService method that clears/defer pending continuation candidates for an accepted manual steering turn.
    • The steering turn's own stream-end should arm the next continuation normally.
  4. Define accepted pre-stream failure behavior.

    • If a steering message is persisted but the stream never starts and no retry will process it, do not let goal continuation proceed blindly.
    • Safe default: pause/gate the goal on terminal accepted-pre-stream failure because the model never saw the steering content.
    • If existing auto-retry will process the accepted turn, keep continuation suppressed until retry succeeds or terminally fails.
  5. Policy is control metadata, not model/retry metadata.

    • goalInterventionPolicy should affect only the current manual send's goal-safety behavior.
    • Strip or avoid propagating it into provider request options, active stream context, persisted retrySendOptions, startup-recovery options, and synthetic goal-continuation send options.
  6. Steering accounting becomes normal, not rare.

    • A user-origin stream while a goal remains active should be intentionally defined.
    • Proposed semantics: steering turns may add cost to the active goal budget but do not increment the goal turn cap, matching current originKind === "user" behavior.
    • If a steering turn pushes a goal into budget_limited with originKind: "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.ts
  • src/common/orpc/types.ts (likely type inference only)
  • Optionally a small shared helper/type module if schema-local typing becomes awkward.

Steps:

  1. Add the option to SendMessageOptionsSchema:

    goalInterventionPolicy: z.enum(["steer", "pause"]).optional()

    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.

  2. Derive or export a type where needed:

    type GoalInterventionPolicy = NonNullable<SendMessageOptions["goalInterventionPolicy"]>;
  3. Add a tiny resolver/helper if it clarifies semantics:

    function normalizeGoalInterventionPolicy(
      value: SendMessageOptions["goalInterventionPolicy"] | undefined,
      fallback: GoalInterventionPolicy
    ): GoalInterventionPolicy {
      return value ?? fallback;
    }

Quality gate:

  • Run targeted typecheck or relevant schema tests after backend/frontend usage is in place.

Phase 2 — Backend session behavior

Files:

  • src/node/services/agentSession.ts
  • src/node/services/workspaceService.ts if policy resolution should be centralized before queueing.
  • src/node/services/messageQueue.ts

Steps:

  1. Replace applyManualUserMessageGoalSafety() with a policy-aware form, e.g.:

    private async applyManualUserMessageGoalSafety(input: {
      policy: GoalInterventionPolicy;
    }): Promise<void> {
      assert(input.policy === "steer" || input.policy === "pause", "invalid goal intervention policy");
    
      const goal = await goalService.acknowledgeUser(this.workspaceId);
      if (goal?.status !== "active") return;
      if (input.policy !== "pause") return;
    
      await goalService.setGoal({
        workspaceId: this.workspaceId,
        status: "paused",
        initiator: "auto",
      });
    }

    Keep the existing try/catch/logging around the pause mutation.

  2. In AgentSession.sendMessage(...), resolve policy separately for accepted manual sends:

    • Synthetic sends: no manual safety hook, unchanged.
    • Accepted non-edit manual sends: default to options.goalInterventionPolicy ?? "steer".
    • Accepted edit sends: default to options.goalInterventionPolicy ?? "pause".
    • Explicit "pause" always pauses if a goal is active.
    • Explicit "steer" acknowledges but does not pause.
    • Do not run the safety hook for empty-message, invalid-model, attachment, or other pre-accept validation failures.
  3. Move/apply the accepted-send policy at the durable acceptance boundary:

    • For ordinary sends, after the user message is appended successfully to history.
    • For on-send compaction, after the compaction request/follow-up container is durably appended.
    • Before any pending goal continuation can dispatch ahead of this accepted user turn.
  4. Add a deterministic continuation-race mitigation for accepted steering:

    • Add a small WorkspaceGoalService method or equivalent session bridge hook to clear/defer pending continuation candidates for an accepted manual steering turn without changing goal status.
    • Call it when a manual steering turn is accepted.
    • Let the steering turn's stream-end arm the next continuation candidate normally.
    • Add a regression test proving an already-pending continuation cannot fire before the accepted steering turn.
  5. On pricing-gate rejection paths where preserveRejectedManualSend(...) returns true:

    • Apply a safe rejection policy, likely "pause", because the steering message was not accepted by the model.
    • Add a comment explaining this is intentionally different from accepted "steer" sends.
  6. Define terminal accepted-pre-stream failure handling:

    • If the accepted steering turn will be retried by existing retry machinery, keep goal continuation suppressed until retry resolves.
    • If no retry will process the accepted turn, pause or acknowledgment-gate the goal because the model never saw the steering content.
    • Add tests for the selected path.
  7. Ensure queued sends preserve/aggregate policy:

    • Add a queue-level field in MessageQueue such as goalInterventionPolicy?: GoalInterventionPolicy.

    • Only manual entries should contribute policy; synthetic entries should not accidentally pause a goal.

    • On addInternal, compute sticky pause:

      const incomingPolicy = options?.goalInterventionPolicy;
      this.goalInterventionPolicy =
        this.goalInterventionPolicy === "pause" || incomingPolicy === "pause"
          ? "pause"
          : (incomingPolicy ?? this.goalInterventionPolicy);
    • In produceMessage(), reattach goalInterventionPolicy to produced options if present.

    • In clear(), reset it.

  8. Treat policy as control metadata:

    • Strip or omit goalInterventionPolicy from provider/model request options.
    • Do not persist it into retrySendOptions or startup recovery state.
    • Do not pass it into synthetic goal-continuation or budget-wrap-up send options.
  9. Confirm streamWithHistory(...) and aiService.streamMessage(...) do not need to consume this option. It is a send/session policy, not a model provider option.

Quality gate:

  • Run targeted backend tests after adding tests in Phase 4:
    • bun test src/node/services/agentSession.goalAutoPause.test.ts
    • bun test src/node/services/messageQueue.test.ts
    • relevant workspaceService.test.ts slices if touched.

Phase 3 — Frontend policy selection and UI

Files:

  • src/browser/features/ChatInput/index.tsx
  • src/browser/features/ChatInput/types.ts
  • src/browser/features/ChatInput/sendDispatchModes.ts or a sibling goal-policy config if needed.
  • Optional queued-message UI files if displaying policy while queued:
    • src/browser/features/Messages/QueuedMessage.tsx
    • src/common/types/message.ts
    • src/common/orpc/schemas/stream.ts queued-message event schema
    • src/node/services/agentSession.ts emitQueuedMessageChanged() payload

Steps:

  1. Import/use goal helpers in ChatInput:

    import { isGoalRunning } from "@/common/types/goal";
  2. Compute default policy only for workspace, running-goal, non-edit sends:

    const runningGoalActive = variant === "workspace" && isGoalRunning(workspaceGoal?.status);
    const shouldDefaultSteerGoal = runningGoalActive && !editingMessageForUi;
  3. Extend handleSend overrides:

    const handleSend = async (overrides?: {
      queueDispatchMode?: QueueDispatchMode;
      goalInterventionPolicy?: GoalInterventionPolicy;
    }) => { ... }
  4. Add policy to constructed sendOptions:

    const goalInterventionPolicy =
      overrides?.goalInterventionPolicy ?? (shouldDefaultSteerGoal ? "steer" : undefined);
    
    const sendOptions = {
      ...,
      ...(goalInterventionPolicy ? { goalInterventionPolicy } : {}),
    };
  5. 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.

  6. Add a goal-aware send-menu action visible only when runningGoalActive && !editingMessageForUi:

    • Label: Send and pause goal.
    • Action: handleSend({ goalInterventionPolicy: "pause" }).
    • Keep existing send timing entries unchanged.
    • Do not introduce a full timing × policy matrix yet; that is more UI than necessary.
  7. 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:

  • Run targeted UI tests/stories if existing harness supports ChatInput.
  • At minimum run TypeScript/lint after schema and UI changes.

Phase 4 — Tests

Backend tests:

  1. Update src/node/services/agentSession.goalAutoPause.test.ts:

    • Replace/rename the current “manual user messages auto-pause active goals before streaming” test with default steering expectation:
      • active goal remains active
      • recordGoalLifecycleEvent("goal_paused", ...) is not called
    • Add explicit policy test:
      • send with { goalInterventionPolicy: "pause" }
      • active goal becomes paused
      • lifecycle event records initiator: "auto"
    • Keep synthetic-message tests unchanged.
    • Update acknowledgment test:
      • default steering clears requireUserAcknowledgmentSinceMs while keeping status active.
    • Add rejection-path test:
      • pricing gate rejects and rejected manual message is preserved
      • active goal pauses/gates for safety because the steering was not accepted.
  2. Update src/node/services/messageQueue.test.ts:

    • queued steering policy survives produce/clear
    • explicit pause policy survives queueing
    • pause is sticky when mixed with later/default steering entries
    • clear resets policy.
  3. Update/add src/node/services/workspaceService.test.ts only if WorkspaceService gains policy-specific logic:

    • busy send queues policy
    • explicit pause policy is passed to the queue
    • default normal sends do not get blocked by policy logic.

Additional backend regression cases from advisor review:

  • Pre-accept validation failures (empty/invalid model/invalid attachment) do not acknowledge, pause, or clear pending goal gates.
  • Accepted pre-stream/startup failure follows the defined safe behavior (retry suppression or pause/gate on terminal failure).
  • A pending continuation candidate cannot fire before an accepted steering message is processed.
  • goalInterventionPolicy does not leak into synthetic goal-continuation sends or persisted retry/startup-recovery send options.
  • User-origin active-goal accounting expectations are asserted or documented: steering cost may count against budget; steering does not increment turn cap.

Frontend tests/stories:

  1. If a ChatInput test harness exists or is practical:

    • running goal + normal send includes goalInterventionPolicy: "steer"
    • no running goal omits the policy
    • edit send while running goal does not default to "steer"
    • explicit menu action sends "pause"
    • queue dispatch override still passes through.
  2. 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:

  • Avoid copy-only assertions on menu prose.
  • Prefer behavior assertions on the sent API payload and goal status transitions.

Phase 5 — Documentation/comments cleanup

Files:

  • src/common/types/goal.ts
  • nearby comments in AgentSession / goal tests.

Steps:

  1. Update comments that currently describe paused as caused by “fresh message mid-stream” so they do not encode stale behavior.
  2. Add a short comment at the policy resolver explaining:
    • accepted normal sends steer by default
    • rejected sends pause/gate because the model never saw the steering content
    • edits/explicit interrupts remain stronger interventions.

Do not add standalone docs unless requested; keep rationale near the code.

Acceptance criteria

  • Normal manual message while a goal is running no longer auto-pauses the goal after the message is accepted for sending.
  • The same send still clears any pending user-acknowledgment goal gate.
  • Explicit Send and pause goal pauses an active goal and records the existing auto-pause lifecycle event.
  • Queued steering messages dispatch before the next goal continuation and do not pause the goal.
  • Queued explicit pause messages preserve the pause policy until dispatch.
  • Pricing-gate rejection or other pre-stream rejection does not let a goal blindly continue as if the user’s steering had been processed.
  • Edit sends and explicit interrupts keep safe/pause/gate behavior unless intentionally changed later.
  • Pending goal continuation candidates cannot race ahead of an accepted steering message.
  • Accepted pre-stream failures either retry the accepted steering turn or pause/gate before continuation can proceed.
  • goalInterventionPolicy does not leak into provider requests, persisted retry options, startup recovery, or synthetic continuation options.
  • Synthetic goal continuations and budget-limit wrap-up messages are unaffected.
  • Existing send timing modes (tool-end, turn-end) continue to work.

Dogfooding and quality gates

Dogfooding must follow the dogfood, agent-browser, and dev-server-sandbox skills: use the direct agent-browser binary (never npx), gather screenshots/video evidence, write findings incrementally, and run the app in an isolated sandbox when possible.

Setup

  1. Prepare a dogfood evidence directory, e.g. ./dogfood-output/goal-intervention-policy/, with screenshots/, videos/, and a report copied from the dogfood report template.
  2. Before browser automation, load the installed agent-browser workflow content so commands match the local CLI version:
    • agent-browser skills get core
    • If testing the desktop Electron shell rather than the web/dev UI, also load agent-browser skills get electron.
  3. Start an isolated Mux dev server with a temporary MUX_ROOT:
    • Default seeded sandbox: make dev-server-sandbox
    • Clean sandbox when avoiding existing providers/projects: make dev-server-sandbox DEV_SERVER_SANDBOX_ARGS="--clean-projects"
    • Use KEEP_SANDBOX=1 only when debugging and remember provider config may contain API keys; the sandbox intentionally does not copy secrets.json.
  4. Use the reported VITE_PORT/target URL from the sandbox output as the agent-browser target.
  5. Start a named browser session and orient:
    • agent-browser --session goal-intervention open <target-url>
    • agent-browser --session goal-intervention wait --load networkidle
    • agent-browser --session goal-intervention screenshot --annotate dogfood-output/goal-intervention-policy/screenshots/initial.png
    • agent-browser --session goal-intervention snapshot -i
  6. Enable the Goals experiment and configure test providers as needed. If real provider calls are expensive/unavailable, use the lightest available local/test model path that still exercises goal continuation.
  7. During dogfooding, use snapshot -i before interactions, re-snapshot after every page-changing action, capture agent-browser console / agent-browser errors periodically, and use type rather than fill while recording videos so repros are watchable.

Gate 1 — default steering

  1. Create/open a workspace.
  2. Start a running goal from the Goal tab or /goal <objective>.
  3. Capture annotated screenshot: goal status is running/active.
  4. Start a repro video before sending steering input:
    • agent-browser --session goal-intervention record start dogfood-output/goal-intervention-policy/videos/default-steering.webm
  5. While a stream/goal turn is running, type a steering message at human pace and press Enter.
  6. Capture step screenshots and final annotated screenshot showing:
    • the message is queued or sent according to current turn state
    • the goal remains running/active, not paused
    • the agent responds to the steering message
    • a later goal continuation can resume.
  7. Stop the video and record the exact steps/evidence paths in the dogfood report.

Gate 2 — explicit pause override

  1. Start another running goal or resume the existing one.
  2. Start a repro video before opening the send menu.
  3. Open the send menu with right-click/long-press on Send.
  4. Capture an annotated screenshot showing Send and pause goal.
  5. Use that action.
  6. Capture step screenshots/video showing the goal becomes paused.
  7. Stop the video and write the repro/evidence paths into the report.

Gate 3 — queued policy behavior

  1. Trigger a foreground tool or long-running turn.
  2. Queue a normal steering message.
  3. Capture screenshot/video evidence that queued UI does not imply pausing and goal remains active after dispatch.
  4. Queue an explicit pause message.
  5. Verify queued UI, if implemented, marks it as pause-intent and the goal pauses after dispatch.
  6. Add console/errors output to the report if any warnings or exceptions appear.

Gate 4 — regression checks

Run:

  • targeted backend tests listed above
  • targeted frontend/UI tests or stories if added
  • make typecheck
  • make lint or make static-check if the change is broad enough and runtime allows.

Gate 5 — wrap-up evidence

  1. Re-read the dogfood report and ensure screenshots/videos referenced by each finding exist.
  2. Update summary counts if any issues were found.
  3. Close the browser session with agent-browser --session goal-intervention close.
  4. Attach the key screenshots/video files or provide their paths in the final implementation summary.

Risks and mitigations

  • Rejected steering message blind continuation: mitigate by keeping rejection path safe/pause/gated.
  • Mixed queued policies: mitigate with sticky pause aggregation in MessageQueue.
  • Edit semantics: treat edits as pause by default because they rewrite history rather than steer the next turn.
  • UI clutter: add one conditional pause action instead of a full policy/timing matrix.
  • Hidden direct send callsites: backend default for accepted manual non-edit sends should be "steer", and implementation should audit direct api.workspace.sendMessage callsites.
  • Budget accounting confusion: user-origin steering turns currently should not increment goal turn count, but may still affect cost/budget. Keep this behavior unless product explicitly wants steering turns to count as goal turns.

Implementation order

  1. Shared schema/type field.
  2. Backend policy resolver and applyManualUserMessageGoalSafety refactor.
  3. Queue policy aggregation.
  4. Backend tests.
  5. Frontend default steering + explicit pause menu action.
  6. Optional queued-message pause label.
  7. Frontend tests/stories.
  8. Comment cleanup.
  9. Dogfood and validation.

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

  1. Read the pull-requests skill before any commit or PR action (done for this plan; repeat in Exec if the session has changed).
  2. Re-check the worktree:
    • git status --short
    • git diff --stat
    • ensure dogfood artifacts are intentionally included or intentionally excluded from the commit based on repo policy.
  3. Capture attribution values before creating the PR:
    • printf '%s\n' "$MUX_MODEL_STRING" "$MUX_THINKING_LEVEL" "$MUX_COSTS_USD"

Commit plan

  1. Review the final diff at a high level:
    • changed product/test files: 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 report/evidence lives under 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.
  2. Run or confirm validation before committing:
    • bun test src/node/services/agentSession.goalAutoPause.test.ts src/node/services/messageQueue.test.ts src/node/services/agentSession.budgetGate.test.ts --timeout 30000
    • bun test ./tests/ui/chat/sendModeDropdown.test.ts --timeout 120000
    • make static-check
  3. Stage the intended files and commit with a concise message, e.g.:
    • git add <intended files>
    • git commit -m "feat: steer active goals from normal user sends"
  4. Push the branch:
    • git push --set-upstream origin HEAD if no upstream exists, otherwise git push.

Pull request plan

  1. Check whether a PR already exists for the branch:
    • gh pr view --json number,url,title,state.
  2. If no PR exists, create one with a title following repo convention:
    • 🤖 feat: steer active goals from normal user sends
  3. Build the PR body in a unique temp file from mktemp; do not use a hard-coded /tmp/pr-body.md path.
  4. PR body sections:
    • Summary: normal sends steer active goals by default; explicit send-and-pause remains available.
    • Background: previous auto-pause made it hard to steer running goals.
    • Implementation: shared policy enum, backend default/pause handling, queue aggregation, stale continuation clearing, frontend menu action.
    • Validation: targeted backend/UI tests, make static-check, dogfood evidence paths.
    • Risks: goal continuation race handling, rejected-send safety, edit semantics.
    • <details><summary>📋 Implementation Plan</summary> containing this plan file’s contents, per repo preference.
    • Attribution footer from the pull-requests skill using $MUX_MODEL_STRING, $MUX_THINKING_LEVEL, and $MUX_COSTS_USD.
  5. Use gh pr new --body-file "$PR_BODY" or gh pr edit --body-file "$PR_BODY" as appropriate.

PR readiness loop

  1. Request Codex review with a real multiline body file/heredoc:
    • gh pr comment <number> --body-file - <<'EOF' ... EOF
  2. Run ./scripts/wait_pr_ready.sh <pr_number>.
  3. If Codex leaves comments:
    • inspect all PR reviews, review comments, and issue comments;
    • fix locally;
    • resolve each thread with ./scripts/resolve_pr_comment.sh <thread_id>;
    • push and request @codex review again.
  4. If CI fails, reproduce locally, fix, validate, push, and rerun readiness.
  5. Stop only when Codex approves, all Codex threads are resolved, and required CI checks pass.

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.md
  • dogfood-output/goal-intervention-policy/screenshots/mock-default-steering-sent.png
  • dogfood-output/goal-intervention-policy/screenshots/final-explicit-pause-menu-open-2.png
  • dogfood-output/goal-intervention-policy/screenshots/final-explicit-pause-sent-paused.png
  • dogfood-output/goal-intervention-policy/videos/final-explicit-pause-success.webm

Acceptance criteria for this follow-up

  • A commit exists containing the intended implementation/test changes.
  • A PR exists or the existing PR is updated for the current branch.
  • PR body includes validation, dogfood evidence, implementation plan details, and the required attribution footer.
  • Branch is pushed to the remote.
  • PR readiness loop has run until CI and Codex gates are ready, or any blocker is reported with exact evidence.

Generated with mux • Model: openai:gpt-5.5 • Thinking: xhigh • Cost: $79.45

@ThomasK33
Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

@ThomasK33 ThomasK33 force-pushed the agent-control-db8w branch from 4c4e88b to 0aaa360 Compare May 18, 2026 20:49
@ThomasK33
Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

@ThomasK33 ThomasK33 added this pull request to the merge queue May 19, 2026
@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 19, 2026
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`_
@ThomasK33 ThomasK33 force-pushed the agent-control-db8w branch from 0aaa360 to ee320c7 Compare May 19, 2026 07:12
@ThomasK33
Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

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".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant