Repository navigation
Description
Note from the reporter: This is an issue I've experienced quite a bit - I haven't personally investigated it but Opus4.6 did a pretty deep dive on it; essentially what happens is at some point in the session the review panel stops showing updates - I still see all changes in the session history, but the review and changes panels stop showing updates. Full details from the agent are below - I have experienced the issue several times in several sessions over several opencode versions using several different models and providers. I truly hope the information below is as accurate as possible, I'm unfortunately unfamiliar with the opencode codebase and otherwise I wouldn't be able to provide as much detail or research as the model has; even if the research is off, the description of the issue and when/how/where it appears should 100% be correct; hope you don't hate me for submitting extra detail in a report from a model
Versions affected
- Current version: 1.3.0 (updated during session)
- Session originally created on: 1.2.26
- Also reproduced on: 1.2.21 (separate project, claude-opus-4.5, 4,591 steps — same failure)
- Previously used versions with the issue: 1.1.28, 1.1.33, 1.1.34, 1.1.42, 1.2.21, 1.2.26, 1.2.27, 1.3.0
Description
The review panel shows no diffs (empty) for sessions that have been active for more than ~7 days, or for sessions with high step counts. The session history panel still shows individual changes correctly — the issue is specifically with the aggregated review/changes view.
Root cause
snapshot/index.ts uses git write-tree to capture state but never creates commit objects, leaving all tree objects unreachable in git's object model. The cleanup() function runs git gc --prune=7.days hourly, which prunes these unreachable trees. When summary.ts's computeDiff() later tries to diff the first step-start hash against the last step-finish hash, the from hash no longer exists in the object store, git diff-tree fails with fatal: bad object, and the error is silently swallowed — producing an empty diff.
Steps to Reproduce
- Start a session in a git-tracked project
- Make file changes across multiple assistant turns (generating step-start/step-finish parts with snapshot hashes)
- Either:
- Continue the session for 7+ days, OR
- Generate hundreds of steps (Claude models via github-copilot provider produce 3-15x more steps per turn than other models due to parallel tool calls)
- Wait for
cleanup()to rungit gc --prune=7.days(runs hourly with 1-minute initial delay) - Open the review panel
Expected: Review panel shows diffs of all changes made during the session
Actual: Review panel is empty — no diffs shown. Session history still shows individual changes.
Detailed Root Cause Analysis
The bug chain
Step 1: snapshot/index.ts — track() creates unreachable trees
const track = Effect.fnUntraced(function* () {
// ...
yield* add() // git add .
const result = yield* git(args(["write-tree"]), { cwd: state.directory })
return result.text.trim() // Returns a TREE hash — no commit is ever created
})git write-tree creates a tree object from the current index. This tree is not referenced by any commit, tag, or ref, making it an unreachable object that is eligible for garbage collection.
Step 2: processor.ts — stores hashes in step parts
On start-step: calls Snapshot.track(), stores tree hash in step-start part.
On finish-step: calls Snapshot.track() again, stores tree hash in step-finish part.
Step 3: snapshot/index.ts — cleanup() prunes unreachable objects
const cleanup = Effect.fnUntraced(function* () {
yield* git(args(["gc", "--prune=7.days"]), { cwd: state.directory })
})Runs every hour. --prune=7.days destroys any unreachable object older than 7 days. Since the tree objects from write-tree are never anchored by commits, they are always unreachable.
Step 4: summary.ts — computeDiff() fails silently
export async function computeDiff(input: { messages: MessageV2.WithParts[] }) {
let from: string | undefined // earliest step-start snapshot hash
let to: string | undefined // latest step-finish snapshot hash
// ... scans all messages for first step-start and last step-finish ...
if (from && to) return Snapshot.diffFull(from, to)
return []
}When diffFull(from, to) calls git diff-tree with a pruned from hash, git returns fatal: bad object. The error is silently swallowed (likely via .nothrow()), producing an empty result.
Evidence from database forensics
In a 6-day session (session ses_30601aff5ffe0sU89p1Q7fZvjy) with 1,200 steps:
| Metric | Value |
|---|---|
| Distinct tree hashes in DB | 185 |
| Still alive in object store | 179 |
| Garbage collected | 6 |
First step-start hash |
9841247c... (DEAD — created Mar 17) |
Last step-finish hash |
106a1529... (alive — created Mar 23) |
computeDiff() result |
Empty (fatal: bad object on from) |
The 6 dead hashes are all from the first day of the session (Mar 17) — exactly matching the 7-day prune window.
A second session on a different project (ses_32a2d666, claude-opus-4.5, v1.2.21, 4,591 steps, "PREFLIGHT_OK") shows the identical failure pattern: first hash gone, summary_files=0.
Per-step snapshots DO work
Individual step pairs correctly capture changes. For example, when editing faq.md:
step-startat 07:06:04 → hashd4333c...step-finishat 07:06:11 → hash857b11...(different — change captured!)patchpart correctly listsfaq.mdin changed files
The per-step mechanism works. The problem is only with the session-level computeDiff() which needs the very first hash from the session, and that's the one most likely to be gc'd.
Why this disproportionately affects Claude/Opus models
Claude models produce dramatically more steps per session than other models:
| Model | Provider | Steps | Review works? |
|---|---|---|---|
| claude-opus-4.5 | github-copilot | 4,591 | No |
| claude-opus-4.6 | github-copilot | 1,200 | No |
| minimax-m2.5-free | opencode | 316 | Yes |
| glm-4.7-free | opencode | 60–130 | Yes |
Each step calls track() → git add . && write-tree, creating more unreachable objects. More steps = more object churn = higher chance that git gc --auto triggers sooner (git auto-gc triggers based on loose object count). Combined with longer session lifetimes typical of complex multi-day work, these sessions reliably hit the prune window.
Related Issues
| Issue | Status | Relevance |
|---|---|---|
| #15977 | Closed (not planned) | Directly identifies that write-tree creates unreachable tree objects |
| #12719 | Open | Same symptom (stale hash), different trigger (git add failure) |
| #10034 | Open | Root cause analysis noting .nothrow() silences errors |
| #17397 | Open (has PR #17423) | Notes prune=7.days hardcoding |
| #13065 | Open | Same symptom (empty review panel), undiagnosed |
| #10716 | Open | session_diff/*.json all empty |
Proposed Fixes
Fix 1: Anchor trees with commits (recommended)
After write-tree, create a commit object to make the tree reachable:
const track = Effect.fnUntraced(function* () {
if (!(yield* enabled())) return
yield* init()
yield* add()
const treeResult = yield* git(args(["write-tree"]), { cwd: state.directory })
const hash = treeResult.text.trim()
// Anchor the tree with a commit to prevent gc
const parentRef = yield* git(
args(["rev-parse", "--verify", "refs/snapshots/latest"]),
{ cwd: state.directory }
).pipe(Effect.option)
const commitArgs = parentRef._tag === "Some"
? ["commit-tree", hash, "-p", parentRef.value.text.trim(), "-m", `snapshot`]
: ["commit-tree", hash, "-m", `snapshot`]
const commitResult = yield* git(args(commitArgs), { cwd: state.directory })
yield* git(
args(["update-ref", "refs/snapshots/latest", commitResult.text.trim()]),
{ cwd: state.directory }
)
return hash // Still return tree hash — git diff-tree works with both tree and commit hashes
})Fix 2: Disable gc or use --prune=never
Simplest change — modify cleanup():
yield* git(args(["gc", "--prune=never"]), { cwd: state.directory })This prevents pruning at the cost of unbounded disk growth in the snapshot repo.
Fix 3: Pin individual tree hashes with refs
yield* git(args(["update-ref", `refs/snapshots/t-${hash.slice(0, 12)}`, hash]), { cwd: state.directory })Lighter than full commits but accumulates refs.
User Workaround
Starting a new session resets the baseline. The new session's first step-start hash will be fresh and won't be gc'd for at least 7 days.
Alternatively, users can manually anchor trees:
SNAP_DIR=~/.local/share/opencode/snapshot/<project_id>
TREE=$(git --git-dir="$SNAP_DIR" --work-tree="$(pwd)" write-tree)
COMMIT=$(git --git-dir="$SNAP_DIR" commit-tree "$TREE" -m "anchor")
git --git-dir="$SNAP_DIR" update-ref refs/snapshots/anchor "$COMMIT"
Activity
ForbiddenEra commentedon Mar 27, 2026
Again, I'm really not 100% sure about the root cause or anything, I sent claude 4.6 down the rabbit hole and everything. I've seen it happen sometimes and not others too.. like I have one project open, where I'm more just using the model to do some file management (myrient, iykyk) but it's definitely created some scripts, I see no changes or anything, I'm like 99.9% sure it hasn't created a git repo, though it doesn't say "create a git repo" in the ui..
but I have another project using the same model where I had it start writing some stuff (one prompt so far) and opened it back up and "create a git repo" was a button, so I did, and now I see changes in that one (so far, who knows if/when it'll break?)
this does feel like basic functionality though, not sure why it seems to be broken; I definitely feel like it miiiiiiight be related to the model but maybe not, I haven't done enough testing to be sure but like, I have 7 oc projects open and like 1 works, I think, I just checked another, it shows a change to one file but not others.
:'(
ForbiddenEra commentedon Mar 29, 2026
so I just tried with both ChatGPT and Opus, if I created a git repo FIRST before opening in OC, no issue?!
closing a project, deleting the project dir, re-creating and opening just opens the old session tho (with same issue)
ForbiddenEra commentedon Mar 29, 2026
also if this is the case then the above research is either hallucinogenic-ish or a separate bug; I will research further and update..?
ForbiddenEra commentedon Mar 29, 2026
Update: Different Root Cause Identified
After deeper investigation, I've found that the issue I'm experiencing (and likely many of the "empty review panel" reports) is actually a different and more fundamental bug than the
git gc --prune=7.daysissue described in the original report. The gc/prune issue is likely real and separate, but what I'm actually hitting is simpler and more common.The Actual Root Cause: Snapshots Disabled for Non-Git Directories
File:
packages/opencode/src/snapshot/index.ts—enabled():When OpenCode opens a directory that has no
.gitdirectory,fromDirectory()inproject.tssetsvcs: undefined. The snapshot system checksstate.vcs !== "git"and returns false — snapshots are completely disabled. No tree hashes are ever recorded in step parts.computeDiff()finds nofrom/tohashes → empty review panel.Evidence from my database: Sessions for directories opened without git have step parts with NO
"snapshot"field:{"type":"step-start"} {"reason":"tool-calls","type":"step-finish",...}Compare to sessions where
.gitexisted when the project was opened:{"snapshot":"4b825dc642cb6eb9a060e54bf8d69288fbee4904","type":"step-start"} {"reason":"tool-calls","snapshot":"da4cd800edf4abd7c6ebdb3870325f4ca5d8460b","type":"step-finish",...}The Global Project Problem Makes This Worse
When no
.gitis found, the project gets assigned toProjectID.global(literal string"global"). The upsert infromDirectory()overwrites the global project row every time ANY non-git directory is opened, including thevcsandworktreefields:This creates cascading issues:
The "Create a Git Repo" button is shown/hidden based on global state, not per-directory state. The button appears when
project.vcsis falsy (session.tsxline ~640). Since all non-git projects share the global project row, the button visibility depends on which directory was opened LAST — if the last directory happened to have.git,vcs="git"and the button is hidden for ALL non-git directories.Snapshot repos can point to wrong directories. The snapshot path uses
Hash.fast(ctx.worktree), but the global project's worktree keeps getting overwritten. In my case, thesnapshot/global/directory contains:sez-bitest, opened months ago)23c1e088.../→ worktree/home/.../foapp(opened later with.git)5d27e0d2.../→ worktree/home/.../octest(opened later with.git).git)Even
git initwithout commits doesn't fully fix it. If.gitexists but there are no commits,git rev-list --max-parents=0 HEADreturns nothing, so the project STILL falls back toProjectID.global. You get a proper snapshot subdir andvcs="git", but still share the global project row. Only after making at least one commit does the project get its own ID.Reproduction Steps (Simplified)
opencode web, navigate to the directory)What Works vs What Doesn't
git init+ at least 1 commit BEFORE opening in OCvcs="git"git init(no commit) BEFORE opening in OCvcs="git"vcs=null, snapshots entirely disabledgit initmid-session (by assistant or user)vcs=null, no re-detectionSuggested Fixes
Remove the VCS gate from
enabled(). The snapshot system creates its own independent git repo in~/.local/share/opencode/snapshot/— there's no technical reason to require the user's project to also have git. Changeenabled()to just check the config flag.Don't share
ProjectID.globalacross unrelated directories. Each non-git directory should get a unique, stable project ID (e.g., hash the absolute path). The current design means opening any non-git directory overwrites the global project's worktree/vcs, breaking all other non-git projects.Watch for
.gitcreation and re-detect. If the user or assistant runsgit initmid-session, OpenCode should detect this and callInstance.reload()to re-initialize the project.Fix the "Create a Git Repo" button visibility. It should be based on the current session's directory having a
.git, not on the shared global project'svcsfield.Expose a "Re-detect Project" action in the UI (or auto-trigger it) so users can recover from this state without restarting.
Relationship to Original Report
The
git gc --prune=7.daysissue from the original report is likely a separate, real bug that affects long-running sessions where snapshots ARE working. It would cause review to break after ~7 days when the initial tree hashes get garbage collected. But many of the "empty review panel" reports (including mine) are caused by this more fundamental issue: snapshots never being created in the first place because no git repo existed when the project was first opened.Versions Tested
github-actions commentedon Jul 3, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.