fix(code-index): verify fresh waits cheaply and type delivered saves - #2341
Conversation
|
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: 784aa6d782
ℹ️ 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".
| return if value.get("result").is_some() { | ||
| HookEventNotifyOutcomeV1::Delivered |
There was a problem hiding this comment.
Propagate hook admission failures before returning Delivered
When routing or code-index admission fails, the daemon still returns an empty successful result: on_custom_request calls dispatch_notification, whose dispatch discards HostAdmissionOutcome, and then unconditionally returns {} (tracedecay-mcp/src/server/rmcp.rs:883-886; crates/tracedecay/src/mcp/server/requests.rs:344-350). Consequently this branch reports Delivered even when the edited paths never entered the scheduler, so an immediate status read can still expose the stale generation—the condition this acknowledgement is intended to prevent. The response needs to carry the actual admission outcome rather than treating every result as delivery.
AGENTS.md reference: AGENTS.md:L189-L191
Useful? React with 👍 / 👎.
| fn sample(path: &Path) -> std::io::Result<Option<StatKeyV1>> { | ||
| match std::fs::symlink_metadata(path) { | ||
| Ok(metadata) => Ok(Some(StatKeyV1::of(&metadata))), |
There was a problem hiding this comment.
Follow symlink targets when validating ignore files
On Unix, if core.excludesFile is a symlink—a common dotfiles setup—editing its target does not change the symlink metadata returned here. Because roster_evidence records that configured excludes path through sample, CachedCandidateRosterV1::holds continues reusing the old candidate list after the ignore rules change. Newly included or excluded source files therefore remain invisible to fresh waits until unrelated Git metadata or a recorded directory changes; file evidence should follow the target or decline to cache symlinked ignore files.
Useful? React with 👍 / 👎.
Two truth bugs in the code-index readiness surface, fixed in one lane.
wait_for { state: fresh }means verified against the source as of the requestCause. #2314 made a
freshwait whose current reading already saidcurrentreturnreachedwithout checking the worktree, because the check was expensive and waited behind the scheduler mutex: a stat walk plus a content hash of every candidate. So a save no hook reported stayed invisible until the 15 minute backstop, while the wait saidfresh.Change.
freshalways sweeps the source before it answers.readyandgraph_readystill return at once when the current reading satisfies them.request_fresh_nowrecords the observed change through the hint/epoch authority itself).SourceSweepCacheV1(on the fence) makes the sweep cheap without weakening it:touch -d,cp --preserveandrsync -arewrites are still re-read and caught;.gitignore,info/exclude, the global excludes file and.git/configkeep their settled keys (git's untracked-cache model: adding, removing or renaming an entry moves the directory's ctime). Otherwise gix walks again (thread cap from perf(daemon): reuse worker threads and release mimalloc heaps #2295);ponytail:note).request_fresh_now_backgroundnow delegates to the fence), and the retained/restore reconcile paths.WorktreeStatSweepV1::content_matchesis deleted; the stat signature stays only as the durable restart witness's negative cache.timed_out { last_state }, neverreached.Tests (code-index-runtime).
fresh_wait_catches_an_unreported_save_and_returns_after_its_reindex: edit without a hint,wait_for_readiness(Fresh, 2 min)→Reached, generation changed, served texts["pub fn source() -> u32 { 2 }", "pub fn source() -> u32 { 2 }\n"]. Fails on master (fix(cli): type readiness waits and projectless refusals end to end #2314 early return restored):assert_ne!sees the same generationgeneration.v1.defde92f.00000001.dfae…before and after.fresh_wait_verifies_the_source_while_a_pass_holds_the_scheduler: with the scheduler mutex held, a quiet 1 s wait →Reached; after an unreported edit a 500 ms wait →TimedOut { staleness_state: Refreshing, latest_generation_id: <old> }; after release →Reachedwith{ 3 }served. Fails on master:an unreported save must not read as fresh: Reached. Fails with the early return removed but the sweep still taking the lock: quiet waitTimedOut { last: Some(… staleness_state: Some(Fresh) …) }.fresh_wait_on_a_current_index_reaches_inside_one_second: 1 s budget →Reached, elapsed < 1 s.source_sweep_rereads_only_files_whose_settled_stat_moved: after the stats settle, sweeps report(true, walked: true, candidates: 2, hashed: 2), then(true, walked: false, candidates: 2, hashed: 0); a same-length rewrite with its mtime restored →(false, walked: false, candidates: 2, hashed: 1).A delivered save makes status stale until the new generation seals
Fixes #2330
Cause.
notify_hook_eventwrote the request, shut down its write half and returnedDeliveredwithout reading the reply. The daemon admits the edited paths into the code-index queue while handling that request, so a status read issued right afterDeliveredcould run before the hint existed, and report the outdated generationcurrent/freshwithhook_hint_count: 0. (Before #2314 areadywait's sweep hid this; the early return exposed it.)Change. Delivery keeps the connection open and reads the daemon's JSON-RPC reply (it answers once the event is admitted):
result→Delivered, an error →Malformed, EOF →Unavailable, still inside the existing 750 msHOOK_EVENT_NOTIFY_TIMEOUT. Production hook callers already ignore the outcome, so a slow daemon yieldsTimedOutrather than blocking a host.Test.
daemon_suite::dirty_worktree_symbol_reads_test(real daemon). Fail-before (fire-and-forget restored in the test binary, same daemon binary):code_index_journey.rs:548 left: Some("daad5f9e046416f8e1f60976c33d663239abb391") right: None, the #2330 literal. Pass-after:1 passed.Runtime proof (branch
perf+production,hotpathCLI, isolatedHOME, one daemon undersystemd-run --scope -p MemoryMax=6G -p MemorySwapMax=1G, shallow clone of this repo: 6,555 files, 5,701 indexed)The 6 GB cap parks the native graph decode on this corpus (#2123:
needs 2819267149 resident bytes; 1980021145 are available), so the profile setsindex.native_graph_activation.v1 = falseat the project layer.freshdoes not depend on the graph.Sweep cost, Hotpath, same corpus and daemon session (load average 25–200 on a shared 96-core host):
stat_signature+content_verify(the old sweep, still run by the restart witness)source_sweepThe reindex takes minutes because a whole generation is rebuilt on a loaded host; the wait reports that honestly.
Verification
cargo test -p tracedecay-code-index-runtime -p tracedecay-mcp --lib: 529 + 387 passed (full runs under load average 200–460 timed out 1–8 unrelated 2–5 s bounds; each passed alone and full runs passed at normal load)cargo test -p tracedecay --features test-helpers --test daemon_suite(perf CLI asTRACEDECAY_TEST_BIN): 55 passedcargo test -p tracedecay-cli --test core_cli_suite -- tool_status tool_daemon projectless_json hook: 30 passedcargo test -p tracedecay --features test-transport,test-helpers --lib -- core_hooks dispatch_tests hook_event: 50 passedcargo clippy -p tracedecay-code-index-runtime -p tracedecay --all-targets -- -D warnings, with and withouttracedecay/test-transport,tracedecay/test-helpers: cleancargo fmt --all -- --check: cleancargo check --workspace --all-targets --target x86_64-pc-windows-gnu --features tracedecay/test-transport,tracedecay/test-helpers,tracedecay-cli/test-transport: exit 0