Batch git hash-object when fingerprinting a working-tree review target - #110
Merged
Conversation
patriyang
added a commit
that referenced
this pull request
Aug 11, 2026
… the ones they should not be offered (#111) Closes #109 Closes #105 Two changes on one surface: whether a Codex thread this plugin starts can *use* the MCP servers it is offered, and whether the operator can decide which ones it is offered at all. #105 was filed as an open policy question with three options and a prerequisite nobody had tested. Probing a live app-server to settle that prerequisite turned up #109, which changes the premise — so the two land together. ## #109 — every MCP tool call was failing as a phantom user rejection Reproduced through the shipped entry point, before the fix: ``` node plugins/codex/scripts/codex-companion.mjs task --json \ "Call the codegraph MCP status tool once and report the indexed file count..." → "failureMessage": "user rejected MCP tool call" ``` No user was asked. Tracing the JSONL against a raw `codex app-server`, the app-server sends a server→client **request** `mcpServer/elicitation/request` carrying `_meta.codex_approval_kind: "mcp_tool_call"`, and `AppServerClientBase.handleServerRequest` answered *every* server request with `-32601`. The app-server reads that refusal as a denial rather than as "this client does not implement that method", and reports it to the model as a user rejection. `handleServerRequest` now answers elicitations: - **approval-kind** (`_meta.codex_approval_kind` present) → `{ action: "accept", content: {}, _meta: null }`. These threads run `approvalPolicy: "never"` with nobody at the keyboard; an approval gate has no one to answer it. - **anything else** — a real data-collection form, or `mode: "url"` → `{ action: "decline", content: null, _meta: null }`. Fabricating form values would be worse than declining, and a protocol-valid decline still avoids the phantom rejection. - **every other server method** keeps `-32601`, which is the right answer for a method the client genuinely does not implement. Both transports inherit this from the base class, so the broker path is covered too — and in shared mode the broker is the process that answers, which is worth knowing when testing: a broker started before this change keeps rejecting until it restarts. This means MCP tool calls from plugin threads are now approved unattended. That is deliberate rather than incidental, it is the direction the operator picked, and it is stated plainly in the README — an MCP tool is not confined by the thread's sandbox the way a shell command is. The knob below is the control. ## #105 — `CODEX_DISABLED_MCP_SERVERS` Comma-separated server names to disable on threads this plugin starts. Unset by default, so nothing changes for anyone who does not opt in — issue option 1 stays the default, option 2 becomes available per-server. Two things worth recording, both established against a live app-server (codex-cli 0.147.0) rather than inferred from the generated types: - **The mechanism the issue guessed does not work.** `config: { mcp_servers: {} }` on `thread/start` leaves every server attached — same at the CLI (`codex exec -c 'mcp_servers={}'`). - **`mcp_servers.<name>.enabled = false` does work**, as a bare dotted key, in both the `thread/start` `config` map and `-c`. That is the lever used here. Names must match `[A-Za-z0-9_-]+`; anything else is skipped with a warning. That is not fussiness: `-c 'mcp_servers."hermes-vault".enabled=false'` fails config loading outright (`invalid transport in mcp_servers."hermes-vault"`) because the quoted segment is read as a *new* server table, so emitting a quoted key would break `thread/start` for the whole run instead of just leaving one server enabled. Hyphens are fine unquoted. Also fixed while in those two functions: `buildThreadParams` / `buildResumeParams` only built `params.config` when `writableRoots` was non-empty, silently dropping `options.config` otherwise. The map is now built unconditionally and attached only when non-empty — with nothing to say, the params are byte-identical to before. ## Verification `node --test --test-concurrency=1 tests/*.test.mjs` — **354 pass, 0 fail** (346 on `main`; +8 here). Tests were written first and confirmed red: reverting only `app-server.mjs` reds the three elicitation tests, and reverting only `codex.mjs` reds the disable tests. New coverage asserts the recorded client reply on the wire (accept / decline / `-32601`) and the recorded `thread/start` and `thread/resume` config maps, via the fake app-server fixture. End-to-end against a live Codex and the shipped entry point, same workspace, only the env var differing: | Run | Result | | --- | --- | | before the fix | `user rejected MCP tool call` | | after, env unset | `CALLED 42` — the tool actually ran | | after, `CODEX_DISABLED_MCP_SERVERS=codegraph` | `NO_CODEGRAPH` — not offered | ## Left alone - **Probing servers for workspace fitness** (issue #105 option 3). It depends on servers answering a health question promptly, which is the exact thing the server behind #100 failed to do. - **`mcp_servers.<name>.tool_timeout_sec` / `startup_timeout_sec`**, which `codex mcp list --json` shows exist per server. A more targeted lever than the global budgets from #104 for the original stall, but a separate change — worth its own issue. - **A wildcard "disable everything" value.** Named servers only until something asks for more. ## Collision note `#102` was in flight in a parallel worktree while this was being built and landed first as #110. This branch is rebased on top of it (version bumped to 1.0.46 rather than 1.0.45), and the full suite was re-run after the rebase: **361 pass, 0 fail**. No overlap between the two changes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
captureRepoStateIdentityfingerprints a working-tree review target so a queued background review can refuse to run once the repository has moved under it. Building that fingerprint hashed content one path at a time: every dirty tracked path and every untracked regular file went through its owngit hash-object --no-filters -- <path>spawnSync.That cost is
O(dirty paths)process startups, paid inline on the--backgroundenqueue path — the interactive call the user waits on before a job ID comes back.Measured before the change on a synthetic repo (N dirty tracked + N untracked files), with
gitinstrumented through a PATH shim:git hash-objectprocessesExactly
2Nprocess startups, ~6.4 ms per path, linear.How
captureWorkingTreeDigestis now classify → batch → assemble:lstatper tracked and untracked path decides whether it is a regular file (needs an OID) or a non-regular path (symlink, directory/gitlink, missing, other), which keeps its existing token logic untouched.hashRegularFilesBatchedhashes the regular files from both sets in chunks bounded by path count (256) and cumulative UTF-8 path bytes (8 KiB, conservative enough to stay well under Windows' ~32767-character command line including the fixed arguments and quoting).git hash-objectaccepts multiple paths and emits one OID per line in argument order; a chunk's output is accepted only when the process succeeded and the non-empty line count equals the chunk length.A chunk that fails for any reason falls back to hashing that chunk's paths individually, so one bad path (deleted mid-run, unreadable) cannot poison its neighbours. The per-path failure result is carried through, so a tracked file that fails to hash still throws with the real spawn error or the actual status/signal/stderr — the same fidelity as the
gitCheckedcall it replaced — while untracked files keep their tolerantskipped:(...)behavior and never throw.Same tree, after:
git hash-objectprocesses51× faster at N=400, and the digest is byte-identical to what the old code produced for the same tree (
e4c0b753…c299a1before and after).Semantics preserved
Every per-path behavior is unchanged:
missing,symlink:<sha256 of readlink target>,submodule:<head>:<status>:<staged>:<unstaged>,other:<mode>:<size>, and the untrackedinspectUntrackedFilebranch — including that theMAX_UNTRACKED_BYTES/ binary / directory display limits still apply only to non-regular untracked paths and never affect regular-file identity. The digest composition (status output, then\0tracked\0…, then\0untracked\0…) is untouched.Tests
Four new tests in
tests/git.test.mjs:gitshim onPATHlogging each subcommand, assertshash-objectinvocations stay a small constant. Verified red against the pre-change implementation:expected at most 8 hash-object calls, got 600.-in both the tracked and untracked sets, guarding the--separator and the argv batching.MAX_UNTRACKED_BYTES: no drift right after capture, and each mutation still detected.Full suite: 352 tests, 0 failures (
npm test -- --test-concurrency=1; the parallel default spawns enough app-servers to produce spurious broker/runtime failures in this repo).Notes
mainafter Shell-escape every dynamic value in the companion wait commands #108 merged mid-change; version bumped to 1.0.45 since 1.0.44 is already published with different plugin source.review --backgrounddispatch of this branch stalled on an in-flightcodegraph/codegraph_explorecall in the fresh worktree; a re-dispatch reviewed clean. Recorded as evidence on A review thread is offered every configured MCP server, including ones that cannot serve the workspace #105 rather than fixed here.Closes #102