@causyne/agent 1.10.12 → 1.10.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/docs/ARCHITECTURE.md +136 -7
- package/docs/SECURITY_REVIEW.md +13 -1
- package/docs/THIRD_PARTY_NOTICES.md +43 -10
- package/package.json +11 -4
- package/src/agentLoop.js +161 -93
- package/src/agentProfiles.js +135 -0
- package/src/agentProfiles.ts +143 -0
- package/src/approvalGrants.js +17 -35
- package/src/approvalGrants.ts +54 -0
- package/src/auditLog.d.ts +3 -0
- package/src/benchmarks/planCache.js +159 -0
- package/src/browser/camofoxRuntime.js +93 -0
- package/src/browser/camofoxRuntime.ts +112 -0
- package/src/browser/causyneBrowser.js +495 -0
- package/src/browser/causyneBrowser.ts +594 -0
- package/src/browser/searchProviders.js +156 -0
- package/src/browser/searchProviders.ts +156 -0
- package/src/browser/snapshotWindow.js +28 -0
- package/src/browser/snapshotWindow.ts +40 -0
- package/src/browser/urlPolicy.js +89 -0
- package/src/browser/urlPolicy.ts +89 -0
- package/src/checkpoints.js +19 -6
- package/src/commands.js +120 -8
- package/src/config.d.ts +2 -0
- package/src/cortex/autoCondenser.js +1 -1
- package/src/hooks.js +150 -0
- package/src/hooks.ts +193 -0
- package/src/mcp/mcpClient.js +520 -132
- package/src/mcp/mcpClient.ts +584 -0
- package/src/mcp/mcpRegistry.js +399 -185
- package/src/mcp/mcpRegistry.ts +451 -0
- package/src/planCache.js +529 -225
- package/src/planCache.ts +840 -0
- package/src/plugins/pluginManager.js +460 -0
- package/src/plugins/pluginRunner.js +50 -0
- package/src/plugins/pluginSkills.js +84 -0
- package/src/plugins/pluginSkills.ts +106 -0
- package/src/provisioning/capabilityProvisioner.d.ts +10 -0
- package/src/provisioning/capabilityProvisioner.js +5 -0
- package/src/provisioning/capabilityTools.js +2 -1
- package/src/sandbox.js +53 -2
- package/src/sessionJournal.js +471 -0
- package/src/sessionJournal.ts +496 -0
- package/src/sessionStore.js +3 -279
- package/src/skillsManager.js +18 -0
- package/src/subagentJournal.js +215 -0
- package/src/subagentJournal.ts +254 -0
- package/src/taskShape.js +179 -198
- package/src/taskShape.ts +259 -0
- package/src/toolPacks.js +6 -3
- package/src/tools/browserAutomator.js +45 -241
- package/src/tools/browserAutomator.ts +57 -0
- package/src/tools/deferredMcpSearch.js +2 -2
- package/src/tools/manageLocalAiDependencies.js +1 -1
- package/src/tools/pluginManager.js +61 -0
- package/src/tools/registry.js +47 -53
- package/src/tools/subagentOrchestrator.js +73 -12
- package/src/tools/toolForge.js +47 -95
- package/src/tools/webFetch.js +183 -68
- package/src/tools/webFetch.ts +185 -0
- package/src/tools/webSearch.js +50 -75
- package/src/tools/webSearch.ts +56 -0
- package/src/types/contracts.js +8 -0
- package/src/types/contracts.ts +263 -0
- package/src/utils/paths.d.ts +3 -0
- package/src/version.d.ts +1 -0
- package/src/mcp/jsonRpc.js +0 -165
package/src/planCache.js
CHANGED
|
@@ -2,7 +2,6 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import { causyneHome } from './utils/paths.js';
|
|
5
|
-
|
|
6
5
|
// Agentic Plan Caching.
|
|
7
6
|
//
|
|
8
7
|
// THE WASTE THIS ATTACKS. The agent rediscovers the same approach from scratch
|
|
@@ -32,10 +31,9 @@ import { causyneHome } from './utils/paths.js';
|
|
|
32
31
|
// WHAT IS STORED. Tool names, a short whitelisted action token, and whether the
|
|
33
32
|
// step worked. No paths, no arguments, no file contents, no command lines. A
|
|
34
33
|
// procedure, not a transcript.
|
|
35
|
-
|
|
36
34
|
const PLAN_DIR = path.join(causyneHome(), 'plans');
|
|
37
35
|
const PLAN_FILE = path.join(PLAN_DIR, 'index.json');
|
|
38
|
-
|
|
36
|
+
const METRICS_FILE = path.join(PLAN_DIR, 'metrics.json');
|
|
39
37
|
/** Two plans must overlap this much on keywords before one is offered for the other. */
|
|
40
38
|
// Tuned against the asymmetry, not for elegance. A false positive costs a few
|
|
41
39
|
// tokens of clearly-labelled prior art the model is told to verify. A false
|
|
@@ -43,47 +41,57 @@ const PLAN_FILE = path.join(PLAN_DIR, 'index.json');
|
|
|
43
41
|
// ended a real run at the daily ceiling. Archetype is already a hard gate, so
|
|
44
42
|
// the remaining risk is "same archetype, different task", and that is cheap.
|
|
45
43
|
const MATCH_THRESHOLD = 0.38;
|
|
46
|
-
|
|
47
44
|
/** Ceiling on retained templates. Pruned by usefulness, not by age alone. */
|
|
48
45
|
const MAX_PLANS = 200;
|
|
49
|
-
|
|
50
46
|
/** A plan longer than this is a transcript, not a procedure. */
|
|
51
47
|
const MAX_STEPS = 24;
|
|
52
|
-
|
|
53
48
|
/** Keywords kept per plan. Enough to discriminate, short enough to stay cheap. */
|
|
54
49
|
const MAX_KEYWORDS = 12;
|
|
55
|
-
|
|
50
|
+
/** Exact keyword-set hits stay O(1); fuzzy matching is a bounded fallback. */
|
|
51
|
+
const RECENT_LOOKUP_WINDOW = 32;
|
|
52
|
+
const MIN_LOOKUPS_BEFORE_DISABLE = 20;
|
|
53
|
+
const MIN_PLANS_BEFORE_DISABLE = 10;
|
|
54
|
+
const MIN_HEALTHY_HIT_RATE = 0.10;
|
|
55
|
+
const DISABLE_COOLDOWN_MS = 5 * 60 * 1000;
|
|
56
|
+
const SEED_FAILURE_CLASSES = new Set([
|
|
57
|
+
'not_found', 'permission', 'timeout', 'network', 'rate_limited',
|
|
58
|
+
'invalid_args', 'syntax', 'exit_nonzero', 'failed'
|
|
59
|
+
]);
|
|
60
|
+
const METRICS_FLUSH_INTERVAL = 10;
|
|
61
|
+
let plansCache = null;
|
|
62
|
+
let plansMtimeMs = -1;
|
|
63
|
+
let exactIndex = new Map();
|
|
64
|
+
let metricsCache = null;
|
|
65
|
+
let dirtyMetricEvents = 0;
|
|
56
66
|
/**
|
|
57
67
|
* Words that carry no intent. Deliberately small: an aggressive list throws
|
|
58
68
|
* away the domain nouns that make two tasks comparable.
|
|
59
69
|
* @type {ReadonlySet<string>}
|
|
60
70
|
*/
|
|
61
71
|
const STOPWORDS = new Set([
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
72
|
+
'the', 'a', 'an', 'and', 'or', 'but', 'for', 'to', 'of', 'in', 'on', 'at',
|
|
73
|
+
'is', 'are', 'be', 'was', 'were', 'do', 'does', 'did', 'can', 'could',
|
|
74
|
+
'with', 'from', 'into', 'then', 'this', 'that', 'it', 'its', 'as', 'by',
|
|
75
|
+
'please', 'just', 'now', 'my', 'me', 'you', 'your', 'i',
|
|
76
|
+
// Generic creation verbs. Every task has one, so they inflate the union
|
|
77
|
+
// without discriminating between tasks — the exact shape that made
|
|
78
|
+
// "buat pena 3D di Blender" and "bikin pulpen 3D di blender" score 0.40
|
|
79
|
+
// when they describe the same job. `build`, `fix`, `debug` and `render` are
|
|
80
|
+
// deliberately NOT here: those DO separate tasks inside one archetype.
|
|
81
|
+
'make', 'create', 'creating', 'made', 'generate',
|
|
82
|
+
// Indonesian — the owner writes in both, and a mixed-language prompt must
|
|
83
|
+
// key the same way whichever language framed it.
|
|
84
|
+
'dan', 'atau', 'yang', 'untuk', 'dari', 'ke', 'di', 'itu', 'ini', 'dengan',
|
|
85
|
+
'tolong', 'saya', 'aku', 'kamu', 'adalah', 'pada', 'juga', 'agar', 'jadi',
|
|
86
|
+
'buat', 'bikin', 'membuat', 'lalu', 'kemudian', 'terus', 'dulu', 'saja',
|
|
87
|
+
'sekarang', 'harus', 'bisa', 'sudah', 'belum', 'akan', 'mau', 'coba'
|
|
78
88
|
]);
|
|
79
|
-
|
|
80
89
|
// HONEST LIMIT. Matching is LEXICAL. A task cached in English and re-asked in
|
|
81
90
|
// Indonesian (or the reverse) will miss, because Jaccard cannot know that
|
|
82
91
|
// "pen" and "pena" are the same object. That is a deliberate trade: this path
|
|
83
92
|
// is deterministic, instant, and works on a machine with no embedding model
|
|
84
93
|
// provisioned. Cortex is the semantic route and already carries embeddings;
|
|
85
94
|
// these two compose rather than compete.
|
|
86
|
-
|
|
87
95
|
/**
|
|
88
96
|
* One executed step, reduced to what is reusable.
|
|
89
97
|
*
|
|
@@ -99,7 +107,6 @@ const STOPWORDS = new Set([
|
|
|
99
107
|
* @property {string} [why] Failure CLASS when ok is false. A closed vocabulary,
|
|
100
108
|
* never the error text.
|
|
101
109
|
*/
|
|
102
|
-
|
|
103
110
|
/**
|
|
104
111
|
* @typedef {Object} PlanTemplate
|
|
105
112
|
* @property {string} id
|
|
@@ -111,46 +118,225 @@ const STOPWORDS = new Set([
|
|
|
111
118
|
* @property {number} createdAt
|
|
112
119
|
* @property {number} updatedAt
|
|
113
120
|
*/
|
|
114
|
-
|
|
115
121
|
/**
|
|
116
122
|
* @typedef {Object} PlanMatch
|
|
117
123
|
* @property {PlanTemplate} template
|
|
118
124
|
* @property {number} similarity
|
|
125
|
+
* @property {'exact'|'fuzzy'} matchType
|
|
126
|
+
* @property {{keywordCount: number, lookupMs: number}} telemetry
|
|
119
127
|
*/
|
|
120
|
-
|
|
121
128
|
function ensureDir() {
|
|
122
|
-
|
|
129
|
+
if (!fs.existsSync(PLAN_DIR))
|
|
130
|
+
fs.mkdirSync(PLAN_DIR, { recursive: true, mode: 0o700 });
|
|
131
|
+
}
|
|
132
|
+
/** @returns {string} */
|
|
133
|
+
function planSignature(archetype, keywords) {
|
|
134
|
+
return `${String(archetype || '')}\u0000${[...new Set(keywords || [])].sort().join('\u0001')}`;
|
|
135
|
+
}
|
|
136
|
+
function rebuildIndex(plans) {
|
|
137
|
+
exactIndex = new Map();
|
|
138
|
+
for (const template of plans) {
|
|
139
|
+
const signature = planSignature(template.archetype, template.keywords);
|
|
140
|
+
const current = exactIndex.get(signature);
|
|
141
|
+
if (!current || (template.wins || 0) > (current.wins || 0)) {
|
|
142
|
+
exactIndex.set(signature, template);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function isRecord(value) {
|
|
147
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
148
|
+
}
|
|
149
|
+
function isPlanStep(value) {
|
|
150
|
+
if (!isRecord(value))
|
|
151
|
+
return false;
|
|
152
|
+
return typeof value.tool === 'string'
|
|
153
|
+
&& typeof value.note === 'string'
|
|
154
|
+
&& typeof value.ok === 'boolean'
|
|
155
|
+
&& (value.why === undefined
|
|
156
|
+
|| (typeof value.why === 'string' && SEED_FAILURE_CLASSES.has(value.why)));
|
|
157
|
+
}
|
|
158
|
+
function isPlanTemplate(value) {
|
|
159
|
+
if (!isRecord(value))
|
|
160
|
+
return false;
|
|
161
|
+
return typeof value.id === 'string'
|
|
162
|
+
&& typeof value.archetype === 'string'
|
|
163
|
+
&& Array.isArray(value.keywords)
|
|
164
|
+
&& value.keywords.every((keyword) => typeof keyword === 'string')
|
|
165
|
+
&& Array.isArray(value.steps)
|
|
166
|
+
&& value.steps.every(isPlanStep)
|
|
167
|
+
&& typeof value.uses === 'number'
|
|
168
|
+
&& Number.isFinite(value.uses)
|
|
169
|
+
&& typeof value.wins === 'number'
|
|
170
|
+
&& Number.isFinite(value.wins)
|
|
171
|
+
&& typeof value.createdAt === 'number'
|
|
172
|
+
&& Number.isFinite(value.createdAt)
|
|
173
|
+
&& typeof value.updatedAt === 'number'
|
|
174
|
+
&& Number.isFinite(value.updatedAt);
|
|
123
175
|
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* @returns {PlanTemplate[]}
|
|
127
|
-
*/
|
|
128
176
|
function readAll() {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
177
|
+
try {
|
|
178
|
+
if (!fs.existsSync(PLAN_FILE)) {
|
|
179
|
+
plansCache = [];
|
|
180
|
+
plansMtimeMs = -1;
|
|
181
|
+
rebuildIndex(plansCache);
|
|
182
|
+
return plansCache;
|
|
183
|
+
}
|
|
184
|
+
const mtimeMs = fs.statSync(PLAN_FILE).mtimeMs;
|
|
185
|
+
if (plansCache && mtimeMs === plansMtimeMs)
|
|
186
|
+
return plansCache;
|
|
187
|
+
const parsed = JSON.parse(fs.readFileSync(PLAN_FILE, 'utf8'));
|
|
188
|
+
plansCache = Array.isArray(parsed) ? parsed.filter(isPlanTemplate) : [];
|
|
189
|
+
plansMtimeMs = mtimeMs;
|
|
190
|
+
rebuildIndex(plansCache);
|
|
191
|
+
return plansCache;
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
plansCache = [];
|
|
195
|
+
plansMtimeMs = -1;
|
|
196
|
+
rebuildIndex(plansCache);
|
|
197
|
+
return plansCache; // a corrupt cache degrades to a cold one, never to a failed task
|
|
198
|
+
}
|
|
136
199
|
}
|
|
137
|
-
|
|
138
200
|
/**
|
|
139
201
|
* @param {PlanTemplate[]} plans
|
|
140
202
|
* @returns {boolean}
|
|
141
203
|
*/
|
|
142
204
|
function writeAll(plans) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
205
|
+
try {
|
|
206
|
+
ensureDir();
|
|
207
|
+
const temp = `${PLAN_FILE}.tmp_${process.pid}`;
|
|
208
|
+
fs.writeFileSync(temp, JSON.stringify(plans, null, 2), { mode: 0o600 });
|
|
209
|
+
fs.renameSync(temp, PLAN_FILE); // atomic: a torn write would erase the cache
|
|
210
|
+
plansCache = plans;
|
|
211
|
+
plansMtimeMs = fs.statSync(PLAN_FILE).mtimeMs;
|
|
212
|
+
rebuildIndex(plans);
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
catch (e) {
|
|
216
|
+
return false; // losing the cache must never fail the task
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function defaultMetrics() {
|
|
220
|
+
return {
|
|
221
|
+
version: 1,
|
|
222
|
+
lookups: 0,
|
|
223
|
+
hits: 0,
|
|
224
|
+
exactHits: 0,
|
|
225
|
+
fuzzyHits: 0,
|
|
226
|
+
misses: 0,
|
|
227
|
+
bypassed: 0,
|
|
228
|
+
records: 0,
|
|
229
|
+
totalLookupMs: 0,
|
|
230
|
+
maxLookupMs: 0,
|
|
231
|
+
totalRecordMs: 0,
|
|
232
|
+
recent: [],
|
|
233
|
+
disabledUntil: 0,
|
|
234
|
+
lastLookupAt: 0,
|
|
235
|
+
lastRecordAt: 0
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const METRIC_NUMBER_FIELDS = [
|
|
239
|
+
'lookups', 'hits', 'exactHits', 'fuzzyHits', 'misses', 'bypassed', 'records',
|
|
240
|
+
'totalLookupMs', 'maxLookupMs', 'totalRecordMs', 'disabledUntil',
|
|
241
|
+
'lastLookupAt', 'lastRecordAt'
|
|
242
|
+
];
|
|
243
|
+
function normalizeMetrics(value) {
|
|
244
|
+
const metrics = defaultMetrics();
|
|
245
|
+
if (!isRecord(value))
|
|
246
|
+
return metrics;
|
|
247
|
+
for (const key of METRIC_NUMBER_FIELDS) {
|
|
248
|
+
const candidate = value[key];
|
|
249
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate) && candidate >= 0) {
|
|
250
|
+
metrics[key] = candidate;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (Array.isArray(value.recent)) {
|
|
254
|
+
metrics.recent = value.recent
|
|
255
|
+
.filter((entry) => entry === 0 || entry === 1)
|
|
256
|
+
.slice(-RECENT_LOOKUP_WINDOW);
|
|
257
|
+
}
|
|
258
|
+
return metrics;
|
|
259
|
+
}
|
|
260
|
+
function readMetrics() {
|
|
261
|
+
if (metricsCache)
|
|
262
|
+
return metricsCache;
|
|
263
|
+
try {
|
|
264
|
+
if (!fs.existsSync(METRICS_FILE)) {
|
|
265
|
+
metricsCache = defaultMetrics();
|
|
266
|
+
return metricsCache;
|
|
267
|
+
}
|
|
268
|
+
const parsed = JSON.parse(fs.readFileSync(METRICS_FILE, 'utf8'));
|
|
269
|
+
metricsCache = normalizeMetrics(parsed);
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
metricsCache = defaultMetrics();
|
|
273
|
+
}
|
|
274
|
+
return metricsCache;
|
|
275
|
+
}
|
|
276
|
+
function writeMetrics(force = false) {
|
|
277
|
+
if (!metricsCache || (!force && dirtyMetricEvents < METRICS_FLUSH_INTERVAL))
|
|
278
|
+
return false;
|
|
279
|
+
try {
|
|
280
|
+
ensureDir();
|
|
281
|
+
const temp = `${METRICS_FILE}.tmp_${process.pid}`;
|
|
282
|
+
fs.writeFileSync(temp, JSON.stringify(metricsCache, null, 2), { mode: 0o600 });
|
|
283
|
+
fs.renameSync(temp, METRICS_FILE);
|
|
284
|
+
dirtyMetricEvents = 0;
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
catch (e) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function recentHitRate(metrics) {
|
|
292
|
+
if (!metrics.recent.length)
|
|
293
|
+
return 0;
|
|
294
|
+
return metrics.recent.reduce((sum, value) => sum + value, 0) / metrics.recent.length;
|
|
295
|
+
}
|
|
296
|
+
function shouldTemporarilyDisable(metrics, planCount, now) {
|
|
297
|
+
if (metrics.disabledUntil > now)
|
|
298
|
+
return true;
|
|
299
|
+
if (metrics.disabledUntil > 0 && metrics.disabledUntil <= now) {
|
|
300
|
+
metrics.disabledUntil = 0;
|
|
301
|
+
metrics.recent = [];
|
|
302
|
+
}
|
|
303
|
+
if (planCount < MIN_PLANS_BEFORE_DISABLE || metrics.recent.length < MIN_LOOKUPS_BEFORE_DISABLE) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
if (recentHitRate(metrics) >= MIN_HEALTHY_HIT_RATE)
|
|
307
|
+
return false;
|
|
308
|
+
metrics.disabledUntil = now + DISABLE_COOLDOWN_MS;
|
|
309
|
+
writeMetrics(true);
|
|
148
310
|
return true;
|
|
149
|
-
} catch (e) {
|
|
150
|
-
return false; // losing the cache must never fail the task
|
|
151
|
-
}
|
|
152
311
|
}
|
|
153
|
-
|
|
312
|
+
function recordLookupTelemetry({ hit, matchType = null, durationMs, bypassed = false }) {
|
|
313
|
+
const metrics = readMetrics();
|
|
314
|
+
const now = Date.now();
|
|
315
|
+
metrics.lastLookupAt = now;
|
|
316
|
+
if (bypassed) {
|
|
317
|
+
metrics.bypassed += 1;
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
metrics.lookups += 1;
|
|
321
|
+
metrics.totalLookupMs += durationMs;
|
|
322
|
+
metrics.maxLookupMs = Math.max(metrics.maxLookupMs, durationMs);
|
|
323
|
+
metrics.recent.push(hit ? 1 : 0);
|
|
324
|
+
if (metrics.recent.length > RECENT_LOOKUP_WINDOW)
|
|
325
|
+
metrics.recent.shift();
|
|
326
|
+
if (hit) {
|
|
327
|
+
metrics.hits += 1;
|
|
328
|
+
if (matchType === 'exact')
|
|
329
|
+
metrics.exactHits += 1;
|
|
330
|
+
if (matchType === 'fuzzy')
|
|
331
|
+
metrics.fuzzyHits += 1;
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
metrics.misses += 1;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
dirtyMetricEvents += 1;
|
|
338
|
+
writeMetrics(false);
|
|
339
|
+
}
|
|
154
340
|
/**
|
|
155
341
|
* Intent keywords, computed without a model.
|
|
156
342
|
*
|
|
@@ -158,15 +344,13 @@ function writeAll(plans) {
|
|
|
158
344
|
* @returns {string[]} unique, lowercase, order not significant
|
|
159
345
|
*/
|
|
160
346
|
export function extractKeywords(text) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
return [...new Set(words)].slice(0, MAX_KEYWORDS);
|
|
347
|
+
const words = String(text || '')
|
|
348
|
+
.toLowerCase()
|
|
349
|
+
.replace(/[^\p{L}\p{N}\s_-]/gu, ' ')
|
|
350
|
+
.split(/\s+/)
|
|
351
|
+
.filter((w) => w.length >= 3 && w.length <= 24 && !STOPWORDS.has(w) && !/^\d+$/.test(w));
|
|
352
|
+
return [...new Set(words)].slice(0, MAX_KEYWORDS);
|
|
168
353
|
}
|
|
169
|
-
|
|
170
354
|
/**
|
|
171
355
|
* Jaccard overlap. Symmetric, bounded 0..1, and cheap enough to run against
|
|
172
356
|
* every stored plan without anyone noticing.
|
|
@@ -176,15 +360,16 @@ export function extractKeywords(text) {
|
|
|
176
360
|
* @returns {number}
|
|
177
361
|
*/
|
|
178
362
|
export function similarity(a, b) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
363
|
+
const left = new Set(a || []);
|
|
364
|
+
const right = new Set(b || []);
|
|
365
|
+
if (left.size === 0 || right.size === 0)
|
|
366
|
+
return 0;
|
|
367
|
+
let shared = 0;
|
|
368
|
+
for (const word of left)
|
|
369
|
+
if (right.has(word))
|
|
370
|
+
shared++;
|
|
371
|
+
return shared / (left.size + right.size - shared);
|
|
186
372
|
}
|
|
187
|
-
|
|
188
373
|
/**
|
|
189
374
|
* Reduce a tool call to a reusable step.
|
|
190
375
|
*
|
|
@@ -198,26 +383,24 @@ export function similarity(a, b) {
|
|
|
198
383
|
* @returns {PlanStep}
|
|
199
384
|
*/
|
|
200
385
|
export function toStep(tool, args, ok, result) {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
386
|
+
const SAFE_KEYS = ['action', 'verb', 'mode', 'operation', 'kind', 'type'];
|
|
387
|
+
let note = '';
|
|
388
|
+
for (const key of SAFE_KEYS) {
|
|
389
|
+
const value = isRecord(args) ? args[key] : undefined;
|
|
390
|
+
if (typeof value === 'string' && value) {
|
|
391
|
+
note = value.toLowerCase().replace(/[^a-z0-9_.-]/g, '').slice(0, 32);
|
|
392
|
+
if (note)
|
|
393
|
+
break;
|
|
394
|
+
}
|
|
209
395
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
return step;
|
|
396
|
+
const step = { tool: String(tool || '').slice(0, 64), note, ok: Boolean(ok) };
|
|
397
|
+
if (!step.ok) {
|
|
398
|
+
const why = failureClass(result);
|
|
399
|
+
if (why)
|
|
400
|
+
step.why = why;
|
|
401
|
+
}
|
|
402
|
+
return step;
|
|
219
403
|
}
|
|
220
|
-
|
|
221
404
|
/**
|
|
222
405
|
* Why a step failed, as a CLASS rather than as a message.
|
|
223
406
|
*
|
|
@@ -234,57 +417,103 @@ export function toStep(tool, args, ok, result) {
|
|
|
234
417
|
* @returns {string} one of the classes below, or '' when nothing matched
|
|
235
418
|
*/
|
|
236
419
|
export function failureClass(result) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
420
|
+
if (!result)
|
|
421
|
+
return '';
|
|
422
|
+
const source = isRecord(result) ? result : {};
|
|
423
|
+
const text = [source.error, source.message, source.stderr, source.reason]
|
|
424
|
+
.filter((v) => typeof v === 'string')
|
|
425
|
+
.join(' ')
|
|
426
|
+
.toLowerCase()
|
|
427
|
+
.slice(0, 500);
|
|
428
|
+
if (!text)
|
|
429
|
+
return typeof source.exitCode === 'number' && source.exitCode !== 0 ? 'exit_nonzero' : '';
|
|
430
|
+
if (/not found|no such file|enoent|command not found|cannot find/.test(text))
|
|
431
|
+
return 'not_found';
|
|
432
|
+
if (/permission|denied|eacces|eperm|unauthor|forbidden/.test(text))
|
|
433
|
+
return 'permission';
|
|
434
|
+
if (/timed? ?out|etimedout|deadline/.test(text))
|
|
435
|
+
return 'timeout';
|
|
436
|
+
if (/econnrefused|enotfound|network|dns|socket|offline/.test(text))
|
|
437
|
+
return 'network';
|
|
438
|
+
if (/rate limit|429|quota|budget/.test(text))
|
|
439
|
+
return 'rate_limited';
|
|
440
|
+
if (/invalid|malformed|schema|required (property|field)|bad request|400/.test(text))
|
|
441
|
+
return 'invalid_args';
|
|
442
|
+
if (/syntax ?error|parse error|unexpected token/.test(text))
|
|
443
|
+
return 'syntax';
|
|
444
|
+
if (/exit code|non-?zero|exited with/.test(text))
|
|
445
|
+
return 'exit_nonzero';
|
|
446
|
+
return 'failed';
|
|
254
447
|
}
|
|
255
|
-
|
|
256
448
|
/**
|
|
257
449
|
* The best stored plan for this request, if one is close enough.
|
|
258
450
|
*
|
|
259
|
-
* @param {{prompt: string, archetype?: string}} query
|
|
451
|
+
* @param {{prompt: string, archetype?: string, allowFuzzy?: boolean}} query
|
|
260
452
|
* @returns {PlanMatch|null}
|
|
261
453
|
*/
|
|
262
454
|
export function lookupPlan(query) {
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
455
|
+
const startedAt = performance.now();
|
|
456
|
+
const keywords = extractKeywords(query?.prompt);
|
|
457
|
+
if (keywords.length === 0) {
|
|
458
|
+
recordLookupTelemetry({ hit: false, durationMs: performance.now() - startedAt });
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
const archetype = String(query?.archetype || '');
|
|
462
|
+
const plans = readAll();
|
|
463
|
+
const now = Date.now();
|
|
464
|
+
if (shouldTemporarilyDisable(readMetrics(), plans.length, now)) {
|
|
465
|
+
recordLookupTelemetry({ hit: false, durationMs: performance.now() - startedAt, bypassed: true });
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
const exact = exactIndex.get(planSignature(archetype, keywords));
|
|
469
|
+
if (exact) {
|
|
470
|
+
const durationMs = performance.now() - startedAt;
|
|
471
|
+
recordLookupTelemetry({ hit: true, matchType: 'exact', durationMs });
|
|
472
|
+
return {
|
|
473
|
+
template: exact,
|
|
474
|
+
similarity: 1,
|
|
475
|
+
matchType: 'exact',
|
|
476
|
+
telemetry: { keywordCount: keywords.length, lookupMs: durationMs }
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
// Low-confidence task classification is not enough evidence for fuzzy reuse.
|
|
480
|
+
// Exact matches remain safe; broader reuse waits until the task shape is at
|
|
481
|
+
// least medium confidence, avoiding the accuracy loss reported for aggressive
|
|
482
|
+
// fuzzy thresholds in APC Table 6.
|
|
483
|
+
if (query?.allowFuzzy === false) {
|
|
484
|
+
const durationMs = performance.now() - startedAt;
|
|
485
|
+
recordLookupTelemetry({ hit: false, durationMs });
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
let best = null;
|
|
489
|
+
for (const template of plans) {
|
|
490
|
+
// Archetype is a hard gate, not a score. A "build a 3D scene" plan sharing
|
|
491
|
+
// three words with "debug the build" is not a plan for it.
|
|
492
|
+
if (archetype && template.archetype && template.archetype !== archetype)
|
|
493
|
+
continue;
|
|
494
|
+
const score = similarity(keywords, template.keywords);
|
|
495
|
+
if (score < MATCH_THRESHOLD)
|
|
496
|
+
continue;
|
|
497
|
+
// Ties break on demonstrated usefulness, so a plan that has actually worked
|
|
498
|
+
// outranks one that merely matched before.
|
|
499
|
+
const winRate = template.uses > 0 ? template.wins / template.uses : 0;
|
|
500
|
+
const ranked = score + winRate * 0.1;
|
|
501
|
+
if (!best || ranked > best.ranked)
|
|
502
|
+
best = { template, similarity: score, ranked };
|
|
503
|
+
}
|
|
504
|
+
const durationMs = performance.now() - startedAt;
|
|
505
|
+
if (!best) {
|
|
506
|
+
recordLookupTelemetry({ hit: false, durationMs });
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
recordLookupTelemetry({ hit: true, matchType: 'fuzzy', durationMs });
|
|
510
|
+
return {
|
|
511
|
+
template: best.template,
|
|
512
|
+
similarity: best.similarity,
|
|
513
|
+
matchType: 'fuzzy',
|
|
514
|
+
telemetry: { keywordCount: keywords.length, lookupMs: durationMs }
|
|
515
|
+
};
|
|
286
516
|
}
|
|
287
|
-
|
|
288
517
|
/**
|
|
289
518
|
* Store or reinforce the procedure a task actually followed.
|
|
290
519
|
*
|
|
@@ -292,46 +521,108 @@ export function lookupPlan(query) {
|
|
|
292
521
|
* @returns {PlanTemplate|null} the stored template, or null when there was nothing worth storing
|
|
293
522
|
*/
|
|
294
523
|
export function recordPlan(outcome) {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
524
|
+
const startedAt = performance.now();
|
|
525
|
+
const steps = Array.isArray(outcome?.steps)
|
|
526
|
+
? outcome.steps.filter(isPlanStep).slice(0, MAX_STEPS)
|
|
527
|
+
: [];
|
|
528
|
+
// A one-step task teaches nothing a fresh model would not do anyway.
|
|
529
|
+
if (steps.length < 2)
|
|
530
|
+
return null;
|
|
531
|
+
const keywords = extractKeywords(outcome?.prompt);
|
|
532
|
+
if (keywords.length === 0)
|
|
533
|
+
return null;
|
|
534
|
+
const plans = readAll();
|
|
535
|
+
const archetype = String(outcome?.archetype || '');
|
|
536
|
+
const existing = plans.find((t) => t.archetype === archetype && similarity(keywords, t.keywords) >= 0.8);
|
|
537
|
+
if (existing) {
|
|
538
|
+
existing.steps = steps; // the newest working route wins
|
|
539
|
+
existing.uses += 1;
|
|
540
|
+
existing.wins += 1;
|
|
541
|
+
existing.updatedAt = Date.now();
|
|
542
|
+
writeAll(prune(plans));
|
|
543
|
+
const metrics = readMetrics();
|
|
544
|
+
metrics.records += 1;
|
|
545
|
+
metrics.totalRecordMs += performance.now() - startedAt;
|
|
546
|
+
metrics.lastRecordAt = Date.now();
|
|
547
|
+
dirtyMetricEvents += 1;
|
|
548
|
+
writeMetrics(false);
|
|
549
|
+
return existing;
|
|
550
|
+
}
|
|
551
|
+
const template = {
|
|
552
|
+
id: `p_${Date.now().toString(36)}_${crypto.randomBytes(3).toString('hex')}`,
|
|
553
|
+
archetype,
|
|
554
|
+
keywords,
|
|
555
|
+
steps,
|
|
556
|
+
uses: 1,
|
|
557
|
+
wins: 1,
|
|
558
|
+
createdAt: Date.now(),
|
|
559
|
+
updatedAt: Date.now()
|
|
560
|
+
};
|
|
561
|
+
plans.push(template);
|
|
314
562
|
writeAll(prune(plans));
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
563
|
+
const metrics = readMetrics();
|
|
564
|
+
metrics.records += 1;
|
|
565
|
+
metrics.totalRecordMs += performance.now() - startedAt;
|
|
566
|
+
metrics.lastRecordAt = Date.now();
|
|
567
|
+
dirtyMetricEvents += 1;
|
|
568
|
+
writeMetrics(false);
|
|
569
|
+
return template;
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Pre-populate a cold cache from user-supplied, argument-free plan samples.
|
|
573
|
+
* Only tool names, short action labels and closed-vocabulary outcomes survive;
|
|
574
|
+
* paths, prompts passed to tools and command lines have no field to enter.
|
|
575
|
+
*/
|
|
576
|
+
export function prewarmPlans(samples) {
|
|
577
|
+
if (!Array.isArray(samples))
|
|
578
|
+
return { accepted: 0, rejected: 1, errors: ['Expected a JSON array'] };
|
|
579
|
+
const errors = [];
|
|
580
|
+
let accepted = 0;
|
|
581
|
+
for (const [index, sample] of samples.slice(0, 100).entries()) {
|
|
582
|
+
const sampleRecord = isRecord(sample) ? sample : {};
|
|
583
|
+
const prompt = typeof sampleRecord.prompt === 'string' ? sampleRecord.prompt.trim() : '';
|
|
584
|
+
const archetype = typeof sampleRecord.archetype === 'string' ? sampleRecord.archetype.trim().slice(0, 64) : '';
|
|
585
|
+
const rawSteps = Array.isArray(sampleRecord.steps) ? sampleRecord.steps : [];
|
|
586
|
+
if (!prompt || rawSteps.length < 2) {
|
|
587
|
+
errors.push(`Sample ${index + 1}: prompt and at least two steps are required`);
|
|
588
|
+
continue;
|
|
589
|
+
}
|
|
590
|
+
const steps = [];
|
|
591
|
+
let invalid = false;
|
|
592
|
+
for (const rawStep of rawSteps.slice(0, MAX_STEPS)) {
|
|
593
|
+
const source = typeof rawStep === 'string'
|
|
594
|
+
? { tool: rawStep }
|
|
595
|
+
: isRecord(rawStep) ? rawStep : {};
|
|
596
|
+
const tool = typeof source.tool === 'string' ? source.tool.trim() : '';
|
|
597
|
+
if (!/^[A-Za-z][A-Za-z0-9_.-]{0,63}$/.test(tool)) {
|
|
598
|
+
invalid = true;
|
|
599
|
+
break;
|
|
600
|
+
}
|
|
601
|
+
const note = typeof source.action === 'string'
|
|
602
|
+
? source.action.toLowerCase().replace(/[^a-z0-9_.-]/g, '').slice(0, 32)
|
|
603
|
+
: '';
|
|
604
|
+
const step = { tool, note, ok: source.ok !== false };
|
|
605
|
+
if (!step.ok) {
|
|
606
|
+
step.why = typeof source.why === 'string'
|
|
607
|
+
&& SEED_FAILURE_CLASSES.has(source.why)
|
|
608
|
+
? source.why
|
|
609
|
+
: 'failed';
|
|
610
|
+
}
|
|
611
|
+
steps.push(step);
|
|
612
|
+
}
|
|
613
|
+
if (invalid) {
|
|
614
|
+
errors.push(`Sample ${index + 1}: invalid tool name`);
|
|
615
|
+
continue;
|
|
616
|
+
}
|
|
617
|
+
if (recordPlan({ prompt, archetype, steps }))
|
|
618
|
+
accepted++;
|
|
619
|
+
else
|
|
620
|
+
errors.push(`Sample ${index + 1}: could not be stored`);
|
|
621
|
+
}
|
|
622
|
+
if (samples.length > 100)
|
|
623
|
+
errors.push(`Only the first 100 of ${samples.length} samples were considered`);
|
|
624
|
+
return { accepted, rejected: errors.length, errors: errors.slice(0, 20) };
|
|
333
625
|
}
|
|
334
|
-
|
|
335
626
|
/**
|
|
336
627
|
* Record that an offered plan did NOT lead anywhere, so a bad template loses
|
|
337
628
|
* its rank instead of being offered forever.
|
|
@@ -340,14 +631,14 @@ export function recordPlan(outcome) {
|
|
|
340
631
|
* @returns {boolean}
|
|
341
632
|
*/
|
|
342
633
|
export function recordMiss(id) {
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
634
|
+
const plans = readAll();
|
|
635
|
+
const template = plans.find((t) => t.id === id);
|
|
636
|
+
if (!template)
|
|
637
|
+
return false;
|
|
638
|
+
template.uses += 1;
|
|
639
|
+
template.updatedAt = Date.now();
|
|
640
|
+
return writeAll(plans);
|
|
349
641
|
}
|
|
350
|
-
|
|
351
642
|
/**
|
|
352
643
|
* Keep the cache bounded by usefulness rather than by age alone: a plan used
|
|
353
644
|
* ten times last month is worth more than one stored yesterday and never hit.
|
|
@@ -356,19 +647,17 @@ export function recordMiss(id) {
|
|
|
356
647
|
* @returns {PlanTemplate[]}
|
|
357
648
|
*/
|
|
358
649
|
export function prune(plans) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
(
|
|
367
|
-
return value(b) - value(a);
|
|
650
|
+
if (plans.length <= MAX_PLANS)
|
|
651
|
+
return plans;
|
|
652
|
+
const now = Date.now();
|
|
653
|
+
const MONTH = 30 * 24 * 60 * 60 * 1000;
|
|
654
|
+
return [...plans]
|
|
655
|
+
.sort((a, b) => {
|
|
656
|
+
const value = (t) => (t.uses > 0 ? t.wins / t.uses : 0) * Math.pow(2, -(now - t.updatedAt) / MONTH) * Math.log2(1 + t.uses);
|
|
657
|
+
return value(b) - value(a);
|
|
368
658
|
})
|
|
369
|
-
|
|
659
|
+
.slice(0, MAX_PLANS);
|
|
370
660
|
}
|
|
371
|
-
|
|
372
661
|
/**
|
|
373
662
|
* The prompt fragment offered to the model on a hit.
|
|
374
663
|
*
|
|
@@ -382,56 +671,71 @@ export function prune(plans) {
|
|
|
382
671
|
* @returns {string} '' when there is no usable match
|
|
383
672
|
*/
|
|
384
673
|
export function renderPlanHint(match) {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
const failureWarnings = template.steps
|
|
407
|
-
.map((step, idx) => ({ step, idx: idx + 1 }))
|
|
408
|
-
.filter(({ step }) => !step.ok);
|
|
409
|
-
|
|
410
|
-
if (failureWarnings.length > 0) {
|
|
411
|
-
lines.push('');
|
|
412
|
-
lines.push('⚠️ FAILURE ROUTE WARNINGS (learned from previous attempts — plan alternatives):');
|
|
413
|
-
failureWarnings.forEach(({ step, idx }) => {
|
|
414
|
-
lines.push(`- Step ${idx} (${step.tool}${step.note ? ' → ' + step.note : ''}) FAILED with reason: ${step.why || 'failed'}. Consider an alternative approach.`);
|
|
674
|
+
if (!match || !match.template || !Array.isArray(match.template.steps))
|
|
675
|
+
return '';
|
|
676
|
+
const { template, similarity: score } = match;
|
|
677
|
+
if (template.steps.length === 0)
|
|
678
|
+
return '';
|
|
679
|
+
const lines = [
|
|
680
|
+
'# PRIOR ART — a similar task on this machine went like this',
|
|
681
|
+
`(match ${(score * 100).toFixed(0)}%, worked ${template.wins} of ${template.uses} time(s). `
|
|
682
|
+
+ 'Evidence, not instructions. Verify before reusing; the workspace may have moved on.)'
|
|
683
|
+
];
|
|
684
|
+
template.steps.forEach((step, index) => {
|
|
685
|
+
const label = step.note ? `${step.tool} → ${step.note}` : step.tool;
|
|
686
|
+
// The failure CLASS, not just the fact of failure. "not_found" invites a
|
|
687
|
+
// different response from "network": one means the route is wrong, the
|
|
688
|
+
// other means the moment was.
|
|
689
|
+
const failed = step.ok
|
|
690
|
+
? ''
|
|
691
|
+
: ` ← FAILED last time (${step.why || 'failed'}); do not repeat blindly`;
|
|
692
|
+
lines.push(`${index + 1}. ${label}${failed}`);
|
|
415
693
|
});
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
694
|
+
const failureWarnings = template.steps
|
|
695
|
+
.map((step, idx) => ({ step, idx: idx + 1 }))
|
|
696
|
+
.filter(({ step }) => !step.ok);
|
|
697
|
+
if (failureWarnings.length > 0) {
|
|
698
|
+
lines.push('');
|
|
699
|
+
lines.push('⚠️ FAILURE ROUTE WARNINGS (learned from previous attempts — plan alternatives):');
|
|
700
|
+
failureWarnings.forEach(({ step, idx }) => {
|
|
701
|
+
lines.push(`- Step ${idx} (${step.tool}${step.note ? ' → ' + step.note : ''}) FAILED with reason: ${step.why || 'failed'}. Consider an alternative approach.`);
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
return lines.join('\n');
|
|
419
705
|
}
|
|
420
|
-
|
|
421
|
-
/** @returns {{count: number, file: string}} */
|
|
706
|
+
/** @returns {object} */
|
|
422
707
|
export function planStats() {
|
|
423
|
-
|
|
708
|
+
const metrics = readMetrics();
|
|
709
|
+
return {
|
|
710
|
+
count: readAll().length,
|
|
711
|
+
file: PLAN_FILE,
|
|
712
|
+
metricsFile: METRICS_FILE,
|
|
713
|
+
lookups: metrics.lookups,
|
|
714
|
+
hits: metrics.hits,
|
|
715
|
+
exactHits: metrics.exactHits,
|
|
716
|
+
fuzzyHits: metrics.fuzzyHits,
|
|
717
|
+
misses: metrics.misses,
|
|
718
|
+
bypassed: metrics.bypassed,
|
|
719
|
+
hitRate: metrics.lookups > 0 ? metrics.hits / metrics.lookups : 0,
|
|
720
|
+
recentHitRate: recentHitRate(metrics),
|
|
721
|
+
averageLookupMs: metrics.lookups > 0 ? metrics.totalLookupMs / metrics.lookups : 0,
|
|
722
|
+
maxLookupMs: metrics.maxLookupMs,
|
|
723
|
+
averageRecordMs: metrics.records > 0 ? metrics.totalRecordMs / metrics.records : 0,
|
|
724
|
+
disabledUntil: metrics.disabledUntil,
|
|
725
|
+
disabled: metrics.disabledUntil > Date.now()
|
|
726
|
+
};
|
|
424
727
|
}
|
|
425
|
-
|
|
426
728
|
/**
|
|
427
729
|
* Remove every stored plan. Used by `/cortex --forget` style operations and by
|
|
428
730
|
* tests; never called automatically.
|
|
429
731
|
* @returns {number} how many were removed
|
|
430
732
|
*/
|
|
431
733
|
export function forgetPlans() {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
734
|
+
const count = readAll().length;
|
|
735
|
+
writeAll([]);
|
|
736
|
+
metricsCache = defaultMetrics();
|
|
737
|
+
dirtyMetricEvents = 1;
|
|
738
|
+
writeMetrics(true);
|
|
739
|
+
return count;
|
|
435
740
|
}
|
|
436
|
-
|
|
437
|
-
export { PLAN_FILE, PLAN_DIR, MATCH_THRESHOLD, MAX_PLANS };
|
|
741
|
+
export { PLAN_FILE, PLAN_DIR, METRICS_FILE, MATCH_THRESHOLD, MAX_PLANS, MIN_LOOKUPS_BEFORE_DISABLE, MIN_PLANS_BEFORE_DISABLE, MIN_HEALTHY_HIT_RATE, DISABLE_COOLDOWN_MS };
|