Per-tool-call filesystem snapshots (hostlib)

harn-hostlib ships a Gemini-style /restore primitive paralleling the staged filesystem mode: a snapshot captures the pre-image of paths a single mutating tool call is about to touch, so a client can surgically roll the change back without affecting untracked work.

The capability is registered by harn_hostlib::install_default as part of the fs module:

MethodResult
hostlib_fs_snapshotRegister (and optionally pre-capture paths for) a snapshot keyed by scope_id — canonically the ACP toolCallId.
hostlib_fs_restoreWrite the captured pre-image back onto disk; delete paths the snapshot saw as absent.
hostlib_fs_list_snapshotsList the snapshots registered for a session, sorted by capture time.
hostlib_fs_drop_snapshotRemove a snapshot's in-memory and on-disk state.

Capture modes

Explicit. Pass a paths list to hostlib_fs_snapshot; the bytes are copied into the snapshot immediately:

hostlib_fs_snapshot({
  session_id: "sess_abc",
  scope_id: "tc_42",
  paths: ["/work/src/lib.rs"],
})

Auto-on-write. Omit paths and the snapshot is "open" — the mutating tool builtins (hostlib_tools_write_file, hostlib_tools_delete_file) lazy-capture each path's pre-image into the active open snapshot bound to the current harn_vm::agent_sessions::current_tool_call_id. The agent loop sets that thread-local automatically when it dispatches a tool call, so a single hostlib_fs_snapshot({session_id, scope_id}) registration is enough to roll the next mutation back.

Storage layout

<workspace>/.harn/state/snapshots/<session_id>/<snapshot_id>/
  manifest.json     # path -> { kind: "file" | "absent", body_hash?, mode? }
  bodies/<sha256>   # content-addressed; deduped across snapshots

Snapshot manifests record the snapshot id, originating scope id, capture timestamp, workspace root, and per-path entries. File bodies are content-addressed by SHA-256.

When a session bundle exceeds the per-session byte cap (harn_hostlib::fs_snapshot::DEFAULT_SESSION_BYTE_CAP, 1 GiB) the oldest snapshots are evicted in insertion order. Embedders can override the cap per session with harn_hostlib::fs_snapshot::configure_session_byte_cap. The ACP server also calls harn_hostlib::fs_snapshot::drop_session_snapshots on session/close so closed sessions never leak their snapshot bundles.

Snapshots are session-scoped and ephemeral. Durable rollback across process restarts is out of scope; consumers that need it bundle the relevant state into a session via session/load.

ACP surface

The ACP adapter advertises the primitive in the initialize response under agentCapabilities.sessionCapabilities:

{
  "sessionCapabilities": {
    "close": {},
    "list": {},
    "resume": {},
    "rollback": {},
    "redo": {},
    "restoreToolCall": {}
  }
}

Clients drive a restore with:

{
  "method": "session/restore_tool_call",
  "params": {
    "sessionId": "sess_abc",
    "toolCallId": "tc_42"
  }
}

The server routes the call through harn_hostlib::fs_snapshot::restore and emits a canonical session/update carrying the restored paths and a _meta.harn.kind = "tool_call_restored" discriminator:

Completed prompt-turn rollback uses the same snapshots through session/rollback and session/redo. Harn owns the transcript checkpoint stack; hostlib owns the file pre-images. During rollback the ACP adapter captures redo file snapshots, restores the turn's original snapshots, and then moves the transcript back to the checkpoint boundary.

{
  "sessionId": "sess_abc",
  "update": {
    "sessionUpdate": "tool_call_update",
    "toolCallId": "tc_42",
    "status": "restored",
    "_meta": {
      "harn": {
        "kind": "tool_call_restored",
        "restoredPaths": ["/work/src/lib.rs"]
      }
    }
  }
}