@bglocation/tune-context 2.0.1 → 2.1.0

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.
@@ -10,7 +10,7 @@
10
10
  "name": "tune-context",
11
11
  "source": "./",
12
12
  "description": "Context-efficiency configurator for Claude Code: doctrine skills (token-efficiency, caveman, cdd, phase-workflow) plus a tune-context configurator that scaffolds a lean CLAUDE.md, subagents, settings and hooks.",
13
- "version": "2.0.1",
13
+ "version": "2.1.0",
14
14
  "author": {
15
15
  "name": "Szymon Walczak",
16
16
  "url": "https://bglocation.dev"
@@ -2,7 +2,7 @@
2
2
  "name": "tune-context",
3
3
  "displayName": "tune-context",
4
4
  "description": "Context-efficiency configurator for Claude Code: doctrine skills (token-efficiency, caveman, cdd, phase-workflow) plus a tune-context configurator that scaffolds a lean CLAUDE.md, subagents, settings and hooks.",
5
- "version": "2.0.1",
5
+ "version": "2.1.0",
6
6
  "author": {
7
7
  "name": "Szymon Walczak",
8
8
  "url": "https://bglocation.dev"
package/CHANGELOG.md CHANGED
@@ -5,6 +5,48 @@ All notable changes to `@bglocation/tune-context` are documented here.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.1.0] — 2026-07-26
9
+
10
+ ### Changed
11
+
12
+ - **The context reminder no longer goes silent after a badly-timed crossing.**
13
+ It now fires a second and final time at **twice** the threshold (240k by
14
+ default, or 2× `TUNE_CONTEXT_REMIND_TOKENS`). The reminder tells Claude to stay
15
+ quiet mid-task — correctly, since only the model can see whether a refactor is
16
+ half-finished — but fire-once was consumed the moment the *model* read it, and
17
+ re-arming required a compaction. Measured on one real session: it fired at
18
+ ~166k, Claude stayed silent mid-task, and nothing spoke again until ~346k, 2.9×
19
+ the threshold. There is deliberately no third notice: this is the only hook
20
+ that *adds* tokens to the context it exists to shrink, so the cost stays
21
+ bounded at one extra note per cycle.
22
+ - The text sent to the model now states plainly that the user cannot see the
23
+ reminder, so silence means they never learn about it.
24
+
25
+ ### Added
26
+
27
+ - A `systemMessage` alongside the existing `additionalContext`, carrying the size
28
+ and `/compact` in one line. **Best-effort only** — see below.
29
+
30
+ ### Notes on measurement
31
+
32
+ The intended fix was to show the user a line directly, removing the model's
33
+ monopoly on whether they hear about a heavy session at all. A two-marker probe
34
+ settled what the docs could not:
35
+
36
+ - `systemMessage` does **not** render for `UserPromptSubmit` (measured in the
37
+ VSCode extension), despite the settings schema promising "Display a message to
38
+ the user (all hooks)". The probe's `additionalContext` marker did arrive, which
39
+ is what makes this a clean negative rather than a probe that failed to load.
40
+ - `systemMessage` does **not** enter the model's context, so it costs no tokens.
41
+
42
+ It is therefore emitted but depended on by nothing, and other clients were not
43
+ measured. The escalation above is what actually closes the gap.
44
+
45
+ The same measurement caught a change that would have made things worse: the
46
+ planned model-facing text said "the user has already been shown this, do not
47
+ repeat it". Shipped unmeasured, it would have suppressed the only channel that
48
+ works.
49
+
8
50
  ## [2.0.1] — 2026-07-26
9
51
 
10
52
  ### Fixed
package/README.md CHANGED
@@ -170,15 +170,21 @@ supplies the missing measurement: it reads the tail of the session transcript,
170
170
  sums the tokens actually re-sent, and once that passes **120,000** it injects a
171
171
  single short note suggesting `/compact`.
172
172
 
173
- - **Once per crossing, not per prompt.** The note itself costs tokens on the turn
174
- it appears in, so it re-arms only after a compaction-sized drop. Measured on a
175
- 12.6-hour session: 4 reminders total.
176
- - **The model picks the moment.** The reminder tells Claude to stay quiet
177
- mid-task and raise it when the thread closes the hook can measure size, but
178
- only the model can see whether you're mid-refactor.
173
+ - **The note goes to Claude, not to your screen.** Hooks can only reach you
174
+ through the model a `systemMessage` on this event was measured and does not
175
+ render so what you actually see is Claude bringing it up in its reply.
176
+ - **The model picks the moment.** The note tells Claude to stay quiet mid-task
177
+ and raise it at the first natural break: the hook can measure size, but only
178
+ the model can see whether you're mid-refactor.
179
+ - **Twice per crossing at most.** Once at the threshold, once more at **240,000**
180
+ if the context keeps growing — because a note that lands mid-task can be
181
+ correctly absorbed and then forgotten. Measured on one session: it fired at
182
+ 166k, Claude stayed silent, and nothing spoke again until 346k. The second
183
+ notice halves that blind spot; there is deliberately no third.
179
184
  - **Raise it for a large context window:** `export TUNE_CONTEXT_REMIND_TOKENS=400000`.
180
185
  120k is ~60% of a standard 200k window; the transcript doesn't record the window
181
- size, so this can't be auto-scaled.
186
+ size, so this can't be auto-scaled. Both levels scale with the override — the
187
+ second notice is always at twice the threshold.
182
188
  - **Turn it off:** delete the `UserPromptSubmit` entry from `settings.json`.
183
189
 
184
190
  Depth lives in the `phase-workflow` and `token-efficiency` skills (both installed
@@ -137,31 +137,94 @@ function pruneState(state, now) {
137
137
  return state;
138
138
  }
139
139
 
140
+ // TASK-080 fallback, chosen by the PO after measurement killed the preferred
141
+ // design (see userNotice()): a SECOND and final notice at twice the threshold.
142
+ //
143
+ // The problem it solves is measured, not hypothetical. Fire-once is consumed
144
+ // when the model reads the reminder, and the reminder itself tells the model to
145
+ // stay quiet mid-task — correctly, since only it can see whether a refactor is
146
+ // half-finished. In session c9329adb that combination cost the user everything
147
+ // between ~166k and ~346k: one firing, silently absorbed, and no re-arm until a
148
+ // compaction that nobody had suggested.
149
+ //
150
+ // Why 2x and not "every +N tokens": periodic nagging would break the rule this
151
+ // hook lives under — it is the only hook that ADDS tokens to the context it
152
+ // exists to shrink. One extra notice per cycle is a bounded, one-off cost that
153
+ // halves the worst-case blind spot; an unbounded series is the feature eating
154
+ // itself. Deliberately NOT a third level.
155
+ const ESCALATION_MULTIPLIER = 2;
156
+
157
+ /** How many notices this session has earned at `total`: 0, 1 or 2. */
158
+ function levelFor(total, threshold) {
159
+ if (total >= threshold * ESCALATION_MULTIPLIER) return 2;
160
+ if (total >= threshold) return 1;
161
+ return 0;
162
+ }
163
+
140
164
  /**
141
165
  * Pure decision: given the previous reading for this session and the current
142
166
  * one, should a reminder fire, and what is the session's next state?
143
167
  *
144
- * Fires ONCE per crossing, not on every prompt above the threshold. A reminder
145
- * costs tokens on the very prompt it appears in, so a context-efficiency tool
146
- * that nagged every turn would be spending the thing it claims to save.
168
+ * Fires at most TWICE per crossing (threshold, then 2x threshold), never on
169
+ * every prompt above the line. A reminder costs tokens on the very prompt it
170
+ * appears in, so a context-efficiency tool that nagged every turn would be
171
+ * spending the thing it claims to save.
147
172
  */
148
173
  export function decide(prev, total, threshold) {
149
- let fired = prev?.fired === true;
174
+ // `fired: true` is how state written before TASK-080 recorded a single
175
+ // firing; read it as level 1 so an in-flight session whose hook is upgraded
176
+ // mid-run is not re-notified from scratch.
177
+ let reached = typeof prev?.levels === 'number' ? prev.levels : prev?.fired === true ? 1 : 0;
150
178
  // A large drop means the context was compacted/cleared: arm again so the
151
179
  // NEXT time it grows past the threshold the user hears about it.
152
- if (prev && typeof prev.last === 'number' && total < prev.last * REARM_DROP_RATIO) fired = false;
180
+ if (prev && typeof prev.last === 'number' && total < prev.last * REARM_DROP_RATIO) reached = 0;
181
+
182
+ const level = levelFor(total, threshold);
183
+ const remind = level > reached;
184
+ return { remind, escalated: remind && level === 2, next: { last: total, levels: Math.max(reached, level), ts: Date.now() } };
185
+ }
153
186
 
154
- const remind = !fired && total >= threshold;
155
- return { remind, next: { last: total, fired: fired || remind, ts: Date.now() } };
187
+ /**
188
+ * BEST-EFFORT user-facing channel (`systemMessage`). Emitted because it is free
189
+ * and correct per the hook schema — NOT because it is known to be shown.
190
+ *
191
+ * Measured 2026-07-26, TASK-080, with a two-marker probe: `systemMessage` does
192
+ * not reach the model's context (so it costs no tokens), and it did not appear
193
+ * on screen either, in the VSCode extension, for a UserPromptSubmit hook. The
194
+ * same probe's `additionalContext` marker arrived, which is what makes that a
195
+ * clean negative rather than a probe that failed to load. Other clients were
196
+ * not measured, so this stays: a line that costs nothing and may render
197
+ * somewhere else. **Nothing may depend on it** — in particular reminderText()
198
+ * must never tell the model the user has already seen the number.
199
+ *
200
+ * One sentence, deliberately: the size is the one fact a user cannot get
201
+ * anywhere else, and /compact is the one action.
202
+ */
203
+ export function userNotice(total) {
204
+ const k = Math.round(total / 1000);
205
+ return `[tune-context] Context is at ~${k}k tokens — consider /compact when the current thread closes.`;
156
206
  }
157
207
 
158
- export function reminderText(total) {
208
+ /**
209
+ * The channel that actually works (`additionalContext`): the model reads it and
210
+ * relays it. The timing judgement stays here, because only the model can tell
211
+ * whether the current thread is finished — but see the second sentence: it must
212
+ * relay eventually, because staying silent forever means the user never learns.
213
+ */
214
+ export function reminderText(total, { escalated = false } = {}) {
159
215
  const k = Math.round(total / 1000);
160
216
  return [
161
217
  `[tune-context] This session is now carrying ~${k}k tokens of context, and every further turn re-sends all of it.`,
218
+ // The measured failure this sentence exists to prevent: at ~166k the model
219
+ // stayed quiet mid-task and simply never came back to it, so the user first
220
+ // heard about the cost at ~346k.
221
+ 'The user CANNOT see this reminder — it reaches you alone, so if you never mention it they will never know.',
162
222
  'If the current work thread is finished: suggest /compact (or /clear when switching to unrelated work).',
163
- 'If you are mid-task: say nothing now and raise it when the thread closes.',
223
+ 'If you are mid-task: stay quiet now, but raise it at the first natural break rather than dropping it.',
164
224
  'The PreCompact hook preserves branch, dirty files and recent commits across a compaction — judgment calls and open threads are NOT preserved, so write those down first.',
225
+ // Kept short on purpose: it rides ON TOP of the full reminder, inside the
226
+ // same <700 char budget.
227
+ ...(escalated ? ['This is the second and final automatic notice — nothing more fires until a compaction.'] : []),
165
228
  ].join(' ');
166
229
  }
167
230
 
@@ -182,7 +245,7 @@ function main() {
182
245
  const session = input.session_id || 'unknown';
183
246
  const path = stateFile();
184
247
  const state = readState(path);
185
- const { remind, next } = decide(state[session], total, thresholdTokens());
248
+ const { remind, escalated, next } = decide(state[session], total, thresholdTokens());
186
249
 
187
250
  state[session] = next;
188
251
  try {
@@ -194,11 +257,14 @@ function main() {
194
257
  }
195
258
 
196
259
  if (!remind) return;
260
+ // additionalContext is the channel that is known to arrive; systemMessage
261
+ // is emitted alongside it as a free best-effort (see userNotice()).
197
262
  process.stdout.write(
198
263
  JSON.stringify({
264
+ systemMessage: userNotice(total),
199
265
  hookSpecificOutput: {
200
266
  hookEventName: 'UserPromptSubmit',
201
- additionalContext: reminderText(total),
267
+ additionalContext: reminderText(total, { escalated }),
202
268
  },
203
269
  }),
204
270
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bglocation/tune-context",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Context-efficiency configurator for Claude Code \u2014 token-saving doctrine packaged as skills, plus a tune-context configurator that scaffolds a lean CLAUDE.md, doctrine skills, subagents, settings, hooks and MCP wiring.",
5
5
  "type": "module",
6
6
  "license": "MIT",