Skip to content

fix(stella-tools): save_state success output carries the saved content's identity - #3299

Open
macanderson wants to merge 1 commit into
mainfrom
fix-3297-save-state-identity
Open

fix(stella-tools): save_state success output carries the saved content's identity#3299
macanderson wants to merge 1 commit into
mainfrom
fix-3297-save-state-identity

Conversation

@macanderson

@macanderson macanderson commented Aug 14, 2026

Copy link
Copy Markdown
Owner

What & why

save_state's success output was the last constant-shape output on the 12-tool surface (#3297's sweep of the other 11 explains why each is exempt): two saves of different same-length content under one key rendered byte-identical saved {key} ({N} bytes) outputs, and the stagnation detector (crates/stella-core/src/loop_detect.rs, rung 3) deliberately kills consecutive same-tool calls whose outputs are byte-identical — whatever their arguments. A legitimate checkpoint loop (a fixed-width counter or constant-size JSON blob updated under one key) could therefore be killed as stagnant mid-solve — the same hazard class that converted a correct edit_file run into an agent_error loss on Terminal-Bench (#3176), was fixed for edit_file/apply_edits by #3187, and re-arose here because the tool purge (#3244) deleted edit.rs/write.rs/staleness.rs and closed #3188 as moot without retiring the class.

The success output now appends the saved content's identity:

saved {key} ({N} bytes, sha256/8 {digest})

{digest} is the first 8 hex chars of the content's sha256, computed via foundry_gate::digest — the crate's existing, documented "one hashing primitive" — rather than reintroducing a second Sha256-to-hex helper to replace the deleted staleness::sha256_8 (no new dependency; sha2 was already in use). Content-derived only, per the detector's settled output-keying contract: no timings, no randomness (#2706), so a loop genuinely re-saving identical bytes still renders byte-identical output and is still caught. The saved {key} ({N} bytes prefix is stable — the identity is appended, never restructured. ToolSchema is unchanged.

Closes #3297

Refs #3176, #3187, #3188, #3244, #2706.

The witness

Stella's definition of done is a test that fails on the old code and passes on the new.

  • This PR includes a witness test (fails on main, passes here)

Two tests in crates/stella-tools/src/registry/tests.rs, beside the existing scratch dispatch tests, checked the two-sided way (format string temporarily reverted to the old constant shape, witness watched failing, fix restored):

  • save_state_outputs_differ_for_different_same_length_contentfails on the old format string, where both saves render byte-identical output:
    • old, counter=1: saved checkpoint (9 bytes)
    • old, counter=2: saved checkpoint (9 bytes) (identical — the assertion output was left: "saved checkpoint (9 bytes)" / right: "saved checkpoint (9 bytes)")
    • passes with the fix, where the outputs are distinct:
    • new, counter=1: saved checkpoint (9 bytes, sha256/8 055ea690)
    • new, counter=2: saved checkpoint (9 bytes, sha256/8 1424b099)
  • save_state_output_is_identical_for_identical_content — re-saving identical bytes still renders byte-identical output (passes on both sides, guarding the detector's catch of genuinely stuck loops).

The gate

  • cargo fmt --check
  • cargo clippy -p stella-tools --all-targets -- -D warnings (no other crate pins the message — swept rg '"saved |saved \{' workspace-wide)
  • cargo test -p stella-tools (222 + 1 integration, all green)
  • make doc-warnings CARGO_SCOPE="-p stella-tools" (rustdoc -D warnings, private items included)
  • make tool-docs — OK; docs/tools/save_state.toml captures no observed example and the schema is unchanged, so no regeneration needed
  • Docs updated where behavior changed: the module-doc contract list in scratch.rs now states the output-identity contract and why it is content-derived
  • CLA signed
  • Closes #3297 appears both above and as a commit trailer

Nothing left behind

  • There is nothing: everything I noticed is fixed in this PR. (The issue's own sweep of the other 11 tools at e352cf486 found no further instance of the class, and I re-checked rg 'content: format!' crates/stella-tools/src — the remaining constant-shape success string is delete_state's deleted {key}, which carries the repeat-delete exemption the issue documents: a second delete of one key errors, so distinct effects cannot repeat silently.)

Ground-rule check

  • No I/O added to stella-core; no new deps (reuses foundry_gate::digest, already backed by the existing sha2 dependency)
  • No new outbound network calls

Anything reviewers should know?

Alternative rejected: reintroducing a staleness.rs-style sha256_8 helper (the issue's suggested reference shape). foundry_gate::digest already exists in this crate and documents itself as "the workspace's one hashing primitive", so a second Sha256-to-hex function would be duplication; the 8-char truncation happens at the one call site, on a value that is always 64 ASCII hex chars.

Summary by Sourcery

Ensure save_state success outputs embed content identity to avoid false stagnation detection while keeping identical-content saves byte-identical.

Bug Fixes:

  • Prevent the stagnation detector from incorrectly terminating legitimate save_state checkpoint loops by making outputs differ for different same-length content under the same key.

Enhancements:

  • Include a truncated sha256-based identity in save_state success messages while preserving a stable prefix and deterministic, content-derived behavior.
  • Document the save_state output-identity contract and its rationale in the scratch module docs.

Tests:

  • Add regression tests verifying save_state outputs differ for different same-length content and remain identical for identical content.

…t's identity

save_state's success string was the last constant-shape output on the
12-tool surface: two saves of different same-length content under one
key rendered byte-identical "saved {key} (N bytes)" outputs, and the
engine's stagnation detector (stella-core's loop_detect, rung 3)
deliberately keys on byte-identical consecutive same-tool outputs — so
a legitimate checkpoint loop (a fixed-width counter updated under one
key) could be killed as stagnant mid-solve, the same hazard class
#3176/#3187 fixed for edit_file before the tool purge (#3244) deleted
that surface and its staleness.rs helper.

The success output now appends the saved content's identity:

    saved {key} ({N} bytes, sha256/8 {digest})

where the digest is the first 8 hex chars of the content's sha256,
computed via foundry_gate::digest — the crate's existing hashing
primitive — rather than reintroducing a second Sha256-to-hex helper.
Content-derived only, per the detector's settled contract: no timings,
no randomness (#2706), so a loop genuinely re-saving identical bytes
still renders byte-identical output and is still caught.

Witness (two-sided, in registry/tests.rs beside the existing scratch
dispatch tests): different same-length content under one key must
render distinct outputs — fails on the old format string ("saved
checkpoint (9 bytes)" twice), passes with the digest; and identical
content re-saved must render byte-identical output, which holds on
both sides.

docs/tools/save_state.toml captures no observed example and the
ToolSchema is unchanged, so make tool-docs is unaffected (verified).

Closes #3297
@vercel

vercel Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
stella-cli-docs Ignored Ignored Aug 14, 2026 8:24pm

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Sorry @macanderson, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Updates the save_state tool's success output to include a content-derived sha256/8 identity to avoid false-positive stagnation detection, and adds tests and documentation to lock in this behavior.

Sequence diagram for updated save_state success output

sequenceDiagram
    participant SaveState
    participant StdFs as std_fs_write
    participant FoundryGate as foundry_gate_digest

    SaveState->>StdFs: write(path, content)
    alt Ok(())
        SaveState->>FoundryGate: digest(content.as_bytes)
        FoundryGate-->>SaveState: sha256_hex_64
        SaveState-->>SaveState: identity = sha256_hex_64[..8]
        SaveState-->>SaveState: ToolOutput::Ok { content = format!("saved {key} (N bytes, sha256/8 {identity})") }
    else Err(e)
        SaveState-->>SaveState: ToolOutput::error(format!("failed to save {key}: {e}"))
    end
Loading

File-Level Changes

Change Details Files
Make save_state success output content-identity-aware to prevent byte-identical outputs for different same-length content under one key.
  • Change SaveState Tool implementation to compute a sha256 digest of the saved content via foundry_gate::digest and append the first 8 hex chars to the success string.
  • Keep the existing 'saved {key} ({N} bytes' prefix stable while extending it to 'saved {key} ({N} bytes, sha256/8 {digest})' so ToolSchema and callers remain compatible.
  • Ensure the digest computation is purely content-derived (no timing/randomness) to preserve the stagnation detector's semantics and avoid new dependencies.
crates/stella-tools/src/scratch.rs
Add regression tests asserting the new output-identity behavior of save_state while preserving the detector's guarantee for truly identical content.
  • Add a test that saves different same-length contents under the same key and asserts the outputs differ and share the stable prefix.
  • Add a complementary test that re-saves identical content under the same key and asserts the outputs are byte-identical.
  • Document in test comments the relationship to the stagnation detector in stella-core::loop_detect and the rationale for using a content-derived sha256/8 suffix.
crates/stella-tools/src/registry/tests.rs
Document the new save_state output-identity contract in the scratch module docs. crates/stella-tools/src/scratch.rs

Assessment against linked issues

Issue Objective Addressed Explanation
#3188 Modify the write_file tool so that its success output string includes a content-derived identity (e.g., append a sha256/8 {digest} of the written file contents) instead of being a constant wrote {bytes} bytes to {path} message. The PR changes the save_state tool’s success output to include a sha256/8 digest and updates its tests and documentation. It does not modify write_file or its success string, nor does it add any digest-based identity to write_file outputs.
#3188 Add a test (witness) ensuring that two write_file calls writing different same-length content to the same path produce distinct success outputs, and that identical content still produces identical outputs. The PR adds witness tests for save_state (save_state_outputs_differ_for_different_same_length_content and save_state_output_is_identical_for_identical_content). There are no tests involving write_file, so the behavior specified for write_file is not verified by this PR.
#3188 Audit other tools’ constant success strings for the same stagnation-detector hazard and adjust them as needed while addressing write_file. The PR focuses exclusively on save_state and mentions a prior sweep of tools in its description, but in this diff it does not perform any new audit or changes to other tools’ success strings. It therefore does not carry out the requested audit in the context of fixing write_file.
#3297 Modify save_state success output so it includes content-derived identity (sha256/8 of the saved content), keeping it deterministic and avoiding timings or randomness.
#3297 Add witness tests that (a) fail on the old constant-shape output and pass with the digest, verifying different same-length contents under one key produce distinct outputs, and (b) verify identical content produces identical output.
#3297 Update inline documentation/comments to reflect the new save_state output-identity contract and its relation to the stagnation detector.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

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

Labels

None yet

Projects

None yet

1 participant