Skip to content

fix(agents): roll subagent token usage and cost into parent session - #10103

Closed
filipkujawa wants to merge 2 commits into
mainfrom
fix/subagent-usage-rollup
Closed

filipkujawa wants to merge 2 commits into
mainfrom
fix/subagent-usage-rollup

Conversation

@filipkujawa

Copy link
Copy Markdown
Collaborator

Subagents run in their own session, so their tokens and cost never reached the parent. Every delegating turn under-reported cost-per-outcome and slipped past per-session budgets.

This folds a finished subagent's usage/cost into the parent's accumulated_* totals at the run_subagent_task chokepoint (sync + async delegate). The parent's context window is left untouched, so compaction is unaffected. Accumulated totals now use an atomic SQL increment, so a background subagent rolling up concurrently with the parent's reply loop can't clobber it.

Existing cost/usage readers (ACP usage_update, CLI display) pick this up for free.

assisted by claude code

Subagents spawned via the delegate tool run in their own sessions, so their
token usage and cost never reached the parent session - under-reporting
cost-per-outcome and per-session budgets whenever delegation occurred.

Fold a finished subagent's lifetime usage/cost into the parent session's
accumulated_* totals at the run_subagent_task chokepoint (covers sync and
async delegate), without touching the parent's usage (context window) columns
so compaction triggers are unaffected. Accumulated totals are now incremented
via a single atomic SQL statement (SessionManager::add_accumulated_usage), and
update_session_metrics uses the same path, so a background subagent rolling up
concurrently with the parent's reply loop cannot clobber it.
@filipkujawa

Copy link
Copy Markdown
Collaborator Author

Ran the same delegating task on pre-fix vs post-fix binaries, then inspected sessions.db:

┌──────────────────┬──────────────────┐
│  parent acc_tok  │ subagent acc_tok │
├──────────────────┼──────────────────┤
│ pre-fix: 18,782  │ 5,918            │
├──────────────────┼──────────────────┤
│ post-fix: 24,774 │ 5,947            │
└──────────────────┴──────────────────┘

Pre-fix the subagent's ~5.9K tokens are missing from the parent (bug reproduced). Post-fix 24,774 − 5,947 = 18,827 ≈ pre-fix parent-own (18,782) → subagent spend now rolls up. Cost rolls up too (parent $0.0208, both sessions non-zero). Parent context window stayed ~9.4K in both, so compaction is unaffected.

@filipkujawa
filipkujawa requested a review from michaelneale June 30, 2026 05:17
Ok(())
}

async fn add_accumulated_usage(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

is this for migration from old?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

No, not a migration. It adds a delta to a session's accumulated_usage/accumulated_cost., called on every usage update (and again when a subagent's totals roll up into its parent). The COALESCE bits aren't backfilling old rows- the columns are nullable (a fresh session starts with NULL accumulated tokens), so without them NULL + delta would evaluate to NULL and wipe the total on first increment

@michaelneale

Copy link
Copy Markdown
Collaborator

nice @filipkujawa - would it be possible to make it a little more atomic so when it updates it is one SQL call when it can. Fairly minor thing but wondered if worth a try.

Something like the following:

one UPDATE sessions ... that both sets current context usage and increments accumulated usage/cost:

  UPDATE sessions SET
    schedule_id = ?,
    total_tokens = ?,
    input_tokens = ?,
    output_tokens = ?,
    cache_read_tokens = ?,
    cache_write_tokens = ?,

    accumulated_total_tokens =
      COALESCE(accumulated_total_tokens, 0) + COALESCE(?, 0),
    accumulated_input_tokens =
      COALESCE(accumulated_input_tokens, 0) + COALESCE(?, 0),
    accumulated_output_tokens =
      COALESCE(accumulated_output_tokens, 0) + COALESCE(?, 0),
    accumulated_cache_read_tokens =
      COALESCE(accumulated_cache_read_tokens, 0) + COALESCE(?, 0),
    accumulated_cache_write_tokens =
      COALESCE(accumulated_cache_write_tokens, 0) + COALESCE(?, 0),

    accumulated_cost = CASE
      WHEN ? IS NULL THEN accumulated_cost
      ELSE COALESCE(accumulated_cost, 0) + ?
    END,

    updated_at = datetime('now')
  WHERE id = ?

  Binding order would be:

  .bind(schedule_id)
  .bind(current_usage.total_tokens)
  .bind(current_usage.input_tokens)
  .bind(current_usage.output_tokens)
  .bind(current_usage.cache_read_input_tokens)
  .bind(current_usage.cache_write_input_tokens)
  .bind(accumulated_delta.total_tokens)
  .bind(accumulated_delta.input_tokens)
  .bind(accumulated_delta.output_tokens)
  .bind(accumulated_delta.cache_read_input_tokens)
  .bind(accumulated_delta.cache_write_input_tokens)
  .bind(cost_delta)
  .bind(cost_delta)
  .bind(session_id)


crates/goose/src/session/session_manager.rs:1577

  Specifically, add a new SessionStorage::update_usage_metrics(...) method near the PR’s new
  SessionStorage::add_accumulated_usage(...).

  Then expose it with a SessionManager::update_usage_metrics(...) wrapper near:

  crates/goose/src/session/session_manager.rs:395

  Finally call it from:

  crates/goose/src/agents/reply_parts.rs:545

  replacing the two current calls:

  manager.update(...).usage(...).apply().await?;
  manager.add_accumulated_usage(...).await?;

Address review: replace the two-statement parent metrics update (set usage +
add_accumulated_usage) with one SessionManager::update_usage_metrics that sets
the context-window columns and increments the accumulated totals/cost in a
single atomic UPDATE. add_accumulated_usage remains for the subagent roll-up,
which only touches accumulated columns.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e4bd435dec

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +54 to +56
let result = get_agent_messages(params).await;

roll_up_usage_to_parent(&session_manager, &parent_session_id, &subagent_session_id).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make subagent usage roll-up abort-safe

Because the roll-up is sequenced after awaiting the entire subagent run, an async delegate that is cancelled and then force-aborted by handle_load_task_result after its 5-second grace period drops this future while it is still inside get_agent_messages(params).await, so execution never reaches the roll-up call. Any usage already persisted in the subagent session before that abort remains absent from the parent, preserving under-reporting for stuck/cancelled background delegates; move the roll-up to an abort-safe cleanup path or have the abort path add the persisted subagent totals.

Useful? React with 👍 / 👎.

@filipkujawa

Copy link
Copy Markdown
Collaborator Author

superseded-by #10172

@filipkujawa filipkujawa closed this Jul 1, 2026
88plug added a commit to 88plug/goose-plus that referenced this pull request Jul 4, 2026
Subagents spawned via delegate run in their own session, so their token
usage and cost never reached the parent - under-reporting cost-per-outcome
and per-session budgets whenever delegation occurred.

Folds a finished subagent's lifetime usage/cost into the parent's
accumulated_* totals at the run_subagent_task chokepoint (covers sync and
async delegate), without touching the parent's usage (context window)
columns so compaction triggers are unaffected. Both the reply loop's own
usage update and a concurrent subagent roll-up now go through single atomic
UPDATE statements (SessionManager::update_usage_metrics /
add_accumulated_usage), so neither can clobber the other - verified via a
50-concurrent-writer regression test.

Adapted from a closed upstream PR (aaif-goose/goose#10103, closed as
superseded by a much larger in-progress usage-ledger redesign that hasn't
landed) - the underlying fix is sound and independently useful regardless
of whether that redesign ever ships.
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.

3 participants