Skip to content

feat(stats): bill Codex active time from real per-turn intervals + model compute - #40

Merged
takashi-matsuyama merged 1 commit into
mainfrom
feat/billing-grade-v2-codex-turns
Jun 4, 2026
Merged

takashi-matsuyama merged 1 commit into
mainfrom
feat/billing-grade-v2-codex-turns

Conversation

@takashi-matsuyama

Copy link
Copy Markdown
Member

Summary

Billing-grade work-measurement v2: improve the fidelity of Codex active time and add a model-compute measure. Builds directly on the basou stats work shipped in 0.5.0.

The Codex import adapter previously derived active time the same way as the Claude adapter — gap-capped point timestamps (the engaged-turns method). But a Codex rollout records explicit per-turn boundaries (task_started / task_complete, paired by turn_id, with a reported duration_ms), so the in-turn duration can be the log's real wall-clock span instead of a gap-capped approximation.

What changed

  • Codex active time now uses real per-turn intervals. Active time is the union of the real per-turn intervals (in-turn, uncapped, crediting the session's final turn) and the gap-capped engagement series (between-turn bridging). active_time_method becomes turn-intervals when any turn interval contributed, else engaged-turns.
    • The active-time semantics are unchanged — still human-engaged time with idle gaps over 5 minutes excluded. Only the in-turn precision improves, so a re-imported Codex session's active time typically rises (long turns are no longer truncated at the gap cap, and the final turn is counted).
    • Claude imports are unchanged: their transcripts carry no explicit per-turn duration, so no false precision is introduced.
  • New session.metrics.machine_active_time_ms (additive, optional): model compute time = summed per-turn duration. A subset of a session's active time. A plain sum, not wall-clock-deduplicated, so two concurrent sessions can sum past their billable (union) active wall-clock — intended (two models working at once is two machine-hours in one wall-clock hour). Surfaced as a Model working line in basou stats (and --by-source / --by-day / --json), a machine segment on basou session show's Work: line, and the basou view Stats tab. Re-import (--force, or basou refresh) to backfill.

Robustness (from adversarial review)

  • De-duplicate task_complete by turn_id so a repeated completion cannot double-count machine time and break the machine <= active subset.
  • Emit machine_active_time_ms only when every completed turn carried a duration; a session mixing duration-bearing and duration-less completions would otherwise report a partial compute total as if complete.
  • Clamp a turn start reconstructed from duration_ms (when task_started is absent) to the session floor, so no active interval predates the session, and bound each turn's machine contribution to its in-session span so a clamped turn stays a subset of active time.

Compatibility

machine_active_time_ms and the turn-intervals method are additive and optional; no schema_version bump. Existing imported sessions are unaffected until re-imported (consistent with how active_* was introduced).

Test plan

  • pnpm -r build && pnpm typecheck && pnpm test && pnpm lint all green (707 core + 417 cli + 1 sdk; lint clean apart from pre-existing unrelated warnings).
  • New unit tests cover: real turn intervals beating in-turn gap-cap, between-turn bridging vs over-cap idle, start reconstruction, turn_id de-dup, partial-duration omission, reconstructed-start clamping, schema validation, work-stats roll-up (per-session / by-source / by-day / totals), and the new rendered lines in stats / session show.
  • Validated against real local Codex rollouts: machine <= active holds for every session, no active interval precedes started_at, and older rollouts without duration_ms correctly omit machine time while still deriving turn-interval active time.

🤖 Generated with Claude Code

…del compute

The Codex import adapter derived active time the same way as the Claude
adapter: gap-capped point timestamps (the "engaged-turns" method). But a
Codex rollout records explicit per-turn boundaries -- task_started /
task_complete, paired by turn_id, with a reported duration_ms -- so the
in-turn duration can be the log's real wall-clock span instead of a
gap-capped approximation.

Active time for Codex sessions is now the union of the real per-turn
intervals (in-turn, uncapped, and crediting the session's final turn) and
the gap-capped engagement series (between-turn bridging). The active-time
SEMANTICS are unchanged: still human-engaged time with idle gaps over 5
minutes excluded; only the in-turn precision improves, so a re-imported
Codex session's active time typically rises (long turns are no longer
truncated at the gap cap, and the final turn is counted). active_time_method
is "turn-intervals" when any turn interval contributed, else "engaged-turns".
Claude imports are unchanged: their transcripts carry no per-turn duration.

Adds session.metrics.machine_active_time_ms (additive, optional): model
compute time = the summed per-turn duration, a subset of a session's active
time. It is a plain sum, NOT wall-clock-deduplicated, so two concurrent
sessions can sum past their billable (union) active wall-clock: intended, as
two models working at once did two machine-hours in one wall-clock hour.
Surfaced as a "Model working" line in stats (and --by-source / --by-day /
--json), a "machine" segment on session show's Work line, and the view Stats
tab. Re-import (--force, or refresh) to backfill existing Codex sessions.

Robustness:
- De-duplicate task_complete by turn_id so a repeated completion cannot
  double-count machine time and break the machine <= active subset.
- Emit machine time only when every completed turn carried a duration; a
  session that mixes duration-bearing and duration-less completions would
  otherwise report a partial compute total as if it were complete.
- Clamp a turn start reconstructed from duration (when task_started is
  absent) to the session floor, so no active interval predates the session,
  and bound each turn's machine contribution to its in-session span so a
  clamped turn stays a subset of active time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@takashi-matsuyama
takashi-matsuyama force-pushed the feat/billing-grade-v2-codex-turns branch from e857441 to e7525eb Compare June 4, 2026 02:03
@takashi-matsuyama
takashi-matsuyama merged commit c260b2c into main Jun 4, 2026
7 checks passed
@takashi-matsuyama
takashi-matsuyama deleted the feat/billing-grade-v2-codex-turns branch June 4, 2026 07:06
takashi-matsuyama added a commit that referenced this pull request Jun 4, 2026
…s) (#43)

Promote the Unreleased section to 0.6.0 and bump all four package
manifests (core / cli / sdk / basou) to 0.6.0 in lockstep.

Highlights since 0.5.0:
- billing-grade work-measurement v2: Codex active time from real
  per-turn intervals, plus a machine_active_time_ms model-compute
  measure (#40).
- @basou/sdk v0.2: a runtime, read-only programmatic API over
  @basou/core's readers for reading .basou/ provenance (#41).
- @basou/core JSON Schema artifacts for the on-disk formats, generated
  from the Zod schemas and shipped under ./schemas/* (#42).

Also: document the four-package lockstep bump in CONTRIBUTING, and
record the 0.6.0 pre-publish dry-run tarball contents in the release
checklist (core now ships its schemas/, cli ships two entry points).

This commit only prepares the release; tagging v0.6.0 and the OIDC
publish remain maintainer actions.

Co-authored-by: Claude Opus 4.8 (1M context) <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.

1 participant