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
- Run a session long enough to accumulate a few hundred thousand tokens (or more).
/goal some task (optionally with a token budget, e.g. 50,000).
- 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.
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
tokensUsedto the full session history total, so even a largetokenBudgettriggersbudgetLimitedseconds after creation.Environment
1.18.16(V1 stable, Windows)@prevalentware/opencode-goal-plugin0.1.31Repro steps
/goal some task(optionally with a token budget, e.g. 50,000)./goalorget_goal.Observed:
create_goalreturnstokensUsed: 0with correct budget.get_goalreportstokensUsed: 3015786(the whole session),status: budgetLimited, stop reasontoken budget reached (3015786/50000)— even though the goal was created seconds ago and no goal work has happened yet.Root cause
experimental.chat.messages.transformis invoked on every message assembly and calls:tokensFromMessages(messages)sums tokens over all messages in the session (output.messagesis the full message list), not just messages produced after goal creation.accountUsagethen overwrites the goal counter with that cumulative value:So
tokensUsedbecomes the session total, not the goal's consumption. Any budget smaller than the pre-goal session history tripsbudgetLimitedimmediately.Suggested fix
Record a one-time baseline (session cumulative count at the first observation after goal creation) and account the increment:
usageBaselinefield to the goal state (schema +normalizeGoal+ create-time init).accountUsage:I applied this patch locally (plugin loaded from a local path +
usageBaselineadded toGoalSchema/normalizeGoal/create init) and verified: goal staysactiveafter creation,tokensUsedstarts 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.