klypix-mcp 1.51.1 → 1.53.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 +30 -12
- package/bin/klypix-a2a.mjs +7 -4
- package/bin/klypix-install.mjs +26 -2
- package/bin/klypix-mcp.mjs +6 -2
- package/bin/klypix-runtime.mjs +33 -0
- package/bin/klypix-semantic-warm.mjs +93 -0
- package/bin/klypix-worker.mjs +67 -3
- package/package.json +12 -6
- package/src/agent-presence.mjs +9 -0
- package/src/brain-doctor.mjs +26 -6
- package/src/brain-semantic.mjs +33 -14
- package/src/global-brain-hook.mjs +7 -0
- package/src/klypix-core.mjs +54 -26
- package/src/klypix-format.mjs +73 -55
- package/src/mcp-supervisor.mjs +27 -0
- package/src/project-graph.mjs +340 -0
- package/src/runtime-inspector.mjs +296 -0
- package/src/semantic-memory.mjs +240 -34
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ npx klypix-mcp conformance
|
|
|
79
79
|
|
|
80
80
|
It runs in a temporary fixture and touches nothing else. It checks tool discovery, task memory,
|
|
81
81
|
truthful peer reporting, overlap surfacing, proactive logging, and in-band delivery of a peer note.
|
|
82
|
-
It verifies 12 required coordination behaviours — not the
|
|
82
|
+
It verifies 12 required coordination behaviours — not the 19 tools, and not the retrieval engine.
|
|
83
83
|
|
|
84
84
|
---
|
|
85
85
|
|
|
@@ -162,18 +162,21 @@ not a filing convention.
|
|
|
162
162
|
- **You can ask what the project believed then.** `brain_ask` with `as_of: 2026-03-01` reweights
|
|
163
163
|
ranking by card lifecycle dates, so corrections made later do not leak backwards.
|
|
164
164
|
- **Retrieval is local.** Lexical by default. If the optional on-device model is installed,
|
|
165
|
-
`brain_ask` and `search_all_brains`
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
165
|
+
`brain_ask` and `search_all_brains` use BGE semantic ranking with lexical help for exact
|
|
166
|
+
identifiers, paths, and versions — still entirely on your machine. The previous cross-encoder
|
|
167
|
+
is available for experiments with `KLYPIX_RERANK=1`, but is off by default because it reduced
|
|
168
|
+
precision and added latency on the frozen human-paraphrase evaluation. Without the embedding
|
|
169
|
+
model, retrieval degrades cleanly to lexical. `npx klypix-mcp install` deliberately does not
|
|
170
|
+
install that model, so a fresh install is lexical.
|
|
169
171
|
|
|
170
172
|
### Bounded semantic-memory runtime
|
|
171
173
|
|
|
172
174
|
Long-lived MCP and A2A workers use the bounded semantic-memory runtime by default. Models load only
|
|
173
175
|
when semantic work is requested, native inference is serialized per process, embedding work is
|
|
174
|
-
split into small batches,
|
|
175
|
-
|
|
176
|
-
|
|
176
|
+
split into small batches, and temporary tensors are released after use. Loaded models retire after
|
|
177
|
+
an idle interval and transparently reload on the next semantic request, so warm queries stay fast
|
|
178
|
+
without permanently pinning native model memory. These controls change the resource lifecycle only;
|
|
179
|
+
brain cards, project coordination, and the on-disk brain format are unchanged.
|
|
177
180
|
|
|
178
181
|
The previous runtime remains available as an emergency rollback. Set
|
|
179
182
|
`KLYPIX_SEMANTIC_MEMORY_MODE=legacy` in the MCP server environment and reconnect or restart the
|
|
@@ -184,6 +187,12 @@ Run the deterministic lifecycle tests with `npm run test:memory`. For an opt-in
|
|
|
184
187
|
against a disposable or backed-up brain, set `KLYPIX_MEMORY_SOAK_BRAIN` to its path and run
|
|
185
188
|
`npm run test:memory:soak`.
|
|
186
189
|
|
|
190
|
+
For process-level attribution, run `npx klypix-mcp runtime` (or add `--json`; `--watch 30` samples
|
|
191
|
+
every 30 seconds). It reports KLYPIX workers, supervisors, and legacy launcher overhead separately,
|
|
192
|
+
excludes the owning IDE/chat application's RAM, redacts command-line secrets, and never opens a
|
|
193
|
+
brain or terminates a process. Multiple processes under one host are reported as parallel sessions,
|
|
194
|
+
not called duplicates without an authoritative logical-session receipt.
|
|
195
|
+
|
|
187
196
|
---
|
|
188
197
|
|
|
189
198
|
## Supported hosts and their integration level
|
|
@@ -365,6 +374,7 @@ The MCP verbs below are what agents call. These are what **you** call:
|
|
|
365
374
|
| `npx klypix-mcp install` | Install the engine + Claude Code hooks on this machine (see Quick start) |
|
|
366
375
|
| `npx klypix-mcp link` | Wire this project for Cursor, Cline, Windsurf, Copilot, Gemini CLI, Aider (`--check` audits) |
|
|
367
376
|
| `npx klypix-mcp doctor` | One verdict: version, hosts, live sessions, tool count, drift. Exits non-zero — usable as a CI gate |
|
|
377
|
+
| `npx klypix-mcp runtime` | Passive per-connection process/RAM attribution (`--json`, optional `--watch seconds`); never kills or deduplicates |
|
|
368
378
|
| `npx klypix-mcp conformance` | Launch two real MCP clients against this build and verify coordination behaviour |
|
|
369
379
|
| `npx klypix-mcp git-driver` | Register the lossless `.klypix` merge driver for a repo (`status` to check) |
|
|
370
380
|
| `npx klypix-mcp diff [ref]` | Card-level brain diff against a git ref, as markdown |
|
|
@@ -373,7 +383,7 @@ The MCP verbs below are what agents call. These are what **you** call:
|
|
|
373
383
|
|
|
374
384
|
---
|
|
375
385
|
|
|
376
|
-
## The
|
|
386
|
+
## The 19 verbs
|
|
377
387
|
|
|
378
388
|
| Tool | What it does |
|
|
379
389
|
|---|---|
|
|
@@ -388,6 +398,7 @@ The MCP verbs below are what agents call. These are what **you** call:
|
|
|
388
398
|
| `brain_message` | Session-to-session coordination notes (24h TTL, never written into the brain) |
|
|
389
399
|
| `brain_sync` | Context Gateway: task capsule, active-task peers, exact-file overlap, one-time alerts, timing |
|
|
390
400
|
| `brain_connect` | Find and draw related-but-unlinked cards |
|
|
401
|
+
| `project_map_context` | Read-only, bounded code-graph evidence beside correction-aware brain context; Graphify artifacts are supported but never installed or run |
|
|
391
402
|
| `canvas_view` | Returns the board as a structured render spec plus a text summary, and declares an MCP Apps (SEP-1865) UI resource |
|
|
392
403
|
| `read_canvas` | A canvas as markdown (cards, connection graph, `[[links]]`, `#tags`) |
|
|
393
404
|
| `search_canvases` | Search across canvases by name and content |
|
|
@@ -396,7 +407,7 @@ The MCP verbs below are what agents call. These are what **you** call:
|
|
|
396
407
|
| `add_to_canvas` | Append cards/connections (positions preserved) |
|
|
397
408
|
| `list_canvases` | List every `.klypix` in the vault |
|
|
398
409
|
|
|
399
|
-
Exactly
|
|
410
|
+
Exactly 19, machine-verifiable with `npx klypix-mcp doctor`.
|
|
400
411
|
|
|
401
412
|
> **`canvas_view`:** no MCP Apps host has been observed rendering the UI resource yet — there is no
|
|
402
413
|
> screenshot and no host-level test. Hosts without the extension get clean text, which is the path
|
|
@@ -488,6 +499,12 @@ settings and project files. The check is detached and fail-open, developer-owned
|
|
|
488
499
|
protected, concurrent sessions collapse behind one lock, and `KLYPIX_AUTO_UPDATE=0` opts out
|
|
489
500
|
entirely.
|
|
490
501
|
|
|
502
|
+
When the optional semantic runtime is already enabled, an update also schedules one detached,
|
|
503
|
+
single-writer cache migration across registered brains. That removes the multi-minute first-query
|
|
504
|
+
re-index after a model/cache upgrade; cache writes are model-keyed and atomic across concurrent
|
|
505
|
+
agent sessions. Lexical-only installs download nothing. Set `KLYPIX_SEMANTIC_WARM_ON_UPDATE=0` to
|
|
506
|
+
keep lazy first-use indexing instead.
|
|
507
|
+
|
|
491
508
|
## Security and permissions
|
|
492
509
|
|
|
493
510
|
- **Apache-2.0, source public** at [github.com/dahshanlabs/klypix-mcp](https://github.com/dahshanlabs/klypix-mcp).
|
|
@@ -495,7 +512,8 @@ entirely.
|
|
|
495
512
|
deterministic and local; the only LLM anywhere is *your* agent. The one exception in this package
|
|
496
513
|
is the supervisor's once-per-24h npm version check described above — turn it off with
|
|
497
514
|
`KLYPIX_AUTO_UPDATE=0`.
|
|
498
|
-
- **The optional semantic model runs on device.**
|
|
515
|
+
- **The optional semantic model runs on device.** Enabling it (or upgrading its model) can fetch
|
|
516
|
+
model weights from Hugging Face; retrieval inference and brain data stay local.
|
|
499
517
|
- **Coordination state is local files.** The brain is a file in your repo; the presence lane is a
|
|
500
518
|
file under your home directory. Nothing is uploaded.
|
|
501
519
|
- **`install` writes to your home directory:** `~/.claude/project-brain` (engine + runtime),
|
|
@@ -581,7 +599,7 @@ Your `brain.klypix` is yours — it is a plain ZIP and stays readable with or wi
|
|
|
581
599
|
Issues and pull requests: [github.com/dahshanlabs/klypix-mcp](https://github.com/dahshanlabs/klypix-mcp).
|
|
582
600
|
Questions or feedback: [hello@klypix.com](mailto:hello@klypix.com).
|
|
583
601
|
|
|
584
|
-
The repository carries
|
|
602
|
+
The repository carries 49 test files, 45 of them in the `npm test` chain, covering the presence
|
|
585
603
|
lane and its cross-machine relay, the Context Gateway, supervisor hot-swap, auto-update, retrieval
|
|
586
604
|
quality, decay, challenge, lenses, the format guard, the git tools (including a real `git merge`
|
|
587
605
|
through the merge driver), uninstall, and conformance. Run them with `npm test` from a clone — they
|
package/bin/klypix-a2a.mjs
CHANGED
|
@@ -146,8 +146,8 @@ function agentCard(publicUrl) {
|
|
|
146
146
|
}] : []),
|
|
147
147
|
{
|
|
148
148
|
id: 'brain_connect',
|
|
149
|
-
name: '
|
|
150
|
-
description: '
|
|
149
|
+
name: 'Repair orphaned brain cards (connect related cards)',
|
|
150
|
+
description: 'Propose related links for orphaned decision/milestone cards by default, with a before→projected receipt; apply draws only additive, removable connections and never archives or rewrites cards. Pass scope:"all" for deliberate whole-graph densification.',
|
|
151
151
|
tags: ['brain', 'graph', 'connect', 'memory'],
|
|
152
152
|
examples: ['Connect the related cards in my project brain'],
|
|
153
153
|
inputModes: ['text/plain', 'application/json'],
|
|
@@ -256,8 +256,11 @@ async function runSkill(skill, args, text, via) {
|
|
|
256
256
|
const target = confinedCanvas(args.canvas);
|
|
257
257
|
if (!target.ok) return refusedCanvas(args.canvas);
|
|
258
258
|
const max = Math.max(1, Math.min(200, Math.floor(finiteNumber(args.max, 24))));
|
|
259
|
-
const threshold =
|
|
260
|
-
|
|
259
|
+
const threshold = args.threshold == null
|
|
260
|
+
? null
|
|
261
|
+
: Math.max(0.3, Math.min(1, finiteNumber(args.threshold, 0.55)));
|
|
262
|
+
const scope = args.scope === 'all' ? 'all' : 'orphans';
|
|
263
|
+
return await opBrainConnect({ vault: VAULT, canvas: target.canvas, apply: args.apply === true, max, threshold, scope, log });
|
|
261
264
|
}
|
|
262
265
|
case 'make_board':
|
|
263
266
|
{
|
package/bin/klypix-install.mjs
CHANGED
|
@@ -17,6 +17,7 @@ import fs from 'fs';
|
|
|
17
17
|
import os from 'os';
|
|
18
18
|
import path from 'path';
|
|
19
19
|
import crypto from 'crypto';
|
|
20
|
+
import { spawn } from 'child_process';
|
|
20
21
|
import { fileURLToPath } from 'url';
|
|
21
22
|
import {
|
|
22
23
|
connectCodexMcpServer,
|
|
@@ -225,7 +226,7 @@ const flatten = (code) => code
|
|
|
225
226
|
.replace(/\.\.\/src\/klypix-(core|format)\.mjs/g, './klypix-$1.mjs')
|
|
226
227
|
// brain-doctor + agent-rules (the server's lazy `import('../src/brain-doctor.mjs')`
|
|
227
228
|
// for the brain_doctor tool) → flat sibling refs in the runtime layout.
|
|
228
|
-
.replace(/\.\.\/src\/(brain-doctor|agent-rules|mcp-presence|mcp-supervisor|mcp-auto-update)\.mjs/g, './$1.mjs')
|
|
229
|
+
.replace(/\.\.\/src\/(brain-doctor|agent-rules|mcp-presence|mcp-supervisor|mcp-auto-update|semantic-memory|runtime-inspector|project-graph)\.mjs/g, './$1.mjs')
|
|
229
230
|
.replace(/klypix-worker\.mjs/g, 'klypix-mcp-worker.mjs')
|
|
230
231
|
.replace(/const PKG_VERSION = \(\(\) => \{[\s\S]*?\}\)\(\);/, `const PKG_VERSION = '${VERSION}'; // baked at install (flat layout has no package.json)`);
|
|
231
232
|
|
|
@@ -292,7 +293,7 @@ try {
|
|
|
292
293
|
// canvas-view-app.html is the canvas_view MCP App UI — staged raw (an HTML
|
|
293
294
|
// file must never get a JS-comment banner) beside the flat server, which
|
|
294
295
|
// resolves it via its ./canvas-view-app.html candidate path.
|
|
295
|
-
for (const f of ['global-brain-hook.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'brain-note.mjs', 'brain-git-hook.mjs', 'klypix-format.mjs', 'klypix-core.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'finding-routing.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
|
|
296
|
+
for (const f of ['global-brain-hook.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'brain-note.mjs', 'brain-git-hook.mjs', 'klypix-format.mjs', 'klypix-core.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'finding-routing.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'canvas-view-app.html']) {
|
|
296
297
|
const s = path.join(SRC, f); if (exists(s)) staged.push({ dst: f, content: fs.readFileSync(s, 'utf8') });
|
|
297
298
|
}
|
|
298
299
|
for (const [src, dst] of [
|
|
@@ -300,6 +301,8 @@ try {
|
|
|
300
301
|
['klypix-worker.mjs', 'klypix-mcp-worker.mjs'],
|
|
301
302
|
['klypix-a2a.mjs', 'klypix-a2a-server.mjs'],
|
|
302
303
|
['klypix-conformance.mjs', 'klypix-conformance.mjs'],
|
|
304
|
+
['klypix-runtime.mjs', 'klypix-runtime.mjs'],
|
|
305
|
+
['klypix-semantic-warm.mjs', 'klypix-semantic-warm.mjs'],
|
|
303
306
|
]) {
|
|
304
307
|
const s = path.join(BIN, src); if (exists(s)) staged.push({ dst, content: flatten(fs.readFileSync(s, 'utf8')) });
|
|
305
308
|
}
|
|
@@ -381,6 +384,27 @@ try {
|
|
|
381
384
|
const notWired = ['SessionStart', 'UserPromptSubmit', 'Stop', 'PostToolUse'].filter(e => !wiredFor(e));
|
|
382
385
|
|
|
383
386
|
if (gotLock) releaseLock();
|
|
387
|
+
// Users who already enabled the optional local semantic runtime should not
|
|
388
|
+
// pay a multi-minute first question after a model/cache contract upgrade.
|
|
389
|
+
// Migrate registered brains once in a detached process after the atomic
|
|
390
|
+
// runtime commit. Fresh lexical-only installs download nothing.
|
|
391
|
+
let semanticWarm = 'not-enabled';
|
|
392
|
+
const semanticRuntime = path.join(BRAIN_DIR, 'semantic', 'node_modules', '@huggingface', 'transformers');
|
|
393
|
+
if (process.env.KLYPIX_SEMANTIC_WARM_ON_UPDATE !== '0' && exists(semanticRuntime)) {
|
|
394
|
+
try {
|
|
395
|
+
const warmArgs = [path.join(BRAIN_DIR, 'klypix-semantic-warm.mjs'), '--brain-dir', BRAIN_DIR];
|
|
396
|
+
const currentBrain = ['brain.klypix', 'brain.any'].map(name => path.join(process.cwd(), name)).find(exists);
|
|
397
|
+
if (currentBrain) warmArgs.push('--brain', currentBrain);
|
|
398
|
+
const warm = spawn(process.execPath, warmArgs, {
|
|
399
|
+
detached: true,
|
|
400
|
+
stdio: 'ignore',
|
|
401
|
+
windowsHide: true,
|
|
402
|
+
env: { ...process.env, KLYPIX_SEMANTIC_WARM_ON_UPDATE: '1' },
|
|
403
|
+
});
|
|
404
|
+
warm.unref();
|
|
405
|
+
semanticWarm = 'scheduled';
|
|
406
|
+
} catch { semanticWarm = 'deferred'; }
|
|
407
|
+
}
|
|
384
408
|
if (!RUNTIME_ONLY) reportCodex(codex);
|
|
385
409
|
console.log(`✓ installed klypix brain v${VERSION} → ${BRAIN_DIR} (${n} scripts, ${deps} dep packages)`);
|
|
386
410
|
if (RUNTIME_ONLY) console.log('✓ runtime-only update: host settings and project files were preserved');
|
package/bin/klypix-mcp.mjs
CHANGED
|
@@ -19,7 +19,7 @@ const PKG_VERSION = (() => {
|
|
|
19
19
|
}
|
|
20
20
|
})();
|
|
21
21
|
|
|
22
|
-
const DIRECT = new Set(['install', 'link', 'doctor', 'conformance', 'garden-code', 'init', 'git-driver', 'diff', 'pr-brief', 'uninstall']);
|
|
22
|
+
const DIRECT = new Set(['install', 'link', 'doctor', 'runtime', 'conformance', 'garden-code', 'init', 'git-driver', 'diff', 'pr-brief', 'uninstall']);
|
|
23
23
|
|
|
24
24
|
const USAGE = [
|
|
25
25
|
`klypix-mcp ${PKG_VERSION} — shared project brain + MCP coordination server.`,
|
|
@@ -28,6 +28,7 @@ const USAGE = [
|
|
|
28
28
|
' install [--force] [--codex-hooks] install/update this machine\'s brain engine + Claude Code hooks',
|
|
29
29
|
' link [dir] [--check] project this project\'s 14 managed agent config files (--check audits, writes nothing, exits 1 on drift)',
|
|
30
30
|
' doctor [--npm] [--all] [--json] read-only self-check; exits 1 on drift',
|
|
31
|
+
' runtime [--json] [--watch seconds] passive MCP process/RAM attribution; never terminates a process',
|
|
31
32
|
' conformance [--json] launch two real MCP clients against this build',
|
|
32
33
|
' init seed a starter ./brain.klypix + print an MCP config',
|
|
33
34
|
' garden-code [brain] print the human approval code for brain_garden',
|
|
@@ -54,7 +55,10 @@ if (verb && !verb.startsWith('-') && !DIRECT.has(verb)) {
|
|
|
54
55
|
process.exit(2);
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
if (
|
|
58
|
+
if (verb === 'runtime') {
|
|
59
|
+
process.argv.splice(2, 1);
|
|
60
|
+
await import('./klypix-runtime.mjs');
|
|
61
|
+
} else if (DIRECT.has(verb)) {
|
|
58
62
|
await import('./klypix-worker.mjs');
|
|
59
63
|
} else {
|
|
60
64
|
const { runMcpSupervisor } = await import('../src/mcp-supervisor.mjs');
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { formatRuntimeReport, inspectKlypixRuntime } from '../src/runtime-inspector.mjs';
|
|
5
|
+
|
|
6
|
+
const args = process.argv.slice(2);
|
|
7
|
+
const valueAfter = (flag) => {
|
|
8
|
+
const index = args.indexOf(flag);
|
|
9
|
+
return index >= 0 ? args[index + 1] : null;
|
|
10
|
+
};
|
|
11
|
+
const json = args.includes('--json');
|
|
12
|
+
const brainDir = valueAfter('--brain-dir') || path.join(os.homedir(), '.claude', 'project-brain');
|
|
13
|
+
const watchSeconds = Math.max(0, Number(valueAfter('--watch') || 0));
|
|
14
|
+
|
|
15
|
+
const sample = () => {
|
|
16
|
+
try {
|
|
17
|
+
const report = inspectKlypixRuntime({ brainDir });
|
|
18
|
+
process.stdout.write((json ? JSON.stringify(report) : formatRuntimeReport(report)) + '\n');
|
|
19
|
+
} catch (error) {
|
|
20
|
+
const message = error?.message || String(error);
|
|
21
|
+
if (json) process.stdout.write(JSON.stringify({ schemaVersion: 1, passive: true, mutated: false, error: message }) + '\n');
|
|
22
|
+
else process.stderr.write(`KLYPIX runtime inspection failed: ${message}\n`);
|
|
23
|
+
process.exitCode = 1;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
sample();
|
|
28
|
+
if (watchSeconds > 0) {
|
|
29
|
+
const timer = setInterval(sample, Math.max(5, watchSeconds) * 1_000);
|
|
30
|
+
process.once('SIGINT', () => { clearInterval(timer); process.exit(); });
|
|
31
|
+
process.once('SIGTERM', () => { clearInterval(timer); process.exit(); });
|
|
32
|
+
}
|
|
33
|
+
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// One detached, best-effort cache migration after a compatible core update.
|
|
3
|
+
// It runs only when the optional semantic runtime already exists, processes
|
|
4
|
+
// registered brains sequentially, and never changes brain content.
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { parseKlypix } from '../src/klypix-format.mjs';
|
|
9
|
+
import { readRegisteredProjectBrains } from '../src/mcp-auto-update.mjs';
|
|
10
|
+
import {
|
|
11
|
+
EMBEDDING_CACHE_KEY,
|
|
12
|
+
disposeSemanticModels,
|
|
13
|
+
getEmbedderForUse,
|
|
14
|
+
vectorsForBrain,
|
|
15
|
+
} from '../src/semantic-memory.mjs';
|
|
16
|
+
|
|
17
|
+
const arg = (name, fallback = null) => {
|
|
18
|
+
const index = process.argv.indexOf(`--${name}`);
|
|
19
|
+
return index >= 0 && process.argv[index + 1] ? process.argv[index + 1] : fallback;
|
|
20
|
+
};
|
|
21
|
+
const brainDir = path.resolve(arg('brain-dir', path.join(os.homedir(), '.claude', 'project-brain')));
|
|
22
|
+
const lockFile = path.join(brainDir, '.semantic-update-warm.lock');
|
|
23
|
+
const statusFile = path.join(brainDir, '.semantic-update-warm.json');
|
|
24
|
+
const explicitBrain = arg('brain');
|
|
25
|
+
const startedAt = Date.now();
|
|
26
|
+
let lock = null;
|
|
27
|
+
|
|
28
|
+
const atomicJson = (file, value) => {
|
|
29
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
30
|
+
fs.writeFileSync(tmp, `${JSON.stringify(value, null, 2)}\n`);
|
|
31
|
+
fs.renameSync(tmp, file);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
fs.mkdirSync(brainDir, { recursive: true });
|
|
36
|
+
try {
|
|
37
|
+
lock = fs.openSync(lockFile, 'wx');
|
|
38
|
+
} catch (error) {
|
|
39
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
40
|
+
let stale = false;
|
|
41
|
+
try { stale = Date.now() - fs.statSync(lockFile).mtimeMs > 30 * 60_000; } catch { stale = true; }
|
|
42
|
+
if (!stale) process.exit(0);
|
|
43
|
+
try { fs.unlinkSync(lockFile); } catch { process.exit(0); }
|
|
44
|
+
lock = fs.openSync(lockFile, 'wx');
|
|
45
|
+
}
|
|
46
|
+
fs.writeFileSync(lock, JSON.stringify({ pid: process.pid, startedAt, modelKey: EMBEDDING_CACHE_KEY }));
|
|
47
|
+
|
|
48
|
+
const registered = readRegisteredProjectBrains(brainDir).map(item => item.brainPath);
|
|
49
|
+
const candidates = [...new Set([explicitBrain ? path.resolve(explicitBrain) : null, ...registered].filter(Boolean))]
|
|
50
|
+
.filter(file => {
|
|
51
|
+
try { return fs.statSync(file).isFile(); } catch { return false; }
|
|
52
|
+
});
|
|
53
|
+
if (!candidates.length) {
|
|
54
|
+
atomicJson(statusFile, { protocol: 1, result: 'no-registered-brains', modelKey: EMBEDDING_CACHE_KEY, completedAt: new Date().toISOString() });
|
|
55
|
+
} else {
|
|
56
|
+
const pipe = await getEmbedderForUse(() => {}, 5 * 60_000);
|
|
57
|
+
if (!pipe) {
|
|
58
|
+
atomicJson(statusFile, { protocol: 1, result: 'semantic-unavailable', modelKey: EMBEDDING_CACHE_KEY, completedAt: new Date().toISOString() });
|
|
59
|
+
} else {
|
|
60
|
+
let warmed = 0, failed = 0, cards = 0;
|
|
61
|
+
for (const file of candidates) {
|
|
62
|
+
try {
|
|
63
|
+
const { struct } = await parseKlypix(fs.readFileSync(file));
|
|
64
|
+
const vectors = await vectorsForBrain(pipe, file, struct.cards);
|
|
65
|
+
warmed++;
|
|
66
|
+
cards += vectors.size;
|
|
67
|
+
} catch { failed++; }
|
|
68
|
+
}
|
|
69
|
+
atomicJson(statusFile, {
|
|
70
|
+
protocol: 1,
|
|
71
|
+
result: failed ? (warmed ? 'partial' : 'failed') : 'ready',
|
|
72
|
+
modelKey: EMBEDDING_CACHE_KEY,
|
|
73
|
+
brains: candidates.length,
|
|
74
|
+
warmed,
|
|
75
|
+
failed,
|
|
76
|
+
cards,
|
|
77
|
+
durationMs: Date.now() - startedAt,
|
|
78
|
+
completedAt: new Date().toISOString(),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// Background migration is fail-open: a later brain_ask can still warm lazily.
|
|
84
|
+
} finally {
|
|
85
|
+
try { await disposeSemanticModels(); } catch { /* */ }
|
|
86
|
+
try { if (lock != null) fs.closeSync(lock); } catch { /* */ }
|
|
87
|
+
try { fs.unlinkSync(lockFile); } catch { /* */ }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// onnxruntime can retain background handles after model disposal on Windows.
|
|
91
|
+
// This is a detached one-shot migration, so terminate deterministically after
|
|
92
|
+
// every cache/status write and cleanup has completed.
|
|
93
|
+
process.exit(0);
|
package/bin/klypix-worker.mjs
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
opBrainInsights, opBrainConnect, opBrainReconcile, opBrainGarden, opCreateCanvas, opAddToCanvas, opBrainNote, opBrainMessage, opBrainAsk, opBrainChallenge, opCanvasView, opBrainLens,
|
|
31
31
|
opBrainTaskContext,
|
|
32
32
|
} from '../src/klypix-core.mjs';
|
|
33
|
+
import { projectGraphContextMarkdown, queryProjectGraph } from '../src/project-graph.mjs';
|
|
33
34
|
import { auditProject, compactAgentsBrief, linkProject, mcpServerEntry } from '../src/agent-rules.mjs';
|
|
34
35
|
import { createMcpPresence, KLYPIX_MCP_INSTRUCTIONS } from '../src/mcp-presence.mjs';
|
|
35
36
|
import {
|
|
@@ -201,6 +202,7 @@ const toContent = (r) => {
|
|
|
201
202
|
? { type: 'image', data: b.data, mimeType: b.mime }
|
|
202
203
|
: { type: 'text', text: b.text });
|
|
203
204
|
const result = r.isError ? { content, isError: true } : { content };
|
|
205
|
+
if (r.structured && typeof r.structured === 'object') result.structuredContent = r.structured;
|
|
204
206
|
return mcpPresence.decorateToolResult(result);
|
|
205
207
|
};
|
|
206
208
|
|
|
@@ -242,6 +244,67 @@ server.registerTool('brain_ask', {
|
|
|
242
244
|
},
|
|
243
245
|
}, async ({ question, canvas, as_of, k }) => toContent(await opBrainAsk({ vault: mcpPresence.vault, canvas, question, as_of, k, log })));
|
|
244
246
|
|
|
247
|
+
server.registerTool('project_map_context', {
|
|
248
|
+
title: 'Project Map context - current code structure plus brain decisions',
|
|
249
|
+
description: 'Read-only combined context for a coding question. It queries a provider-neutral, bounded view of the generated project graph (Graphify graphify-out/graph.json is supported first) and places that CURRENT CODE evidence beside fast, correction-aware KLYPIX brain cards (decisions and rationale). KLYPIX never installs or runs Graphify and never copies graph nodes into brain.klypix. Missing graph artifacts degrade cleanly to brain-only context. Source-file anchors are accepted only when they stay inside the declared project root. Set deep_history:true only when superseded history is genuinely needed; the default never loads the local embedding model.',
|
|
250
|
+
annotations: {
|
|
251
|
+
destructiveHint: false,
|
|
252
|
+
idempotentHint: true,
|
|
253
|
+
openWorldHint: false,
|
|
254
|
+
},
|
|
255
|
+
inputSchema: {
|
|
256
|
+
question: z.string().min(1).describe('Question or code concept to ground in both current structure and project memory.'),
|
|
257
|
+
project: z.string().optional().describe('Absolute project root. Defaults to this MCP connection\'s configured project/vault.'),
|
|
258
|
+
graph_path: z.string().optional().describe('Optional project-relative graph JSON path. Defaults to graphify-out/graph.json and may not escape the project root.'),
|
|
259
|
+
depth: z.number().optional().describe('Relationship hops around the best code matches (0-3, default 1).'),
|
|
260
|
+
max_nodes: z.number().optional().describe('Maximum code nodes returned (default 60, capped 200).'),
|
|
261
|
+
k: z.number().optional().describe('Maximum brain cards returned (default 8; fast mode caps 8, deep history caps 20).'),
|
|
262
|
+
deep_history: z.boolean().optional().describe('false (default) uses the sub-second lexical-fast correction-aware path; true opts into whole-brain semantic/history retrieval, which may cold-load the local model.'),
|
|
263
|
+
},
|
|
264
|
+
}, async ({ question, project, graph_path, depth, max_nodes, k, deep_history }) => {
|
|
265
|
+
let graphResult;
|
|
266
|
+
let graphMarkdown;
|
|
267
|
+
try {
|
|
268
|
+
graphResult = queryProjectGraph({
|
|
269
|
+
project: project || mcpPresence.vault,
|
|
270
|
+
graphPath: graph_path,
|
|
271
|
+
query: question,
|
|
272
|
+
depth,
|
|
273
|
+
maxNodes: max_nodes,
|
|
274
|
+
});
|
|
275
|
+
graphMarkdown = projectGraphContextMarkdown(graphResult);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
graphResult = { schemaVersion: 1, status: 'invalid', error: error?.message || String(error) };
|
|
278
|
+
graphMarkdown = `# Project Map\n\nThe generated graph could not be read safely: ${graphResult.error}`;
|
|
279
|
+
}
|
|
280
|
+
const graphFiles = Array.isArray(graphResult?.nodes)
|
|
281
|
+
? [...new Set(graphResult.nodes.map(node => node.sourceFile).filter(Boolean))].slice(0, 20)
|
|
282
|
+
: [];
|
|
283
|
+
const brainResult = deep_history
|
|
284
|
+
? await opBrainAsk({
|
|
285
|
+
vault: mcpPresence.vault,
|
|
286
|
+
question,
|
|
287
|
+
k: Math.max(1, Math.min(20, Number(k) || 8)),
|
|
288
|
+
log,
|
|
289
|
+
})
|
|
290
|
+
: await opBrainTaskContext({
|
|
291
|
+
vault: mcpPresence.vault,
|
|
292
|
+
intent: question,
|
|
293
|
+
files: graphFiles,
|
|
294
|
+
k: Math.max(1, Math.min(8, Number(k) || 8)),
|
|
295
|
+
budgetChars: 4_500,
|
|
296
|
+
});
|
|
297
|
+
const brainMarkdown = brainResult.blocks
|
|
298
|
+
.filter(block => block.kind === 'text')
|
|
299
|
+
.map(block => block.text)
|
|
300
|
+
.join('\n\n');
|
|
301
|
+
return toContent({
|
|
302
|
+
blocks: [{ kind: 'text', text: `${graphMarkdown}\n\n---\n\n# Project Brain - ${deep_history ? 'decisions and history' : 'current decisions and corrections'}\n\n${brainMarkdown}` }],
|
|
303
|
+
isError: brainResult.isError,
|
|
304
|
+
structured: { projectGraph: graphResult, brainContext: brainResult.context || null, deepHistory: deep_history === true },
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
|
|
245
308
|
server.registerTool('brain_challenge', {
|
|
246
309
|
title: 'Challenge a decision against the brain (argue back with receipts)',
|
|
247
310
|
description: 'BEFORE committing to a significant decision, ask the brain to ARGUE BACK: prior decisions that deterministically contradict the claim (correction-cue / opposite-polarity evidence — never mere topical similarity), 🛠 standing rules that dispute it, approaches tried before and REVERSED (with the correction/successor as the receipt), and open questions it collides with. Candidates, not verdicts — silence means "no deterministic contradiction signal", not verified consistency. Cards captured by a DIFFERENT agent are flagged so you coordinate instead of overriding. Dismiss a confirmed-false pair (after capturing the claim) with brain_connect pairs + relationship:"not_contradiction". Defaults to the project brain ("brain").',
|
|
@@ -278,16 +341,17 @@ server.registerTool('brain_lens', {
|
|
|
278
341
|
|
|
279
342
|
server.registerTool('brain_connect', {
|
|
280
343
|
title: 'Connect related-but-unlinked brain cards (densify the graph)',
|
|
281
|
-
description: '
|
|
344
|
+
description: 'Repairs orphaned decision/milestone cards first (scope:"orphans", the default) by proposing genuinely related unlinked pairs — semantic similarity at a conservative 0.55 threshold when the on-device model is installed, else shared tags + [[mentions]]. The dry run includes a before→projected orphan receipt; apply:true draws only additive, removable arrows and reports the measured after count. It NEVER archives or rewrites cards. Use scope:"all" for deliberate whole-graph densification. To DISMISS a brain_reconcile false-positive contradiction, pass pairs:[{fromId,toId}] with relationship:"not_contradiction".',
|
|
282
345
|
inputSchema: {
|
|
283
346
|
canvas: z.string().optional().describe('Canvas filename/path. Defaults to the project brain ("brain").'),
|
|
284
347
|
apply: z.boolean().optional().describe('false (default) = suggest only; true = draw the connections.'),
|
|
285
348
|
max: z.number().optional().describe('Max connections to propose/draw (default 24).'),
|
|
286
|
-
threshold: z.number().optional().describe('Min semantic similarity 0–1
|
|
349
|
+
threshold: z.number().optional().describe('Min semantic similarity 0–1. Default 0.55 for orphan repair; 0.45 for scope:"all". Higher = fewer, tighter links.'),
|
|
350
|
+
scope: z.enum(['orphans', 'all']).optional().describe('"orphans" (default) repairs isolated decision/milestone cards; "all" proposes across every live card.'),
|
|
287
351
|
pairs: z.array(z.object({ fromId: z.string(), toId: z.string() })).optional().describe('Explicit card-id pairs to connect (bypasses auto-proposal). Use to dismiss a reconcile false-positive: pass the two card ids with relationship:"not_contradiction".'),
|
|
288
352
|
relationship: z.string().optional().describe('Relationship for explicit `pairs` (e.g. "not_contradiction" to permanently dismiss a contradiction candidate, or "relates_to", "depends_on", "supports").'),
|
|
289
353
|
},
|
|
290
|
-
}, async ({ canvas, apply, max, threshold, pairs, relationship }) => toContent(await opBrainConnect({ vault: mcpPresence.vault, canvas, apply, max, threshold, pairs, relationship, log })));
|
|
354
|
+
}, async ({ canvas, apply, max, threshold, scope, pairs, relationship }) => toContent(await opBrainConnect({ vault: mcpPresence.vault, canvas, apply, max, threshold, scope, pairs, relationship, log })));
|
|
291
355
|
|
|
292
356
|
server.registerTool('brain_reconcile', {
|
|
293
357
|
title: 'Reconcile the brain — contradictions between cards + unrecorded migrations',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.53.0",
|
|
4
4
|
"description": "Shared project brain and MCP coordination server for multi-agent coding.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
"klypix-conformance": "bin/klypix-conformance.mjs",
|
|
38
38
|
"klypix-link": "bin/klypix-link.mjs",
|
|
39
39
|
"klypix-doctor": "bin/klypix-doctor.mjs",
|
|
40
|
+
"klypix-runtime": "bin/klypix-runtime.mjs",
|
|
40
41
|
"klypix-a2a": "bin/klypix-a2a.mjs",
|
|
41
42
|
"klypix-read": "bin/klypix-read.mjs",
|
|
42
43
|
"klypix-write": "bin/klypix-write.mjs",
|
|
@@ -53,6 +54,9 @@
|
|
|
53
54
|
"./mcp-presence": "./src/mcp-presence.mjs",
|
|
54
55
|
"./presence-relay": "./src/presence-relay.mjs",
|
|
55
56
|
"./supervisor": "./src/mcp-supervisor.mjs",
|
|
57
|
+
"./runtime-inspector": "./src/runtime-inspector.mjs",
|
|
58
|
+
"./runtime": "./src/runtime-inspector.mjs",
|
|
59
|
+
"./project-graph": "./src/project-graph.mjs",
|
|
56
60
|
"./auto-update": "./src/mcp-auto-update.mjs"
|
|
57
61
|
},
|
|
58
62
|
"files": [
|
|
@@ -70,18 +74,20 @@
|
|
|
70
74
|
"node": ">=18"
|
|
71
75
|
},
|
|
72
76
|
"scripts": {
|
|
73
|
-
"test": "node test/
|
|
77
|
+
"test:project-graph": "node test/project-graph.mjs",
|
|
78
|
+
"test": "node test/project-graph.mjs && node test/mcp-auto-update.mjs && node test/mcp-supervisor.mjs && node test/runtime-inspector.mjs && node test/codex-hooks.mjs && node test/agent-presence.mjs && node test/finding-routing.mjs && node test/finding-routing-hook.mjs && node test/presence-relay.mjs && node test/context-gateway.mjs && node test/conformance.mjs && node test/brain-doctor.mjs && node test/version-currency.mjs && node test/ship-capture.mjs && node test/lane-message.mjs && node test/brain-quality.mjs && node test/brain-connect-orphans.mjs && node test/brief-and-recall.mjs && node test/layout-cluster.mjs && node test/brain-ask.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/skill-staleness.mjs && node test/canvas-view.mjs && node test/status-completeness.mjs && node test/semantic-gate.mjs && node test/memory-runtime.mjs && node test/semantic-cache.mjs && node test/decay-status.mjs && node test/decay-hook.mjs && node test/evidence-anchors.mjs && node test/presence-visibility.mjs && node test/merge-brains.mjs && node test/concurrent-writes.mjs && node test/a2a-smoke.mjs && node test/cli-args.mjs && node test/format-guard.mjs && node test/git-tools.mjs && node test/uninstall.mjs",
|
|
74
79
|
"test:memory": "node test/memory-runtime.mjs",
|
|
75
|
-
"test:memory:soak": "node --expose-gc test/memory-soak.mjs"
|
|
80
|
+
"test:memory:soak": "node --expose-gc test/memory-soak.mjs",
|
|
81
|
+
"runtime": "node bin/klypix-runtime.mjs"
|
|
76
82
|
},
|
|
77
83
|
"dependencies": {
|
|
78
|
-
"@modelcontextprotocol/ext-apps": "^1.7.
|
|
79
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
84
|
+
"@modelcontextprotocol/ext-apps": "^1.7.5",
|
|
85
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
80
86
|
"fractional-indexing": "^3.2.0",
|
|
81
87
|
"jszip": "^3.10.1",
|
|
82
88
|
"zod": "^4.3.6"
|
|
83
89
|
},
|
|
84
|
-
"
|
|
90
|
+
"devDependencies": {
|
|
85
91
|
"@huggingface/transformers": "^4.2.0"
|
|
86
92
|
}
|
|
87
93
|
}
|
package/src/agent-presence.mjs
CHANGED
|
@@ -225,6 +225,12 @@ export function upsertSession({
|
|
|
225
225
|
? String(intent || '').replace(/\s+/g, ' ').trim().slice(0, 160)
|
|
226
226
|
: (previous.intent || '');
|
|
227
227
|
const intentChanged = intent !== undefined && nextIntent !== (previous.intent || '');
|
|
228
|
+
// A connection heartbeat proves transport liveness, not that a task is in
|
|
229
|
+
// progress. Stamp only events that carry real user/tool work so doctor can
|
|
230
|
+
// distinguish an idle connected host from an active sync-silent session.
|
|
231
|
+
const activityEvent = /^(?:McpToolUse|McpTaskStart|McpTaskCheckpoint|UserPromptSubmit|PreToolUse|PostToolUse)$/i.test(String(event || ''))
|
|
232
|
+
? String(event)
|
|
233
|
+
: null;
|
|
228
234
|
const next = {
|
|
229
235
|
...previous,
|
|
230
236
|
id: String(id),
|
|
@@ -240,6 +246,9 @@ export function upsertSession({
|
|
|
240
246
|
: (previous.intentAt ? { intentAt: previous.intentAt, intentSource: previous.intentSource || null } : {})),
|
|
241
247
|
files: mergedFiles,
|
|
242
248
|
event: event ?? previous.event ?? null,
|
|
249
|
+
...(activityEvent
|
|
250
|
+
? { activityAt: now, activityKind: activityEvent }
|
|
251
|
+
: (previous.activityAt ? { activityAt: previous.activityAt, activityKind: previous.activityKind || null } : {})),
|
|
243
252
|
...(Object.keys(channelSeen).length
|
|
244
253
|
? { channels: Object.keys(channelSeen), channelSeen }
|
|
245
254
|
: {}),
|
package/src/brain-doctor.mjs
CHANGED
|
@@ -263,6 +263,13 @@ function inspectPeers(brainDir, brainPath, now, selfId = null) {
|
|
|
263
263
|
hostPid: s.hostPid || null,
|
|
264
264
|
// Sync-rich vs sync-silent: has this session declared any task scope?
|
|
265
265
|
synced: Boolean(String(s.intent || '').trim() || (Array.isArray(s.files) && s.files.length)),
|
|
266
|
+
// Transport liveness is not task activity. Only real prompt/tool work
|
|
267
|
+
// stamps activityAt; heartbeats deliberately preserve rather than refresh it.
|
|
268
|
+
activityAt: Number(s.activityAt || 0) || null,
|
|
269
|
+
activityKind: s.activityKind || null,
|
|
270
|
+
activeUnscoped: !Boolean(String(s.intent || '').trim() || (Array.isArray(s.files) && s.files.length))
|
|
271
|
+
&& Number(s.activityAt || 0) > 0
|
|
272
|
+
&& now - Number(s.activityAt) < SESSION_FRESH_MS,
|
|
266
273
|
lastSeenMin: Math.round((now - liveness.lastSeen) / 60000),
|
|
267
274
|
}));
|
|
268
275
|
// Twin rows = channel merge NOT occurring: >1 live row sharing one hostPid is
|
|
@@ -278,7 +285,13 @@ function inspectPeers(brainDir, brainPath, now, selfId = null) {
|
|
|
278
285
|
now,
|
|
279
286
|
})
|
|
280
287
|
: { sent: 0, receipts: [] };
|
|
281
|
-
|
|
288
|
+
const syncedCount = live.filter(p => p.synced).length;
|
|
289
|
+
const activeUnscopedCount = live.filter(p => p.activeUnscoped).length;
|
|
290
|
+
return {
|
|
291
|
+
file, live, count: live.length, rawRecent, syncedCount, activeUnscopedCount,
|
|
292
|
+
idleUnscopedCount: Math.max(0, live.length - syncedCount - activeUnscopedCount),
|
|
293
|
+
twinGroups, receipts,
|
|
294
|
+
};
|
|
282
295
|
}
|
|
283
296
|
|
|
284
297
|
// ── DECAY-GUARD layer (fast-decay status protection, 2026-07-28) ─────────────
|
|
@@ -408,9 +421,9 @@ export function inspect(opts = {}) {
|
|
|
408
421
|
// coordination. That is a readiness gap, not file/runtime drift: say PARTIAL
|
|
409
422
|
// instead of the misleading all-clear that prompted the field audit.
|
|
410
423
|
const readinessWarnings = [];
|
|
411
|
-
const
|
|
412
|
-
if (peers.count > 1 &&
|
|
413
|
-
readinessWarnings.push(`${
|
|
424
|
+
const activeSilentSessions = peers.activeUnscopedCount || 0;
|
|
425
|
+
if (peers.count > 1 && activeSilentSessions > 0) {
|
|
426
|
+
readinessWarnings.push(`${activeSilentSessions} active session${activeSilentSessions === 1 ? '' : 's'} used KLYPIX without declared task scope (${peers.count} live connections total)`);
|
|
414
427
|
}
|
|
415
428
|
if (Number(autoUpdate.harness?.failed || 0) > 0) {
|
|
416
429
|
readinessWarnings.push(`${autoUpdate.harness.failed} automatic harness repair(s) remain partial`);
|
|
@@ -570,10 +583,17 @@ export function render(r, opts = {}) {
|
|
|
570
583
|
if (!r.sessions.count) L.push(`${ok} ${c.bold}SESSIONS${c.rst} 0 active sessions ${c.dim}(saved/recent chats are history, not active)${c.rst}`);
|
|
571
584
|
else {
|
|
572
585
|
const hiddenIdle = Math.max(0, (r.sessions.rawRecent || r.sessions.count) - r.sessions.count);
|
|
573
|
-
|
|
586
|
+
const activeSilent = r.sessions.activeUnscopedCount || 0;
|
|
587
|
+
const idleConnections = r.sessions.idleUnscopedCount || 0;
|
|
588
|
+
L.push(`${activeSilent ? warn : ok} ${c.bold}SESSIONS${c.rst} ${r.sessions.count} live connection${r.sessions.count === 1 ? '' : 's'} · ${r.sessions.syncedCount ?? r.sessions.count} with declared task scope${activeSilent ? ` · ${activeSilent} active without scope` : ''}${idleConnections ? ` · ${idleConnections} idle/unscoped (not graded)` : ''} ${c.dim}(all hosts; channel-aware liveness${hiddenIdle ? `; +${hiddenIdle} row(s) with only a dead mcp channel excluded` : ''})${c.rst}`);
|
|
574
589
|
for (const p of r.sessions.live) {
|
|
575
590
|
const intentAge = p.intentAgeMin !== null && p.intentAgeMin - p.lastSeenMin > 3 ? ` (set ${p.intentAgeMin}m ago)` : '';
|
|
576
|
-
|
|
591
|
+
const scopeState = p.synced
|
|
592
|
+
? ''
|
|
593
|
+
: (p.activeUnscoped
|
|
594
|
+
? ` ${c.yel}· active sync-silent (used KLYPIX without brain_sync scope)${c.rst}`
|
|
595
|
+
: ` ${c.dim}· idle connection (no task activity to grade)${c.rst}`);
|
|
596
|
+
L.push(` · ${p.client}:${p.id}${p.branch ? ' @' + p.branch : ''}${p.channels.length ? ` [${p.channels.join('+')}]` : ''}${p.intent ? ` “${p.intent.slice(0, 50)}”${intentAge}` : ''}${scopeState} ${c.dim}(${p.lastSeenMin}m ago)${c.rst}`);
|
|
577
597
|
}
|
|
578
598
|
for (const g of (r.sessions.twinGroups || [])) {
|
|
579
599
|
L.push(` ${c.yel}⚠ twin rows — channel merge NOT occurring: host pid ${g.hostPid} appears as ${g.ids.join(' + ')} (one logical session counted ${g.ids.length}×)${c.rst}`);
|