Skip to content

fix(cli): type readiness waits and projectless refusals end to end - #2314

Merged
ScriptedAlchemy merged 2 commits into
masterfrom
fleet/beta55-typed-waits-refusals
Sep 27, 2026
Merged

ScriptedAlchemy merged 2 commits into
masterfrom
fleet/beta55-typed-waits-refusals

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

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, the tool help and the CLI fake-daemon suite.

(b) wait_for on a current index returned timed_out

Cause. wait_for_readiness swept the worktree before its first read: request_fresh_now takes 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_signature avg 106 ms / p95 230 ms, content_verify avg 135 ms / p95 377 ms) and waits behind any pass holding the lock, so a 1 s budget could expire inside the sweep and report timed_out { last_state: current }. Separately the CLI exited 0 for every wait outcome, and markdown rendered the outcome as {2 field(s)}.

Change.

  • A target the current reading already satisfies is reached at once. That reading is the scheduler's last proof, the answer a plain status read gives. The sweep still runs whenever the reading is pending. (There is no working-tree watcher: a save no hook reported is left to the backstop sweep, as it is for plain status.)
  • The status handler puts the outcome in structuredContent.wait beside the body in every format (the same channel a typed problem uses; CallToolResult drops other members). tracedecay tool status exits 75 (EX_TEMPFAIL, rerun the wait) on timed_out and 1 on unavailable, naming the last state or reason: new reason codes code_index_readiness_wait_timed_out / code_index_readiness_wait_unavailable. Markdown shows **wait:** timed_out (warming).
  • The timeout_ms refusal keeps naming the budget; a test pins it (… 120001 exceeds this call's 120000 ms dispatch budget with 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 for fresh on 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).

$ tracedecay tool status --args '{"wait_for":{"state":"fresh","timeout_ms":1000}}'   # index current
**wait:** reached            wall=0.22s   (×3: 0.22 s, 0.21 s, 0.21 s)
$ tracedecay tool status --args '{"wait_for":{"state":"ready","timeout_ms":1}}'     # fresh project, still warming
**code_index_freshness.status:** warming
**wait:** timed_out (warming)
Error: project route error (code_index_readiness_wait_timed_out): tracedecay_status wait_for timed out before the index reached the requested state; last state: warming
exit=75
$ tracedecay tool status --args '{"wait_for":{"state":"fresh","timeout_ms":200000}}'
Error: config error: daemon tool call failed: tool execution failed: config error: tracedecay_status wait_for.timeout_ms 200000 exceeds this call's 119994 ms dispatch budget

Before (released beta.55, same warming wait): **wait:** {2 field(s)}, exit=0.

(d) The CLI flattened the typed project_required refusal

Cause. Tools on the compatibility route reach the daemon's projectless connection, whose requires_project_error answered an untyped JSON-RPC error. The CLI turned it into config error: daemon tool call failed: … and printed nothing on stdout. (#2269 typed project_required only for application-surface invocations.) Since this finding, #2296 moved tracedecay_search onto the owner route and #2293 stopped flattening owner refusals, so search itself now reports the typed problem on master; status and the other compatibility tools did not.

Change. The projectless refusal goes through tool_error_response with TraceDecayError::project_route("project_required", false, …), so its error data carries kind/code/reason_code/detail/retryable (project_required and project_not_enrolled map to invalid_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 (--json or --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 json must print the owner's problem record (code: project_required, kind: invalid_request, detail: null, legal_actions: ["correct_request"]). status --format json must 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_daemon now asserts the typed project_required route error instead of message fragments.

Journey (branch binary, fresh isolated profile, directory outside any project).

$ tracedecay tool tracedecay_status --format json; echo exit=$?
{"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"}}
Error: project route error (project_required): tracedecay_status requires an initialized code project; run it inside an initialized project or pass --project <path>
exit=1
$ tracedecay tool tracedecay_search --query daemon --format json; echo exit=$?
{"contract":{"schema_id":"schema.application.primitive.search.result","schema_revision":1},"problem":{…,"code":"project_required",…,"detail":null,…,"kind":"invalid_request","legal_actions":["correct_request"],"message":"this operation needs a TraceDecay project, and the request named none; run it inside an initialized project or pass --project <path>",…,"retryable":false,…}}
exit=1

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 passed
  • cargo test -p tracedecay-mcp --lib: 393 passed
  • cargo test -p tracedecay --features test-transport,test-helpers --lib -- dispatch_tests projectless application_surface: 64 passed
  • cargo test -p tracedecay-cli --test core_cli_suite -- tool_status_exit projectless_json tool_cursor tool_surface_transport: 18 passed
  • cargo test -p tracedecay --features test-helpers --test daemon_suite -- stale_client: 1 passed
  • Clippy on master is currently red: fix(domain): TraceDecayError too large for clippy since 21552e3764 #2308 (TraceDecayError over clippy's 128-byte result_large_err threshold since 21552e3), plus two findings in tracedecay-query. So cargo clippy --no-deps -p tracedecay-contracts -p tracedecay-mcp -p tracedecay-code-index-runtime -p tracedecay -p tracedecay-cli --all-targets -- -D warnings ran with CLIPPY_CONF_DIR → large-error-threshold = 256 (a local config for this run, not committed), with and without test-transport,test-helpers. Both clean. cargo fmt --all -- --check: clean. pnpm run contracts:check: contracts up to date.

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.
@changeset-bot

changeset-bot Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: b7fed63

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-27T10:28:05.377510Z 07427e2 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@ScriptedAlchemy
ScriptedAlchemy merged commit 95a63d4 into master Sep 27, 2026
1 check passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment on lines +219 to +224
if self
.dashboard_freshness_read(project_root)
.await?
.is_some_and(|freshness| freshness.readiness(target) == CodeIndexReadinessV1::Reached)
{
return Ok(CodeIndexReadinessWaitReadV1::Reached);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 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 👍 / 👎.

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.

1 participant