Skip to content

fix(core): ignore a corrupt cached project id - #54030

Closed
argszero wants to merge 2 commits into
anomalyco:devfrom
argszero:corrupt-project-id
Closed

argszero wants to merge 2 commits into
anomalyco:devfrom
argszero:corrupt-project-id

Conversation

@argszero

@argszero argszero commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #53991

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

A git repo whose .git/opencode cache file holds control bytes (the report shows 40 NUL bytes) resolved to that value as its project id. Project.cached() read the file, called trim(), and handed whatever survived to ID.make(). trim() only strips whitespace, so NUL bytes passed through untouched and became the id.

The id is not just an opaque key: it is used as a path segment for the shell output directory (<data>/shell/<id>). A NUL byte in a path makes every later filesystem call fail with path ... without null bytes, which is the UnexpectedStatus: 500 in the report, and the TUI cannot offer model selection for that project. Because the cached value was treated as authoritative, the same broken id was re-derived on every resolve — so the stale row stayed in project and the failure repeated, as @layermedya noted.

This change treats a cached value carrying control characters as absent, so resolution falls through to the sources that already work — @layermedya confirmed renaming .git away resolves the directory to a valid id. The cache stays a convenience rather than an authority: a corrupt file no longer becomes the id. git log on the region shows cached() was added as a fast path in front of the real resolution sources; rejecting a value that cannot be a valid id keeps that contract, and a usable cached value is still used unchanged.

How did you verify your code works?

Two tests in packages/core/test/project.test.ts, beside the existing "returns previous cached id from common dir":

  • keeps a usable cached id — a normal cached value is still used as both previous and id, so the fast path is unchanged.
  • ignores a corrupt cached id — a cache file of 40 NUL bytes: previous is undefined, id resolves to the root commit id from the normal source, and the id contains no NUL byte.

Red/green on v2 at 1a8988efe0, pinned bun 1.4.2:

  • before: ignores a corrupt cached id fails because Received is 40 NUL bytes instead of undefined
  • after: both new tests pass

Full packages/core suite: 6519 pass / 49 fail after, versus 6518 pass / 50 fail before — the single delta is this new test. The 49 remaining failures are pre-existing on this machine and unrelated (pty, Session.shell, real-shell and ripgrep-dependent tests fail in this sandbox).

Also clean: tsgo -b tsconfig.json tsconfig.tests.json for packages/core, and oxlint on both edited files.

Screenshots / recordings

If this is a UI change, please include a screenshot or recording.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

A cache file written with control bytes (e.g. NUL) made Project.resolve adopt that value as the project id. The id is also a path segment, so every later start failed and the TUI could not offer model selection, while the corrupt value stayed in the project table and regenerated on each resolve.

Treat a cached value carrying control characters as absent so resolution falls through to the normal sources.
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

@argszero

argszero commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Noting the automated pr-standards:issue result: the body does say Closes #53991, but GitHub only resolves closing keywords into the linked-issue field when the base is the repository's default branch.

This change targets v2 for the reasons in the repo's own guidance: the reported failure is on the v2 line, and AGENTS.md states that the default branch here is v2 and that new branches should be based on it rather than on dev. packages/core/src/project.ts does exist on dev too, but that copy is a different module layout (ProjectDirectories-based, with cached() in a very different file), so this is not a change that can be pointed at dev as-is.

Net effect: the linked-issue field stays empty on a v2 base and this check cannot pass by design, the same as on my other v2-based PRs (e.g. #53996, #53983, #53945). The reference is in the body; no further action from me here.

@opencode-agent opencode-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This looks right to me. I reproduced #53991 on the base from source (serve plus run --server, because standalone can't start from source): a git repo whose .git/opencode holds 40 NUL bytes fails with UnexpectedStatus: 500. With this change the project resolves and the run continues. test/project.test.ts passes (23/23), the new corrupt-cache test fails on the base, and the packages/core typecheck is clean.

Nothing blocking. On v2 nothing ever writes the cache file, so skipping a bad value on each start is enough; no repair step is needed. The two added code comments aren't needed for a fix this small and can be dropped so the diff stays minimal.

A cache file written with control bytes (e.g. NUL) made Project.resolve adopt that value as the project id. The id is also a path segment, so every later start failed and the TUI could not offer model selection, while the corrupt value stayed in the project table and regenerated on each resolve.

Treat a cached value carrying control characters as absent so resolution falls through to the normal sources.
@argszero

argszero commented Oct 8, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for reproducing this independently and for confirming the resolution path.

Both added comments are dropped as suggested — the change is now just the CONTROL_CHARACTERS constant and the one guard inside cached(). Pushed as fa5f26ac6b; the diff against v2 is 2 files, +38/-1.

Re-verified after the edit: the two new tests and the rest of test/project.test.ts pass (23/23), tsgo -b for packages/core is clean, and prettier/oxlint report nothing on the changed file.

Noted on the repair step — if nothing on v2 ever writes the cache, skipping the bad value on each start is the whole fix, and I have not added any write path.

@argszero

Copy link
Copy Markdown
Contributor Author

Closing this to stay within GitHub's 3-open-pull-request limit for contributors without write access, which currently blocks every new pull request here. The branch remains on my fork, so I can reopen it on request.

@argszero argszero closed this Oct 20, 2026
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.

project: NUL-byte project ID breaks fromDirectory and blocks TUI model selection

1 participant