Repository navigation
Conversation
#868) The calculator and the recommendation modelled a fixed K per idle span. Since #867 the proxy picks the count from what the thread waits for. - keepalive: Policy.SpanPings is the keeper's rule for a stored row (the wait cause, the no-tool rule, done, retry, OpenAI, 1-hour tier). It calls WaitPings. The tool-stop rule moves into one shared function. - dash: pingSpans and the expiry read carry the stored facts of the request before each gap, with the thread's tool history, in one statement each. - The calculator has a live-plan rung marked current, and the fixed-K rungs beside it. Each rung and the recommendation state their model (plan: wait_plan or fixed_k). keepalive_wait_plan: false keeps fixed K. - The proxy tells the dashboard the account's plan settings. Assisted-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
amiddavid
left a comment
There was a problem hiding this comment.
| Reviewer | Kind | Started by | Context | Review | Round | Head | Verdict |
|---|---|---|---|---|---|---|---|
| Reviewer 1 (glm, user of @amiddavid) | GLM rits/zai-org/glm-5-3 via Claude Code 2.1.316 | offload-review skill, @amiddavid's session | fresh | 1 of 2 | 1 of 2 | fd354b768116 |
blocking |
Verdict: blocking
Findings
-
keepalive/waitplan.go:168 — the plan replay charges the base count for 1-hour-tier spans; the live plan sends the 1-hour schedule
- What fails: an account with the plan on, base K=2, X=240 s, and a thread on the 1-hour tier (
cache_ttl = 'ephemeral_1h') that goes idle for 2 hours at session end. The proxy sends 1 ping at 59 minutes (ForOneHour:MaxPings1h, default 1;Idle1h, default 59 min). The calculator's "Live plan, base 2 (current)" rung counts 2 pings for that span, charged at the 240 s cadence. The PINGS column and the ping cost are wrong by 2x at the defaults. They are wrong in the other direction when an account tunesMaxPings1habove its base. - Proof:
keepalive/keeper.go:891-896swaps inpol.ForOneHour()for a 1-hour thread;keepalive/types.go:191-207setsMaxPings = MaxPings1h(default 1,internal/modelinfo/cachefrac.go:27) andIdle = 59 min;docs/design.md:251names the live schedule "1 ping at 59 minutes on the 1-hour tier".waitplan.go:168-170returnsp.MaxPings(the base) fors.OneHour, anddash/keepalive.go:1294(planPings) charges that count at the X-second cadence. The PR body says "The 1-hour tier uses the base count, as the keeper does" — the keeper does not.TestDashboardReplayMatchesTheKeeperhas no 1-hour case, so nothing pins this. - A second divergence in the same branch:
OneHourreads the tier the request asked for (the storedcache_ttl), while the keeper reads the tier the provider billed (ResolveTier,keeper.go:267-275:CacheWrite1h >= CacheWrite, sticky per thread). A mixed-tier row (asked 1 h, billed partly 5 m) runs the per-wait plan in the proxy — a tool wait gets up to 8 — but gets the base count in the replay. - Fix: model the 1-hour tier with its own schedule (
Idle1h/MaxPings1h, defaults 59 min / 1), or keep the simplification and state it in the panel copy and the code comment, and pin the gap in a test as the spliced-stop gap is pinned. Drop the "as the keeper does" claim either way.
- What fails: an account with the plan on, base K=2, X=240 s, and a thread on the 1-hour tier (
-
keepalive/waitplan.go:165 — a stored "retry" wait gets 0 pings, but the keeper pings after a failed call
- What fails: a non-2xx response that still reported usage with
cache_readover the 20k floor, followed by a long idle gap. The proxy keeps the account policy on for that request (proxy/keepalive.go:349—trackedgates only the campaign and the manual hold), the keeper tracks it (keeper.go:865refuses only whenusageOKand both cache counts are 0) and plans its base count. The keeper never classifies a wait as "retry" itself: itsWaitingFor(waitplan.go:93) passesfailed = false, so the wait reads unknown or human. The stored row sayswaiting_for = 'retry'(proxy/dashcapture.go:512,535), so the replay charges 0 pings for the span. The plan rung undercounts its ping cost — the flattering direction. - Proof: the trace above; the doc at
waitplan.go:155says "the keeper holds nothing for them", and the code does not do that. The case is rare (it needs a failed call with parsed usage and a long idle after it), so this is minor next to finding 1. - Fix: drop the
WaitingRetrybranch and let a stored "retry" read as its classifier fallback, or make the claim true in the keeper and pin that in a test. Fix the comment either way.
- What fails: a non-2xx response that still reported usage with
Non-blocking
dash/ui/app.js:9580— the plan rung and the fixed rung with the same K share onedata-testid(ka-ladder-k2appears twice). A selector now matches two rows.dash/ui/app.jskaRungCoveragehardcodes "a tool up to 8" and "a person base+1". Wrong copy for an account that tunedkeepalive_tool_wait_max_pingsorkeepalive_human_extra_pings.- The OpenAI cadence (28 minutes) and the 1-hour reach are still judged at the 5-minute policy, as before this PR. Only the COUNT is new. The fixed rungs carry the same simplification, so the comparison stays fair.
proxy/keepalive.go:553DashWaitPolicyfalls back to plan-on (DefaultWaitPolicy) when the tenant lookup fails. The panel then marks the plan rung current for an account that may run fixed-K.- The
seenwindow counts a tool stop or a storedown_tool. The keeper also counts an incomplete name list (calledATool). Same family as the pinned spliced-stop gap.
What I checked
- The full diff of all 14 files, and the code around each change:
keepalive/waitplan.go,keepalive/keeper.go(Record, pingable, ForOneHour, ResolveTier, Plans),keepalive/types.go,internal/waitcause(the classifier, retry and done),dash/keepalive.go(pingSpans, the new expiries, the calc and the recommendation),dash/keepaliveapi.go,dash/api.go,proxy/keepalive.go,proxy/dashcapture.go(whatwaiting_forstores),cmd/context-guru-proxy/main.go,dash/ui/app.js, both docs, both goldens, and the three test files. - That the replay's SQL returns the same rows as the fixed-K paths (
factsCTEvsaddressableCTEvs the oldpingSpans): same filter, same gate, pings excluded. - That
storedWaitreconstructs the keeper's tool-stop rule from the stored cause, and thatSpanPingsmatches the keeper for the 14 drift-test cases plus the OpenAI, old-row and no-tool paths. - The PR body: behavior-change statement, data-dependence statement and
docs/data-dependence.mdrow, golden diffs. Both are present and accurate for the columns read. - No tests were run (live tests not approved); all claims above are from reading the code.
amiddavid
left a comment
There was a problem hiding this comment.
| Reviewer | Kind | Started by | Context | Review | Round | Head | Verdict |
|---|---|---|---|---|---|---|---|
| Reviewer 2 (glm, user of @amiddavid) | GLM rits/zai-org/glm-5-3 via Claude Code 2.1.316 | offload-review skill, @amiddavid's session | fresh | 2 of 2 | 1 of 2 | fd354b768116 |
blocking |
Verdict: blocking
Findings
- proxy/keepalive.go:553 — DashWaitPolicy reads only the account's own config, so the calculator marks the per-wait plan "current" for accounts whose live policy has the plan off.
- What fails: A tenant with
keepalive_wait_planon (the default) and an ACTIVE learned campaign runs a fixed count. The request path forces the plan off for it:pol.WaitPlan = falseat proxy/keepalive.go:460, because the campaign owns the count. A manager strategy withHeadTTL1hhas the same effect through a second door: its Idle band allows up to 3590 s (keepalive/types.go:188), and the keeper applies per-wait counts only below 300 s of Idle (keepalive/keeper.go:903). For such an account,/api/keepalive/calcanswersplan: "wait_plan"and marks the plan rungcurrent;/api/keepalive/recommendmodels the plan interval. The user reads "Live plan, base 2 (current)" while the proxy sends one fixed count for every wait. - Proof: cmd/context-guru-proxy/main.go:745 says the hook exists "so the calculator replays the policy that runs (#868)". The hook returns
tn.Cachefields only (proxy/keepalive.go:567-569). It never consults the campaign runtime orbestStrategy, both of which the request path resolves inresolvePolicyWindows(proxy/keepalive.go:440-462). An armed per-session override also forces the plan off (proxy/keepaliveoverride.go:189); that one is per session, so an account-wide figure cannot carry it, but the campaign and the one-hour strategy are per account and knowable at read time. - Fix: Resolve the effective policy in DashWaitPolicy the way
recorddoes — the tenant's best strategy and active campaign — and reportWaitPlan: falsewhen either owns the count. Or state the limit on the panel: "your account setting, before campaigns and strategies".
- What fails: A tenant with
Non-blocking
- The plan rung ignores the keeper's
Idle < 300 sgate (keepalive/keeper.go:903). The UI clamps X to 60–290 (dash/ui/app.js:9502-9508), so only a direct API call withx >= 300hits it. There the plan rung shows per-wait counts the live policy would not apply at that idle. - dash/ui/app.js:9525 and the ladder hint hardcode "up to 8", "base+1", "base plus 3". An account that tuned
ToolWaitMaxPingsand the extras — the replay itself uses its values — reads wrong bounds on the page. - Two ladder rows share
data-testid="ka-ladder-k2"when the plan rung's base equals a fixed rung's K (both havemax_pings: 2). No test uses that id today. - dash/keepalive.go:1357 still says "this package does not read tenant configuration". Half stale now: the reads receive the tenant's plan settings through the host hook. Only the 20k floor gate stays config-blind, and the panel copy names 20k.
- The spliced-tool-stop gap (a tool stop stored as "human" replays base+1; the keeper reads 8) is stated in the code and pinned by
TestDashboardReplayKnownGapSplicedToolStop. Good handling of a known limit.
What I checked
- The full diff (14 files) and the code around each change: the keeper's decision path (
record,pingable,Plans,WaitPings,WaitingFor), the replay (factsCTE,pingSpansWithFacts,expiries,SpanPings,storedWait,covers), the calculator and recommendation arithmetic, the proxy hook and its wiring, the UI rendering, and both golden files. - The new tests' math, re-derived by hand:
planFixture's spans (5 pings plan vs 2 fixed on session a; 3 on the open person wait; 0 on session b) and expiries (E1 only the plan reaches, E2 only fixed K=2, both at K=3) match what the code produces. The drift test stores what the proxy stores (the classifier's answer, before the keeper's tool-stop override), so it compares the right two things. - SQL: every predicate is parameterized;
toolStopListis built from fixed keeper constants. Ping rows stay excluded throughFilter.where. The facts window partitions by thread and orders byts, id; the old addressable CTE orders bytsalone, so rows with one timestamp can order differently — rare, and only inside one session. - Boundaries: no import cycle (
keepalivedoes not importdash); no request-path behavior change (thewaitplan.gorefactor movestoolStopWaitandtoolStopunchanged); fail-open is untouched, the change is read-only. - Data dependence: the replay reads stored metadata only (stop reason, wait cause, thread id, cache tier, provider).
docs/data-dependence.mdstates this. No new content reads. - Tests were not run: live tests are not approved for this review, and Go is not installed on this machine.
…868) Round 1 fixes for the plan replay. - keepalive: SpanPings returns the count and the spacing. The 1-hour tier uses ForOneHour (MaxPings1h at Idle1h). Idle of 300 s or more keeps the base count, as the keeper does. - dash: the tier is the BILLED tier (ResolveTier rule: asked 1 hour and cache_write_1h >= cache_write, sticky per thread). The calculator states the plan counts, and the plan rung gives way to fixed K at 300 s. - proxy: DashWaitPolicy reports the effective plan. An active campaign or a strategy in the 1-hour band turns it off, and so does a failed account lookup. - Tests: 1-hour, mixed-tier and failed-call cases in the drift test. Assisted-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Round 1: repliesNew head: R1-1 (1-hour tier): fixed in 010dce8
R1-2 (stored retry): rejected, with a test
R2-1 (effective policy): fixed in 010dce8
Non-blocking, all fixed
Mutations (both restored)
Tests, with |
amiddavid
left a comment
There was a problem hiding this comment.
| Reviewer | Kind | Started by | Context | Review | Round | Head | Verdict |
|---|---|---|---|---|---|---|---|
| Reviewer 1 (glm, user of @amiddavid) | GLM rits/zai-org/glm-5-3 via Claude Code 2.1.316 | offload-review skill, @amiddavid's session | resumed | 1 of 2 | 2 of 2 | 010dce8f19b7 |
blocking |
Verdict: blocking
What I checked
- Last review: round 1, sha
fd354b76811611169d242d538383a63bfb4585b8, posted 2026-11-03T00:01:26Z. - New commits since: 1 —
010dce8f"fix(dash): model the 1-hour tier, retry waits and the effective plan (#868)". Full diff read, plus the code around every change. - Conversation read in full: 2 reviews, 1 comment, 0 inline comments (from the three saved files).
- New items since my round 1: the author's reply comment (2026-11-03T00:33:35Z) — every claim in it checked against the code; the new commit; reviewer 2's round-1 review — its finding checked against the fix.
- No tests were run (live tests not approved); all claims below are from reading the code.
Earlier findings
- R1-1 (1-hour tier, base count): fixed.
SpanPingsnow returns the count and the spacing, and the 1-hour tier runsForOneHour()—MaxPings1hatIdle1h, the live schedule.- The tier is the billed tier. The SQL (
factsCTE, dash/keepalive.go:1321-1342) followsResolveTier's rule: asked 1 hour,cache_write_1h >= cache_write, sticky per thread. A read-only row inherits. Thewcodeencoding is sound:ROW_NUMBERstrictly increases per writing row, soMAX(wcode)picks the latest write; the parity bit carries the billed tier; a thread with no writes readshour = 0. - 1-hour reach is judged at a 1-hour lifetime. The "as the keeper does" claim is gone. Six new drift cases pin count and spacing.
- R1-2 (stored "retry"): rejected, and the rejection is right. I accept it.
Recordreturns before it tracks a non-2xx response (keeper.go:773-775). My round-1 trace missed that gate.- I also missed that any arriving request drops the live entry (
arriveAt, proxy.go:1411,ArriveAtdeletesk.live[id]). So the failed call's own arrival ends the previous entry's pings. Both sides give 0, and the new drift case "failed call after a tool call" pins it. The round-1 claim was wrong; the comment now matches the code.
- Reviewer 2's R2-1 (effective policy): fixed for the main cases, but the fix has a divergence — see finding 1.
- The plain account, the shadow campaign, the 1-hour strategy, the 5-minute strategy and the config-off case are all right, and
TestDashWaitPolicyReportsTheEffectivePlanpins them. The lookup-failure fallback is now off, as I asked in round 1.
- The plain account, the shadow campaign, the 1-hour strategy, the 5-minute strategy and the config-off case are all right, and
Findings
- proxy/keepalive.go:581-588 — DashWaitPolicy returns plan-off for any ACTIVE campaign, but the request path lets a manager strategy displace the campaign and keep the plan on
- What fails: an account with
keepalive_wait_planon, keep-alive on, an ACTIVE learned campaign, and a manager strategy with Idle under 300 s (say 240 s) that targets it. On the request path the strategy matches first (applyStrategyWindows, applied != ""), so the campaign branch is skipped — the code's own comment says "An ACTIVE campaign decides the ping count unless a manager strategy matched" (proxy/keepalive.go:456-458). The strategy sets Idle/MaxPings and leavesWaitPlanas the account set it (proxy/keepalivestrategy.go:146-158 never touches WaitPlan). Idle 240 s is under the 300 s gate, so the per-wait plan runs. The hook still returnsoffat :585-587, because its campaign branch tests onlyMode == campaignModeActive. The calculator then reportsplan: fixed_k, drops the plan rung, and the panel says the account setting switched the plan off — the opposite of what the proxy sends. - A second case, same branch: an ACTIVE campaign on an account whose models are unpriced.
predictiveDecideInnerreturnspdecOK = falsewithout a price wherepRead > 0 && pWrite > pRead(proxy/predictive.go:360-365), so the campaign never owns the count on the request path and the plan runs. The hook says off. - Proof: the trace above. The doc comment at :550-552 says the hook reports off "whenever the request path would turn the plan off for the account" — the campaign+strategy case contradicts it. The new test has the active campaign and the 5-minute strategy as separate cases, never both at once.
- Fix: check the strategy first. When a covering strategy with Idle under 300 s exists for the tenant, return the plan-on policy; return off for the campaign only when no strategy covers it. The
pdecOKconditions are per request, so a read hook cannot copy them exactly — either price-check the account's recent models, or state that limit in the doc comment and the panel copy.
- What fails: an account with
Non-blocking
factsCTE's "asked" half readscache_ttl, whichttlTiersets when any breakpoint asks 1 hour (apply/apply.go).ResolveTierrequires every breakpoint to ask (keeper.go:267-275, the comment names the mixed-ask head-over-tail case). The two diverge only for a mixed ask whose writes all billed 1 hour — rare, but the comment onfactsCTEsays "as keepalive.ResolveTier decides it", which is not exact.PlanCounts.MaxPingsreportskeepalive.MaxWaitPings(11), but the real cap ismax(base, 11)(waitplan.go:73). The UI does not render this field today, so nothing is wrong on the page — a trap for the next reader.- Both fixed by the same commit and verified: the 300 s fixed-K model,
plan_countswired through the UI (assigned at app.js:9546, read at 9527),ka-ladder-plantestid, the rewritten stale comment, thecalledAToolgap pinned in the known-gap test.
Data dependence and behavior
Unchanged from round 1: metadata only, no behavior change on the request path. The new hook is a read path. The PR body's statements hold for the new commit.
amiddavid
left a comment
There was a problem hiding this comment.
| Reviewer | Kind | Started by | Context | Review | Round | Head | Verdict |
|---|---|---|---|---|---|---|---|
| Reviewer 2 (glm, user of @amiddavid) | GLM rits/zai-org/glm-5-3 via Claude Code 2.1.316 | offload-review skill, @amiddavid's session | resumed | 2 of 2 | 2 of 2 | 010dce8f19b7 |
blocking |
Verdict: blocking
Findings
- proxy/keepalive.go:581 — DashWaitPolicy lets an active campaign switch the plan off even when a manager strategy owns the policy. The request path resolves the strategy first.
- What fails: an account with
keepalive_wait_planon, an ACTIVE learned campaign, and an active manager strategy with an Idle under 300 s (the ordinary 5-minute band). On the request path the strategy matches, so the campaign does not displace it:resolvePolicyWindowssetsappliedat proxy/keepalive.go:450 and skips thepol.WaitPlan = falseat :458-461. The keeper runs the per-wait plan.DashWaitPolicychecks the campaign first and returns the fixed-K model at :585-587, before it reads the strategy at :589-594. The calculator then shows fixed-K rows only and marks the account's K current. The hint says "every row is a fixed count", while the proxy picks the count per wait. - Proof: the two orders, cited above. The doc comment at :550-558 says "an active campaign owns the count (resolvePolicyWindows)" —
resolvePolicyWindowshands the campaign the count only when no strategy matched (:458 testsapplied == "").TestDashWaitPolicyReportsTheEffectivePlancovers each layer alone, never both together. The precedence is a supported state, not a freak: the comment at :456-457 names the strategy "the more explicit instruction". - Fix: mirror the request path. Resolve
bestStrategyfirst. When one matches, let its Idle decide (300 s or more → off, else the account's plan) and skip the campaign. When none matches, let an active campaign switch the plan off. Add the coexistence case to the test.
- What fails: an account with
Earlier findings, re-checked
Reviewer 1, finding 1 (the 1-hour tier ran the base count, at the asked tier): fixed.
SpanPingsreturns the count and the spacing, and modelsForOneHour(1 ping at 59 min by default).factsCTEcomputes the BILLED tier: the last write of the thread, sticky, with read-only turns inheriting. I checked the parity code againstResolveTiercase by case (a write billed 1 h, a write billed 5 m, a read-only turn that asks 1 h, one that asks 5 m, a thread with no write). All match.planReachesjudges a 1-hour-tier expiry against a 1-hour lifetime.- Six new drift cases compare the count and the spacing. The "as the keeper does" claim is gone.
Reviewer 1, finding 2 (a stored "retry" got 0 pings): rejected by the author. I checked the rejection, and it holds.
- The host's
Arriveretires the earlier entry when the failed request arrives (keeper.go:705-723). Recordreturns at keeper.go:773-775 for any status outside 2xx, before the tracking gates. The round-1 trace stopped at the no-entry gate (:865) and missed this one.- So the keeper holds nothing after a failed call and plans no ping. The replay's 0 matches. The new drift case pins it, and the comment now states the rule.
My finding 1 (DashWaitPolicy read only the account config): fixed in the main cases, with one residual — finding 1 above.
- An active campaign, a 1-hour strategy, a failed lookup and the account switch are all modeled now, each with a test.
Non-blocking from round 1, both reviewers: all fixed.
- The 300 s gate: modeled in
SpanPingsand inKeepAliveCalc, with a test. - The hardcoded copy:
plan_countsreaches the page; the golden gains the field and nothing else. - The duplicate
ka-ladder-k2testid: the plan rung iska-ladder-plan. - The stale comment about tenant configuration: rewritten.
- The incomplete name list: pinned as a second known gap, in the test and its comment.
- The OpenAI cadence still runs at the 5-minute policy, as before this PR. The fixed rungs carry the same simplification, so the comparison stays fair (reviewer 1's own note).
Non-blocking
- The stored
cache_ttlmeans ANY breakpoint asked 1 hour (apply.go:1029-1034).ResolveTierrequires EVERY breakpoint to ask it (apply.go:1082). A mixed ask whose writes all billed 1 hour readsOneHourin the replay, while the keeper runs the 5-minute plan for it. The comments at waitplan.go:137-139 and dash/keepalive.go:1316-1318 say "asked 1 hour on every breakpoint", which the stored column cannot tell. State the limit, or pin it as a known gap as the spliced-stop one is. - The plan boolean is one figure for the account. The live path resolves a strategy per wait cause: windows gate only a person's wait (proxy/keepalive.go:374-379), so a 1-hour strategy with a closed window leaves a person's wait on the account plan at night. The doc comment names the session-override limit; this one has the same shape and no name.
What I checked
- Last review: round 1,
fd354b76811611169d242d538383a63bfb4585b8, 2026-11-03T00:03:26Z. - New commits read: 1 (
010dce8ffix(dash): model the 1-hour tier, retry waits and the effective plan (#868)). - Read in full: 2 reviews, 1 comment, 0 inline comments.
- New items since my last review: the commit above (full diff, plus the code around each change) and the author's reply comment at 2026-11-03T00:33:35Z (each claim checked against the code).
Then:
- The new commit's seven files, and the code around each change:
waitplan.go(SpanPings, the retry comment),dash/keepalive.go(factsCTE,planPings,planReaches, the calculator and the recommendation,PlanCounts),proxy/keepalive.go(DashWaitPolicy), both test files,app.js, the golden. - The
factsCTEparity arithmetic by hand:wcoderises by 2 per row, soMAX(wcode)is the latest write's code; odd means that write billed 1 hour; a read-only row emits NULL, so it inherits. Checked againstResolveTierfor every case above. - The drift test's new model:
Arrivebetween records retires the earlier entry, as the live host does. The 1-hour cases compare the spacing too. - The rejection of reviewer 1's finding 2:
Arriveand the status gate, cited above. DashWaitPolicyagainstresolvePolicyWindows, order by order — the source of finding 1.- Nil-safety:
predRT()returns nil when the keeper is nil;forTenantis nil-safe; the strategy read sits behindh.keeper != nil. - SQL stays parameterized; ping rows stay excluded;
keepaliveimports nodashpackage. No request-path behavior changed: the commit touches the replay, the read hook and tests. - Tests were not run: live tests are not approved for this review, and Go is not installed on this machine.
…ampaign (#868) DashWaitPolicy checked the campaign first. The request path (resolvePolicyWindows) lets a matching manager strategy win, and a campaign applies only when no strategy did. - proxy: find the covering strategy first. Idle of 300 s or more turns the plan off. Less keeps the account setting. Only with no strategy does an active campaign turn the plan off. - proxy: the doc comment states three limits (campaign price gate, the per-session override, a closed 1-hour window). - keepalive, dash: the comments say that the stored cache_ttl means any breakpoint asked 1 hour, where ResolveTier needs every one. - dash: plan_counts.max_pings is max(base, MaxWaitPings). - Tests: campaign plus 5-minute strategy gives plan on. A third known gap (mixed breakpoint tier) is pinned. Assisted-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: DAVID AMID <DAVIDA@il.ibm.com>
Round 2: replyNew head: Blocking (strategy before campaign): fixed in ab1c0ab
Limits, now stated
Tests (box, |
amiddavid
left a comment
There was a problem hiding this comment.
| Reviewer | Kind | Started by | Context | Review | Round | Head | Verdict |
|---|---|---|---|---|---|---|---|
| Reviewer 2 (glm, user of @amiddavid) | GLM rits/zai-org/glm-5-3 via Claude Code 2.1.317 | offload-review skill, @amiddavid's session | resumed | 2 of 2 | 3 of 3 | ab1c0abc5ab6 |
approve |
Verdict: approve
Findings
None. Every finding from rounds 1 and 2 is fixed or settled, and the new commit adds no defect I can show.
Earlier findings, re-checked
My round-2 finding 1, and reviewer 1's round-2 finding 1 (the campaign checked before the strategy): fixed.
DashWaitPolicynow resolves the covering strategy first (proxy/keepalive.go:588-594). An Idle of 300 s or more turns the plan off; less keeps the account's setting and skips the campaign. Only with no covering strategy does an active campaign turn the plan off (:596-603). This mirrorsresolvePolicyWindows(:448-461).- The test gains both coexistence cases: an active campaign plus a 5-minute strategy gives plan on; plus a 1-hour strategy gives plan off. The author also reports a mutation check.
Reviewer 1's round-2 second case (an active campaign on an account with no prices): settled by a stated limit.
- The live path hands the campaign the count only on a request that passes its price gate (
pdecOK). The hook cannot see a single request, so it reports plan off for such an account. The doc comment now names this limit (:561-563). Reviewer 1's own fix text allowed "state that limit in the doc comment". I accept it.
My round-2 non-blocking notes: both addressed.
- The mixed-ask gap: both comments now say the stored
cache_ttlmeans ANY breakpoint asked 1 hour, whereResolveTierneeds EVERY one. A third known-gap test pins it. - The window-collapse limit: stated as the third doc bullet (:565-566).
Reviewer 1's round-2 non-blocking: both addressed.
PlanCounts.MaxPingsis nowmax(base, MaxWaitPings)(dash/keepalive.go:1624), which matchesWaitPings' own capmin(n, max(base, MaxWaitPings)). The golden stays at 11 for the default base, so nothing changed on the wire.
Non-blocking
- One more member of the documented window family, not named: an ACTIVE campaign plus a covering strategy with an Idle under 300 s whose window is CLOSED. On the live path the windows-ignored strategy apply is skipped when a campaign is active (
resolvePolicyWindows, :453), and the windows-respected apply fails on the closed window, so the campaign owns the count and the plan is off. The hook finds the strategy with windows ignored (:589) and reports the plan on. It diverges only while the window is closed. The doc bullet at :565-566 states the mirror image. One sentence there would close it, or a windows-respected lookup when a campaign is active.
What I checked
- Last review: round 2,
010dce8f19b7001b48a5843b78332147e8b4c2ee, 2026-11-03T00:45:42Z. - New commits read: 1 (
ab1c0abcfix(proxy): a covering strategy decides the dashboard plan before a campaign (#868)). - Read in full: 4 reviews, 2 comments, 0 inline comments.
- New items since my last review: reviewer 1's round-2 review at 00:45:16Z (posted seconds before mine, so I had not read it when I wrote round 2 — its finding checked against the fix), the author's round-2 reply at 08:32:19Z (each claim checked against the code), and commit
ab1c0abc(full diff, plus the code around each change).
Then:
- The commit's five files, and the code around each change:
proxy/keepalive.go(DashWaitPolicyand its doc comment),proxy/dashwaitpolicy_test.go,keepalive/waitplan.go(SpanFacts.OneHour),dash/keepalive.go(factsCTE,PlanCounts),dash/keepaliveplan_test.go. - The new precedence against
resolvePolicyWindows, case by case: no layers; a strategy alone (Idle under and over 300 s); a campaign alone; a campaign plus a 5-minute strategy; a campaign plus a 1-hour strategy; a closed window plus a campaign (the non-blocking note above). - The third known-gap test's math: the partial replace makes only the system breakpoint ask 1 hour, so the keeper reads 5 minutes (
AllBreakpointsAskExtendedTTLfails) while the replay reads 1 hour (the storedcache_ttl, a fully billed 1-hour write). The assertions pin the spacing on both sides. Counts.MaxPings = max(k, MaxWaitPings)againstWaitPings' cap, withkdefaulted before the count is built.- The doc comment's three limits against the code: the price gate (
predictiveDecideInnerreturns not-ok without prices), the per-session override, the closed 1-hour window. - No request-path behavior changed: the commit touches the read hook, comments and tests only. SQL, filters and boundaries are untouched by this commit.
- Tests were not run: live tests are not approved for this review, and Go is not installed on this machine. The author reports the package suites green on the box with
-race.
amiddavid
left a comment
There was a problem hiding this comment.
| Reviewer | Kind | Started by | Context | Review | Round | Head | Verdict |
|---|---|---|---|---|---|---|---|
| Reviewer 1 (glm, user of @amiddavid) | GLM rits/zai-org/glm-5-3 via Claude Code 2.1.317 | offload-review skill, @amiddavid's session | resumed | 1 of 2 | 3 of 3 | ab1c0abc5ab6 |
blocking |
Verdict: blocking
What I checked
- Last review: round 2, sha
010dce8f19b7001b48a5843b78332147e8b4c2ee, posted 2026-11-03T00:45:16Z. - New commits read: 1 —
ab1c0abc"fix(proxy): a covering strategy decides the dashboard plan before a campaign (#868)". Full diff read, plus the code around every change. - Read in full: 4 reviews, 2 comments, 0 inline comments (from the three files).
- New items since my round 2: the new commit (every hunk checked against the request path); the author's round-2 reply comment (2026-11-03T08:32:19Z — every claim checked against the code); reviewer 2's round-2 review (2026-11-03T00:45:42Z — same finding as my round 2, its proof checked).
- No tests were run (live tests not approved); all claims below are from reading the code.
Earlier findings
- My round-2 finding (strategy must decide before the campaign): fixed for the unwindowed case.
DashWaitPolicynow followsresolvePolicyWindowsin the same order: the covering strategy first, its Idle decides, the campaign only when no strategy covers (proxy/keepalive.go:588-603).- Two new coexistence cases pin it: campaign + 5-minute strategy gives plan on; campaign + 1-hour strategy gives plan off. Both hold under the new order.
- The
pdecOKprice-gate limit is now stated in the doc comment, as I proposed. - One residual divergence remains, through the strategy time windows — finding 1 below.
- R1-1 (1-hour tier): fixed in round 2, stays fixed. No change to that code in this commit.
- R1-2 (stored "retry"): rejection accepted in round 2. Unchanged.
- Reviewer 2's findings: agree, both closed. R2-1 and their round-2 finding are the same as mine; the fix handles them. Their two round-2 non-blocking notes are addressed: the ANY-vs-EVERY
cache_ttldivergence now has accurate comments and a pinned test; the closed-window limit is now named as the doc comment's third limit.
Findings
- proxy/keepalive.go:589-593 — the hook ignores strategy time windows; with an active campaign and a closed window, the request path turns the plan off but the hook reports it on
- What fails: an account with the plan on, keep-alive on, an ACTIVE learned campaign, and a manager strategy with Idle under 300 s that carries time windows (the ordinary 60–290 s kind). During the strategy's closed hours, for every wait — person or not — the request path turns the plan off: the windowed match finds nothing (
MatchesrequiresInWindow, tenant/keepalivestrategy.go:297-302), the window-ignoring retry is skipped because the campaign is active (proxy/keepalive.go:453, its own comment: "with a campaign and a closed window, the campaign wins"), and the campaign then owns the count and setsWaitPlan = false(:458-461). The hook finds the strategy with windows ignored (:589,bestStrategy(..., true)), sees Idle under 300 s, and returns the plan on (:593). The panel marks the plan rung current and shows per-wait counts, while the proxy sends the campaign's fixed count for every wait during the closed hours. The hook's answer does not change with the hour, so the page shows the same wrong flag all day. - Proof: the trace above. The doc comment's third limit (:565-566) states the mirror direction only — a closed 1-hour window makes the hook say off where a person's wait stays on. The opposite direction (closed window + campaign → path off, hook on) is unstated, and it is the misleading direction: the panel promises a plan that does not run. Without a campaign the hook is right in every case (the retry then runs for non-person waits), so the campaign is a required part of the case — the same coexistence the commit exists to model.
- Fix: when a campaign is active for the tenant, match the strategy with windows respected (
bestStrategy(..., false)), as the request path does — a closed window then falls through to the campaign's off, and the flag is right at the moment the page is read. Or state this direction beside the third limit in the doc comment.
- What fails: an account with the plan on, keep-alive on, an ACTIVE learned campaign, and a manager strategy with Idle under 300 s that carries time windows (the ordinary 60–290 s kind). During the strategy's closed hours, for every wait — person or not — the request path turns the plan off: the windowed match finds nothing (
Non-blocking
- The mixed-tier known gap is handled well: the comments at
waitplan.go(SpanFacts.OneHour) anddash/keepalive.go(factsCTE) now state the ANY-vs-EVERY rule exactly, andTestDashboardReplayKnownGapMixedBreakpointTierpins it. I checked the test's construction:partialreplaces only the first of the twocache_controlbreakpoints inplanBody, so the keeper sees a mixed ask (5-minute plan) while the storedcache_ttland a fully 1-hour-billed write make the replay read 1-hour. Sound. PlanCounts.MaxPingsis nowmax(base, MaxWaitPings), which matches the real cap (waitplan.go:73). The golden stays at 11 for base 2, consistent.- The two new test cases in
TestDashWaitPolicyReportsTheEffectivePlanmatch the new ordering; the single-layer cases still hold.
Data dependence and behavior
Unchanged from round 2: metadata only; the commit touches the read hook, comments and tests. No request-path behavior change.
Behavior change
This changes what the dashboard shows. It does not change what the proxy does.
keepalive_wait_plan: falseon the account. The panel then models fixed K, as in feat(keepalive): choose the ping count from what the thread waits for #867, with the row of the account's K marked current.Summary
What changed
keepalive/waitplan.goPolicy.SpanPings(SpanFacts). It is the keeper's decision for a stored row: wait cause, no-tool rule,done,retry, OpenAI, 1-hour tier. It callsWaitPings.toolStopWait) used by both the keeper and the replay.dashalready importskeepalive's neighbours, andkeepalivedoes not importdash. No new package was needed, anddashstill does not importproxy.dash/keepalive.gopingSpansand a newexpiriesread carry the stored facts of the request that opens each gap:stop_reason,waiting_for,thread_id,provider,cache_ttl, and whether the thread called a tool before.MAX(...) OVERby tenant, session, thread).wait_planrung (current) and the fourfixed_krungs. Each rung has aplanfield. The response keeps a top-levelplanfor the account.plan, andfixed_k(a range) when the plan is on.recMaxPingsis still the base, and still means "plan off" for the fixed-K model.dash/keepaliveapi.go,dash/api.go,proxy/keepalive.go,cmd/context-guru-proxy/main.goSetWaitPolicy. The proxy answers with the account'skeepalive_wait_planand its three counts. Without a hook, the shipped plan is on.dash/ui/app.js: row names ("Live plan, base 2 (current)", "Fixed 2"), new copy, and the fixed-K range under the recommendation.docs/data-dependence.md,docs/how-to/dashboard-guide.md: updated.Golden diffs
Only two files changed. Both are intended.
ka_calc.jsonplanis nowwait_plan.current: true,plan: wait_plan). On this fixture it has the same counts as fixed K=2 (112 expiries, 433 pings): the fixture rows carry no wait cause."plan": "fixed_k".ka_recommend.json: newplan: wait_planandfixed_k(the same range here, for the same reason).I also regenerated the other goldens once.
tools*.jsonandtoolfilter.jsonchanged only in float noise in the last digits, from the box. I did not commit those.How it was tested
All on the shared box, with
-race:go vet ./...: clean.gofmt -l: clean../keepalive/...: ok../dash/...: ok (562 s)../proxy/... ./cmd/...: ok.New tests (
dash/keepaliveplan_test.go):TestDashboardReplayMatchesTheKeeper: 14 cases at base 2 and base 10. It records the real responses in a realKeeper, stores the rows the proxy would write, and compares ping counts.TestDashboardReplayKnownGapSplicedToolStop: pins the one known difference (see below).TestPlanReplayStaysNearTheFixedKCost: relative cost bound.Cost. On 20,000 rows, without
-race: fixed-K reads 177 ms, plan reads 582 ms (about 3.3x). The routes are cached (cachedRoute). The existingTestOverviewStaysFastOnALargeWindowpasses.Mutation checks (both restored):
Data dependence
Metadata only. It reads columns the proxy already writes:
stop_reason,waiting_for,thread_id,provider,cache_ttl, and request times. No text. Storage level:docs/data-dependence.mdhas the dashboard row updated. The data dependence does not change in either direction.Open questions
human. The keeper plans 8 pings for the first, the replay plans base+1. This needs a spliced sniff window and is rare. Fix, if wanted: store the case in a new column. That is a schema change, so I did not do it here.waiting_forand nostop_reasonuse the base count (nothing to replay from).Closes #868
🤖 Generated with Claude Code