Skip to content

feat(runtime-api): read what one tool call changed, from the snapshots around it - #6817

Merged
Hmbown merged 3 commits into
codewhale-hq:wave/0.10.1-nextfrom
gaord:feat/call-change-route
Oct 6, 2026
Merged

Hmbown merged 3 commits into
codewhale-hq:wave/0.10.1-nextfrom
gaord:feat/call-change-route

Conversation

@gaord

@gaord gaord commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

What and why

A client that wants to show what a shell command changed has had nothing to
read. The engine records a command's writes as a command execution rather than
as a file_change item, so the only per-call attribution a client could find was
metadata.mutation, which only file tools publish — the turn's snapshot delta
covers the turn, not the call.

The engine already brackets every call that may write — a file tool, a shell
command, a program, a write-capable MCP tool — with a tool:<call_id> restore
point before it and a post-tool:<call_id> one after, and records both on the
turn. This adds the route that reads that pair:

GET /v1/threads/{id}/turns/{turn_id}/calls/{tool_call_id}/changes

It answers with the change kind, the line counts, the revision the span left and
the patch, per path, all read from the two trees rather than from the working
copy — so the answer is what the call did, not what the file holds now. Every
entry carries the restore point file-revert accepts, so a command's own write
is revertible like any other.

The span is a window of time, not attribution by cause, and the route says so
where it matters: a call the engine judged read-only took no receipts, one whose
closing snapshot was lost has half a pair, an older turn's trees may have been
pruned, and a path the snapshots do not track cannot appear at all. Each of those
is state: "unavailable" with its own reason, never an empty files list,
because "unknown" and "changed nothing" are different answers.

The tool call id is the model endpoint's own opaque string, echoed back verbatim,
so this compares it rather than parsing it: the charset a record id is held to
would refuse call_01_…|<uuid> and answer "no such call" for a call the turn
plainly recorded.

Issue

No-Issue: a new capability for Runtime API clients; nothing in the tracker
describes it.

How I tested it

  • cargo test -p codewhale-tui --lib call_change_route — a route test over a real
    server against a real side-git repo: an id containing | resolves; every path,
    revision and patch comes from the span's own trees after the work tree moved on;
    a read-only call answers call_not_bounded; a half pair answers
    post_snapshot_missing; pruned trees, and a workspace with no store at all,
    answer snapshots_pruned; an unknown turn or a call the turn never ran is a 404;
    limit=0 is a 400.
  • cargo test -p codewhale-tui --lib snapshot:: (101 passed), patch_between (2),
    has_tree (1), turn_artifact (7).
  • cargo test -p codewhale-tui --test feature_registry — 6 passed. This adds a
    route and no flag, so no new row; docs/RUNTIME_API.md is the runtime-api
    row's doc and carries the contract.
  • cargo clippy -p codewhale-tui --all-targets --all-features --locked -- -D warnings -A clippy::uninlined_format_args -A clippy::too_many_arguments -A clippy::unnecessary_map_or — clean.
  • cargo fmt --all -- --check reports only crates/tui/src/child_env.rs, which
    arrived unformatted with the tip commit on main and is not touched here; every
    file this PR touches is formatted (running the formatter across it would put an
    unrelated hunk in this diff).
  • Full lib suite on this branch: 14517 passed, 20 failed. Fifteen of those pass in
    isolation (load-sensitive tests), and all are in modules this change does not
    touch. On this machine core::engine::tests::yolo_mode_does_not_prompt_for_mcp_action
    overflows a test thread's stack on main as well; RUST_MIN_STACK=16M lets the
    suite run to completion.

Checklist

  • One focused change, rebased on current main
  • cargo fmt --all passes, and tests cover new or changed behavior
  • If this adds or changes a user-facing feature, I updated its docs and its row in docs/features.toml (cargo test -p codewhale-tui --test feature_registry checks it)

…s around it

A client that wants to say "this command changed these files" has had nothing to
read. The engine records a shell command's writes as a command execution, never
as a `file_change` item, so the only per-call attribution a client could find was
`metadata.mutation`, which only file tools publish. The turn's snapshot delta
covers the turn rather than the call, and deriving it per call meant diffing two
trees the Runtime API did not expose.

The engine already brackets every call that may write — a file tool, a shell
command, a program, a write-capable MCP tool — with a `tool:<call_id>` restore
point before it and a `post-tool:<call_id>` one after, and records both on the
turn. This adds the route that reads that pair, plus the two small pieces it
needs: `SnapshotRepo::has_tree`, so a receipt whose objects have been pruned is
reported as such instead of as a git failure, and `SnapshotRepo::patch_between`,
one path's unified diff between two trees.

    GET /v1/threads/{id}/turns/{turn_id}/calls/{tool_call_id}/changes

answers with the change kind, the line counts, the revision the span left and the
patch, per path, all read from the two trees rather than from the working copy.
The answer is therefore what the call did, not what the file holds now. Every
entry carries the span's own revision and the restore point `file-revert` accepts,
so a command's write is revertible like any other.

What the span is not is attribution by cause. It is a window of time: another
writer in the same workspace during the call is inside the difference, and a path
the snapshots do not track (a `.gitignore` hit, `node_modules/`, a binary) cannot
appear at all. A call the engine judged read-only took no receipts, one whose
closing snapshot was lost has half a pair, and an older turn's trees may have
been pruned — each answers `state: "unavailable"` with its own `reason`, which is
a different answer from an empty `files` list and has to be read as "unknown"
rather than "changed nothing".

The tool call id is the model endpoint's own opaque string, echoed back verbatim,
so this compares it rather than parsing it: the charset a *record* id is held to
would refuse `call_01_…|<uuid>` and answer "no such call" for a call the turn
plainly recorded.

Signed-off-by: Ben Gao <bengao168@msn.com>
@gaord
gaord requested a review from Hmbown as a code owner October 2, 2026 06:55
@gaord

gaord commented Oct 5, 2026

Copy link
Copy Markdown
Contributor Author

hopefully this could land in 0.10.1. it help users know what have changed over a turn more accurately

@Hmbown Hmbown added this to the v0.10.2 milestone Oct 5, 2026
Hmbown and others added 2 commits October 6, 2026 00:59
# Conflicts:
#	crates/tui/src/snapshot/repo.rs
list_call_changes resolves the side repo on a blocking-pool thread.
Under main's sealed test home that thread is a foreign reader: it
resolves the isolated snapshot root instead of the test's store and
every span reads as pruned. Mint the scope ticket on the handler and
join it on the worker, the same pattern file-revert already uses.

Evidence: call_change_route_reads_one_calls_workspace_span ok;
patch_between/has_tree/run_git_drains neighbors 4/0.
@Hmbown
Hmbown changed the base branch from main to wave/0.10.1-next October 6, 2026 05:29
@Hmbown
Hmbown merged commit eb1b644 into codewhale-hq:wave/0.10.1-next Oct 6, 2026
26 of 27 checks passed
Hmbown pushed a commit that referenced this pull request Oct 6, 2026
Reconcile the complete contributions from #6832 (@aboimpinto), #6867
(@hodeswildsmith455-boop), and #6805 (@LIghtJUNction) with the current
Engine, provider identities, reviewed-plugin policy, and task lifecycle.
Original contributor histories are recorded by subsequent resolved merges.

Fix the already integrated contributor cases: snapshot corruption is an
explicit unavailable/error result (#6817), malformed locales cannot select
an incidental script (#6860), image metadata uses the exact uploaded bytes
and respects available decoders (#6858), automation deletion waits for
actual scheduler reconciliation (#6864), and blocking trust/skill reads stay
off the async executor (#6869). Preserve #6857's compaction regression.

Extend #6872's human-wait lifecycle guard to approval/elevation cards;
retire only the matching ended parent request and refresh activity only
after a delivered decision. Enforce configured finite approval deadlines in
the Engine, including deadline/cancellation races and durable receipts.

Serialize Native Windows ACL admission/retirement across Core processes
with a logon-scoped kernel mutex, preserving exact SID/object validation.
Add an actual child-process lock test; serialize DSH host tests in the
existing extension-host lane. Windows execution proof remains hosted CI.

Reconcile the vendored computer-use plugin with canonical main a656f67455fc
while preserving Core's 0.12.1 embedding contract. Canonical b47/a656 tree
passed Ubuntu/macOS/Windows source and package gates, including 28/28
Windows-focused tests and the controlled desktop fixture; this is separate
from the new Engine head's CI verdict.

Partial adaptation of the discovery-cache priority portion from PR #6393
by @AdityaVG13 (original ac33dd4). Preserve the best match under count
and byte limits without importing the unfinished echolocation/fork design;
the broader draft remains open.

Validation:
- npm test: 1286 passed, 0 failed, 7 skipped; web 767/767.
- npm run check:web: lint, typecheck and production build passed.
- Affected Rust selection: 801/803 initially passed; the two fixture/lifecycle
  expectation failures were corrected and each passed a focused rerun.
- Additional focused Rust: approval 28/28, discovery cache 11/11,
  OrcaRouter synthetic catalog 3/3, and 33/33 lifecycle/API/routing checks.
- Final CI-repair selection: 37/39 initially passed; the BMP feature-proxy
  and feature-registry summary failures were corrected; both corrected tests passed (2/2, 0 failures).
- Qualified Clippy: six packages, all targets/all features, passed with
  the CI style allowances. Portable no-default-feature check passed;
  portable policy verifier 6/6 passed; formatting and diff checks passed.
- Runtime contract: 55 measured metrics passed after explicit remeasurement;
  all 21 structural identities were unchanged. Twelve byte/token-estimate
  budgets account for bounded child-wait disclosure and contributor locale
  descriptions; no runtime field/prompt was removed to lower the budget.
- Persistence budget was not qualified locally: the checker requires a
  clean tree and this shared checkout retains an unrelated operator file.
  Clean hosted CI, fresh stamped build and real DeepSeek TUI acceptance
  remain required before the integration PR can merge to main.

Refs #6872, #6843, #6795.

Co-authored-by: Paulo Aboim Pinto <paulo.aboim.pinto@gmail.com>
Co-authored-by: hodeswildsmith455-boop <hodeswildsmith455-boop@users.noreply.github.com>
Co-authored-by: LIghtJUNction <lightjunction.me@gmail.com>
Co-authored-by: AdityaVG13 <adityavgcode@gmail.com>
Signed-off-by: CodeWhale Bot <bot@codewhale.net>
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.

2 participants