anomalyco / anomalyco/opencode

Sessions in a non-git parent directory are unattributable: `resolve` returns `global` before `project_directory` is consulted

Open
#48,870 2 comments 0 reactions 1 assignee View on GitHub

@jlongster is already working on this.

Since Sep 13, 2026.

Dominant language
TypeScript
Stars
209k
Forks
27.5k
PR merge metrics
PR metrics pending

Description

Summary

When a session's directory is not inside a git repository, Project.resolve returns ID.global
immediately. project_directory — the table that already models "directories belonging to a
project", including a type of root — is never read on that path, and nothing else can
associate the directory with a project.

The result is that any automated setup which runs agents from a parent directory containing
several checkouts
puts every one of those sessions into global, where no project-scoped view
can show them. There is currently no supported way to say "this directory belongs to project X"
for a directory that is not itself a git worktree.

Measurement

One host, one server, automated dispatch to agent seats over POST /session/{id}/prompt_async.
Live session store, read read-only:

sessions total                       693
project_id = "global"                411   (59%)   time_updated: now
next-largest project                 130

The global sessions are not scattered — they are concentrated in a handful of directories, each
one a workspace root holding two or more repository checkouts plus shared material:

/scratch/ws-a         69 sessions
/scratch/ws-b         23
/scratch/ws-c         23
/scratch/ws-d         22
/scratch/ws-e         15
…

None of those roots has a .git. Each does contain checkouts that resolve perfectly well on
their own — one of them appears in its project's sandboxes list with zero sessions attributed to
it, while 12 sessions sit in global at the directory one level up.

The project those checkouts belong to shows sandboxes=7 and its most recent session is 8 days
old
, because all recent work was dispatched at the parent.

Reproduction

mkdir -p /tmp/ws && cd /tmp/ws
git clone <some-repo> repo-a
git clone <other-repo> repo-b
mkdir -p .opencode/agents   # cards shared by both checkouts

# dispatch with directory=/tmp/ws
curl -s -X POST "http://127.0.0.1:4096/session?directory=/tmp/ws" …

Then:

select project_id, directory from session order by time_created desc limit 1;
-- global | /tmp/ws

GET /project never lists /tmp/ws, and the session is invisible in any project-scoped UI.

Mechanism

packages/core/src/project.ts, Project.resolve:

const repo = yield* git.repo.discover(input)
if (!repo) return { id: ID.global, directory: AbsolutePath.make(path.parse(input).root), vcs: undefined }

const previous = yield* cached(repo.commonDirectory)
const id = (yield* remote(repo)) ?? previous ?? (yield* root(repo))

The early return happens before any lookup against project_directory.

packages/core/src/project/sql.ts already carries the association the case needs:

export const ProjectDirectoryTable = sqliteTable("project_directory", {
  project_id: text().references(() => ProjectTable.id, { onDelete: "cascade" }),
  directory:  DatabasePath.absoluteColumn().notNull(),
  type:       text().$type<"main" | "root" | "git_worktree">(),
  strategy:   text(),
  …
}, (t) => [primaryKey({ columns: [t.project_id, t.directory] })])

and packages/core/src/project/directories.ts exposes create / remove / contains / list
over it. type: "root" reads as though a non-worktree directory was anticipated.

packages/opencode/src/project/project.ts also skips persistence for global, so nothing is
recorded even as a side effect:

if (input.projectID === ProjectV2.ID.global) return   // saveProjectDirectory

Why the existing escapes don't cover it

  • sandboxes auto-registration works and is self-healing, but only for a directory that
    resolves to a non-global project — i.e. one that is already inside a git repo.
  • POST /project/git/init clears the gate by making the parent a git root, but the parent then
    becomes its own project. For N workspaces that is N single-use projects rather than N
    directories under the project they belong to, which is the opposite of the grouping wanted.
  • Pointing the session at the checkout instead of the parent attributes correctly, but moves
    the sibling directories outside location.directory. In
    packages/core/src/location-mutation.ts, lexicallyInternal is computed against
    location.directory, so siblings then require external_directory grants — and a relative
    path that escapes is rejected outright with relative_escape, which no permission can allow.
    Shared material next to the checkout becomes unreachable.
  • ProjectDirectories.create would express it exactly, but is not routed over HTTP
    (GET /project/:projectID/directories is the only directory endpoint) and, more importantly,
    resolve would not read the row.

Proposal

Consult project_directory in Project.resolve before falling back to global:

const repo = yield* git.repo.discover(input)
if (!repo) {
  const owner = yield* directories.ownerOf(input)          // (project_id, directory) lookup
  if (owner) return { id: owner, directory: input, vcs: undefined }
  return { id: ID.global, … }
}

and expose create / remove on the existing directories route so a caller can register a
directory without a git repository. Both pieces exist; the change is the lookup and the two
routes.

Happy to send a PR if the shape is agreeable — in particular whether an explicit association
should also win over git discovery, or only serve as the fallback as written above.

Environment

opencode 0.0.0-fix/phantom-unknown-tool-parts-202608232224

Read against origin/dev @ df23b7f948; every file cited above is identical there.

Aside, possibly a separate issue

Unmatched paths on the server return the SPA shell with HTTP 200 rather than 404 — e.g.
POST /nonsense-route-xyz200 with <!doctype html>. That makes probing the HTTP surface
misleading, since a 200 is not evidence a route exists. Happy to split this out.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.