opencode-goal-plugin 0.1.12 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,9 +2,19 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## 0.1.13 — 2026-06-11
6
+
7
+ > Fixes a significant token-tracking bug where the reported token count could be 5–10× higher than what OpenCode displays, making budgets appear exhausted far sooner than expected.
8
+
9
+ - **Fix token tracking to use context window size instead of cumulative API consumption.** Each `message.updated` event carries `input + output + reasoning` tokens where `input` already includes the full conversation context. Accumulating deltas across messages re-counted prior turns every time, inflating the total. The plugin now uses `Math.max` across all message updates so `totalTokens` reflects the peak context window size — matching what OpenCode reports.
10
+ - Rename `tracked_tokens_used` / `tracked_tokens_remaining` → `context_tokens_used` / `context_tokens_remaining` in continuation prompts.
11
+ - Rename `Tokens:` → `Context tokens:` in status and result displays.
12
+ - Rename `tracked token limit` / `tracked token budget` → `context token limit` / `context token budget` in all user-facing messages.
13
+ - Add regression test verifying that multi-message token tracking no longer accumulates across turns.
14
+
5
15
  ## 0.1.12 — 2026-06-08
6
16
 
7
- - Harden `escapeGoalText` to escape all XML closing tags (`</` → `<\/`) instead of only `</goal_objective>`, closing a prompt-injection path where user-supplied goal text could break structural framing in the continuation message.
17
+ - Harden `escapeGoalText` to escape all XML closing tags (`</` → `<\\/`) instead of only `</goal_objective>`, closing a prompt-injection path where user-supplied goal text could break structural framing in the continuation message.
8
18
  - Add unit tests for `outputTokensForMessage`, `budgetWrapupNeeded`, `getSessionID`, `stopReason`, `normalizeOptions` boundary inputs (zero, negative, NaN, null, `budgetWrapupRatio` at 0 and 1), and `escapeGoalText` covering all structural tags.
9
19
 
10
20
  ## 0.1.11 — 2026-06-04
@@ -91,4 +101,4 @@
91
101
 
92
102
  ## 0.1.0
93
103
 
94
- - Initial experimental marker-based `/goal` plugin.
104
+ - Initial experimental marker-based `/goal` plugin.
package/README.md CHANGED
@@ -110,15 +110,15 @@ Markers must appear on their own final line. The bracketed form is canonical, bu
110
110
  |---|---|
111
111
  | Auto-continue turns | 10 |
112
112
  | Max duration | 15 minutes |
113
- | Tracked tokens | 200,000 |
113
+ | Context tokens | 200,000 |
114
114
  | Min delay between continues | 1.5 seconds |
115
115
  | No-progress pause | < 50 output tokens on a stalled turn (after a 2-turn grace window) |
116
- | Budget wrap-up threshold | 80% of tracked token budget |
116
+ | Budget wrap-up threshold | 80% of context token budget |
117
117
  | Auto-continue failure pause | 3 consecutive prompt failures |
118
118
 
119
119
  **Effective turn count.** Each LLM turn on a real task typically takes 30–90 seconds. At that latency, raising `--max-minutes` is usually more useful than raising `--max-turns`. At 45 s/turn, the default 15-minute window gives roughly 15–20 turns of headroom before the turn limit becomes the binding brake.
120
120
 
121
- **Token budget.** The plugin tracks `input + output + reasoning` tokens across all session messages. In high-context sessions (large codebases, long conversation history), input overhead per turn can be substantial and the budget may be exhausted before the turn limit is reached. Treat it as a safety brake, not precise billing accounting.
121
+ **Token budget.** The plugin tracks the session's context window size (`input + output + reasoning` tokens on the latest message). This matches the token count that OpenCode displays, so the numbers should be consistent. When the context window reaches the `--max-tokens` limit, the plugin sends a wrap-up prompt and stops. In high-context sessions (large codebases, long conversation history), the context can grow quickly — treat the budget as a safety brake.
122
122
 
123
123
  **No-progress heuristic.** A low-output turn does not pause immediately anymore. The plugin pauses only after `noProgressTurnsBeforePause` consecutive *stalled* low-output turns — repeated turns with very little output and no meaningful change in the latest assistant checkpoint.
124
124
 
@@ -141,7 +141,7 @@ Override any limit for a single goal:
141
141
  | `--max-turns <n>` | Auto-continue turn limit |
142
142
  | `--max-minutes <n>` | Duration limit in minutes |
143
143
  | `--max-duration-ms <n>` | Duration limit in milliseconds |
144
- | `--max-tokens <n>` | Tracked token limit |
144
+ | `--max-tokens <n>` | Context token limit |
145
145
  | `--cooldown-ms <n>` | Minimum delay between continues |
146
146
  | `--no-progress-threshold <n>` | Output token floor before pausing |
147
147
  | `--no-progress-turns <n>` | Consecutive stalled low-output turns before pausing |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-goal-plugin",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Session-scoped /goal workflow for OpenCode.",
5
5
  "type": "module",
6
6
  "main": "./src/goal-plugin.js",
@@ -47,4 +47,4 @@
47
47
  "author": {
48
48
  "name": "willytop8"
49
49
  }
50
- }
50
+ }
@@ -142,7 +142,7 @@ function formatStatus(goal) {
142
142
  const lines = [
143
143
  `Active goal: ${goal.condition}`,
144
144
  `Auto-continues sent: ${goal.turnCount}/${goal.options.maxTurns}`,
145
- `Tokens: ${goal.totalTokens.toLocaleString()}/${goal.options.maxTokens.toLocaleString()}`,
145
+ `Context tokens: ${goal.totalTokens.toLocaleString()}/${goal.options.maxTokens.toLocaleString()}`,
146
146
  `Elapsed: ${elapsed}s/${Math.round(goal.options.maxDurationMs / 1000)}s`,
147
147
  `Last progress: ${lastProgress}`,
148
148
  `No-progress turns: ${goal.noProgressTurns}`,
@@ -168,7 +168,7 @@ function formatGoalResult(result) {
168
168
  `Last goal: ${result.condition}`,
169
169
  `State: ${result.state}`,
170
170
  `Auto-continues sent: ${result.turnCount}`,
171
- `Tokens: ${result.totalTokens.toLocaleString()}`,
171
+ `Context tokens: ${result.totalTokens.toLocaleString()}`,
172
172
  `Elapsed: ${elapsed}s`,
173
173
  `Last checkpoint: ${lastCheckpoint}`,
174
174
  `Last status: ${result.lastStatus || "No status recorded."}`,
@@ -198,7 +198,7 @@ function stopReason(goal) {
198
198
  if (Date.now() - goal.startedAt >= goal.options.maxDurationMs) {
199
199
  return `max duration reached (${Math.round(goal.options.maxDurationMs / 1000)}s)`
200
200
  }
201
- if (goal.totalTokens >= goal.options.maxTokens) return `max tokens reached (${goal.options.maxTokens})`
201
+ if (goal.totalTokens >= goal.options.maxTokens) return `max context tokens reached (${goal.options.maxTokens.toLocaleString()})`
202
202
  return null
203
203
  }
204
204
 
@@ -679,7 +679,7 @@ function buildLimitWarning(goal) {
679
679
  warnings.push(`${Math.max(0, Math.round(remainingMs / 1000))}s remaining`)
680
680
  }
681
681
  if (remainingTokens <= goal.options.warnTokensRemaining) {
682
- warnings.push(`${Math.max(0, remainingTokens).toLocaleString()} tracked token(s) remaining`)
682
+ warnings.push(`${Math.max(0, remainingTokens).toLocaleString()} context token(s) remaining`)
683
683
  }
684
684
 
685
685
  return warnings.length ? ` Limits are near: ${warnings.join(", ")}.` : ""
@@ -711,8 +711,8 @@ function buildContinueMessage(goal, { budgetWrapup = false } = {}) {
711
711
  "<progress_budget>",
712
712
  `auto_continues_used: ${goal.turnCount}`,
713
713
  `auto_continues_remaining: ${remainingTurns}`,
714
- `tracked_tokens_used: ${goal.totalTokens}`,
715
- `tracked_tokens_remaining: ${remainingTokens}`,
714
+ `context_tokens_used: ${goal.totalTokens}`,
715
+ `context_tokens_remaining: ${remainingTokens}`,
716
716
  `elapsed_seconds: ${elapsedSeconds}`,
717
717
  "</progress_budget>",
718
718
  "",
@@ -721,7 +721,7 @@ function buildContinueMessage(goal, { budgetWrapup = false } = {}) {
721
721
  if (budgetWrapup) {
722
722
  lines.push(
723
723
  "<budget_wrapup>",
724
- "This goal is near its tracked token limit. Finish the current step if it is small and safe.",
724
+ "This goal is near its context token limit. Finish the current step if it is small and safe.",
725
725
  "Then write a concise handoff summary covering what is done, what remains, and the next concrete command or file to inspect.",
726
726
  "Do not output [goal:complete] unless the goal is actually finished and verified.",
727
727
  "After the handoff, stop.",
@@ -1030,7 +1030,7 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
1030
1030
  pushHistory(
1031
1031
  goal,
1032
1032
  "set",
1033
- `Goal created with limits: ${goal.options.maxTurns} auto-continues, ${Math.round(goal.options.maxDurationMs / 1000)}s, ${goal.options.maxTokens.toLocaleString()} tracked tokens.`,
1033
+ `Goal created with limits: ${goal.options.maxTurns} auto-continues, ${Math.round(goal.options.maxDurationMs / 1000)}s, ${goal.options.maxTokens.toLocaleString()} context tokens.`,
1034
1034
  )
1035
1035
 
1036
1036
  cleanupGoal(sessionID)
@@ -1049,7 +1049,7 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
1049
1049
  "",
1050
1050
  `Limits: ${goal.options.maxTurns} auto-continues, ${Math.round(
1051
1051
  goal.options.maxDurationMs / 1000,
1052
- )}s, ${goal.options.maxTokens.toLocaleString()} tracked tokens.`,
1052
+ )}s, ${goal.options.maxTokens.toLocaleString()} context tokens.`,
1053
1053
  ].join("\n"),
1054
1054
  ),
1055
1055
  ]
@@ -1072,7 +1072,13 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
1072
1072
  const currentTokens = totalTokensForMessage(message)
1073
1073
  const previousTokens = seenTokens.get(currentMessageID) || 0
1074
1074
  if (currentTokens > previousTokens) {
1075
- goal.totalTokens += currentTokens - previousTokens
1075
+ // Track the context window size (peak input+output+reasoning),
1076
+ // not cumulative API token consumption. Each message's tokens
1077
+ // include the full conversation context, so accumulating deltas
1078
+ // across messages inflates the count by re-counting prior turns.
1079
+ // Using Math.max gives the current context size, matching what
1080
+ // OpenCode displays and making the budget check intuitive.
1081
+ goal.totalTokens = Math.max(goal.totalTokens, currentTokens)
1076
1082
  seenTokens.set(currentMessageID, currentTokens)
1077
1083
  goal.messageIDs.add(currentMessageID)
1078
1084
  changed = true
@@ -1261,7 +1267,7 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
1261
1267
  activeGoalAfterPrompt,
1262
1268
  budgetWrapup ? "budget-wrapup" : "auto-continue",
1263
1269
  budgetWrapup
1264
- ? "Sent a final handoff request near the tracked token budget."
1270
+ ? "Sent a final handoff request near the context token budget."
1265
1271
  : `Sent auto-continue prompt ${activeGoalAfterPrompt.turnCount}/${activeGoalAfterPrompt.options.maxTurns}.`,
1266
1272
  )
1267
1273
  }