feat: per-stage token usage + cost tracking and an opt-in metrics emission seam (2.5.40) - #673
Conversation
0f7eea2 to
0f14d88
Compare
leandrodamascena
left a comment
There was a problem hiding this comment.
Requesting changes. The happy-path tests pass, but adversarial validation exposed several data-integrity and security failures in the new observability surfaces.
Blocking findings
- The ledger mixes usage across sessions, workflows, and intents
core/tools/aidlc-usage.ts:735-737 stores one workspace-global usage-ledger.json, while core/tools/aidlc-usage.ts:870-877 indexes stage usage only by stage slug. core/hooks/aidlc-statusline.ts:200-209 then displays the global totals as the current session's usage.
Reproduction: I folded 10 input tokens for intent-capture from one session, then 20 from a second session/workflow. The second workflow's stage rollup reported 30 instead of 20.
This makes both statusline totals and completion audit fields increasingly inaccurate over the lifetime of the workspace. The ledger needs session/workflow/intent dimensions or explicit per-workflow baselines.
- Concurrent hooks lose ledger updates
foldTranscriptIntoLedger() performs an unlocked read-modify-write at core/tools/aidlc-usage.ts:1376-1420. Atomic rename prevents malformed JSON but does not prevent one writer from replacing another writer's totals and cursors. writeFileAtomic() also uses a shared <path>.tmp filename at core/tools/aidlc-lib.ts:3276-3283.
Reproduction: I launched 24 processes folding distinct transcripts into the same ledger. Only 7 tokens and 7 cursors survived.
This is especially relevant to concurrent sessions and subagent-heavy workflows. The complete ledger transaction needs a cross-process lock; temporary filenames must also be writer-unique.
- Usage at stage boundaries is omitted and later attributed to the wrong stage
The PostToolUse hook reads the current stage only after the tool has executed at core/hooks/aidlc-fold-usage.ts:68-86. The holdback logic at core/tools/aidlc-usage.ts:1321-1345 then applies that later stage value when it eventually folds the pending message group.
A lifecycle tool can advance the state before PostToolUse runs. Furthermore, the state tool computes and emits STAGE_COMPLETED before the current assistant tool-call has entered the ledger.
Reproduction: a call made during stage-a was held back; after the state moved forward, the next fold recorded all 100 input tokens under stage-b. stage-a remained empty.
Stage identity must be captured with the pending group before a transition can change it, and the call that completes a stage must be included in that stage's completion rollup.
WORKFLOW_COMPLETEDreports only the final stage
core/tools/aidlc-state.ts:1817-1822 calls:
const usageFields = stageRollupFields(pd, completedSlug);Those same fields are attached to WORKFLOW_COMPLETED at core/tools/aidlc-state.ts:1902-1908. Therefore, the workflow event reports the final stage's usage, not whole-workflow usage.
This contradicts the event name and the advertised workflow rollup. It also makes workflow-level metrics incorrect. WORKFLOW_COMPLETED needs a workflow-scoped aggregate independent of the final stage bucket.
- Metrics authorization secrets are exposed in process arguments
core/tools/aidlc-metrics.ts:357-370 expands AIDLC_METRICS_HEADERS into curl -H arguments. Bearer tokens consequently appear in the OS process table and may be collected by endpoint-security/process telemetry.
I reproduced this with a local collector and confirmed the bearer token was visible through ps.
Sensitive headers should be supplied through stdin, a protected file descriptor, or another mechanism that does not place credentials in argv.
Additional findings
- Model normalization can silently apply the wrong price
core/tools/aidlc-usage.ts:217-236 uses unbounded substring matching. Confirmed mappings include:
claude-opus-4-80 => opus-4-8
claude-sonnet-4-60 => sonnet-4-6
notclaude-opus-4-8 => opus-4-8
This violates the stated unknown-generation policy. Matching should enforce generation-token boundaries and validate the provider/model shape.
- Rate overrides cannot add a new generation
Although the implementation comments say an override may add keys, normalization is limited to the hard-coded GENERATION_MATCHERS. Adding an opus-6 row through AIDLC_MODEL_RATES still leaves claude-opus-6 unpriceable.
Normalization should resolve exact keys from the effective rate table rather than only from the built-in matcher list.
- Batch audit events bypass metrics
appendAuditEntries() at core/tools/aidlc-audit.ts:347-375 writes audit blocks directly without calling the metrics tap. Events written through append-batch therefore emit no metrics, contradicting the documented "every audit event" behavior.
- A valid final JSONL object is dropped without a trailing newline
core/tools/aidlc-usage.ts:1209-1215 treats every unterminated final line as partial, even during flush=true. A syntactically complete final JSON object is permanently omitted if the session ends without another newline.
During a final flush, a complete JSON object should be parsed even when EOF follows it directly.
Validation
The three new suites passed: 76 tests and 276 assertions. An additional 120 affected hook and integration tests passed. Typecheck and bun scripts/package.ts --check also passed.
These suites currently do not cover cross-process writes, workflow/session isolation, lifecycle-boundary attribution, complete EOF records, or credential exposure.
The branch is also conflicting with current v2 and requires the expected version/changelog rebase and t252 test-number collision resolution.
31a32f1 to
4b41062
Compare
leandrodamascena
left a comment
There was a problem hiding this comment.
Re-reviewed head 4b41062e. All nine previously reported findings are resolved. Local parity and typecheck pass, targeted concurrency testing passed 20 repeated runs, and CI is green. No remaining blocking findings.
4b41062 to
d6d8c9a
Compare
What
Makes AIDLC cost-aware. Three observability surfaces that were token-blind gain per-stage token usage and dollar cost, plus an opt-in seam that streams audit events and usage magnitudes to any StatsD-compatible forwarder. The implementation was validated against an 8-subagent session with 937 usage turns, 423 unique UUIDs, and 191 cross-file collisions.
User-visible changes (Claude Code harness)
Other harnesses: no producer wired; every consumer degrades silently to no-data.
Design
Testing
Note for maintainers: open PR #705 claims the t267 number with a different filename; per the renumber convention, whichever merges second re-slots (that will be #705).