From dd51d02bc27d224f6653cac54cbd7345791f59e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?m=C3=BCde?= Date: Sun, 5 Jul 2026 23:42:57 +0200 Subject: [PATCH] fix(hive-claude): report Progress.created on first-turn overflow, compacted only when compact succeeded --- hive-claude/src/session.rs | 48 +++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/hive-claude/src/session.rs b/hive-claude/src/session.rs index 074da067..03ea5e2b 100644 --- a/hive-claude/src/session.rs +++ b/hive-claude/src/session.rs @@ -66,19 +66,25 @@ impl InfiniteSession

{ /// [`Error::PromptTooLong`]; rate-limit / auth / hard failures propagate /// unchanged for the caller to handle. pub async fn run(&self, config: &Config, prompt: &str, sink: &impl Sink) -> Result { + // Resolve resume-vs-create once, up front, so `created` reflects the + // session state at the START of the run: the reactive-retry path can't + // read it from the retry (the session exists by then). + let existed = self.store.find_by_title(&self.name).is_some(); let meter = TelemetrySink::new(sink); - let created = match self.attempt(config, prompt, &meter).await { + let created = match self.attempt(config, prompt, &meter, existed).await { Ok(created) => created, Err(Error::PromptTooLong) => { // The session is already past the window — no turn can run on // it and the detail is gone (no checkpoint possible). Compact, // then retry the same prompt once; the retry is the answering - // turn, so its telemetry is what we report. + // turn, so its telemetry is what we report. The failed attempt + // created/resumed the session, so the retry resumes it, and + // `created` still reflects the pre-run state. self.compact(config, sink).await?; let retry = TelemetrySink::new(sink); - let created = self.attempt(config, prompt, &retry).await?; + self.attempt(config, prompt, &retry, true).await?; return Ok(Progress { - created, + created: !existed, compacted: true, telemetry: retry.snapshot(), }); @@ -91,12 +97,15 @@ impl InfiniteSession

{ // says it's due, checkpoint (best-effort) then compact. if self.policy.should_compact(telemetry.usage()) { if let Some(checkpoint) = self.policy.checkpoint_prompt() { - let _ = self.attempt(config, checkpoint, sink).await; + let _ = self.attempt(config, checkpoint, sink, true).await; } - let _ = self.compact(config, sink).await; + // Only claim a compaction if it actually succeeded — the flag feeds + // stats + the auto-reset watermark, so a failed best-effort + // `/compact` must not report that the context shrank. + let compacted = self.compact(config, sink).await.is_ok(); return Ok(Progress { created, - compacted: true, + compacted, telemetry, }); } @@ -121,22 +130,29 @@ impl InfiniteSession

{ } } - /// One resume-or-create turn. Uses the store to pick resume vs. create up - /// front (avoiding a wasted resume-miss spawn), and still self-heals if the - /// backing file vanished between the check and the run. Returns whether a - /// fresh session was created. - async fn attempt(&self, config: &Config, prompt: &str, sink: &impl Sink) -> Result { - let exists = self.store.find_by_title(&self.name).is_some(); - let attach = if exists { + /// One resume-or-create turn. `existed` is the caller's up-front + /// resume-vs-create decision (whether the titled session was on disk before + /// the run) — passing it in rather than re-checking keeps `created` + /// reporting consistent across the reactive-retry path. Still self-heals if + /// the backing file vanished between the check and the run. Returns whether + /// a fresh session was created. + async fn attempt( + &self, + config: &Config, + prompt: &str, + sink: &impl Sink, + existed: bool, + ) -> Result { + let attach = if existed { Attach::Resume(self.name.clone()) } else { Attach::Create(self.name.clone()) }; match Claude::run(config, &attach, prompt, sink).await { - Ok(()) => Ok(!exists), + Ok(()) => Ok(!existed), // We thought it existed but the resume missed (raced an archive) — // self-heal by creating. - Err(Error::SessionNotFound) if exists => { + Err(Error::SessionNotFound) if existed => { Claude::run(config, &Attach::Create(self.name.clone()), prompt, sink).await?; Ok(true) }