Skip to content

fix(approval): RAII guard so a parked waiter is cleaned up on external turn teardown#4819

Merged
senamakel merged 3 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/GH-4774-approval-waiter-drop-guard
Jul 13, 2026
Merged

fix(approval): RAII guard so a parked waiter is cleaned up on external turn teardown#4819
senamakel merged 3 commits into
tinyhumansai:mainfrom
M3gA-Mind:fix/GH-4774-approval-waiter-drop-guard

Conversation

@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Problem

ApprovalGate::intercept_audited_inner (src/openhuman/approval/gate.rs) registers an in-memory waiter and persists a pending_approvals row, then parks on tokio::time::timeout(wait, rx). All cleanup — evict_waiter / store::decide(Deny) / thread+meeting routing-map removal — lived only inside the timeout match arms, so it ran only when the park resolved normally.

Since #4751 (the #4746 harness max_wall_clock_ms backstop, and the outer 900s web backstop), a turn future can now be torn down externally while a tool call is parked in the gate. Dropping the future skips the match arms entirely, leaking:

  • the in-memory waiter,
  • the thread_to_request / meeting_to_request routing mappings,
  • the still-open pending_approvals row (no store::decide(Deny)),

until the store TTL sweeps them. A later "yes"/"no" arriving before that expiry is then routed to a dead approval request and returns without starting a fresh chat turn.

Carved out of #4751 per the Codex reviewer (P2), confirmed by @YellowSnnowmann — deferred because it needs a ≥600s user approval wait and the turn already ends turn_timeout (retryable).

Fix

Add a WaiterGuard RAII guard, created just before the park await. On Drop (i.e. external cancellation, when the match arms never run) it:

  • evicts the waiter,
  • clears the thread/meeting routing mappings so a later reply is not mis-routed,
  • denies the still-open pending row. store::decide is WHERE decided_at IS NULL, so a decision that committed in the same instant is honored rather than overwritten.

The guard is disarm()ed on every normal exit — the timeout match arm already ran the exact teardown for its outcome, so the guard's Drop is reserved solely for external teardown.

Test

waiter_future_dropped_mid_park_evicts_waiter_clears_routing_and_denies_row boxes the parked intercept future, polls it just long enough to park (asserts the waiter is registered and the row is open), then drops it mid-park and asserts the waiter is evicted, routing cleared, and the pending row denied.

Checks

  • cargo fmt — clean.
  • No local build/test (disk-constrained worker) — CI verifies.

Closes #4774

…l turn teardown

ApprovalGate::intercept_audited_inner registers an in-memory waiter and
persists a pending_approvals row, then parks on tokio::time::timeout. All
cleanup (evict_waiter / store::decide(Deny) / thread+meeting routing-map
removal) lived only inside the timeout match arms, so it ran only on a
normal park resolution.

Since tinyhumansai#4751 (the tinyhumansai#4746 wall-clock backstop), a turn future can be torn
down externally while a tool call is parked. Dropping the future skips
the match arms, leaking the waiter, the routing mappings, and the pending
row until the store TTL sweeps them — a later yes/no then routes to a dead
request and returns without starting a fresh turn.

Add a WaiterGuard RAII guard created just before the park await: on Drop
(external cancellation) it evicts the waiter, clears routing, and denies
the still-open row (store::decide is WHERE decided_at IS NULL, so a
same-instant real decision is honored). Disarmed on every normal exit so
the match arms stay authoritative. Regression test tears the parked future
down mid-park and asserts waiter eviction + routing clear + row denied.

Closes tinyhumansai#4774
@M3gA-Mind M3gA-Mind requested a review from a team July 13, 2026 11:57
@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 32 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f9ef8a2b-aa4b-4365-979d-9b3c52b6b600

📥 Commits

Reviewing files that changed from the base of the PR and between f2abbc5 and fabfb17.

📒 Files selected for processing (1)
  • src/openhuman/approval/gate.rs

Comment @coderabbitai help to get the list of available commands.

@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: 8ca1d875e5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread src/openhuman/approval/gate.rs Outdated
…tore vendor pins

On external turn teardown the WaiterGuard drop cleared the thread/meeting
routing maps unconditionally. If a replacement turn had already parked a new
approval on the same thread and overwritten the entry, this deleted the *new*
request's routing, so the next typed yes/no fell through as a fresh chat turn
instead of resolving the live gate. Only remove the mapping when it still
points at this guard's request_id (both thread and meeting maps).

Also restores vendor/tinyagents and vendor/tinycortex to the main pins; the
branch had picked up stray submodule bumps that broke the build (tinycortex
33dda94 lacks the git-diff feature openhuman requires).
@M3gA-Mind

Copy link
Copy Markdown
Collaborator Author

W4 update — pushed fabfb17c2:

  • Addressed the Codex P2 (guard routing cleanup by request id): WaiterGuard::drop now clears the thread/meeting routing maps only when they still point at this guard's own request_id (new clear_thread_route_if_owned / clear_meeting_route_if_owned helpers), so a replacement turn that has re-parked on the same thread keeps its routing. Added guard_cleanup_only_clears_routing_it_still_owns regression test.
  • Restored vendor/tinyagents + vendor/tinycortex to the main pins — the branch had stray submodule bumps that broke the build (tinycortex 33dda94 lacks the git-diff feature). Rust Quality is green again.

All checks green; review thread resolved.

@oxoxDev oxoxDev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review ✅ — approve

Correct and well-reasoned. WaiterGuard is armed just before the timeout(wait, rx).await park and disarm()ed right after the match block, so on external future-drop (the #4751 backstop tearing down a turn mid-park) its sync Drop runs the exact teardown the match arms own: evict_waiter, request-id-scoped routing clear, store::decide(Deny).

Verified

  • No double / missed cleanup: normal exit → match arm cleans up → disarm()Drop early-returns; external drop → guard runs full cleanup. evict_waiter/decide are keyed on the guard's own request_id, so even the unconditional decide(Deny) in Drop only touches this request's row.
  • Sync-safe Drop: store::decide is synchronous rusqlite, routing uses parking_lot::Mutex — no .await/block_on/spawn in Drop, can't panic in async context.
  • Idempotent: decide is UPDATE … WHERE decided_at IS NULL, so a same-instant Approve wins and the guard's Deny is a no-op; the late approve RPC short-circuits on Ok(None).
  • No armed-window gap: only .await between waiter/row registration and the park is the timeout itself, so a drop can't land while registered-but-unarmed. Real external-drop test (Box::pin → poll to park → drop() → assert evicted + routing cleared + row Denied).

Non-blocking

  • [low] Drop test only exercises the chat/thread path; the meeting_key branch is helper-covered but not through an actual mid-park future drop — add an in_call_ctx variant.
  • [low] Pre-existing (not this diff): insert_pending failure path clears thread route but not meeting route → meeting_to_request leak on persist failure with an active call. Optional follow-up.

Codex's routing-scoping P2 fixed in PATCH 3; CodeRabbit APPROVED, no open threads.

@senamakel senamakel merged commit cb1ae9d into tinyhumansai:main Jul 13, 2026
16 checks passed
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.

Approval waiter/pending row leaks when a parked tool call is torn down by the turn wall-clock backstop

3 participants