Runtime: turns carry typed artifact references (items, turn aggregate, workspace delta, read routes) - #6660
Runtime: turns carry typed artifact references (items, turn aggregate, workspace delta, read routes)#6660Hmbown wants to merge 6 commits into
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7f3fbb3fa2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| fn item_artifact_refs(&self, turn: &TurnRecord) -> Vec<TurnArtifactRef> { | ||
| turn.item_ids | ||
| .iter() | ||
| .filter_map(|item_id| self.store.load_item(item_id).ok()) |
There was a problem hiding this comment.
Move artifact item reads off the Tokio worker
When a turn completes, async monitor_turn calls set_turn_artifacts, which reaches this synchronous load_item once per item; on a slow runtime-store disk or a tool-heavy turn, these filesystem reads block a Tokio worker and delay unrelated runtime/API work. Collect the item refs under spawn_blocking or carry forward the refs already observed by the monitor.
AGENTS.md reference: AGENTS.md:L164-L170
Useful? React with 👍 / 👎.
| fn item_artifact_refs(&self, turn: &TurnRecord) -> Vec<TurnArtifactRef> { | ||
| turn.item_ids | ||
| .iter() | ||
| .filter_map(|item_id| self.store.load_item(item_id).ok()) |
There was a problem hiding this comment.
Propagate referenced-item load failures
When an item referenced by turn.item_ids is missing or corrupt, .ok() silently drops that item, so both the list route and terminal settlement publish an incomplete artifact aggregate—and may even mark it settled—rather than reporting the broken store referent. Make this helper return Result and propagate or explicitly record the failure.
AGENTS.md reference: AGENTS.md:L79-L81
Useful? React with 👍 / 👎.
| let live = match precheck_file_target(&root, &relative)? { | ||
| Some(_) => Some(read_confined_bytes(&open_confined_file( | ||
| &root, &relative, false, | ||
| )?)?), |
There was a problem hiding this comment.
Fall back before reading an oversized current file
When a recorded artifact was at most 16 MiB but the workspace path is later replaced by a file larger than that limit, read_confined_bytes returns 413 here before the post-turn snapshot is consulted. The retained, small historical revision therefore becomes unreadable even though the route promises to fall back to the snapshot whenever the live file no longer holds the recorded revision; inspect the live size without failing the request, then try the snapshot.
Useful? React with 👍 / 👎.
| Err(error) => { | ||
| tracing::debug!(%error, "snapshot blob read failed"); | ||
| Ok(None) |
There was a problem hiding this comment.
Surface snapshot-store failures instead of reporting drift
When read_blob fails because Git is unavailable, the side repository is corrupt, or access is denied, this branch treats the failure exactly like a pruned snapshot and the caller returns 409 claiming the recorded revision is gone. Only suppress the specific unknown/pruned-object case; operational snapshot-store errors should remain 5xx so clients do not mistake infrastructure failure for irreversible artifact loss.
Useful? React with 👍 / 👎.
| if let Some(item) = files.remove(&reference.path) { | ||
| reference.item_id = item.item_id; | ||
| reference.tool_call_id = item.tool_call_id; | ||
| reference.tool_name = item.tool_name; | ||
| reference.source = TurnArtifactSource::ToolMutation; |
There was a problem hiding this comment.
Retain delta provenance when receipt bytes do not match
When a file tool writes a path and a shell command, editor, or sub-agent changes that same path later in the turn, the delta reference describes the later bytes, but this block unconditionally copies the tool item identity and relabels the source as tool_mutation based only on the path. This attributes the final content to the wrong tool; only adopt item provenance when the receipt and delta revisions establish that they describe the same output, otherwise retain workspace_changed_during_turn.
Useful? React with 👍 / 👎.
| artifacts.extend( | ||
| files | ||
| .into_values() | ||
| .filter(|item| !delta.tracked_item_paths.contains(&item.path)), | ||
| ); |
There was a problem hiding this comment.
Drop net-zero renames by checking their original path
When a tool renames a to b and a shell command or sub-agent moves b back to a before the post-turn snapshot, the pre/post delta is empty and tracked_item_paths contains the rename's previous_path (a) but not its destination (b). This filter checks only item.path, so the aggregate keeps a stale rename reference to nonexistent b; consider both the current and previous paths when deciding that a tracked item change netted to zero.
Useful? React with 👍 / 👎.
| match component { | ||
| Component::Normal(name) if name != ".git" => parts.push(name.to_str()?), | ||
| _ => return None, |
There was a problem hiding this comment.
Reject artifact paths that the read route cannot accept
On Unix, a filename such as logs\\run.txt is a normal path component here and is published as a delta artifact, but the artifact reader passes it through relative_request_path, which rejects every backslash with 400 before attempting either the workspace or snapshot. This creates references that can never be opened; omit and count such paths here, or make the read-path representation support them consistently.
Useful? React with 👍 / 👎.
| workspace: turn.workspace, | ||
| artifacts, | ||
| item_artifacts, | ||
| thread_workspace: thread.workspace, |
There was a problem hiding this comment.
Pin each turn to the workspace it captured
After a completed thread is updated to use a different workspace, this view resolves every historical file reference against the thread's new workspace rather than the workspace whose snapshots and receipts produced the turn. Old artifacts can consequently return unrelated live bytes, fail to find their post-turn snapshot, or become unreadable; the settlement worker has the same race because it reloads the mutable thread workspace. Persist the turn-time workspace identity and use it for both settlement and reads.
Useful? React with 👍 / 👎.
| let (summary, relative_path, evidence) = if image_handle { | ||
| let (summary, evidence) = image_evidence_summary(sessions_dir, id, artifact_id)?; | ||
| let path = summary.path.clone(); | ||
| (Some(summary), path, Some(evidence)) |
There was a problem hiding this comment.
Map pruned media manifests to the recorded-artifact gone state
When a turn-recorded media artifact's session directory has been pruned, its evidence manifest is missing, so this call returns 404 before the later ArtifactAuthority::TurnRef missing-file mapping can produce 410. The turn record already proves that the artifact ID existed, and non-image turn artifacts in the same situation return 410; map a not-found manifest to gone for turn authority while retaining the saved-session route's existing 404 behavior.
Useful? React with 👍 / 👎.
Refs #6653. This is the first of four slices. Producers now record what they wrote, at the point where they write it: - File tools and apply_patch add `size` and `sha256` to each created, updated or renamed `mutation.files[]` entry. Deleted entries get neither. - The classic spill publishes immutably (like adaptive evidence) and records `artifact_digest`. - A file-mutating call's result names its `tool:<call>` restore snapshot and the session that snapshot is tagged with. The runtime parses those receipts into `TurnItemRecord.artifacts`: - Each ref is confined, typed and carries size and revision; forged or unconfined paths produce no ref. - The legacy `artifact_refs` is now derived from `artifacts` and holds only workspace file paths, so the pinned app's Preview gets data without a contract change. - A restore point is published only when the snapshot's session tag equals the thread's bound session. - Bound threads now start their engine under the bound session id, so a first turn's restore points are ones file-revert accepts. Tests: scripts/dev-test.sh tui with focused filters. 88 passed (new parser, pump, engine restore-point, file and apply_patch receipt tests); 55 passed (truncate, artifacts, tool_result_retrieval); 990 passed, 6 ignored (runtime_threads::, runtime_api::, core::engine::tests). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Refs #6653. This is the second of four slices. Engine: - Every turn that took (or tried to take) a pre-turn snapshot now emits `TurnWorkspaceSnapshots` immediately before `TurnComplete`. This covers shell turns, the no-client failure, and every completed, failed or interrupted model turn. - The event carries the pre-turn result and a watch receiver for the post-turn snapshot, which still runs off the engine loop, so `TurnComplete` stays non-blocking (#234). - Snapshot helpers now return a typed reason for a missing snapshot (disabled, too large, too many files, unsafe location, failed) instead of `None`. - The event has a protocol twin, `turn_workspace_snapshots`. Snapshot repo: two read-only additions. - `diff_snapshots` uses one `diff-tree -M` pass and one `cat-file --batch` pass to report the size and SHA-256 of each changed file. The result is bounded, and it counts what it omits. - `tracked_paths` tells a path that did not change apart from one the snapshots cannot see. Runtime: - `TurnRecord` gains `artifacts` and `workspace`. One function, `merge_turn_artifacts`, computes the aggregate from item refs, plus the delta once it settles. The delta is labelled `workspace_changed_during_turn`. - The settlement task is keyed to the post-turn snapshot finishing, with a 10-minute bound for a hung git. It publishes `turn.artifacts` on every outcome. - Monitor failure without a pair gives `unavailable(not_captured)`, and a restart gives `unavailable(runtime_restarted)`. - New route: `GET /v1/threads/{id}/turns/{turn_id}/artifacts`. Tests: scripts/dev-test.sh tui with focused filters. 283 passed, 1 failed (the oversized-blob fixture used a *.bin name, which snapshots exclude); snapshot::delta then passed 3/3 after renaming it, and the engine pair ordering test passed 1/1; 2383 passed, 6 ignored (runtime_threads::, runtime_api::, core::engine::, core::turn::, protocol_parity, snapshot::, config commands, tui::ui). codewhale-protocol: 73 + 17 passed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…lver Refs #6653. This is the third of four slices. New route: `GET /v1/threads/{id}/turns/{turn_id}/artifacts/{artifact_id}`, with `?offset=&limit=&revision=`. It uses the workspace file read's window contract, plus `artifact`, `source` and `current`. - **Files** are read from wherever the recorded revision still lives: 1. the live workspace (confined, no-follow, `.git` refused); 2. otherwise the turn's post-turn snapshot, via `SnapshotRepo::read_blob`; 3. otherwise 409, naming the current revision. Deleted files return 410, files over 16 MiB return 413, and symlinks return 403. `?revision=` selects an intermediate revision recorded by an item. - **Spills and media** go through `resolve_session_artifact`, which now serves both the session route and the turn route. Both share the session-id validation, confinement, image-manifest and integrity checks. - The turn route roots its reads at the root the artifact writer uses, so a configured `sessions_dir` cannot point a read at another tree. - The turn ref proves ownership, so spills from unbound runtime engines are readable without a SavedSession index. Before this they were always 404. - A hash mismatch returns 409, and pruned bytes return 410. Tests: scripts/dev-test.sh tui with focused filters. 14 passed, 1 failed on the first run (the fixture never wrote the mismatched spill); the route test then passed 1/1 after writing it. snapshot::delta: 4/4. The existing session_artifacts and tool_media_artifact tests passed unchanged after the resolver refactor. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… pump Refs #6653. This is the fourth of four slices. docs/RUNTIME_API.md: - New "Turn artifacts" section: the ref shape, how the aggregate is merged, the `workspace.state` lifecycle and each reason, and what the delta cannot see. It is a workspace diff: concurrent writers are included, and excluded or ignored paths are invisible. Shell writes are recorded per turn, never per call. - Documents both routes and their error table, the `turn.artifacts` event, and the new record fields. - The session-artifact section now points runtime-turn spills at the turn route. - The embedded-client paragraph no longer claims that no preview or artifact contract exists. CHANGELOG entry under Unreleased. crates/tui/CHANGELOG.md and web/lib/changelog.generated.ts were regenerated by sync-changelog.sh and derive-changelog.mjs. The pump now canonicalizes the thread workspace once, with `tokio::fs::canonicalize`. Previously it called `Path::canonicalize` for each receipt path on the async worker (check-blocking-calls-budget). Gates run: - cargo fmt --check: pass. - clippy with CI flags (-p codewhale-tui -p codewhale-protocol, --all-targets --all-features): pass. - check-blocking-calls-budget, check-dead-code-budget, check-command-crate-boundaries, split/module_graph --check, check-runtime-contract-budget (55/55 metrics at budget): pass. - sync-changelog.sh, derive-facts and derive-changelog: ran. derive-install could not run because web node_modules are not installed here (missing `marked`); the changelog does not feed it. - check-versions --range-audit-advisory: OK. - check-contributor-credit v0.10.0: OK. Tests: scripts/dev-test.sh tui with focused filters. 12/12 passed for the artifact tests, and a broad sweep passed 2659, with 6 ignored (runtime_threads::, runtime_api::, core::engine::, core::turn::, protocol_parity, snapshot::, config commands, tui::ui, tools::truncate, tools::file, tools::apply_patch, artifacts::, tool_result_retrieval). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…settle test The web client's stream vocabulary lacked turn.artifacts, failing the version-drift vocabulary check (33 vs 34). The settle test read events right after observing the Settled turn, but the event is emitted after the save, so it raced on the Windows runner. node --test runtime_web_client: 36 pass / 0 fail; cargo test turn_workspace_delta: 1 passed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
7f3fbb3 to
7a5d88e
Compare
Merge origin/fix/6621-thread-snapshot-ownership into feat/turn-artifacts. Cause: #6660 and #6645 each added a workspace snapshot event and a session identity rule. #6660 sent Event::TurnWorkspaceSnapshots (a pre-turn id plus a watch channel for the post-turn snapshot), stamped restore_snapshot_id into tool result metadata, ran the engine under thread.session_id, and advertised a restore point only when the snapshot tag matched the bound session. #6645 runs every Runtime engine under thread.id and records Event::WorkspaceSnapshotTaken receipts on TurnRecord.workspace_snapshots, which is what file-revert and patch-undo resolve against. Two snapshot authorities and two identity rules. Fix: one snapshot authority, #6645's. TurnWorkspaceSnapshots, WorkspaceSnapshot/SnapshotUnavailable, the wire event, the watch-channel settlement and the tool-metadata restore stamp are removed. The engine identity is thread.id. The turn's pre/post delta now diffs the tree ids of the pre_turn and post_turn receipts recorded on the turn (both arrive before TurnComplete under record_restore_points). restore_snapshot_id is the tree id of a receipt recorded on the thread's own turn: the call's tool receipt for a tool write, the pre_turn receipt for a delta change, so file-revert accepts every id a ref advertises, bound session or not. Unavailable reasons come from the snapshot gate notice keyed by the thread id; settlement_timeout is gone because nothing waits any more. Tests (CARGO_BUILD_JOBS=4, focused): - cargo test -p codewhale-tui --lib (turn artifacts, delta, undo, restore, artifact, protocol_parity filters): test result: ok. 350 passed; 0 failed - cargo test -p codewhale-tui --lib (#6645 ownership/undo tests): test result: ok. 37 passed; 0 failed - cargo test -p codewhale-protocol: 73 passed; 17 passed; 0 failed - node --test crates/tui/tests/runtime_web_client.test.mjs: 37 pass, 0 fail Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
| let live = match precheck_file_target(&root, &relative)? { | ||
| Some(_) => Some(read_confined_bytes(&open_confined_file( | ||
| &root, &relative, false, | ||
| )?)?), | ||
| None => None, | ||
| }; |
There was a problem hiding this comment.
🔴 Historical files blocked by current paths
When a recorded file becomes a symlink, read_file_artifact returns 403 before checking its snapshot. A later directory or oversized file similarly blocks historical bytes that remain available.
Learn more
A file artifact identifies the bytes present during a turn. The read route prefers the live file when its hash matches and otherwise reads the turn's post-turn snapshot. precheck_file_target rejects symlinks and directories, and read_confined_bytes rejects files above 16 MiB. Here those errors return before the snapshot fallback, even when the snapshot contains the recorded revision.
Example: A turn writes page.html with revision A. Another process replaces page.html with a symlink. The post-turn snapshot still contains revision A, but reading the turn's artifact returns 403 rather than A.
Recommended fix: Treat an unreadable or disallowed current path as unavailable for the live-revision comparison. Attempt the recorded snapshot first for references with a revision; preserve the live-path error if no valid snapshot matches, where appropriate.
Was this helpful? React with 👍 or 👎 to provide feedback.
| "file is larger than the {FILE_SERVE_MAX_BYTES}-byte serving limit" | ||
| ))); | ||
| } | ||
| let relative = relative_request_path(&artifact.path, false)?; |
There was a problem hiding this comment.
🟡 Spaced filenames open the wrong artifact
When a recorded filename has surrounding spaces, relative_request_path trims artifact.path before opening it. The artifact then returns another file's bytes or fails, despite its recorded path being valid.
Learn more
Tool receipts and snapshot deltas retain literal filenames, including leading and trailing spaces; workspace_relative_path explicitly accepts them. The shared request-path parser trims strings before building its path, so feeding it a stored reference changes the file being addressed. A snapshot fallback also receives the untrimmed name, but the live-path check can read another file and return an unrelated error first.
Example: A tool creates notes.txt and records its SHA-256. Reading its file_... id tests notes.txt in the workspace, rather than notes.txt, and returns a conflict or missing-file response when no matching snapshot is available.
Recommended fix: Validate stored artifact paths without trimming them, while retaining the existing confinement and .git checks. Keep the request-path normalization for user-supplied workspace paths separate from literal recorded filenames.
Was this helpful? React with 👍 or 👎 to provide feedback.
| artifacts.extend( | ||
| files | ||
| .into_values() | ||
| .filter(|item| !delta.tracked_item_paths.contains(&item.path)), | ||
| ); |
There was a problem hiding this comment.
🟡 Truncated delta drops recorded tool files
When a changed tool file falls past the delta limit, merge_turn_artifacts removes its item reference because tracked_item_paths contains it. The turn's artifact list omits a recorded file even when capacity remains after merging.
Learn more
The snapshot diff is bounded to MAX_TURN_ARTIFACTS, while workspace_delta independently probes every item-reported path for presence in either snapshot. A tracked path missing from the bounded diff can therefore be either unchanged or omitted after the diff reached its limit. The merge treats both cases as unchanged and drops the item reference.
Example: A turn changes 1,001 files, including one tool-written z.txt that falls after the first 1,000 diff entries. z.txt is tracked, so its item reference is removed even if duplicate delta paths leave room in the final aggregate.
Recommended fix: When delta.truncated is true, do not use absence from delta.refs to prove an item path had no change. Retain its item reference or probe its exact pre/post blobs before dropping it, and account for any later aggregate cap in workspace.omitted.
Was this helpful? React with 👍 or 👎 to provide feedback.
| - Runtime API: turns now record what they produced. Each item and turn | ||
| carries typed artifact references (path, kind, size, revision, and a | ||
| restore point when file-revert would accept one) for files a tool wrote, | ||
| spilled tool output, and media. | ||
| - Once the post-turn snapshot settles, the turn also lists what changed in | ||
| the workspace while it ran, including shell and sub-agent writes. It then | ||
| publishes `turn.artifacts`. | ||
| - `GET /v1/threads/{id}/turns/{turn_id}/artifacts` lists a turn's | ||
| references, and `.../artifacts/{artifact_id}` reads one from the | ||
| workspace, the post-turn snapshot or the session artifact directory. | ||
| - Spills from unbound runtime threads were unreadable before; they are now | ||
| readable. | ||
| - The legacy `artifact_refs` field is now filled with the workspace files a | ||
| tool call wrote, so Preview in current desktop builds shows them | ||
| ([#6653](https://github.com/Hmbown/Codewhale/issues/6653)). |
There was a problem hiding this comment.
| "turn.steered", | ||
| "turn.steer_dropped", | ||
| "turn.interrupt_requested", | ||
| "turn.artifacts", |
There was a problem hiding this comment.
🔍 Subscribed artifact event has no consumer
The browser subscribes to turn.artifacts, but applyRuntimeEvent leaves its cached turn unchanged. This page has no artifact view; investigate whether the subscription is needed before adding one.
Was this helpful? React with 👍 or 👎 to provide feedback.
Reconciled with #6645: one snapshot authority
Event::TurnWorkspaceSnapshots,WorkspaceSnapshot/SnapshotUnavailable, theturn_workspace_snapshotswire event, the watch-channel settlement, and therestore_snapshot_id/restore_snapshot_session_idstamp in tool result metadata.session_idisthread.id, notthread.session_id.pre_turnandpost_turnreceipts recorded on the turn. Withrecord_restore_points, both arrive beforeTurnComplete, so nothing waits on a post-turn task.settlement_timeoutis gone.pre_turn_snapshot_id/post_turn_snapshot_idare tree ids, because a prune rewrites commit ids.restore_snapshot_idis the tree id of a receipt recorded on the thread's own turn. A tool write gets its call'stoolreceipt and a delta change gets the turn'spre_turnreceipt. File-revert accepts every id a ref advertises, whether or not the thread is bound to a saved session. A tool's own result metadata can no longer name a restore point.snapshots_disabled, gate reasons,snapshot_failed(a pre-turn receipt with no post-turn one) ornot_captured(no receipts).Items 1 and 2 below and the "Restore point gating" and "Settlement bound" review points describe the pre-reconcile design. The section above supersedes them.
Closes #6653
Summary
Runtime turns now carry typed artifact references for what they produced. The desktop Preview can show a turn's files and large outputs without the user hunting for them.
This reuses the existing stores: the snapshot side repo and the session artifact directory. The engine is still the only process that takes snapshots; the runtime only reads the side repo.
The PR lands as four ordered commits, and the build was green after each one:
9c763e068).apply_patchaddsize/sha256to eachmutation.files[]entry.artifact_digest.tool:<call>restore snapshot and that snapshot's session tag.TurnItemRecord.artifacts. Refs are confined, and forged or unconfined paths produce no ref.artifact_refsis now a derived projection holding only workspace file paths, so the pinned app's Preview and transcript cards get data with no app change and no engine-pin bump.a920c4866).TurnWorkspaceSnapshots(pre-turn result plus a watch receiver for the post-turn snapshot) immediately beforeTurnComplete.TurnCompletestays non-blocking (Post-turn UI freeze: terminal unresponsive for seconds after stream ends before copy/paste/selection works #234).SnapshotRepo::diff_snapshots/tracked_pathsare read-only.merge_turn_artifactsfunction computesTurnRecord.artifacts.TurnRecord.workspacegoespending→settled|unavailable(reason), andturn.artifactsis published on every outcome.GET /v1/threads/{id}/turns/{turn_id}/artifacts.fae4d82cf).GET …/artifacts/{artifact_id}?offset=&limit=&revision=.resolve_session_artifact, which is now shared by the session route and the turn route.c5fe3ff95): the "Turn artifacts" section indocs/RUNTIME_API.md, the endpoint list, events and data model.Review points addressed
restore_snapshot_session_id.EngineConfig.session_id). Before, a first turn skippedSyncSessionand tagged its snapshots with a random id that file-revert refused.workspace_changed_during_turn. The docs say it includes concurrent writers (editor, other threads, background jobs).tracked_pathsso it never mistakes "invisible" for "unchanged".workspace_too_large,too_many_files,unsafe_location.unavailable(not_captured). A restart getsunavailable(runtime_restarted).protocol_parity.rsmaps the new event to aturn_workspace_snapshotswire twin.artifacts::artifact_sessions_root(), the same function the writer uses. The classic spill is now immutable, as adaptive evidence already was.Out of scope
artifactsand the turn routes. That needs an engine-pin bump, which is a founder decision; the legacy projection already fills the pinned app's cards for file writes.Testing
Local, with
CARGO_BUILD_JOBS=3viascripts/dev-cargo.sh/scripts/dev-test.sh. The counts are real:cargo fmt --all -- --check: pass.cargo clippy -p codewhale-tui -p codewhale-protocol --all-targets --all-features --locked -- -D warnings -A clippy::uninlined_format_args -A clippy::too_many_arguments -A clippy::unnecessary_map_or: pass. Only the touched crates were run, not--workspace.origin/main: 1557 passed, 6 ignored. The areas covered were runtime_threads, runtime_api, core::engine, core::turn, protocol_parity, snapshot, tools::truncate/file/apply_patch and artifacts.codewhale-protocol: 73 + 17 passed.diff_snapshots(created with a shell-style write, updated, deleted, renamed, over 16 MiB, bounded) andread_blob;TurnCompleteand the restore-point stamp;?revision=, 409/410/413/403/404, and a spill with no SavedSession.split/module_graph.py --check: pass.node_modulesare not installed locally.--range-audit-advisory: OK. check-contributor-credit v0.10.0: OK.cargo test --workspacesuite was not run locally; CI owns it.Not done: the manual live check. I did not run
codewhale servewith a real model turn (apply_patch + shell write + large output, then curl the routes), and I did not check the pinned desktop app's Preview cards. Both need a real provider call, which is provider spend. The route behaviour is covered by the loopback API test against a real snapshot repo.Checklist
runtime_threads/turn_artifacts.rsreplaces the never-filledartifact_refsauthority (now derived).runtime_api/turn_artifacts.rsshares the session resolver, and the session route's inline read logic moved into it.docs/RUNTIME_API.md, CHANGELOG)🤖 Generated with Claude Code