Skip to content

(MOT-3867) feat(harness): pause and ask to continue at max_turns#304

Open
ytallo wants to merge 4 commits into
mainfrom
harness/ask-continue-on-max-turns
Open

(MOT-3867) feat(harness): pause and ask to continue at max_turns#304
ytallo wants to merge 4 commits into
mainfrom
harness/ask-continue-on-max-turns

Conversation

@ytallo

@ytallo ytallo commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

Fixes MOT-3867

Summary

When a top-level turn hits max_turns, the harness now pauses and asks whether to continue instead of silently ending. The turn parks in a non-terminal waiting state with a visible prompt; the user resumes it (with a fresh budget) via a Continue button in the console — a parked turn blocks the chat input, so a button is the affordance — or by replying. Autonomous turns (spawn children, parentless/CLI spawns, reactive harness::react turns) auto-end: there is no interactive user to answer.

This also fixes a latent bug: harness notice entries were written with empty content, so the console dropped them — the existing max_turns notice was invisible.

Behavior

  • A turn that reaches the cap appends a visible notice ("Reached the max-turns limit (N). Reply to continue, or stop here.") and parks (status: waiting).
  • Clicking Continue (or replying) resumes the same turn with max_turns += default_max_turns; the turn count keeps climbing.
  • harness::stop cancels a parked turn cleanly.

Changes

Harness

  • The max_turns guard parks interactive top-level turns (reusing the non-terminal AwaitingFunctions state + an awaiting_continue flag on TurnRecord) with a visible notice and a waiting session status, instead of finalizing.
  • An interactive flag on TurnRecord marks harness::send turns as the only ones that park. Autonomous turns — harness::spawn children, parentless/CLI spawns, and harness::react reactions — auto-end at the cap, so nothing parks waiting for a user who isn't there. Wake-injected turns (subscription notifications) inherit the session's interactivity.
  • New public function harness::continue ({session_id, turn_id?}{continuing}): resume a parked turn with a fresh budget, under the session lock; a no-op when there is nothing to continue. Added to the wire-surface catalog (+ golden).
  • harness::send also resumes a parked turn (reply-to-continue).
  • harness::stop enqueues a step so a parked turn observes the abort and finalizes cancelled.
  • append_custom gains a display argument so the notice renders.
  • New config knob ask_to_continue_on_max_turns (default true).
  • Park-path failures are no longer silent: if the notice append or the waiting status update fails, the harness logs a warning naming the consequence (the console can't offer continue/stop).

Waiting status (session-manager + console)

  • SessionStatus::Waiting variant (golden schemas regenerated). session-manager bumps to 1.1.0 and the harness manifest now requires session-manager: "^1.1.0": an older session-manager rejects set-status("waiting") — silently, from the console's point of view — and also skips persisted waiting records as malformed on store replay, so the continue option never appears. This mismatch is exactly how the feature "didn't work" in earlier live testing.
  • Console: continue + stop buttons shown when the session is waiting (wired to harness::continue); the composer input stays usable while parked ("press continue, or type to steer…"); waiting added to the status unions with a sidebar dot and a dock signal.

Testing

  • harness: cargo test — 175 passing.
  • session-manager: cargo test — 66 passing (incl. a waiting set-status scenario); goldens regenerated.
  • console/web: tsc typecheck and vitest — 884 passing.
  • Verified end-to-end against a running engine (fresh validation after the rebase onto current main):
    • park at the cap → session waiting, visible notice;
    • resume via harness::continue, via the console Continue button, and via reply — all complete with a real model turn;
    • stop-while-parked → cancelled;
    • parentless harness::spawn at the cap → auto-ends (does not park);
    • negative check: pairing the new harness with a pre-1.1.0 session-manager reproduces the "no continue option" failure (status update rejected and swallowed), which the version requirement + warning now prevent/surface.

@vercel

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
workers Ready Ready Preview, Comment Jul 3, 2026 8:28pm
workers-tech-spec Ready Ready Preview, Comment Jul 3, 2026 8:28pm

Request Review

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A new waiting session status is introduced to park an interactive top-level turn when max_turns is reached instead of auto-finalizing it. The harness turn loop conditionally parks the turn, the send handler gains a resume path, the stop handler unparks before aborting, sub-agent turns are marked non-interactive, and the web console surfaces the waiting state via a status dot and dock attention signal.

Changes

Waiting Status for Interactive max_turns Pause

Layer / File(s) Summary
TurnRecord fields, WorkerConfig knob, and status type extensions
harness/src/types/turn.rs, harness/src/config.rs, console/web/src/types/chat.ts, console/web/src/lib/sessions/types.ts, session-manager/src/types.rs
TurnRecord gains awaiting_continue and interactive boolean fields (serde-defaulted to false). WorkerConfig gains ask_to_continue_on_max_turns: bool (default true). ConversationStatus and SessionStatus union types are extended with 'waiting'. SessionStatus::Waiting receives doc comments.
append_custom display parameter and call-site updates
harness/src/clients/session.rs, harness/src/turn_loop.rs
SessionClient::append_custom gains display: Option<&str> and populates the payload's content/display fields accordingly. All existing call sites in turn_loop (run_step, finalize_failed, assemble_context) are updated to pass trailing None.
park_awaiting_continue helper and run_step conditional parking
harness/src/turn_loop.rs
Adds park_awaiting_continue which writes a deterministic notice entry, sets TurnStatus::AwaitingFunctions, marks record.awaiting_continue = true, and returns without enqueuing. run_step's max_turns guard calls this helper when the turn is interactive, top-level, and the config knob is enabled; otherwise it follows the existing finalize path.
send::start interactive flag and seed_or_merge resume branch
harness/src/functions/send.rs
send::start gains an interactive: bool parameter threaded through seed_or_merge and seed_new. seed_or_merge adds a branch for turns with awaiting_continue set: under a per-session lock it either calls resume_awaiting_continue (clears flag, extends budget, increments step, sets Running, re-enqueues) or falls back to seed_new. A unit test for extended_max_turns saturating behavior is added.
Stop handler unparking and subagent/run non-interactive marking
harness/src/functions/stop.rs, harness/src/subagent.rs, harness/src/functions/run.rs
stop::handle detects awaiting_continue, clears the parked state, increments the step, and re-enqueues before persisting so the loop sees the abort flag. Subagent child TurnRecords are explicitly set awaiting_continue: false, interactive: false. run::handle passes false to send::start.
SessionStatus schema expansion and BDD scenario
session-manager/tests/golden/schemas/session.*.json, session-manager/tests/features/status.feature
All golden JSON schemas update SessionStatus from a flat enum to a oneOf that adds a waiting variant with a description. A new BDD scenario verifies the working → waiting transition is persisted and the session::status-changed event contains the correct status/previous_status.
Frontend waiting status UI
console/web/src/components/sidebar/ConversationRow.tsx, console/web/src/lib/chat-activity.ts
ConversationRow renders an accent StatusDot with statusReason title for the waiting branch. getDockSignal returns 'attention' early when active.status === 'waiting'.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant send_handle as send::handle
  participant turn_loop as run_step
  participant TurnStore
  participant SessionManager
  participant WebConsole

  Note over turn_loop: Turn reaches max_turns (interactive + top-level)
  turn_loop->>TurnStore: park_awaiting_continue (set awaiting_continue=true, AwaitingFunctions)
  turn_loop->>SessionManager: append notice entry (max_turns_await_continue)
  SessionManager-->>WebConsole: status-changed → waiting
  WebConsole-->>User: StatusDot (accent) + dock attention signal

  User->>send_handle: send new message (resume intent)
  send_handle->>TurnStore: seed_or_merge (interactive=true)
  TurnStore-->>send_handle: existing turn with awaiting_continue=true
  send_handle->>TurnStore: acquire lock + re-read
  send_handle->>TurnStore: resume_awaiting_continue (extend budget, set Running)
  send_handle->>turn_loop: enqueue_step
  turn_loop-->>SessionManager: status-changed → working
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • andersonleal

Poem

🐇 Hop, hop — the turns run out, the rabbit needs a rest,
A waiting dot glows accent-bright upon the sidebar's crest.
"Continue?" asks the harness, lock acquired with care,
The budget grows, the step resumes — no session left in air!
From park_awaiting_continue to the schema's oneOf dance,
Every golden JSON file got a brand new waiting chance! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main behavior change: pausing at max_turns and asking the user to continue.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch harness/ask-continue-on-max-turns

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Jun 20, 2026

Copy link
Copy Markdown
Contributor

skill-check — worker

0 verified, 32 skipped (no docs/).

Layer Result
structure
vale
ai
render

Four for four. Nicely done.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@harness/src/functions/send.rs`:
- Around line 296-303: The issue is that the record state mutations
(awaiting_continue, status, step increment) are persisted via
crate::state::put_turn before the crate::turn_loop::enqueue_step call, so if
enqueue_step fails, the turn record is left in a persisted but inconsistent
state with no queued step to process. Reorder the operations so that
crate::turn_loop::enqueue_step is called before crate::state::put_turn, ensuring
that if the enqueue operation fails, the record state changes are never
persisted and the turn remains in a valid terminal state.

In `@harness/src/functions/stop.rs`:
- Around line 62-72: The issue is that the state changes (awaiting_continue =
false and status = Running) are persisted via put_turn before the enqueue_step
call, so if enqueue_step fails and the function is retried, the parked flag will
be false and the enqueue won't be retried, causing the turn to stall. Reorder
the code so that the put_turn call happens after the enqueue_step call succeeds.
This ensures that if enqueue_step fails, the state remains unchanged and the
retry will still see parked as true and attempt the enqueue again.

In `@session-manager/tests/features/status.feature`:
- Around line 146-153: The test scenario at lines 146-153 passes a reason field
with a non-error status (waiting) but does not verify that status_reason is
cleared/ignored in the response. After the session::get call, add an assertion
that meta.status_reason is absent or null to ensure the contract is protected
that status_reason should only be present for error statuses. Additionally,
optionally add an assertion on the delivery event to ui::status to verify that
status_reason is not included in the event payload either.

In `@session-manager/tests/golden/schemas/session.set-status.json`:
- Around line 8-25: The schema now includes "waiting" as a valid status option
in the enum (alongside idle, working, done, and error), but there is a stale
description somewhere around line 45 of the session.set-status.json file that
only documents the original four statuses. Update the description field to
include "waiting" in the list of valid target statuses so the documentation
accurately reflects all possible enum values defined in the schema.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: db5b192c-8a12-4d3e-96ff-2388ebbcfde4

📥 Commits

Reviewing files that changed from the base of the PR and between 5179186 and ed9fd33.

📒 Files selected for processing (24)
  • console/web/src/components/sidebar/ConversationRow.tsx
  • console/web/src/lib/chat-activity.ts
  • console/web/src/lib/sessions/types.ts
  • console/web/src/types/chat.ts
  • harness/src/clients/session.rs
  • harness/src/config.rs
  • harness/src/functions/run.rs
  • harness/src/functions/send.rs
  • harness/src/functions/stop.rs
  • harness/src/subagent.rs
  • harness/src/turn_loop.rs
  • harness/src/types/turn.rs
  • session-manager/src/types.rs
  • session-manager/tests/features/status.feature
  • session-manager/tests/golden/schemas/session.create.json
  • session-manager/tests/golden/schemas/session.ensure.json
  • session-manager/tests/golden/schemas/session.fork.json
  • session-manager/tests/golden/schemas/session.get.json
  • session-manager/tests/golden/schemas/session.list.json
  • session-manager/tests/golden/schemas/session.set-meta.json
  • session-manager/tests/golden/schemas/session.set-status.json
  • session-manager/tests/golden/schemas/session.store.get-meta.json
  • session-manager/tests/golden/schemas/session.store.list-metas.json
  • session-manager/tests/golden/schemas/session.store.put-meta.json

Comment on lines +296 to +303
record.awaiting_continue = false;
record.options.max_turns = extended_max_turns(record.options.max_turns, cfg.default_max_turns);
record.step += 1;
record.status = TurnStatus::Running;
record.updated_at = AgentMessage::now_ms();
crate::state::put_turn(&deps.iii, record, cfg.session_timeout_ms).await?;
crate::turn_loop::enqueue_step(&deps.iii, &record.session_id, &record.turn_id, record.step)
.await?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rollback parked-state mutations if resume enqueue fails.

Line 301 commits awaiting_continue = false + status = Running + step/budget bump before Line 302 enqueue. If enqueue fails, the turn can get stuck non-terminal with no queued step to continue or cancel it.

Suggested fix
 async fn resume_awaiting_continue(
     deps: &Deps,
     cfg: &WorkerConfig,
     record: &mut TurnRecord,
 ) -> Result<(), HarnessError> {
+    let prev_step = record.step;
+    let prev_max_turns = record.options.max_turns;
     record.awaiting_continue = false;
     record.options.max_turns = extended_max_turns(record.options.max_turns, cfg.default_max_turns);
     record.step += 1;
     record.status = TurnStatus::Running;
     record.updated_at = AgentMessage::now_ms();
     crate::state::put_turn(&deps.iii, record, cfg.session_timeout_ms).await?;
-    crate::turn_loop::enqueue_step(&deps.iii, &record.session_id, &record.turn_id, record.step)
-        .await?;
+    if let Err(err) =
+        crate::turn_loop::enqueue_step(&deps.iii, &record.session_id, &record.turn_id, record.step)
+            .await
+    {
+        // Keep the turn resumable on retry if enqueue fails.
+        record.awaiting_continue = true;
+        record.options.max_turns = prev_max_turns;
+        record.step = prev_step;
+        record.status = TurnStatus::AwaitingFunctions;
+        record.updated_at = AgentMessage::now_ms();
+        let _ = crate::state::put_turn(&deps.iii, record, cfg.session_timeout_ms).await;
+        return Err(err);
+    }
     Ok(())
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
record.awaiting_continue = false;
record.options.max_turns = extended_max_turns(record.options.max_turns, cfg.default_max_turns);
record.step += 1;
record.status = TurnStatus::Running;
record.updated_at = AgentMessage::now_ms();
crate::state::put_turn(&deps.iii, record, cfg.session_timeout_ms).await?;
crate::turn_loop::enqueue_step(&deps.iii, &record.session_id, &record.turn_id, record.step)
.await?;
async fn resume_awaiting_continue(
deps: &Deps,
cfg: &WorkerConfig,
record: &mut TurnRecord,
) -> Result<(), HarnessError> {
let prev_step = record.step;
let prev_max_turns = record.options.max_turns;
record.awaiting_continue = false;
record.options.max_turns = extended_max_turns(record.options.max_turns, cfg.default_max_turns);
record.step += 1;
record.status = TurnStatus::Running;
record.updated_at = AgentMessage::now_ms();
crate::state::put_turn(&deps.iii, record, cfg.session_timeout_ms).await?;
if let Err(err) =
crate::turn_loop::enqueue_step(&deps.iii, &record.session_id, &record.turn_id, record.step)
.await
{
// Keep the turn resumable on retry if enqueue fails.
record.awaiting_continue = true;
record.options.max_turns = prev_max_turns;
record.step = prev_step;
record.status = TurnStatus::AwaitingFunctions;
record.updated_at = AgentMessage::now_ms();
let _ = crate::state::put_turn(&deps.iii, record, cfg.session_timeout_ms).await;
return Err(err);
}
Ok(())
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/src/functions/send.rs` around lines 296 - 303, The issue is that the
record state mutations (awaiting_continue, status, step increment) are persisted
via crate::state::put_turn before the crate::turn_loop::enqueue_step call, so if
enqueue_step fails, the turn record is left in a persisted but inconsistent
state with no queued step to process. Reorder the operations so that
crate::turn_loop::enqueue_step is called before crate::state::put_turn, ensuring
that if the enqueue operation fails, the record state changes are never
persisted and the turn remains in a valid terminal state.

Comment on lines +62 to +72
let parked = record.awaiting_continue;
if parked {
record.awaiting_continue = false;
record.step += 1;
record.status = crate::types::turn::TurnStatus::Running;
}
crate::state::put_turn(&deps.iii, &record, cfg.session_timeout_ms).await?;
if parked {
crate::turn_loop::enqueue_step(&deps.iii, &record.session_id, &record.turn_id, record.step)
.await?;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Preserve retryability when stop enqueue fails for parked turns.

After Line 68 persists awaiting_continue = false, an enqueue failure at Line 70 returns an error but leaves the turn in Running with no queued step. A retry then sees parked = false and won’t enqueue, so cancellation can stall indefinitely.

Suggested fix
     let parked = record.awaiting_continue;
+    let prev_step = record.step;
     if parked {
         record.awaiting_continue = false;
         record.step += 1;
         record.status = crate::types::turn::TurnStatus::Running;
     }
     crate::state::put_turn(&deps.iii, &record, cfg.session_timeout_ms).await?;
     if parked {
-        crate::turn_loop::enqueue_step(&deps.iii, &record.session_id, &record.turn_id, record.step)
-            .await?;
+        if let Err(err) =
+            crate::turn_loop::enqueue_step(&deps.iii, &record.session_id, &record.turn_id, record.step)
+                .await
+        {
+            // Restore parked state so retry can enqueue again.
+            record.awaiting_continue = true;
+            record.step = prev_step;
+            record.status = crate::types::turn::TurnStatus::AwaitingFunctions;
+            record.updated_at = crate::types::message::AgentMessage::now_ms();
+            let _ = crate::state::put_turn(&deps.iii, &record, cfg.session_timeout_ms).await;
+            return Err(err);
+        }
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let parked = record.awaiting_continue;
if parked {
record.awaiting_continue = false;
record.step += 1;
record.status = crate::types::turn::TurnStatus::Running;
}
crate::state::put_turn(&deps.iii, &record, cfg.session_timeout_ms).await?;
if parked {
crate::turn_loop::enqueue_step(&deps.iii, &record.session_id, &record.turn_id, record.step)
.await?;
}
let parked = record.awaiting_continue;
let prev_step = record.step;
if parked {
record.awaiting_continue = false;
record.step += 1;
record.status = crate::types::turn::TurnStatus::Running;
}
crate::state::put_turn(&deps.iii, &record, cfg.session_timeout_ms).await?;
if parked {
if let Err(err) =
crate::turn_loop::enqueue_step(&deps.iii, &record.session_id, &record.turn_id, record.step)
.await
{
// Restore parked state so retry can enqueue again.
record.awaiting_continue = true;
record.step = prev_step;
record.status = crate::types::turn::TurnStatus::AwaitingFunctions;
record.updated_at = crate::types::message::AgentMessage::now_ms();
let _ = crate::state::put_turn(&deps.iii, &record, cfg.session_timeout_ms).await;
return Err(err);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@harness/src/functions/stop.rs` around lines 62 - 72, The issue is that the
state changes (awaiting_continue = false and status = Running) are persisted via
put_turn before the enqueue_step call, so if enqueue_step fails and the function
is retried, the parked flag will be false and the enqueue won't be retried,
causing the turn to stall. Reorder the code so that the put_turn call happens
after the enqueue_step call succeeds. This ensures that if enqueue_step fails,
the state remains unchanged and the retry will still see parked as true and
attempt the enqueue again.

Comment on lines +146 to +153
{ "session_id": "s_001", "status": "waiting", "reason": "max_turns" }
"""
And I call "session::get" with:
"""
{ "session_id": "s_001" }
"""
Then the response field "meta.status" is "waiting"
And delivery 1 to "ui::status" has "status" = "waiting"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert status_reason is cleared for waiting transitions.

Line 146 passes a reason with a non-error status, but this scenario doesn’t verify that status_reason is actually cleared/ignored. Please add an assertion after session::get that meta.status_reason is absent (and optionally the event payload too) so this contract stays protected.

Suggested test additions
     Then the response field "meta.status" is "waiting"
+    And the response has no field "meta.status_reason"
     And delivery 1 to "ui::status" has "status" = "waiting"
     And delivery 1 to "ui::status" has "previous_status" = "working"
+    And delivery 1 to "ui::status" has no field "status_reason"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@session-manager/tests/features/status.feature` around lines 146 - 153, The
test scenario at lines 146-153 passes a reason field with a non-error status
(waiting) but does not verify that status_reason is cleared/ignored in the
response. After the session::get call, add an assertion that meta.status_reason
is absent or null to ensure the contract is protected that status_reason should
only be present for error statuses. Additionally, optionally add an assertion on
the delivery event to ui::status to verify that status_reason is not included in
the event payload either.

Comment thread session-manager/tests/golden/schemas/session.set-status.json
ytallo added 4 commits July 3, 2026 16:55
When an interactive top-level turn reaches max_turns, the harness now
parks the turn and asks the user whether to continue instead of silently
ending. Replying resumes the same turn with a fresh budget
(max_turns += default_max_turns; turn_count stays monotonic). Sub-agents
and harness::run turns keep auto-ending (no interactive user to answer).

- Park interactive top-level turns at the cap, reusing the non-terminal
  AwaitingFunctions state plus a new awaiting_continue flag on TurnRecord;
  a new interactive flag distinguishes send (true) from run/spawn (false).
- Resume on the next harness::send under the session lock: extend the
  budget, bump the step, re-enqueue. harness::stop enqueues a step so a
  parked turn observes the abort and finalizes cancelled.
- Fix invisible notices: append_custom now populates the custom message
  display/content, so the console renders the prompt (it dropped entries
  with empty content). The prior max_turns notice never showed.
- Add SessionStatus::Waiting and surface it in the console (status unions,
  sidebar dot, attention dock signal) so a parked turn reads distinctly
  from idle.
- Gate behind config ask_to_continue_on_max_turns (default true).
A turn parked at max_turns is non-terminal, so the console disables the
chat input and the user can't reply to resume. Add an explicit way to
continue:

- New public function harness::continue ({session_id, turn_id?} ->
  {continuing}): resume a turn parked at max_turns with a fresh budget,
  sharing the send resume path, under the session lock. No-op when there
  is nothing to continue (safe to double-click). Added to the wire-surface
  catalog with a golden.
- Console: a "continue" button shown when the session is in the `waiting`
  state, wired through backend.continueTurn -> harness::continue. The
  composer input is also unblocked while parked so the user can type to
  steer instead.
harness::react / notify_agent deliver events by injecting a message and
re-seeding a turn in an existing session. Seed that turn with the prior
turn's interactive flag instead of a hardcoded default: a user-facing chat
keeps pausing at max_turns, while an autonomous (spawned/reactive) session
auto-ends — a parked turn there would wait forever for a user who isn't
watching it.
The console offers the continue/stop choice purely off the parked notice
and the session's `waiting` status. Both writes were fire-and-forget, so a
session-manager too old to know `waiting` rejected the status update
silently and the pause was invisible: the turn parked with no visible way
to resume it. Log a warning on either failure.

The `waiting` status variant is a session-manager API addition: bump it to
1.1.0 and require ^1.1.0 in the harness manifest so a deployment can't pair
this harness with a session-manager that drops the status (and skips
persisted `waiting` records as malformed on replay).
@ytallo
ytallo force-pushed the harness/ask-continue-on-max-turns branch from dbe4655 to 5dbb831 Compare July 3, 2026 20:28
@ytallo ytallo changed the title feat(harness): pause and ask to continue at max_turns (MOT-3867) feat(harness): pause and ask to continue at max_turns Jul 3, 2026
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.

3 participants