Skip to content

fix(cli): a run pulled from a gateway can be forked, and says so when it cannot - #89

Merged
xizhuomengcontin merged 2 commits into
mainfrom
fix/fork-without-snapshots
Sep 16, 2026
Merged

xizhuomengcontin merged 2 commits into
mainfrom
fix/fork-without-snapshots

Conversation

@xizhuomengcontin

@xizhuomengcontin xizhuomengcontin commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Orca-Code-Review — push 2

Severity Count Δ vs previous push
P0 0 0
P1 0 0
P2 0 0
P3 0 0

✅ no blocking findings

The gateway console prints this under Compare from step → "fork it locally", with the
promise "fork from this step with the same files and conversation prefix":

orca pull '<run>'
orca compare last --from 2

Copy-pasted verbatim, the second command dies:

git read-tree failed (128): fatal: failed to unpack tree object 4ec8e609ea10fcd77d2d3a36374e89c031ee830b

Why

orca pull never brings the workspace snapshots down, and cannot. The archive allowlist is
manifest.json, events.jsonl, redactions.json and blobs/PUSHED_TOP_LEVEL in
sync.ts, and the same set the gateway enforces. I checked the gateway's half by pushing
hand-built archives one member at a time:

member verdict
the three files accepted
fs/objects/<ab>/<hex> "…/fs/objects/…" is not part of a trace archive
fs/HEAD, fs/config, fs/hooks/…, fs/info/exclude same
shell-frames.jsonl same

So a pulled run's fs.snapshot events name trees whose objects were never sent, and the
restore had no way to know that.

replay.ts already guarded the neighbouring mistake — restoring from the wrong store — and
says so in a comment right above the call. It could not guard against there being no store at
all, because nothing asked.

What this changes

  • ShadowIndex.has(tree) answers it without writing a byte, the way gitlinks already
    answers "would this restore refuse?" before it refuses.
  • A missing tree reports fork.no_snapshot — the tree, the store, the cause, where the run
    was recorded, and the command that works — then fails with one line. The detail goes in the
    warning because compare renders each leg's error in one cell of a table, and anything past
    the first line is lost exactly where it is most likely to be read.
  • --no-fs now covers the restore, not just the fork's own capture, so "the conversation is
    what I am forking"
    is sayable. It was undocumented on replay, missing from compare's
    allowlist, and dropped from the argv compare builds per fork — the silent no-op that list
    already warns about twice, for two spellings of the same fork.
$ orca compare <pulled-run> --from 2 --models deepseek/deepseek-v4-flash
warn fork.no_snapshot seq=1 tree=4ec8e609ea10… store=…/.orca/runs/<run>/fs
     why="a gateway archive carries manifest, events, redactions and blobs — never the
          filesystem snapshots, so a pulled run names trees whose objects were never sent"
     recorded_in=… next="orca replay <run> --from 1 --no-fs   # fork the conversation, without the workspace"

MODEL                       VERDICT  RUN
deepseek/deepseek-v4-flash  fail     no workspace snapshot for the checkpoint at seq 1 — see fork.no_snapshot; --no-fs forks without it

$ orca compare <pulled-run> --from 2 --no-fs --models deepseek/deepseek-v4-flash
deepseek/deepseek-v4-flash  pass     208/310  7.2s  run_d71af6b7a178

Verification

Against the live gateway, three rounds of record → push → pull → compare, with a non-empty
workspace so the checkpoint names a real tree: the default reports fork.no_snapshot and
refuses 3/3, --no-fs passes 3/3. Forking in the directory that still has the store restores
notes.md and src/mod.js into the worktree exactly as before.

A run whose checkpoint is git's empty tree needs none of this — the empty tree resolves in any
repository — so a conversation-only run pulls and forks with no friction, which is the common
case and now measurably so.

The new test deletes the run's fs/ store, which is what the wire leaves behind. Without this
change it fails with the original failed to unpack tree object; with it, both branches pass.

Full suite on Windows: 10 failed / 2414 passed / 40 skipped — the same 10 pre-existing platform
failures as main, one more passing test. compare.test.ts > forks every model from the SAME parent run is one of those 10 and fails identically with this change stashed.

Not in scope

orca push --fs still packs fs/ and the gateway still refuses it — --fs has been unable to
complete since it was added in #35, and was unreachable behind the flag allowlist until #88 made
it callable. Worth settling separately: either the wire learns to carry snapshots, or --fs goes,
or push translates the rejection. It is not what the console's "fork it locally" path needs.

🤖 Generated with Claude Code

… it cannot

`orca pull` never brings the workspace snapshots down. A gateway archive carries
manifest.json, events.jsonl, redactions.json and blobs/ — the allowlist both
sides implement — and never the `fs/` shadow store, so a pulled run's
`fs.snapshot` events name trees whose objects were never sent.

The fork path restored from those trees unconditionally and died inside git:

    git read-tree failed (128): fatal: failed to unpack tree object 4ec8e609ea10…

which names neither the cause nor anything to do about it. That is the exact
path the gateway console prints under "fork it locally": `orca pull <run>`, then
`orca compare last --from N`.

`replay.ts` already guarded the neighbouring mistake — restoring from the WRONG
store — and said so in a comment. It could not guard against there being no
store at all, because nothing asked.

- `ShadowIndex.has(tree)` answers it without writing a byte, the same way
  `gitlinks` already answers "would this restore refuse?" before it refuses.
- Missing tree now reports `fork.no_snapshot` with the tree, the store, the
  cause, where the run was recorded, and the command that works — then fails
  with one line, because `compare` renders each leg's error in one table cell
  and anything past the first line is lost exactly where it is most read.
- `--no-fs` now covers the restore as well as the fork's own capture, so
  "the conversation is what I am forking" is sayable. It was undocumented on
  `replay`, absent from `compare`'s allowlist, and dropped from the argv
  `compare` builds per fork — the silent no-op that list already warns about
  twice, for two spellings of the same fork.

Verified against the live gateway, three rounds of record → push → pull →
compare: the default reports `fork.no_snapshot` and refuses, `--no-fs` passes.
Forking in the directory that still has the store restores the workspace as
before. A run whose checkpoint is git's empty tree needs none of this — the
empty tree resolves anywhere — so a conversation-only run pulls and forks with
no friction at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@orcacode-review orcacode-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🐳 OrcaCode Review

No findings — nothing to flag in this PR. Great work!

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 764 calls · 81.9M tokens · 99% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

`prettier --check .` is the first step of the CI check job, and the argv array
in the new test was written compact where prettier wants one element per line.
Nothing else changed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@orcacode-review orcacode-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🐳 OrcaCode Review

No findings — nothing to flag in this PR. Great work!

OrcaCode Review — Route Smarter. Ship Safer. Spend Less.
Engine-reported: 521 calls · 45.1M tokens · 99% cached

❤️ Share · Install OrcaCode Review

Free on GitHub — the review runs on your own OrcaRouter key. If it helped, a shout-out goes a long way.

Share: X · Reddit · LinkedIn
Follow: X · Discord · LinkedIn · OrcaRouter

@xizhuomengcontin
xizhuomengcontin merged commit 0f71c7e into main Sep 16, 2026
6 checks passed
@xizhuomengcontin
xizhuomengcontin deleted the fix/fork-without-snapshots branch September 16, 2026 02:12
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.

1 participant