Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Phase 2: Per-session billing mode + two-ledger UI (FEA-1434) - #254

Closed
aeyeCEO wants to merge 6 commits into
mainfrom
FEA-1434-billing-mode
Closed

aeyeCEO wants to merge 6 commits into
mainfrom
FEA-1434-billing-mode

Conversation

@aeyeCEO

@aeyeCEO aeyeCEO commented May 29, 2026

Copy link
Copy Markdown
Contributor

Parent PRD: PRD-414 — Accurate token cost calculation & reconciliation across multi-vendor agents.

Summary

Phase 2 of the token cost work. Distinguishes API-metered sessions from subscription-covered sessions (Claude Max, ChatGPT Pro, Cursor Pro, Copilot, OpenCode) so a heavy Max user doesn't see "$1,800 spent" on a $200 flat plan.

  • Canonical BillingMode type in src/shared/billing-mode.ts (api | claude_pro | claude_max | codex_chatgpt_pro | cursor_pro | copilot_seat | opencode | unknown).
  • Credential-based detector at src/main/billing-mode-detector.ts. File-existence-only probes — token contents are NEVER read or logged.
  • Shared tagSpawnedSession helper invoked at every Claude/Codex spawn site (symphony-loop, symphony-interactive, chat-providers ClaudeProvider + CodexProvider, codex.ts both review paths, learnings.ts shell-script spawn).
  • File importers (Cursor / Copilot / OpenCode) stamp their billing mode at import time.
  • Sidecar sessions table gets a billing_mode column via additive ALTER migration. The sync-service runs an idempotent migration before any SELECT to handle the sidecar-startup race.
  • Sync output merges launch-metadata over the row value AND batch-writes the merged value back to the sidecar so the local Sessions UI matches what cloud sees. The writeback UPDATE excludes 'api', 'claude_max', 'claude_pro' from the importer-default set so importer re-runs can't clobber a deliberate value.
  • Bootstrap: launch-metadata pre-written at every `entry.localPath` so per-repo sessions all get stamped.
  • Sessions UI splits into two ledgers — "Metered API" and "Subscription-covered" with the API-equivalent cost shown in a muted style. Subscription cost NEVER sums into the headline.
  • `AGENT_SESSION_SYNC_SCHEMA_VERSION` bumped 1 → 2. Relay accepts v1 payloads (no `billingMode`) and treats as `unknown`.

Review chain:

  • Round 1 (codex): caught 1 P1 (UI/cloud parity broken) + 2 P2 (main spawn paths not stamped + sync-service migration race). All fixed.
  • Round 2 (codex): caught 4 P2 (writeback race, bootstrap multi-cwd, ClaudeProvider not stamped, spawnClaudeReview not stamped). All fixed.
  • Round 3 (Claude): caught 2 Medium (dead privacy setting + importer clobber guard gap) + 1 Low (dead claude_pro type entry). Privacy setting deferred to FEA-1445; clobber guard tightened to exclude 'api', 'claude_max', 'claude_pro'; claude_pro documented.

Schema-version coordination

This PR bumps `AGENT_SESSION_SYNC_SCHEMA_VERSION` from 1 → 2. Sibling PR (#253) also bumps 1 → 2. Whichever merges first wins; the second needs to rebase to 2 → 3.

Follow-up tickets filed during the work

  • FEA-1439 — Remove v1 schema acceptance from agent-session sync relay (post rollout). Referenced in agent-session-sync-contract.ts.
  • FEA-1441 — Invalidate billingModeByCwd cache on launch-metadata file change.
  • FEA-1445 — Wire showSubscriptionEquivalentCost through sidecar (Settings → privacy toggle).

Test plan

  • CI pipeline green
  • Smoke: launch Claude with `ANTHROPIC_API_KEY=...`; verify session shows in Metered API ledger
  • Smoke: launch Claude via OAuth (Max plan); verify session shows in Subscription-covered ledger with "would have cost $X on API"
  • DB upgrade: open a profile that pre-dates this PR; verify `billing_mode` column is added with default `unknown` and existing rows are preserved
  • Importer race: tag a session as 'api' via launch-metadata, force a sidecar re-import, verify the row stays at 'api' (not overwritten by importer default)

🤖 Generated with Claude Code

Andrew Eye and others added 6 commits May 28, 2026 20:20
- Add canonical BillingMode enum (src/shared/billing-mode.ts) and a
  best-effort detector (src/main/billing-mode-detector.ts) that maps
  (harness, env, credentials-file existence) to api / claude_max /
  codex_chatgpt_pro / cursor_pro / copilot_seat / opencode / unknown.
  Credentials files are existence-checked only — contents are never
  read or logged. ANTHROPIC_API_KEY / OPENAI_API_KEY beat any
  credentials file.
- Stamp harness + billingMode at every Claude/Codex spawn site
  (symphony-loop LLM commit, symphony-interactive streamClaudeChat
  and Haiku commit, codex.ts engineer-chat / Codex review / Codex
  conversation, chat-providers Codex, symphony-utils bootstrap) via
  a new recordSessionSpawn shared helper that writes into the
  worktree's launch-metadata.json. Per-harness importers
  (cursor/copilot/opencode) stamp a fixed mode on the sessions row
  via the new setSessionBillingMode prepared statement; codex
  importer defaults to codex_chatgpt_pro (overridden by launch
  metadata when desktop spawned it with an API key).
- Schema migration in build-agent-monitor.mjs adds
  `billing_mode TEXT NOT NULL DEFAULT 'unknown'` + an index, mirroring
  the existing harness migration. The generated db.js carries the
  ALTER TABLE + setSessionBillingMode statement; verified by building
  the agent-monitor tree.
- Sync contract bumped to v2 with optional billingMode on
  SyncedAgentSession. The desktop sync service joins the sessions row
  with launch-metadata so the env-detected mode wins over the
  importer-stamped row value. Legacy v1 payloads remain decodable —
  see the inline note in agent-session-sync-contract.ts; the cloud
  relay must accept missing billingMode as 'unknown' until a removal
  ticket is filed.
- Sessions.tsx renders two stat tiles ("Metered API spend" /
  "Subscription-covered") and a per-row cost cell that says
  "Covered · $X equiv." for subscription modes. Subscription cost is
  NEVER summed into the headline (it's on its own muted tile).
- Add showSubscriptionEquivalentCost privacy setting (default true)
  on SettingsStore + DesktopSettings so users can hide the
  subscription dollar figure for screen-sharing.
- Step 7 (quota indicator from anthropic-ratelimit-* headers) is
  deferred — no header plumbing currently exists between the Claude
  Code subprocess and the desktop; the stream-json output swallows
  HTTP headers. The UI silently omits the quota pill, matching the
  plan's accepted-for-v1 fallback.
- Bump apps/desktop version 0.15.93 -> 0.15.94.

Testing:
- pnpm -C apps/desktop typecheck: clean.
- pnpm -C apps/desktop lint: clean.
- pnpm -C apps/desktop test: 1834 pass / 0 fail (tsx) + 104 pass / 0
  fail (node --test). New tests: 11 billing-mode-detector cases (env,
  disk, dispatch, anti-leak guard) + 3 schema-migration cases
  (additive, idempotent, index) + 3 sync-service round-trip cases
  (column → payload, launch-metadata override, schema version bump).
- pnpm -C apps/desktop build:agent-monitor: clean; generated db.js
  shows the new ALTER + setSessionBillingMode statement; all four
  non-Claude importers received the stamp; client bundle includes the
  two-ledger UI logic.

Risks:
- External contract bump (schema v2). Outbound payloads still
  decode under v1 readers — they just omit billingMode — but a
  cloud relay that enforces strict v2 requires a coordinated rollout.
  Tracking ticket for removing the v1 acceptance path needs to be
  filed; the inline comment in agent-session-sync-contract.ts is the
  pointer.
- Existing sessions migrate to billing_mode='unknown' on first DB
  open; UI shows them under neither ledger (unknown ≠ api ≠
  subscription). New rows from the next sidecar startup carry a real
  mode.
…ation

Codex review of 3ebc393 surfaced three issues; this commit stacks
fixes on top.

- [P1] Two-ledger UI was reading sessions.billing_mode directly but
  that column stayed at the importer default (or schema 'unknown')
  for Claude-hook-created sessions even when launch-metadata carried
  the real mode. loadSyncedSessions now writes the resolved mode
  back to the row (only over importer defaults — never a deliberate
  setting) so local UI matches the cloud relay's view.
- [P2] recordSessionSpawn was wired at the LLM-commit helper but
  NOT at the main handleLoopRequest spawn paths (Plan, Execute,
  RequestChanges, GeneratePrd, RequestPrdChanges, EvaluatePrd,
  EvaluateFeature, EvaluatePlan, EvaluateCode, Decompose,
  Bootstrap). Extract a one-call tagSpawnedSession helper into
  symphony-utils.ts and invoke it once at the shared spawn-prep
  block in symphony-loop.ts, plus at the learnings-processing
  spawn (process-chat-learnings.sh invokes claude internally).
- [P2] AgentSessionSyncService did SELECT billing_mode FROM sessions
  directly; if the sidecar startup ALTER hadn't landed yet, this
  threw `no such column`. Added an idempotent ensureBillingModeColumn
  migration (try/SELECT → catch/ALTER) run inside syncOnce so the
  sync service is self-sufficient regardless of sidecar boot order.

Bump apps/desktop version 0.15.94 -> 0.15.95.

Testing:
- pnpm -C apps/desktop typecheck: clean.
- pnpm -C apps/desktop lint: clean.
- pnpm -C apps/desktop test: 1842 pass / 0 fail (tsx) + 104 pass /
  0 fail (node --test). 8 new tests: 3 writeback / migration cases
  in agent-session-sync-service.test.ts (writeback fires for
  importer defaults, NEVER clobbers a non-default value,
  ensureBillingModeColumn adds the column + index idempotently);
  5 tagSpawnedSession cases in symphony-utils.test.ts (claude API,
  codex API, cursor fixed-default, malformed-worktree resilience,
  source-level regression guard that asserts symphony-loop.ts
  imports + invokes tagSpawnedSession).

Risks:
- The writeback only fires when launch-metadata's billingMode
  differs from the row column AND the row is a known importer
  default. The set is hardcoded ({unknown, codex_chatgpt_pro,
  cursor_pro, copilot_seat, opencode}); a future taxonomy
  expansion needs to update BILLING_MODE_IMPORTER_DEFAULTS or
  switch to a priority-rank source field. Inline comment flags
  this for follow-up.
- ensureBillingModeColumn runs on every syncOnce. The probe is one
  SELECT LIMIT 1 — cheap — but a sidecar with corrupt schema would
  hit the inner try/catch and silently skip. Acceptable: the sync
  service already swallows all unexpected DB errors at the
  syncOnce boundary; this just keeps the column-missing path
  recoverable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The FEA-1434 review-fix agent flagged that agent-session-sync-contract.ts
still says "Tracking ticket TBD — see the report" for removing the v1
decode path. The tracking ticket (FEA-1439) was created out-of-band
during the FEA-1434 rollout; this commit just substitutes it into the
comment so future readers find it cleanly.

No behavior change. No tests touched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closedloop convention requires a version bump on every commit touching
apps/desktop/. 6ea51df edited the contract comment without bumping;
adding the bump now as a follow-up commit per the "create new commit
rather than amend" rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex review of ea40777 surfaced four P2 findings; this commit stacks
fixes on top.

- [A] loadSyncedSessions UPDATE was `WHERE id = ?` only — between the
  read loop scheduling a writeback and the batched flush, a concurrent
  writer (sidecar importer or manual fix) could have replaced the
  importer default with a deliberate non-default value, and the
  unconditional UPDATE would clobber it. Extract IMPORTER_DEFAULT_
  BILLING_MODES (single source of truth for the read-side Set and the
  write-side WHERE-IN placeholders), tighten the UPDATE to
  `WHERE id = ? AND (billing_mode IS NULL OR billing_mode IN (...))`.
  IS NULL branch mirrors the read-side null allowance — SQL IN does
  not match NULL.
- [B] Bootstrap's upstream tagSpawnedSession stamped
  `worktreeDir ?? claudeWorkDir`, but the generated bootstrap script
  `cd`s into each `entry.localPath` before each `$CLAUDE_BIN` so the
  per-repo sessions' `sessions.cwd` landed at `entry.localPath` (not
  covered by the upstream stamp). Extract
  tagBootstrapPerRepoLaunchMetadata helper and invoke it after the
  manifest is built so launch-metadata is pre-written at every
  runnable entry.localPath before the bash script spawns. The shell
  script can't easily call the JS helper inline — static cwds known
  at spawn time make pre-writing the cleanest fix.
- [C] ClaudeProvider.spawn in chat-providers.ts did not stamp
  launch-metadata; CodexProvider at the sibling site did. Mirror the
  Codex stamp before delegating to processManager.spawnStreaming —
  uses recordSessionSpawn with detectBillingModeForHarness("claude",
  shellEnv). Only stamps when resolveSpawnCwd returned a path (no
  worktree → no metadata file).
- [D] spawnClaudeReview() in codex.ts spawned `claude` for the
  PR-review flow without stamping. The sibling spawnCodexReviewProcess
  was already stamped. Hoist the shellEnv into a const, add
  recordSessionSpawn before spawn(claudeBin, ...).

Out of scope (filed as a follow-up FEA below): the billingModeByCwd
cache in launch-metadata.ts keeps the first observed metadata for the
process lifetime, so a re-tag in the same cwd is masked until the
desktop app restarts. Real but minor — restart fixes it.

Bump apps/desktop version 0.15.96 -> 0.15.97.

Testing:
- pnpm -C apps/desktop typecheck: clean.
- pnpm -C apps/desktop lint: clean.
- pnpm -C apps/desktop test: 1848 pass / 0 fail (tsx) + 104 pass /
  0 fail (node --test). +6 new tests vs round 1 baseline (1842+104):
  agent-session-sync-service.test.ts adds (1) a TOCTOU race test that
  monkey-patches db.prepare to inject a concurrent UPDATE between
  prepare() and run() and asserts the deliberate value survives, and
  (2) a source-level guard asserting the UPDATE SQL contains the
  importer-default WHERE-clause predicate. symphony-utils.test.ts adds
  (3) a unit test for tagBootstrapPerRepoLaunchMetadata with two
  fixture entries, (4) a source-level guard that the Bootstrap branch
  invokes the per-repo helper, (5) a source-level guard that
  ClaudeProvider's class body contains recordSessionSpawn (not just
  somewhere in the file — CodexProvider already had it), and (6) a
  source-level guard scoped to the spawnClaudeReview function body.

Risks:
- The race test relies on monkey-patching db.prepare, which is a
  white-box hack. If the sync service stops caching the prepared
  statement, or switches to a different SQL builder, the interceptor
  may not fire — but the source-level guard catches the regression
  independently.
- The Bootstrap per-repo stamp runs once per runnable entry, before
  the bash script spawns. If a future refactor moves the bootstrap
  script to clone repos lazily (where entry.localPath is not known
  at spawn time), the pre-write strategy breaks. The source-level
  guard would still pass (the call is still there) so the failure
  mode would be runtime-only; flagged for future review.
- ClaudeProvider's stamp uses resolveSpawnCwd to compute the cwd
  once and reuse it for both the stamp and the spawn. Pre-fix the
  stamp would have been skipped silently when params.cwd was
  undefined; post-fix it is still skipped — the cwd-required path
  is the same as before.

Follow-up FEA suggestion (not addressed in this commit, see Out of
scope above): "Invalidate billingModeByCwd cache on launch-metadata
file change" — the cache in launch-metadata.ts keeps the first
observed metadata for the lifetime of the desktop process, so a
re-stamp in the same cwd (e.g. after the user fixes their API key in
settings without restarting) is masked. Real but minor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… guard

Claude review of ee4f7e4 surfaced three findings; this commit stacks
fixes on top.

- [Finding 1 / M] showSubscriptionEquivalentCost was fully dead:
  defined in DEFAULT_DESKTOP_SETTINGS + SettingsStore getter/setter +
  desktop:get-settings IPC, but never read by Sessions.tsx — flipping
  it had zero UI effect. Two options were considered; Option A (wire
  it through a new sidecar /api/settings route + iframe polling +
  conditional render) is >150 LOC across the build-script patch, the
  sidecar route, a transport mechanism, the client polling, and tests.
  Picked Option B per review guidance: remove the dead field from
  contracts.ts and the getter/setter from settings-store.ts, and defer
  the privacy toggle to FEA-1445 (filed before commit). Regression
  test in settings-migration.test.ts pins the field-and-method removal
  so a future merge cannot silently re-add a dead setting.
- [Finding 2 / M] Sidecar importer could clobber writeback 'api' mode.
  The sidecar's setSessionBillingMode UPDATE was
  COALESCE(billing_mode,'') != ?, so if the desktop sync-service had
  persisted 'api' (because the user has OPENAI_API_KEY) and the
  sidecar restarted, codex-import.js would overwrite 'api' →
  'codex_chatgpt_pro' until the next sync cycle restored it (~5s
  window where Sessions UI mis-buckets). Narrow the UPDATE to
  NOT IN (?, 'api', 'claude_max', 'claude_pro') so importers can
  still promote 'unknown' but never demote a deliberate non-default
  value. The exclusion list is sourced from a protectedModes array
  in build-agent-monitor.mjs (single source of truth — adding a new
  protected mode is a one-line array edit). All four importers
  (codex / cursor / copilot / opencode) updated to pass the
  additional placeholder bindings.
- [Finding 3 / L] claude_pro is a dead BillingMode entry today —
  detectClaudeBillingMode always returns claude_max for OAuth Claude
  because the credentials file does not distinguish Pro from Max.
  Per review guidance, leave it in the union (removing it would be a
  cloud-relay schema breaking change) and document why. Added a
  multi-line comment in billing-mode.ts explaining the three reasons
  it stays: reserved for a future signal, cloud-relay contract
  stability, and the new importer-clobber guard already protects it.
  Regression test in billing-mode-detector.test.ts pins (a) the type
  variant, (b) SUBSCRIPTION_MODES membership, and (c) the
  reserved-for-future-signal comment text so a "dead enum entry"
  cleanup pass cannot silently drop the variant.

Side effect of Finding 2: rebuilt apps/desktop/.generated/agent-
monitor/ (gitignored — only the build-script patch source is checked
in). The new prepared statement renders as
  UPDATE sessions SET billing_mode = ?
   WHERE id = ?
     AND COALESCE(billing_mode, '') NOT IN (?, ?, ?, ?)
verified in the regenerated server/db.js.

Filed follow-up FEA-1445 "Subscription cost privacy toggle — wire
showSubscriptionEquivalentCost through sidecar" for the Option-A
path. Slug: FEA-1445.

Bump apps/desktop version 0.15.97 -> 0.15.98.

Testing:
- pnpm -C apps/desktop typecheck: clean.
- pnpm -C apps/desktop lint: clean.
- pnpm -C apps/desktop test: 1860 pass / 0 fail (tsx, +5 new tests
  vs round 2 baseline 1855) and 104 pass / 0 fail (node --test).
  New tests:
    1. settings-migration.test.ts: showSubscriptionEquivalentCost is
       NOT in DEFAULT_DESKTOP_SETTINGS (deferred to FEA-1445).
    2. settings-migration.test.ts: SettingsStore has no
       get/setShowSubscriptionEquivalentCost method.
    3. billing-mode-importer-clobber-guard.test.ts (new file, 7
       tests): importer promotes 'unknown' → harness default; cannot
       clobber 'api', 'claude_max', 'claude_pro'; is a no-op when
       the row already carries the target mode; can still promote
       between importer defaults; build-script source pins the
       protected-mode exclusion list.
    4. billing-mode-detector.test.ts: claude_pro is still a valid
       BillingMode union variant (type-level pin).
    5. billing-mode-detector.test.ts: claude_pro remains in
       SUBSCRIPTION_MODES.
    6. billing-mode-detector.test.ts: billing-mode.ts comment still
       explains why claude_pro is reserved-for-future-use.

Risks:
- The protected-mode exclusion list lives in two places: the SQL
  patch in build-agent-monitor.mjs (renders the prepared statement)
  and the JS arg arrays in each importer (bound at .run() time).
  Adding a new protected mode requires both edits. The new clobber-
  guard test pins the build-script side; the importers will throw
  at runtime on a placeholder/arg-count mismatch.
- Option B (Finding 1) is a contract change to DesktopSettings —
  the field is gone. Per AGENTS.md "Breaking Changes", this is an
  internal contract (IPC, shipped atomically with the app), so no
  legacy migration is required. The settings file on disk simply
  loses the unused key on the next save; downgrade is the inverse
  — older app versions default the missing field to true.
- Finding 3 keeps claude_pro in the union. If a future
  detectClaudeBillingMode learns to distinguish tiers, the
  taxonomy is ready; if not, the variant stays dormant with no
  runtime cost.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@aeyeCEO
aeyeCEO requested a review from a team May 29, 2026 04:02
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@github-actions

Copy link
Copy Markdown

Compatibility Smoke Test Results ⚠️

Status: skipped
Electron SHA: 43d86c8a788d83fe487fbe0dfa63a64b52920149
Symphony Alpha SHA (last-known-good): 24741ed9be45cc761195d7a7b6613bd30cdfce84
Note: Skipped because the stage GitHub App credentials are not configured for this workflow run.

View Actions run

const ledgerTotals = sessions.reduce(
(acc, s) => {
const cost = s.cost ?? 0;
if (isApiMetered(s.billing_mode)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This rollup drops anything that isn't 'api' or a subscription mode, so an 'unknown' session lands in neither card. But down at line 462 that same row still renders its cost as a bare dollar amount, identical to an api row. 'unknown' is the schema default for every pre-migration row and for any spawn where detection fell through (no API key env, no creds file), so you'll routinely have rows showing '$3.00' in the column that aren't in the Metered API spend total. The two cards won't add up to what the user sees in the table. Either fold unknown into the api bucket or render it muted so it's clear it's uncounted.

// Existence-only check — never read or log the contents of this file. The
// OAuth token inside the credentials file must not appear in logs or
// telemetry; see CLAUDE.md "spawn|argv|env|secrets".
if (existsSync(expandHome(claudeCredentialsPath))) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This misses Bedrock/Vertex. A run with CLAUDE_CODE_USE_BEDROCK or CLAUDE_CODE_USE_VERTEX is metered through the cloud provider and has no ANTHROPIC_API_KEY, so if there's a stale ~/.claude/.credentials.json from a past OAuth login it gets tagged claude_max and disappears from the headline. That's the expensive direction to get wrong, real spend rendered as covered. The doc comment only calls out the Pro-vs-Max ambiguity, this case is worth handling too.

// canonical enum in src/shared/billing-mode.ts on the desktop side. Inlined
// here because the agent-monitor client is bundled separately from the
// desktop main TypeScript tree.
const SUBSCRIPTION_BILLING_MODES = new Set<string>([

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a hand copy of SUBSCRIPTION_MODES in billing-mode.ts with nothing tying them together. Add a 9th subscription mode there and the importers/detector will stamp it but this UI silently buckets it as uncounted, and no build gate catches the drift. The separate-bundle reason is fair, but worth at least a test that asserts the two sets match.

@thadeusb thadeusb left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ledger split is built well, the importer clobber guard and the v1/v2 contract handling especially. One real thing: 'unknown' sessions show a dollar cost in the table but land in neither rollup card, and 'unknown' is the default for every pre-migration row, so the cards won't reconcile with what's on screen. Couple smaller notes on detection inline.

@aeyeCEO

aeyeCEO commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by a clean single-PR rebuild on FEA-1431-genai-prices (off latest main).

The FEA-1434 two-ledger / per-session billing-mode architecture is being KEPT — only the pricing foundation underneath it is being rebuilt on @pydantic/genai-prices (PRD-414 v2). This work is being consolidated into one PR rather than three phases. Head commit archived locally as tag archive/pr-254-FEA-1434 for reference during the rebuild.

@aeyeCEO aeyeCEO closed this May 29, 2026
@aeyeCEO
aeyeCEO deleted the FEA-1434-billing-mode branch May 29, 2026 15:02
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants