klypix-mcp 1.96.0 → 1.98.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -15
- package/bin/klypix-install.mjs +59 -0
- package/bin/klypix-semantic-warm.mjs +30 -6
- package/bin/klypix-worker.mjs +2 -2
- package/package.json +2 -2
- package/src/global-brain-hook.mjs +22 -4
- package/src/klypix-core.mjs +53 -14
- package/src/klypix-format.mjs +69 -8
- package/src/mcp-presence.mjs +238 -22
- package/src/mcp-verify.mjs +9 -2
- package/src/repo-state.mjs +96 -1
- package/src/semantic-install.mjs +84 -0
- package/src/semantic-memory.mjs +166 -33
- package/src/setup.mjs +2 -2
package/README.md
CHANGED
|
@@ -235,13 +235,23 @@ not a filing convention.
|
|
|
235
235
|
session's brief. That is brief priority, not a retrieval-ranking boost.
|
|
236
236
|
- **You can ask what the project believed then.** `brain_ask` with `as_of: 2026-03-01` reweights
|
|
237
237
|
ranking by card lifecycle dates, so corrections made later do not leak backwards.
|
|
238
|
-
- **Retrieval is local.**
|
|
239
|
-
`
|
|
240
|
-
|
|
241
|
-
is available for experiments with `KLYPIX_RERANK=1`, but is off by default because
|
|
242
|
-
precision and added latency on the frozen human-paraphrase evaluation.
|
|
243
|
-
|
|
244
|
-
|
|
238
|
+
- **Retrieval is local.** With the on-device model ("search by meaning"), `brain_ask` and
|
|
239
|
+
`search_all_brains` use BGE semantic ranking with lexical help for exact identifiers, paths, and
|
|
240
|
+
versions — still entirely on your machine. Without it, retrieval is keyword-only. The previous
|
|
241
|
+
cross-encoder is available for experiments with `KLYPIX_RERANK=1`, but is off by default because
|
|
242
|
+
it reduced precision and added latency on the frozen human-paraphrase evaluation.
|
|
243
|
+
- **Search by meaning is installed for you (1.97).** `npx klypix-mcp install` run by a person in a
|
|
244
|
+
terminal installs it: about 150 MB to download once, about 110 MB on disk after the files Node
|
|
245
|
+
never loads are removed. Skip it with `--no-semantic` (or `KLYPIX_SEMANTIC=0`). Installs with no
|
|
246
|
+
terminal — CI, scripts, an AI agent running the install — get it only with `--semantic`. The
|
|
247
|
+
Claude plugin never downloads it. Measured on the frozen 102-question set: the right card in the
|
|
248
|
+
top 5 for 54 questions with it, 19 without.
|
|
249
|
+
- **Preparing a brain never makes a question wait.** Each card is turned into a meaning vector once
|
|
250
|
+
(about 45 ms a card on a laptop CPU, one card at a time, ~305 MB peak), newest cards first, by a
|
|
251
|
+
low-priority background job that saves as it goes. A question on a brain that is not ready yet
|
|
252
|
+
answers right away from what is ready plus keywords, and says how far along the preparation is
|
|
253
|
+
("Getting this brain ready for search by meaning: 900 of 3,670 cards so far, about 5 min left").
|
|
254
|
+
After that, only new or edited cards are prepared.
|
|
245
255
|
|
|
246
256
|
### Bounded semantic-memory runtime
|
|
247
257
|
|
|
@@ -518,12 +528,22 @@ Future hosts get baseline support merely by connecting the MCP server. A deeper
|
|
|
518
528
|
|
|
519
529
|
## Overlap warnings
|
|
520
530
|
|
|
521
|
-
|
|
522
|
-
declared task, and the exact paths in common. A one-time alert is queued to whichever session got
|
|
523
|
-
there first, so a late arrival is not the only one who knows.
|
|
531
|
+
`brain_sync` reports three kinds of overlap with other live sessions on the same brain:
|
|
524
532
|
|
|
525
|
-
|
|
526
|
-
|
|
533
|
+
- **Same file.** Both sessions declared, or were observed editing, the same path. You see the peer,
|
|
534
|
+
its task and the paths in common, and a one-time alert is queued to whichever session got there
|
|
535
|
+
first, so a late arrival is not the only one who knows.
|
|
536
|
+
- **Would conflict at merge.** The two sessions are on different branches, and a test merge of the
|
|
537
|
+
two branch tips (`git merge-tree`, git 2.38 or newer) conflicts in named files, even though no
|
|
538
|
+
declared file overlaps. This sees committed work only: an uncommitted edit is invisible to git.
|
|
539
|
+
It also gets a one-time alert. Files the brain tooling regenerates on every branch (AGENTS.md,
|
|
540
|
+
rule files, `.mcp.json`, `brain.klypix`) are not counted.
|
|
541
|
+
- **Same folder (advisory).** Both sessions work in one folder with no file in common yet. No alert
|
|
542
|
+
is sent. A folder that at least three sessions, and more than half of them, touch is treated as
|
|
543
|
+
noise and not reported.
|
|
544
|
+
|
|
545
|
+
This warns. It does not prevent. Nothing blocks an edit. A session that never declared its files
|
|
546
|
+
and was never observed editing is invisible to the file and folder checks.
|
|
527
547
|
|
|
528
548
|
## Handoffs and messages
|
|
529
549
|
|
|
@@ -747,6 +767,15 @@ lost 0 of 46. If the control ever loses nothing, the run reports **inconclusive*
|
|
|
747
767
|
|
|
748
768
|
Latest results, with hardware and date: [BENCHMARKS.md](BENCHMARKS.md).
|
|
749
769
|
|
|
770
|
+
**Does it change what agents do?** That needs real agents, so it lives in the repository rather than
|
|
771
|
+
the package: [`bench/coordination/`](bench/coordination/README.md) runs Claude Code or Codex on four
|
|
772
|
+
small repos where another session knows something the agent needs (a decision changed today, work
|
|
773
|
+
overlapping on another branch, a note from a session that has ended, a root cause another vendor's
|
|
774
|
+
session found). Each run is graded by a test written after the agent finishes, against no shared
|
|
775
|
+
channel, a human copy-paste, `AGENTS.md` kept tidy or appended, and KLYPIX over MCP only. Every number
|
|
776
|
+
is printed with its sample size and interval; tools it has not measured are listed as not measured.
|
|
777
|
+
`--dry-run` costs nothing and proves the graders.
|
|
778
|
+
|
|
750
779
|
## Git and concurrency
|
|
751
780
|
|
|
752
781
|
One file in your repo, committed with your code — versioned, branchable, portable. So two
|
|
@@ -1095,9 +1124,10 @@ Read this section before you build on any of it.
|
|
|
1095
1124
|
loop prevention, stable message IDs and per-recipient-machine acknowledgement primitives), but no
|
|
1096
1125
|
transport: carrying frames between machines is the desktop app's job. With `klypix-mcp` alone,
|
|
1097
1126
|
coordination is machine-local.
|
|
1098
|
-
- **
|
|
1099
|
-
|
|
1100
|
-
|
|
1127
|
+
- **File overlap is exact-path.** A session needs declared files or observed edits to be checked;
|
|
1128
|
+
`src/auth/token.ts` does not match a rename. Folder overlap is advisory. The merge test sees
|
|
1129
|
+
committed work only, needs git 2.38 or newer, and runs only for sessions on different local
|
|
1130
|
+
branches of the same repository.
|
|
1101
1131
|
- **Overlap warnings are advisory.** Nothing is blocked. One severity string in the payload reads
|
|
1102
1132
|
`blocking`; the mechanism is not.
|
|
1103
1133
|
- **Codex has no automatic capture**, with or without `--codex-hooks`. The Codex hook never writes
|
package/bin/klypix-install.mjs
CHANGED
|
@@ -12,6 +12,10 @@
|
|
|
12
12
|
// npx klypix-mcp install --codex-hooks # optional prompt/file awareness; Codex asks for trust
|
|
13
13
|
// npx klypix-mcp install --allow-untagged # acknowledge deploying an UNTAGGED source checkout
|
|
14
14
|
// # (dev deploy; also KLYPIX_MCP_ALLOW_UNTAGGED=1)
|
|
15
|
+
// npx klypix-mcp install --semantic # also install search by meaning (~150 MB download, once)
|
|
16
|
+
// npx klypix-mcp install --no-semantic # never (also KLYPIX_SEMANTIC=0)
|
|
17
|
+
// A person running install in a terminal gets search by meaning by default;
|
|
18
|
+
// scripted, CI and agent-run installs (no terminal) only with --semantic.
|
|
15
19
|
//
|
|
16
20
|
// Never-throws-silently: it's an explicit CLI, so it reports what it did and exits
|
|
17
21
|
// non-zero on a real failure. Never wires a broken settings.json (refuse + restore).
|
|
@@ -35,6 +39,7 @@ import { brainInstallDecision, deploySourceDecision } from '../src/install-versi
|
|
|
35
39
|
import { acquireInstallLockSync, releaseInstallLockSync } from '../src/install-lock.mjs';
|
|
36
40
|
import { collectRepoState } from '../src/repo-state.mjs';
|
|
37
41
|
import { runSetup, renderBrief } from '../src/setup.mjs';
|
|
42
|
+
import { SEMANTIC_RUNTIME_MANIFEST, dirBytes, pruneSemanticRuntime, semanticInstallDecision } from '../src/semantic-install.mjs';
|
|
38
43
|
|
|
39
44
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
40
45
|
const PKG_ROOT = path.resolve(__dirname, '..');
|
|
@@ -60,6 +65,10 @@ const SETUP_JSON = process.argv.includes('--json');
|
|
|
60
65
|
// this is source authority (knowingly deploy an untagged working tree).
|
|
61
66
|
const ALLOW_UNTAGGED = process.argv.includes('--allow-untagged')
|
|
62
67
|
|| process.env.KLYPIX_MCP_ALLOW_UNTAGGED === '1';
|
|
68
|
+
// Search by meaning (1.97.0). Measured 2026-10-11 on the real 102-question
|
|
69
|
+
// set: the right card in the top 5 for 54 questions with it, 19 without.
|
|
70
|
+
const SEMANTIC_FLAG = process.argv.includes('--semantic');
|
|
71
|
+
const NO_SEMANTIC = process.argv.includes('--no-semantic') || process.env.KLYPIX_SEMANTIC === '0';
|
|
63
72
|
|
|
64
73
|
const HOME = os.homedir();
|
|
65
74
|
const CLAUDE_DIR = path.join(HOME, '.claude');
|
|
@@ -143,6 +152,52 @@ function wireCodex() {
|
|
|
143
152
|
};
|
|
144
153
|
}
|
|
145
154
|
|
|
155
|
+
// ── Search by meaning: the optional on-device runtime (src/semantic-install.mjs) ──
|
|
156
|
+
function installSemanticRuntimeIfWanted() {
|
|
157
|
+
const semanticDir = path.join(BRAIN_DIR, 'semantic');
|
|
158
|
+
const transformers = path.join(semanticDir, 'node_modules', '@huggingface', 'transformers');
|
|
159
|
+
const decision = semanticInstallDecision({
|
|
160
|
+
optOut: NO_SEMANTIC,
|
|
161
|
+
flag: SEMANTIC_FLAG,
|
|
162
|
+
runtimeOnly: RUNTIME_ONLY,
|
|
163
|
+
plugin: String(process.env.KLYPIX_PLUGIN ?? '').trim() === '1',
|
|
164
|
+
present: exists(transformers),
|
|
165
|
+
ci: Boolean(process.env.CI),
|
|
166
|
+
tty: Boolean(process.stdout.isTTY),
|
|
167
|
+
});
|
|
168
|
+
if (decision !== 'install') return { decision };
|
|
169
|
+
console.log('• Installing search by meaning (about 150 MB to download, once; skip with --no-semantic)…');
|
|
170
|
+
const started = Date.now();
|
|
171
|
+
try {
|
|
172
|
+
fs.mkdirSync(semanticDir, { recursive: true });
|
|
173
|
+
fs.writeFileSync(path.join(semanticDir, 'package.json'), `${JSON.stringify(SEMANTIC_RUNTIME_MANIFEST, null, 2)}\n`);
|
|
174
|
+
const npmArgs = ['install', '--prefix', semanticDir, '--omit=dev', '--no-audit', '--no-fund', '--loglevel=error'];
|
|
175
|
+
if (process.platform === 'win32') execFileSync('cmd.exe', ['/d', '/s', '/c', 'npm.cmd', ...npmArgs], { stdio: ['ignore', 'ignore', 'pipe'], timeout: 600_000, windowsHide: true });
|
|
176
|
+
else execFileSync('npm', npmArgs, { stdio: ['ignore', 'ignore', 'pipe'], timeout: 600_000 });
|
|
177
|
+
if (!exists(transformers)) throw new Error('npm finished but the runtime is missing');
|
|
178
|
+
let freed = 0;
|
|
179
|
+
try { freed = pruneSemanticRuntime(semanticDir); } catch { /* pruning is an optimisation */ }
|
|
180
|
+
return { decision, ok: true, seconds: Math.round((Date.now() - started) / 1000), sizeMb: Math.round(dirBytes(path.join(semanticDir, 'node_modules')) / 1048576), freedMb: Math.round(freed / 1048576) };
|
|
181
|
+
} catch (error) {
|
|
182
|
+
const why = String(error?.stderr || error?.message || error).split('\n').map(s => s.trim()).filter(Boolean).slice(-1)[0] || 'npm failed';
|
|
183
|
+
return { decision, ok: false, error: why.slice(0, 200) };
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function reportSemantic(step) {
|
|
188
|
+
const turnOn = 'npx klypix-mcp install --semantic';
|
|
189
|
+
if (!step) return;
|
|
190
|
+
if (step.decision === 'install' && step.ok) {
|
|
191
|
+
console.log(`✓ search by meaning installed in ${step.seconds}s (${step.sizeMb} MB on disk). Your project brains are being prepared in the background, newest cards first: questions work right away and get better as it finishes.`);
|
|
192
|
+
} else if (step.decision === 'install') {
|
|
193
|
+
console.error(`⚠ search by meaning could not be installed (${step.error}). Keyword search still works. Try again with: ${turnOn}`);
|
|
194
|
+
} else if (step.decision === 'present') {
|
|
195
|
+
console.log('✓ search by meaning: on');
|
|
196
|
+
} else if (step.decision === 'skip-noninteractive' || step.decision === 'skip-ci') {
|
|
197
|
+
console.log(`• search by meaning is off. Turn it on with: ${turnOn} (about 150 MB, once; agents then find the right card about 3x as often)`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
146
201
|
function reportCodex(result) {
|
|
147
202
|
if (result.ok) {
|
|
148
203
|
const mcp = result.mcp.action === 'skipped-no-project-brain'
|
|
@@ -594,6 +649,9 @@ try {
|
|
|
594
649
|
const notWired = ['SessionStart', 'UserPromptSubmit', 'Stop', 'PostToolUse', 'PreToolUse'].filter(e => !wiredFor(e));
|
|
595
650
|
|
|
596
651
|
releaseInstallLockSync(installLock);
|
|
652
|
+
// Search by meaning: installed AFTER the brain itself is committed, so a
|
|
653
|
+
// slow or failed download never touches a working keyword-only install.
|
|
654
|
+
const semanticStep = installSemanticRuntimeIfWanted();
|
|
597
655
|
// Users who already enabled the optional local semantic runtime should not
|
|
598
656
|
// pay a multi-minute first question after a model/cache contract upgrade.
|
|
599
657
|
// Migrate registered brains once in a detached process after the atomic
|
|
@@ -617,6 +675,7 @@ try {
|
|
|
617
675
|
}
|
|
618
676
|
if (!RUNTIME_ONLY) reportCodex(codex);
|
|
619
677
|
console.log(`✓ installed klypix brain v${VERSION} → ${BRAIN_DIR} (${n} scripts, ${deps} dep packages)`);
|
|
678
|
+
reportSemantic(semanticStep);
|
|
620
679
|
if (RUNTIME_ONLY) console.log('✓ runtime-only update: host settings and project files were preserved');
|
|
621
680
|
else if (!notWired.length) console.log('✓ wired 5 hooks: SessionStart · UserPromptSubmit (--prompt) · Stop (--capture) · PostToolUse (--live) · PreToolUse (--guard) → settings.json');
|
|
622
681
|
else console.error(`⚠ readiness: ${notWired.length} hook(s) did NOT take (${notWired.join(', ')}) — the brain will read but not capture/sync. Re-run \`npx klypix-mcp install --force\` or check ${SETTINGS}.`);
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// One detached, best-effort
|
|
3
|
-
//
|
|
4
|
-
//
|
|
2
|
+
// One detached, best-effort preparation of brains for search by meaning: after
|
|
3
|
+
// an install or a compatible core update (every registered brain), or started
|
|
4
|
+
// by a question on a brain that is not ready yet (--brain <file> --only: that
|
|
5
|
+
// brain alone). It runs at below-normal priority, reports progress to the
|
|
6
|
+
// status file while it works (brainPreparationStatus reads it), refreshes its
|
|
7
|
+
// lock so a long run is never taken for a dead one, and never changes brain
|
|
8
|
+
// content.
|
|
5
9
|
import fs from 'fs';
|
|
6
10
|
import os from 'os';
|
|
7
11
|
import path from 'path';
|
|
@@ -9,6 +13,7 @@ import { parseKlypix } from '../src/klypix-format.mjs';
|
|
|
9
13
|
import { readRegisteredProjectBrains } from '../src/mcp-auto-update.mjs';
|
|
10
14
|
import {
|
|
11
15
|
EMBEDDING_CACHE_KEY,
|
|
16
|
+
cachedVectorsForBrain,
|
|
12
17
|
disposeSemanticModels,
|
|
13
18
|
getEmbedderForUse,
|
|
14
19
|
vectorsForBrain,
|
|
@@ -22,6 +27,7 @@ const brainDir = path.resolve(arg('brain-dir', path.join(os.homedir(), '.claude'
|
|
|
22
27
|
const lockFile = path.join(brainDir, '.semantic-update-warm.lock');
|
|
23
28
|
const statusFile = path.join(brainDir, '.semantic-update-warm.json');
|
|
24
29
|
const explicitBrain = arg('brain');
|
|
30
|
+
const onlyExplicit = process.argv.includes('--only');
|
|
25
31
|
const startedAt = Date.now();
|
|
26
32
|
let lock = null;
|
|
27
33
|
|
|
@@ -31,6 +37,9 @@ const atomicJson = (file, value) => {
|
|
|
31
37
|
fs.renameSync(tmp, file);
|
|
32
38
|
};
|
|
33
39
|
|
|
40
|
+
// A background job: never compete with the person's own work for the CPU.
|
|
41
|
+
try { os.setPriority(os.constants.priority.PRIORITY_BELOW_NORMAL); } catch { /* best-effort */ }
|
|
42
|
+
|
|
34
43
|
try {
|
|
35
44
|
fs.mkdirSync(brainDir, { recursive: true });
|
|
36
45
|
try {
|
|
@@ -45,13 +54,17 @@ try {
|
|
|
45
54
|
}
|
|
46
55
|
fs.writeFileSync(lock, JSON.stringify({ pid: process.pid, startedAt, modelKey: EMBEDDING_CACHE_KEY }));
|
|
47
56
|
|
|
48
|
-
const registered = readRegisteredProjectBrains(brainDir).map(item => item.brainPath);
|
|
57
|
+
const registered = onlyExplicit && explicitBrain ? [] : readRegisteredProjectBrains(brainDir).map(item => item.brainPath);
|
|
49
58
|
const candidates = [...new Set([explicitBrain ? path.resolve(explicitBrain) : null, ...registered].filter(Boolean))]
|
|
50
59
|
.filter(file => {
|
|
51
60
|
try { return fs.statSync(file).isFile(); } catch { return false; }
|
|
52
61
|
});
|
|
53
62
|
if (!candidates.length) {
|
|
54
|
-
|
|
63
|
+
// Nothing to prepare yet, but fetch the model now (~33 MB, once) so the
|
|
64
|
+
// first question on a new brain does not pay for the download.
|
|
65
|
+
let model = 'unavailable';
|
|
66
|
+
try { model = (await getEmbedderForUse(() => {}, 5 * 60_000)) ? 'downloaded' : 'unavailable'; } catch { /* the first question fetches it */ }
|
|
67
|
+
atomicJson(statusFile, { protocol: 1, result: 'no-registered-brains', model, modelKey: EMBEDDING_CACHE_KEY, completedAt: new Date().toISOString() });
|
|
55
68
|
} else {
|
|
56
69
|
const pipe = await getEmbedderForUse(() => {}, 5 * 60_000);
|
|
57
70
|
if (!pipe) {
|
|
@@ -61,7 +74,18 @@ try {
|
|
|
61
74
|
for (const file of candidates) {
|
|
62
75
|
try {
|
|
63
76
|
const { struct } = await parseKlypix(fs.readFileSync(file));
|
|
64
|
-
const
|
|
77
|
+
const brainStarted = new Date().toISOString();
|
|
78
|
+
let lastReport = 0;
|
|
79
|
+
const report = ({ done, total }, force = false) => {
|
|
80
|
+
if (!force && Date.now() - lastReport < 3_000) return;
|
|
81
|
+
lastReport = Date.now();
|
|
82
|
+
try { atomicJson(statusFile, { protocol: 1, result: 'preparing', modelKey: EMBEDDING_CACHE_KEY, brain: file, done, total, startedAt: brainStarted, updatedAt: new Date().toISOString() }); } catch { /* advisory */ }
|
|
83
|
+
try { const now = new Date(); fs.utimesSync(lockFile, now, now); } catch { /* advisory */ }
|
|
84
|
+
};
|
|
85
|
+
let alreadyReady = 0;
|
|
86
|
+
try { alreadyReady = cachedVectorsForBrain(file, struct.cards).size; } catch { /* starts from zero */ }
|
|
87
|
+
report({ done: alreadyReady, total: struct.cards.filter(c => c.type !== 'container' && String(c.text || '').trim()).length }, true);
|
|
88
|
+
const vectors = await vectorsForBrain(pipe, file, struct.cards, { maxEmbed: Infinity, onProgress: report });
|
|
65
89
|
warmed++;
|
|
66
90
|
cards += vectors.size;
|
|
67
91
|
} catch { failed++; }
|
package/bin/klypix-worker.mjs
CHANGED
|
@@ -508,7 +508,7 @@ server.registerTool('search_all_brains', {
|
|
|
508
508
|
|
|
509
509
|
server.registerTool('brain_ask', {
|
|
510
510
|
title: 'Ask the project brain a question (whole-brain, correction-aware answer)',
|
|
511
|
-
description: 'Answer a natural-language question from the WHOLE project brain — "what did we decide about X?", "where did the auth work land?", "why did we drop Y?". Ranks every card
|
|
511
|
+
description: 'Answer a natural-language question from the WHOLE project brain — "what did we decide about X?", "where did the auth work land?", "why did we drop Y?". Ranks every card by meaning with the on-device model (installed by `npx klypix-mcp install` run in a terminal, or with `--semantic`), with keywords for exact names; without the model it is keyword-only, and while a brain is still being prepared the answer says how far along it is. INCLUDES superseded/archived history (flagged, so you can see how a decision changed), and attaches each stale card\'s live CORRECTION so the answer reflects the current truth, not an outdated card. Returns a synthesis-ready context (full cards + provenance + lifecycle) for you to turn into a direct, cited answer — it does not itself write prose. Prefer this over search_canvases when the user asks a QUESTION (not a keyword lookup). Optional as_of (YYYY-MM-DD) answers "what was true then". Cards are often worded differently from the question, so end the question with one more line, `Also: <8–14 words a card answering it would likely use: synonyms, feature, file or tool names>`; they widen the search while the question still leads (measured: right card in the top 5 for 60 of 102 test questions instead of 55; 24 instead of 19 when no local model is installed). Defaults to the project brain ("brain").',
|
|
512
512
|
inputSchema: {
|
|
513
513
|
question: z.string().describe('The natural-language question to answer from the brain.'),
|
|
514
514
|
canvas: z.string().optional().describe('Brain canvas filename/path. Defaults to the project brain ("brain").'),
|
|
@@ -1066,7 +1066,7 @@ server.registerTool('brain_reopen', {
|
|
|
1066
1066
|
|
|
1067
1067
|
server.registerTool('brain_sync', {
|
|
1068
1068
|
title: 'KLYPIX Context Gateway — synchronize task, peers, conflicts, and relevant memory',
|
|
1069
|
-
description: 'APPROVAL-FREE task gateway over the authorized MCP connection. Call FIRST with a concise intent and expected files, again when scope changes, and with phase:"complete" before the final response. One bounded response returns compact task-relevant brain context, active TASK peers (idle connections hidden), one-time messages, structured exact-file conflicts, and late-arrival overlap alerts. A completion that supplies machine-checkable result manifests is fail-closed: invalid, conflicting, or incomparable evidence returns needs-reconciliation and retains task scope. Works on any MCP host — it needs only the authorized MCP connection, so native lifecycle hooks are optional. LIMITS:
|
|
1069
|
+
description: 'APPROVAL-FREE task gateway over the authorized MCP connection. Call FIRST with a concise intent and expected files, again when scope changes, and with phase:"complete" before the final response. One bounded response returns compact task-relevant brain context, active TASK peers (idle connections hidden), one-time messages, structured exact-file conflicts, and late-arrival overlap alerts. A completion that supplies machine-checkable result manifests is fail-closed: invalid, conflicting, or incomparable evidence returns needs-reconciliation and retains task scope. `results` is ONLY for such manifests, never a summary of the work: with nothing machine-checkable to claim, complete without it. Works on any MCP host — it needs only the authorized MCP connection, so native lifecycle hooks are optional. LIMITS: blocking overlap is EXACT-PATH over the declared files of each session plus the edits its host observed; sessions on different branches are also test-merged with git merge-tree (committed work only); a same-folder overlap is an advisory notice; coordination/result reconciliation is machine-local and OS-user-local (a teammate on another machine is invisible), and file overlap remains ADVISORY.',
|
|
1070
1070
|
annotations: {
|
|
1071
1071
|
destructiveHint: false,
|
|
1072
1072
|
idempotentHint: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.98.0",
|
|
4
4
|
"mcpName": "io.github.dahshanlabs/klypix-mcp",
|
|
5
5
|
"description": "Active state management for multi-agent coding: a shared, versioned project brain over MCP.",
|
|
6
6
|
"type": "module",
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
"bench": "node bin/klypix-mcp.mjs bench",
|
|
87
87
|
"test:bench": "node test/bench.mjs",
|
|
88
88
|
"pretest": "node test/publish-workflow.mjs",
|
|
89
|
-
"test": "node test/publish-verdict.mjs && node test/npx-owned-names.mjs && node test/project-graph.mjs && node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/session-mailbox.mjs && node test/session-reopen.mjs && node test/lane-write-retry.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/install-rename-backoff.mjs && node test/project-binding-rebind.mjs && node test/context-gateway.mjs && node test/repo-state.mjs && node test/released-tag-guard.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/capture-gap.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brain-connect-orphans.mjs && node test/orphan-gardener.mjs && node test/brief-and-recall.mjs && node test/guard-cards.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/brain-ask-words.mjs && node test/retrieval-fusion.mjs && node test/eval-retrieval.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/partial-notes.mjs && node test/lifecycle-prefix.mjs && node test/close-link-safety.mjs && node test/resolve-ledger.mjs && node test/plan-fulfillment.mjs && node test/arrange-receipts.mjs && node test/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-security.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/semantic-hash-parity.mjs && node test/enrichment.mjs && node test/provenance.mjs && node test/confirm-trail.mjs && node test/hook-fallback.mjs && node test/eval-hook-lane.mjs && node test/hook-unified-lane.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/marker-suffix-grammar.mjs && node test/evidence-anchors.mjs && node test/brain-evidence.mjs && node test/presence-visibility.mjs && node test/undeclared-active.mjs && node test/presence-liveness.mjs && node test/observed-scope.mjs && node test/release-lease.mjs && node test/release-reconcile.mjs && node test/release-ancestry.mjs && node test/release-claim-join.mjs && node test/release-claims.mjs && node test/release-handshake.mjs && node test/completion-guard.mjs && node test/merge-brains.mjs && node test/merge-readings.mjs && node test/revival-map.mjs && node test/merge-scale.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/capture-write-failure.mjs && node test/a2a-smoke.mjs && node test/one-command-setup.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/canvas-groups.mjs && node test/git-tools.mjs && node test/link-compat.mjs && node test/uninstall.mjs && node test/current-guidance.mjs && node test/status-shape.mjs && node test/status-hook.mjs && node test/read-canvas-saved-readings.mjs && node test/document-readings.mjs && node test/open-lease.mjs && node test/agent-card-style.mjs && node test/tool-schema-freeze.mjs && node test/app-lease-vectors.mjs && node test/app-tools-stdio.mjs && node test/card-contents-files.mjs && node test/codex-app-table.mjs && node test/plugin-mode.mjs && node test/app-bridge-vectors.mjs && node test/app-bridge-client.mjs && node test/app-tools-static.mjs && node test/app-tools-stdio-live.mjs",
|
|
89
|
+
"test": "node test/publish-verdict.mjs && node test/npx-owned-names.mjs && node test/project-graph.mjs && node test/project-map-cli.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/request-identity.mjs && node test/session-identity-core.mjs && node test/agent-presence.mjs && node test/message-delivery-v3.mjs && node test/claude-message-delivery-v3.mjs && node test/session-mailbox.mjs && node test/session-reopen.mjs && node test/lane-write-retry.mjs && node test/result-reconcile.mjs && node test/evidence-publication-gate.mjs && node test/release-evidence-cli.mjs && node test/intent-guard.mjs && node test/git-capture-install.mjs && node test/brain-history.mjs && node test/brain-graveyard.mjs && node test/archived-visibility.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/install-version.mjs && node test/install-rename-backoff.mjs && node test/project-binding-rebind.mjs && node test/context-gateway.mjs && node test/repo-state.mjs && node test/released-tag-guard.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/capture-gap.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brain-connect-orphans.mjs && node test/orphan-gardener.mjs && node test/brief-and-recall.mjs && node test/guard-cards.mjs && node test/layout-cluster.mjs && node test/brain-ask.mjs && node test/brain-ask-words.mjs && node test/retrieval-fusion.mjs && node test/eval-retrieval.mjs && node test/field-report-2026-07-04.mjs && node test/autoprop.mjs && node test/overlay-recency-2026-07-12.mjs && node test/brain-challenge.mjs && node test/brain-lens.mjs && node test/brain-kind.mjs && node test/rule-drafts.mjs && node test/claim-engine.mjs && node test/partial-notes.mjs && node test/lifecycle-prefix.mjs && node test/close-link-safety.mjs && node test/resolve-ledger.mjs && node test/plan-fulfillment.mjs && node test/arrange-receipts.mjs && node test/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-security.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/semantic-preparation.mjs && node test/semantic-hash-parity.mjs && node test/enrichment.mjs && node test/provenance.mjs && node test/confirm-trail.mjs && node test/hook-fallback.mjs && node test/eval-hook-lane.mjs && node test/hook-unified-lane.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/marker-suffix-grammar.mjs && node test/evidence-anchors.mjs && node test/brain-evidence.mjs && node test/presence-visibility.mjs && node test/undeclared-active.mjs && node test/presence-liveness.mjs && node test/observed-scope.mjs && node test/overlap-tiers.mjs && node test/coordination-bench.mjs && node test/release-lease.mjs && node test/release-reconcile.mjs && node test/release-ancestry.mjs && node test/release-claim-join.mjs && node test/release-claims.mjs && node test/release-handshake.mjs && node test/completion-guard.mjs && node test/results-not-manifests.mjs && node test/merge-brains.mjs && node test/merge-readings.mjs && node test/revival-map.mjs && node test/merge-scale.mjs && node test/concurrent-writes.mjs && node test/lock-interop.mjs && node test/capture-write-failure.mjs && node test/a2a-smoke.mjs && node test/one-command-setup.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/canvas-groups.mjs && node test/git-tools.mjs && node test/link-compat.mjs && node test/uninstall.mjs && node test/current-guidance.mjs && node test/current-first.mjs && node test/status-shape.mjs && node test/status-hook.mjs && node test/read-canvas-saved-readings.mjs && node test/document-readings.mjs && node test/open-lease.mjs && node test/agent-card-style.mjs && node test/tool-schema-freeze.mjs && node test/app-lease-vectors.mjs && node test/app-tools-stdio.mjs && node test/card-contents-files.mjs && node test/codex-app-table.mjs && node test/plugin-mode.mjs && node test/app-bridge-vectors.mjs && node test/app-bridge-client.mjs && node test/app-tools-static.mjs && node test/app-tools-stdio-live.mjs",
|
|
90
90
|
"test:memory": "node test/memory-runtime.mjs",
|
|
91
91
|
"test:memory:soak": "node --expose-gc test/memory-soak.mjs",
|
|
92
92
|
"runtime": "node bin/klypix-runtime.mjs",
|
|
@@ -1462,6 +1462,22 @@ function peerFooter(sid) {
|
|
|
1462
1462
|
const lines = ['', `## ⚠️ Other live session(s) in this project — ${peers.length} total, as of ${asOf}${hiddenIdle ? ` (+${hiddenIdle} inactive not shown)` : ''} — coordinate (the brain is shared, NOT live-merged)`];
|
|
1463
1463
|
const releaseLine = releaseLeaseFooterLine(list, now);
|
|
1464
1464
|
if (releaseLine) lines.push(releaseLine);
|
|
1465
|
+
// Same branch, said ONCE (1.98.0). Two sessions can only share a branch
|
|
1466
|
+
// name by sharing a checkout (git refuses one branch in two worktrees), so
|
|
1467
|
+
// the risk is concrete: a broad `git add` sweeps the other session's edits
|
|
1468
|
+
// into your commit. Per-peer, this line repeated on every row of a busy
|
|
1469
|
+
// lane and stopped being read.
|
|
1470
|
+
const sameBranchCount = myBranch ? peers.filter(p => p.branch === myBranch).length : 0;
|
|
1471
|
+
if (sameBranchCount) lines.push(`- ⚠️ ${sameBranchCount} of these session(s) share your branch \`${myBranch}\` (one checkout): stage only your own files — never \`git add -u\` / \`-A\` — and coordinate before you commit.`);
|
|
1472
|
+
// Same folder, no shared file yet (twin of mcp-presence findNearbyWork, no
|
|
1473
|
+
// git): with 3+ scoped sessions, a folder most of them touch is noise.
|
|
1474
|
+
const dirOf = (k) => { const at = k.lastIndexOf('/'); return at > 0 ? k.slice(0, at) : ''; };
|
|
1475
|
+
const dirsOfScope = (s) => new Set(scopeOf(s).map(fileKey).map(dirOf).filter(Boolean));
|
|
1476
|
+
const myDirs = dirsOfScope(me);
|
|
1477
|
+
const scopedRows = [me, ...peers].filter(s => s && scopeOf(s).length);
|
|
1478
|
+
const dirCount = new Map();
|
|
1479
|
+
for (const s of scopedRows) for (const d of dirsOfScope(s)) dirCount.set(d, (dirCount.get(d) || 0) + 1);
|
|
1480
|
+
const dirIsNoise = (d) => { const n = dirCount.get(d) || 0; return n >= 3 && n > scopedRows.length / 2; };
|
|
1465
1481
|
for (const p of peers.slice(0, 4)) {
|
|
1466
1482
|
const bits = [];
|
|
1467
1483
|
if (p.branch) bits.push(`branch \`${p.branch}\``);
|
|
@@ -1496,10 +1512,12 @@ function peerFooter(sid) {
|
|
|
1496
1512
|
warn = ` · ⚠️ both edited: ${marked.join(', ')} — expect a conflict, KEEP BOTH${legend}`;
|
|
1497
1513
|
}
|
|
1498
1514
|
// State the fact, never the git operation. This runs in a latency-sensitive
|
|
1499
|
-
// hook and must not spawn git
|
|
1500
|
-
//
|
|
1501
|
-
|
|
1502
|
-
|
|
1515
|
+
// hook and must not spawn git. The same-branch fact is stated once above
|
|
1516
|
+
// the list; per peer, only a shared folder is added (advisory).
|
|
1517
|
+
else {
|
|
1518
|
+
const near = [...dirsOfScope(p)].filter(d => myDirs.has(d) && !dirIsNoise(d)).sort().slice(0, 2);
|
|
1519
|
+
if (near.length) warn = ` · 📁 same folder: ${near.map(d => `${d}/`).join(', ')} — no shared file yet`;
|
|
1520
|
+
}
|
|
1503
1521
|
// Unique over the full live list (not just shown peers) — the MSG router
|
|
1504
1522
|
// resolves a prefix against every row, so uniqueness must match that set.
|
|
1505
1523
|
lines.push(`- session ${shortestUniquePeerPrefix(list, p.id)} · ${bits.join(' · ')}${warn}`);
|
package/src/klypix-core.mjs
CHANGED
|
@@ -34,7 +34,7 @@ import {
|
|
|
34
34
|
challengeBrain, challengeContextToMarkdown, buildRenderSpec, structToBrief,
|
|
35
35
|
brainLensData, lensToMarkdown, deathDateOfCard,
|
|
36
36
|
statusContextToMarkdown, findFulfillmentCandidates, findStaleOpenCards, releaseFulfilledOpens, openStatusSummary, perAreaTableToMarkdown,
|
|
37
|
-
splitQueryTokens, scoreCardsAgainstQuery, correctionOverlaysFor, currentGuidanceFor, currentGuidancePrefix,
|
|
37
|
+
splitQueryTokens, scoreCardsAgainstQuery, correctionOverlaysFor, supersededByNote, currentGuidanceFor, currentGuidancePrefix,
|
|
38
38
|
isFastDecayCard, isUnresolvedOpenCard, isSkillCard, validateGuard, guardSidecarPathFor, ensureGuardSidecar, DECAY_STALE_MS, formatDecayAge,
|
|
39
39
|
isPlanCard, planFulfillmentFor, PLAN_PAIR_SIM_BRAIN, isAgconfTwinId,
|
|
40
40
|
readPendingShips, clearPendingShips, pendingShipCards, formatCaptureReceipts, parseVerifySuffix, amendmentFirst,
|
|
@@ -56,7 +56,7 @@ import {
|
|
|
56
56
|
dot, embedTexts, getEmbedder, getEmbedderForUse, withRerankerForUse,
|
|
57
57
|
rerankHits, semanticFallbackNotice, semanticMemorySnapshot,
|
|
58
58
|
semanticRuntimeInstalled, shouldPrewarmSemantic, vectorsForBrain,
|
|
59
|
-
cachedVectorsForBrain,
|
|
59
|
+
cachedVectorsForBrain, startBrainPreparation, preparationNotice,
|
|
60
60
|
} from './semantic-memory.mjs';
|
|
61
61
|
|
|
62
62
|
// The MCP and A2A faces prewarm through this protocol-neutral module. Bounded
|
|
@@ -435,6 +435,9 @@ export async function opSearchCanvases({ vault, query }) {
|
|
|
435
435
|
const isArchived = (c) => /^archive$/i.test(c.area || '');
|
|
436
436
|
const n = struct.counts;
|
|
437
437
|
const head = `## ${rel} — "${struct.title}" · ${n.live ?? n.cards} live cards${n.archived ? `, ${n.archived} archived` : ''}, ${n.connections} connections${saved ? ` · saved ${saved}` : ''}${nameMatch && !matched.length ? ' (name/title match)' : ''}`;
|
|
438
|
+
// A stale hit names its replacement (1.98.0) — never a bare "archived".
|
|
439
|
+
let successors = new Map();
|
|
440
|
+
try { successors = correctionOverlaysFor(struct, matched.slice(0, 8)); } catch { successors = new Map(); }
|
|
438
441
|
const body = matched.slice(0, 8).map(c => {
|
|
439
442
|
const pos = (c.pos && c.pos.x != null) ? ` @(${Math.round(c.pos.x)},${Math.round(c.pos.y)})` : '';
|
|
440
443
|
const allTags = [...new Set([...(c.tags || []), ...itemTags(c)])];
|
|
@@ -449,7 +452,10 @@ export async function opSearchCanvases({ vault, query }) {
|
|
|
449
452
|
const fromReading = reading && !hit(c.title) && !hit(c.text) && hit(reading.text)
|
|
450
453
|
? `\n in KLYPIX's saved reading: ${escapeInstructionLines(reading.text.replace(/\s+/g, ' ').slice(0, 240))}`
|
|
451
454
|
: '';
|
|
452
|
-
|
|
455
|
+
const replaced = successors.has(c.id)
|
|
456
|
+
? `\n ↪ ${supersededByNote(successors.get(c.id))} — read that card for the current state`
|
|
457
|
+
: '';
|
|
458
|
+
return `- [${c.type}] "${c.title || '(card)'}" (${c.id})${pos}${date ? ` · ${date}` : ''}${tags}${arch}\n ${escapeInstructionLines(ownText)}${fromReading}${replaced}`;
|
|
453
459
|
}).join('\n');
|
|
454
460
|
hits.push(matched.length ? `${head}\n${body}` : head);
|
|
455
461
|
}
|
|
@@ -503,7 +509,13 @@ export async function opSearchAllBrains({ vault, query, as_of, log = () => {} })
|
|
|
503
509
|
let struct;
|
|
504
510
|
try { ({ struct } = await parseKlypix(fs.readFileSync(b.path))); } catch { continue; }
|
|
505
511
|
let vecs = null;
|
|
506
|
-
if (qv) { try {
|
|
512
|
+
if (qv) { try { const r = await questionVectors(pipe, b.path, struct.cards); vecs = r.usable ? r.vecs : null; } catch { /* lexical for this brain */ } }
|
|
513
|
+
// Replacement per stale card (1.98.0), only when this brain HAS lifecycle
|
|
514
|
+
// edges; skipped under as_of (a correction is a present-day fact).
|
|
515
|
+
let successors = new Map();
|
|
516
|
+
if (asOfTs == null && (struct.connections || []).some(cn => cn.label === 'superseded by' || cn.label === 'closed by')) {
|
|
517
|
+
try { successors = correctionOverlaysFor(struct, struct.cards); } catch { successors = new Map(); }
|
|
518
|
+
}
|
|
507
519
|
for (const c of struct.cards) {
|
|
508
520
|
if (c.type === 'container') continue;
|
|
509
521
|
const t = String(c.text || '').toLowerCase();
|
|
@@ -529,7 +541,7 @@ export async function opSearchAllBrains({ vault, query, as_of, log = () => {} })
|
|
|
529
541
|
if (isArchived) score -= 1;
|
|
530
542
|
if (isCur) score += sem != null ? 1.5 : 1; // current-project locality prior
|
|
531
543
|
}
|
|
532
|
-
scored.push({ score, sem, cur: isCur, project: b.project || path.basename(path.dirname(b.path)), area: c.area, c });
|
|
544
|
+
scored.push({ score, sem, cur: isCur, project: b.project || path.basename(path.dirname(b.path)), area: c.area, c, successor: successors.get(c.id) || null });
|
|
533
545
|
}
|
|
534
546
|
}
|
|
535
547
|
if (!scored.length) return { blocks: [text(`No matches for "${query}" across ${brains.length} registered brain(s).`)] };
|
|
@@ -537,7 +549,7 @@ export async function opSearchAllBrains({ vault, query, as_of, log = () => {} })
|
|
|
537
549
|
const top = scored.slice(0, 20);
|
|
538
550
|
const lines = top.map(h => {
|
|
539
551
|
const when = h.c.createdAt ? new Date(h.c.createdAt).toISOString().slice(0, 10) : '';
|
|
540
|
-
return `- ${h.cur ? '★ ' : ''}[${h.project}${h.area ? ' › ' + h.area : ''}] ${when} ${String(h.c.text || '').replace(/\s+/g, ' ').slice(0, 240)}`;
|
|
552
|
+
return `- ${h.cur ? '★ ' : ''}[${h.project}${h.area ? ' › ' + h.area : ''}] ${when} ${String(h.c.text || '').replace(/\s+/g, ' ').slice(0, 240)}${h.successor ? `\n ↪ ${supersededByNote(h.successor)}` : ''}`;
|
|
541
553
|
});
|
|
542
554
|
// Never say "warming — retry" to someone who simply has not installed the
|
|
543
555
|
// runtime: for them that advice is wrong forever, and their results are the
|
|
@@ -765,6 +777,21 @@ export async function opBrainTaskContext({
|
|
|
765
777
|
};
|
|
766
778
|
}
|
|
767
779
|
|
|
780
|
+
// Card meanings for a question, never waiting on a preparation (1.97.0): at
|
|
781
|
+
// most a few seconds of embedding inline; whatever is still missing is handed
|
|
782
|
+
// to the background preparation (startBrainPreparation) and the answer says
|
|
783
|
+
// so (preparationNotice). Meaning ranking is used once at least
|
|
784
|
+
// MEANING_READY_SHARE of the cards are ready — below that the keyword path
|
|
785
|
+
// covers every card, which a mostly-unprepared meaning ranking would not.
|
|
786
|
+
const MEANING_READY_SHARE = 0.5;
|
|
787
|
+
async function questionVectors(pipe, file, cards) {
|
|
788
|
+
const stats = {};
|
|
789
|
+
const vecs = await vectorsForBrain(pipe, file, cards, { stats });
|
|
790
|
+
if (stats.pending > 0) startBrainPreparation(file);
|
|
791
|
+
const usable = Boolean(vecs && vecs.size) && (!stats.wanted || stats.ready / stats.wanted >= MEANING_READY_SHARE);
|
|
792
|
+
return { vecs, stats, usable };
|
|
793
|
+
}
|
|
794
|
+
|
|
768
795
|
export async function opBrainAsk({ vault, canvas, question, as_of, k = 10, log = () => {} }) {
|
|
769
796
|
// A last line `Also: <words>` is the caller's guess at the card's wording; it
|
|
770
797
|
// joins the meaning query only (splitAskWords in klypix-format.mjs).
|
|
@@ -781,13 +808,14 @@ export async function opBrainAsk({ vault, canvas, question, as_of, k = 10, log =
|
|
|
781
808
|
// Semantic blend (best-effort, time-bounded): embed the question + the brain's
|
|
782
809
|
// cards on-device, hand rankForQuestion a Map<cardId, cosine>. A missing/warming
|
|
783
810
|
// model degrades cleanly to pure lexical.
|
|
784
|
-
let semantic = null, mode = 'lexical', cardVecs = null;
|
|
811
|
+
let semantic = null, mode = 'lexical', cardVecs = null, prep = null;
|
|
785
812
|
try {
|
|
786
813
|
const pipe = await getEmbedderForUse(log, 20_000);
|
|
787
814
|
if (pipe) {
|
|
788
815
|
const [qv] = await embedTexts(pipe, [words ? `${q}\n${words}` : q], { kind: 'query' });
|
|
789
|
-
const vecs = await
|
|
790
|
-
|
|
816
|
+
const { vecs, stats, usable } = await questionVectors(pipe, t.file, struct.cards);
|
|
817
|
+
prep = stats;
|
|
818
|
+
if (qv && usable) { semantic = new Map(); for (const [id, v] of vecs) semantic.set(id, dot(qv, v)); cardVecs = vecs; mode = `semantic+lexical (on-device)${words ? ' + your Also words' : ''}`; }
|
|
791
819
|
}
|
|
792
820
|
} catch { semantic = null; cardVecs = null; }
|
|
793
821
|
// Parity with search_all_brains (8445e9c): never say "warming — retry" to a
|
|
@@ -801,6 +829,7 @@ export async function opBrainAsk({ vault, canvas, question, as_of, k = 10, log =
|
|
|
801
829
|
if (!semantic) {
|
|
802
830
|
fallbackNotice = semanticFallbackNotice(false);
|
|
803
831
|
if (fallbackNotice) mode = fallbackNotice.startsWith('LEXICAL ONLY') ? 'lexical-only (no semantic runtime)' : 'lexical (semantic warming)';
|
|
832
|
+
if (prep?.pending > 0) mode = 'lexical (preparing search by meaning)';
|
|
804
833
|
if (words) mode += ' + your Also words';
|
|
805
834
|
}
|
|
806
835
|
const kk = Math.max(1, Math.min(20, k || 10));
|
|
@@ -849,7 +878,11 @@ export async function opBrainAsk({ vault, canvas, question, as_of, k = 10, log =
|
|
|
849
878
|
mode += ' + status-mode';
|
|
850
879
|
} catch { statusMd = ''; }
|
|
851
880
|
}
|
|
852
|
-
|
|
881
|
+
// While the brain is still being prepared, that is the one thing to say: the
|
|
882
|
+
// runtime IS installed, so the "warming — retry" fallback line would only
|
|
883
|
+
// repeat it less precisely.
|
|
884
|
+
const prepNotice = preparationNotice(t.file, prep, { usedMeaning: Boolean(semantic) });
|
|
885
|
+
const noticeMd = prepNotice ? `> ${prepNotice}\n\n` : (fallbackNotice ? `> ${fallbackNotice}\n\n` : '');
|
|
853
886
|
const evidenceCache = new Map();
|
|
854
887
|
const evidenceForCard = (card) => formatCardEvidence(inspectCardEvidence(card, { projectRoot: path.dirname(t.file), cache: evidenceCache }));
|
|
855
888
|
return { blocks: [text(stamp + noticeMd + statusMd + questionContextToMarkdown(q, result, { mode, as_of: timeTravel ? as_of : null, evidenceForCard }))] };
|
|
@@ -874,8 +907,8 @@ export async function opBrainChallenge({ vault, canvas, claim, k = 8, via, log =
|
|
|
874
907
|
const pipe = await getEmbedderForUse(log, 20_000);
|
|
875
908
|
if (pipe) {
|
|
876
909
|
const [qv] = await embedTexts(pipe, [q], { kind: 'query' });
|
|
877
|
-
const vecs = await
|
|
878
|
-
if (qv &&
|
|
910
|
+
const { vecs, usable } = await questionVectors(pipe, t.file, struct.cards);
|
|
911
|
+
if (qv && usable) { semantic = new Map(); for (const [id, v] of vecs) semantic.set(id, dot(qv, v)); mode = 'semantic+lexical (on-device)'; }
|
|
879
912
|
}
|
|
880
913
|
} catch { semantic = null; }
|
|
881
914
|
const result = challengeBrain(struct, q, { semantic, k: Math.max(1, Math.min(20, k || 8)) });
|
|
@@ -1163,7 +1196,11 @@ export async function opBrainReconcile({ vault, canvas, root, mode = 'all', ref
|
|
|
1163
1196
|
if (mode === 'plans' && cold && installed) {
|
|
1164
1197
|
try {
|
|
1165
1198
|
const pipe = await getEmbedderForUse(log, 20_000);
|
|
1166
|
-
if (pipe)
|
|
1199
|
+
if (pipe) {
|
|
1200
|
+
const stats = {};
|
|
1201
|
+
vecs = await vectorsForBrain(pipe, file, struct.cards, { stats });
|
|
1202
|
+
if (stats.pending > 0) startBrainPreparation(file);
|
|
1203
|
+
}
|
|
1167
1204
|
} catch (e) { embedErr = e; }
|
|
1168
1205
|
}
|
|
1169
1206
|
let pairSim = null, how;
|
|
@@ -1699,7 +1736,9 @@ export async function opBrainConnect({ vault, canvas, apply = false, max = 24, t
|
|
|
1699
1736
|
try { pipe = await getEmbedderForUse(log, 20_000); } catch { /* saturated/unavailable → structural */ }
|
|
1700
1737
|
if (pipe) {
|
|
1701
1738
|
try {
|
|
1702
|
-
const
|
|
1739
|
+
const stats = {};
|
|
1740
|
+
const vecs = await vectorsForBrain(pipe, file, struct.cards, { stats });
|
|
1741
|
+
if (stats.pending > 0) startBrainPreparation(file);
|
|
1703
1742
|
const items = live.filter(c => vecs.get(c.id));
|
|
1704
1743
|
const sources = normalizedScope === 'orphans' ? items.filter(c => orphanIds.has(c.id)) : items;
|
|
1705
1744
|
for (const a of sources) {
|