All three have the same shape — the system learns something and only stderr
hears it — and each was flagged in the handoff as a silent-discard defect.
The gate's install outcome. `container_tool_hooks::install` returned Some or
None and both call sites wrote `let _ =`. A mission whose gate never installed
left a record indistinguishable from one whose gate stood there and matched
nothing. `EnsuredContainer` now carries the outcome to the callers that have a
pool, and they record `gate.installed` (with the settings path) or
`gate.absent` on the mission, so "was this mission gated?" is answerable from
the mission.
The inert marker. `vm_tool_gate` writes an `inert` file when it cannot parse
its input and allows everything, precisely so an inert gate does not look like
a permissive one. The only reader was a unit test. `drain_inert` now reads and
clears it at every tap drain, and a `gate.inert` event with the occurrence count
lands beside the calls that ran unchecked.
The judge's spend. `LlmEvent::Usage` arrived on every judge call and was
matched by `Ok(_) => {}`. Two plan exhaustions (2026-08-29, 2026-09-09) with
no row anywhere saying a judge token had been spent; `usage_events` had no
provider or model column. The loop now accumulates requests and tokens onto the
Verdict — counting a request BEFORE the stream opens, so a 429 the provider
refused still counts, because the retry storm was made of those — and
`record` writes a `kind = 'judge'` row with provider, model, mission and
request count. Migration 0085 adds the columns, all nullable, so the two
existing writers are untouched.
Tests: a scripted-provider verdict records one request and nonzero tokens; a
provider that refuses still records the request and zero tokens.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01WZb5A2kfVfjpdwSochkuHz
27 lines
1.3 KiB
SQL
27 lines
1.3 KiB
SQL
-- Who was paid, for what, on which mission.
|
|
--
|
|
-- usage_events recorded tokens and credits and nothing about the provider or
|
|
-- model behind them, so the z.ai weekly plan emptied twice (2026-08-29,
|
|
-- 2026-09-09) with no row anywhere saying a judge token had been spent. The
|
|
-- first signal each time was every done_when mission failing at once.
|
|
--
|
|
-- `requests` is the count that matters for a plan limit: a verdict is up to
|
|
-- MAX_TOOL_CALLS + 1 model calls and a blocked phase used to retry the whole
|
|
-- loop 180 times. A request the provider refused with a 429 still counts —
|
|
-- it was made.
|
|
--
|
|
-- All nullable, so every existing writer (cm-billing, world.rs) is untouched
|
|
-- and every existing row stays valid.
|
|
ALTER TABLE usage_events
|
|
ADD COLUMN IF NOT EXISTS provider TEXT,
|
|
ADD COLUMN IF NOT EXISTS model TEXT,
|
|
ADD COLUMN IF NOT EXISTS mission_id UUID REFERENCES missions (id) ON DELETE SET NULL,
|
|
ADD COLUMN IF NOT EXISTS requests INTEGER;
|
|
|
|
CREATE INDEX IF NOT EXISTS usage_events_provider_created_idx
|
|
ON usage_events (provider, created_at)
|
|
WHERE provider IS NOT NULL;
|
|
|
|
COMMENT ON COLUMN usage_events.provider IS 'Provider family that served this usage (e.g. glm, anthropic); NULL on rows written before it was recorded.';
|
|
COMMENT ON COLUMN usage_events.requests IS 'Model requests made, including ones the provider refused.';
|