fix(cli): type readiness waits and projectless refusals end to end - #2314
Conversation
A `tracedecay_status` `wait_for` on an index that was already current
could still answer `timed_out { last_state: current }`: before reading
freshness the wait swept the whole worktree (stat walk plus content
digests, behind the scheduler lock), and a short budget ran out inside
the sweep. A target the current reading already satisfies is now reached
at once; the sweep still runs whenever the reading is pending.
The wait's outcome travels as structured content beside the rendered
status in every format, so `tracedecay tool status` exits 75 when the
wait ends `timed_out` and 1 when it ends `unavailable`, naming the last
state or reason on stderr; markdown now shows the outcome instead of
`{2 field(s)}`.
A compatibility tool called without a project in reach was refused by
the projectless daemon route as an untyped JSON-RPC error, which the CLI
flattened into `config error: daemon tool call failed: ...` with empty
stdout. The refusal now carries the typed `project_required` problem
(`project_not_enrolled` alike), and `tracedecay tool --format json`
prints `{"problem": ...}` on stdout before exiting non-zero, as the
owner-answered tools already do.
|
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 07427e2351
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if self | ||
| .dashboard_freshness_read(project_root) | ||
| .await? | ||
| .is_some_and(|freshness| freshness.readiness(target) == CodeIndexReadinessV1::Reached) | ||
| { | ||
| return Ok(CodeIndexReadinessWaitReadV1::Reached); |
There was a problem hiding this comment.
Revalidate fresh targets before returning reached
When a file changes after the last settled generation without a hook notification—for example, an editor or script writes directly before the backstop sweep—the cached dashboard reading still reports Fresh, so this new short circuit returns Reached without request_fresh_now detecting the edit. Consequently, both fresh and ready waits can succeed while retrieval still serves the previous source; restrict the cache-only fast path to GraphReady, or otherwise prove the current source before returning.
AGENTS.md reference: AGENTS.md:L192-L194
Useful? React with 👍 / 👎.
Two findings from running the released 1.0.0-beta.55. Both are daemon outcomes that did not reach the CLI as typed state; they share
tool_command.rs, thetoolhelp and the CLI fake-daemon suite.(b)
wait_foron a current index returnedtimed_outCause.
wait_for_readinessswept the worktree before its first read:request_fresh_nowtakes the scheduler lock and runs the stat walk plus sealed-digest content comparison over every candidate file. On a ~200k-symbol index that sweep alone costs hundreds of milliseconds (hotpath on this repo:daemon.code_index.freshness.stat_signatureavg 106 ms / p95 230 ms,content_verifyavg 135 ms / p95 377 ms) and waits behind any pass holding the lock, so a 1 s budget could expire inside the sweep and reporttimed_out { last_state: current }. Separately the CLI exited 0 for every wait outcome, and markdown rendered the outcome as{2 field(s)}.Change.
structuredContent.waitbeside the body in every format (the same channel a typedproblemuses;CallToolResultdrops other members).tracedecay tool statusexits 75 (EX_TEMPFAIL, rerun the wait) ontimed_outand 1 onunavailable, naming the last state or reason: new reason codescode_index_readiness_wait_timed_out/code_index_readiness_wait_unavailable. Markdown shows**wait:** timed_out (warming).timeout_msrefusal keeps naming the budget; a test pins it (… 120001 exceeds this call's 120000 ms dispatch budgetwith no carried deadline).Tests (fail with the fix reverted, pass with it).
readiness_wait_reaches_a_target_the_current_reading_already_holds(code-index-runtime): with the scheduler lock held, a 200 ms wait forfreshon a settled worktree. Before:a current worktree must not time out behind the source sweep: TimedOut { last: Some(… staleness_state: Some(Fresh) …) }. After:Reached.status_wait_outcome_rides_beside_the_body_and_the_budget_refusal_names_it(tracedecay lib). Before:left: Null right: Object {"wait": {"last_state":"warming","outcome":"timed_out"}}, markdown**wait:** {2 field(s)}. After: structured content in both formats,**wait:** timed_out (warming), and the pinned budget refusal.tool_status_exit_follows_the_wait_outcome(core_cli_suite, real CLI against a scripted daemon). Before:left: Some(0) right: Some(75). After: 75 with the last state on stderr; 0 when reached.Journey (branch binary, isolated profile, this repo as corpus).
Before (released beta.55, same warming wait):
**wait:** {2 field(s)},exit=0.(d) The CLI flattened the typed
project_requiredrefusalCause. Tools on the compatibility route reach the daemon's projectless connection, whose
requires_project_erroranswered an untyped JSON-RPC error. The CLI turned it intoconfig error: daemon tool call failed: …and printed nothing on stdout. (#2269 typedproject_requiredonly for application-surface invocations.) Since this finding, #2296 movedtracedecay_searchonto the owner route and #2293 stopped flattening owner refusals, sosearchitself now reports the typed problem on master;statusand the other compatibility tools did not.Change. The projectless refusal goes through
tool_error_responsewithTraceDecayError::project_route("project_required", false, …), so its errordatacarrieskind/code/reason_code/detail/retryable(project_requiredandproject_not_enrolledmap toinvalid_request).project_route_problem(tool, &error)builds that object once (including #2293's typed route detail), for the MCP error and for the CLI. For a JSON request (--jsonor--format json) the CLI prints{"problem": …}on stdout, then exits non-zero with the error on stderr.Test.
projectless_json_tool_call_prints_the_typed_refusal(core_cli_suite, real daemon, directory outside any project) covers both routes.search --format jsonmust print the owner's problem record (code: project_required,kind: invalid_request,detail: null,legal_actions: ["correct_request"]).status --format jsonmust print{"problem":{"code":"project_required","detail":"tracedecay_status requires an initialized code project; run it inside an initialized project or pass --project <path>","kind":"invalid_request","reason_code":"project_required","retryable":false,"tool":"tracedecay_status"}}. Both exit 1. Before (fix reverted, pre-#2296 base):stdout is not one JSON document (EOF while parsing a value at line 1 column 0).version_skewed_client_cannot_crash_the_daemonnow asserts the typedproject_requiredroute error instead of message fragments.Journey (branch binary, fresh isolated profile, directory outside any project).
Before (released beta.55): empty stdout,
Error: config error: daemon tool call failed: tracedecay_search requires an initialized code project,exit=1.Verification
cargo test -p tracedecay-code-index-runtime --lib -- readiness_wait: 2 passedcargo test -p tracedecay-mcp --lib: 393 passedcargo test -p tracedecay --features test-transport,test-helpers --lib -- dispatch_tests projectless application_surface: 64 passedcargo test -p tracedecay-cli --test core_cli_suite -- tool_status_exit projectless_json tool_cursor tool_surface_transport: 18 passedcargo test -p tracedecay --features test-helpers --test daemon_suite -- stale_client: 1 passedTraceDecayErrorover clippy's 128-byteresult_large_errthreshold since 21552e3), plus two findings intracedecay-query. Socargo clippy --no-deps -p tracedecay-contracts -p tracedecay-mcp -p tracedecay-code-index-runtime -p tracedecay -p tracedecay-cli --all-targets -- -D warningsran withCLIPPY_CONF_DIR→large-error-threshold = 256(a local config for this run, not committed), with and withouttest-transport,test-helpers. Both clean.cargo fmt --all -- --check: clean.pnpm run contracts:check:contracts up to date.