Skip to content

fix(cli): type unredeemable cursors instead of unavailable - #2269

Merged
ScriptedAlchemy merged 3 commits into
masterfrom
fleet/cli-cursor-and-fnbody-use
Sep 26, 2026
Merged

ScriptedAlchemy merged 3 commits into
masterfrom
fleet/cli-cursor-and-fnbody-use

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

Fixes #2252

Cause

The cursor was never bound to a connection or process. ccq2 cursors are stateless, HMAC-authenticated with the project's durable cursor key, and a second tracedecay tool process redeems them fine. On master, starship src/modules/mod.rs::handle pages all 112 callees across 12 separate CLI processes.

The report's second call ran from scratch/, not from scratch/starship. The CLI resolves the project from the working directory, so that call routed to the enclosing tracedecay checkout. That checkout carries an identity marker from the operator's own profile, but the isolated profile never enrolled it. The daemon refused the route as project_not_enrolled, and project_open_problem flattened the refusal into a retryable application.surface.unavailable, so retrying could never help. MCP worked because tracedecay serve -p . pinned the project.

The same flattening hid every unservable cursor:

  • A cursor from another project: master reported omission unavailable, termination unavailable.
  • A cursor past its TTL, or whose generation is no longer held: master reported unavailable.
  • A request that names no project at all: master reported retryable application.surface.unavailable.

Change

  • Route refusals (daemon). project_not_enrolled, and the new project_required for a handshake with no project path, are terminal invalid_request problems. They carry the route's own remedy text and the correct_request legal action.
  • Cursor rejections (tracedecay-query). PreparedQueryErrorV1::Foreign covers a cursor issued for another scope digest. Verification maps a key this authority does not hold to Stale rather than Unavailable.
  • Callable code queries (tracedecay-code-index-runtime). An authenticated cursor whose generation is gone is Stale. A rejected cursor returns a Failed outcome with the new typed omissions cursor_expired / cursor_foreign. Only an unavailable authority stays retryable Unavailable.
  • Contracts. OmissionReason::{CursorExpired, CursorForeign}, with the TypeScript SDK operations regenerated.

No cursor store was added: the daemon already owns the durable key, and redemption is stateless.

Fail before / pass after

tracedecay-cli core_cli_suite::tool_cursor_test::a_cursor_presented_where_it_cannot_be_served_is_typed, run with this branch's sources reverted to origin/master and the tests kept:

left:  [{"count":0,"domain":"symbol","reason":"unavailable"}]
right: [{"count":0,"domain":"symbol","reason":"cursor_foreign"}]

It passes on the branch. It also asserts project_not_enrolled (kind invalid_request, retryable: false, ["correct_request"]) from a checkout enrolled by another profile, and project_required from a directory outside any repository. Then the same cursor still returns a disjoint 10-row page where it was issued.

callees_cursors_page_to_the_end_across_tool_processes asserts pages [10, 10, 5] and exactly leaf_01..leaf_25 across separate processes. It also passes on master: it is the cross-process regression guard for the journey the issue describes.

tracedecay-query canonical_execution_equivalence::unredeemable_prepared_cursors_reject_with_their_typed_state asserts Foreign for another scope, Stale for expiry, and Stale for an authority without the key. On master those are Invalid, Stale, and Unavailable; the Foreign variant did not exist.

Journey (debug CLI from this branch, isolated profile, one daemon under systemd-run --user --scope -p MemoryMax=6G -p MemorySwapMax=1G)

starship@cc825b00, handle callees paged with successive tracedecay tool tracedecay_callees ... --json processes:

page total=112 returned=10 ... (x11)
page total=112 returned=2

The same starship cursor presented elsewhere:

== from scratch/ (inside an operator-enrolled checkout)
{'kind': 'invalid_request', 'code': 'project_not_enrolled', 'retryable': False, 'retry': 'never', 'legal_actions': ['correct_request']}
no TraceDecay index found at '/fast/tmp/fleet-cli-cursor-and-fnbody-use': project is not enrolled in the authenticated profile; run 'tracedecay init' in that directory, or start the MCP server with 'tracedecay serve --path <project>'
== from /tmp (no project at all)
{'kind': 'invalid_request', 'code': 'project_required', 'retryable': False, 'retry': 'never', 'legal_actions': ['correct_request']}
== from another enrolled project
omissions [{'domain': 'symbol', 'count': 0, 'reason': 'cursor_foreign'}] termination failed
== back in starship
page2 returned 10 total 112

A 70-minute-old fixture cursor (TTL is 15 minutes) now returns omissions [{'reason': 'cursor_expired'}] termination failed. Earlier in the same session, master answered that cursor with unavailable while it was still inside its TTL but its generation was no longer held.

Suites

  • tracedecay-query: lib 263 (1 ignored), canonical_execution_equivalence 5, retrieval_contract_spine 2, search_quality_suite 71
  • tracedecay-code-index-runtime: lib 516 (2 ignored)
  • tracedecay-contracts: lib 420, contracts_suite 277
  • tracedecay lib: 756 of 758 passed in a full run. The two failures are shared-state flakes: replay::client_identity_startup_replays_retained_profile_receipts (SQLite database is locked) and dashboard::tests::shutdown_deadline_aborts_joins_and_clears_dashboard_task (process-global dashboard manager not idle). Both pass in isolation, and neither touches this diff. After the rebase, daemon::tests::socket + daemon::invocation_dispatch: 20.
  • tracedecay-cli core_cli_suite tool_: 51. After the rebase, tool_cursor_test + tool_surface_transport: 14.
  • mcp_suite (test-transport; serve_harness|callers|callees|cursor|enroll): 24
  • cargo clippy -p tracedecay-query -p tracedecay-contracts -p tracedecay-code-index-runtime -p tracedecay -p tracedecay-cli --all-targets -- -D warnings: clean, both without and with tracedecay/test-transport.
  • cargo fmt --all -- --check: clean. dashboard contracts:check: up to date. sdks/typescript typecheck: clean.

The runtime's query_authority_lookup_preserves_real_mount_identity_isolation failed once (repository id mismatch for a linked worktree) and passed on both reruns. It does not touch this diff.

@changeset-bot

changeset-bot Bot commented Sep 26, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 561ac27

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

@ScriptedAlchemy
ScriptedAlchemy merged commit bf0ddaf into master Sep 26, 2026
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 26, 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-26T20:44:15.499852Z 561ac27 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.

@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: 561ac2736b

ℹ️ 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 reason == OmissionReason::Unavailable {
RetrievalPortOutcome::Unavailable(evidence)
} else {
RetrievalPortOutcome::Failed(evidence)

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 Mark failed cursor outcomes as tool errors

When an expired, foreign, or malformed cursor reaches this branch, the application returns an Ok evidence envelope whose termination is failed; however, render_result_parts only sets semantic_error for Err(ApplicationProblem) and the canonical human renderer labels every evidence envelope as successful. The resulting MCP payload therefore lacks top-level isError, so tracedecay tool exits 0 despite reporting a failed cursor redemption, silently misleading scripts and CI. Propagate OperationTermination::Failed into the tool's semantic-error classification.

AGENTS.md reference: AGENTS.md:L189-L190

Useful? React with 👍 / 👎.

@ScriptedAlchemy
ScriptedAlchemy deleted the fleet/cli-cursor-and-fnbody-use branch September 26, 2026 21:59
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.

fix(cli): tool cursors cannot be redeemed by a later tool call

1 participant