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§
- Loop
Guard Trip Day - 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.
- Loop
Guard Trip Event - 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_LIMITcharacters. - epoch_
day - The UTC day
secsfalls on, counted from the Unix epoch. - first_
day_ of_ window - The first day of a window of
daysdays that ends with the daynow_secsfalls on,daysclamped to 1..=LOOP_GUARD_LOG_RETENTION_DAYS. - short_
hash 🔒 - The first 16 hex digits of
text’s SHA-256.