Skip to content

recordEngineUsage drops the payer observation it is already holding on the Pilot's drains #356

Description

@serge-ivo

Found while assessing #346.

What happens

recordEngineUsage takes an optional authResolved and maps it to the ledger's payer
(migration 0092). It has four call sites, and only one passes the observation:

Call site Passes authResolved? Has it available?
routes/coding.ts:698 (console capture poll) ✅ yes yes
workflows/coding-session.ts:379 (the Pilot's own capture drain) ❌ no yes — in snap
workflows/coding-session.ts:663 (acts-final-drain) ❌ no yes — in snap
routes/coding.ts:1473 (session end) ❌ no no — /coding/end doesn't return it

The runner puts authResolved on every /coding/capture snapshot, unconditionally and
independent of drainUsage (packages/browser-runner/src/coding/runtime.ts:190, with the comment
"Reported even when the process is not alive"). Both workflow drains call that exact endpoint with
drainUsage: true and then destructure the response — const { usage, acts, ...pane } = snap
so the field is in hand and discarded one line before the ledger write.

lib/usage.ts:164 states the opposite as the justification for the parameter being optional:

several drain paths (session end, the Pilot's own capture) have no observation to pass

Session end is correct. The Pilot's own capture is not: it holds the same snapshot
routes/coding.ts:697 reads the value off.

Why it matters

The workflow drain's own comment says it is "the only path collecting the Engine's own spend for
the longest and most expensive sessions the platform has". So the sessions whose payer attribution
matters most are exactly the ones recording payer = NULL, while a short session watched in the
console records it correctly. The Usage page then reports "Payer not established" for autonomous
work whose credential the platform did observe.

It does not re-open #343 — NULL is excluded from CHARGED_SQL, so nothing is blocked over
money nobody owes. The damage is that the column added to answer "who paid?" is unpopulated on the
majority of engine rows, for no reason in the data.

Fix

Pass it, the same way routes/coding.ts:697 does — the wire object carries it even though
CodingPaneSnapshot (lib/coding-loop.ts:38) does not declare it. Adding authResolved?: EngineAuthResolved to that interface would make the field visible to both drains and remove the
cast at the third site.

Optionally, and separately: /coding/end could return authResolved alongside {usage, acts}
(packages/browser-runner/src/coding/runtime.ts:224) so the closing turn is attributed too. That
one needs a runner release, so it is worth doing only if the first fix leaves a visible gap.

A test worth more than the fix: assert every recordEngineUsage call whose records come from a
/coding/capture response also passes authResolved. The failure mode here is a fifth drain
path added later without it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    backendBackend / Worker / API workbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions