Skip to content

Commit 77a56cf

Browse files
committed
Merge branch 'main' into codex/session-title-fix (21ed173)
Adopt upstream's own fix for the adaptive-evidence footer assertion (normalized separator comparison, supersedes our MAIN_SEPARATOR variant). Budget re-based to measured 676495: +9 from the session-title regression tests, the rest absorbs the pre-release repair batch 21ed173 which landed without a budget update. Session-title tests still pass.
2 parents 65192f2 + 21ed173 commit 77a56cf

27 files changed

Lines changed: 427 additions & 142 deletions

CHANGELOG.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,39 @@ File edits, terminal width, and Windows installation.
140140
- Acceptance-level Gherkin coverage locking the existing user-command
141141
precedence, alias shadowing, fallback, and invalid-command error contract
142142
(PR #4992).
143+
- Agent Plugins v1.0.0: consume, publish, and slugify packaged sub-agent
144+
briefs, with an install/update/uninstall on-ramp in the TUI (PR #5182). A
145+
plugin bundles a prompt, posture, and routing as one shareable artifact;
146+
on-disk migration of the older `plugin.toml` scaffold is deliberately out
147+
of scope for this train.
148+
- `send_later`: a model-callable one-shot delayed continuation tool, so the
149+
model can schedule a single future nudge without an operator-approved
150+
durable automation (PR #5138).
151+
- `/advisor`: an opt-in background advisor watcher for live turns (PR #5139).
152+
- Notification quiet mode with per-category switches and action-first copy
153+
(PR #5066).
154+
- Automation scheduling forms — one-shot `ONCE`, five-field cron, and honest
155+
watcher modes — created through the approval-gated `automation` tool
156+
(PR #5183).
157+
- Sub-agent `resume_from` continuation chains (PR #5142), child-result
158+
diff-tainting when a claimed diff is not visible to git, per-turn usage
159+
receipts on the exec stream-json stream, and spawn receipts that report
160+
the model each sub-agent actually ran on.
161+
- Transport resilience: sub-agent exec transport retries with a 600 s
162+
default (PR #5210), SSE header stalls retryable instead of fatal, and
163+
headless turn resume after mid-stream network drops with an `EX_TEMPFAIL`
164+
exit.
165+
- Session durability and control: a deterministic compaction continuation
166+
contract (PR #5064), persisting interrupted output (PR #5206), stop-word
167+
cancellation (PR #5207), token-counter refresh (PR #5204), deny-by-default
168+
approval cards (PR #5090), and the Operate completion gate (PR #5067).
169+
- zh-Hant promoted to a full shipped locale with complete `en.json` parity
170+
(PR #5143).
171+
- A persistent update-available chip in the header, with the startup update
172+
check throttled and naming the right command.
173+
- RLM static intent extraction for code blocks (`rlm_block_intent.rs`)
174+
landed as groundwork for a future code-mode approval flow; it is not yet
175+
wired into the turn pipeline and ships dormant by design.
143176

144177
### Changed
145178

@@ -186,6 +219,20 @@ File edits, terminal width, and Windows installation.
186219
futures-util to 0.3.33, libc to 0.2.189, actions/stale to 11.0.0, and
187220
docker/login-action to 4.5.2. The locked graph also includes the
188221
event-listener 5.4.2 fix for RUSTSEC-2026-0221.
222+
- The progress surface now speaks plainly everywhere: the last user-visible
223+
"Work update is pending" notices say "To-do list", the tool constructor and
224+
the docs name `todo_write` as the single canonical progress tool, and
225+
`work_update`, `TodoWrite`, and `todo` stay registered as hidden
226+
compatibility aliases so saved transcripts keep replaying.
227+
- Sub-agent and `agents/wait` waits stay short by default and by cap:
228+
blocking waits default to 30 s and refuse to block past 120 s, because a
229+
blocked wait deafens the session to typed input and settled children
230+
already report back as `<codewhale:subagent.done>` sentinels.
231+
- `Bash` `action=wait` honors `timeout_secs` (seconds) and bare `timeout`
232+
(milliseconds) alongside canonical `timeout_ms`, and `block` as an alias
233+
for `wait`, so a habit formed on other wait tools gets the duration it
234+
asked for instead of silently falling back to the 30 s default; the result
235+
metadata reports the real `wait_timeout_ms` applied.
189236

190237
### Fixed
191238

@@ -362,6 +409,21 @@ File edits, terminal width, and Windows installation.
362409
- Transcript wheel scrolling under iTerm2: xterm alternate-scroll (DECSET
363410
1007) now stays off while mouse capture is active, so wheel events arrive as
364411
mouse events instead of being converted into arrow keys (#5223, PR #5234).
412+
- A stalled model stream no longer ends the turn as `Completed` over a
413+
frozen reasoning block: a mid-stream chunk-timeout now counts toward the
414+
stream-error budget, so a stall with nothing streamed retries the request
415+
transparently, and a stall that exhausts the retry budget fails the turn
416+
with the real reason instead of reporting success.
417+
- A finished background shell task now wakes the engine even when no goal is
418+
active: the idle loop starts an ordinary runtime turn so the completion
419+
reaches the model immediately instead of sitting unclaimed until the user
420+
types (a dead provider route claims the completion once and reports where
421+
the output lives instead of re-arming the same error every tick).
422+
- Sub-agent final reports that exceed the summary budget are now spilled to
423+
a session artifact, and the truncation footer names the
424+
`retrieve_tool_result` ref for the elided middle instead of telling the
425+
model the bytes are unrecoverable; write failures degrade to the honest
426+
no-ref footer.
365427

366428
### Removed
367429

crates/tui/CHANGELOG.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,39 @@ File edits, terminal width, and Windows installation.
140140
- Acceptance-level Gherkin coverage locking the existing user-command
141141
precedence, alias shadowing, fallback, and invalid-command error contract
142142
(PR #4992).
143+
- Agent Plugins v1.0.0: consume, publish, and slugify packaged sub-agent
144+
briefs, with an install/update/uninstall on-ramp in the TUI (PR #5182). A
145+
plugin bundles a prompt, posture, and routing as one shareable artifact;
146+
on-disk migration of the older `plugin.toml` scaffold is deliberately out
147+
of scope for this train.
148+
- `send_later`: a model-callable one-shot delayed continuation tool, so the
149+
model can schedule a single future nudge without an operator-approved
150+
durable automation (PR #5138).
151+
- `/advisor`: an opt-in background advisor watcher for live turns (PR #5139).
152+
- Notification quiet mode with per-category switches and action-first copy
153+
(PR #5066).
154+
- Automation scheduling forms — one-shot `ONCE`, five-field cron, and honest
155+
watcher modes — created through the approval-gated `automation` tool
156+
(PR #5183).
157+
- Sub-agent `resume_from` continuation chains (PR #5142), child-result
158+
diff-tainting when a claimed diff is not visible to git, per-turn usage
159+
receipts on the exec stream-json stream, and spawn receipts that report
160+
the model each sub-agent actually ran on.
161+
- Transport resilience: sub-agent exec transport retries with a 600 s
162+
default (PR #5210), SSE header stalls retryable instead of fatal, and
163+
headless turn resume after mid-stream network drops with an `EX_TEMPFAIL`
164+
exit.
165+
- Session durability and control: a deterministic compaction continuation
166+
contract (PR #5064), persisting interrupted output (PR #5206), stop-word
167+
cancellation (PR #5207), token-counter refresh (PR #5204), deny-by-default
168+
approval cards (PR #5090), and the Operate completion gate (PR #5067).
169+
- zh-Hant promoted to a full shipped locale with complete `en.json` parity
170+
(PR #5143).
171+
- A persistent update-available chip in the header, with the startup update
172+
check throttled and naming the right command.
173+
- RLM static intent extraction for code blocks (`rlm_block_intent.rs`)
174+
landed as groundwork for a future code-mode approval flow; it is not yet
175+
wired into the turn pipeline and ships dormant by design.
143176

144177
### Changed
145178

@@ -186,6 +219,20 @@ File edits, terminal width, and Windows installation.
186219
futures-util to 0.3.33, libc to 0.2.189, actions/stale to 11.0.0, and
187220
docker/login-action to 4.5.2. The locked graph also includes the
188221
event-listener 5.4.2 fix for RUSTSEC-2026-0221.
222+
- The progress surface now speaks plainly everywhere: the last user-visible
223+
"Work update is pending" notices say "To-do list", the tool constructor and
224+
the docs name `todo_write` as the single canonical progress tool, and
225+
`work_update`, `TodoWrite`, and `todo` stay registered as hidden
226+
compatibility aliases so saved transcripts keep replaying.
227+
- Sub-agent and `agents/wait` waits stay short by default and by cap:
228+
blocking waits default to 30 s and refuse to block past 120 s, because a
229+
blocked wait deafens the session to typed input and settled children
230+
already report back as `<codewhale:subagent.done>` sentinels.
231+
- `Bash` `action=wait` honors `timeout_secs` (seconds) and bare `timeout`
232+
(milliseconds) alongside canonical `timeout_ms`, and `block` as an alias
233+
for `wait`, so a habit formed on other wait tools gets the duration it
234+
asked for instead of silently falling back to the 30 s default; the result
235+
metadata reports the real `wait_timeout_ms` applied.
189236

190237
### Fixed
191238

@@ -362,6 +409,21 @@ File edits, terminal width, and Windows installation.
362409
- Transcript wheel scrolling under iTerm2: xterm alternate-scroll (DECSET
363410
1007) now stays off while mouse capture is active, so wheel events arrive as
364411
mouse events instead of being converted into arrow keys (#5223, PR #5234).
412+
- A stalled model stream no longer ends the turn as `Completed` over a
413+
frozen reasoning block: a mid-stream chunk-timeout now counts toward the
414+
stream-error budget, so a stall with nothing streamed retries the request
415+
transparently, and a stall that exhausts the retry budget fails the turn
416+
with the real reason instead of reporting success.
417+
- A finished background shell task now wakes the engine even when no goal is
418+
active: the idle loop starts an ordinary runtime turn so the completion
419+
reaches the model immediately instead of sitting unclaimed until the user
420+
types (a dead provider route claims the completion once and reports where
421+
the output lives instead of re-arming the same error every tick).
422+
- Sub-agent final reports that exceed the summary budget are now spilled to
423+
a session artifact, and the truncation footer names the
424+
`retrieve_tool_result` ref for the elided middle instead of telling the
425+
model the bytes are unrecoverable; write failures degrade to the honest
426+
no-ref footer.
365427

366428
### Removed
367429

crates/tui/src/commands/groups/core/voice.rs

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -480,21 +480,21 @@ async fn transcribe_local_whisper(audio_samples: &[i16]) -> Result<String, Strin
480480
.arg("auto")
481481
.arg("--output-txt")
482482
.output();
483-
if let Ok(out) = output {
484-
if out.status.success() {
485-
let txt = String::from_utf8_lossy(&out.stdout).trim().to_string();
483+
if let Ok(out) = output
484+
&& out.status.success()
485+
{
486+
let txt = String::from_utf8_lossy(&out.stdout).trim().to_string();
487+
let _ = std::fs::remove_file(&tmp);
488+
if !txt.is_empty() {
489+
return Ok(txt);
490+
}
491+
// Some builds write to .txt sidecar
492+
let sidecar = tmp.with_extension("txt");
493+
if let Ok(s) = std::fs::read_to_string(&sidecar) {
494+
let _ = std::fs::remove_file(&sidecar);
486495
let _ = std::fs::remove_file(&tmp);
487-
if !txt.is_empty() {
488-
return Ok(txt);
489-
}
490-
// Some builds write to .txt sidecar
491-
let sidecar = tmp.with_extension("txt");
492-
if let Ok(s) = std::fs::read_to_string(&sidecar) {
493-
let _ = std::fs::remove_file(&sidecar);
494-
let _ = std::fs::remove_file(&tmp);
495-
if !s.trim().is_empty() {
496-
return Ok(s.trim().to_string());
497-
}
496+
if !s.trim().is_empty() {
497+
return Ok(s.trim().to_string());
498498
}
499499
}
500500
}

crates/tui/src/commands/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -703,15 +703,15 @@ mod tests {
703703

704704
let mut context = crate::tools::spec::ToolContext::new(app.workspace.clone());
705705
context.runtime.work = Some(work);
706-
crate::tools::todo::TodoWriteTool::work_update(app.todos.clone())
706+
crate::tools::todo::TodoWriteTool::new(app.todos.clone())
707707
.execute(
708708
serde_json::json!({
709709
"todos": [{"content": "relay the staged graph", "status": "in_progress"}]
710710
}),
711711
&context,
712712
)
713713
.await
714-
.expect("graph-backed work_update");
714+
.expect("graph-backed todo_write");
715715

716716
assert!(
717717
app.todos.lock().await.snapshot().is_empty(),

crates/tui/src/core/engine.rs

Lines changed: 76 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,19 +1891,12 @@ impl Engine {
18911891
}
18921892
}
18931893

1894-
/// Whether the idle loop should poll for background shell completion:
1895-
/// only while a goal is active and a background job is running or has
1896-
/// finished without being claimed yet.
1894+
/// Whether the idle loop should poll for background shell completion: a
1895+
/// background job is running or has finished without being claimed yet.
1896+
/// Plain interactive sessions arm exactly like goal sessions — a finished
1897+
/// background task must reach the model without waiting for the user to
1898+
/// type, the same wake an idle sub-agent completion already gets.
18971899
fn idle_shell_wake_armed(&self) -> bool {
1898-
let goal_active = self
1899-
.config
1900-
.goal_state
1901-
.lock()
1902-
.map(|state| state.snapshot().is_active())
1903-
.unwrap_or(false);
1904-
if !goal_active {
1905-
return false;
1906-
}
19071900
self.shell_manager
19081901
.lock()
19091902
.map(|manager| manager.may_have_undelivered_completion())
@@ -1918,18 +1911,82 @@ impl Engine {
19181911
.unwrap_or(false)
19191912
}
19201913

1921-
/// An idle-engine wake for finished background shell work: queue a goal
1922-
/// continuation. The evidence itself is claimed by the boundary drain in
1923-
/// `handle_send_message`, so the continuation turn reads the completion
1924-
/// payload the same way a user-initiated turn would.
1914+
/// An idle-engine wake for finished background shell work. With an active
1915+
/// goal this queues a goal continuation; without one it starts an ordinary
1916+
/// runtime turn so the completion reaches the model immediately instead of
1917+
/// sitting unclaimed until the user types. Either way the evidence itself
1918+
/// is claimed by the boundary drain in `handle_send_message`, so the
1919+
/// follow-up turn reads the completion payload the same way a
1920+
/// user-initiated turn would.
19251921
async fn handle_idle_shell_completion_wake(&mut self) {
1922+
let goal_active = self
1923+
.config
1924+
.goal_state
1925+
.lock()
1926+
.map(|state| state.snapshot().is_active())
1927+
.unwrap_or(false);
1928+
if goal_active {
1929+
let _ = self
1930+
.tx_event
1931+
.send(Event::status(
1932+
"Background shell work finished; continuing the active goal".to_string(),
1933+
))
1934+
.await;
1935+
self.schedule_goal_continuation(Vec::new());
1936+
return;
1937+
}
1938+
let route = match self.current_runtime_route() {
1939+
Ok(route) => route,
1940+
Err(err) => {
1941+
// No route, no turn. Claim the once-only completion now so a
1942+
// dead route cannot re-arm the wake into the same error every
1943+
// poll tick; the user sees what finished and where the output
1944+
// lives, and the next healthy turn proceeds normally.
1945+
let finished = self
1946+
.shell_manager
1947+
.lock()
1948+
.map(|mut manager| manager.drain_finished_jobs_with_evidence().len())
1949+
.unwrap_or(0);
1950+
let _ = self
1951+
.tx_event
1952+
.send(Event::error(ErrorEnvelope::fatal_auth(format!(
1953+
"{finished} background shell task(s) finished, but the turn cannot resume because the provider route is no longer valid: {err}. Their output stays available via /jobs."
1954+
))))
1955+
.await;
1956+
return;
1957+
}
1958+
};
19261959
let _ = self
19271960
.tx_event
19281961
.send(Event::status(
1929-
"Background shell work finished; continuing the active goal".to_string(),
1962+
"Background shell work finished; resuming the turn".to_string(),
19301963
))
19311964
.await;
1932-
self.schedule_goal_continuation(Vec::new());
1965+
let _ = self
1966+
.handle_send_message(
1967+
"[runtime] A background shell task finished; its completion evidence follows."
1968+
.to_string(),
1969+
self.current_mode,
1970+
route,
1971+
self.config.compaction.clone(),
1972+
self.config.goal_objective.clone(),
1973+
self.config.goal_token_budget,
1974+
self.config.goal_status,
1975+
self.session.reasoning_effort.clone(),
1976+
self.session.reasoning_effort_auto,
1977+
self.session.auto_model,
1978+
self.session.allow_shell,
1979+
self.session.trust_mode,
1980+
self.session.auto_approve,
1981+
self.session.approval_mode,
1982+
self.config.translation_enabled,
1983+
self.config.allowed_tools.clone(),
1984+
Vec::new(),
1985+
self.config.hook_executor.clone(),
1986+
self.config.verbosity.clone(),
1987+
UserInputProvenance::Runtime,
1988+
)
1989+
.await;
19331990
}
19341991

19351992
/// Run the engine event loop
@@ -3544,7 +3601,7 @@ impl Engine {
35443601
Some(SubAgentForkContext {
35453602
messages: self.messages_with_turn_metadata(),
35463603
structured_state_block: state.to_system_block(),
3547-
// Resolve at spawn time so a work update earlier in this turn
3604+
// Resolve at spawn time so a todo_write earlier in this turn
35483605
// reaches the child rather than freezing turn-start state.
35493606
work_source: Some(self.work_state_source()),
35503607
})

crates/tui/src/core/engine/tests.rs

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10900,10 +10900,10 @@ async fn run_graph_backed_work_update(
1090010900
use crate::tools::spec::ToolSpec as _;
1090110901
let mut context = crate::tools::spec::ToolContext::new(std::env::temp_dir());
1090210902
context.runtime.work = Some(work.clone());
10903-
crate::tools::todo::TodoWriteTool::work_update(todos.clone())
10903+
crate::tools::todo::TodoWriteTool::new(todos.clone())
1090410904
.execute(json!({ "todos": items }), &context)
1090510905
.await
10906-
.expect("graph-backed work_update");
10906+
.expect("graph-backed todo_write");
1090710907
}
1090810908

1090910909
/// #3983 runtime regression: a real graph-backed `work_update` stages the new
@@ -15737,13 +15737,16 @@ async fn idle_engine_wakes_for_finished_background_shell_only_while_goal_active(
1573715737
tokio::time::sleep(Duration::from_millis(25)).await;
1573815738
}
1573915739

15740-
// No active goal: the wake stays disarmed and the idle receive keeps
15741-
// waiting — completions belong to the next user-initiated turn.
15742-
let disarmed =
15743-
tokio::time::timeout(Duration::from_millis(300), engine.next_run_input(false)).await;
15740+
// No active goal: the wake still arms — a finished background task must
15741+
// reach the model without waiting for the user to type, the same wake an
15742+
// idle sub-agent completion already gets.
15743+
let input = tokio::time::timeout(Duration::from_secs(10), engine.next_run_input(false))
15744+
.await
15745+
.expect("idle engine must wake for finished background shell work even without a goal")
15746+
.expect("engine input");
1574415747
assert!(
15745-
disarmed.is_err(),
15746-
"without an active goal the engine must not start turns for shell completions"
15748+
matches!(input, EngineRunInput::ShellCompletionWake),
15749+
"wake input expected without an active goal"
1574715750
);
1574815751

1574915752
engine

0 commit comments

Comments
 (0)