New:Socket for Asana Is Now Available.Learn more
Get Started

@ooples/token-optimizer-mcp

Package Overview
Dependencies
Maintainers
1
Versions
35
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@ooples/token-optimizer-mcp - npm Package Compare versions

Comparing version
5.6.0
to
5.6.1
+8
-0
CHANGELOG.md

@@ -8,2 +8,10 @@ # Changelog

## [5.6.1](https://github.com/ooples/token-optimizer-mcp/compare/v5.6.0...v5.6.1) (2026-08-08)
### Bug Fixes
* **keepwarm,lessons:** make the tripwire reachable, the gaps per-session, the anchors real ([#295](https://github.com/ooples/token-optimizer-mcp/issues/295)) ([c22bc55](https://github.com/ooples/token-optimizer-mcp/commit/c22bc55600014c59c61e6a225034f5c6a2f20b93))
* **keepwarm,lessons:** score refreshes with the model that bought them ([#293](https://github.com/ooples/token-optimizer-mcp/issues/293)) ([ff158bf](https://github.com/ooples/token-optimizer-mcp/commit/ff158bf3d4021c5b13398392e1b28d824bebf8fc))
## [5.6.0](https://github.com/ooples/token-optimizer-mcp/compare/v5.5.0...v5.6.0) (2026-08-08)

@@ -10,0 +18,0 @@

+1
-1

@@ -1,1 +0,1 @@

{"version":3,"file":"wiki-read.d.ts","sourceRoot":"","sources":["../../../src/tools/intelligence/wiki-read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAaH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,2DAA2D;IAC3D,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,+EAA+E;IAC/E,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,8FAA8F;IAC9F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAcD,wBAAsB,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,cAAc,CAAC,CAyHrF;AAED,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCrC,CAAC"}
{"version":3,"file":"wiki-read.d.ts","sourceRoot":"","sources":["../../../src/tools/intelligence/wiki-read.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAcH,MAAM,WAAW,eAAe;IAC9B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wDAAwD;IACxD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,WAAW,EAAE,CAAC;IACxB,2DAA2D;IAC3D,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,+EAA+E;IAC/E,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,8FAA8F;IAC9F,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAcD,wBAAsB,QAAQ,CAC5B,OAAO,GAAE,eAAoB,GAC5B,OAAO,CAAC,cAAc,CAAC,CAiIzB;AAED,eAAO,MAAM,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmCrC,CAAC"}

@@ -29,3 +29,4 @@ /**

function coreUrl(name) {
return pathToFileURL(path.join(here, '..', '..', '..', 'hooks-core', name)).href;
return pathToFileURL(path.join(here, '..', '..', '..', 'hooks-core', name))
.href;
}

@@ -44,3 +45,7 @@ function toFinding(node) {

export async function wikiRead(options = {}) {
const empty = { findings: [], shared: [], unresolvedAnchors: [] };
const empty = {
findings: [],
shared: [],
unresolvedAnchors: [],
};
const anchors = Array.isArray(options?.anchors)

@@ -78,3 +83,5 @@ ? options.anchors.filter((a) => typeof a === 'string' && a.trim())

const primary = options.projectRoot ??
(anchors.length ? wiki.projectRootFor(anchors[0].split('#')[0], process.cwd()) : process.cwd());
(anchors.length
? wiki.projectRootFor(anchors[0].split('#')[0], process.cwd())
: process.cwd());
const seen = new Set();

@@ -81,0 +88,0 @@ const findings = [];

@@ -1,1 +0,1 @@

{"version":3,"file":"wiki-read.js","sourceRoot":"","sources":["../../../src/tools/intelligence/wiki-read.ts"],"names":[],"mappings":"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"}
{"version":3,"file":"wiki-read.js","sourceRoot":"","sources":["../../../src/tools/intelligence/wiki-read.ts"],"names":[],"mappings":"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"}

@@ -27,3 +27,3 @@ /**

import { record, readMetrics } from './metrics.mjs';
import { record, readMetrics, readBalance } from './metrics.mjs';
import { WRITE_MULTIPLIER, READ_MULTIPLIER } from './cache.mjs';

@@ -58,10 +58,33 @@

export function gapDistribution(dir, { events = readMetrics(dir) } = {}) {
const stamps = events.map((e) => e.at).filter(Number.isFinite).sort((a, b) => a - b);
// PARTITIONED BY SESSION, because the interval between the last event of one session and the
// first of the next is not a gap between turns at all. The log spans days, so an overnight
// sixteen-hour boundary was counted as one -- dominating p90 and p99 and diluting
// probabilityWithin in the conservative direction, so ttlTier answered "neither tier pays" on
// projects where it would have paid. Silent, because that bias only ever declines to act.
//
// A long gap WITHIN a session is kept. It is real evidence that caching does not pay there, and
// dropping it would bias the answer the other way -- making keep-warm look better than it is,
// which is the direction this project cares about most.
//
// Events carrying no sessionId are pooled into one group rather than discarded: most kinds do
// carry one, and discarding the rest would throw away whole projects' history for a technicality.
const bySession = new Map();
for (const event of events) {
if (!Number.isFinite(event?.at)) continue;
const key = event.sessionId || '';
if (!bySession.has(key)) bySession.set(key, []);
bySession.get(key).push(event.at);
}
const stamps = [...bySession.values()].flat();
if (stamps.length < 8) return null;
const gaps = [];
for (let i = 1; i < stamps.length; i++) {
const gap = stamps[i] - stamps[i - 1];
// A burst of events inside one turn is not a gap between turns.
if (gap > 250) gaps.push(gap);
for (const session of bySession.values()) {
session.sort((a, b) => a - b);
for (let i = 1; i < session.length; i++) {
const gap = session[i] - session[i - 1];
// A burst of events inside one turn is not a gap between turns.
if (gap > 250) gaps.push(gap);
}
}

@@ -140,3 +163,10 @@ if (gaps.length < 6) return null;

const perTurn = hit * READ_MULTIPLIER + (1 - hit) * tier.writeMultiplier;
const turns = Math.max(1, turnsPerSession);
// THE SUPPLIED VALUE, not only the computed one. Math.max(1, NaN) is NaN -- Math.max
// propagates it rather than clamping -- so perTurn became NaN, the `perTurn >= 1` guard below
// read `NaN >= 1` as false and PASSED, and this returned action:'refresh' with an
// expectedValue of NaN and a reason string reading "NaN% of gaps land inside 5m". A positive
// verdict built entirely out of NaN. The guard was on the wrong side of the computation.
const turns = Number.isFinite(turnsPerSession) && turnsPerSession >= 1
? turnsPerSession
: DEFAULT_TURNS;
return {

@@ -180,5 +210,17 @@ tier,

const tierSpec = TIERS.find((t) => t.name === tier) || TIERS[0];
// THE SAME LEDGER keepWarmDecision BUYS THE REFRESH WITH. That function is explicit that a
// refresh is a PING, which READS the prefix -- costOfPing = prefixTokens * READ_MULTIPLIER --
// and its comment warns that pricing the ping as a write "overstates its cost by more than
// twelvefold". This scored the very same refresh as a re-WRITE, charging (writeMultiplier - 1)
// on both branches.
//
// The disagreement runs in the direction that kills the feature. Mean realised per refresh under
// the old lines was p*(0.9h - 0.25), negative below a 27.8% hit rate, while the decision's own
// model makes the ping pay above 8.7%. For any project whose gaps fall in that band, `tripwire`
// accumulates a negative balance and permanently disables a policy that is genuinely paying --
// and tells the user "keep-warm has lost N tokens ... stopping". The backstop fired on its own
// accounting error rather than on a distribution shift.
const realised = hit
? prefixTokens * (1 - READ_MULTIPLIER) - prefixTokens * (tierSpec.writeMultiplier - 1)
: -prefixTokens * (tierSpec.writeMultiplier - 1);
? prefixTokens * (tierSpec.writeMultiplier - READ_MULTIPLIER) - prefixTokens * READ_MULTIPLIER
: -prefixTokens * READ_MULTIPLIER;
record(dir, { kind: 'keepwarm', action: 'outcome', tier, prefixTokens, hit: Boolean(hit), realised: Math.round(realised) });

@@ -195,3 +237,12 @@ }

*/
export function tripwire(dir, { events = readMetrics(dir) } = {}) {
/**
* The backstop, read from the log that is NOT windowed.
*
* readBalance rather than readMetrics, and that distinction is the difference between a backstop
* that works and one that cannot. There is one outcome per refresh, in a log dominated by reads
* and captures, so through the 5000-event window the ten TRIPWIRE_MIN demands aged out before the
* tenth was written -- and this returned "only N/10 refreshes observed" for the life of the
* project. A guard that can never reach its own threshold is not a guard.
*/
export function tripwire(dir, { events = readBalance(dir) } = {}) {
const outcomes = events.filter((e) => e.kind === 'keepwarm' && e.action === 'outcome');

@@ -221,4 +272,13 @@ if (outcomes.length < TRIPWIRE_MIN) {

*/
export function shouldKeepWarm(dir, { prefixTokens, events = readMetrics(dir) } = {}) {
const trip = tripwire(dir, { events });
export function shouldKeepWarm(dir, {
prefixTokens,
events = readMetrics(dir),
// TWO SOURCES, because the two consumers want different things. gapDistribution wants the
// firehose -- every event is a timestamp and the recent ones describe the current rhythm. The
// tripwire wants the unwindowed balance log, because its outcomes are rare and are exactly what
// the window evicts. Passing one shared array to both, as this used to, meant whichever reader
// was chosen was wrong for one of them.
outcomes = readBalance(dir),
} = {}) {
const trip = tripwire(dir, { events: outcomes });
if (trip.tripped) return { action: 'skip', reason: trip.reason, trippedWire: true };

@@ -229,3 +289,11 @@

if (!best) {
// THE TWO MODELS ANSWER DIFFERENT QUESTIONS, so they may legitimately disagree. ttlTier asks
// whether holding a cache beats not caching at all; keepWarmDecision asks whether ONE ping
// beats letting the entry lapse -- and a ping can pay where no tier does. Coercing everything
// that was not 'unknown' to 'skip' while keeping the decision's reason verbatim produced a
// refusal justified by a GAIN: `{ action: 'skip', reason: '...expected gain 130 tokens' }`.
// This module's own docstring says a refusal that cannot be checked is indistinguishable from
// a bug; one that contradicts itself is worse.
const decision = keepWarmDecision({ prefixTokens, gaps });
if (decision.action === 'refresh') return decision;
return { action: decision.action === 'unknown' ? 'unknown' : 'skip', reason: decision.reason };

@@ -232,0 +300,0 @@ }

@@ -31,2 +31,4 @@ /**

import { ORIGIN_HARVESTED, ORIGIN_HUMAN } from './curate.mjs';
import { safeTrigger } from './inject.mjs';
import { isFsSafePath } from './paths.mjs';

@@ -93,2 +95,9 @@ /** Lessons longer than this are prose, not instructions. */

const piece = rendered[i];
// A SINGLE oversize turn must not end the digest. One pasted stack trace in the last turn
// made the budget test below true on the FIRST iteration, so `out` stayed empty, this
// returned null, and harvest-worker skipped the entire feedback pass for that session --
// no extraction, no lesson validation, no metrics record, no signal anywhere. The loop runs
// backwards precisely to keep the end, and a big terminal turn is exactly what a session
// that went wrong tends to produce.
if (piece.length > maxChars) continue;
if (total + piece.length > maxChars) break;

@@ -167,3 +176,3 @@ out.unshift(piece);

*/
export function validateLessons(raw, turns) {
export function validateLessons(raw, turns, { knownFiles = null } = {}) {
let parsed = raw;

@@ -203,5 +212,13 @@ if (typeof raw === 'string') {

}
try {
new RegExp(trigger);
} catch {
// THE SAME GATE THE INJECTOR USES. `new RegExp` only proves the pattern COMPILES. inject.mjs
// additionally refuses sources over 200 characters and nested-quantifier ReDoS shapes -- and
// when it refuses, appliesToCommand falls back to a LITERAL substring search of the regex
// SOURCE against the command. A regex source is not a substring of any real command --
// a word-boundary-anchored pattern for the jest runner does not appear literally inside
// `npx jest --watch` --
// so a lesson stored with such a trigger is written to the graph, counted as delivered in the
// metrics record, and can never surface for the rest of its life. Both rejection shapes are
// realistic from the extraction prompt: a long alternation over test runners passes 200
// characters easily, and `(\w+\s*)+\.csproj` is the kind of thing a model emits unprompted.
if (!safeTrigger(trigger)) {
rejected.push({ reason: 'bad-trigger-regex', trigger });

@@ -227,4 +244,15 @@ continue;

confidence: verified ? 0.95 : 0.6,
// HELD TO THE FILES THE SESSION ACTUALLY TOUCHED, exactly as the finding path already is.
// harvest-worker calls validate() with `knownFiles: filesIn(digest)` and says why in its own
// comment -- "a model that invents a plausible path cannot anchor a finding to it" -- while
// this path accepted any non-empty string. The identical hallucination therefore walked
// straight through, and a lesson anchored to a file nobody opened is then injected on every
// future touch of it: a permanent instruction attached to code it was never about.
//
// isFsSafePath as well, because these strings reach the filesystem later through the graph.
// A path holding U+10FFFF ABORTS the process inside libuv rather than throwing, so no
// try/catch downstream can contain it.
anchors: Array.isArray(item?.anchors)
? item.anchors.filter((a) => typeof a === 'string' && a.trim())
? item.anchors.filter((a) => typeof a === 'string' && a.trim() && isFsSafePath(a)
&& (!knownFiles || knownFiles.includes(a)))
: [],

@@ -231,0 +259,0 @@ });

@@ -79,2 +79,7 @@ /**

'forecast-outcome',
// The keep-warm tripwire's evidence. It needs TRIPWIRE_MIN outcomes before it may have an
// opinion, and there is one outcome per refresh -- so through the windowed reader the tenth
// aged out before it was written and the backstop was structurally unable to fire, reporting
// "only N/10 refreshes observed" for the life of the project.
'keepwarm',
]);

@@ -81,0 +86,0 @@

@@ -29,3 +29,3 @@ // GENERATED FILE -- do not edit.

import { record, readMetrics } from './metrics.mjs';
import { record, readMetrics, readBalance } from './metrics.mjs';
import { WRITE_MULTIPLIER, READ_MULTIPLIER } from './cache.mjs';

@@ -60,10 +60,33 @@

export function gapDistribution(dir, { events = readMetrics(dir) } = {}) {
const stamps = events.map((e) => e.at).filter(Number.isFinite).sort((a, b) => a - b);
// PARTITIONED BY SESSION, because the interval between the last event of one session and the
// first of the next is not a gap between turns at all. The log spans days, so an overnight
// sixteen-hour boundary was counted as one -- dominating p90 and p99 and diluting
// probabilityWithin in the conservative direction, so ttlTier answered "neither tier pays" on
// projects where it would have paid. Silent, because that bias only ever declines to act.
//
// A long gap WITHIN a session is kept. It is real evidence that caching does not pay there, and
// dropping it would bias the answer the other way -- making keep-warm look better than it is,
// which is the direction this project cares about most.
//
// Events carrying no sessionId are pooled into one group rather than discarded: most kinds do
// carry one, and discarding the rest would throw away whole projects' history for a technicality.
const bySession = new Map();
for (const event of events) {
if (!Number.isFinite(event?.at)) continue;
const key = event.sessionId || '';
if (!bySession.has(key)) bySession.set(key, []);
bySession.get(key).push(event.at);
}
const stamps = [...bySession.values()].flat();
if (stamps.length < 8) return null;
const gaps = [];
for (let i = 1; i < stamps.length; i++) {
const gap = stamps[i] - stamps[i - 1];
// A burst of events inside one turn is not a gap between turns.
if (gap > 250) gaps.push(gap);
for (const session of bySession.values()) {
session.sort((a, b) => a - b);
for (let i = 1; i < session.length; i++) {
const gap = session[i] - session[i - 1];
// A burst of events inside one turn is not a gap between turns.
if (gap > 250) gaps.push(gap);
}
}

@@ -142,3 +165,10 @@ if (gaps.length < 6) return null;

const perTurn = hit * READ_MULTIPLIER + (1 - hit) * tier.writeMultiplier;
const turns = Math.max(1, turnsPerSession);
// THE SUPPLIED VALUE, not only the computed one. Math.max(1, NaN) is NaN -- Math.max
// propagates it rather than clamping -- so perTurn became NaN, the `perTurn >= 1` guard below
// read `NaN >= 1` as false and PASSED, and this returned action:'refresh' with an
// expectedValue of NaN and a reason string reading "NaN% of gaps land inside 5m". A positive
// verdict built entirely out of NaN. The guard was on the wrong side of the computation.
const turns = Number.isFinite(turnsPerSession) && turnsPerSession >= 1
? turnsPerSession
: DEFAULT_TURNS;
return {

@@ -182,5 +212,17 @@ tier,

const tierSpec = TIERS.find((t) => t.name === tier) || TIERS[0];
// THE SAME LEDGER keepWarmDecision BUYS THE REFRESH WITH. That function is explicit that a
// refresh is a PING, which READS the prefix -- costOfPing = prefixTokens * READ_MULTIPLIER --
// and its comment warns that pricing the ping as a write "overstates its cost by more than
// twelvefold". This scored the very same refresh as a re-WRITE, charging (writeMultiplier - 1)
// on both branches.
//
// The disagreement runs in the direction that kills the feature. Mean realised per refresh under
// the old lines was p*(0.9h - 0.25), negative below a 27.8% hit rate, while the decision's own
// model makes the ping pay above 8.7%. For any project whose gaps fall in that band, `tripwire`
// accumulates a negative balance and permanently disables a policy that is genuinely paying --
// and tells the user "keep-warm has lost N tokens ... stopping". The backstop fired on its own
// accounting error rather than on a distribution shift.
const realised = hit
? prefixTokens * (1 - READ_MULTIPLIER) - prefixTokens * (tierSpec.writeMultiplier - 1)
: -prefixTokens * (tierSpec.writeMultiplier - 1);
? prefixTokens * (tierSpec.writeMultiplier - READ_MULTIPLIER) - prefixTokens * READ_MULTIPLIER
: -prefixTokens * READ_MULTIPLIER;
record(dir, { kind: 'keepwarm', action: 'outcome', tier, prefixTokens, hit: Boolean(hit), realised: Math.round(realised) });

@@ -197,3 +239,12 @@ }

*/
export function tripwire(dir, { events = readMetrics(dir) } = {}) {
/**
* The backstop, read from the log that is NOT windowed.
*
* readBalance rather than readMetrics, and that distinction is the difference between a backstop
* that works and one that cannot. There is one outcome per refresh, in a log dominated by reads
* and captures, so through the 5000-event window the ten TRIPWIRE_MIN demands aged out before the
* tenth was written -- and this returned "only N/10 refreshes observed" for the life of the
* project. A guard that can never reach its own threshold is not a guard.
*/
export function tripwire(dir, { events = readBalance(dir) } = {}) {
const outcomes = events.filter((e) => e.kind === 'keepwarm' && e.action === 'outcome');

@@ -223,4 +274,13 @@ if (outcomes.length < TRIPWIRE_MIN) {

*/
export function shouldKeepWarm(dir, { prefixTokens, events = readMetrics(dir) } = {}) {
const trip = tripwire(dir, { events });
export function shouldKeepWarm(dir, {
prefixTokens,
events = readMetrics(dir),
// TWO SOURCES, because the two consumers want different things. gapDistribution wants the
// firehose -- every event is a timestamp and the recent ones describe the current rhythm. The
// tripwire wants the unwindowed balance log, because its outcomes are rare and are exactly what
// the window evicts. Passing one shared array to both, as this used to, meant whichever reader
// was chosen was wrong for one of them.
outcomes = readBalance(dir),
} = {}) {
const trip = tripwire(dir, { events: outcomes });
if (trip.tripped) return { action: 'skip', reason: trip.reason, trippedWire: true };

@@ -231,3 +291,11 @@

if (!best) {
// THE TWO MODELS ANSWER DIFFERENT QUESTIONS, so they may legitimately disagree. ttlTier asks
// whether holding a cache beats not caching at all; keepWarmDecision asks whether ONE ping
// beats letting the entry lapse -- and a ping can pay where no tier does. Coercing everything
// that was not 'unknown' to 'skip' while keeping the decision's reason verbatim produced a
// refusal justified by a GAIN: `{ action: 'skip', reason: '...expected gain 130 tokens' }`.
// This module's own docstring says a refusal that cannot be checked is indistinguishable from
// a bug; one that contradicts itself is worse.
const decision = keepWarmDecision({ prefixTokens, gaps });
if (decision.action === 'refresh') return decision;
return { action: decision.action === 'unknown' ? 'unknown' : 'skip', reason: decision.reason };

@@ -234,0 +302,0 @@ }

@@ -33,2 +33,4 @@ // GENERATED FILE -- do not edit.

import { ORIGIN_HARVESTED, ORIGIN_HUMAN } from './curate.mjs';
import { safeTrigger } from './inject.mjs';
import { isFsSafePath } from './paths.mjs';

@@ -95,2 +97,9 @@ /** Lessons longer than this are prose, not instructions. */

const piece = rendered[i];
// A SINGLE oversize turn must not end the digest. One pasted stack trace in the last turn
// made the budget test below true on the FIRST iteration, so `out` stayed empty, this
// returned null, and harvest-worker skipped the entire feedback pass for that session --
// no extraction, no lesson validation, no metrics record, no signal anywhere. The loop runs
// backwards precisely to keep the end, and a big terminal turn is exactly what a session
// that went wrong tends to produce.
if (piece.length > maxChars) continue;
if (total + piece.length > maxChars) break;

@@ -169,3 +178,3 @@ out.unshift(piece);

*/
export function validateLessons(raw, turns) {
export function validateLessons(raw, turns, { knownFiles = null } = {}) {
let parsed = raw;

@@ -205,5 +214,13 @@ if (typeof raw === 'string') {

}
try {
new RegExp(trigger);
} catch {
// THE SAME GATE THE INJECTOR USES. `new RegExp` only proves the pattern COMPILES. inject.mjs
// additionally refuses sources over 200 characters and nested-quantifier ReDoS shapes -- and
// when it refuses, appliesToCommand falls back to a LITERAL substring search of the regex
// SOURCE against the command. A regex source is not a substring of any real command --
// a word-boundary-anchored pattern for the jest runner does not appear literally inside
// `npx jest --watch` --
// so a lesson stored with such a trigger is written to the graph, counted as delivered in the
// metrics record, and can never surface for the rest of its life. Both rejection shapes are
// realistic from the extraction prompt: a long alternation over test runners passes 200
// characters easily, and `(\w+\s*)+\.csproj` is the kind of thing a model emits unprompted.
if (!safeTrigger(trigger)) {
rejected.push({ reason: 'bad-trigger-regex', trigger });

@@ -229,4 +246,15 @@ continue;

confidence: verified ? 0.95 : 0.6,
// HELD TO THE FILES THE SESSION ACTUALLY TOUCHED, exactly as the finding path already is.
// harvest-worker calls validate() with `knownFiles: filesIn(digest)` and says why in its own
// comment -- "a model that invents a plausible path cannot anchor a finding to it" -- while
// this path accepted any non-empty string. The identical hallucination therefore walked
// straight through, and a lesson anchored to a file nobody opened is then injected on every
// future touch of it: a permanent instruction attached to code it was never about.
//
// isFsSafePath as well, because these strings reach the filesystem later through the graph.
// A path holding U+10FFFF ABORTS the process inside libuv rather than throwing, so no
// try/catch downstream can contain it.
anchors: Array.isArray(item?.anchors)
? item.anchors.filter((a) => typeof a === 'string' && a.trim())
? item.anchors.filter((a) => typeof a === 'string' && a.trim() && isFsSafePath(a)
&& (!knownFiles || knownFiles.includes(a)))
: [],

@@ -233,0 +261,0 @@ });

@@ -81,2 +81,7 @@ // GENERATED FILE -- do not edit.

'forecast-outcome',
// The keep-warm tripwire's evidence. It needs TRIPWIRE_MIN outcomes before it may have an
// opinion, and there is one outcome per refresh -- so through the windowed reader the tenth
// aged out before it was written and the backstop was structurally unable to fire, reporting
// "only N/10 refreshes observed" for the life of the project.
'keepwarm',
]);

@@ -83,0 +88,0 @@

@@ -29,3 +29,3 @@ // GENERATED FILE -- do not edit.

import { record, readMetrics } from './metrics.mjs';
import { record, readMetrics, readBalance } from './metrics.mjs';
import { WRITE_MULTIPLIER, READ_MULTIPLIER } from './cache.mjs';

@@ -60,10 +60,33 @@

export function gapDistribution(dir, { events = readMetrics(dir) } = {}) {
const stamps = events.map((e) => e.at).filter(Number.isFinite).sort((a, b) => a - b);
// PARTITIONED BY SESSION, because the interval between the last event of one session and the
// first of the next is not a gap between turns at all. The log spans days, so an overnight
// sixteen-hour boundary was counted as one -- dominating p90 and p99 and diluting
// probabilityWithin in the conservative direction, so ttlTier answered "neither tier pays" on
// projects where it would have paid. Silent, because that bias only ever declines to act.
//
// A long gap WITHIN a session is kept. It is real evidence that caching does not pay there, and
// dropping it would bias the answer the other way -- making keep-warm look better than it is,
// which is the direction this project cares about most.
//
// Events carrying no sessionId are pooled into one group rather than discarded: most kinds do
// carry one, and discarding the rest would throw away whole projects' history for a technicality.
const bySession = new Map();
for (const event of events) {
if (!Number.isFinite(event?.at)) continue;
const key = event.sessionId || '';
if (!bySession.has(key)) bySession.set(key, []);
bySession.get(key).push(event.at);
}
const stamps = [...bySession.values()].flat();
if (stamps.length < 8) return null;
const gaps = [];
for (let i = 1; i < stamps.length; i++) {
const gap = stamps[i] - stamps[i - 1];
// A burst of events inside one turn is not a gap between turns.
if (gap > 250) gaps.push(gap);
for (const session of bySession.values()) {
session.sort((a, b) => a - b);
for (let i = 1; i < session.length; i++) {
const gap = session[i] - session[i - 1];
// A burst of events inside one turn is not a gap between turns.
if (gap > 250) gaps.push(gap);
}
}

@@ -142,3 +165,10 @@ if (gaps.length < 6) return null;

const perTurn = hit * READ_MULTIPLIER + (1 - hit) * tier.writeMultiplier;
const turns = Math.max(1, turnsPerSession);
// THE SUPPLIED VALUE, not only the computed one. Math.max(1, NaN) is NaN -- Math.max
// propagates it rather than clamping -- so perTurn became NaN, the `perTurn >= 1` guard below
// read `NaN >= 1` as false and PASSED, and this returned action:'refresh' with an
// expectedValue of NaN and a reason string reading "NaN% of gaps land inside 5m". A positive
// verdict built entirely out of NaN. The guard was on the wrong side of the computation.
const turns = Number.isFinite(turnsPerSession) && turnsPerSession >= 1
? turnsPerSession
: DEFAULT_TURNS;
return {

@@ -182,5 +212,17 @@ tier,

const tierSpec = TIERS.find((t) => t.name === tier) || TIERS[0];
// THE SAME LEDGER keepWarmDecision BUYS THE REFRESH WITH. That function is explicit that a
// refresh is a PING, which READS the prefix -- costOfPing = prefixTokens * READ_MULTIPLIER --
// and its comment warns that pricing the ping as a write "overstates its cost by more than
// twelvefold". This scored the very same refresh as a re-WRITE, charging (writeMultiplier - 1)
// on both branches.
//
// The disagreement runs in the direction that kills the feature. Mean realised per refresh under
// the old lines was p*(0.9h - 0.25), negative below a 27.8% hit rate, while the decision's own
// model makes the ping pay above 8.7%. For any project whose gaps fall in that band, `tripwire`
// accumulates a negative balance and permanently disables a policy that is genuinely paying --
// and tells the user "keep-warm has lost N tokens ... stopping". The backstop fired on its own
// accounting error rather than on a distribution shift.
const realised = hit
? prefixTokens * (1 - READ_MULTIPLIER) - prefixTokens * (tierSpec.writeMultiplier - 1)
: -prefixTokens * (tierSpec.writeMultiplier - 1);
? prefixTokens * (tierSpec.writeMultiplier - READ_MULTIPLIER) - prefixTokens * READ_MULTIPLIER
: -prefixTokens * READ_MULTIPLIER;
record(dir, { kind: 'keepwarm', action: 'outcome', tier, prefixTokens, hit: Boolean(hit), realised: Math.round(realised) });

@@ -197,3 +239,12 @@ }

*/
export function tripwire(dir, { events = readMetrics(dir) } = {}) {
/**
* The backstop, read from the log that is NOT windowed.
*
* readBalance rather than readMetrics, and that distinction is the difference between a backstop
* that works and one that cannot. There is one outcome per refresh, in a log dominated by reads
* and captures, so through the 5000-event window the ten TRIPWIRE_MIN demands aged out before the
* tenth was written -- and this returned "only N/10 refreshes observed" for the life of the
* project. A guard that can never reach its own threshold is not a guard.
*/
export function tripwire(dir, { events = readBalance(dir) } = {}) {
const outcomes = events.filter((e) => e.kind === 'keepwarm' && e.action === 'outcome');

@@ -223,4 +274,13 @@ if (outcomes.length < TRIPWIRE_MIN) {

*/
export function shouldKeepWarm(dir, { prefixTokens, events = readMetrics(dir) } = {}) {
const trip = tripwire(dir, { events });
export function shouldKeepWarm(dir, {
prefixTokens,
events = readMetrics(dir),
// TWO SOURCES, because the two consumers want different things. gapDistribution wants the
// firehose -- every event is a timestamp and the recent ones describe the current rhythm. The
// tripwire wants the unwindowed balance log, because its outcomes are rare and are exactly what
// the window evicts. Passing one shared array to both, as this used to, meant whichever reader
// was chosen was wrong for one of them.
outcomes = readBalance(dir),
} = {}) {
const trip = tripwire(dir, { events: outcomes });
if (trip.tripped) return { action: 'skip', reason: trip.reason, trippedWire: true };

@@ -231,3 +291,11 @@

if (!best) {
// THE TWO MODELS ANSWER DIFFERENT QUESTIONS, so they may legitimately disagree. ttlTier asks
// whether holding a cache beats not caching at all; keepWarmDecision asks whether ONE ping
// beats letting the entry lapse -- and a ping can pay where no tier does. Coercing everything
// that was not 'unknown' to 'skip' while keeping the decision's reason verbatim produced a
// refusal justified by a GAIN: `{ action: 'skip', reason: '...expected gain 130 tokens' }`.
// This module's own docstring says a refusal that cannot be checked is indistinguishable from
// a bug; one that contradicts itself is worse.
const decision = keepWarmDecision({ prefixTokens, gaps });
if (decision.action === 'refresh') return decision;
return { action: decision.action === 'unknown' ? 'unknown' : 'skip', reason: decision.reason };

@@ -234,0 +302,0 @@ }

@@ -33,2 +33,4 @@ // GENERATED FILE -- do not edit.

import { ORIGIN_HARVESTED, ORIGIN_HUMAN } from './curate.mjs';
import { safeTrigger } from './inject.mjs';
import { isFsSafePath } from './paths.mjs';

@@ -95,2 +97,9 @@ /** Lessons longer than this are prose, not instructions. */

const piece = rendered[i];
// A SINGLE oversize turn must not end the digest. One pasted stack trace in the last turn
// made the budget test below true on the FIRST iteration, so `out` stayed empty, this
// returned null, and harvest-worker skipped the entire feedback pass for that session --
// no extraction, no lesson validation, no metrics record, no signal anywhere. The loop runs
// backwards precisely to keep the end, and a big terminal turn is exactly what a session
// that went wrong tends to produce.
if (piece.length > maxChars) continue;
if (total + piece.length > maxChars) break;

@@ -169,3 +178,3 @@ out.unshift(piece);

*/
export function validateLessons(raw, turns) {
export function validateLessons(raw, turns, { knownFiles = null } = {}) {
let parsed = raw;

@@ -205,5 +214,13 @@ if (typeof raw === 'string') {

}
try {
new RegExp(trigger);
} catch {
// THE SAME GATE THE INJECTOR USES. `new RegExp` only proves the pattern COMPILES. inject.mjs
// additionally refuses sources over 200 characters and nested-quantifier ReDoS shapes -- and
// when it refuses, appliesToCommand falls back to a LITERAL substring search of the regex
// SOURCE against the command. A regex source is not a substring of any real command --
// a word-boundary-anchored pattern for the jest runner does not appear literally inside
// `npx jest --watch` --
// so a lesson stored with such a trigger is written to the graph, counted as delivered in the
// metrics record, and can never surface for the rest of its life. Both rejection shapes are
// realistic from the extraction prompt: a long alternation over test runners passes 200
// characters easily, and `(\w+\s*)+\.csproj` is the kind of thing a model emits unprompted.
if (!safeTrigger(trigger)) {
rejected.push({ reason: 'bad-trigger-regex', trigger });

@@ -229,4 +246,15 @@ continue;

confidence: verified ? 0.95 : 0.6,
// HELD TO THE FILES THE SESSION ACTUALLY TOUCHED, exactly as the finding path already is.
// harvest-worker calls validate() with `knownFiles: filesIn(digest)` and says why in its own
// comment -- "a model that invents a plausible path cannot anchor a finding to it" -- while
// this path accepted any non-empty string. The identical hallucination therefore walked
// straight through, and a lesson anchored to a file nobody opened is then injected on every
// future touch of it: a permanent instruction attached to code it was never about.
//
// isFsSafePath as well, because these strings reach the filesystem later through the graph.
// A path holding U+10FFFF ABORTS the process inside libuv rather than throwing, so no
// try/catch downstream can contain it.
anchors: Array.isArray(item?.anchors)
? item.anchors.filter((a) => typeof a === 'string' && a.trim())
? item.anchors.filter((a) => typeof a === 'string' && a.trim() && isFsSafePath(a)
&& (!knownFiles || knownFiles.includes(a)))
: [],

@@ -233,0 +261,0 @@ });

@@ -81,2 +81,7 @@ // GENERATED FILE -- do not edit.

'forecast-outcome',
// The keep-warm tripwire's evidence. It needs TRIPWIRE_MIN outcomes before it may have an
// opinion, and there is one outcome per refresh -- so through the windowed reader the tenth
// aged out before it was written and the backstop was structurally unable to fire, reporting
// "only N/10 refreshes observed" for the life of the project.
'keepwarm',
]);

@@ -83,0 +88,0 @@

@@ -29,3 +29,3 @@ // GENERATED FILE -- do not edit.

import { record, readMetrics } from './metrics.mjs';
import { record, readMetrics, readBalance } from './metrics.mjs';
import { WRITE_MULTIPLIER, READ_MULTIPLIER } from './cache.mjs';

@@ -60,10 +60,33 @@

export function gapDistribution(dir, { events = readMetrics(dir) } = {}) {
const stamps = events.map((e) => e.at).filter(Number.isFinite).sort((a, b) => a - b);
// PARTITIONED BY SESSION, because the interval between the last event of one session and the
// first of the next is not a gap between turns at all. The log spans days, so an overnight
// sixteen-hour boundary was counted as one -- dominating p90 and p99 and diluting
// probabilityWithin in the conservative direction, so ttlTier answered "neither tier pays" on
// projects where it would have paid. Silent, because that bias only ever declines to act.
//
// A long gap WITHIN a session is kept. It is real evidence that caching does not pay there, and
// dropping it would bias the answer the other way -- making keep-warm look better than it is,
// which is the direction this project cares about most.
//
// Events carrying no sessionId are pooled into one group rather than discarded: most kinds do
// carry one, and discarding the rest would throw away whole projects' history for a technicality.
const bySession = new Map();
for (const event of events) {
if (!Number.isFinite(event?.at)) continue;
const key = event.sessionId || '';
if (!bySession.has(key)) bySession.set(key, []);
bySession.get(key).push(event.at);
}
const stamps = [...bySession.values()].flat();
if (stamps.length < 8) return null;
const gaps = [];
for (let i = 1; i < stamps.length; i++) {
const gap = stamps[i] - stamps[i - 1];
// A burst of events inside one turn is not a gap between turns.
if (gap > 250) gaps.push(gap);
for (const session of bySession.values()) {
session.sort((a, b) => a - b);
for (let i = 1; i < session.length; i++) {
const gap = session[i] - session[i - 1];
// A burst of events inside one turn is not a gap between turns.
if (gap > 250) gaps.push(gap);
}
}

@@ -142,3 +165,10 @@ if (gaps.length < 6) return null;

const perTurn = hit * READ_MULTIPLIER + (1 - hit) * tier.writeMultiplier;
const turns = Math.max(1, turnsPerSession);
// THE SUPPLIED VALUE, not only the computed one. Math.max(1, NaN) is NaN -- Math.max
// propagates it rather than clamping -- so perTurn became NaN, the `perTurn >= 1` guard below
// read `NaN >= 1` as false and PASSED, and this returned action:'refresh' with an
// expectedValue of NaN and a reason string reading "NaN% of gaps land inside 5m". A positive
// verdict built entirely out of NaN. The guard was on the wrong side of the computation.
const turns = Number.isFinite(turnsPerSession) && turnsPerSession >= 1
? turnsPerSession
: DEFAULT_TURNS;
return {

@@ -182,5 +212,17 @@ tier,

const tierSpec = TIERS.find((t) => t.name === tier) || TIERS[0];
// THE SAME LEDGER keepWarmDecision BUYS THE REFRESH WITH. That function is explicit that a
// refresh is a PING, which READS the prefix -- costOfPing = prefixTokens * READ_MULTIPLIER --
// and its comment warns that pricing the ping as a write "overstates its cost by more than
// twelvefold". This scored the very same refresh as a re-WRITE, charging (writeMultiplier - 1)
// on both branches.
//
// The disagreement runs in the direction that kills the feature. Mean realised per refresh under
// the old lines was p*(0.9h - 0.25), negative below a 27.8% hit rate, while the decision's own
// model makes the ping pay above 8.7%. For any project whose gaps fall in that band, `tripwire`
// accumulates a negative balance and permanently disables a policy that is genuinely paying --
// and tells the user "keep-warm has lost N tokens ... stopping". The backstop fired on its own
// accounting error rather than on a distribution shift.
const realised = hit
? prefixTokens * (1 - READ_MULTIPLIER) - prefixTokens * (tierSpec.writeMultiplier - 1)
: -prefixTokens * (tierSpec.writeMultiplier - 1);
? prefixTokens * (tierSpec.writeMultiplier - READ_MULTIPLIER) - prefixTokens * READ_MULTIPLIER
: -prefixTokens * READ_MULTIPLIER;
record(dir, { kind: 'keepwarm', action: 'outcome', tier, prefixTokens, hit: Boolean(hit), realised: Math.round(realised) });

@@ -197,3 +239,12 @@ }

*/
export function tripwire(dir, { events = readMetrics(dir) } = {}) {
/**
* The backstop, read from the log that is NOT windowed.
*
* readBalance rather than readMetrics, and that distinction is the difference between a backstop
* that works and one that cannot. There is one outcome per refresh, in a log dominated by reads
* and captures, so through the 5000-event window the ten TRIPWIRE_MIN demands aged out before the
* tenth was written -- and this returned "only N/10 refreshes observed" for the life of the
* project. A guard that can never reach its own threshold is not a guard.
*/
export function tripwire(dir, { events = readBalance(dir) } = {}) {
const outcomes = events.filter((e) => e.kind === 'keepwarm' && e.action === 'outcome');

@@ -223,4 +274,13 @@ if (outcomes.length < TRIPWIRE_MIN) {

*/
export function shouldKeepWarm(dir, { prefixTokens, events = readMetrics(dir) } = {}) {
const trip = tripwire(dir, { events });
export function shouldKeepWarm(dir, {
prefixTokens,
events = readMetrics(dir),
// TWO SOURCES, because the two consumers want different things. gapDistribution wants the
// firehose -- every event is a timestamp and the recent ones describe the current rhythm. The
// tripwire wants the unwindowed balance log, because its outcomes are rare and are exactly what
// the window evicts. Passing one shared array to both, as this used to, meant whichever reader
// was chosen was wrong for one of them.
outcomes = readBalance(dir),
} = {}) {
const trip = tripwire(dir, { events: outcomes });
if (trip.tripped) return { action: 'skip', reason: trip.reason, trippedWire: true };

@@ -231,3 +291,11 @@

if (!best) {
// THE TWO MODELS ANSWER DIFFERENT QUESTIONS, so they may legitimately disagree. ttlTier asks
// whether holding a cache beats not caching at all; keepWarmDecision asks whether ONE ping
// beats letting the entry lapse -- and a ping can pay where no tier does. Coercing everything
// that was not 'unknown' to 'skip' while keeping the decision's reason verbatim produced a
// refusal justified by a GAIN: `{ action: 'skip', reason: '...expected gain 130 tokens' }`.
// This module's own docstring says a refusal that cannot be checked is indistinguishable from
// a bug; one that contradicts itself is worse.
const decision = keepWarmDecision({ prefixTokens, gaps });
if (decision.action === 'refresh') return decision;
return { action: decision.action === 'unknown' ? 'unknown' : 'skip', reason: decision.reason };

@@ -234,0 +302,0 @@ }

@@ -33,2 +33,4 @@ // GENERATED FILE -- do not edit.

import { ORIGIN_HARVESTED, ORIGIN_HUMAN } from './curate.mjs';
import { safeTrigger } from './inject.mjs';
import { isFsSafePath } from './paths.mjs';

@@ -95,2 +97,9 @@ /** Lessons longer than this are prose, not instructions. */

const piece = rendered[i];
// A SINGLE oversize turn must not end the digest. One pasted stack trace in the last turn
// made the budget test below true on the FIRST iteration, so `out` stayed empty, this
// returned null, and harvest-worker skipped the entire feedback pass for that session --
// no extraction, no lesson validation, no metrics record, no signal anywhere. The loop runs
// backwards precisely to keep the end, and a big terminal turn is exactly what a session
// that went wrong tends to produce.
if (piece.length > maxChars) continue;
if (total + piece.length > maxChars) break;

@@ -169,3 +178,3 @@ out.unshift(piece);

*/
export function validateLessons(raw, turns) {
export function validateLessons(raw, turns, { knownFiles = null } = {}) {
let parsed = raw;

@@ -205,5 +214,13 @@ if (typeof raw === 'string') {

}
try {
new RegExp(trigger);
} catch {
// THE SAME GATE THE INJECTOR USES. `new RegExp` only proves the pattern COMPILES. inject.mjs
// additionally refuses sources over 200 characters and nested-quantifier ReDoS shapes -- and
// when it refuses, appliesToCommand falls back to a LITERAL substring search of the regex
// SOURCE against the command. A regex source is not a substring of any real command --
// a word-boundary-anchored pattern for the jest runner does not appear literally inside
// `npx jest --watch` --
// so a lesson stored with such a trigger is written to the graph, counted as delivered in the
// metrics record, and can never surface for the rest of its life. Both rejection shapes are
// realistic from the extraction prompt: a long alternation over test runners passes 200
// characters easily, and `(\w+\s*)+\.csproj` is the kind of thing a model emits unprompted.
if (!safeTrigger(trigger)) {
rejected.push({ reason: 'bad-trigger-regex', trigger });

@@ -229,4 +246,15 @@ continue;

confidence: verified ? 0.95 : 0.6,
// HELD TO THE FILES THE SESSION ACTUALLY TOUCHED, exactly as the finding path already is.
// harvest-worker calls validate() with `knownFiles: filesIn(digest)` and says why in its own
// comment -- "a model that invents a plausible path cannot anchor a finding to it" -- while
// this path accepted any non-empty string. The identical hallucination therefore walked
// straight through, and a lesson anchored to a file nobody opened is then injected on every
// future touch of it: a permanent instruction attached to code it was never about.
//
// isFsSafePath as well, because these strings reach the filesystem later through the graph.
// A path holding U+10FFFF ABORTS the process inside libuv rather than throwing, so no
// try/catch downstream can contain it.
anchors: Array.isArray(item?.anchors)
? item.anchors.filter((a) => typeof a === 'string' && a.trim())
? item.anchors.filter((a) => typeof a === 'string' && a.trim() && isFsSafePath(a)
&& (!knownFiles || knownFiles.includes(a)))
: [],

@@ -233,0 +261,0 @@ });

@@ -81,2 +81,7 @@ // GENERATED FILE -- do not edit.

'forecast-outcome',
// The keep-warm tripwire's evidence. It needs TRIPWIRE_MIN outcomes before it may have an
// opinion, and there is one outcome per refresh -- so through the windowed reader the tenth
// aged out before it was written and the backstop was structurally unable to fire, reporting
// "only N/10 refreshes observed" for the life of the project.
'keepwarm',
]);

@@ -83,0 +88,0 @@

@@ -29,3 +29,3 @@ // GENERATED FILE -- do not edit.

import { record, readMetrics } from './metrics.mjs';
import { record, readMetrics, readBalance } from './metrics.mjs';
import { WRITE_MULTIPLIER, READ_MULTIPLIER } from './cache.mjs';

@@ -60,10 +60,33 @@

export function gapDistribution(dir, { events = readMetrics(dir) } = {}) {
const stamps = events.map((e) => e.at).filter(Number.isFinite).sort((a, b) => a - b);
// PARTITIONED BY SESSION, because the interval between the last event of one session and the
// first of the next is not a gap between turns at all. The log spans days, so an overnight
// sixteen-hour boundary was counted as one -- dominating p90 and p99 and diluting
// probabilityWithin in the conservative direction, so ttlTier answered "neither tier pays" on
// projects where it would have paid. Silent, because that bias only ever declines to act.
//
// A long gap WITHIN a session is kept. It is real evidence that caching does not pay there, and
// dropping it would bias the answer the other way -- making keep-warm look better than it is,
// which is the direction this project cares about most.
//
// Events carrying no sessionId are pooled into one group rather than discarded: most kinds do
// carry one, and discarding the rest would throw away whole projects' history for a technicality.
const bySession = new Map();
for (const event of events) {
if (!Number.isFinite(event?.at)) continue;
const key = event.sessionId || '';
if (!bySession.has(key)) bySession.set(key, []);
bySession.get(key).push(event.at);
}
const stamps = [...bySession.values()].flat();
if (stamps.length < 8) return null;
const gaps = [];
for (let i = 1; i < stamps.length; i++) {
const gap = stamps[i] - stamps[i - 1];
// A burst of events inside one turn is not a gap between turns.
if (gap > 250) gaps.push(gap);
for (const session of bySession.values()) {
session.sort((a, b) => a - b);
for (let i = 1; i < session.length; i++) {
const gap = session[i] - session[i - 1];
// A burst of events inside one turn is not a gap between turns.
if (gap > 250) gaps.push(gap);
}
}

@@ -142,3 +165,10 @@ if (gaps.length < 6) return null;

const perTurn = hit * READ_MULTIPLIER + (1 - hit) * tier.writeMultiplier;
const turns = Math.max(1, turnsPerSession);
// THE SUPPLIED VALUE, not only the computed one. Math.max(1, NaN) is NaN -- Math.max
// propagates it rather than clamping -- so perTurn became NaN, the `perTurn >= 1` guard below
// read `NaN >= 1` as false and PASSED, and this returned action:'refresh' with an
// expectedValue of NaN and a reason string reading "NaN% of gaps land inside 5m". A positive
// verdict built entirely out of NaN. The guard was on the wrong side of the computation.
const turns = Number.isFinite(turnsPerSession) && turnsPerSession >= 1
? turnsPerSession
: DEFAULT_TURNS;
return {

@@ -182,5 +212,17 @@ tier,

const tierSpec = TIERS.find((t) => t.name === tier) || TIERS[0];
// THE SAME LEDGER keepWarmDecision BUYS THE REFRESH WITH. That function is explicit that a
// refresh is a PING, which READS the prefix -- costOfPing = prefixTokens * READ_MULTIPLIER --
// and its comment warns that pricing the ping as a write "overstates its cost by more than
// twelvefold". This scored the very same refresh as a re-WRITE, charging (writeMultiplier - 1)
// on both branches.
//
// The disagreement runs in the direction that kills the feature. Mean realised per refresh under
// the old lines was p*(0.9h - 0.25), negative below a 27.8% hit rate, while the decision's own
// model makes the ping pay above 8.7%. For any project whose gaps fall in that band, `tripwire`
// accumulates a negative balance and permanently disables a policy that is genuinely paying --
// and tells the user "keep-warm has lost N tokens ... stopping". The backstop fired on its own
// accounting error rather than on a distribution shift.
const realised = hit
? prefixTokens * (1 - READ_MULTIPLIER) - prefixTokens * (tierSpec.writeMultiplier - 1)
: -prefixTokens * (tierSpec.writeMultiplier - 1);
? prefixTokens * (tierSpec.writeMultiplier - READ_MULTIPLIER) - prefixTokens * READ_MULTIPLIER
: -prefixTokens * READ_MULTIPLIER;
record(dir, { kind: 'keepwarm', action: 'outcome', tier, prefixTokens, hit: Boolean(hit), realised: Math.round(realised) });

@@ -197,3 +239,12 @@ }

*/
export function tripwire(dir, { events = readMetrics(dir) } = {}) {
/**
* The backstop, read from the log that is NOT windowed.
*
* readBalance rather than readMetrics, and that distinction is the difference between a backstop
* that works and one that cannot. There is one outcome per refresh, in a log dominated by reads
* and captures, so through the 5000-event window the ten TRIPWIRE_MIN demands aged out before the
* tenth was written -- and this returned "only N/10 refreshes observed" for the life of the
* project. A guard that can never reach its own threshold is not a guard.
*/
export function tripwire(dir, { events = readBalance(dir) } = {}) {
const outcomes = events.filter((e) => e.kind === 'keepwarm' && e.action === 'outcome');

@@ -223,4 +274,13 @@ if (outcomes.length < TRIPWIRE_MIN) {

*/
export function shouldKeepWarm(dir, { prefixTokens, events = readMetrics(dir) } = {}) {
const trip = tripwire(dir, { events });
export function shouldKeepWarm(dir, {
prefixTokens,
events = readMetrics(dir),
// TWO SOURCES, because the two consumers want different things. gapDistribution wants the
// firehose -- every event is a timestamp and the recent ones describe the current rhythm. The
// tripwire wants the unwindowed balance log, because its outcomes are rare and are exactly what
// the window evicts. Passing one shared array to both, as this used to, meant whichever reader
// was chosen was wrong for one of them.
outcomes = readBalance(dir),
} = {}) {
const trip = tripwire(dir, { events: outcomes });
if (trip.tripped) return { action: 'skip', reason: trip.reason, trippedWire: true };

@@ -231,3 +291,11 @@

if (!best) {
// THE TWO MODELS ANSWER DIFFERENT QUESTIONS, so they may legitimately disagree. ttlTier asks
// whether holding a cache beats not caching at all; keepWarmDecision asks whether ONE ping
// beats letting the entry lapse -- and a ping can pay where no tier does. Coercing everything
// that was not 'unknown' to 'skip' while keeping the decision's reason verbatim produced a
// refusal justified by a GAIN: `{ action: 'skip', reason: '...expected gain 130 tokens' }`.
// This module's own docstring says a refusal that cannot be checked is indistinguishable from
// a bug; one that contradicts itself is worse.
const decision = keepWarmDecision({ prefixTokens, gaps });
if (decision.action === 'refresh') return decision;
return { action: decision.action === 'unknown' ? 'unknown' : 'skip', reason: decision.reason };

@@ -234,0 +302,0 @@ }

@@ -33,2 +33,4 @@ // GENERATED FILE -- do not edit.

import { ORIGIN_HARVESTED, ORIGIN_HUMAN } from './curate.mjs';
import { safeTrigger } from './inject.mjs';
import { isFsSafePath } from './paths.mjs';

@@ -95,2 +97,9 @@ /** Lessons longer than this are prose, not instructions. */

const piece = rendered[i];
// A SINGLE oversize turn must not end the digest. One pasted stack trace in the last turn
// made the budget test below true on the FIRST iteration, so `out` stayed empty, this
// returned null, and harvest-worker skipped the entire feedback pass for that session --
// no extraction, no lesson validation, no metrics record, no signal anywhere. The loop runs
// backwards precisely to keep the end, and a big terminal turn is exactly what a session
// that went wrong tends to produce.
if (piece.length > maxChars) continue;
if (total + piece.length > maxChars) break;

@@ -169,3 +178,3 @@ out.unshift(piece);

*/
export function validateLessons(raw, turns) {
export function validateLessons(raw, turns, { knownFiles = null } = {}) {
let parsed = raw;

@@ -205,5 +214,13 @@ if (typeof raw === 'string') {

}
try {
new RegExp(trigger);
} catch {
// THE SAME GATE THE INJECTOR USES. `new RegExp` only proves the pattern COMPILES. inject.mjs
// additionally refuses sources over 200 characters and nested-quantifier ReDoS shapes -- and
// when it refuses, appliesToCommand falls back to a LITERAL substring search of the regex
// SOURCE against the command. A regex source is not a substring of any real command --
// a word-boundary-anchored pattern for the jest runner does not appear literally inside
// `npx jest --watch` --
// so a lesson stored with such a trigger is written to the graph, counted as delivered in the
// metrics record, and can never surface for the rest of its life. Both rejection shapes are
// realistic from the extraction prompt: a long alternation over test runners passes 200
// characters easily, and `(\w+\s*)+\.csproj` is the kind of thing a model emits unprompted.
if (!safeTrigger(trigger)) {
rejected.push({ reason: 'bad-trigger-regex', trigger });

@@ -229,4 +246,15 @@ continue;

confidence: verified ? 0.95 : 0.6,
// HELD TO THE FILES THE SESSION ACTUALLY TOUCHED, exactly as the finding path already is.
// harvest-worker calls validate() with `knownFiles: filesIn(digest)` and says why in its own
// comment -- "a model that invents a plausible path cannot anchor a finding to it" -- while
// this path accepted any non-empty string. The identical hallucination therefore walked
// straight through, and a lesson anchored to a file nobody opened is then injected on every
// future touch of it: a permanent instruction attached to code it was never about.
//
// isFsSafePath as well, because these strings reach the filesystem later through the graph.
// A path holding U+10FFFF ABORTS the process inside libuv rather than throwing, so no
// try/catch downstream can contain it.
anchors: Array.isArray(item?.anchors)
? item.anchors.filter((a) => typeof a === 'string' && a.trim())
? item.anchors.filter((a) => typeof a === 'string' && a.trim() && isFsSafePath(a)
&& (!knownFiles || knownFiles.includes(a)))
: [],

@@ -233,0 +261,0 @@ });

@@ -81,2 +81,7 @@ // GENERATED FILE -- do not edit.

'forecast-outcome',
// The keep-warm tripwire's evidence. It needs TRIPWIRE_MIN outcomes before it may have an
// opinion, and there is one outcome per refresh -- so through the windowed reader the tenth
// aged out before it was written and the backstop was structurally unable to fire, reporting
// "only N/10 refreshes observed" for the life of the project.
'keepwarm',
]);

@@ -83,0 +88,0 @@

@@ -29,3 +29,3 @@ // GENERATED FILE -- do not edit.

import { record, readMetrics } from './metrics.mjs';
import { record, readMetrics, readBalance } from './metrics.mjs';
import { WRITE_MULTIPLIER, READ_MULTIPLIER } from './cache.mjs';

@@ -60,10 +60,33 @@

export function gapDistribution(dir, { events = readMetrics(dir) } = {}) {
const stamps = events.map((e) => e.at).filter(Number.isFinite).sort((a, b) => a - b);
// PARTITIONED BY SESSION, because the interval between the last event of one session and the
// first of the next is not a gap between turns at all. The log spans days, so an overnight
// sixteen-hour boundary was counted as one -- dominating p90 and p99 and diluting
// probabilityWithin in the conservative direction, so ttlTier answered "neither tier pays" on
// projects where it would have paid. Silent, because that bias only ever declines to act.
//
// A long gap WITHIN a session is kept. It is real evidence that caching does not pay there, and
// dropping it would bias the answer the other way -- making keep-warm look better than it is,
// which is the direction this project cares about most.
//
// Events carrying no sessionId are pooled into one group rather than discarded: most kinds do
// carry one, and discarding the rest would throw away whole projects' history for a technicality.
const bySession = new Map();
for (const event of events) {
if (!Number.isFinite(event?.at)) continue;
const key = event.sessionId || '';
if (!bySession.has(key)) bySession.set(key, []);
bySession.get(key).push(event.at);
}
const stamps = [...bySession.values()].flat();
if (stamps.length < 8) return null;
const gaps = [];
for (let i = 1; i < stamps.length; i++) {
const gap = stamps[i] - stamps[i - 1];
// A burst of events inside one turn is not a gap between turns.
if (gap > 250) gaps.push(gap);
for (const session of bySession.values()) {
session.sort((a, b) => a - b);
for (let i = 1; i < session.length; i++) {
const gap = session[i] - session[i - 1];
// A burst of events inside one turn is not a gap between turns.
if (gap > 250) gaps.push(gap);
}
}

@@ -142,3 +165,10 @@ if (gaps.length < 6) return null;

const perTurn = hit * READ_MULTIPLIER + (1 - hit) * tier.writeMultiplier;
const turns = Math.max(1, turnsPerSession);
// THE SUPPLIED VALUE, not only the computed one. Math.max(1, NaN) is NaN -- Math.max
// propagates it rather than clamping -- so perTurn became NaN, the `perTurn >= 1` guard below
// read `NaN >= 1` as false and PASSED, and this returned action:'refresh' with an
// expectedValue of NaN and a reason string reading "NaN% of gaps land inside 5m". A positive
// verdict built entirely out of NaN. The guard was on the wrong side of the computation.
const turns = Number.isFinite(turnsPerSession) && turnsPerSession >= 1
? turnsPerSession
: DEFAULT_TURNS;
return {

@@ -182,5 +212,17 @@ tier,

const tierSpec = TIERS.find((t) => t.name === tier) || TIERS[0];
// THE SAME LEDGER keepWarmDecision BUYS THE REFRESH WITH. That function is explicit that a
// refresh is a PING, which READS the prefix -- costOfPing = prefixTokens * READ_MULTIPLIER --
// and its comment warns that pricing the ping as a write "overstates its cost by more than
// twelvefold". This scored the very same refresh as a re-WRITE, charging (writeMultiplier - 1)
// on both branches.
//
// The disagreement runs in the direction that kills the feature. Mean realised per refresh under
// the old lines was p*(0.9h - 0.25), negative below a 27.8% hit rate, while the decision's own
// model makes the ping pay above 8.7%. For any project whose gaps fall in that band, `tripwire`
// accumulates a negative balance and permanently disables a policy that is genuinely paying --
// and tells the user "keep-warm has lost N tokens ... stopping". The backstop fired on its own
// accounting error rather than on a distribution shift.
const realised = hit
? prefixTokens * (1 - READ_MULTIPLIER) - prefixTokens * (tierSpec.writeMultiplier - 1)
: -prefixTokens * (tierSpec.writeMultiplier - 1);
? prefixTokens * (tierSpec.writeMultiplier - READ_MULTIPLIER) - prefixTokens * READ_MULTIPLIER
: -prefixTokens * READ_MULTIPLIER;
record(dir, { kind: 'keepwarm', action: 'outcome', tier, prefixTokens, hit: Boolean(hit), realised: Math.round(realised) });

@@ -197,3 +239,12 @@ }

*/
export function tripwire(dir, { events = readMetrics(dir) } = {}) {
/**
* The backstop, read from the log that is NOT windowed.
*
* readBalance rather than readMetrics, and that distinction is the difference between a backstop
* that works and one that cannot. There is one outcome per refresh, in a log dominated by reads
* and captures, so through the 5000-event window the ten TRIPWIRE_MIN demands aged out before the
* tenth was written -- and this returned "only N/10 refreshes observed" for the life of the
* project. A guard that can never reach its own threshold is not a guard.
*/
export function tripwire(dir, { events = readBalance(dir) } = {}) {
const outcomes = events.filter((e) => e.kind === 'keepwarm' && e.action === 'outcome');

@@ -223,4 +274,13 @@ if (outcomes.length < TRIPWIRE_MIN) {

*/
export function shouldKeepWarm(dir, { prefixTokens, events = readMetrics(dir) } = {}) {
const trip = tripwire(dir, { events });
export function shouldKeepWarm(dir, {
prefixTokens,
events = readMetrics(dir),
// TWO SOURCES, because the two consumers want different things. gapDistribution wants the
// firehose -- every event is a timestamp and the recent ones describe the current rhythm. The
// tripwire wants the unwindowed balance log, because its outcomes are rare and are exactly what
// the window evicts. Passing one shared array to both, as this used to, meant whichever reader
// was chosen was wrong for one of them.
outcomes = readBalance(dir),
} = {}) {
const trip = tripwire(dir, { events: outcomes });
if (trip.tripped) return { action: 'skip', reason: trip.reason, trippedWire: true };

@@ -231,3 +291,11 @@

if (!best) {
// THE TWO MODELS ANSWER DIFFERENT QUESTIONS, so they may legitimately disagree. ttlTier asks
// whether holding a cache beats not caching at all; keepWarmDecision asks whether ONE ping
// beats letting the entry lapse -- and a ping can pay where no tier does. Coercing everything
// that was not 'unknown' to 'skip' while keeping the decision's reason verbatim produced a
// refusal justified by a GAIN: `{ action: 'skip', reason: '...expected gain 130 tokens' }`.
// This module's own docstring says a refusal that cannot be checked is indistinguishable from
// a bug; one that contradicts itself is worse.
const decision = keepWarmDecision({ prefixTokens, gaps });
if (decision.action === 'refresh') return decision;
return { action: decision.action === 'unknown' ? 'unknown' : 'skip', reason: decision.reason };

@@ -234,0 +302,0 @@ }

@@ -33,2 +33,4 @@ // GENERATED FILE -- do not edit.

import { ORIGIN_HARVESTED, ORIGIN_HUMAN } from './curate.mjs';
import { safeTrigger } from './inject.mjs';
import { isFsSafePath } from './paths.mjs';

@@ -95,2 +97,9 @@ /** Lessons longer than this are prose, not instructions. */

const piece = rendered[i];
// A SINGLE oversize turn must not end the digest. One pasted stack trace in the last turn
// made the budget test below true on the FIRST iteration, so `out` stayed empty, this
// returned null, and harvest-worker skipped the entire feedback pass for that session --
// no extraction, no lesson validation, no metrics record, no signal anywhere. The loop runs
// backwards precisely to keep the end, and a big terminal turn is exactly what a session
// that went wrong tends to produce.
if (piece.length > maxChars) continue;
if (total + piece.length > maxChars) break;

@@ -169,3 +178,3 @@ out.unshift(piece);

*/
export function validateLessons(raw, turns) {
export function validateLessons(raw, turns, { knownFiles = null } = {}) {
let parsed = raw;

@@ -205,5 +214,13 @@ if (typeof raw === 'string') {

}
try {
new RegExp(trigger);
} catch {
// THE SAME GATE THE INJECTOR USES. `new RegExp` only proves the pattern COMPILES. inject.mjs
// additionally refuses sources over 200 characters and nested-quantifier ReDoS shapes -- and
// when it refuses, appliesToCommand falls back to a LITERAL substring search of the regex
// SOURCE against the command. A regex source is not a substring of any real command --
// a word-boundary-anchored pattern for the jest runner does not appear literally inside
// `npx jest --watch` --
// so a lesson stored with such a trigger is written to the graph, counted as delivered in the
// metrics record, and can never surface for the rest of its life. Both rejection shapes are
// realistic from the extraction prompt: a long alternation over test runners passes 200
// characters easily, and `(\w+\s*)+\.csproj` is the kind of thing a model emits unprompted.
if (!safeTrigger(trigger)) {
rejected.push({ reason: 'bad-trigger-regex', trigger });

@@ -229,4 +246,15 @@ continue;

confidence: verified ? 0.95 : 0.6,
// HELD TO THE FILES THE SESSION ACTUALLY TOUCHED, exactly as the finding path already is.
// harvest-worker calls validate() with `knownFiles: filesIn(digest)` and says why in its own
// comment -- "a model that invents a plausible path cannot anchor a finding to it" -- while
// this path accepted any non-empty string. The identical hallucination therefore walked
// straight through, and a lesson anchored to a file nobody opened is then injected on every
// future touch of it: a permanent instruction attached to code it was never about.
//
// isFsSafePath as well, because these strings reach the filesystem later through the graph.
// A path holding U+10FFFF ABORTS the process inside libuv rather than throwing, so no
// try/catch downstream can contain it.
anchors: Array.isArray(item?.anchors)
? item.anchors.filter((a) => typeof a === 'string' && a.trim())
? item.anchors.filter((a) => typeof a === 'string' && a.trim() && isFsSafePath(a)
&& (!knownFiles || knownFiles.includes(a)))
: [],

@@ -233,0 +261,0 @@ });

@@ -81,2 +81,7 @@ // GENERATED FILE -- do not edit.

'forecast-outcome',
// The keep-warm tripwire's evidence. It needs TRIPWIRE_MIN outcomes before it may have an
// opinion, and there is one outcome per refresh -- so through the windowed reader the tenth
// aged out before it was written and the backstop was structurally unable to fire, reporting
// "only N/10 refreshes observed" for the life of the project.
'keepwarm',
]);

@@ -83,0 +88,0 @@

{
"name": "@ooples/token-optimizer-mcp",
"version": "5.6.0",
"version": "5.6.1",
"mcpName": "io.github.ooples/token-optimizer-mcp",

@@ -5,0 +5,0 @@ "description": "Intelligent context window optimization for Claude Code - store content externally via caching and compression, freeing up your context window for what matters",

{
"name": "token-optimizer",
"description": "Aggressive context-window optimization by DEFAULT: enforcing hooks route expensive built-in file, search and shell calls to cached/diffed MCP tools, cutting token usage 60-90% without the user configuring anything.",
"version": "5.6.0",
"version": "5.6.1",
"author": {

@@ -6,0 +6,0 @@ "name": "ooples"

@@ -97,3 +97,8 @@ #!/usr/bin/env node

const rawLessons = await extract(feedback, { prompt: LESSON_PROMPT });
const { lessons, rejected } = validateLessons(rawLessons, turns);
// The same restriction the finding path above applies, for the same reason: a model that
// invents a plausible path must not be able to anchor a lesson to it. `turns` is the
// archived transcript for this session, so its rendered digest is the honest file list.
const { lessons, rejected } = validateLessons(rawLessons, turns, {
knownFiles: filesIn(feedback),
});

@@ -100,0 +105,0 @@ // Anchors are optional on a lesson -- "always run npm test" is about no

@@ -29,3 +29,3 @@ // GENERATED FILE -- do not edit.

import { record, readMetrics } from './metrics.mjs';
import { record, readMetrics, readBalance } from './metrics.mjs';
import { WRITE_MULTIPLIER, READ_MULTIPLIER } from './cache.mjs';

@@ -60,10 +60,33 @@

export function gapDistribution(dir, { events = readMetrics(dir) } = {}) {
const stamps = events.map((e) => e.at).filter(Number.isFinite).sort((a, b) => a - b);
// PARTITIONED BY SESSION, because the interval between the last event of one session and the
// first of the next is not a gap between turns at all. The log spans days, so an overnight
// sixteen-hour boundary was counted as one -- dominating p90 and p99 and diluting
// probabilityWithin in the conservative direction, so ttlTier answered "neither tier pays" on
// projects where it would have paid. Silent, because that bias only ever declines to act.
//
// A long gap WITHIN a session is kept. It is real evidence that caching does not pay there, and
// dropping it would bias the answer the other way -- making keep-warm look better than it is,
// which is the direction this project cares about most.
//
// Events carrying no sessionId are pooled into one group rather than discarded: most kinds do
// carry one, and discarding the rest would throw away whole projects' history for a technicality.
const bySession = new Map();
for (const event of events) {
if (!Number.isFinite(event?.at)) continue;
const key = event.sessionId || '';
if (!bySession.has(key)) bySession.set(key, []);
bySession.get(key).push(event.at);
}
const stamps = [...bySession.values()].flat();
if (stamps.length < 8) return null;
const gaps = [];
for (let i = 1; i < stamps.length; i++) {
const gap = stamps[i] - stamps[i - 1];
// A burst of events inside one turn is not a gap between turns.
if (gap > 250) gaps.push(gap);
for (const session of bySession.values()) {
session.sort((a, b) => a - b);
for (let i = 1; i < session.length; i++) {
const gap = session[i] - session[i - 1];
// A burst of events inside one turn is not a gap between turns.
if (gap > 250) gaps.push(gap);
}
}

@@ -142,3 +165,10 @@ if (gaps.length < 6) return null;

const perTurn = hit * READ_MULTIPLIER + (1 - hit) * tier.writeMultiplier;
const turns = Math.max(1, turnsPerSession);
// THE SUPPLIED VALUE, not only the computed one. Math.max(1, NaN) is NaN -- Math.max
// propagates it rather than clamping -- so perTurn became NaN, the `perTurn >= 1` guard below
// read `NaN >= 1` as false and PASSED, and this returned action:'refresh' with an
// expectedValue of NaN and a reason string reading "NaN% of gaps land inside 5m". A positive
// verdict built entirely out of NaN. The guard was on the wrong side of the computation.
const turns = Number.isFinite(turnsPerSession) && turnsPerSession >= 1
? turnsPerSession
: DEFAULT_TURNS;
return {

@@ -182,5 +212,17 @@ tier,

const tierSpec = TIERS.find((t) => t.name === tier) || TIERS[0];
// THE SAME LEDGER keepWarmDecision BUYS THE REFRESH WITH. That function is explicit that a
// refresh is a PING, which READS the prefix -- costOfPing = prefixTokens * READ_MULTIPLIER --
// and its comment warns that pricing the ping as a write "overstates its cost by more than
// twelvefold". This scored the very same refresh as a re-WRITE, charging (writeMultiplier - 1)
// on both branches.
//
// The disagreement runs in the direction that kills the feature. Mean realised per refresh under
// the old lines was p*(0.9h - 0.25), negative below a 27.8% hit rate, while the decision's own
// model makes the ping pay above 8.7%. For any project whose gaps fall in that band, `tripwire`
// accumulates a negative balance and permanently disables a policy that is genuinely paying --
// and tells the user "keep-warm has lost N tokens ... stopping". The backstop fired on its own
// accounting error rather than on a distribution shift.
const realised = hit
? prefixTokens * (1 - READ_MULTIPLIER) - prefixTokens * (tierSpec.writeMultiplier - 1)
: -prefixTokens * (tierSpec.writeMultiplier - 1);
? prefixTokens * (tierSpec.writeMultiplier - READ_MULTIPLIER) - prefixTokens * READ_MULTIPLIER
: -prefixTokens * READ_MULTIPLIER;
record(dir, { kind: 'keepwarm', action: 'outcome', tier, prefixTokens, hit: Boolean(hit), realised: Math.round(realised) });

@@ -197,3 +239,12 @@ }

*/
export function tripwire(dir, { events = readMetrics(dir) } = {}) {
/**
* The backstop, read from the log that is NOT windowed.
*
* readBalance rather than readMetrics, and that distinction is the difference between a backstop
* that works and one that cannot. There is one outcome per refresh, in a log dominated by reads
* and captures, so through the 5000-event window the ten TRIPWIRE_MIN demands aged out before the
* tenth was written -- and this returned "only N/10 refreshes observed" for the life of the
* project. A guard that can never reach its own threshold is not a guard.
*/
export function tripwire(dir, { events = readBalance(dir) } = {}) {
const outcomes = events.filter((e) => e.kind === 'keepwarm' && e.action === 'outcome');

@@ -223,4 +274,13 @@ if (outcomes.length < TRIPWIRE_MIN) {

*/
export function shouldKeepWarm(dir, { prefixTokens, events = readMetrics(dir) } = {}) {
const trip = tripwire(dir, { events });
export function shouldKeepWarm(dir, {
prefixTokens,
events = readMetrics(dir),
// TWO SOURCES, because the two consumers want different things. gapDistribution wants the
// firehose -- every event is a timestamp and the recent ones describe the current rhythm. The
// tripwire wants the unwindowed balance log, because its outcomes are rare and are exactly what
// the window evicts. Passing one shared array to both, as this used to, meant whichever reader
// was chosen was wrong for one of them.
outcomes = readBalance(dir),
} = {}) {
const trip = tripwire(dir, { events: outcomes });
if (trip.tripped) return { action: 'skip', reason: trip.reason, trippedWire: true };

@@ -231,3 +291,11 @@

if (!best) {
// THE TWO MODELS ANSWER DIFFERENT QUESTIONS, so they may legitimately disagree. ttlTier asks
// whether holding a cache beats not caching at all; keepWarmDecision asks whether ONE ping
// beats letting the entry lapse -- and a ping can pay where no tier does. Coercing everything
// that was not 'unknown' to 'skip' while keeping the decision's reason verbatim produced a
// refusal justified by a GAIN: `{ action: 'skip', reason: '...expected gain 130 tokens' }`.
// This module's own docstring says a refusal that cannot be checked is indistinguishable from
// a bug; one that contradicts itself is worse.
const decision = keepWarmDecision({ prefixTokens, gaps });
if (decision.action === 'refresh') return decision;
return { action: decision.action === 'unknown' ? 'unknown' : 'skip', reason: decision.reason };

@@ -234,0 +302,0 @@ }

@@ -33,2 +33,4 @@ // GENERATED FILE -- do not edit.

import { ORIGIN_HARVESTED, ORIGIN_HUMAN } from './curate.mjs';
import { safeTrigger } from './inject.mjs';
import { isFsSafePath } from './paths.mjs';

@@ -95,2 +97,9 @@ /** Lessons longer than this are prose, not instructions. */

const piece = rendered[i];
// A SINGLE oversize turn must not end the digest. One pasted stack trace in the last turn
// made the budget test below true on the FIRST iteration, so `out` stayed empty, this
// returned null, and harvest-worker skipped the entire feedback pass for that session --
// no extraction, no lesson validation, no metrics record, no signal anywhere. The loop runs
// backwards precisely to keep the end, and a big terminal turn is exactly what a session
// that went wrong tends to produce.
if (piece.length > maxChars) continue;
if (total + piece.length > maxChars) break;

@@ -169,3 +178,3 @@ out.unshift(piece);

*/
export function validateLessons(raw, turns) {
export function validateLessons(raw, turns, { knownFiles = null } = {}) {
let parsed = raw;

@@ -205,5 +214,13 @@ if (typeof raw === 'string') {

}
try {
new RegExp(trigger);
} catch {
// THE SAME GATE THE INJECTOR USES. `new RegExp` only proves the pattern COMPILES. inject.mjs
// additionally refuses sources over 200 characters and nested-quantifier ReDoS shapes -- and
// when it refuses, appliesToCommand falls back to a LITERAL substring search of the regex
// SOURCE against the command. A regex source is not a substring of any real command --
// a word-boundary-anchored pattern for the jest runner does not appear literally inside
// `npx jest --watch` --
// so a lesson stored with such a trigger is written to the graph, counted as delivered in the
// metrics record, and can never surface for the rest of its life. Both rejection shapes are
// realistic from the extraction prompt: a long alternation over test runners passes 200
// characters easily, and `(\w+\s*)+\.csproj` is the kind of thing a model emits unprompted.
if (!safeTrigger(trigger)) {
rejected.push({ reason: 'bad-trigger-regex', trigger });

@@ -229,4 +246,15 @@ continue;

confidence: verified ? 0.95 : 0.6,
// HELD TO THE FILES THE SESSION ACTUALLY TOUCHED, exactly as the finding path already is.
// harvest-worker calls validate() with `knownFiles: filesIn(digest)` and says why in its own
// comment -- "a model that invents a plausible path cannot anchor a finding to it" -- while
// this path accepted any non-empty string. The identical hallucination therefore walked
// straight through, and a lesson anchored to a file nobody opened is then injected on every
// future touch of it: a permanent instruction attached to code it was never about.
//
// isFsSafePath as well, because these strings reach the filesystem later through the graph.
// A path holding U+10FFFF ABORTS the process inside libuv rather than throwing, so no
// try/catch downstream can contain it.
anchors: Array.isArray(item?.anchors)
? item.anchors.filter((a) => typeof a === 'string' && a.trim())
? item.anchors.filter((a) => typeof a === 'string' && a.trim() && isFsSafePath(a)
&& (!knownFiles || knownFiles.includes(a)))
: [],

@@ -233,0 +261,0 @@ });

@@ -81,2 +81,7 @@ // GENERATED FILE -- do not edit.

'forecast-outcome',
// The keep-warm tripwire's evidence. It needs TRIPWIRE_MIN outcomes before it may have an
// opinion, and there is one outcome per refresh -- so through the windowed reader the tenth
// aged out before it was written and the backstop was structurally unable to fire, reporting
// "only N/10 refreshes observed" for the life of the project.
'keepwarm',
]);

@@ -83,0 +88,0 @@