Skip to content

Token accounting counts entire session history, goal instantly goes budgetLimited #22

Description

@xiaoyun172

Summary

Token accounting uses the session-wide cumulative token count instead of the increment since the goal was created. In a long-running session, creating a goal instantly sets tokensUsed to the full session history total, so even a large tokenBudget triggers budgetLimited seconds after creation.

Environment

  • OpenCode: 1.18.16 (V1 stable, Windows)
  • Plugin: @prevalentware/opencode-goal-plugin 0.1.31
  • Session history at goal creation: ~3,015,786 tokens

Repro steps

  1. Run a session long enough to accumulate a few hundred thousand tokens (or more).
  2. /goal some task (optionally with a token budget, e.g. 50,000).
  3. Immediately check state with /goal or get_goal.

Observed:

  • create_goal returns tokensUsed: 0 with correct budget.
  • After the first message transform, get_goal reports tokensUsed: 3015786 (the whole session), status: budgetLimited, stop reason token budget reached (3015786/50000) — even though the goal was created seconds ago and no goal work has happened yet.

Root cause

experimental.chat.messages.transform is invoked on every message assembly and calls:

await accountUsage(sessionID, tokensFromMessages(output.messages));

tokensFromMessages(messages) sums tokens over all messages in the session (output.messages is the full message list), not just messages produced after goal creation.

accountUsage then overwrites the goal counter with that cumulative value:

goal.tokensUsed = Math.max(goal.tokensUsed, Math.max(0, Math.ceil(tokensUsed)));

So tokensUsed becomes the session total, not the goal's consumption. Any budget smaller than the pre-goal session history trips budgetLimited immediately.

Suggested fix

Record a one-time baseline (session cumulative count at the first observation after goal creation) and account the increment:

  • Add an optional usageBaseline field to the goal state (schema + normalizeGoal + create-time init).
  • In accountUsage:
if (typeof tokensUsed === "number" && Number.isFinite(tokensUsed)) {
  const current = Math.max(0, Math.ceil(tokensUsed));
  if (goal.usageBaseline == null) {
    goal.usageBaseline = current; // snapshot session cumulative at goal start
    goal.tokensUsed = 0;
  } else {
    goal.tokensUsed = Math.max(goal.tokensUsed, current - goal.usageBaseline);
  }
}

I applied this patch locally (plugin loaded from a local path + usageBaseline added to GoalSchema/normalizeGoal/create init) and verified: goal stays active after creation, tokensUsed starts at 0 and only grows by the delta, budget no longer trips on pre-goal session history.

Secondary observation (lower priority)

When exact usage metadata is unavailable, the estimator re-sums the full message list every transform, so the "delta" can jump by tens of thousands of tokens in one turn in very long sessions (observed +222k in ~5s). Budget values in long sessions should probably be sized generously, or the estimator could be replaced by a cheaper delta-friendly metric.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions