Skip to main content

Module loop_guard_log

Module loop_guard_log 

Source
Expand description

The loop guard’s log, which outlives the process.

One row for every decision the guard took, and one row per day, model, gglib version and mode counting the requests it scanned. Unlike every counter in super::defects, it survives a restart.

It exists for one reader. ADR 0011’s kill criterion asks whether the guard’s trips reach zero across a denominator large enough that one would have been expected, and a denominator that large is more traffic than one daemon run is likely to see. The per-process ledger cannot answer that, and says why it should not try: a defect rate is a claim about recent traffic on this build of everything. This is not a rate kept for a loop to react to. Nothing reads it but a person, through gglib proxy trips, the daemon’s GET /api/proxy/loop-guard-trips and the panel under the guard’s own setting; every row is dated, so the reader chooses the window; and every row names the gglib version and the guard’s mode, so a reading can be kept to one gglib release and one behaviour instead of averaged across them. note re-notes every later turn of a stuck conversation, while refuse refuses every one — which, for a client with no recovery path, ends the session — so their trip counts are different populations.

That meets the objection only in part. The version is the workspace’s, so development builds between two releases share it. Nothing here records the llama.cpp build or the model file, and the removed defect_windows scoped evidence by the first of those. A reading that spans a llama.cpp upgrade or a re-downloaded model has to be split by date, by hand.

§What a trip row is, and is not

A row records the guard’s decision — noted or refused — not what reached the model. Under note the request goes on after the decision, and the note can still fail to arrive: a chat template with no branch for the tool role drops the last message and the note inside it; a conversation that also exceeds the context budget is refused context_length_exceeded; an embedding model, an unknown model or a failed admission refuses the request before it is sent; and an upstream that dies mid-request can fail the retry. Each of those is still a row here. The dashboard’s loop_guard_trips is bumped only by a forwarded or refused request’s snapshot, so under note this log can count more than the dashboard does for the same run.

The log can also count less than happened. A decision the writer cannot queue — a full queue, or a writer already stopped — is dropped while its scan is still counted, so that day reads as fewer trips over the same denominator. A flush the database refuses loses its trips and scans together, and a forced exit loses whatever the writer had not yet flushed. How many were lost reaches only the daemon’s log, as a warning; no reading shows it. A zero read from here rules out a trip only as far as those warnings are absent.

Only the proxy’s pre-dispatch scan writes here. The agent loop runs the same two detectors and records nothing (#1091).

§What is stored

No conversation text: no message, tool name, argument or tool result. The tool-call signature (name:hash|…) and the session id are each kept only as the first 16 hex digits of their SHA-256: stable keys, so a query on the table can tell “the same loop, seventeen times” from seventeen loops; the per-day summary every reader shows carries only the number of distinct sessions. They are correlation keys, not a privacy boundary — anyone holding the data directory can hash a guess and compare. The one client-chosen string kept as given is the model name, bounded to MODEL_NAME_LIMIT characters.

Structs§

LoopGuardTripDay
One day of the log for one model, gglib version and mode: how many requests the guard scanned, and how many it acted on — ordinarily some of those, though a trip whose scan was lost has none.
LoopGuardTripEvent
One decision the loop guard took about one request.

Constants§

GGLIB_VERSION
The version every row is stamped with: the workspace’s, which is gglib’s.
LOOP_GUARD_LOG_DEFAULT_DAYS
The window a reader gets when it names none.
LOOP_GUARD_LOG_RETENTION_DAYS
How many days the log keeps, and so the widest window a reader can ask for.
MODEL_NAME_LIMIT
The longest model name a row keeps. The client chooses the name, and the proxy sets no request-size limit of its own.
SECS_PER_DAY 🔒

Functions§

bounded_model_name
The model name a row keeps: the client’s, cut at MODEL_NAME_LIMIT characters.
epoch_day
The UTC day secs falls on, counted from the Unix epoch.
first_day_of_window
The first day of a window of days days that ends with the day now_secs falls on, days clamped to 1..=LOOP_GUARD_LOG_RETENTION_DAYS.
short_hash 🔒
The first 16 hex digits of text’s SHA-256.