Skip to content

fix: group a NULL analytics group_key as unattributed instead of failing the query (#1347) - #1351

Merged
sakibsadmanshajib merged 8 commits into
mainfrom
fix/analytics-null-group-key
Aug 29, 2026
Merged

sakibsadmanshajib merged 8 commits into
mainfrom
fix/analytics-null-group-key

Conversation

@sakibsadmanshajib

@sakibsadmanshajib sakibsadmanshajib commented Aug 29, 2026 •

Copy link
Copy Markdown
Owner

Closes #1347.

What was broken

GET /api/v1/accounts/current/analytics/errors?group_by=api_key&window=24h answered 500 on the console overview page, the first page a user sees after signing in. The card degraded honestly rather than showing a false zero, which is the right failure direction, but the rows it could not scan are exactly the ones an operator most wants to see.

err="usage: scan error summary row: cannot scan into dest[0] (col: group_key):
     cannot scan NULL into *string"

usage_events.api_key_id is nullable and is the only nullable grouping column: endpoint and model_alias are both NOT NULL (supabase/migrations/20260330_02_usage_accounting.sql), which is why group_by=model and group_by=endpoint answered 200 on the same account. One NULL in the grouped column failed the scan for the whole result set, so a single unattributable row took the entire summary down.

Where the NULLs actually come from

Three sources, none of them a data defect:

  1. Ordinary chat traffic, which is the dominant one on the demo box today. A request billed to an account through a signed-in session carries no API key at all. The qa-tester workspace has 14 such rows in the last 24 hours, all of them successful chat_completions calls, and that is what was breaking its overview card. This is broader than the issue framing of a pre-auth error.
  2. An error recorded before a key was resolved, as the issue describes.
  3. A key deleted after the fact, since the foreign key is ON DELETE SET NULL.

The sibling call, checked

The issue notes analytics/usage?group_by=model answers 200 and infers the errors summary is the only affected path. That inference holds for the grouping column, not for the endpoint. GetUsageSummary and GetSpendSummary carry the identical hole on the same nullable column, and the new test shows all three failing with the same error before this change:

GetErrorSummary must tolerate a NULL group key: usage: scan error summary row: ... cannot scan NULL into *string
GetUsageSummary must tolerate a NULL group key: usage: scan usage summary row: ... cannot scan NULL into *string
GetSpendSummary must tolerate a NULL group key: usage: scan spend summary row: ... cannot scan NULL into *string

All three are fixed here rather than only the one the issue names, because all three route through the same shape and the other two would have surfaced the same 500 the first time anyone grouped them by API key. Checked the rest of the package too: ListEvents and ListAttempts already scan api_key_id into *uuid.UUID, so no other unsafe scan remains.

The fix

The scan destination is nullable, and a NULL renders as an explicit unattributed bucket rather than being dropped. Dropping those rows would trade a loud 500 for a quiet undercount, which is the worse failure. The sentinel cannot collide with a real group key, which renders as a UUID.

The console half, which the live capture forced

The plan was to leave the front end alone: the overview card sums error_count without reading the key, and the analytics table renders group_key verbatim, so the bucket reads correctly there with no change.

Capturing the proof disproved that for one widget. Top API keys by spend labels any group key missing from the account key list as "Deleted key", so with the summary now returning the bucket, that row rendered as Deleted key / unattrib on the qa-tester workspace. Nothing was deleted; that spend is chat traffic that never carried a key. It now reads Unattributed / no key on record, wording that is true under all three causes of a NULL, since nothing in the row says which one applies.

The contract constant lives in a new dependency-free apps/web-console/lib/control-plane/contract.ts rather than in client.ts, which imports next/headers and the server Supabase client: cache-metrics.ts is a pure module and a value import from client.ts would drag that graph into it. TestUnattributedGroupKeyMatchesTheConsole reads the console file and pins the Go and TypeScript literals to each other, so a rename on either side fails loudly instead of silently restoring the deleted-key label.

Tests that can actually fail

TestSummariesByAPIKey_NullKeyGroupsAsUnattributed seeds one attributed row and one genuinely NULL row, then asserts the fixture itself contains exactly one NULL api_key_id before asserting anything else, so a fixture that quietly stopped producing a NULL fails loudly rather than passing. It also asserts the attributed row keeps its own UUID and its own token, credit and count values, so a fix that collapsed every row into the bucket, or corrupted the attributed one, would not pass either.

Four mutations were run, each against the real substrate (live Postgres with the full migration chain for the Go ones, a rebuilt image for the console one):

  1. Scan destination reverted to the non-nullable type: RED on all three subtests, with the production error string verbatim.
  2. Sentinel replaced with an empty string: RED on all three, proving the assertions pin the bucket and not merely the absence of an error.
  3. Console constant renamed to unattributed_traffic: RED on the cross-language guard.
  4. Console label branch removed: RED on the vitest case, expected Deleted key to be Unattributed.

The vitest case keys its fixture by the wire literal rather than by the constant the code compares against, so it cannot pass by asserting that a value equals itself.

Verification

  • go test -tags integration ./internal/usage/... -count=1 against live Postgres: pass, whole package.
  • go test ./apps/control-plane/... -count=1 -short: pass.
  • npm run test:unit (web console): 902 pass. The one failing suite, ci-web-e2e-secret-free, fails identically on main inside the Docker image, which carries no .github/ directory; it passes in CI.
  • npm run build (web console): pass.
  • node tools/lint-no-token-in-proof-captures.mjs: pass.
  • ./internal/usage/... is already in the CI live-Postgres package list, so the live test runs on every PR rather than skipping silently.

Live before and after against real demo data, same account and same bearer, one control-plane deployed and unfixed and one built from this branch against a restore of the same database: 500 and 200 respectively, the 200 carrying four attributed key buckets plus {"group_key":"unattributed","error_count":0,"total_requests":14}. Full procedure in docs/proof/analytics-null-group-key-1347/capture-log.txt; screenshots posted below.

Buglog entry

{"id":"bug-2026-08-29-analytics-null-group-key","date":"2026-08-29","title":"Analytics summaries returned 500 when a grouped column was NULL","error_message":"usage: scan error summary row: cannot scan into dest[0] (col: group_key): cannot scan NULL into *string","root_cause":"GetErrorSummary, GetUsageSummary and GetSpendSummary scanned group_key into a non-nullable string. usage_events.api_key_id is nullable (NULL for chat traffic with no API key, for errors recorded before a key is resolved, and for keys deleted under ON DELETE SET NULL), so one unattributable row failed the scan for the entire result set and the console overview card rendered its error state on the first page after sign-in.","fix":"Scan group_key into a nullable destination in all three summaries and map NULL to an explicit unattributed bucket rather than dropping the row. The console Top API keys widget no longer labels that bucket Deleted key. Pinned by TestSummariesByAPIKey_NullKeyGroupsAsUnattributed, TestUnattributedGroupKeyMatchesTheConsole and a console unit case, each proven red by its own mutation.","tags":["control-plane","usage","analytics","postgres","null-scan","console-overview","web-console"],"files":["apps/control-plane/internal/usage/repository.go","apps/control-plane/internal/usage/repository_live_test.go","apps/control-plane/internal/usage/wire_contract_test.go","apps/web-console/lib/analytics/cache-metrics.ts","apps/web-console/lib/control-plane/contract.ts"]}

…ing the query (#1347)

GET /api/v1/accounts/current/analytics/errors?group_by=api_key answered 500
on the console overview page, the first page a user sees after signing in.
The card degraded honestly rather than showing a false zero, but the rows it
could not scan are exactly the ones an operator most wants to see.

usage_events.api_key_id is nullable and is the only nullable grouping column;
endpoint and model_alias are both NOT NULL, which is why group_by=model and
group_by=endpoint were unaffected. A single NULL in the grouped column failed
the scan for the whole result set, so one unattributable row took the entire
summary down.

Three live sources produce that NULL, and none of them is a data defect. On
the demo box today the dominant one is ordinary chat traffic: a request billed
to an account through a signed-in session carries no API key at all, and the
qa-tester workspace has 14 such rows in the last 24 hours. The second is an
error recorded before a key was resolved. The third is a key deleted after the
fact, since the foreign key is ON DELETE SET NULL.

The scan destination is now nullable and a NULL renders as an explicit
unattributed bucket rather than being dropped, because a silent undercount is
a worse failure than a visible bucket. The sentinel cannot collide with a real
group key, which renders as a UUID.

GetUsageSummary and GetSpendSummary carried the identical hole on the same
grouping column, and both are fixed here. The new test proves all three failed
before and pass after; its fixture contains a genuine NULL group_key and
asserts the attributed row is still keyed by its own UUID, so a fix that
collapsed every row into the bucket would not pass either.
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Aug 29, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

Next included review available in 23 minutes.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 17225489-0bd9-4d02-831c-a032e3a84ee3

📥 Commits

Reviewing files that changed from the base of the PR and between 0c418db and 221ed5b.

📒 Files selected for processing (6)
  • apps/control-plane/internal/usage/wire_contract_test.go
  • apps/web-console/__tests__/console-cache-visibility.test.tsx
  • apps/web-console/lib/analytics/cache-metrics.ts
  • apps/web-console/lib/analytics/overview-fetch.ts
  • apps/web-console/lib/control-plane/contract.ts
  • docs/proof/analytics-null-group-key-1347/capture-log.txt

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7232854e-6446-4cb3-9bde-48e17763f418

📥 Commits

Reviewing files that changed from the base of the PR and between 37d49fc and 0c418db.

📒 Files selected for processing (5)
  • apps/control-plane/internal/usage/repository.go
  • apps/control-plane/internal/usage/repository_live_test.go
  • apps/web-console/__tests__/console-cache-visibility.test.tsx
  • apps/web-console/lib/analytics/cache-metrics.ts
  • apps/web-console/lib/control-plane/client.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

Analytics summaries now map NULL API-key grouping values to unattributed. The web console uses the same key to display these rows as “Unattributed” instead of “Deleted key”.

Changes

Analytics unattributed grouping

Layer / File(s) Summary
Nullable summary grouping
apps/control-plane/internal/usage/repository.go
The usage, spend, and error summaries scan nullable grouping values and map NULL values to UnattributedGroupKey.
Live summary validation
apps/control-plane/internal/usage/repository_live_test.go
Live PostgreSQL tests seed attributed and NULL-key events and verify all three summaries return the expected groups and metrics.
Console unattributed rendering
apps/web-console/lib/control-plane/client.ts, apps/web-console/lib/analytics/cache-metrics.ts, apps/web-console/__tests__/console-cache-visibility.test.tsx
The console shares the unattributed key and labels matching rows “Unattributed” with the suffix “no key”.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 0c418

Analytics summaries now show API-key-less activity as “unattributed” instead of failing the entire result, preserving visibility for affected accounts. No actionable merge-blocking risk remains after normal checks and review.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 7 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #1347. All three analytics summaries handle NULL group_key values, preserve them under the explicit unattributed bucket, and include a live-Postgres regression test with an a…
Out of Scope Changes check ✅ Passed The changes are within scope. The additional usage and spend summary fixes address the same nullable grouping defect, and the client labeling update supports the new unattributed bucket required by th…
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: mapping NULL analytics group keys to an unattributed bucket instead of failing the query.
Full details: Linked Issues check

Explanation

The changes satisfy issue #1347. All three analytics summaries handle NULL group_key values, preserve them under the explicit unattributed bucket, and include a live-Postgres regression test with an actual NULL value. Console labeling also distinguishes unattributed rows from deleted keys.

Full details: Out of Scope Changes check

Explanation

The changes are within scope. The additional usage and spend summary fixes address the same nullable grouping defect, and the client labeling update supports the new unattributed bucket required by the fix.

✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/analytics-null-group-key

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Review follow-up on two counts.

Antigravity flagged that the live test asserted every value on the
unattributed bucket but only the existence of the attributed one, so a
regression that corrupted the attributed row's tokens or credits would have
passed. Both summaries now assert its values. It also flagged that the
fixture guard's comment overstated what the guard buys: the bucket lookups
below would already fail if the row stopped being NULL. The guard stays,
because it fails pointing at the fixture rather than at the fix, but the
comment now says that rather than claiming the assertions are blind.

The console change comes from the live capture, which contradicted the
reasoning for leaving the front end alone. The Top API keys by spend widget
labels any group key missing from the account's key list as "Deleted key",
and with the summary now returning the unattributed bucket that row rendered
as "Deleted key" with the suffix "unattrib" on the qa-tester workspace. The
spend in that bucket is console and chat traffic that carried no API key at
all, not a deleted key, so the old label asserts something untrue about the
account. It now reads "Unattributed" with the suffix "no key".

The sentinel is exported from the console client as UNATTRIBUTED_GROUP_KEY,
mirroring usage.UnattributedGroupKey, so the contract value is written once
on each side rather than as a bare string in a comparison.
Text half of the visual proof for PR #1351, committed under docs/proof so npm run lint:proof-tokens actually scans it. The screenshots themselves go to the visual-proof-assets release, per the orchestrator contract.
@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Visual proof

Console overview for the qa-tester workspace, same account and same session in both shots, differing only in which control-plane the console calls. First: the deployed unfixed control-plane, Recent errors renders its failure state. Second: this branch, the card renders 11 real errors. Third: the analytics page grouped by API key on this branch, where the new bucket appears as unattributed on the chart and as Unattributed with the suffix no key in Top API keys by spend, rather than as a deleted key. Full procedure and the before and after API responses are in docs/proof/analytics-null-group-key-1347/capture-log.txt.

pr1351-20260829053937-2683-before.png

pr1351-20260829053939-21909-after.png

pr1351-20260829053942-18974-analytics.png

CodeRabbit review follow-up.

The bucket collects three causes that the row itself cannot tell apart:
traffic that carried no API key, an error recorded before a key was resolved,
and a key deleted after the fact under ON DELETE SET NULL. The suffix read
"no key", which is a claim about history and is wrong for the third. It now
reads "no key on record", which is true under all three: whatever happened
earlier, the row carries no key now.

The reviewer's other suggestion, an explicit provenance field on the
control-plane response, is not implementable from the data. The column is
NULL in all three cases and nothing records which one occurred, so a
provenance field would have to invent a distinction the database does not
hold. Cause-neutral wording is the honest option available.

The label was already cause-neutral and is unchanged. The doc comment on
fetchTopKeys claimed the only way a spend row has no matching key is a
genuinely gone key or a failed fetch, which this branch makes false; it now
names the bucket as the third case.
…in it to Go

Second review pass, three findings, all of them correct.

The constant was exported from lib/control-plane/client.ts, which imports
next/headers and the server Supabase client. Type-only imports from that
module erase at build time, which is why lib/analytics/cache-metrics.ts
promises to take nothing else from it; importing a value broke that promise
and pulled a server-only dependency graph into a module that is meant to be
pure. Amending that doc comment to permit the import, which is what the
previous commit did, was rationalizing the defect rather than fixing it. The
constant now lives in lib/control-plane/contract.ts, which imports nothing,
and client.ts is back to its original content.

The Go constant and the TypeScript one were two decoupled strings with
nothing bridging them, so a rename on either side would leave the console
labelling every unattributed row as a deleted key again, silently, with
every test still green. TestUnattributedGroupKeyMatchesTheConsole reads the
console file and pins the two literals to each other. It needs no database
and runs in the short leg. Proven red by renaming the console constant to
unattributed_traffic.

The vitest case keyed its fixture by the same constant the code compares
against, so it asserted that a value equals itself and would have survived
the constant drifting away from what the control-plane actually sends. The
fixture now uses the wire literal, and the constant is asserted against that
literal separately.

Not changed, deliberately: the analytics table and charts still render
group_key verbatim, so the bucket appears there as the lowercase literal
next to raw key UUIDs, model aliases and endpoint paths. That column shows
identifiers exactly as the API returns them, which is what lets an operator
line a row up against a response; prettifying this one value and nothing
else around it would be the inconsistency.
The analytics screenshot was re-taken from the branch head after the suffix changed to "no key on record", so the committed log matches the image posted on the PR rather than an earlier build of it.
@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Visual proof

Re-captured from the branch head after review. Top API keys by spend, row 3, now reads Unattributed with the suffix no key on record, replacing the earlier no key: the same bucket also collects rows whose key was genuinely deleted under ON DELETE SET NULL, and nothing in the row says which case it is, so the suffix has to hold for both. This supersedes the third image in the previous proof comment.

pr1351-20260829060018-13418-analytics-final.png

@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Adversarial review, streams and dispositions

Three streams ran against this branch. Every finding is listed with what happened to it.

CodeRabbit CLI (coderabbit review --base main --committed)

Ran twice, and completed both times. The GitHub-side CodeRabbit check reports "Review rate limited", so the local CLI run is the one that carries weight here.

First pass, on the Go-only commit: no findings.

Second pass, after the console change: 2 findings, both Major.

  1. cache-metrics.ts, deleted-key rows and no-key rows presented alike. Fixed in commit b377813. The suffix read "no key", which is a claim about history and is false for a key deleted under ON DELETE SET NULL; it now reads "no key on record", which holds under all three causes. The reviewer's alternative, a provenance field on the response, is not implementable from the data: the column is NULL in all three cases and nothing records which one happened, so such a field would have to invent a distinction the database does not hold. That reasoning is in the commit message.
  2. docs/proof/.../capture-log.txt, remove the account identifier. Rebutted. qa-tester@hive.test is a synthetic fixture address on the RFC 2606 reserved .test TLD. It is not a mailbox, not routable, and not attached to a person. It already appears in docs/proof/public-auth-origin-2026-08-21/browser-login.log, docs/proof/chat-w0-surface-gaps/README.md, .wolf/buglog.jsonl and vendor/open-webui/src/lib/hive/displayName.ts on main. Naming the workspace is also what makes the capture checkable: an operator can go look at the same 14 rows. The repository hygiene rule this would engage bans real credentials and PII, and this is neither.

Antigravity (agy --model gemini-3.1-pro-high --effort high)

First pass, on the Go-only commit: 2 findings.

  1. The live test asserted every value on the unattributed bucket but only the existence of the attributed one. Fixed in commit 0c418db. Both summaries now assert its token counts, credits and entry count.
  2. The fixture guard's comment overstated what the guard buys. Fixed in the same commit. The bucket lookups would already fail if the row stopped being NULL; the guard stays because it fails pointing at the fixture instead of at the fix, and the comment now says that.

It also confirmed, independently, that the pgx pointer-to-pointer scan is the right idiom, that the sentinel cannot merge distinct buckets under any group_by, and that no other unsafe string scan remains in the package.

Second pass, on the console change: 5 findings.

  1. Importing a value from client.ts drags a server-only dependency graph (next/headers, the server Supabase client) into a module that promises to stay pure. Fixed in commit e9abbb3. This one was a genuine miss: the previous commit amended that module's doc comment to permit the import, which was rationalizing the defect rather than fixing it. The constant now lives in lib/control-plane/contract.ts, which imports nothing, and client.ts is byte-identical to main again.
  2. The suffix is wrong for a genuinely deleted key. Same finding as CodeRabbit's first, already fixed.
  3. The Go and TypeScript constants can drift with nothing to catch it. Fixed in e9abbb3. TestUnattributedGroupKeyMatchesTheConsole reads the console file and pins the two literals to each other. It needs no database and runs in the short leg. Proven red by renaming the console constant to unattributed_traffic.
  4. The vitest case asserted a tautology, keying its fixture by the same constant the code compares against. Fixed in e9abbb3. The fixture now uses the wire literal and the constant is asserted against that literal separately.
  5. The analytics table and charts render the raw lowercase unattributed. Rebutted. That column renders identifiers exactly as the API returns them: raw key UUIDs, model aliases, endpoint paths. Prettifying this one value while its neighbours stay raw is the inconsistency, and rendering it verbatim is what lets an operator line a table row up against a response body.

Local test runs

  • go test -tags integration ./internal/usage/... -count=1 against real Postgres with the full migration chain: pass.
  • go test ./apps/control-plane/... -count=1 -short: pass.
  • npm run test:unit (web console): 902 pass. The one failing suite, ci-web-e2e-secret-free, fails identically on main inside the Docker image because that image carries no .github/ directory; it passes in CI, where the full repo is checked out.
  • npm run build (web console): pass.
  • node tools/lint-no-token-in-proof-captures.mjs: pass.

Every mutation used to prove a test can fail is recorded in the PR body and the commit messages: the scan destination reverted, the sentinel replaced with an empty string, the console label branch removed, and the console constant renamed. Four mutations, four reds.

Comments only, no behaviour change. Both sentences had survived several rewrites and read like it.
CodeRabbit follow-up, comments only. The sentence predates this branch and my edit made it worse: extending the list of causes to three left it asserting that a null return distinguishes all of them. It distinguishes exactly one, the failed fetch. The bucket is told apart from a genuinely deleted key by its group key.
@sakibsadmanshajib

Copy link
Copy Markdown
Owner Author

Third CodeRabbit pass, on the complete branch

Ran again after the console rework: 1 finding, Minor, and it is correct.

lib/analytics/overview-fetch.ts:197, the doc comment on fetchTopKeys claimed the function "already tells those apart via null". It does not. null means one thing only: the Promise.all failed. A successful fetch can still carry the unattributed bucket or a genuinely deleted key, and neither is distinguished by the return value.

The sentence predates this branch, but my earlier edit made it worse by extending its list of causes from two to three while leaving the claim intact. Fixed in 221ed5b: the comment now says the null return distinguishes exactly one of the three, the failed fetch, and that the bucket is told apart from a gone key by its group key rather than by this return value. Comments only, no behaviour change.

That closes every finding from all three streams. Running totals across the branch:

Stream Runs Findings Fixed Rebutted
CodeRabbit CLI 3 3 2 1
Antigravity gemini-3.1-pro-high 2 7 6 1

Nothing is left open. The two rebuttals are argued in the review-streams comment above: the .test fixture address in the proof log, and rendering the raw sentinel in the analytics table alongside raw UUIDs.

@sakibsadmanshajib
sakibsadmanshajib merged commit 5e18725 into main Aug 29, 2026
30 checks passed
@sakibsadmanshajib
sakibsadmanshajib deleted the fix/analytics-null-group-key branch August 29, 2026 06:33
sakibsadmanshajib added a commit that referenced this pull request Aug 29, 2026
## Summary

This is the batched buglog follow-up for the pull requests merged to
`main` on 2026-08-29. Its diff is `.wolf/buglog.jsonl` and nothing else.

Per `.claude/rules/openwolf.md`, every fixed bug, error, failed test or
failed build must be logged, but the line may never be appended on a fix
branch. `merge=union` in `.gitattributes` resolves concurrent appends
locally and is ignored by GitHub's server side merge, so two branches
that both appended land in hard conflict there. An unmergeable pull
request gets no `refs/pull/N/merge`, no `pull_request` run and therefore
zero checks, and the required status gate then blocks the merge for a
reason the page never states (issue #873). Each fix accordingly carried
its entry in its own pull request body, and this pull request copies
them onto `main` in one batch, which the protocol explicitly prefers
over one pull request per entry.

## Scope examined

Fifty nine pull requests merged to `main` on 2026-08-29. Forty eight of
them carried at least one entry, for eighty two entries in total. Thirty
two of those were already on `main` and are skipped, leaving fifty
appended here from thirty four pull requests.

The largest block of skips comes from #1342, the equivalent batch for
the 2026-08-28 merges, which merged earlier the same day and already
landed thirty six entries covering #1257, #1268, #1276, #1277, #1287,
#1292, #1293, #1294, #1296, #1301, #1303, #1305, #1313, #1335 and #1337.

## What landed

Fifty entries appended, one JSON object per line, append only. The 232
pre-existing lines are byte identical to `origin/main` (verified by
hashing the first 232 lines of the result against the base file). Every
line in the resulting file parses as JSON and carries `error_message`,
`root_cause`, `fix` and `tags`.

| Source | Entries |
|---|---|
| #1083 | 2 |
| #1277 | 1 |
| #1278 | 1 |
| #1298 | 1 |
| #1334 | 1 |
| #1336 | 3 |
| #1343 | 1 |
| #1346 | 1 |
| #1351 | 1 |
| #1365 | 2 |
| #1368 | 1 |
| #1369 | 1 |
| #1371 | 3 |
| #1375 | 3 |
| #1376 | 1 |
| #1378 | 1 |
| #1379 | 2 |
| #1388 | 5 |
| #1389 | 3 |
| #1390 | 2 |
| #1393 | 1 |
| #1394 | 1 |
| #1410 | 1 |
| #1417 | 1 |
| #1421 | 1 |
| #1423 | 1 |
| #1424 | 1 |
| #1426 | 1 |
| #1429 | 1 |
| #1431 | 1 |
| #1433 | 1 |
| #1434 | 1 |
| #1436 | 1 |
| #1439 | 1 |

Entries are copied verbatim from their source pull request bodies.
Nothing was rewritten, no field was invented, and no field was added. No
JSON needed repair: all eighty two extracted entries parsed on the first
attempt and all four required fields were present on every one.

## Merged pull requests that carried no entry

Eleven of the fifty nine. Recorded here because the gap is itself the
useful signal.

| Pull request | Title | Assessment |
|---|---|---|
| #1013 | chore(deps): bump the go-minor-patch group across 1 directory
with 4 updates | Dependabot bump, no defect fixed, no entry expected |
| #1015 | chore(deps): bump the go-minor-patch group across 1 directory
with 6 updates | Dependabot bump, no entry expected |
| #1016 | chore(deps): bump golang from 1.26-alpine to 1.27-alpine in
/deploy/docker | Dependabot bump, no entry expected |
| #1218 | chore(deps): bump postcss from 8.5.19 to 8.5.26 in
/apps/desktop | Dependabot bump, no entry expected |
| #1219 | chore(deps): bump golang.org/x/crypto from 0.41.0 to 0.52.0 in
/apps/control-plane | Dependabot bump, no entry expected |
| #1342 | chore: batch buglog entries for the 2026-08-28 merges | The
previous batch pull request itself, correctly carries no entry of its
own |
| #1364 | chore: remove four dead skills and record the patterns that
cost time | Protocol gap. The body records patterns that cost time,
which is the shape of a buglog entry, but none was written as one |
| #1383 | test: retire stale expected-failure markers, restore the ones
that are true (#1381, #1382, #1324) | Protocol gap. Stale `it.fails`
markers reading as red is a real defect that was fixed here and should
have carried an entry |
| #1384 | docs: correct D-047, hive-auto reverted to variable pricing
(D-059) | Decision ledger correction, arguably a documentation defect,
no entry written |
| #1387 | chore(deps): bump next from 15.5.23 to 16.3.3 in
/apps/agent-console | Dependabot bump, no entry expected |
| #1398 | docs: rescue the 2026-08-25 parity captures and add the
2026-08-29 QA matrix evidence | Documentation and evidence rescue, no
entry written |

Six of the eleven are Dependabot bumps and one is the previous batch, so
the genuine protocol gaps are #1364, #1383, #1384 and #1398. Of those,
#1383 is the one worth a follow-up: it fixed a real defect class (a
stale expected-failure marker reads as a red "Expect test to fail" and
gets dismissed as pre-existing) and left no record.

## Entries skipped as already present

Thirty two. Thirty of them matched an entry already on `main` on
`error_message`, `id` or `fix`. Two more from #1278 are semantic
duplicates that an exact match would have missed, and were skipped after
reading the landed entries they duplicate:

- #1278's `streaming content_block_start omits text field` entry is
covered by the consolidated
`bug-2026-08-28-anthropic-sdk-wire-conformance` entry landed from #1296,
whose root cause names the same `omitempty` on
`StreamContentBlock.Text`.
- #1278's `GET /v1/models leaked an upstream provider name` entry is
covered by `BUG-1284`, landed from #1300, which names the same
`public.model_aliases.summary` publication path.

#1278's third entry, on `top_k` forwarding producing a 400, is not
covered anywhere on `main` and is appended here. #1342 recorded #1278 as
fully "merged into #1296", which was accurate for two of its three
entries.

## Note on entry quality

One appended entry is thin: #1277's parity re-score record carries
`error_message` of `n/a` and a root cause of "console had no
privacy/data-policy surface at all". It is a parity gap record rather
than a defect record. It is included exactly as written rather than
embellished, per the protocol's preference for the author's own words.

## Test plan

- [x] Branch cut fresh from `origin/main`, diff is `.wolf/buglog.jsonl`
and nothing else
- [x] First 232 lines byte identical to the base file (md5 match)
- [x] All 282 resulting lines parse as JSON and carry `error_message`,
`root_cause`, `fix` and `tags`
- [x] No `.wolf/` telemetry (`anatomy.md`, `memory.md`,
`token-ledger.json`, `hooks/_session.json`, `buglog.json`) in the commit
- [ ] The six required checks report green via the inert path allowlist
in `.github/workflows/ci.yml`

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Console overview Recent errors card returns 500: cannot scan NULL into *string for group_key

1 participant