Skip to content

fix(rhai): restore the documented timeout ordering and honour a reused session's policy - #5288

Merged
graycyrus merged 1 commit into
tinyhumansai:mainfrom
graycyrus:fix/rhai-timeout-and-session-policy
Jul 31, 2026
Merged

fix(rhai): restore the documented timeout ordering and honour a reused session's policy#5288
graycyrus merged 1 commit into
tinyhumansai:mainfrom
graycyrus:fix/rhai-timeout-and-session-policy

Conversation

@graycyrus

@graycyrus graycyrus commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Restores the timeout ordering rhai_workflows' own README promises — the three bounds had collapsed so the harness could win the race and drop the cell future mid-cleanup.
  • A reused REPL session now has its bounds computed from the policy it was actually built with, fixing a path that silently dropped the session and lost every binding.
  • summarize_calls no longer reports a failed capability call to the model as ok: true.
  • LRU eviction can no longer evict a session whose cell is still running.

Problem

E-M1 — the layering was inverted. README.md states the harness ToolTimeout::Secs backstop sits above all other bounds and the tool's own timeout below it. In fact timeout_policy() handed the harness the same clamped secs as the inner ReplPolicy.timeout, while the outer spawn_blocking backstop was secs + 5. So harness == inner < outer.

When the harness won that tie it dropped the run_cell future, skipping the RhaiError::Timeout taxonomy, finish_cell accounting, close_session, and the outer-backstop manager.close cleanup — while the detached blocking thread kept holding the session mutex until the inner deadline, so follow-up calls got SessionBusy.

E-M2 — reused sessions ran on a stale policy while the wrapper enforced a new one. get_or_create's build closure only runs for a fresh session, so a reused session keeps its original ReplPolicy — yet outer_bound and limits_remaining were computed from the newly resolved one.

Concretely: a session built with 300s, reused by a cell passing timeout_secs: 30 that runs 60s, hits the 35s outer backstop while its inner deadline is still 300s. The outer-backstop arm calls manager.close(&key)dropping the whole session and losing all its bindings, which is exactly the "inner should fire first" invariant the code's own comment says it defends.

E-m5 / E-m6summarize_calls hardcoded ok: true (the vendor's ReplCallRecord carries no success flag), so a failed-but-caught capability call was summarized to the model as successful; and LRU eviction could evict an in-flight slot, after which finish_cell returned None and limits_remaining reported a full budget for a session that had just done work.

Solution

  • One source for the three boundsouter_backstop_secs(inner) = inner + 5, harness_backstop_secs(inner) = outer + 5. Ordering is now inner < outer < harness for every input, pinned both as a pure assertion over the computed values and through the live RhaiTool::timeout_policy(). README rewritten in the same commit to match.
  • SlotHandle carries the policy its session was built with, and every per-call bound is computed from that, warning when a caller requests a different one. The session cache is deliberately not keyed on a policy fingerprint: that would fragment sessions and lose bindings by design rather than by bug.
  • Call outcomes tracked host-side as calls dispatch through the bridge, so summarize_calls reports real success/failure.
  • Busy slots are ineligible for both the idle-TTL sweep and the cap eviction; if every slot is busy the incoming session is admitted one over cap rather than evicting a running cell.

Nothing under vendor/ is modified. Two items are worth filing upstream instead: a success flag on ReplCallRecord, and a set_policy on ReplSession — the absence of the latter is precisely why E-M2 is fixed host-side.

Submission Checklist

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — each changed mechanism has a direct test; cargo test --lib openhuman::rhai_workflows = 42 passed, 0 failed
  • Coverage matrix updated — N/A: behaviour-only bug fix, no feature rows added/removed/renamed
  • All affected feature IDs from the matrix are listed under ## RelatedN/A: no matrix feature rows affected
  • No new external network dependencies introduced
  • Manual smoke checklist updated if this touches release-cut surfaces — N/A: agent-tool internal; the rhai tool is orchestrator-surface only (ToolScope::AgentOnly)
  • Linked issue closed via Closes #NNNN/A: found by code review, no tracking issue filed yet

Impact

  • Runtime/platform: Rust core, openhuman::rhai_workflows only.
  • Behaviour: a long-running cell now reliably hits its own inner deadline first, so it produces a proper RhaiError::Timeout with accounting and cleanup instead of a dropped future and a wedged mutex. A reused session with a differing requested policy keeps its bindings and logs a warning rather than being destroyed.
  • Performance: the effective harness ceiling for a rhai call rises by 10s (inner + 10) so the inner deadline can always fire first. Deliberate — that headroom is what makes the cleanup path reachable.
  • Security: unchanged. Tier gating, the tool exclusion list, and the kill switches are untouched.

Related

  • Closes: N/A
  • Follow-up PR(s)/TODOs: upstream tinyagents — add a success flag to ReplCallRecord and a set_policy to ReplSession.
  • Merge order: fully independent — touches only src/openhuman/rhai_workflows/, no overlap with the other PRs in this batch.

AI Authored PR Metadata (required for Codex/Linear PRs)

Linear Issue

  • Key: N/A
  • URL: N/A

Commit & Branch

  • Branch: fix/rhai-timeout-and-session-policy
  • Commit SHA: ed11b94ec

Validation Run

  • pnpm --filter openhuman-app format:check — N/A, no frontend files changed
  • pnpm typecheck — N/A, no TypeScript changed
  • Focused tests: GGML_NATIVE=OFF cargo test --lib openhuman::rhai_workflows42 passed, 0 failed
  • Rust fmt/check (if changed): GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml clean
  • Tauri fmt/check (if changed): N/A, app/src-tauri untouched

Validation Blocked

  • command: N/A
  • error: N/A
  • impact: N/A

Behavior Changes

  • Intended behavior change: the inner rhai deadline always fires before the outer backstop, which always fires before the harness timeout; a reused session's own policy governs its bounds.
  • User-visible effect: a timed-out rhai cell reports a real timeout with intact session state instead of intermittently wedging the session into SessionBusy.

Parity Contract

  • Legacy behavior preserved: clamping, tier gating, the excluded-tool list, kill switches, LRU cap / idle TTL values, and the session-key scheme are all unchanged.
  • Guard/fallback/dispatch parity checks: the existing timeout_is_always_bounded and session-isolation tests still pass unmodified in intent.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this one
  • Resolution: N/A

Summary by CodeRabbit

  • Reliability

    • Improved timeout handling with consistent safeguards across workflow execution layers.
    • Preserved session policies when sessions are reused and protected active sessions from eviction.
    • Improved handling and reporting of failed or unknown tool calls.
  • Documentation

    • Clarified timeout ordering, session policy behavior, cleanup, and tool-call success reporting.

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 89e5a7ee-f5f5-40d9-a737-d85c11e02ec8

📥 Commits

Reviewing files that changed from the base of the PR and between bb83836 and c70993c.

📒 Files selected for processing (7)
  • src/openhuman/rhai_workflows/README.md
  • src/openhuman/rhai_workflows/bridge.rs
  • src/openhuman/rhai_workflows/ops.rs
  • src/openhuman/rhai_workflows/policy.rs
  • src/openhuman/rhai_workflows/sessions.rs
  • src/openhuman/rhai_workflows/tools.rs
  • src/openhuman/rhai_workflows/types.rs

📝 Walkthrough

Walkthrough

Rhai workflows now use ordered timeout backstops, retain session policies across reuse, protect busy sessions from eviction, and track tool-call success or failure through call summaries.

Changes

Rhai execution safeguards

Layer / File(s) Summary
Layered timeout backstops
src/openhuman/rhai_workflows/policy.rs, src/openhuman/rhai_workflows/tools.rs, src/openhuman/rhai_workflows/ops.rs, src/openhuman/rhai_workflows/README.md
Timeout helpers establish inner, outer, and harness deadlines. Workflow timeout configuration uses the harness backstop. Documentation describes enforcement and cleanup behavior.
Session policy and eviction
src/openhuman/rhai_workflows/sessions.rs, src/openhuman/rhai_workflows/ops.rs, src/openhuman/rhai_workflows/README.md
Sessions retain their construction policy across reuse. Eviction skips busy sessions, removes idle alternatives, and permits bounded over-cap insertion when all sessions are busy.
Tool outcome reporting
src/openhuman/rhai_workflows/bridge.rs, src/openhuman/rhai_workflows/ops.rs, src/openhuman/rhai_workflows/types.rs
Dispatch records tool outcomes through thread-local tracking. Cell completion passes outcomes to call summarization, which reports tracked failures as ok: false. Documentation describes caught and batched tool failures.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant RhaiEvaluation
  participant RhaiToolAdapter
  participant ToolResult
  participant summarize_calls
  RhaiEvaluation->>RhaiToolAdapter: dispatch tool call
  RhaiToolAdapter->>ToolResult: execute tool
  ToolResult-->>RhaiToolAdapter: return result or error
  RhaiToolAdapter->>RhaiEvaluation: record outcome by call ID
  RhaiEvaluation->>summarize_calls: pass outcome map
  summarize_calls-->>RhaiEvaluation: return call summaries
Loading

Possibly related PRs

Suggested labels: rust-core, bug

Suggested reviewers: m3ga-mind

Poem

A rabbit watches deadlines align,
Three clocks tick in a careful design.
Busy sessions stay safe in their nest,
Failed tools now plainly confess.
Hop, hop—the summaries tell the rest!


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

…d session's policy (E-M1/E-M2)

E-M1 — the README promised `inner deadline < outer backstop < harness timeout`,
but `timeout_policy()` handed the harness the SAME clamped `secs` as the inner
`ReplPolicy.timeout`, so the harness could win the race and drop the `run_cell`
future — skipping the `RhaiError::Timeout` taxonomy, `finish_cell` accounting,
`close_session`, and the outer-backstop cleanup, while the detached blocking
thread kept the session mutex until the inner deadline and later calls got
`SessionBusy`. The three bounds are now derived from one place: inner = `secs`,
outer = `secs + 5`, harness = `secs + 10`, pinned by tests over the computed
values and by the live `RhaiTool::timeout_policy()`. README updated to match.

E-M2 — `get_or_create`'s build closure only runs for a FRESH session, so a
reused session kept its original policy while the wrapper computed `outer_bound`
and `limits_remaining` from the newly resolved one. A 300s session reused with
`timeout_secs: 30` hit the 35s outer backstop while its inner deadline was still
300s, and the backstop arm calls `manager.close(&key)` — dropping the whole
session and losing every binding it held. `SlotHandle` now carries the policy the
session was actually built with, and every per-call bound is computed from that,
warning when a caller requests a different one. The cache is deliberately NOT
keyed on a policy fingerprint: that would fragment sessions and lose bindings by
design rather than by bug.

E-m5 — `summarize_calls` hardcoded `ok: true` because the vendor's
`ReplCallRecord` carries no success flag, so a failed-but-caught capability call
was reported to the model as successful. Outcomes are now tracked host-side as
calls dispatch through the bridge.

E-m6 — LRU eviction could evict a slot whose cell was still in flight, making
`finish_cell` return `None` so `limits_remaining` reported a full budget for a
session that had just done work. Busy slots are now ineligible for both the
idle-TTL sweep and the cap eviction; if every slot is busy the incoming session
is admitted one over cap rather than evicting a running cell.

Nothing under vendor/ is modified. Worth filing upstream: a success flag on
`ReplCallRecord`, and a `set_policy` on `ReplSession` (its absence is why E-M2
is fixed host-side).

42 rhai_workflows tests pass.
@graycyrus
graycyrus force-pushed the fix/rhai-timeout-and-session-policy branch from ed11b94 to c70993c Compare July 30, 2026 19:09
@graycyrus
graycyrus marked this pull request as ready for review July 31, 2026 05:57
@graycyrus
graycyrus requested a review from a team July 31, 2026 05:57
@graycyrus
graycyrus merged commit 8144a54 into tinyhumansai:main Jul 31, 2026
21 of 24 checks passed

@greptile-apps greptile-apps 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.

graycyrus has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai coderabbitai Bot added bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. labels Jul 31, 2026

@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: c70993c547

ℹ️ 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".

// would drop the whole tool-execution future, skipping the
// `RhaiError::Timeout` taxonomy, `finish_cell` accounting,
// `close_session`, and the outer-backstop session cleanup.
ToolTimeout::Secs(harness_backstop_secs(secs))

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 Keep the harness above reused session timeouts

When a later cell reuses a session that was created with a longer timeout_secs but supplies a shorter value, this still computes the harness deadline from the new arguments, while run_cell now computes the inner and outer deadlines from handle.policy. In the OpenHuman tool executor that harness timeout is enforced before execute can finish, so a 300s session reused with timeout_secs: 30 can be killed around the shorter harness budget while the session's own 300s deadline and 305s outer backstop are still running, again skipping finish_cell/close_session and leaving the session busy.

Useful? React with 👍 / 👎.

Comment on lines +248 to +253
None => {
tracing::warn!(
live_sessions = map.len(),
"[rhai_workflows] session cap reached but every session is in flight — \
admitting one over cap rather than evicting a running cell"
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Bound all-busy session admission

When all 16 slots are in flight and a new unique session_id arrives, this branch only logs and get_or_create immediately inserts the new slot; the next such call repeats, so the process-global map can grow 17, 18, ... despite the MAX_SESSIONS finite cap. In concurrent chats or background agent runs with long Rhai cells, this removes the LRU bound and can accumulate arbitrary sessions until they finish; return a cap-exceeded/busy error or add a hard overshoot limit instead of admitting every new key.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant