feat(scheduler): add cache_ttl "never" sentinel for always-warm lanes - #245
Conversation
There was a problem hiding this comment.
All reported issues were addressed across 20 files
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
|
Note on the red The v0.33.0 schema added those four leaves without updating the dashboard's |
620c138 to
222c184
Compare
Brings the cache_ttl "never" sentinel (PR cortexkit#245) into the live union branch so the always-warm lanes can drop the 999h workaround. Resolutions: - rpc-handlers.ts: both imports (getSkillMemoryStats + parseCacheTtl). - rpc-handlers.test.ts / execute-status.test.ts: the PR branch was cut before external-memory made buildStatusDetail and executeStatus async, so its two new tests called them synchronously and asserted against a Promise. Added the awaits; the external-memory describe block also lost its closing braces to the merge and was restored.
|
Implementation looks right on our read (the "never" sentinel threading matches how cache_ttl flows through the scheduler). It needs a rebase onto current master before we can run the review gates — the release waves since it was opened moved the surrounding scheduler code. Happy to review as soon as it's rebased. |
222c184 to
86d362b
Compare
|
Rebased onto current master ( One conflict, in Gates on the rebased tree:
Note on lint: |
Lanes kept warm by external keepwarm mechanisms (prewarm proxies, dedicated cache-keep tools) re-warm the provider prompt cache out-of-band, so MC's idle>TTL heuristic false-positives: both TTL consumers (scheduler idle-execute and the ttl_idle m[0] fold) initiate a rebuild believed free that is actually a full paid cache-write (measured 450-560K tokens on large sessions). cache_ttl: "never" (string or per-model value, case-insensitive) disables both consumers: parseCacheTtl returns Infinity, so elapsed>ttl / elapsed>=ttl never fire. Rust scheduler mirrors the sentinel with u64::MAX (predicates already use saturating_sub). Status surfaces render "never expires (always-warm lane)" instead of a bogus countdown: /ctx-status, the TUI sidebar (JSON-safe cacheNeverExpires flag on StatusDetail), and Pi's status dialog (which previously parsed the TTL with a private fallback parser). hardCacheExpired extracted to a pure computeHardCacheExpired helper with direct coverage of the never-chain. Tradeoff documented in CONFIGURATION.md: on a genuinely cold start the free-fold window is not detected on such lanes; mutations then apply at the execute threshold.
…TL diagnostics - rpc-handlers: the cacheNeverExpires branch assigned Infinity to cacheRemainingMs; JSON.stringify converts Infinity to null over RPC, violating the numeric StatusDetail contract. Use 0 and let the cacheNeverExpires flag carry the semantics (the TUI keys on it first). - computeHardCacheExpired: the extraction dropped the invalid-cache-ttl-fallback pass outcome and session log on parse failure. Add an onInvalid callback; the transform call site restores the exact pre-extraction record + log. - test: seed last_response_time in the never-TTL status test — the guarded branch only runs when lastResponseTime > 0, so the assertion was vacuous without it (verified red: Infinity revert now fails it).
86d362b to
16315e4
Compare
There was a problem hiding this comment.
1 issue found across 20 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/plugin/src/plugin/rpc-handlers.ts">
<violation number="1" location="packages/plugin/src/plugin/rpc-handlers.ts:740">
P3: For never-expiring lanes the code collapses both `cacheTtlMs` and `cacheRemainingMs` to `0` as a JSON-safe workaround, relaying correctness entirely to the new `cacheNeverExpires` flag. This is fine for the in-batch TUI consumers (they key on `cacheNeverExpires` first), but the required numeric fields now report values that are indistinguishable from a brand-new/expired lane to any consumer that reads `cacheExpired`/`cacheRemainingMs`/`cacheTtlMs` without checking the flag. Consider documenting this convention on the `StatusDetail` fields (or keeping `cacheTtlMs` at the raw parsed value and only guarding `cacheRemainingMs`) so future RPC consumers don't misinterpret 0 remaining as "expired".</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| detail.cacheTtlMs = safeParseTtl(detail.cacheTtl); | ||
| if (detail.cacheTtlMs === Number.POSITIVE_INFINITY) { | ||
| detail.cacheNeverExpires = true; | ||
| detail.cacheTtlMs = 0; |
There was a problem hiding this comment.
P3: For never-expiring lanes the code collapses both cacheTtlMs and cacheRemainingMs to 0 as a JSON-safe workaround, relaying correctness entirely to the new cacheNeverExpires flag. This is fine for the in-batch TUI consumers (they key on cacheNeverExpires first), but the required numeric fields now report values that are indistinguishable from a brand-new/expired lane to any consumer that reads cacheExpired/cacheRemainingMs/cacheTtlMs without checking the flag. Consider documenting this convention on the StatusDetail fields (or keeping cacheTtlMs at the raw parsed value and only guarding cacheRemainingMs) so future RPC consumers don't misinterpret 0 remaining as "expired".
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/plugin/src/plugin/rpc-handlers.ts, line 740:
<comment>For never-expiring lanes the code collapses both `cacheTtlMs` and `cacheRemainingMs` to `0` as a JSON-safe workaround, relaying correctness entirely to the new `cacheNeverExpires` flag. This is fine for the in-batch TUI consumers (they key on `cacheNeverExpires` first), but the required numeric fields now report values that are indistinguishable from a brand-new/expired lane to any consumer that reads `cacheExpired`/`cacheRemainingMs`/`cacheTtlMs` without checking the flag. Consider documenting this convention on the `StatusDetail` fields (or keeping `cacheTtlMs` at the raw parsed value and only guarding `cacheRemainingMs`) so future RPC consumers don't misinterpret 0 remaining as "expired".</comment>
<file context>
@@ -741,11 +734,22 @@ export function buildStatusDetail(
+ detail.cacheTtlMs = safeParseTtl(detail.cacheTtl);
+ if (detail.cacheTtlMs === Number.POSITIVE_INFINITY) {
+ detail.cacheNeverExpires = true;
+ detail.cacheTtlMs = 0;
+ }
if (detail.lastResponseTime > 0) {
</file context>
|
Merged — thanks for the rebase and for the thorough consumer coverage. Reviewed all four TTL consumers (scheduler defer→execute, HARD-fold trigger, /ctx-status, RPC/TUI status) and each handles the sentinel correctly; the RPC test that pins Infinity out of the JSON contract (and seeds last_response_time so the assertion can't pass vacuously) is exactly the kind of test we want. Nice touch replacing Pi's duplicate local TTL parser with the shared one — that removes a drift class along the way. The docs' honest cold-start caveat (a dead keepwarm means no free-fold detection until the execute threshold) is appreciated. Ships in the next release. |
Post-merge review on #245 (cubic): collapsing the never-lane to 0 made a warm lane numerically indistinguishable from a fresh/expired one for any consumer that reads the numeric fields without the cacheNeverExpires flag. -1 discriminates by value alone (falsy-value contract: -1 never / 0 expired-or-unset / N live), documented on the wire type. Pi's dialog is in-process (no JSON boundary) and legitimately keeps Infinity.
|
Follow-up on cubic's post-merge comment: valid catch, fixed on master (410c1bd). Collapsing the never-lane to |
Problem
cache_ttlassumes idle > TTL means the provider evicted the prompt cache, so the next prefix rebuild is free. Two consumers act on that assumption:scheduler.ts), andmustMaterializefires thettl_idleHARD fold (viahardCacheExpiredintransform.ts).On lanes kept warm by an external keepwarm mechanism (prewarm proxies, dedicated cache-keep tools that re-warm the provider cache out-of-band), the assumption is wrong: the cache never goes cold, MC's
lastResponseTimegoes stale anyway, and both consumers false-positive. MC then initiates a rebuild it believes is free that is actually a full paid cache-write — measured 450-560K tokens per fold on large sessions. There was no way to express "this lane never goes cold": the config requires a duration.Fix
cache_ttl: "never"(case-insensitive, works as the string form or any per-model value):parseCacheTtl("never")returnsInfinity; both consumers go inert through the existing comparisons (elapsed > Infinity/elapsed >= Infinityare never true) — no new branches in the hot path.u64::MAX(its predicates already usesaturating_sub, so no overflow path)./ctx-statusshowsnever expires (always-warm lane); the TUI sidebar gets a JSON-safecacheNeverExpiresflag onStatusDetail(Infinity does not survive JSON-RPC); Pi's status dialog now uses the sharedparseCacheTtl(it previously used a private fallback parser that would have shown a 5m countdown).hardCacheExpiredis extracted into a purecomputeHardCacheExpiredhelper so the"never" -> Infinity -> falsechain has direct unit coverage instead of only flag-consumption coverage.Docs:
CONFIGURATION.mddescribes the sentinel, what it disables, and the tradeoff — after a genuinely cold start (e.g. the keepwarm process died), the free-fold window is not detected on such lanes; mutations then apply at the execute threshold.Verification
tsc --noEmitclean on bothcomputeHardCacheExpiredtestscheduler.rs's test module (note: I could not runcargo testlocally — the crates workspace has path deps outside this repo; the change is a 3-line early return mirroring the TS logic)build-schema/build-config-docsdrift tests green);check:tui-compiledgreenNeed help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by cubic
Adds
cache_ttl: "never"to disable the idle-TTL heuristic on always-warm lanes, preventing false executes and paid cache rebuilds. Status surfaces now show “never expires” and keep RPC fields JSON-safe; invalid-TTL diagnostics are restored.New Features
parseCacheTtl("never")returns Infinity (TS) /u64::MAX(Rust) so idle-execute andttl_idlenever fire;computeHardCacheExpiredexported for consistent checks withonInvalid./ctx-status, TUI, Pi) render “never expires” via a JSON-safecacheNeverExpiresflag; Pi uses the shared parser; docs and schema updated.Bug Fixes
Infinityby keepingcacheRemainingMsnumeric (0) when TTL is “never” and keying oncacheNeverExpires; auto-execute messaging switches to threshold-only.onInvalidcallback.Written for commit 16315e4. Summary will update on new commits.
Greptile Summary
Adds an always-warm cache sentinel across scheduler and status paths.
cache_ttl: "never"as a non-expiring TTL in TypeScript and Rust.Confidence Score: 5/5
The PR appears safe to merge.
No blocking failures remain in the fixes associated with the previous review threads.
Important Files Changed
neversentinel as positive infinity.u64::MAX.Reviews (5): Last reviewed commit: "fix(scheduler): keep cacheRemainingMs JS..." | Re-trigger Greptile