fix(cli): a run pulled from a gateway can be forked, and says so when it cannot - #89
Conversation
… 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>
There was a problem hiding this comment.
🐳 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>
There was a problem hiding this comment.
🐳 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
Orca-Code-Review — push 2
✅ 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":
Copy-pasted verbatim, the second command dies:
Why
orca pullnever brings the workspace snapshots down, and cannot. The archive allowlist ismanifest.json,events.jsonl,redactions.jsonandblobs/—PUSHED_TOP_LEVELinsync.ts, and the same set the gateway enforces. I checked the gateway's half by pushinghand-built archives one member at a time:
fs/objects/<ab>/<hex>"…/fs/objects/…" is not part of a trace archivefs/HEAD,fs/config,fs/hooks/…,fs/info/excludeshell-frames.jsonlSo a pulled run's
fs.snapshotevents name trees whose objects were never sent, and therestore had no way to know that.
replay.tsalready guarded the neighbouring mistake — restoring from the wrong store — andsays 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 waygitlinksalreadyanswers "would this restore refuse?" before it refuses.
fork.no_snapshot— the tree, the store, the cause, where the runwas recorded, and the command that works — then fails with one line. The detail goes in the
warning because
comparerenders each leg's error in one cell of a table, and anything pastthe first line is lost exactly where it is most likely to be read.
--no-fsnow covers the restore, not just the fork's own capture, so "the conversation iswhat I am forking" is sayable. It was undocumented on
replay, missing fromcompare'sallowlist, and dropped from the argv
comparebuilds per fork — the silent no-op that listalready warns about twice, for two spellings of the same fork.
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_snapshotandrefuses 3/3,
--no-fspasses 3/3. Forking in the directory that still has the store restoresnotes.mdandsrc/mod.jsinto 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 thischange 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 runis one of those 10 and fails identically with this change stashed.Not in scope
orca push --fsstill packsfs/and the gateway still refuses it —--fshas been unable tocomplete 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
--fsgoes,or push translates the rejection. It is not what the console's "fork it locally" path needs.
🤖 Generated with Claude Code