Skip to content

fix(memory/sources): size ingest RPC budget for multi-window sessions - #5531

Merged
M3gA-Mind merged 2 commits into
tinyhumansai:mainfrom
YellowSnnowmann:fix/ingest-timeout-budget-5509
Aug 13, 2026
Merged

fix(memory/sources): size ingest RPC budget for multi-window sessions#5531
M3gA-Mind merged 2 commits into
tinyhumansai:mainfrom
YellowSnnowmann:fix/ingest-timeout-budget-5509

Conversation

@YellowSnnowmann

@YellowSnnowmann YellowSnnowmann commented Aug 13, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Resize the ingest_coding_sessions RPC wall-clock ceiling so a legitimate multi-session backfill is not killed mid-flight.
  • The old ceiling (120 + N×30) assumed one LLM call per session; TinyCortex's persona pipeline splits an oversized session into windows and issues one LLM call per window, so a multi-window session drives several sequential calls and blows the budget.
  • Extract the computation into a pure, unit-tested ingest_budget(max_sessions) and correct the false comment.

Problem

Reported in #5509 (Bug 1). src/openhuman/memory/sources/rpc.rs computed the RPC timeout as 120 + min(max_sessions, 1000) × 30 on the premise, stated in its own comment, that "each session drives at most one LLM call." That premise is false: digest_session splits each session into WINDOW_CHARS-sized windows and fires one LLM call per window (each observed at 20–45 s). A dense backfill of 15 sessions therefore hit the exact 120 + 15×30 = 570 s ceiling and was killed mid-flight, dropping the remaining sessions.

Solution

  • Extract ingest_budget(max_sessions) — a pure function sized for multiple windows per session (PER_SESSION_SECS = 120, ~4 sequential per-window calls) instead of one. A healthy backfill now finishes well inside the ceiling while a genuinely wedged run still terminates.
  • Preserve the untrusted-input cap (MAX_SESSIONS_FOR_BUDGET = 1000) so an inflated max_sessions cannot turn the ceiling into an effectively-infinite wait.
  • Correct the false "one LLM call per session" comment to describe the real per-window call pattern.
  • ingest_budget takes usize to match the request field type (CodingSessionIngestRequest::max_sessions).

This is the RPC-timeout half of #5509. The digest-truncation half (DIGEST_MAX_OUTPUT_TOKENS raise + non-committable truncated windows) lives in TinyCortex — tinyhumansai/tinycortex#145. That half is what makes the extra per-window calls succeed; the vendored vendor/tinymemorytinycortex submodule pointer is bumped in a follow-up PR once #145 merges. #5509 should be closed by that follow-up (both halves present), not by this PR alone.

Submission Checklist

  • Tests added or updated (happy path + failure / edge case) — budget_tests: the 15-session regression (proves the new ceiling exceeds the old 570 s) and the untrusted-max_sessions cap.
  • Diff coverage ≥ 80% — the two added tests exercise every line of the new ingest_budget function (the only changed executable lines).
  • Coverage matrix updated — N/A: behaviour-only change (timeout-budget arithmetic; no new feature row).
  • All affected feature IDs listed under ## RelatedN/A: no matrix feature touched.
  • No new external network dependencies introduced.
  • Manual smoke checklist updated if this touches release-cut surfaces — N/A: internal RPC timeout budget only.
  • Linked issue closed via Closes #NNN — intentionally not closing here; see ## Related (needs the digest half + submodule bump).

Impact

  • Desktop/CLI coding-session ingest only. No API surface change. Purely widens an internal wall-clock ceiling; a wedged run is still bounded and terminated.
  • No performance/security/migration implications: the budget is a kill-ceiling, not a latency target.

Related


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

Linear Issue

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

Commit & Branch

  • Branch: fix/ingest-timeout-budget-5509

Summary by CodeRabbit

  • Bug Fixes
    • Improved reliability when ingesting coding sessions and processing multiple LLM windows.
    • Increased per-session processing time while using smaller batches to reduce timeout failures.
    • Added safeguards that cap processing limits for unusually large requests.
    • Improved drain processing by using repeated, bounded batches within the available timeout window.

The `ingest_coding_sessions` RPC computed its wall-clock ceiling as
`120 + min(max_sessions, 1000) * 30` on the premise that "each session
drives at most one LLM call". That premise is false: TinyCortex's persona
pipeline splits an oversized session into `WINDOW_CHARS`-sized windows and
issues one LLM call per window, so a multi-window session drives several
sequential calls. A dense backfill blew the ceiling — 15 sessions hit the
exact 570 s budget (`120 + 15*30`) and were killed mid-flight.

Extract the computation into a pure `ingest_budget(max_sessions)` and size
the per-session allowance for multiple windows (`PER_SESSION_SECS = 120`,
~4 sequential calls) rather than one, so a legitimate backfill runs to
completion while a genuine infinite hang still terminates. The untrusted
`max_sessions` cap (1000) is preserved, and the false comment is corrected.
Adds unit tests for the new formula, including the 15-session regression
and the cap. `ingest_budget` takes `usize` to match the request field type.

This is the RPC-timeout half of tinyhumansai#5509. The digest-truncation half lives in
tinycortex (retain observations when a digest is truncated); the vendored
tinycortex submodule pointer is bumped in a follow-up once that lands.

Part of tinyhumansai#5509
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 895c14b7-48d2-4083-be76-affddef20fe4

📥 Commits

Reviewing files that changed from the base of the PR and between dc9fb78 and 69199c8.

📒 Files selected for processing (3)
  • app/src/services/memorySourcesService.test.ts
  • app/src/services/memorySourcesService.ts
  • src/openhuman/memory/sources/rpc.rs

📝 Walkthrough

Walkthrough

The ingestion client now processes five sessions per batch with a 585-second timeout. The RPC calculates a per-session budget of 90 seconds plus a 120-second base, capped at 600 seconds. Tests cover scaling, overflow-safe capping, timeout ordering, and repeated drain passes.

Changes

Ingestion timeout scaling

Layer / File(s) Summary
Ingestion budget calculation and validation
src/openhuman/memory/sources/rpc.rs
The RPC uses a capped ingest_budget formula with saturating arithmetic. Tests cover scaling, maximum inputs, and the server-client timeout relationship.
Client batch and timeout alignment
app/src/services/memorySourcesService.ts, app/src/services/memorySourcesService.test.ts
The client limits batches to five sessions, uses a 585-second timeout, and validates repeated bounded drain passes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Mergeability Score: ⚪ Minimal · up to 69199

The PR widens the bounded ingest timeout to support legitimate multi-window sessions without changing the API or permissions. No actionable merge-blocking risk remains after normal checks and review.

Possibly related issues

Possibly related PRs

Suggested labels: memory, bug

Suggested reviewers: senamakel

Poem

A rabbit trims the batch to five,
While capped clocks keep the work alive.
Ninety seconds guide each run,
Six hundred ends the race when done.
Hop, hop—the windows fit! 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR fixes the multi-window timeout, but leaves digest truncation, cursor commits, and user notifications from issue [#5509] unresolved. Implement the remaining digest, retry or cursor, and notification requirements, or split the issue scope explicitly.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: sizing the ingest RPC budget for sessions that require multiple LLM windows.
Out of Scope Changes check ✅ Passed The server budget, client batch sizing, timeout tests, and comments directly support the linked issue's timeout objective.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@YellowSnnowmann
YellowSnnowmann marked this pull request as ready for review August 13, 2026 13:29
@YellowSnnowmann
YellowSnnowmann requested a review from a team August 13, 2026 13:29

@tinysweeper tinysweeper 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.

tinysweeper found nothing blocking. Approving.

             $0.0078 · 13,742 in / 2,050 out · 9,048 cached (66%) · z-ai/glm-5.2
critique:    $0.0022 · 3,253 in  / 831 out   · 2,333 cached (72%) · z-ai/glm-5.2
security:    $0.0023 · 3,232 in  / 187 out   · 896 cached (28%)   · z-ai/glm-5.2
tests:       $0.0021 · 3,147 in  / 805 out   · 2,366 cached (75%) · z-ai/glm-5.2
description: $0.0011 · 4,110 in  / 227 out   · 3,453 cached (84%) · z-ai/glm-5.2

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Aug 13, 2026
@coderabbitai coderabbitai Bot added the bug label Aug 13, 2026
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 13, 2026
@M3gA-Mind

Copy link
Copy Markdown
Collaborator

Review — comment only (no approval)

Reviewed dc9fb786b against origin/main baaf89490. The diagnosis is right and the extraction is clean, but I think the fix is on the wrong side of the wire: for #5509's exact repro it buys about 15 seconds, not 22 minutes.

F1 — the client aborts at 585 s, 15 s after the old server ceiling

This raises the 15-session server ceiling 570 s → 1920 s. The only shipped caller imposes its own bound on the same call — app/src/services/memorySourcesService.ts:231-234, 259-262:

CODING_SESSION_BATCH_MAX = 15;
CODING_SESSION_BASE_TIMEOUT_MS = 120_000;
CODING_SESSION_PER_SESSION_TIMEOUT_MS = 30_000;
CODING_SESSION_RPC_GRACE_MS = 15_000;
// timeoutMs = 120s + 15×30s + 15s = 585s

That is a mirror of the old server formula plus a grace, and the comment right above it (:225-231) says so: "A single ingest RPC is bounded so it stays under the core RPC client's ten-minute ceiling: 120s + 15 * 30s + 15s ≈ 585s." It's a documented two-sided contract; this PR changes one side and leaves that comment describing a formula that no longer exists.

#5509 reports Run started 08:31:52, died 08:41:22 — exactly 570 s, so the server ceiling was indeed the binding constraint. After this change:

before after
server ceiling (15 sessions) 570 s 1920 s
client ceiling (15 sessions) 585 s 585 s
what fires server @ 570 s client @ 585 s

Net gain ≈ 15 s; the other 1335 s is never observed. And it can't be recovered by raising the client constants either — coreRpcClient.ts:47 sets PER_CALL_TIMEOUT_MAX_MS = 10 * 60 * 1_000 and resolvePerCallTimeoutMs (:49-57) clamps every per-call override to 600 s. A server budget above ~600 s is unreachable by construction. (Separately and pre-existing: coreRpcClient.ts:656-659 drops timeoutMs entirely on the cloud/tunnel transports, which use a flat 30 s — so no client path anywhere reaches the new ceiling.)

So #5509's acceptance criterion — "bulk ingest of 15 large Codex sessions completes without hitting the RPC timeout ceiling" — still isn't met. What would close it: raise the client constants and cut CODING_SESSION_BATCH_MAX so a pass fits under 600 s (the drainCodingSessions loop at :322 already exists to scale by passes, with 2000 passes of headroom); or land tinycortex#145 so 30 s/session stops being structurally undersized; or derive one budget from the other instead of hand-syncing two copies of the same arithmetic — which is how they drifted apart.

Counter-argument, for the record: the client abort is an AbortController on fetch and does not cancel the core-side future, and #5509 confirms cursors commit incrementally. So the server does keep working past 585 s and the next run finds more done. That's a genuine benefit — but it's accidental, invisible to the user (error toast + aborted drain, CodingSessionsCard.tsx:106-109), and it pins a blocking-pool thread the whole time (rpc.rs:91-98 wraps runtime.block_on inside spawn_blocking). If that's the intended value, worth stating in the PR.

F2 — the MAX_SESSIONS_FOR_BUDGET cap no longer does what its doc claims

The doc says an inflated max_sessions "cannot turn the ceiling into an effectively-infinite wait", but at the cap the ceiling is now 120 + 1000×120 = 120_120 s = 33.4 hours (was 8.4 h) — on a blocking-pool worker. The UI clamps to ≤ 15, but the method is an advertised programmatic entry point (platform/about_app/catalog_data.rs:561), so a direct RPC caller can pass 1000. Capping the resulting Duration (e.g. .min(Duration::from_secs(3_600))) rather than the multiplier would make the stated guarantee true while keeping the per-session scaling.

F3 — the stated derivation of PER_SESSION_SECS = 120 doesn't match the PR's own data

"budgets for up to roughly four sequential LLM calls at the provider's own per-call timeout" implies 30 s/call, but the PR body and #5509 both report windows "each observed at 20–45 s" — at the observed upper end that's ~2.7 calls, not four, and no named provider-timeout constant is referenced so the figure isn't checkable. The budget is aggregate across the batch so it still works out in practice; this is accuracy-of-docs only. Given the PR's headline is "the old comment stated a false premise", it seems worth not replacing it with a second unverifiable one. Also: #5509 asked for a budget that factors in windows-per-session; this is a flat 4× — defensible for a kill-ceiling, but worth saying it's a deliberate simplification.

Verified fine

Three-dot diff is one file, +72/−6; the branch is 63 commits behind main but merge-base b508b3ce7 is an ancestor of main and there's a single commit, so the noisy two-dot view is staleness, not a hidden revert. No overflow (sessions capped before the as u64 multiply). The extraction itself — pure function, named constants replacing three inline magic numbers, false "one LLM call per session" comment removed — is a straight improvement. And the Closes discipline is right: deliberately not auto-closing #5509, with the reason stated.

Tests

budget_tests are non-vacuous in the narrow sense — reverting PER_SESSION_SECS to 30 makes ingest_budget(15) return 570 and fails both assertions. But they're change-detector tests: they assert the formula the same commit just wrote, and can't fail for any reason connected to the actual bug. Nothing pins the thing that actually broke — that the server budget must be the looser of the two for the same max_sessions, and that the client's stays under PER_CALL_TIMEOUT_MAX_MS. That guard is cheap and would have caught this gap.

CI

Still in flight: Rust Quality (fmt, clippy), Rust Feature-Gate Smoke, Rust RSS Benchmark pending; CodeRabbit in progress; Rust Core Coverage (the lane that compiles mod budget_tests) not listed yet and TinyCortex Memory Tests skipping. Nothing has run the new tests so far.

Bottom line: clean refactor, correct diagnosis, honest PR body — but the binding ceiling just moves from the server's 570 s to the client's 585 s. I'd pair it with the memorySourcesService.ts change (or a CODING_SESSION_BATCH_MAX reduction) before treating #5509 Bug 1 as addressed, and fix the now-stale comment at memorySourcesService.ts:225-231 either way. Approve/merge is the maintainer's call — deliberately not approving.

… ceiling

Address review on tinyhumansai#5531: the server-only budget raise did not reach the
client. The frontend RPC client clamps every per-call timeout to
PER_CALL_TIMEOUT_MAX_MS = 600s (coreRpcClient.ts), and memorySourcesService
bounds the same call at 120s + 15*30s + 15s = 585s — a mirror of the *old*
server formula. So raising the server ceiling to 1920s only moved the binding
constraint from the server's 570s to the client's 585s (~15s gain), and any
budget above 600s is unreachable by construction.

Fix the real constraint on both sides of the wire so a pass fits under the
600s ceiling and large histories drain across passes:

- Server (rpc.rs): size ingest_budget at 120 + N*90s (an honest multi-window
  estimate matching the 20-45s/window observed in tinyhumansai#5509) and hard-cap the
  Duration at 600s. The Duration cap replaces the multiplier cap, so an
  untrusted max_sessions=1000 can no longer pin the blocking worker for ~33h.
- Client (memorySourcesService.ts): drop CODING_SESSION_BATCH_MAX 15 -> 5 and
  raise per-session 30s -> 90s, so a pass is 120 + 5*90 + 15 = 585s < 600s;
  drainCodingSessions already iterates passes (2000-pass cap covers ~10k
  sessions). Correct the stale "120s + 15*30s" comment.
- Keep the server budget the tighter of the two (570s < client 585s) so the
  server returns a clean structured timeout before the client's fetch aborts.

Tests: server_budget_is_reachable_and_tighter_than_the_client pins the
cross-wire invariant (server <= client <= 600s) that would have caught the
server-only gap; budget_is_capped_at_the_reachable_ceiling pins the cap; the
client test guards timeoutMs <= 600s.

Part of tinyhumansai#5509
@YellowSnnowmann

Copy link
Copy Markdown
Collaborator Author

Verified all three findings against the code and you're right — F1 is the load-bearing one. Reworked in 69199c829.

F1 — reachable ceiling. Confirmed: memorySourcesService.ts:259-262 bounds the same call at 120 + 15×30 + 15 = 585s, a mirror of the old server formula, and coreRpcClient.ts:49-57 clamps every override to PER_CALL_TIMEOUT_MAX_MS = 600s, so a server budget above that is unreachable. Raising the server ceiling alone just moved the binding constraint from 570s to 585s. So the fix now targets the batch, not the ceiling:

  • Client: CODING_SESSION_BATCH_MAX 15 → 5, per-session 30s → 90s. A pass is now 120 + 5×90 + 15 = 585s < 600s — reachable — and drainCodingSessions (2000-pass cap ≈ 10k sessions) drains a 15-session backlog in 3 passes. Fixed the stale :225-231 comment to describe the real model and the 600s clamp.
  • Server (ingest_budget): 120 + N×90s (the honest multi-window number, see F3), hard-capped at 600s.

F2 — the cap. Fixed by capping the resulting Duration at 600s rather than the multiplier, as you suggested. max_sessions = 1000 now yields 600s, not 33.4h, so the advertised programmatic RPC can't pin a blocking-pool thread past the reachable ceiling.

F3 — the derivation. Dropped the "4 calls × 30s" figure; PER_SESSION_SECS = 90 is now documented as ~3 windows at the observed 20–45s/window span, a deliberate flat estimate rather than an unverifiable per-session window count.

On "pin the invariant that actually broke" — done, and this is the part I'd most like your eyes on. Added server_budget_is_reachable_and_tighter_than_the_client, which asserts server(5)=570 ≤ client=585 ≤ 600 — the guard that would have failed on the server-only 1920s version. The client test also now asserts timeoutMs ≤ 600s.

One deliberate divergence from your framing. You leaned toward the server being the looser of the two (client aborts, server grinds on invisibly past it and the next pass finds more). I kept the server tighter (570 < 585) on purpose: with the batch now sized to fit under 600s, I'd rather the server hit its ceiling first and return a clean structured timeout the UI can render — and release the blocking thread immediately — than pin the thread while the user sees only the raw fetch-abort toast at CodingSessionsCard.tsx:106-109 you flagged. The "server keeps working past the client abort" benefit is real but, as you said, accidental and invisible; sizing the batch to fit removes the need to rely on it. Happy to flip if you still prefer server-looser — it's a one-constant change plus the invariant test.

Unrelated CI note. The red Rust Core Coverage lane is pre-existing broken-main, not from this change: openhuman::memory::binding::tests panic with "no reactor running" because the module-bind path eagerly tokio::spawns the ingestion worker (queue.rs:191), violating the "binding stays synchronous" invariant those tests document (binding_tests.rs:342). It's byte-identical on origin/main and fails on the same pinned tinymemory pointer — this PR is just the first to scope coverage into openhuman::memory. Root fix belongs in vendored tinymemory (defer the worker spawn out of the synchronous bind path); not folding it in here.

@coderabbitai coderabbitai Bot added the memory Memory store, memory tree, recall, summarization, and embeddings in src/openhuman/memory/. label Aug 13, 2026

@tinysweeper tinysweeper 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.

tinysweeper found nothing blocking. Approving.

             $0.0558 · 29,733 in / 18,526 out · 17,405 cached (59%) · z-ai/glm-5.2
critique:    $0.0261 · 9,762 in  / 9,528 out  · 6,442 cached (66%)  · z-ai/glm-5.2
security:    $0.0054 · 8,570 in  / 1,386 out  · 7,112 cached (83%)  · z-ai/glm-5.2
tests:       $0.0119 · 5,177 in  / 4,360 out  · 3,851 cached (74%)  · z-ai/glm-5.2
description: $0.0125 · 6,224 in  / 3,252 out  · 0 cached (0%)       · z-ai/glm-5.2

// Every pass stays bounded to the timeout-safe per-call maximum.
expect(mockedCall).toHaveBeenLastCalledWith(
expect.objectContaining({ params: { backfill: false, max_sessions: 15 } })
expect.objectContaining({ params: { backfill: false, max_sessions: 5 } })

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

priority medium critique uncertain

Align mock returns and pass count with the new max_sessions cap of 5

The test now asserts each call sends max_sessions: 5, but still expects 40 sessions processed in 3 passes (// 15 + 15 + 10). If max_sessions is a per-call batch cap — which is the entire purpose of clamping it to 5 for timeout safety (120s + 5*90s + 15s = 585s) — then 3 passes can process at most 15 sessions, not 40. The mock evidently returns ~15 per call regardless of the param, so the test passes but exercises a scenario the real backend (respecting max_sessions: 5) could never produce. The // 15 + 15 + 10 comment is now stale. If a future maintainer makes the mock realistic (return ≤5 per call), the passes: 3 and sessionsProcessed: 40 assertions will break. Confidence: 0.65 (the mock setup is not shown in the diff; inferred from the comment and totals).

[RULE] Tests must assert behavior that can actually occur ·

@tinysweeper

tinysweeper Bot commented Aug 13, 2026

Copy link
Copy Markdown

What this change touches

3 files, +142 -20 across 2 components. The code graph knows nothing about these files yet — normal for newly added files, and a cold index otherwise.

flowchart LR
  n0["src/openhuman/memory/sources<br/>1 file +116 -6"]:::changed
  n1["app/src/services<br/>2 files +26 -14<br/>1 finding"]:::flagged
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed. Grey: untouched, reached through an import or a call. Orange: has findings. Red: has a finding that blocks the merge.

Component Files Lines Findings
src/openhuman/memory/sources changed 1 +116 -6
app/src/services changed 2 +26 -14 1 (medium)
Changed files

src/openhuman/memory/sources

  • src/openhuman/memory/sources/rpc.rs

app/src/services

  • app/src/services/memorySourcesService.test.ts
  • app/src/services/memorySourcesService.ts

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot added priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later. and removed priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. labels Aug 13, 2026
@M3gA-Mind
M3gA-Mind merged commit 09d4b71 into tinyhumansai:main Aug 13, 2026
31 of 41 checks passed
@github-project-automation github-project-automation Bot moved this from Todo to Done in Team Openhuman Aug 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug memory Memory store, memory tree, recall, summarization, and embeddings in src/openhuman/memory/. priority: p2 Soon. Real but survivable — a rough edge, a gap, a thing that will bite later.

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants