@inetafrica/open-claudia 2.6.57 → 2.6.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +8 -1
- package/bin/cli.js +4 -0
- package/bin/kpi.js +55 -0
- package/core/dream.js +384 -11
- package/core/pack-review.js +45 -4
- package/core/packs.js +37 -5
- package/core/recall/discoverer.js +219 -50
- package/core/recall/graph.js +29 -2
- package/core/recall/kpi.js +227 -0
- package/core/recall/metrics.js +216 -9
- package/core/recall/tuning.js +125 -0
- package/core/recall/warm-walker.js +12 -1
- package/core/runner.js +5 -1
- package/core/system-prompt.js +5 -1
- package/core/transcript-index.js +21 -1
- package/package.json +2 -2
- package/test-recall-discoverer.js +52 -8
|
@@ -30,6 +30,7 @@ let pending = null; // { resolve, reject, timer } for the in-flight walk
|
|
|
30
30
|
let chain = Promise.resolve(); // serialises walks (one message in flight)
|
|
31
31
|
let msgCount = 0; // messages sent to the current process
|
|
32
32
|
let charCount = 0; // prompt chars sent to the current process
|
|
33
|
+
let costTotal = 0; // cumulative total_cost_usd reported by the session
|
|
33
34
|
|
|
34
35
|
function isEnabled() {
|
|
35
36
|
return String(process.env.RECALL_WARM_WALKER || "on").toLowerCase() !== "off";
|
|
@@ -60,6 +61,7 @@ function spawnChild(cfg) {
|
|
|
60
61
|
child = proc;
|
|
61
62
|
msgCount = 0;
|
|
62
63
|
charCount = 0;
|
|
64
|
+
costTotal = 0;
|
|
63
65
|
let buf = "";
|
|
64
66
|
let asstText = "";
|
|
65
67
|
|
|
@@ -88,7 +90,14 @@ function spawnChild(cfg) {
|
|
|
88
90
|
} else if (evt.type === "result") {
|
|
89
91
|
const text = (typeof evt.result === "string" && evt.result) ? evt.result : asstText;
|
|
90
92
|
asstText = "";
|
|
91
|
-
|
|
93
|
+
// total_cost_usd is cumulative for the stream-json session; the delta
|
|
94
|
+
// from the previous result is this walk's cost.
|
|
95
|
+
let costUsd = null;
|
|
96
|
+
if (typeof evt.total_cost_usd === "number") {
|
|
97
|
+
costUsd = Math.max(0, evt.total_cost_usd - costTotal);
|
|
98
|
+
costTotal = evt.total_cost_usd;
|
|
99
|
+
}
|
|
100
|
+
if (text && !evt.is_error) settle("resolve", { text: redactSensitive(String(text).trim()), costUsd });
|
|
92
101
|
else settle("reject", new Error("warm walker: empty/error result"));
|
|
93
102
|
}
|
|
94
103
|
}
|
|
@@ -136,6 +145,8 @@ function doWalk(promptText, opts) {
|
|
|
136
145
|
|
|
137
146
|
// Serialise: one message in flight at a time. A failed walk must not poison the
|
|
138
147
|
// queue, so the chain swallows outcomes while callers still see their result.
|
|
148
|
+
// Resolves { text, costUsd } — costUsd is the per-walk delta of the session's
|
|
149
|
+
// cumulative total_cost_usd (null when the CLI doesn't report it).
|
|
139
150
|
function walkWarm(promptText, opts = {}) {
|
|
140
151
|
const run = () => doWalk(promptText, opts);
|
|
141
152
|
const p = chain.then(run, run);
|
package/core/runner.js
CHANGED
|
@@ -1027,15 +1027,17 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1027
1027
|
// `open-claudia pack show <dir>` / `entity show <slug>` — so the banner
|
|
1028
1028
|
// reflects what was read, not what was pushed. (consumeLastInjected is
|
|
1029
1029
|
// drained here to keep the per-turn buffer from leaking into the next turn.)
|
|
1030
|
+
let turnRecallTier = "";
|
|
1030
1031
|
try {
|
|
1031
1032
|
const injected = require("./system-prompt").consumeLastInjected();
|
|
1032
1033
|
if (injected && injected.recall) {
|
|
1033
1034
|
const r = injected.recall;
|
|
1035
|
+
turnRecallTier = String(r.tier || "");
|
|
1034
1036
|
const esc = (s) => String(s).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1035
1037
|
const fmt = (arr, icon) => arr.map((x) => (x.why ? `${icon} <b>${esc(x.name)}</b> — ${esc(x.why)}` : `${icon} <b>${esc(x.name)}</b>`));
|
|
1036
1038
|
// 🧠 packs/entities recall — gated by /recall.
|
|
1037
1039
|
if (settings.showRecall) {
|
|
1038
|
-
const lines = [...fmt(r.packs || [], "📦"), ...fmt(r.entities || [], "👤")];
|
|
1040
|
+
const lines = [...fmt(r.packs || [], "📦"), ...fmt(r.entities || [], "👤"), ...fmt(r.episodes || [], "📓")];
|
|
1039
1041
|
if (lines.length) send(`🧠 <b>Recall this turn</b> (${esc(r.engine)})\n${lines.join("\n")}`, telegramHtmlOpts()).catch(() => {});
|
|
1040
1042
|
else if (r.gated) send(`🧠 <b>Recall</b> (${esc(r.engine)}): skipped by pre-gate — trivial turn.`, telegramHtmlOpts()).catch(() => {});
|
|
1041
1043
|
}
|
|
@@ -1517,10 +1519,12 @@ async function runClaude(prompt, cwd, replyToMsgId, opts = {}) {
|
|
|
1517
1519
|
// Post-turn pack review: fire-and-forget on a cheap model; never
|
|
1518
1520
|
// blocks queue drain or the next turn.
|
|
1519
1521
|
if ((code === 0 || code === null) && assistantText.trim()) {
|
|
1522
|
+
const WRITE_TOOLS = new Set(["Edit", "Write", "NotebookEdit", "Bash", "Shell", "Skill", "Task", "Agent"]);
|
|
1520
1523
|
packReview.reviewTurn({
|
|
1521
1524
|
userText: prompt,
|
|
1522
1525
|
assistantText,
|
|
1523
1526
|
channelId,
|
|
1527
|
+
signals: { tier: turnRecallTier, wrote: toolUses.some((t) => WRITE_TOOLS.has(t)) },
|
|
1524
1528
|
announce: (text) => chatContext.run(store, () => send(text)),
|
|
1525
1529
|
});
|
|
1526
1530
|
}
|
package/core/system-prompt.js
CHANGED
|
@@ -743,22 +743,26 @@ async function promptWithDynamicContext(prompt, opts = {}) {
|
|
|
743
743
|
});
|
|
744
744
|
const { packBlock, entityBlock } = result;
|
|
745
745
|
const toolBlock = result.toolBlock || "";
|
|
746
|
+
const episodeBlock = result.episodeBlock || "";
|
|
746
747
|
const why = result.why || {};
|
|
747
748
|
lastInjected.recall = {
|
|
748
749
|
engine: engine.name || recall.activeEngineName(settings),
|
|
749
750
|
gated: !!result.gated,
|
|
751
|
+
tier: result.tier || "",
|
|
750
752
|
packs: (result.packMatches || []).map((m) => ({ name: m.name || m.dir, why: why[`pack:${m.dir}`] || "" })),
|
|
751
753
|
entities: (result.entityMatches || []).map((m) => ({ name: m.name || m.slug, why: why[`entity:${m.slug}`] || "" })),
|
|
752
754
|
// Tools surfaced as first-class discoverer nodes this turn (name + why).
|
|
753
755
|
// Drives the /tooltrace "surfaced" banner; the toolBlock below is the
|
|
754
756
|
// context the agent actually sees.
|
|
755
757
|
tools: (result.toolMatches || []).map((m) => ({ name: m.name, why: m.why || why[`tool:${m.name}`] || "" })),
|
|
758
|
+
// Past-conversation episodes the walker kept (drives the /recall banner).
|
|
759
|
+
episodes: (result.episodeMatches || []).map((m) => ({ name: m.name, why: m.why || "" })),
|
|
756
760
|
};
|
|
757
761
|
const budgetNote = budget.omitted > 0
|
|
758
762
|
? `\n\n## Memory budget\n${budget.omitted} matched memory item${budget.omitted === 1 ? " was" : "s were"} omitted to keep this turn under the recall budget (${budget.maxChars} chars). Use \`open-claudia pack show <dir>\`, \`entity show <slug>\`, or transcript search if deeper context is needed.`
|
|
759
763
|
: "";
|
|
760
764
|
const transcriptPointer = opts.includeTranscriptPointer ? `${transcriptPointerNote(state)}\n\n` : "";
|
|
761
|
-
return `${transcriptPointer}${buildDynamicContextBlock()}${packBlock}${entityBlock}${toolBlock}${budgetNote}${voiceReplyGuidance()}\n\nCurrent user request:\n${prompt}`;
|
|
765
|
+
return `${transcriptPointer}${buildDynamicContextBlock()}${packBlock}${entityBlock}${toolBlock}${episodeBlock}${budgetNote}${voiceReplyGuidance()}\n\nCurrent user request:\n${prompt}`;
|
|
762
766
|
} catch (e) {
|
|
763
767
|
return prompt;
|
|
764
768
|
}
|
package/core/transcript-index.js
CHANGED
|
@@ -145,6 +145,26 @@ function rebuild(transcriptsDir = defaultTranscriptsDir()) {
|
|
|
145
145
|
return indexAll(transcriptsDir);
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
// Nightly upkeep: catch up new appends, then drop index rows for transcript
|
|
149
|
+
// files that no longer exist on disk — dead sessions stop matching and the
|
|
150
|
+
// FTS table stays proportional to what is actually recallable.
|
|
151
|
+
function tend(transcriptsDir = defaultTranscriptsDir()) {
|
|
152
|
+
const db = open(transcriptsDir);
|
|
153
|
+
if (!db) return { available: false, added: 0, prunedFiles: 0 };
|
|
154
|
+
const { added } = indexAll(transcriptsDir);
|
|
155
|
+
let prunedFiles = 0;
|
|
156
|
+
try {
|
|
157
|
+
const rows = db.prepare("SELECT path FROM files").all();
|
|
158
|
+
for (const r of rows) {
|
|
159
|
+
if (fs.existsSync(r.path)) continue;
|
|
160
|
+
db.prepare("DELETE FROM entries WHERE file = ?").run(r.path);
|
|
161
|
+
db.prepare("DELETE FROM files WHERE path = ?").run(r.path);
|
|
162
|
+
prunedFiles++;
|
|
163
|
+
}
|
|
164
|
+
} catch (e) {}
|
|
165
|
+
return { available: true, added, prunedFiles };
|
|
166
|
+
}
|
|
167
|
+
|
|
148
168
|
// FTS5 MATCH chokes on unbalanced quotes/operators in natural queries, so
|
|
149
169
|
// by default each term is quoted (implicit AND). Pass raw=true for full
|
|
150
170
|
// FTS5 query syntax (NEAR, OR, prefix*).
|
|
@@ -184,4 +204,4 @@ function stats(transcriptsDir = defaultTranscriptsDir()) {
|
|
|
184
204
|
}
|
|
185
205
|
}
|
|
186
206
|
|
|
187
|
-
module.exports = { available, open, indexFile, indexAll, rebuild, search, stats, sanitizeQuery, defaultTranscriptsDir };
|
|
207
|
+
module.exports = { available, open, indexFile, indexAll, rebuild, tend, search, stats, sanitizeQuery, defaultTranscriptsDir };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.59",
|
|
4
4
|
"description": "Your always-on AI coding assistant — Claude Code, Cursor Agent, and OpenAI Codex via Telegram or Kazee Chat",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"setup": "node setup.js",
|
|
11
11
|
"start": "node bot.js",
|
|
12
|
-
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tool-graph.js"
|
|
12
|
+
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=node node test-recall-evolution.js"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
15
|
"bot.js",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Discoverer engine: pre-gate, seed → (graph) → fail-open injection. The LLM
|
|
2
2
|
// walker is disabled here (RECALL_DISCOVERER_WALKER=off) so the engine takes
|
|
3
|
-
// its deterministic fail-open path:
|
|
3
|
+
// its deterministic fail-open path. Contract: the seeds tier keeps ALL
|
|
4
|
+
// user-origin keyword seeds (packs, entities, tools); the full tier without a
|
|
5
|
+
// judge keeps only the top-3 user-origin pack/entity seeds — an unjudged tool
|
|
6
|
+
// or episode is a guess, not a memory.
|
|
4
7
|
const assert = require("assert");
|
|
5
8
|
const fs = require("fs");
|
|
6
9
|
const os = require("os");
|
|
@@ -24,13 +27,21 @@ toolsLib.addTool((() => {
|
|
|
24
27
|
return s;
|
|
25
28
|
})(), { name: "mikrotik-audit", description: "audit the mikrotik router fleet inventory" });
|
|
26
29
|
|
|
27
|
-
// --- pre-gate ---
|
|
30
|
+
// --- tiered pre-gate ---
|
|
28
31
|
assert.strictEqual(disc.needsRecall("", 0), false, "empty → no recall");
|
|
29
32
|
assert.strictEqual(disc.needsRecall("thanks", 0), false, "pleasantry → no recall");
|
|
30
33
|
assert.strictEqual(disc.needsRecall("ok", 0), false, "terse ack → no recall");
|
|
31
34
|
assert.strictEqual(disc.needsRecall("yo", 0), false, "two words, no seeds → no recall");
|
|
32
35
|
assert.strictEqual(disc.needsRecall("how do I deploy the mobile app", 0), true, "substantive → recall");
|
|
33
|
-
|
|
36
|
+
// The old gate kept ANY turn with seeds (measured: gated 0.4% of turns, so
|
|
37
|
+
// "thanks!" paid a full walk). New contract: pleasantries gate regardless.
|
|
38
|
+
assert.strictEqual(disc.needsRecall("k", 5), false, "terse ack gates even when seeds match");
|
|
39
|
+
assert.strictEqual(disc.recallTier("thanks!", 8), "skip", "pleasantry + seeds → skip");
|
|
40
|
+
assert.strictEqual(disc.recallTier("👍", 3), "skip", "emoji-only → skip");
|
|
41
|
+
assert.strictEqual(disc.recallTier("restart bot", 3), "seeds", "short command → seeds-only tier");
|
|
42
|
+
assert.strictEqual(disc.recallTier("show fup logs", 2), "seeds", "≤4 words → seeds-only tier");
|
|
43
|
+
assert.strictEqual(disc.recallTier("yes we need to see the 100", 2), "full", "substantive yes-prefixed turn → full");
|
|
44
|
+
assert.strictEqual(disc.recallTier("how do I deploy the mobile app", 4), "full", "substantive → full");
|
|
34
45
|
|
|
35
46
|
// --- shared stub corpus + helpers ---
|
|
36
47
|
const packs = {
|
|
@@ -93,16 +104,28 @@ const helpers = { packsLib, entitiesLib, mergeMatches, buildPackBlock, buildEnti
|
|
|
93
104
|
});
|
|
94
105
|
assert.strictEqual(typeof out2.packBlock, "string");
|
|
95
106
|
|
|
96
|
-
// tool seeding: a
|
|
97
|
-
// renders
|
|
107
|
+
// tool seeding (seeds tier): a ≤4-word command matching a tool keeps it as a
|
|
108
|
+
// user-origin seed and renders the toolBlock; no tool match → empty toolBlock
|
|
98
109
|
const outTool = await disc.run({
|
|
99
|
-
userText: "audit
|
|
100
|
-
fullContext: "audit
|
|
110
|
+
userText: "audit mikrotik router fleet", contextText: "",
|
|
111
|
+
fullContext: "audit mikrotik router fleet", packLimit: 6, budget: {}, helpers,
|
|
101
112
|
});
|
|
102
|
-
assert.
|
|
113
|
+
assert.strictEqual(outTool.tier, "seeds", "4-word command lands on the seeds tier");
|
|
114
|
+
assert.ok(outTool.toolMatches.some((m) => m.name === "mikrotik-audit"), "matching tool is kept (seeds-tier user seed)");
|
|
103
115
|
assert.ok(/mikrotik-audit/.test(outTool.toolBlock), "kept tool renders into the toolBlock");
|
|
104
116
|
assert.ok(/Tools that may help here/.test(outTool.toolBlock), "toolBlock carries its heading");
|
|
105
117
|
|
|
118
|
+
// full-tier fail-open: with no judge, an unjudged TOOL is a guess — dropped.
|
|
119
|
+
// (Measured before the fix: a failed walk kept 15 unjudged seeds including
|
|
120
|
+
// WiFi tools for a dream question.) Pack/entity user seeds still inject.
|
|
121
|
+
const outToolFull = await disc.run({
|
|
122
|
+
userText: "audit the mikrotik router fleet inventory please", contextText: "",
|
|
123
|
+
fullContext: "audit the mikrotik router fleet inventory please", packLimit: 6, budget: {}, helpers,
|
|
124
|
+
});
|
|
125
|
+
assert.strictEqual(outToolFull.tier, "full", "substantive ask lands on the full tier");
|
|
126
|
+
assert.strictEqual(outToolFull.toolMatches.length, 0, "fail-open drops unjudged tools at full tier");
|
|
127
|
+
assert.strictEqual(outToolFull.toolBlock, "", "no unjudged toolBlock at full tier");
|
|
128
|
+
|
|
106
129
|
const outNoTool = await disc.run({
|
|
107
130
|
userText: "help with the mobile app", contextText: "", fullContext: "help with the mobile app",
|
|
108
131
|
packLimit: 6, budget: {}, helpers,
|
|
@@ -110,5 +133,26 @@ const helpers = { packsLib, entitiesLib, mergeMatches, buildPackBlock, buildEnti
|
|
|
110
133
|
assert.strictEqual(outNoTool.toolBlock, "", "no tool match → empty toolBlock");
|
|
111
134
|
assert.strictEqual(outNoTool.toolMatches.length, 0, "no tool match → no toolMatches");
|
|
112
135
|
|
|
136
|
+
// seeds tier: a short command-like turn skips graph+walker but still injects
|
|
137
|
+
// keyword seeds directly (classic baseline, ~ms path).
|
|
138
|
+
builtPacks = null;
|
|
139
|
+
const seedsTier = await disc.run({
|
|
140
|
+
userText: "mobile app status", contextText: "", fullContext: "mobile app status",
|
|
141
|
+
packLimit: 6, budget: {}, helpers,
|
|
142
|
+
});
|
|
143
|
+
assert.strictEqual(seedsTier.gated, false, "seeds tier is not gated");
|
|
144
|
+
assert.strictEqual(seedsTier.packBlock, "PACKBLOCK", "seeds tier injects keyword seeds");
|
|
145
|
+
assert.ok(seedsTier.packMatches.some((m) => m.dir === "kazee-mobile"), "seeds tier keeps the user-origin seed");
|
|
146
|
+
assert.strictEqual(seedsTier.episodeBlock, "", "seeds tier never injects episodes");
|
|
147
|
+
|
|
148
|
+
// episodes: with the walker off, transcript hits are candidates but the
|
|
149
|
+
// fail-open path must DROP them — an unjudged snippet never injects.
|
|
150
|
+
const epi = await disc.run({
|
|
151
|
+
userText: "help me deploy the mobile app updater again", contextText: "",
|
|
152
|
+
fullContext: "help me deploy the mobile app updater again", packLimit: 6, budget: {}, helpers,
|
|
153
|
+
});
|
|
154
|
+
assert.strictEqual(epi.episodeBlock, "", "fail-open drops unjudged episodes");
|
|
155
|
+
assert.deepStrictEqual(epi.episodeMatches, [], "fail-open keeps no episodeMatches");
|
|
156
|
+
|
|
113
157
|
console.log("recall discoverer OK");
|
|
114
158
|
})().catch((e) => { console.error(e); process.exit(1); });
|