klypix-mcp 1.104.0 → 1.106.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 +16 -1
- package/bin/klypix-install.mjs +8 -3
- package/bin/klypix-mcp.mjs +3 -1
- package/bin/klypix-rescue.mjs +190 -0
- package/bin/klypix-worker.mjs +8 -2
- package/package.json +3 -3
- package/src/agent-presence.mjs +23 -13
- package/src/agent-rules.mjs +3 -1
- package/src/codex-brain-hook.mjs +98 -14
- package/src/global-brain-hook.mjs +68 -4
- package/src/klypix-core.mjs +63 -2
- package/src/mcp-presence.mjs +33 -2
- package/src/network-bridge.mjs +187 -0
- package/src/presence-relay.mjs +1 -1
- package/src/whose-classify.mjs +2795 -0
- package/src/whose-ledger.mjs +863 -0
- package/src/whose-rescue.mjs +1220 -0
- package/src/whose.mjs +302 -0
package/README.md
CHANGED
|
@@ -10,10 +10,17 @@ over MCP by a process on your machine. In the standalone setup, brain data stays
|
|
|
10
10
|
your agent's provider can still receive what the agent reads. Integration depth differs by host — see
|
|
11
11
|
[Supported hosts](#supported-hosts-and-their-integration-level).
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Klypix is a **portable project workspace with a shared brain**. This repository provides its
|
|
14
|
+
local MCP server for shared project memory in coding tools. The **Apache-2.0 engine works without the KLYPIX
|
|
15
|
+
desktop app or a KLYPIX account**. The optional
|
|
14
16
|
[Windows workspace](https://klypix.com/developers) lets people inspect the same project brain
|
|
15
17
|
beside their files. App-based sharing has its own cloud path; see [Security and permissions](#security-and-permissions).
|
|
16
18
|
|
|
19
|
+
For the **Projects & Ideas** path — saved project materials in ChatGPT or Claude chat — follow
|
|
20
|
+
[Save project materials and use them with your assistant](https://klypix.com/guides/connect-klypix-to-chatgpt-and-claude).
|
|
21
|
+
That uses Klypix's separate hosted connection for the spaces you authorize. Installing this local
|
|
22
|
+
MCP server does not set up that connection.
|
|
23
|
+
|
|
17
24
|
[Install](#quick-start) · [Try your first decision](#your-first-decision-and-correction) ·
|
|
18
25
|
[Supported hosts](#supported-hosts-and-their-integration-level) ·
|
|
19
26
|
[Privacy](#security-and-permissions) · [Limits](#current-limitations) · [Uninstall](#uninstall)
|
|
@@ -38,6 +45,11 @@ It creates `brain.klypix` if the project has none, wires the editors it finds on
|
|
|
38
45
|
registers the `.klypix` merge driver if this is a git repo, and exits only after a real MCP
|
|
39
46
|
handshake has counted the tools that answered.
|
|
40
47
|
|
|
48
|
+
Next, reconnect your coding host and [try your first decision and correction](#your-first-decision-and-correction).
|
|
49
|
+
A successful trial means a fresh session finds your recorded decision and its reason, then finds
|
|
50
|
+
the corrected decision as current. Check the returned source cards; a successful install alone
|
|
51
|
+
does not prove that your host has loaded the tools or that a particular decision was retrieved.
|
|
52
|
+
|
|
41
53
|
[](#supported-hosts-and-their-integration-level)
|
|
42
54
|
[](#supported-hosts-and-their-integration-level)
|
|
43
55
|
[](#supported-hosts-and-their-integration-level)
|
|
@@ -158,6 +170,9 @@ so running it from `src/` is fine), gives the project a brain if it doesn't have
|
|
|
158
170
|
agent tools you actually have installed, registers the card-level `.klypix` merge driver if it's a
|
|
159
171
|
git repo, and then **proves the result** before it exits:
|
|
160
172
|
|
|
173
|
+
Illustrative output; detected editors, file counts, tool counts and timings vary with the machine
|
|
174
|
+
and installed version:
|
|
175
|
+
|
|
161
176
|
```text
|
|
162
177
|
project E:\work\api (git repository root)
|
|
163
178
|
brain created brain.klypix — a starter brain, ready for its first decision
|
package/bin/klypix-install.mjs
CHANGED
|
@@ -495,7 +495,7 @@ try {
|
|
|
495
495
|
// a newer klypix-format cannot. merge-brains and the driver ship here so
|
|
496
496
|
// brain-history's restore merge, the KLYPIX core and the git driver all
|
|
497
497
|
// find one engine in this directory.
|
|
498
|
-
for (const f of ['global-brain-hook.mjs', 'klypix-format.mjs', 'brain-graveyard.mjs', 'merge-brains.mjs', 'klypix-merge-driver.mjs', 'capture-gap.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'enrichment.mjs', 'provenance.mjs', 'brain-note.mjs', 'brain-evidence.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'app-lease.mjs', 'klypix-core.mjs', 'app-bridge-protocol.mjs', 'app-bridge-client.mjs', 'app-tools.mjs', 'card-files.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'editor-detect.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'repo-state.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'session-handoff.mjs', 'live-projection.mjs', 'canvas-view-app.html']) {
|
|
498
|
+
for (const f of ['global-brain-hook.mjs', 'klypix-format.mjs', 'brain-graveyard.mjs', 'merge-brains.mjs', 'klypix-merge-driver.mjs', 'capture-gap.mjs', 'brain-semantic.mjs', 'semantic-memory.mjs', 'enrichment.mjs', 'provenance.mjs', 'brain-note.mjs', 'brain-evidence.mjs', 'brain-git-hook.mjs', 'git-capture-install.mjs', 'brain-history.mjs', 'app-lease.mjs', 'klypix-core.mjs', 'app-bridge-protocol.mjs', 'app-bridge-client.mjs', 'app-tools.mjs', 'card-files.mjs', 'brain-write-lock.mjs', 'agent-rules.mjs', 'brain-doctor.mjs', 'editor-detect.mjs', 'agent-presence.mjs', 'mcp-presence.mjs', 'repo-state.mjs', 'result-reconcile.mjs', 'finding-routing.mjs', 'presence-relay.mjs', 'network-bridge.mjs', 'mcp-supervisor.mjs', 'mcp-auto-update.mjs', 'runtime-inspector.mjs', 'project-graph.mjs', 'bench.mjs', 'codex-brain-hook.mjs', 'codex-hooks.mjs', 'session-handoff.mjs', 'live-projection.mjs', 'whose.mjs', 'whose-classify.mjs', 'whose-ledger.mjs', 'whose-rescue.mjs', 'canvas-view-app.html']) {
|
|
499
499
|
const s = path.join(SRC, f); if (exists(s)) staged.push({ dst: f, content: fs.readFileSync(s, 'utf8') });
|
|
500
500
|
}
|
|
501
501
|
for (const [src, dst] of [
|
|
@@ -568,11 +568,16 @@ try {
|
|
|
568
568
|
['SessionStart', { matcher: 'startup|resume|clear', hooks: [{ type: 'command', command: brainCmd('') }] }],
|
|
569
569
|
['UserPromptSubmit', { hooks: [{ type: 'command', command: brainCmd('--prompt'), timeout: 10 }] }],
|
|
570
570
|
['Stop', { hooks: [{ type: 'command', command: brainCmd('--capture') }] }],
|
|
571
|
-
['PostToolUse', { matcher: 'Bash|PowerShell|Edit|Write', hooks: [{ type: 'command', command: brainCmd('--live'), timeout: 10 }] }],
|
|
571
|
+
['PostToolUse', { matcher: 'Bash|PowerShell|Edit|Write|MultiEdit|NotebookEdit', hooks: [{ type: 'command', command: brainCmd('--live'), timeout: 10 }] }],
|
|
572
572
|
// Guard cards (2026-08-24): pre-action lane. Fast path — reads only the
|
|
573
573
|
// compiled guard sidecar; 'warn' injects context, 'block' denies with
|
|
574
574
|
// the card's message. Absent guards = stat + one tiny read, then no-op.
|
|
575
575
|
['PreToolUse', { matcher: 'Bash|PowerShell|Edit|Write', hooks: [{ type: 'command', command: brainCmd('--guard'), timeout: 10 }] }],
|
|
576
|
+
// Session end (1.105.0): a closed or cut-off Claude Code session leaves
|
|
577
|
+
// the lane at once. Before this, only the 10-minute TTL removed it, so the
|
|
578
|
+
// next session read a dead peer as "live" and refused to continue its
|
|
579
|
+
// work (round-4 continuity bench, 2026-10-20). Codex already had this.
|
|
580
|
+
['SessionEnd', { hooks: [{ type: 'command', command: brainCmd('--end'), timeout: 10 }] }],
|
|
576
581
|
];
|
|
577
582
|
const stripOurs = (arr) => (Array.isArray(arr) ? arr : [])
|
|
578
583
|
.map(g => (g && Array.isArray(g.hooks)) ? { ...g, hooks: g.hooks.filter(h => !(typeof h?.command === 'string' && h.command.includes(HOOK_MARK))) } : g)
|
|
@@ -679,7 +684,7 @@ try {
|
|
|
679
684
|
console.log(`✓ installed klypix brain v${VERSION} → ${BRAIN_DIR} (${n} scripts, ${deps} dep packages)`);
|
|
680
685
|
reportSemantic(semanticStep);
|
|
681
686
|
if (RUNTIME_ONLY) console.log('✓ runtime-only update: host settings and project files were preserved');
|
|
682
|
-
else if (!notWired.length) console.log('✓ wired
|
|
687
|
+
else if (!notWired.length) console.log('✓ wired 6 hooks: SessionEnd (--end) · SessionStart · UserPromptSubmit (--prompt) · Stop (--capture) · PostToolUse (--live) · PreToolUse (--guard) → settings.json');
|
|
683
688
|
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}.`);
|
|
684
689
|
console.log(`✓ MCP supervisor runs from the local bundle (node ${fwd(path.join(BRAIN_DIR, 'klypix-mcp-server.mjs'))}) — compatible core updates activate without restarting the host.`);
|
|
685
690
|
if (migrated) console.log(`✓ migrated ${migrated.file} klypix-canvas server: ${migrated.from} → ${migrated.to} (backup: .mcp.json.klypix-bak). Reconnect (/mcp) or restart to pick it up.`);
|
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', 'runtime', 'conformance', 'garden-code', 'init', 'git-driver', 'git-hook', 'brain-history', 'brain-deleted', 'orphans', 'diff', 'pr-brief', 'uninstall', 'bench', 'sessions']);
|
|
22
|
+
const DIRECT = new Set(['install', 'link', 'doctor', 'runtime', 'conformance', 'garden-code', 'init', 'git-driver', 'git-hook', 'brain-history', 'brain-deleted', 'orphans', 'diff', 'pr-brief', 'uninstall', 'bench', 'sessions', 'rescue']);
|
|
23
23
|
|
|
24
24
|
const USAGE = [
|
|
25
25
|
`klypix-mcp ${PKG_VERSION} — shared project brain + MCP coordination server.`,
|
|
@@ -41,6 +41,8 @@ const USAGE = [
|
|
|
41
41
|
' git-hook [install|remove|status] wire the agent-neutral commit-capture hook (any agent/branch/worktree → brain cards)',
|
|
42
42
|
' brain-history [list|restore <id>] restore points for this brain — undo an accidental delete, edit, or overwrite',
|
|
43
43
|
' brain-deleted [list|restore|purge] recycle bin for this brain — cards you deleted, kept recoverable',
|
|
44
|
+
' rescue [list|show <id>|restore <id> [paths…]|prune]',
|
|
45
|
+
' copies of uncommitted files KLYPIX saved before a git command could discard them',
|
|
44
46
|
' orphans [brain] [--apply|--areas] orphan gardener backfill: report unconnected cards, link the confident subset (dry-run default)',
|
|
45
47
|
' diff [ref] [--brain <path>] readable brain diff vs a git ref (default HEAD) — markdown to stdout',
|
|
46
48
|
' pr-brief [baseRef] [--brain <path>] brain decisions touching the files changed since baseRef — PR-comment markdown',
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `klypix-mcp rescue [list|show <n|id>|restore <n|id> [paths…]|prune]`
|
|
3
|
+
// The human surface for the rescue copies KLYPIX takes before a git command
|
|
4
|
+
// could discard uncommitted files (src/whose-rescue.mjs). A copy nobody can
|
|
5
|
+
// find is a copy nobody trusts, so `list` is the default, `show` says exactly
|
|
6
|
+
// what a copy holds and what it left out, and `restore` says what it changed
|
|
7
|
+
// and how to undo it — a restore first saves the files it is about to replace.
|
|
8
|
+
//
|
|
9
|
+
// A restore writes the working tree only. It never stages, commits, switches a
|
|
10
|
+
// branch or touches the git index.
|
|
11
|
+
//
|
|
12
|
+
// ARGV: process.argv.slice(2) — the standalone shape. The dispatcher
|
|
13
|
+
// (bin/klypix-worker.mjs runVerb) removes its own `rescue` token first.
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import {
|
|
16
|
+
listRescuesResult, showRescue, restoreRescue, pruneRescues,
|
|
17
|
+
rescueRestoreCommand, RESCUE_KEEP, RESCUE_MAX_AGE_MS,
|
|
18
|
+
} from '../src/whose-rescue.mjs';
|
|
19
|
+
|
|
20
|
+
const USAGE = [
|
|
21
|
+
'klypix-mcp rescue — copies of uncommitted files KLYPIX saved before a git command could discard them.',
|
|
22
|
+
'',
|
|
23
|
+
'Usage:',
|
|
24
|
+
' npx klypix-mcp rescue [list] [--all] newest first (--all: every worktree of this repository)',
|
|
25
|
+
' npx klypix-mcp rescue show <n|id> what one copy holds, and what it left out',
|
|
26
|
+
' npx klypix-mcp rescue restore <n|id> [paths…] write its files back (all, or the paths/folders named)',
|
|
27
|
+
' npx klypix-mcp rescue prune [--days 14] [--keep 50] [--dry-run]',
|
|
28
|
+
'',
|
|
29
|
+
'Flags: --repo <dir> (default: the current folder), --json, --other-worktree (restore a copy taken in another worktree here).',
|
|
30
|
+
'<n> is the number `list` shows; an id may be shortened while it stays unique.',
|
|
31
|
+
'A restore first saves the files it replaces as a new copy, so it can be undone the same way.',
|
|
32
|
+
'It writes the working tree only: nothing is staged, committed or switched.',
|
|
33
|
+
].join('\n');
|
|
34
|
+
|
|
35
|
+
const raw = process.argv.slice(2);
|
|
36
|
+
if (raw.includes('--help') || raw.includes('-h')) { console.log(USAGE); process.exit(0); }
|
|
37
|
+
|
|
38
|
+
const VALUE_FLAGS = new Set(['--repo', '--days', '--keep']);
|
|
39
|
+
const BOOL_FLAGS = new Set(['--json', '--all', '--dry-run', '--other-worktree']);
|
|
40
|
+
const flags = {};
|
|
41
|
+
const positional = [];
|
|
42
|
+
for (let i = 0; i < raw.length; i++) {
|
|
43
|
+
const a = raw[i];
|
|
44
|
+
if (a === '--') { positional.push(...raw.slice(i + 1)); break; }
|
|
45
|
+
if (VALUE_FLAGS.has(a)) {
|
|
46
|
+
if (i + 1 >= raw.length) { console.error(`klypix-mcp rescue: ${a} needs a value.\n\n${USAGE}`); process.exit(2); }
|
|
47
|
+
flags[a] = raw[++i];
|
|
48
|
+
} else if (BOOL_FLAGS.has(a)) flags[a] = true;
|
|
49
|
+
else if (a.startsWith('-') && a.length > 1) { console.error(`klypix-mcp rescue: unknown flag ${a}\n\n${USAGE}`); process.exit(2); }
|
|
50
|
+
else positional.push(a);
|
|
51
|
+
}
|
|
52
|
+
const ACTIONS = ['list', 'show', 'restore', 'prune'];
|
|
53
|
+
const action = ACTIONS.includes(positional[0]) ? positional.shift() : 'list';
|
|
54
|
+
if (action === 'list' && positional.length) {
|
|
55
|
+
console.error(`klypix-mcp rescue: unknown command "${positional[0]}".\n\n${USAGE}`);
|
|
56
|
+
process.exit(2);
|
|
57
|
+
}
|
|
58
|
+
const repoRoot = path.resolve(flags['--repo'] || process.cwd());
|
|
59
|
+
const json = Boolean(flags['--json']);
|
|
60
|
+
const out = (value) => console.log(JSON.stringify(value, null, 2));
|
|
61
|
+
|
|
62
|
+
const ago = (ts) => {
|
|
63
|
+
const m = Math.max(0, Math.round((Date.now() - ts) / 60000));
|
|
64
|
+
if (m < 1) return 'just now';
|
|
65
|
+
if (m < 60) return `${m}m ago`;
|
|
66
|
+
const h = Math.round(m / 60);
|
|
67
|
+
return h < 48 ? `${h}h ago` : `${Math.round(h / 24)}d ago`;
|
|
68
|
+
};
|
|
69
|
+
const utc = (ts) => new Date(ts).toISOString().replace('T', ' ').replace(/\.\d+Z$/, ' UTC');
|
|
70
|
+
const plural = (n, word, many = `${word}s`) => `${n} ${n === 1 ? word : many}`;
|
|
71
|
+
const SHOW = 12;
|
|
72
|
+
const listLines = (items, fmt) => {
|
|
73
|
+
for (const x of items.slice(0, SHOW)) console.log(` ${fmt(x)}`);
|
|
74
|
+
if (items.length > SHOW) console.log(` …and ${items.length - SHOW} more`);
|
|
75
|
+
};
|
|
76
|
+
const summary = (e) => {
|
|
77
|
+
const absent = e.deleted.length ? ` (${e.deleted.length} absent)` : '';
|
|
78
|
+
const left = e.skipped.length ? `, ${e.skipped.length} not included` : '';
|
|
79
|
+
return `${plural(e.paths.length, 'file')}${absent}${left}`;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
if (action === 'list') {
|
|
83
|
+
const res = listRescuesResult({ repoRoot, allWorktrees: Boolean(flags['--all']) });
|
|
84
|
+
if (!res.ok) { console.error(`klypix-mcp rescue: ${res.error}`); process.exit(1); }
|
|
85
|
+
if (json) { out(res.rescues); process.exit(0); }
|
|
86
|
+
if (!res.rescues.length) {
|
|
87
|
+
console.log(`No rescue copies in ${res.top}${flags['--all'] ? '' : ' (this worktree)'}.`);
|
|
88
|
+
if (res.elsewhere) console.log(`${plural(res.elsewhere, 'copy', 'copies')} taken in other worktrees of this repository: add --all.`);
|
|
89
|
+
console.log('KLYPIX takes one before a git command could discard uncommitted files.');
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
console.log(`Rescue copies in ${res.top}, newest first:`);
|
|
93
|
+
// Numbers count this worktree's copies only — the same numbering `show <n>`
|
|
94
|
+
// and `restore <n>` use — so a copy from another worktree gets none.
|
|
95
|
+
let n = 0;
|
|
96
|
+
for (const e of res.rescues) {
|
|
97
|
+
const num = e.here ? String(++n) : '-';
|
|
98
|
+
const where = e.here ? '' : ` [${e.worktree}]`;
|
|
99
|
+
console.log(` ${num.padStart(3)} ${e.id} ${ago(e.at).padEnd(9)} ${e.reason} · ${summary(e)}${where}`);
|
|
100
|
+
}
|
|
101
|
+
if (!flags['--all'] && res.elsewhere) console.log(` (+${plural(res.elsewhere, 'copy', 'copies')} from other worktrees: add --all)`);
|
|
102
|
+
console.log('');
|
|
103
|
+
console.log('Show one: npx klypix-mcp rescue show <n|id>');
|
|
104
|
+
console.log('Restore one: npx klypix-mcp rescue restore <n|id> [paths…]');
|
|
105
|
+
console.log('A restore saves the current files first, so it can be undone the same way.');
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (action === 'show') {
|
|
110
|
+
if (!positional[0]) { console.error(`Usage: npx klypix-mcp rescue show <n|id>\n\n${USAGE}`); process.exit(2); }
|
|
111
|
+
const res = showRescue({ repoRoot, id: positional[0] });
|
|
112
|
+
if (json) { out(res); process.exit(res.ok ? 0 : 1); }
|
|
113
|
+
if (!res.ok) { console.error(`klypix-mcp rescue: ${res.error}`); process.exit(1); }
|
|
114
|
+
console.log(`Rescue ${res.id} — ${res.reason}`);
|
|
115
|
+
console.log(` taken ${ago(res.at)} (${utc(res.at)})${res.sid ? ` by session ${res.sid}` : ''}`);
|
|
116
|
+
console.log(` on ${res.branch || 'a detached HEAD'}${res.head ? ` @ ${res.head.slice(0, 10)}` : ' (no commit yet)'}${res.worktree ? `, in ${res.worktree}` : ''}${res.here ? '' : ' — ANOTHER worktree'}`);
|
|
117
|
+
console.log(` git ${res.ref} (commit ${res.commit.slice(0, 10)})`);
|
|
118
|
+
console.log(`\n${plural(res.files.length, 'file')}; ${res.stat.added} line(s) added and ${res.stat.removed} removed against that HEAD:`);
|
|
119
|
+
const width = Math.min(48, Math.max(...res.files.map((f) => f.path.length), 4));
|
|
120
|
+
listLines(res.files, (f) => {
|
|
121
|
+
const lines = f.binary ? 'binary' : f.change === 'deleted' || f.change === 'absent' ? '' : `+${f.added} -${f.removed}`;
|
|
122
|
+
return `${f.change.padEnd(12)} ${f.path.padEnd(width)} ${lines}`.trimEnd();
|
|
123
|
+
});
|
|
124
|
+
if (res.skipped.length) {
|
|
125
|
+
console.log('\nNot included:');
|
|
126
|
+
listLines(res.skipped, (s) => `${s.path} (${s.why})`);
|
|
127
|
+
}
|
|
128
|
+
console.log(`\nRestore: ${rescueRestoreCommand(res.id)} [paths…]`);
|
|
129
|
+
console.log(`Or with git: ${res.head ? `git diff ${res.commit.slice(0, 10)}^ ${res.commit.slice(0, 10)}` : `git show ${res.commit.slice(0, 10)}`}`);
|
|
130
|
+
process.exit(0);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (action === 'restore') {
|
|
134
|
+
if (!positional[0]) {
|
|
135
|
+
console.error('Usage: npx klypix-mcp rescue restore <n|id> [paths…]');
|
|
136
|
+
console.error('Run `npx klypix-mcp rescue list` to see the copies.');
|
|
137
|
+
process.exit(2);
|
|
138
|
+
}
|
|
139
|
+
const [id, ...paths] = positional;
|
|
140
|
+
const res = restoreRescue({
|
|
141
|
+
repoRoot, id, paths: paths.length ? paths : null, sid: 'cli', cwd: process.cwd(),
|
|
142
|
+
allowOtherWorktree: Boolean(flags['--other-worktree']),
|
|
143
|
+
});
|
|
144
|
+
if (json) { out(res); process.exit(res.ok && !res.skipped?.length ? 0 : 1); }
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
console.error(`Restore failed: ${res.error}`);
|
|
147
|
+
if (res.skipped?.length) listLines(res.skipped, (s) => `${s.path} — ${s.why}`);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
const written = res.restored.filter((p) => !res.deleted.includes(p));
|
|
151
|
+
if (res.restored.length) {
|
|
152
|
+
console.log(`Restored ${plural(res.restored.length, 'path')} from rescue ${res.from}:`);
|
|
153
|
+
listLines(written, (p) => p);
|
|
154
|
+
if (res.deleted.length) listLines(res.deleted, (p) => `removed ${p} (it did not exist when the copy was taken)`);
|
|
155
|
+
} else console.log(`Nothing needed writing from rescue ${res.from}.`);
|
|
156
|
+
if (res.unchanged.length) {
|
|
157
|
+
console.log('Already as in the copy:');
|
|
158
|
+
listLines(res.unchanged, (p) => p);
|
|
159
|
+
}
|
|
160
|
+
if (res.skipped.length) {
|
|
161
|
+
console.log('Left as it is:');
|
|
162
|
+
listLines(res.skipped, (s) => `${s.path} — ${s.why}`);
|
|
163
|
+
}
|
|
164
|
+
if (res.backupId) {
|
|
165
|
+
console.log(`\nWhat was there before is saved as ${res.backupId}. Undo this restore: ${rescueRestoreCommand(res.backupId)}`);
|
|
166
|
+
}
|
|
167
|
+
console.log('Only the working tree changed: nothing was staged or committed. `git status` shows the result.');
|
|
168
|
+
process.exit(res.skipped.length ? 1 : 0);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// prune
|
|
172
|
+
const days = flags['--days'] != null ? Number(flags['--days']) : RESCUE_MAX_AGE_MS / 86400000;
|
|
173
|
+
const keep = flags['--keep'] != null ? Number(flags['--keep']) : RESCUE_KEEP;
|
|
174
|
+
if (!Number.isFinite(days) || days < 0 || !Number.isInteger(keep) || keep < 0) {
|
|
175
|
+
console.error(`klypix-mcp rescue prune: --days takes a number of days and --keep a whole number.\n\n${USAGE}`);
|
|
176
|
+
process.exit(2);
|
|
177
|
+
}
|
|
178
|
+
const res = pruneRescues({ repoRoot, keep, maxAgeMs: days * 86400000, dryRun: Boolean(flags['--dry-run']) });
|
|
179
|
+
if (json) { out(res); process.exit(res.ok ? 0 : 1); }
|
|
180
|
+
if (!res.ok) { console.error(`klypix-mcp rescue: ${res.error}`); process.exit(1); }
|
|
181
|
+
const rule = `older than ${days} day(s), keeping the newest ${keep} of each worktree`;
|
|
182
|
+
if (flags['--dry-run']) {
|
|
183
|
+
const would = res.wouldDelete || [];
|
|
184
|
+
console.log(would.length ? `Would prune ${plural(would.length, 'copy', 'copies')} ${rule}:` : `Nothing to prune (${rule}).`);
|
|
185
|
+
listLines(would, (x) => x);
|
|
186
|
+
console.log('DRY RUN — nothing was deleted.');
|
|
187
|
+
} else {
|
|
188
|
+
console.log(res.deleted.length ? `Pruned ${plural(res.deleted.length, 'copy', 'copies')} ${rule}; ${res.kept} remain.` : `Nothing to prune (${rule}); ${res.kept} kept.`);
|
|
189
|
+
}
|
|
190
|
+
process.exit(0);
|
package/bin/klypix-worker.mjs
CHANGED
|
@@ -190,6 +190,11 @@ await runVerb('pr-brief', './klypix-pr-brief.mjs');
|
|
|
190
190
|
// CLI's own --help) said no uninstall existed at all.
|
|
191
191
|
await runVerb('uninstall', './klypix-uninstall.mjs');
|
|
192
192
|
|
|
193
|
+
// `npx klypix-mcp rescue` — list, show and restore the rescue copies KLYPIX
|
|
194
|
+
// takes before a git command could discard uncommitted files (whose-rescue.mjs).
|
|
195
|
+
// A restore writes the working tree only, after saving what it replaces.
|
|
196
|
+
await runVerb('rescue', './klypix-rescue.mjs');
|
|
197
|
+
|
|
193
198
|
// `npx klypix-mcp garden-code` — the HUMAN half of the garden approval gate.
|
|
194
199
|
// brain_garden's apply requires an 8-char code derived from the exact dormant
|
|
195
200
|
// candidate set + day; the agent is deliberately never shown it. The human runs
|
|
@@ -854,7 +859,7 @@ server.registerTool('brain_note', {
|
|
|
854
859
|
|
|
855
860
|
server.registerTool('brain_message', {
|
|
856
861
|
title: 'Message another agent session on this project — live, idle, or recently closed (one-time note, not a brain card)',
|
|
857
|
-
description: 'Leave a DELIBERATE, targeted note for another agent session working on this project ("merged the hook refactor — rebase before you commit", "don\'t touch canvasStore, mid-refactor"). Address it to a session id from the brain_sync peer list or brain_doctor. A LIVE session gets it on its next lifecycle event or KLYPIX tool call, even if it is idle right now. A session KLYPIX has identified before that is NOT on the lane now (closed, or quiet) still receives a directed note the moment it next acts — the note is kept for up to 7 days — so if you would otherwise ask the human to relay or paste something to another agent session, send it here instead and tell them it was sent or queued. If the human wants a queued note handled sooner, brain_reopen asks them whether to reopen that session now. Broadcasts ("all") reach only sessions live right now. Any MCP client can send and receive through the shared machine-local presence lane. Delivery contract: a supported action offers the note into model-visible context, a later independent action acknowledges it, and it retires either by an explicit brain_message_receipt ("acted on it") or by auto-consumption on a further independent action; your receipt line names which, and neither is proof a human read it. Expiry or capacity loss leaves a failed per-recipient receipt instead of silently disappearing. OS-user-local and machine-local. Ephemeral and NOT persisted to the brain — for a durable decision use brain_note instead.',
|
|
862
|
+
description: 'Leave a DELIBERATE, targeted note for another agent session working on this project ("merged the hook refactor — rebase before you commit", "don\'t touch canvasStore, mid-refactor"). Address it to a session id from the brain_sync peer list or brain_doctor. A LIVE session gets it on its next lifecycle event or KLYPIX tool call, even if it is idle right now. A session KLYPIX has identified before that is NOT on the lane now (closed, or quiet) still receives a directed note the moment it next acts — the note is kept for up to 7 days — so if you would otherwise ask the human to relay or paste something to another agent session, send it here instead and tell them it was sent or queued. If the human wants a queued note handled sooner, brain_reopen asks them whether to reopen that session now. Broadcasts ("all") reach only sessions live right now. Any MCP client can send and receive through the shared machine-local presence lane. Delivery contract: a supported action offers the note into model-visible context, a later independent action acknowledges it, and it retires either by an explicit brain_message_receipt ("acted on it") or by auto-consumption on a further independent action; your receipt line names which, and neither is proof a human read it. Expiry or capacity loss leaves a failed per-recipient receipt instead of silently disappearing. OS-user-local and machine-local; with KLYPIX network installed on both computers, a session on another computer in the same project is reached too (its id starts with net:, and there is no read receipt across computers). Ephemeral and NOT persisted to the brain — for a durable decision use brain_note instead.',
|
|
858
863
|
inputSchema: {
|
|
859
864
|
text: z.string().describe('The note to deliver (kept to 400 chars).'),
|
|
860
865
|
to: z.string().optional().describe('Target — a session id (or unique prefix of at least 8 characters) from the peer list, or an exact unique branch; omit or "all" for every session live right now. A directed id may name a session that is not running: the note then waits and is delivered when that session next starts.'),
|
|
@@ -873,6 +878,7 @@ server.registerTool('brain_message', {
|
|
|
873
878
|
to,
|
|
874
879
|
via: extra.klypixClientName,
|
|
875
880
|
from: extra.klypixRequestIdentity.sessionId,
|
|
881
|
+
networkSend: mcpPresence.networkSend,
|
|
876
882
|
}));
|
|
877
883
|
});
|
|
878
884
|
|
|
@@ -1124,7 +1130,7 @@ server.registerTool('brain_handoff', {
|
|
|
1124
1130
|
|
|
1125
1131
|
server.registerTool('brain_sync', {
|
|
1126
1132
|
title: 'KLYPIX Context Gateway — synchronize task, peers, conflicts, and relevant memory',
|
|
1127
|
-
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
|
|
1133
|
+
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 computer appears only when KLYPIX network is installed on both computers (presence, intent, files and brain_message, no result reconciliation), and file overlap remains ADVISORY.',
|
|
1128
1134
|
annotations: {
|
|
1129
1135
|
destructiveHint: false,
|
|
1130
1136
|
idempotentHint: true,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "klypix-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.106.0",
|
|
4
4
|
"mcpName": "io.github.dahshanlabs/klypix-mcp",
|
|
5
5
|
"description": "Shared project memory for Claude Code, Codex and Cursor: decisions and corrections through MCP.",
|
|
6
6
|
"type": "module",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"bench": "node bin/klypix-mcp.mjs bench",
|
|
92
92
|
"test:bench": "node test/bench.mjs",
|
|
93
93
|
"pretest": "node test/publish-workflow.mjs",
|
|
94
|
-
"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/session-handoff.mjs && node test/lock-notes.mjs && node test/live-projection.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/marker-after-colon.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",
|
|
94
|
+
"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/session-handoff.mjs && node test/lock-notes.mjs && node test/whose-classify.mjs && node test/whose-ledger.mjs && node test/whose-rescue.mjs && node test/whose-hook.mjs && node test/live-projection.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/network-bridge.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/marker-after-colon.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",
|
|
95
95
|
"test:memory": "node test/memory-runtime.mjs",
|
|
96
96
|
"test:memory:soak": "node --expose-gc test/memory-soak.mjs",
|
|
97
97
|
"runtime": "node bin/klypix-runtime.mjs",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
},
|
|
102
102
|
"dependencies": {
|
|
103
103
|
"@modelcontextprotocol/ext-apps": "^1.7.5",
|
|
104
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
104
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
105
105
|
"fractional-indexing": "^3.2.0",
|
|
106
106
|
"jpeg-js": "^0.4.4",
|
|
107
107
|
"jszip": "^3.10.1",
|
package/src/agent-presence.mjs
CHANGED
|
@@ -114,7 +114,7 @@ export function isProcessAlive(pid) {
|
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
export function isDeadHostRow(session, now = Date.now(), probe = isProcessAlive) {
|
|
117
|
-
if (!session || session.via === 'cloud') return false;
|
|
117
|
+
if (!session || (session.via === 'cloud' || session.via === 'network')) return false;
|
|
118
118
|
if (String(session.hostPidSource || '') !== 'env') return false;
|
|
119
119
|
if (!session.machine || String(session.machine) !== String(MACHINE_ID)) return false;
|
|
120
120
|
if (now - Number(session.lastSeen || 0) < DEAD_HOST_GRACE_MS) return false;
|
|
@@ -635,7 +635,7 @@ const pruneDirectory = (rows, now) => (Array.isArray(rows) ? rows : [])
|
|
|
635
635
|
function rememberSession(directory, row, now, { ended = false } = {}) {
|
|
636
636
|
const rows = pruneDirectory(directory, now);
|
|
637
637
|
const id = recipientKey(row?.logicalSessionId || '');
|
|
638
|
-
if (!id || row?.via === 'cloud') return rows;
|
|
638
|
+
if (!id || (row?.via === 'cloud' || row?.via === 'network')) return rows;
|
|
639
639
|
const index = rows.findIndex((entry) => entry.id === id);
|
|
640
640
|
const prev = index >= 0 ? rows[index] : null;
|
|
641
641
|
const scope = [...new Set([...normalizeFiles(row.files), ...normalizeFiles(row.observedFiles)])].slice(-8);
|
|
@@ -1297,6 +1297,9 @@ export function upsertSession({
|
|
|
1297
1297
|
// machine renders once, as local. Rows land with via:'cloud' + channelSeen.cloud
|
|
1298
1298
|
// so existing TTL pruning governs their lifetime (P3) and buildPresenceFrame
|
|
1299
1299
|
// refuses to re-broadcast them (loop prevention).
|
|
1300
|
+
// Where a remote row came from. Only these are ever written by upsertRemoteSessions.
|
|
1301
|
+
export const REMOTE_VIAS = new Set(['cloud', 'network']);
|
|
1302
|
+
|
|
1300
1303
|
export function upsertRemoteSessions({ brainPath, rows, machineId = MACHINE_ID, home, now = Date.now() }) {
|
|
1301
1304
|
if (!brainPath || !Array.isArray(rows) || !rows.length) return withWriteVerdict([], false, 'nothing-to-write');
|
|
1302
1305
|
const laneFile = laneFileFor(brainPath, home);
|
|
@@ -1309,10 +1312,12 @@ export function upsertRemoteSessions({ brainPath, rows, machineId = MACHINE_ID,
|
|
|
1309
1312
|
const data = laneRead.data;
|
|
1310
1313
|
const sessions = pruneSessions(data.sessions, now);
|
|
1311
1314
|
for (const row of rows) {
|
|
1312
|
-
|
|
1315
|
+
// 'cloud' rows ride the Supabase presence channel; 'network' rows come from
|
|
1316
|
+
// the KLYPIX network engine (network-bridge.mjs). Both are remote, never local.
|
|
1317
|
+
if (!row?.id || !row.machine || !REMOTE_VIAS.has(row.via)) continue;
|
|
1313
1318
|
if (String(row.machine) === String(machineId)) continue; // never a remote row for this machine
|
|
1314
1319
|
const existingIdx = sessions.findIndex((session) => session.id === row.id);
|
|
1315
|
-
if (existingIdx >= 0 && sessions[existingIdx].via
|
|
1320
|
+
if (existingIdx >= 0 && !REMOTE_VIAS.has(sessions[existingIdx].via)) continue; // local row wins
|
|
1316
1321
|
const previous = existingIdx >= 0 ? sessions[existingIdx] : {};
|
|
1317
1322
|
// REPLACE semantics, like `files` below: the frame is a full snapshot of
|
|
1318
1323
|
// the sender's row, so an empty/absent observed lane must CLEAR the
|
|
@@ -1338,11 +1343,12 @@ export function upsertRemoteSessions({ brainPath, rows, machineId = MACHINE_ID,
|
|
|
1338
1343
|
...(remoteObserved.length ? { observedFiles: remoteObserved } : {}),
|
|
1339
1344
|
machine: String(row.machine),
|
|
1340
1345
|
host: row.host ?? previous.host ?? null,
|
|
1341
|
-
via:
|
|
1342
|
-
channels: [
|
|
1343
|
-
|
|
1346
|
+
via: row.via,
|
|
1347
|
+
channels: [row.via],
|
|
1348
|
+
...(row.via === 'network' && row.address ? { networkAddress: String(row.address).slice(0, 200) } : {}),
|
|
1349
|
+
transport: { [row.via]: { status: 'relay-best-effort', at: now } },
|
|
1344
1350
|
deliveryReachability: 'relay-best-effort',
|
|
1345
|
-
channelSeen: {
|
|
1351
|
+
channelSeen: { [row.via]: now }, // receiver clock (P4) — frame time never decides freshness
|
|
1346
1352
|
startedAt: previous.startedAt || now,
|
|
1347
1353
|
lastSeen: now,
|
|
1348
1354
|
};
|
|
@@ -1399,7 +1405,7 @@ export function purgeRemotePresence({ brainPath, home, now = Date.now() }) {
|
|
|
1399
1405
|
};
|
|
1400
1406
|
const data = laneRead.data;
|
|
1401
1407
|
const maintainedSessions = pruneSessions(data.sessions, now);
|
|
1402
|
-
const sessions = maintainedSessions.filter((session) => session.via
|
|
1408
|
+
const sessions = maintainedSessions.filter((session) => !REMOTE_VIAS.has(session.via));
|
|
1403
1409
|
const maintainedMessages = maintainMessages(data.messages, now);
|
|
1404
1410
|
const messages = maintainedMessages.filter((message) =>
|
|
1405
1411
|
!String(message?.dedupeKey || '').startsWith(REMOTE_MESSAGE_DEDUPE_PREFIX));
|
|
@@ -2032,7 +2038,7 @@ export function rotateEndedSessionIdentity({
|
|
|
2032
2038
|
|
|
2033
2039
|
const existing = sessions.find((session) => recipientKey(session.id) === toKey
|
|
2034
2040
|
|| normalizeAliases(session.aliases).includes(toKey)) || null;
|
|
2035
|
-
if (existing?.via === 'cloud') {
|
|
2041
|
+
if ((existing?.via === 'cloud' || existing?.via === 'network')) {
|
|
2036
2042
|
return { ok: false, changed: false, reason: 'destination-remote-only' };
|
|
2037
2043
|
}
|
|
2038
2044
|
const channelSeen = freshChannelSeen(existing?.channelSeen, now);
|
|
@@ -2141,7 +2147,7 @@ export function switchMcpSessionIdentity({
|
|
|
2141
2147
|
// A stale alias on A must not turn this fresh switch back into an A→B
|
|
2142
2148
|
// rekey. Only an independently stored B row may contribute B's own state.
|
|
2143
2149
|
const existing = destinationCandidate === from ? null : destinationCandidate;
|
|
2144
|
-
if (existing?.via === 'cloud') {
|
|
2150
|
+
if ((existing?.via === 'cloud' || existing?.via === 'network')) {
|
|
2145
2151
|
return { ok: false, changed: false, reason: 'destination-remote-only' };
|
|
2146
2152
|
}
|
|
2147
2153
|
|
|
@@ -2809,7 +2815,11 @@ export function postPresenceMessage({
|
|
|
2809
2815
|
// Receipt truth must survive peers ending before the sender checks doctor.
|
|
2810
2816
|
// Snapshot only recipient session ids (already lane metadata) at SEND time;
|
|
2811
2817
|
// old messages without this additive field retain reconstruction fallback.
|
|
2812
|
-
|
|
2818
|
+
// A session on another computer reached over the KLYPIX network is never a
|
|
2819
|
+
// local-lane recipient: nothing on this PC would ever collect the note, so
|
|
2820
|
+
// it would only age into a failed receipt. brain_message routes those
|
|
2821
|
+
// through the engine (klypix-core.mjs opBrainMessage).
|
|
2822
|
+
const resolution = resolveMessageTarget({ to: target }, sessions.filter((row) => row?.via !== 'network'));
|
|
2813
2823
|
const candidateIds = resolution.ids
|
|
2814
2824
|
.filter((id) => id && id !== senderId)
|
|
2815
2825
|
.map((id) => String(id).slice(0, 160));
|
|
@@ -3629,7 +3639,7 @@ export function mergePresenceRows(sessions) {
|
|
|
3629
3639
|
|| (Array.isArray(row.channels) && row.channels.includes('lifecycle')));
|
|
3630
3640
|
const isFoldableOrphan = (group) => group.length === 1
|
|
3631
3641
|
&& group.every((row) => !recipientKey(row.logicalSessionId)
|
|
3632
|
-
&& row.via !== 'cloud'
|
|
3642
|
+
&& (row.via !== 'cloud' && row.via !== 'network')
|
|
3633
3643
|
&& Number(row.hostPid) > 0
|
|
3634
3644
|
&& row.machine
|
|
3635
3645
|
&& Array.isArray(row.channels) && row.channels.length
|
package/src/agent-rules.mjs
CHANGED
|
@@ -515,7 +515,9 @@ commits and the checkouts it worked in (with their uncommitted files) automatica
|
|
|
515
515
|
\`brain_sync\` may list **unfinished work** another session left without a card: if the human asks
|
|
516
516
|
you to continue it, take it with \`brain_handoff\` {action:"take", session:"<id>"} first, and look
|
|
517
517
|
first in the checkout the record names — it may be a side folder, not the project. That list is history, not a lock — a stopped session holds no claim on its
|
|
518
|
-
files.
|
|
518
|
+
files. A LIVE session's uncommitted edits are its work in progress: never sweep, discard or stash them
|
|
519
|
+
(\`git add -A\`, \`commit -a\`, \`reset --hard\`, \`stash\`); commit only your own files. A stopped session's leftovers
|
|
520
|
+
are yours to finish and commit when you are continuing its work; never discard them without the human. Never ask the human to paste a summary into the next session.
|
|
519
521
|
|
|
520
522
|
**A note that asks agents to keep off a file**, whatever its wording (here, in AGENTS.md / CLAUDE.md or any
|
|
521
523
|
notes file) can outlive the work it protected. Before you obey one, or before you edit the file
|