Skip to content

fix(capture): keep sessions token columns nullable (absent = NULL, not 0) - #318

Open
khustup2 wants to merge 3 commits into
mainfrom
fix/sessions-token-columns-zero-fill
Open

fix(capture): keep sessions token columns nullable (absent = NULL, not 0)#318
khustup2 wants to merge 3 commits into
mainfrom
fix/sessions-token-columns-zero-fill

Conversation

@khustup2

@khustup2 khustup2 commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

What

Keep the four per-turn token/cache columns on sessionsinput_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens — as nullable BIGINT, and have the capture writer persist SQL NULL when a count is absent rather than a silent 0.

Why

The absent-vs-zero distinction is load-bearing: a measured cache_read_input_tokens = 0 ("nothing was read from cache" — a real datapoint) must stay distinct from "no usage data was produced". Collapsing absent → 0 erases that signal.

The reason absent counts previously caused storage write failures was a data-engine limitation in how omitted/NULL scalar values were handled on read; that is being addressed at the engine layer separately. On the client side the fix is: always emit all four columns (so a batched multi-row INSERT stays column-consistent), writing the measured value when present and NULL when absent.

Changes

  • sessions-summaries.ts: token columns are BIGINT (nullable, no DEFAULT). Additive / heal-safe on a populated table.
  • capture-handler.ts (usageColumns): always emits all four columns per turn — measured value or SQL NULL.
  • Doc comments + CHANGELOG updated to the NULL-for-absent semantics.
  • Tests assert NULL for absent counts and a literal 0 only for a genuinely measured 0.

Validation

Typecheck clean; SQL-safety audit passes; 304 capture + catalog tests green; lint clean on changed files.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Token and cache usage fields are now consistently included for every captured turn.
    • When token/cache usage is missing or incomplete, the corresponding stored values are written as SQL NULL (instead of being omitted), keeping them distinct from a measured 0.
    • Improved transcript persistence behavior for non-assistant turns and for missing-transcript scenarios.
  • Documentation
    • Updated release notes and inline descriptions to match the SQL NULL vs 0 semantics.
  • Maintenance
    • Bumped honeycomb plugin/package versions to 0.22.1.

@github-actions

Copy link
Copy Markdown

Thanks for the PR. Before we can merge, please read our CLA and sign it by posting the comment below.


I have read the CLA Document and I hereby sign the CLA


You can retrigger this bot by commenting recheck in this Pull Request. Posted by the CLA Assistant Lite bot.

@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Per-turn token and cache columns are now emitted for every captured event. Present counts are written as values and absent counts as SQL NULL; related contracts, tests, schema documentation, and version metadata were updated.

Changes

Turn usage persistence

Layer / File(s) Summary
Capture writer column emission
src/daemon/runtime/capture/capture-handler.ts
buildRow and usageColumns(event) always emit all four token/cache columns, using SQL NULL for absent values; flush signature grouping remains as defense-in-depth.
Usage persistence contracts
src/daemon/runtime/capture/event-contract.ts, src/daemon/storage/catalog/sessions-summaries.ts, src/hooks/claude-code/transcript.ts, src/hooks/normalize.ts
Comments describe event-level omission, SQL NULL persistence, and measured-zero handling.
Persistence behavior validation
tests/daemon/runtime/capture/*, tests/daemon/storage/catalog/sessions-usage-columns.test.ts
Tests verify fixed column presence, absent values as NULL, measured zero values, and additive schema-healing SQL.
Release metadata
.claude-plugin/*, CHANGELOG.md, harnesses/*
Release notes and manifests are updated to version 0.22.1.

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

Possibly related PRs

Suggested labels: release-approved

Suggested reviewers: thenotoriousllama

Poem

I’m a rabbit with rows to arrange,
Four token fields no longer change.
Missing counts become NULL,
Measured zeros still hop along—
A tidy session trail!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title says absent token counts stay NULL, but the PR actually zero-fills persisted session token columns and makes absent equal 0. Rewrite it to describe the zero-fill behavior, e.g. "fix(capture): zero-fill sessions token columns on persist".
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sessions-token-columns-zero-fill

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.

The four per-turn token columns (input_tokens, output_tokens,
cache_read_input_tokens, cache_creation_input_tokens) were designed as
nullable BIGINT so an absent count (never produced) stayed distinct from a
measured 0 (PRD-060a a-AC-6). That distinction is not representable on
pg-deeplake: it maps a scalar SQL column to a NON-NULLABLE deeplake type
(SQL nullability is ignored, `pg_to_deeplake_type` never sees attnotnull),
so a capture INSERT that OMITTED a token column was rejected at flush with

  Failed to flush inserts: Invalid value for column
  'cache_creation_input_tokens'. Reason - 'Invalid Operation: None value
  for scalar type'

(sqlstate XX000). This surfaced in prod as a steady internal-error stream
from a single org whose sessions were silently failing to persist.

Collapse absent -> 0:
- usageColumns always emits all four counts (the measured value, else 0)
  for every event; this also keeps a batched multi-row INSERT
  column-consistent.
- The catalog columns become BIGINT NOT NULL DEFAULT 0, matching every
  other scalar in the array and staying heal-safe on a populated table.

The reader stays null-tolerant: an all-zero session still reports ROI
"absent" via roi-savings' totalTokens===0 guard, so dashboard semantics are
unchanged. No DB migration is needed -- the always-emit writer supplies a
value even on an existing non-nullable column.
@khustup2
khustup2 force-pushed the fix/sessions-token-columns-zero-fill branch from f09cf70 to 515a536 Compare July 16, 2026 16:25
…t 0)

Reverses the zero-fill direction. The four per-turn token/cache columns
(input_tokens, output_tokens, cache_read_input_tokens,
cache_creation_input_tokens) stay nullable BIGINT so "token data absent"
(the count was never produced) stays DISTINCT from a measured 0 — e.g. a
real cache_read_input_tokens = 0 means "nothing read from cache", which
must not be confused with "no usage data".

The capture writer still emits all four columns on EVERY turn so a batched
multi-row INSERT stays column-consistent (BUG-03), but now writes the
measured value when present and SQL NULL when absent, instead of a silent
0. Schema goes back to plain `BIGINT` (no DEFAULT); it remains additive /
heal-safe on a populated table.

Tests updated to assert NULL for absent counts and a real 0 only for a
measured 0.
@khustup2 khustup2 changed the title fix(capture): zero-fill sessions token columns (a-AC-6 reversed) fix(capture): keep sessions token columns nullable (absent = NULL, not 0) Jul 23, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 3-5: Update the v0.22.1 release note to state that absent
token/cache usage counts are persisted as 0, backed by BIGINT NOT NULL DEFAULT 0
columns, rather than SQL NULL. Preserve the distinction that measured zero
remains a valid measured value.

In `@src/daemon/runtime/capture/capture-handler.ts`:
- Around line 922-940: Restore zero-fill for absent token counts in usageColumns
so every value uses numeric 0, and update the related contract documentation; in
src/daemon/runtime/capture/capture-handler.ts:712-717 and 922-940 document and
implement absent/measured-zero collapse. Restore BIGINT NOT NULL DEFAULT 0 in
src/daemon/storage/catalog/sessions-summaries.ts:57-67, restore downstream
zero-fill and invalid-count behavior in src/hooks/normalize.ts:272-273 and
312-313, and update the listed capture tests in
src/.../capture-token-usage.test.ts:233-278 and
capture-transcript-model.test.ts:253-253 to expect "0". Update
src/daemon/storage/catalog/sessions-usage-columns.test.ts:30-36 to assert the
non-null default and matching heal/create SQL.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f910e344-1e53-4057-88d5-87a2bf38b2e4

📥 Commits

Reviewing files that changed from the base of the PR and between 82532dd and 39defba.

📒 Files selected for processing (8)
  • CHANGELOG.md
  • src/daemon/runtime/capture/capture-handler.ts
  • src/daemon/storage/catalog/sessions-summaries.ts
  • src/hooks/claude-code/transcript.ts
  • src/hooks/normalize.ts
  • tests/daemon/runtime/capture/capture-token-usage.test.ts
  • tests/daemon/runtime/capture/capture-transcript-model.test.ts
  • tests/daemon/storage/catalog/sessions-usage-columns.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/hooks/claude-code/transcript.ts

Comment thread CHANGELOG.md
Comment on lines +3 to +5
## v0.22.1 — 2026-07-16

Fixed a bug where token/cache usage counts could be omitted from stored session rows, causing storage write failures. All four counts are now always written on every turn — the measured value when present, or SQL NULL when absent — kept distinct from a measured 0.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document the zero-fill behavior accurately.

This says absent counts are persisted as SQL NULL, but PR #318 changes the contract to write 0 for absent counts and use BIGINT NOT NULL DEFAULT 0. Update the release note to avoid misleading users about session-row semantics.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 3 - 5, Update the v0.22.1 release note to state
that absent token/cache usage counts are persisted as 0, backed by BIGINT NOT
NULL DEFAULT 0 columns, rather than SQL NULL. Preserve the distinction that
measured zero remains a valid measured value.

Comment on lines +922 to +940
* The per-turn token/cache columns EVERY event contributes to its `sessions` row
* (PRD-060a a-AC-5 / a-AC-6). All four columns are ALWAYS emitted so a batched
* multi-row INSERT stays column-consistent, but each carries the MEASURED value only
* when the assistant turn produced it — otherwise SQL NULL.
*
* NULL = "token data absent", kept DISTINCT from a measured 0 (a-AC-6): a real
* `cache_read_input_tokens = 0` means "nothing read from cache", which must not be
* confused with "the count was never produced". The zod boundary already guaranteed
* every present count is a non-negative integer, so no re-validation is needed here.
*/
function usageColumns(event: CaptureEvent): ReadonlyArray<readonly [string, number]> {
if (event.kind !== "assistant_message" || event.usage === undefined) return [];
const u = event.usage;
const cols: Array<readonly [string, number]> = [];
if (u.input !== undefined) cols.push(["input_tokens", u.input]);
if (u.output !== undefined) cols.push(["output_tokens", u.output]);
if (u.cacheRead !== undefined) cols.push(["cache_read_input_tokens", u.cacheRead]);
if (u.cacheCreation !== undefined) cols.push(["cache_creation_input_tokens", u.cacheCreation]);
return cols;
function usageColumns(event: CaptureEvent): ReadonlyArray<readonly [string, ColumnValue]> {
const u = event.kind === "assistant_message" ? event.usage : undefined;
const cell = (n: number | undefined): ColumnValue => (n === undefined ? val.raw("NULL") : val.num(n));
return [
["input_tokens", cell(u?.input)],
["output_tokens", cell(u?.output)],
["cache_read_input_tokens", cell(u?.cacheRead)],
["cache_creation_input_tokens", cell(u?.cacheCreation)],
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Restore zero-fill; SQL NULL recreates the capture failure this PR is meant to fix.

usageColumns() now sends raw NULL into pg-deeplake scalar fields. The PR objective and the changed catalog-test comment both state that these scalars reject None; existing healed tables also retain their prior non-nullable columns because this PR includes no migration. Emit val.num(n ?? 0), retain BIGINT NOT NULL DEFAULT 0, and update the contracts/tests to assert literal 0 for absent counts.

  • src/daemon/runtime/capture/capture-handler.ts#L922-L940: zero-fill every absent count instead of emitting val.raw("NULL").
  • src/daemon/runtime/capture/capture-handler.ts#L712-L717: document absent and measured-zero collapse.
  • src/daemon/storage/catalog/sessions-summaries.ts#L57-L67: restore BIGINT NOT NULL DEFAULT 0.
  • src/hooks/normalize.ts#L272-L273: restore the downstream zero-fill contract.
  • src/hooks/normalize.ts#L312-L313: state invalid counts become absent and persist as 0.
  • tests/daemon/runtime/capture/capture-token-usage.test.ts#L233-L278: expect all absent token columns to be "0".
  • tests/daemon/runtime/capture/capture-transcript-model.test.ts#L253-L253: expect "0".
  • tests/daemon/storage/catalog/sessions-usage-columns.test.ts#L30-L36: assert BIGINT NOT NULL DEFAULT 0 and matching heal/create SQL.
📍 Affects 6 files
  • src/daemon/runtime/capture/capture-handler.ts#L922-L940 (this comment)
  • src/daemon/runtime/capture/capture-handler.ts#L712-L717
  • src/daemon/storage/catalog/sessions-summaries.ts#L57-L67
  • src/hooks/normalize.ts#L272-L273
  • src/hooks/normalize.ts#L312-L313
  • tests/daemon/runtime/capture/capture-token-usage.test.ts#L233-L278
  • tests/daemon/runtime/capture/capture-transcript-model.test.ts#L253-L253
  • tests/daemon/storage/catalog/sessions-usage-columns.test.ts#L30-L36
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/daemon/runtime/capture/capture-handler.ts` around lines 922 - 940,
Restore zero-fill for absent token counts in usageColumns so every value uses
numeric 0, and update the related contract documentation; in
src/daemon/runtime/capture/capture-handler.ts:712-717 and 922-940 document and
implement absent/measured-zero collapse. Restore BIGINT NOT NULL DEFAULT 0 in
src/daemon/storage/catalog/sessions-summaries.ts:57-67, restore downstream
zero-fill and invalid-count behavior in src/hooks/normalize.ts:272-273 and
312-313, and update the listed capture tests in
src/.../capture-token-usage.test.ts:233-278 and
capture-transcript-model.test.ts:253-253 to expect "0". Update
src/daemon/storage/catalog/sessions-usage-columns.test.ts:30-36 to assert the
non-null default and matching heal/create SQL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants