Repository navigation
Releases: salitaba/token-norm
Release list
v0.11.0
Added
doctorcommand.node scripts/install-local.mjs doctorruns nine checks
against a live install: Node >= 22,opencode --version, the package bundle
being present, the sha256 of the installed plugin againstdist/plugin.js,
the audit script being readable, the settings (imported fromdist/config.js
viatakeConfigDiagnostics()),python3, the OpenCode database being
readable, and duplicate plugin registration inopencode.json. It exits 1
only on afail; awarnis reported and still exits 0, so it can gate a
script without failing on the merely unusual.--dry-runoninstallanduninstall. Prints the file operations that
would run and touches nothing. The preview and the real run read the same
plan(), so the two cannot drift. The argv parse was rewritten to filter
--flags rather than readingprocess.argv[2], which is what makes
uninstall --dry-runwork rather than being read as an unknown command.
Documentation
- The
token_norm_statuspayload is declared a public interface. Stable
within a major version,peak >= currentalways, and reads are
side-effect-free — stated indocs/how-it-works.mdabove the payload example
and in the README. No field changed; this documents the existing contract. docs/install.mdshows a--dry-runexample and replaces the "check it
loaded" section with annotateddoctoroutput, explaining what the sha256
and settings checks actually prove.
Internal
- 29 new tests.
test/contract.test.tsis new and asserts behavioral
contracts through the plugin hooks: a throwing audit, a throwing toast, an
async-rejecting toast and an exploding provider all leave the tool call
intact; compaction recovery dropscurrenttoHEALTHYwhilepeakholds
PRESSURE; status reads never latch the peak; block mode keeps the handoff
and cheap tools open even at 50000x over limit; and pressure alone never
blocks.test/install-local.test.tsgrew to 15 tests covering the new
command surface, including exit codes.
Provenance
Built from 8a483717fadc1c026d52063b8a22cd09d050e54c.
| artifact | sha256 |
|---|---|
dist/plugin.js |
4bb0bf8057f0f70622143119549cf026f973f32ef898a1a8c08e6396e9d1f7c0 |
scripts/usage-audit.py |
ad5d1c4e18eac2c73dfc7267ff6c8a1f553783d7f62cd9d32d54f25f2ffa0c73 |
Verify an installed copy:
shasum -a 256 ~/.config/opencode/plugins/opencode-token-norm.js
shasum -a 256 ~/.config/opencode/scripts/usage-audit.pyv0.10.0
Added
token_norm_statusreportspolicy.current,policy.peakand
policy.driver.currentis severity at this instant and may fall;
peakis the session high-water mark and never does;drivernames the axis
(calls,budget,context) behindcurrent. Additive: the top-level
statefield remains, now documented as a deprecated alias ofpolicy.peak,
andrecommendationis still derived from the peak. The snapshot enforces
peak >= currentitself, so a stale stored level cannot produce an
inconsistent payload.- Executable invariants for the policy machine. Idempotence,
BLOCKEDonly
underblockmode with a hard limit exceeded (and always, then),
HANDOFF_RECOMMENDEDonly underhandoffmode with pressure and an armed
pause (and always, then), observe/warn measuring identically, state never
below the axis max, and at most one rendered block with exactly one header for
every subset of due sections — each checked over the whole reachable input
space rather than on hand-picked examples.
Fixed
- The driver axis at
HEALTHYis no longer reported ascontext. The
most-specific-first rule matched every axis when nothing was wrong, so a
healthy session — including one whose context window could not be resolved at
all — named context as its driver. It now attributes to the calls axis, which
always exists.
Documentation
- README leads with the positioning, the checkpoint ladder, and a
before/after; adds an enforcement-mode table, a Node/OpenCode/plugin
compatibility row, and splits evidence into observed / benchmark / hypothesis
(including the retracted wall-time finding: +2.6 s, p = 0.83). docs/how-it-works.mddocuments the status payload field by field, the
currentvspeakdistinction, weighted vs raw call counts (why
calls 37andbudget.toolCalls: 52are both right), the
measurement → policy → rendering pipeline, and a call-by-call session
walkthrough.- Process-local state is stated as an explicit guarantee in the README, and
effective fresh tokensis called out as cost-weighted input rather than a
provider token total.
Provenance
Built from ef2f9b1dfe865c2c4be06964848694c4a3287791.
| artifact | sha256 |
|---|---|
dist/plugin.js |
4bb0bf8057f0f70622143119549cf026f973f32ef898a1a8c08e6396e9d1f7c0 |
scripts/usage-audit.py |
ad5d1c4e18eac2c73dfc7267ff6c8a1f553783d7f62cd9d32d54f25f2ffa0c73 |
Verify an installed copy:
shasum -a 256 ~/.config/opencode/plugins/opencode-token-norm.js
shasum -a 256 ~/.config/opencode/scripts/usage-audit.pyv0.9.0
Added
- Weighted tool-call budgets.
TOKEN_NORM_TOOL_WEIGHTSand
TOKEN_NORM_PHASE_WEIGHTSacceptname=weightcomma lists (positive numbers;
unlisted names weigh 1), so abashcall can cost more than aread, or a
planning-mode call less than a build-mode one. Phase is the latest assistant
message mode, resolved per step throughmessageIDwith a session-latest
fallback. A malformed entry is dropped, weighs 1, and reports into the config
diagnostics.
Changed
TOKEN_NORM_MAX_TOOL_CALLSnow compares weighted calls (tool weight ×
phase weight; cheap tools still contribute nothing) and the metric is labeled
"Weighted tool calls". Raw call counts continue to drive the announce, audit,
and task-boundary thresholds and the policy call-count axis, so weights can
never delay or advance a reminder.
Provenance
Built from 6cb11a3ca8ccb024be5754e811626c294ebbc882.
| artifact | sha256 |
|---|---|
dist/plugin.js |
5517525e2ea43979a298b575cfe6314894cb3042e561765ef8dddae71e0f7b94 |
scripts/usage-audit.py |
ad5d1c4e18eac2c73dfc7267ff6c8a1f553783d7f62cd9d32d54f25f2ffa0c73 |
Verify an installed copy:
shasum -a 256 ~/.config/opencode/plugins/opencode-token-norm.js
shasum -a 256 ~/.config/opencode/scripts/usage-audit.pyv0.8.0
Fixed
-
Benchmark harness no longer charges the treatment arm for a cold plugin
install. Loading a plugin makes opencode npm-install@opencode-ai/plugin
intoXDG_CONFIG_HOME/opencode, andbench/run.mjsgives every run a fresh
HOMEand config dir, so that install ran from scratch on every run: 66.7 s
measured viaopencode debug config. Both arms install, but only on treatment
does it block startup, where it was published as plugin wall-time overhead.
Seeding the npm cache alone still costs 9.2 s, so the harness now resolves the
tree once per invocation and hardlinksnode_modules+package.json+
package-lock.jsoninto each run's config dir for both arms before
opencodestarts. Verified: the plugin-load window drops to 0.213 s,
inside the baseline range. The re-run is complete
(power-string-sweep-v2.jsonl, 40/40, $0.2923): window 0.158 s vs 0.278 s
per arm, wall difference +2.6 s, p = 0.83 — was +27.4 s, p = 0.018. No
wall-time separation remains. -
session.deletedevents missinginfo.id, and user messages missing
sessionID, no longer throw into the event handler's catch-all (which also
skipped handoff arming and the task-boundary check for that event).
Changed
-
Benchmark wall-time claim corrected: harness artifact, not plugin cost.
docs/benchmark.mdreported the power study's only significant separation as
wall time, +27.4 s per run in the treatment arm (p = 0.018). A paired per-run
decomposition of all 40 runs (from each record's preservedrun_dir— the
session DB for message/tool timings, the opencode log for phase boundaries)
shows the gap is entirely startup, and inside startup it is one window:
plugin loading, baseline mean 0.25 s vs treatment 23.22 s. Cause is the
harness's per-runHOMEisolation — only the treatment arm loads a plugin, so
only it blocks on the plugin dependency install during startup (npm, not bun;
see Fixed above). Plugin runtime is not involved: importingdist/plugin.js
costs 0.24 s under bun, the tool hook 0.004 ms, the audit spawn ~45 ms.
Adjusted for that window the difference is +4.45 s, paired positive in 13 of
20 rather than 17 of 20 — superseded by the fixed-harness re-run at
+2.6 s, p = 0.83. -
README first screen. The install command moved from line 69 to line 19.
Badges cut from seven to three (version, test, license), the hand-written
table of contents dropped in favour of GitHub's own heading outline, the
duplicated handoff statistic removed (the same numbers are in
## Observed behavior), the demo caption folded into the intro paragraph, and
the before/after diagram relocated todocs/how-it-works.md, next to the
architecture diagram it belongs with. No content was lost, only moved or
de-duplicated. -
Unified policy state machine. Severity is now computed once, in
src/budget/policy.ts, as a monotone state machine
(HEALTHY<ATTENTION<PRESSURE<HANDOFF_RECOMMENDED<BLOCKED)
over three axes -- call count, configured budgets, and the context window.
The mode is applied after the max, not inside an axis. Previously eight
independent decision sites in the tool hook each owned a threshold and a
latch, which allowed three separate<system-reminder>blocks and two toasts
on a single tool call. The plugin now emits at most one block per tool
call, with a state header naming the driving axis and a fixed section
order: boundary, announce, audit, budget, handoff. At most one toast, titled
by state. -
Reminders no longer suppress each other. The early returns after the
task-boundary and announce reminders are gone. Deferral was a side effect of
those returns rather than a design goal: it spread co-occurring thresholds
across separate tool calls and skipped the budget check entirely on those
calls. Everything due on a call now lands together in the one block. -
token_norm_statusanswers from the enforcement machine. Therecommend()
ladder insrc/status.ts, which re-derived severity from
(pressured, exceeded, mode), is replaced by a direct state map. The snapshot
gains astatefield alongsiderecommendation. Two visible contract
changes: a session at or pastTOKEN_NORM_ANNOUNCE_ATwith no budget
pressure now reportswarnwhere it reportedcontinue; and in handoff mode,
pressure without a pause now reportswarnrather thanhandoff, matching
what the plugin would actually do -- a handoff has always required a natural
pause as well. -
TOKEN_NORM_SETTLE_MSandTOKEN_NORM_SWITCH_WAIT_MSare read inconfig.ts
like every other setting, so they are validated and diagnosed too. -
The opencode client and event payloads are no longer typed as
any.src/host.ts
declares narrow interfaces covering only the host fields this plugin actually
reads, so a renamed or removed host method now fails at build time instead of
disappearing into a runtime catch.
Added
- Release provenance. Every build writes
dist/provenance.jsonrecording the
gitShait was built from, whether the working tree was dirty, and the sha256
of both files that leave npm's integrity story once installed --
dist/plugin.jsandscripts/usage-audit.py. The installer copies those two
into~/.config/opencode, where nothing else attests to them and the audit
script is then executed. The digests ship in the tarball, are rendered as a
table in the GitHub release notes, andprovenance.jsonis attached as a
release asset, so an installed copy can be checked withshasum -a 256.
test/packaging.test.tsrehashes the packed bytes, so a stale or wrong
digest fails the build rather than shipping. - Config diagnostics: a malformed or misspelled
TOKEN_NORM_*setting is
reported once at load (log line plus a toast) instead of silently falling back
to its default. Covers non-numeric or non-positive thresholds and budgets, an
out-of-rangeTOKEN_NORM_CONTEXT_WARN, a kill switch set to anything but0
or1, an emptyTOKEN_NORM_CHEAP_TOOLS, and unrecognizedTOKEN_NORM_*
keys. Values still fall back rather than failing the session. - Handoff integration-test matrix: child-session events, a late
session.created,
the settle floor,appendPrompt/submitPromptfailures, and concurrent
handoffs. - Benchmark statistics: per-cell summaries now carry
n,median,sd, and a
seeded bootstrap 95% CI for the mean, plus a per-task treatment-vs-baseline
contrast using a two-sided permutation test. Cells below five runs per arm are
flaggedunderpoweredso a small batch cannot be read as a result. bench/run.mjs --resummarize <jsonl>recomputes a summary from existing run
records with no paid runs and without writing to the input file.- Power study (
bench/results/power-string-sweep.jsonl, 40 paid runs, n=20 per
arm) refuting the earlier N=210-string-sweephypothesis: tool calls differ
by -3.05 (p = 0.65) and both arms are bimodal. Wall time is the one metric that
separates, +27.4 s in the treatment arm (p = 0.018), cause unattributed.
Provenance
Built from 60d4ef36b0b31b7d8b443ac4afac87e109f0886c.
| artifact | sha256 |
|---|---|
dist/plugin.js |
455e2c600b64ccd3612cdaf0c4c6c16c8f9c459360e0567e395f1a240f8a7343 |
scripts/usage-audit.py |
ad5d1c4e18eac2c73dfc7267ff6c8a1f553783d7f62cd9d32d54f25f2ffa0c73 |
Verify an installed copy:
shasum -a 256 ~/.config/opencode/plugins/opencode-token-norm.js
shasum -a 256 ~/.config/opencode/scripts/usage-audit.pyv0.7.2
Fixed
- Deleted-session suppression survives eviction: an id evicted from the bounded
deleted window could be resurrected by a latestep-finish, re-counting spend
already folded into its parent. Deleted ids are now also recorded in a
fixed-size filter with no false negatives, and a live-entry check lets a
restartedsession.createdthrough. Adds the seeded usage-ledger invariant
suite (test/budget-state.invariants.test.ts) covering the eviction
regression.
v0.7.1
Added
- Event-order tests for the budget plugin (
test/event-order.test.ts): reminder
priority within a single call (boundary > announce > audit, each suppressed
reminder deferred to the next call), boundary injection points across an
interleaved event stream, counter and step-usage alignment through
token_norm_status, and handoff arming order (a pause before pressure does
not arm; a pause after pressure does).
v0.7.0
Added
- Medium and long benchmark fixtures (
03-many-bugs,04-long-sweep), per-call
timing metrics (gap,exec) in the harness, and the pilot 2 write-up with a
provider-free latency microbench indocs/benchmark.md.
Changed
token_norm_statusnow labels its scopes:budget(toolCalls,cost,
effectiveTokens) rolls up the session tree, whilesession.contextis the
current session's window. The JSON shape changed accordingly:
session.toolCallsmoved tobudget.toolCalls, and both groups carry a
scopefield, so the tree rollup is no longer ambiguous.- The status provider is injected per plugin instance via
createStatusTool(provider)instead of a module-global registry, so two
plugin instances in one process can no longer shadow each other's reader.
v0.6.0
Added
token_norm_status: a read-only tool that reports the session's tool calls,
context usage, cost and effective tokens against the configured limits, plus
thecontinue | warn | handoff | blockrecommendation enforcement would give
right now. It reads the same accumulators as the reminders, so the two cannot
disagree; unknown sessions report zeros, and the tool is absent when the
budget plugin is not loaded.- Benchmark harness (
bench/run.mjs) with two fixture tasks, plus the pilot
methodology and results write-up indocs/benchmark.md.
v0.5.3
Fixed
- Child-session rollups no longer stop at a fixed 20-level depth:
rootOf
walks parent links to the true root, with a cycle guard instead of a cap. session.deletedaggregates instead of tombstoning: the deleted session's
totals fold into its parent and the entry is dropped, so a long-lived server
keeps no ledger entry per deleted session. Live children reparent to the
grandparent, and a bounded window of deleted ids still swallows late events.- Documented that the model context-limit cache lives for the process: provider
config changes need a restart orTOKEN_NORM_CONTEXT_LIMIT.
v0.5.2
Fixed
handoffdisarms its session-switch waiter on every exit path, not just the
timeout: a thrownexecuteCommandor any other error now clears it via
try/finally, so a latesession.createdcan no longer satisfy a stale
resolver and clear the next handoff's waiter before its own session exists.- README no longer claims the plugin "adds no advice"; the accurate claim is
that it does not rely on agent-authored advice as its enforcement mechanism.