Originally created by: harshitagrawal2O
Rebased onto current main and narrowed to the two net-new files — the CI/CD-config glob work is already carried forward in [#203], so paths.py / README.md / CHANGELOG.md are dropped here.
Adds detect_loop_anomaly() over the append-only cost_events ledger. Over a rolling window (optionally scoped to one entity_id) it flags a runaway/looping burn:
call_burst — number of tool_call events exceeds max_callstoken_burst — summed token units (tokens_in + tokens_out) exceed max_unitsIt returns an advisory LoopAnomaly (counts + a coarse signal label + a redaction-safe explanation). Infra-first slice: detect_loop_anomaly / LoopAnomaly are not wired into any caller yet — the CostObserver seam that gives it a home is [#143].
TypeError in the "never raises" path is fixed. A tz-naive now (or a legacy naive-timestamp row) compared against the ledger's aware timestamps used to raise outside the failure boundary. Now: a naive now returns calm up front, now is optional (defaults to datetime.now(timezone.utc)) so omitting it can't raise, and naive rows are skipped in-loop.CostEvent rows, so call_burst now counts only tool_call rows and token_burst sums only token-kind units (previously it counted all rows / summed mixed units).id PK (part of idx_cost_events) instead of the unindexed TEXT ts, which also removes the mixed-UTC-offset string-ordering hazard.# noqa: S608 justified), the _ANOMALY_ROW_CAP DoS bound, the broken-DB fail-safe test, the not-a-verdict structural test, and the redaction test.entity_id burst-scoping ceiling in a comment so it's on record before anything depends on it.tests/unit/test_cost_loop_anomaly.py — 18 tests: empty/calm, call_burst, token_burst, precedence, window/future boundaries, exact calls == max_calls boundary, per-entity scoping, kind-scoping of each signal, now default, naive now, legacy naive DB row, broken-DB fail-safe, not-a-verdict, and explanation redaction.now, unparseable/naive row, DB failure) returns a calm readout; the detector never raises into its callerLoopAnomaly holds counts + a coarse label only; a test asserts the raw entity_id never appears in the explanationtest_loop_anomaly_is_not_a_verdict asserts it structurallyCostObserver seam and is deferred to [#143].entity_id, so a caller able to rotate the entity id resets the counter — harmless while advisory with no production caller; the [#143] wiring must bound it.id-ordered row cap suits the live "is this entity looping now" read; a far-historical now on a ledger with more than the cap of newer rows can under-count — an accepted trade-off for an advisory readout.
Tickets: #143
Tickets: #152
Tickets: #164
Tickets: #203
Tickets: #321
Originally posted by: fu351
Hi @harshitagrawal2O — thanks again for the CI/CD-config protection half of this. The GitLab/Jenkins/CircleCI/Azure globs are solid and well tested, and all four CI checks are green.
I want to make sure this doesn't stall, so here's where things stand and two clear ways forward — both genuinely fine with me.
Context first, so you're not doing double work: I'm going to land the other half of [#92] (the role-boundary matcher) myself in a separate, small PR.
src/doberman/roles/builtin_roles.yamlstill only has.github/workflows/**in its five role-boundary entries, with no GitLab/Jenkins/CircleCI/Azure equivalents, so that side isn't covered yet. Your glob work isn't being thrown away — I'll carry it forward withCo-Authored-By: harshitagrawal2Ocredit on that PR.That leaves this PR free to become a focused CB.3 slice. As written,
detect_loop_anomalyis advisory cost-row aggregation with no wiring into the decision path — the plan (Slice CB.3) calls for deterministic HMAC action/turn fingerprint counting in a sliding window, aloop_anomalytag below the sustained threshold, an AUTH contribution above it, mode-scaled thresholds, andcombine/reason-code wiring. There's also a real bug in the module's "never raises" guarantee: thets < cutoff/ts > nowcomparisons sit outside thetry/exceptthat guardsdatetime.fromisoformat(), so a naive (no-tzinfo) timestamp compared against an aware cutoff raises an uncaughtTypeError.So the choice is yours:
(a) Rescope this PR to CB.3 only — drop the CI/CD glob commits (they'll already be covered elsewhere), rebase onto current
main, and build the loop-anomaly detector to the plan's spec, including theTypeErrorfix and a regression test for it. Or(b) Close this one and pick up something fresh and self-contained instead — [#198] (engine: escalate bulk-delete of gitignored, uncommitted directories, level-7) or [#199] (engine: decide + fix force-push risk classification beyond the default protected-branch list, level-8) are both good next steps.
Either is genuinely welcome — just let me know which way you'd like to go.
Related
Tickets: #198
Tickets: #199
Tickets:
#92Originally posted by: fu351
Separate from the review above — here's where your work fits in the roadmap, because I don't want the rebase note to read as "start over."
Your cost detector is the beginning of a roadmap item we actually want: #143 — Cost-observability seam:
CostObserverplugin + loop-anomaly detector (level-9,help wanted,roadmap).That issue asks for exactly the two halves you've been building toward:
CostObserverplugin seam — an interface plus entry-point registration, so cost observers can be registered without core importing them by name, andsrc/doberman/storage/cost.pyalready is.So the path I'd suggest: rebase this PR down to just
cost.py+ its tests as described in the review, land it as the detector half, then take [#143] for the seam that makes it pluggable and wires it to a real caller. That also resolves the "ships as inert library code" note — right now nothing callsdetect_loop_anomaly, and [#143] is the issue that gives it a home.Worth flagging one design constraint before you start on the seam, since it's the thing most likely to cause rework: cost observation must stay strictly off the decision path. Logging and telemetry may never alter, gate, or crash a decision. Your current code already respects this —
test_loop_anomaly_is_not_a_verdictasserts it structurally, which was good to see — and the seam needs to preserve that property, not just the detector.If [#143] is more than you want to take on right now, two smaller ones in the same area:
level-7) — detector work against the same interface, much narrower scope.level-7) — closest to the path-rule work you did in [#164], and that work is already merged and credited to you, so you know this code.Comment on whichever you'd like and I'll make sure the context is there before you start. Thanks for sticking with this one.
Related
Tickets: #143
Tickets:
#164Originally posted by: fu351
Checking in, with no pressure attached. Your last push here was 25 July and my review notes went up on the 30th, so I mostly want to be sure the ball isn't sitting in my court by mistake.
Nothing here has gone stale or lost its place. The cost-detector half is still wanted, and [#143] is still open and still yours to take once this lands.
If the rebase-and-narrow I asked for is more of a slog than it looked, say so and I'll get specific about exactly what to keep and what to drop. That request took me one paragraph to type and is genuinely fiddly to carry out, which is a bad trade I'd rather fix than let sit. If you've moved on or things simply got busy, that's completely fine too — say the word and I'll close it with no hard feelings, and your
#152work stays credited either way.If a faster back-and-forth would help, we have a Discord: https://discord.gg/Sfy5XGNqty. I'm happy to walk through the rebase there or right here on this thread, whichever suits you.
Related
Tickets: #143
Originally posted by: fu351
Checking in — no pressure. This branch has conflicts with main now and the earlier review feedback is still open. If you're still on it, happy to answer questions here or on Discord. If you've moved on, no problem at all — say the word and I'll return it to the pool, no hard feelings.
Originally posted by: harshitagrawal2O
Rebased onto current
mainand narrowed this to the two net-new files (src/doberman/storage/cost.py+tests/unit/test_cost_loop_anomaly.py) — the CI/CD glob hunks are dropped since [#203] already carries that work. Thanks for landing it and the credit.Addressed the review:
TypeErrorin the "never raises" path — fixed. A tz-naivenowreturns calm up front,nowis now optional (defaults todatetime.now(timezone.utc)) so omitting it can't raise, and a legacy naive-timestamp row is skipped in-loop rather than crashing the comparison. Regression tests added for a naivenow, a legacy naive DB row, and thenowdefault.test_exact_threshold_is_not_an_anomalycoveringcalls == max_calls(not an anomaly) andcalls == max_calls + 1(is).detect_loop_anomalyyet; [#143] is where the seam + a real caller live.entity_idscoping ceiling — named in a comment.While I was in there I also tightened two things from a self-review pass:
CostEventrows, so counting every row mis-labeled "runaway tool-call loop" and mixed call-counts into token burn.call_burstnow counts onlytool_callrows;token_burstsums only token-kind units. Added kind-scoping tests.idPK (inidx_cost_events) instead of the unindexed TEXTts, so it's a real indexed read and immune to mixed UTC offsets in stored timestamps.Kept everything you liked (parameterized SQL,
_ANOMALY_ROW_CAPbound, the broken-DB fail-safe test, the not-a-verdict structural test, the redaction test). Green locally: ruff, ruff format, lint-imports (both contracts kept), and the full pytest suite. Ready for re-review — and happy to take [#143] next for theCostObserverseam that wires this to a caller.Related
Tickets: #143
Tickets:
#203Ticket changed by: fu351
Originally posted by: fu351
Two in one day — nice. The datetime fix is done properly rather than patched over: a naive
nowreturns calm before any comparison, and naive ledger rows are skipped, so by the timets < cutoffruns both sides are guaranteed tz-aware and theTypeErrorcan't recur. I also like the two calibration fixes you added on your own initiative — kind-scoping the burst counts, and switching the bounded read to the indexed monotonicidinstead of the unindexedts. The detector stays fully off the decision path (nothing wires it in yet), which matches the de-scope: land the detector here, the engine wiring goes to [#143]. Andtest_exact_threshold_is_not_an_anomalypins thecalls == max_callsoff-by-one exactly. Merging.You've now landed two detectors. [#321] (PII / financial-data-class exfil) is still the natural next one from the earlier suggestion, and [#143] is where this loop-anomaly detector eventually gets wired into the engine, if you want to follow it all the way through. Happy to assign either.
Thanks again, harshitagrawal2O.
Related
Tickets: #143
Tickets:
#321