Priority: P1 availability/lifecycle correctness. Source-confirmed cancellation hole; no production stall or repository test execution is claimed.
Evidence at #707 3d955ff998eef120cd398f163c613a54e3383ccb
crates/tracedecay-sessions/src/runtime/hosts/codex/observation.rs::shared_session_meta_with_provenance owns a process-retained CODEX_META_CACHE with in_flight: HashMap<CodexMetaCacheKey, Arc<Notify>>.
The cold path inserts a key, releases the mutex, and awaits a blocking metadata parse. Removal from in_flight and notification happen only in the continuation after that await (including ordinary returned errors). There is no retained completion owner or drop cleanup for cancellation of the future itself.
If the first caller is aborted/dropped after insertion but before that continuation runs, the blocking parse may finish but nobody removes its key or publishes its result. A later caller observing the same canonical path/file-identity key repeatedly sees in_flight, sleeps 10 ms, and retries. Its own cancellation can stop it, but nothing elects a replacement or completes the abandoned fill. An unchanged source therefore remains unable to get past metadata admission until state is otherwise reset; a new file-identity key can mask the problem, so do not describe every later rewrite as permanently blocked.
This is on the real project/profile observation-admission path: try_admit_codex_jsonl_observations awaits this metadata before constructing the per-source admission request. A return-value test for a cancelled caller alone cannot establish recovery for subsequent callers.
The memory reservation is held in the async build scope, while the parser runs in spawn_blocking. Dropping that async scope can release the reservation while the admitted blocking work still runs. Completion ownership and accounting must travel together.
Smallest durable change
Use one cancellation-safe owner for each admitted cache fill, independent of whichever request first waits for it. Reuse the existing suitable retained-operation/singleflight boundary rather than introducing a second global task registry or a generic cache framework.
A detached request may stop waiting, but the fill owner must still settle the parse, remove exactly its own claim, publish a valid result or terminal failure, and wake waiters. If the design instead cancels/relinquishes a fill, fence attempts so a late completion cannot install into or erase a replacement claim. Retain resource charges until actual worker settlement. Started blocking work is not stopped merely by dropping its JoinHandle.
Keep the exact path/file evidence, provider/scope admission and cache capacity rules. Do not fix this with a timeout that deletes arbitrary in_flight entries while their workers still run, by disabling identity validation, or by making every waiter perform another parse. The 10 ms polling loop should not be the mechanism that makes abandoned ownership recoverable; use the existing cancellation/completion notification semantics where available.
Acceptance
- Park the real metadata fill after claim acquisition and before result publication. Abort its first waiter, issue another lookup for the unchanged exact key, then release the worker. The second lookup receives the settled result or a bounded typed failure and can subsequently retry successfully; it never loops behind an orphan claim.
- Exercise failure and cancellation at both the blocking-parse wait and the cache-lock reacquisition. A late old completion cannot remove/overwrite a replacement owner. Concurrent successful callers still share one metadata parse.
- Hold the parser after the first waiter is dropped and verify its memory/work ownership remains charged until settlement. Shutdown does not leave a worker writing into a retired cache.
- Run a focused real Codex admission continuation through this path, checking source cursors and scope isolation remain unchanged. Prefer deterministic barriers over sleeps or a duplicate whole-ingestion suite.
Related #975/#976 cover different owners with similar waiter-versus-settlement mistakes; this is a separate Codex metadata cache, not another analyzer/cache rewrite request. Keep #707 draft.
Priority: P1 availability/lifecycle correctness. Source-confirmed cancellation hole; no production stall or repository test execution is claimed.
Evidence at #707
3d955ff998eef120cd398f163c613a54e3383ccbcrates/tracedecay-sessions/src/runtime/hosts/codex/observation.rs::shared_session_meta_with_provenanceowns a process-retainedCODEX_META_CACHEwithin_flight: HashMap<CodexMetaCacheKey, Arc<Notify>>.The cold path inserts a key, releases the mutex, and awaits a blocking metadata parse. Removal from
in_flightand notification happen only in the continuation after that await (including ordinary returned errors). There is no retained completion owner or drop cleanup for cancellation of the future itself.If the first caller is aborted/dropped after insertion but before that continuation runs, the blocking parse may finish but nobody removes its key or publishes its result. A later caller observing the same canonical path/file-identity key repeatedly sees
in_flight, sleeps 10 ms, and retries. Its own cancellation can stop it, but nothing elects a replacement or completes the abandoned fill. An unchanged source therefore remains unable to get past metadata admission until state is otherwise reset; a new file-identity key can mask the problem, so do not describe every later rewrite as permanently blocked.This is on the real project/profile observation-admission path:
try_admit_codex_jsonl_observationsawaits this metadata before constructing the per-source admission request. A return-value test for a cancelled caller alone cannot establish recovery for subsequent callers.The memory reservation is held in the async build scope, while the parser runs in
spawn_blocking. Dropping that async scope can release the reservation while the admitted blocking work still runs. Completion ownership and accounting must travel together.Smallest durable change
Use one cancellation-safe owner for each admitted cache fill, independent of whichever request first waits for it. Reuse the existing suitable retained-operation/singleflight boundary rather than introducing a second global task registry or a generic cache framework.
A detached request may stop waiting, but the fill owner must still settle the parse, remove exactly its own claim, publish a valid result or terminal failure, and wake waiters. If the design instead cancels/relinquishes a fill, fence attempts so a late completion cannot install into or erase a replacement claim. Retain resource charges until actual worker settlement. Started blocking work is not stopped merely by dropping its JoinHandle.
Keep the exact path/file evidence, provider/scope admission and cache capacity rules. Do not fix this with a timeout that deletes arbitrary
in_flightentries while their workers still run, by disabling identity validation, or by making every waiter perform another parse. The 10 ms polling loop should not be the mechanism that makes abandoned ownership recoverable; use the existing cancellation/completion notification semantics where available.Acceptance
Related #975/#976 cover different owners with similar waiter-versus-settlement mistakes; this is a separate Codex metadata cache, not another analyzer/cache rewrite request. Keep #707 draft.