opencode-goal-plugin 0.1.11 → 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 +16 -1
- package/CONTRIBUTING.md +5 -0
- package/README.md +4 -10
- package/package.json +2 -2
- package/src/goal-plugin.js +20 -12
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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
|
+
|
|
15
|
+
## 0.1.12 — 2026-06-08
|
|
16
|
+
|
|
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.
|
|
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.
|
|
19
|
+
|
|
5
20
|
## 0.1.11 — 2026-06-04
|
|
6
21
|
|
|
7
22
|
- Add `npm run smoke`, a package-export smoke test that exercises the `/goal` command hook without invoking a model.
|
|
@@ -86,4 +101,4 @@
|
|
|
86
101
|
|
|
87
102
|
## 0.1.0
|
|
88
103
|
|
|
89
|
-
- Initial experimental marker-based `/goal` plugin.
|
|
104
|
+
- Initial experimental marker-based `/goal` plugin.
|
package/CONTRIBUTING.md
CHANGED
|
@@ -17,6 +17,7 @@ npm test
|
|
|
17
17
|
npm run test:coverage
|
|
18
18
|
npm run smoke
|
|
19
19
|
npm run check
|
|
20
|
+
npm run smoke
|
|
20
21
|
npm run pack:check
|
|
21
22
|
```
|
|
22
23
|
|
|
@@ -46,6 +47,10 @@ Before publishing or tagging a release:
|
|
|
46
47
|
- perform at least one manual OpenCode smoke test if hook behavior changed
|
|
47
48
|
- refresh compatibility notes if the tested OpenCode surface changed
|
|
48
49
|
|
|
50
|
+
`npm run smoke` verifies the published package entrypoint and `/goal` command hook without
|
|
51
|
+
invoking a model. It does not replace a manual OpenCode smoke test after hook or command
|
|
52
|
+
behavior changes.
|
|
53
|
+
|
|
49
54
|
## Pull Requests
|
|
50
55
|
|
|
51
56
|
Keep pull requests focused. Include:
|
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
|
-
|
|
|
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
|
|
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
|
|
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>` |
|
|
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 |
|
|
@@ -226,12 +226,6 @@ Keep test files outside OpenCode's auto-loaded plugin directory — OpenCode wil
|
|
|
226
226
|
6. Verify `/goal status`, `/goal pause`, `/goal resume`, and `/goal clear` behave as expected.
|
|
227
227
|
7. If you changed hook payload handling or command behavior, repeat the smoke test against the exact OpenCode version and provider/backend combination you care about.
|
|
228
228
|
|
|
229
|
-
### Release checklist
|
|
230
|
-
|
|
231
|
-
- Confirm `npm test`, `npm run test:coverage`, `npm run check`, `npm run smoke`, and `npm run pack:check` all pass.
|
|
232
|
-
- Re-test against a real OpenCode install when touching command hooks, idle-event handling, or system-prompt transforms.
|
|
233
|
-
- Update compatibility notes and changelog entries when behavior or tested surfaces change.
|
|
234
|
-
|
|
235
229
|
## Development
|
|
236
230
|
|
|
237
231
|
```sh
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-goal-plugin",
|
|
3
|
-
"version": "0.1.
|
|
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
|
+
}
|
package/src/goal-plugin.js
CHANGED
|
@@ -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
|
-
`
|
|
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
|
-
`
|
|
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,14 +679,16 @@ 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()}
|
|
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(", ")}.` : ""
|
|
686
686
|
}
|
|
687
687
|
|
|
688
688
|
function escapeGoalText(text) {
|
|
689
|
-
|
|
689
|
+
// Escape every XML closing tag so user-supplied goal text cannot break the
|
|
690
|
+
// structural framing used in buildGoalBlock and buildContinueMessage.
|
|
691
|
+
return String(text).replaceAll("</", "<\\/")
|
|
690
692
|
}
|
|
691
693
|
|
|
692
694
|
function buildGoalBlock(goal) {
|
|
@@ -709,8 +711,8 @@ function buildContinueMessage(goal, { budgetWrapup = false } = {}) {
|
|
|
709
711
|
"<progress_budget>",
|
|
710
712
|
`auto_continues_used: ${goal.turnCount}`,
|
|
711
713
|
`auto_continues_remaining: ${remainingTurns}`,
|
|
712
|
-
`
|
|
713
|
-
`
|
|
714
|
+
`context_tokens_used: ${goal.totalTokens}`,
|
|
715
|
+
`context_tokens_remaining: ${remainingTokens}`,
|
|
714
716
|
`elapsed_seconds: ${elapsedSeconds}`,
|
|
715
717
|
"</progress_budget>",
|
|
716
718
|
"",
|
|
@@ -719,7 +721,7 @@ function buildContinueMessage(goal, { budgetWrapup = false } = {}) {
|
|
|
719
721
|
if (budgetWrapup) {
|
|
720
722
|
lines.push(
|
|
721
723
|
"<budget_wrapup>",
|
|
722
|
-
"This goal is near its
|
|
724
|
+
"This goal is near its context token limit. Finish the current step if it is small and safe.",
|
|
723
725
|
"Then write a concise handoff summary covering what is done, what remains, and the next concrete command or file to inspect.",
|
|
724
726
|
"Do not output [goal:complete] unless the goal is actually finished and verified.",
|
|
725
727
|
"After the handoff, stop.",
|
|
@@ -1028,7 +1030,7 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
|
|
|
1028
1030
|
pushHistory(
|
|
1029
1031
|
goal,
|
|
1030
1032
|
"set",
|
|
1031
|
-
`Goal created with limits: ${goal.options.maxTurns} auto-continues, ${Math.round(goal.options.maxDurationMs / 1000)}s, ${goal.options.maxTokens.toLocaleString()}
|
|
1033
|
+
`Goal created with limits: ${goal.options.maxTurns} auto-continues, ${Math.round(goal.options.maxDurationMs / 1000)}s, ${goal.options.maxTokens.toLocaleString()} context tokens.`,
|
|
1032
1034
|
)
|
|
1033
1035
|
|
|
1034
1036
|
cleanupGoal(sessionID)
|
|
@@ -1047,7 +1049,7 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
|
|
|
1047
1049
|
"",
|
|
1048
1050
|
`Limits: ${goal.options.maxTurns} auto-continues, ${Math.round(
|
|
1049
1051
|
goal.options.maxDurationMs / 1000,
|
|
1050
|
-
)}s, ${goal.options.maxTokens.toLocaleString()}
|
|
1052
|
+
)}s, ${goal.options.maxTokens.toLocaleString()} context tokens.`,
|
|
1051
1053
|
].join("\n"),
|
|
1052
1054
|
),
|
|
1053
1055
|
]
|
|
@@ -1070,7 +1072,13 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
|
|
|
1070
1072
|
const currentTokens = totalTokensForMessage(message)
|
|
1071
1073
|
const previousTokens = seenTokens.get(currentMessageID) || 0
|
|
1072
1074
|
if (currentTokens > previousTokens) {
|
|
1073
|
-
|
|
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)
|
|
1074
1082
|
seenTokens.set(currentMessageID, currentTokens)
|
|
1075
1083
|
goal.messageIDs.add(currentMessageID)
|
|
1076
1084
|
changed = true
|
|
@@ -1259,7 +1267,7 @@ export const GoalPlugin = async ({ client }, pluginOptions = {}) => {
|
|
|
1259
1267
|
activeGoalAfterPrompt,
|
|
1260
1268
|
budgetWrapup ? "budget-wrapup" : "auto-continue",
|
|
1261
1269
|
budgetWrapup
|
|
1262
|
-
? "Sent a final handoff request near the
|
|
1270
|
+
? "Sent a final handoff request near the context token budget."
|
|
1263
1271
|
: `Sent auto-continue prompt ${activeGoalAfterPrompt.turnCount}/${activeGoalAfterPrompt.options.maxTurns}.`,
|
|
1264
1272
|
)
|
|
1265
1273
|
}
|