mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 01:48:20 -10:00
fix(engine): keep the resume receipt and anchors across compaction resyncs
CI flake: launch_card_pty::launch_recent_click_then_enter_resumes_without_another_mouse_event
(macOS, 4 of 113 recorded runs, last on main 6d148e630) timed out waiting
for "Resumed:" while the footer showed "Make room automatically: on" (older
builds: "Auto-compaction enabled").
The flake was a product bug. Engine::new fills compaction.workspace so
compaction can re-state /anchor files, but App::compaction_config never
carries it. Every host SetCompaction therefore compared unequal, so the
engine acknowledged a switch nobody touched. On resume, that ack arrived
after the "Resumed:" toast and replaced it. The same assignment also reset
workspace to None, so after any resume or model switch compaction stopped
re-stating anchors without saying so.
SetCompaction now keeps the engine's workspace when the host sends none,
and reports a status only when `enabled` actually changes. The launch-card
test is unchanged: it still requires the receipt to be visible on screen.
Tests (local, macOS, nextest --retries 0):
- 20 consecutive runs of launch_recent_click_then_enter..., the extended
unchanged_compaction_config_is_acknowledged_silently, and the new
host_compaction_resync_keeps_the_workspace_and_reports_only_the_switch:
60/60 passed.
- lib filter compaction|anchor|resume: 318 passed, 0 failed.
- cargo fmt --check, clippy -p codewhale-tui --all-targets --all-features
with CI flags: clean.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
5ec06e1576
commit
04de8eb67d
5 files changed
+99
-15
No files matched your search
@@ -43,6 +43,10 @@ quieter, and Fleet runs can be checked before they spend anything.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Resuming a session keeps its "Resumed:" confirmation on screen instead of
|
||||
replacing it with "Make room automatically: on" when nothing was switched,
|
||||
and `/anchor` files are still re-stated after compaction once a session has
|
||||
been resumed or its model changed.
|
||||
- The installation page is generated from `docs/INSTALL.md`, so the website
|
||||
and the guide can no longer disagree; broken anchors and unsafe links fail
|
||||
the build ([#6450](https://github.com/Hmbown/Codewhale/pull/6450)).
|
||||
|
||||
@@ -43,6 +43,10 @@ quieter, and Fleet runs can be checked before they spend anything.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Resuming a session keeps its "Resumed:" confirmation on screen instead of
|
||||
replacing it with "Make room automatically: on" when nothing was switched,
|
||||
and `/anchor` files are still re-stated after compaction once a session has
|
||||
been resumed or its model changed.
|
||||
- The installation page is generated from `docs/INSTALL.md`, so the website
|
||||
and the guide can no longer disagree; broken anchors and unsafe links fail
|
||||
the build ([#6450](https://github.com/Hmbown/Codewhale/pull/6450)).
|
||||
|
||||
@@ -1276,6 +1276,25 @@ enum McpConnectRefresh {
|
||||
Force,
|
||||
}
|
||||
|
||||
/// Apply a host's compaction config to the engine's live one and report
|
||||
/// whether the automatic-compaction switch moved.
|
||||
///
|
||||
/// Hosts build this config from route and model state and never carry the
|
||||
/// workspace root: `Engine::new` fills it in so compaction can re-state the
|
||||
/// user's `/anchor` file. A host resync must not erase it, or every resume
|
||||
/// and model switch would silently stop re-stating anchors.
|
||||
fn apply_host_compaction_config(
|
||||
live: &mut crate::compaction::CompactionConfig,
|
||||
mut incoming: crate::compaction::CompactionConfig,
|
||||
) -> bool {
|
||||
if incoming.workspace.is_none() {
|
||||
incoming.workspace = live.workspace.take();
|
||||
}
|
||||
let switched = live.enabled != incoming.enabled;
|
||||
*live = incoming;
|
||||
switched
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
/// Surface the snapshots-disabled notice a blocking snapshot task parked
|
||||
/// (#5930). Called at turn boundaries; each session gets its own notice.
|
||||
@@ -3188,13 +3207,17 @@ impl Engine {
|
||||
.await;
|
||||
}
|
||||
Op::SetCompaction { config } => {
|
||||
// Hosts resend the compaction config on every route
|
||||
// or model sync. An unchanged config is not news; its
|
||||
// acknowledgement used to overwrite a real error in
|
||||
// the footer (U1).
|
||||
if self.config.compaction != config {
|
||||
let enabled = config.enabled;
|
||||
self.config.compaction = config;
|
||||
// Hosts resend the compaction config on every route,
|
||||
// model or session sync, and those syncs move the
|
||||
// model, window and thresholds without the user
|
||||
// touching the switch. Only the switch is news: an
|
||||
// acknowledgement for anything else used to overwrite
|
||||
// a real error in the footer (U1) and the "Resumed:"
|
||||
// receipt a session restore had just shown.
|
||||
let enabled = config.enabled;
|
||||
let switched =
|
||||
apply_host_compaction_config(&mut self.config.compaction, config);
|
||||
if switched {
|
||||
let _ = self
|
||||
.tx_event
|
||||
.send(Event::status(format!(
|
||||
|
||||
@@ -16545,9 +16545,10 @@ async fn same_turn_fork_carries_the_updated_todo() {
|
||||
);
|
||||
}
|
||||
|
||||
/// U1: hosts resend the compaction config on every model or route sync. An
|
||||
/// unchanged config must not produce a status line, which used to overwrite
|
||||
/// a real error (the missing-key notice) in the footer.
|
||||
/// U1: hosts resend the compaction config on every model, route or session
|
||||
/// sync. A config whose switch did not move must not produce a status line,
|
||||
/// which used to overwrite a real error (the missing-key notice) and the
|
||||
/// "Resumed:" receipt in the footer.
|
||||
#[tokio::test]
|
||||
async fn unchanged_compaction_config_is_acknowledged_silently() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
@@ -16566,7 +16567,18 @@ async fn unchanged_compaction_config_is_acknowledged_silently() {
|
||||
})
|
||||
.await
|
||||
.expect("send unchanged config");
|
||||
let mut changed = current;
|
||||
// A session restore resyncs the model and window with the switch as it
|
||||
// was: applied, but not news.
|
||||
let mut resynced = current.clone();
|
||||
resynced.model = format!("{}-resynced", current.model);
|
||||
resynced.effective_context_window = Some(64_000);
|
||||
handle
|
||||
.send(Op::SetCompaction {
|
||||
config: resynced.clone(),
|
||||
})
|
||||
.await
|
||||
.expect("send resynced config");
|
||||
let mut changed = resynced;
|
||||
changed.enabled = !changed.enabled;
|
||||
let expected = if changed.enabled {
|
||||
"Make room automatically: on"
|
||||
@@ -16590,12 +16602,53 @@ async fn unchanged_compaction_config_is_acknowledged_silently() {
|
||||
};
|
||||
assert_eq!(
|
||||
first_status, expected,
|
||||
"the unchanged config produced no status; only the real change did"
|
||||
"unchanged and resynced configs produced no status; only the switch did"
|
||||
);
|
||||
drop(rx);
|
||||
run.abort();
|
||||
}
|
||||
|
||||
/// A host resync carries no workspace root; the engine's own must survive
|
||||
/// it, or compaction stops re-stating the user's `/anchor` file after every
|
||||
/// resume or model switch. Only a moved switch counts as news.
|
||||
#[test]
|
||||
fn host_compaction_resync_keeps_the_workspace_and_reports_only_the_switch() {
|
||||
let workspace = std::path::PathBuf::from("/work/space");
|
||||
let mut live = crate::compaction::CompactionConfig {
|
||||
enabled: true,
|
||||
workspace: Some(workspace.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
let resync = crate::compaction::CompactionConfig {
|
||||
enabled: true,
|
||||
model: "resynced-model".to_string(),
|
||||
effective_context_window: Some(64_000),
|
||||
workspace: None,
|
||||
..Default::default()
|
||||
};
|
||||
assert!(!apply_host_compaction_config(&mut live, resync));
|
||||
assert_eq!(live.model, "resynced-model", "the resync is applied");
|
||||
assert_eq!(live.effective_context_window, Some(64_000));
|
||||
assert_eq!(live.workspace.as_ref(), Some(&workspace));
|
||||
|
||||
let mut switched_off = live.clone();
|
||||
switched_off.enabled = false;
|
||||
switched_off.workspace = None;
|
||||
assert!(apply_host_compaction_config(&mut live, switched_off));
|
||||
assert!(!live.enabled);
|
||||
assert_eq!(live.workspace.as_ref(), Some(&workspace));
|
||||
|
||||
let elsewhere = std::path::PathBuf::from("/other/root");
|
||||
let mut explicit = live.clone();
|
||||
explicit.workspace = Some(elsewhere.clone());
|
||||
assert!(!apply_host_compaction_config(&mut live, explicit));
|
||||
assert_eq!(
|
||||
live.workspace.as_ref(),
|
||||
Some(&elsewhere),
|
||||
"a host that names a workspace still owns it"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn change_mode_op_updates_current_mode_and_emits_status() {
|
||||
let tmp = tempdir().expect("tempdir");
|
||||
|
||||
@@ -48,6 +48,7 @@ export const CHANGELOG: ChangelogRelease[] = [
|
||||
{
|
||||
"heading": "Fixed",
|
||||
"items": [
|
||||
"Resuming a session keeps its \"Resumed:\" confirmation on screen instead of replacing it with \"Make room automatically: on\" when nothing was switched, and /anchor files are still re-stated after compaction once a session has been resumed or its model changed.",
|
||||
"The installation page is generated from docs/INSTALL.md, so the website and the guide can no longer disagree; broken anchors and unsafe links fail the build (#6450).",
|
||||
"codewhale config set refuses a value of the wrong type for a known setting (a word for an on/off switch, text for a number, a choice outside the list) instead of saving it (#6568, thanks @dajiaohuang).",
|
||||
"A turn that stops producing output now reports itself: the turn loop records its phase and last progress, and an overdue phase surfaces instead of hanging silently until the stream idle timeout. A delegated agent's final result is never dropped when the host is busy, so a finished child no longer leaves a ghost Running row behind (#6184).",
|
||||
@@ -58,10 +59,9 @@ export const CHANGELOG: ChangelogRelease[] = [
|
||||
"Upgrading Codewhale no longer turns off the built-in Computer Use. Each build writes the built-in bundle to its own directory, so an upgrade used to present it as never reviewed and disabled. Now the review and enablement carry to the new build when its capabilities are unchanged. Changed capabilities show capabilities-changed and wait for review, and a revoked trust never carries (#6303).",
|
||||
"\"Allow for this conversation\" records a grant for that tool and argument class instead of switching the whole thread to Full Access, so the call you just approved is no longer failed by a Permissions change. An approval also survives a Permissions change that only widens what is allowed, grants end when a thread is archived or deleted, and web.run open grants are scoped by host. Full Access covers MCP tools that declare themselves destructive in every host, including…",
|
||||
"web.run retries a refused page once with a browser user agent, and one site's failure no longer fails the whole call or drops its search results.",
|
||||
"Hooks treat bash, Bash and exec_shell as one tool in tool_name conditions, so the documented example fires.",
|
||||
"macOS no longer reports Codewhale's ordinary heap as GPU (IOAccelerator) memory."
|
||||
"Hooks treat bash, Bash and exec_shell as one tool in tool_name conditions, so the documented example fires."
|
||||
],
|
||||
"itemCount": 19
|
||||
"itemCount": 20
|
||||
},
|
||||
{
|
||||
"heading": "Removed",
|
||||
|
||||
Reference in new issue
Block a user