pi-mega-compact 0.6.3 → 0.6.4
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/dist/extensions/conflict-scan.js +133 -60
- package/dist/extensions/dashboard-server.js +101 -35
- package/dist/extensions/dashboard-server.test.js +2 -2
- package/dist/extensions/mega-runtime.js +47 -20
- package/extensions/conflict-scan.test.ts +129 -0
- package/extensions/conflict-scan.ts +243 -158
- package/extensions/dashboard-server.test.ts +2 -2
- package/extensions/dashboard-server.ts +106 -36
- package/extensions/mega-dashboard.ts +16 -0
- package/extensions/mega-runtime.ts +48 -20
- package/package.json +1 -1
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conflict-scan.test.ts — unit tests for the extension-conflict scanner.
|
|
3
|
+
*
|
|
4
|
+
* Fixture trees are written under a temp dir and scanned via
|
|
5
|
+
* MEGACOMPACT_EXT_SCAN_DIR (which makes collectScanRoots() return that
|
|
6
|
+
* single root). This covers the S24 follow-up fix:
|
|
7
|
+
*
|
|
8
|
+
* 1. node_modules-style code extensions (package.json + pi.extensions) are
|
|
9
|
+
* still detected by source-marker grep (regression).
|
|
10
|
+
* 2. USER-LEVEL extensions installed outside npm (e.g. pi-hermes-memory)
|
|
11
|
+
* now get scanned too — previously only `node_modules` was walked, so a
|
|
12
|
+
* data-only memory store (MEMORY.md + sessions.db, no package.json)
|
|
13
|
+
* was never flagged (the 5000-char file-buffer error slipped through).
|
|
14
|
+
* 3. The data-only memory-store signature is detected even with no source.
|
|
15
|
+
* 4. pi-mega-compact (selfName) is always skipped.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { test, after } from "node:test";
|
|
19
|
+
import assert from "node:assert/strict";
|
|
20
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { tmpdir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { detectConflicts, collectScanRoots } from "./conflict-scan.js";
|
|
24
|
+
|
|
25
|
+
const base = mkdtempSync(join(tmpdir(), "mc-scan-"));
|
|
26
|
+
let n = 0;
|
|
27
|
+
|
|
28
|
+
/** Make a fixture root containing one or more fake extensions, return its path. */
|
|
29
|
+
function fixture(build: (root: string) => void): string {
|
|
30
|
+
const root = join(base, `case-${n++}`);
|
|
31
|
+
mkdirSync(root, { recursive: true });
|
|
32
|
+
build(root);
|
|
33
|
+
return root;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
after(() => {
|
|
37
|
+
rmSync(base, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("scans a user-level, data-only memory store (no package.json)", () => {
|
|
41
|
+
const root = fixture((r) => {
|
|
42
|
+
const ext = join(r, "pi-hermes-memory");
|
|
43
|
+
mkdirSync(ext, { recursive: true });
|
|
44
|
+
// No package.json, no source — just pi's memory-store signature.
|
|
45
|
+
writeFileSync(join(ext, "MEMORY.md"), "# memory\n");
|
|
46
|
+
writeFileSync(join(ext, "sessions.db"), "");
|
|
47
|
+
});
|
|
48
|
+
process.env.MEGACOMPACT_EXT_SCAN_DIR = root;
|
|
49
|
+
try {
|
|
50
|
+
const { conflicts } = detectConflicts();
|
|
51
|
+
assert.ok(conflicts.length >= 1, "expected a memory conflict");
|
|
52
|
+
const hit = conflicts.find((c) => c.kind === "memory");
|
|
53
|
+
assert.ok(hit, "expected a memory-kind conflict");
|
|
54
|
+
assert.equal(hit!.severity, "high");
|
|
55
|
+
assert.ok(
|
|
56
|
+
hit!.evidence.includes("MEMORY.md") ||
|
|
57
|
+
hit!.evidence.includes("sessions.db"),
|
|
58
|
+
"evidence should name the on-disk memory signature",
|
|
59
|
+
);
|
|
60
|
+
} finally {
|
|
61
|
+
delete process.env.MEGACOMPACT_EXT_SCAN_DIR;
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("still detects a code extension by source marker (regression)", () => {
|
|
66
|
+
const root = fixture((r) => {
|
|
67
|
+
// A code extension is a DIRECT child of the scan root (mirrors the
|
|
68
|
+
// node_modules layout: packages live one level under the root).
|
|
69
|
+
const ext = join(r, "some-memory-ext");
|
|
70
|
+
mkdirSync(ext, { recursive: true });
|
|
71
|
+
writeFileSync(
|
|
72
|
+
join(ext, "package.json"),
|
|
73
|
+
JSON.stringify({ name: "some-memory-ext", pi: { extensions: ["x.ts"] } }),
|
|
74
|
+
);
|
|
75
|
+
writeFileSync(join(ext, "index.ts"), "export const MEMORY_TOOL = true;");
|
|
76
|
+
});
|
|
77
|
+
process.env.MEGACOMPACT_EXT_SCAN_DIR = root;
|
|
78
|
+
try {
|
|
79
|
+
const { conflicts } = detectConflicts();
|
|
80
|
+
const hit = conflicts.find((c) => c.package === "some-memory-ext");
|
|
81
|
+
assert.ok(hit, "expected some-memory-ext to be flagged");
|
|
82
|
+
assert.equal(hit!.kind, "memory");
|
|
83
|
+
} finally {
|
|
84
|
+
delete process.env.MEGACOMPACT_EXT_SCAN_DIR;
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("skips pi-mega-compact (selfName) and non-extension dirs", () => {
|
|
89
|
+
const root = fixture((r) => {
|
|
90
|
+
// selfName dir with a memory signature — must be ignored.
|
|
91
|
+
const me = join(r, "node_modules", "pi-mega-compact");
|
|
92
|
+
mkdirSync(me, { recursive: true });
|
|
93
|
+
writeFileSync(join(me, "sessions.db"), "");
|
|
94
|
+
// unrelated dir with no pi.extensions and no memory signature.
|
|
95
|
+
mkdirSync(join(r, "node_modules", "totally-fine"), { recursive: true });
|
|
96
|
+
});
|
|
97
|
+
process.env.MEGACOMPACT_EXT_SCAN_DIR = root;
|
|
98
|
+
try {
|
|
99
|
+
const { scanned, conflicts } = detectConflicts();
|
|
100
|
+
assert.equal(conflicts.length, 0, "no conflicts expected");
|
|
101
|
+
assert.ok(
|
|
102
|
+
!scanned.some((s) => s.includes("pi-mega-compact")),
|
|
103
|
+
"selfName should not appear in scanned",
|
|
104
|
+
);
|
|
105
|
+
} finally {
|
|
106
|
+
delete process.env.MEGACOMPACT_EXT_SCAN_DIR;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("collectScanRoots honors MEGACOMPACT_EXT_SCAN_DIR override", () => {
|
|
111
|
+
const root = fixture(() => {});
|
|
112
|
+
process.env.MEGACOMPACT_EXT_SCAN_DIR = root;
|
|
113
|
+
try {
|
|
114
|
+
const roots = collectScanRoots();
|
|
115
|
+
assert.deepEqual(roots, [root], "override replaces the whole root list");
|
|
116
|
+
} finally {
|
|
117
|
+
delete process.env.MEGACOMPACT_EXT_SCAN_DIR;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("collectScanRoots falls back to node_modules + user dir when no override", () => {
|
|
122
|
+
delete process.env.MEGACOMPACT_EXT_SCAN_DIR;
|
|
123
|
+
delete process.env.MEGACOMPACT_EXT_USER_DIR;
|
|
124
|
+
// No override set and this test file lives under extensions/, so node_modules
|
|
125
|
+
// resolution walks up from here; the user dir (~/.pi/agent) may or may
|
|
126
|
+
// not exist in CI. We only assert the call returns a non-throwing array.
|
|
127
|
+
const roots = collectScanRoots();
|
|
128
|
+
assert.ok(Array.isArray(roots), "collectScanRoots must return an array");
|
|
129
|
+
});
|
|
@@ -12,128 +12,173 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Pi-agnostic: reads package.json + greps source. No pi runtime types, so it is
|
|
14
14
|
* unit-testable against a fixture node_modules tree.
|
|
15
|
+
*
|
|
16
|
+
* SCAN-SCOPE FIX (S24 follow-up): the original scanner only walked the npm
|
|
17
|
+
* `node_modules` tree, so user-level extensions installed outside npm (e.g.
|
|
18
|
+
* `pi-hermes-memory`, a data-only `MEMORY.md` + `sessions.db` memory store)
|
|
19
|
+
* were never inspected — that gap let the 5000-char file-buffer error slip
|
|
20
|
+
* through undetected. We now also scan the user-level extension dir and detect
|
|
21
|
+
* memory stores that ship with no package.json / source to grep.
|
|
15
22
|
*/
|
|
16
23
|
|
|
17
24
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
18
25
|
import { join, dirname } from "node:path";
|
|
19
26
|
import { fileURLToPath } from "node:url";
|
|
27
|
+
import { homedir } from "node:os";
|
|
20
28
|
|
|
21
29
|
export type ConflictKind = "compaction" | "memory" | "tool-output";
|
|
22
30
|
export type ConflictSeverity = "high" | "info";
|
|
23
31
|
|
|
24
32
|
export interface ConflictHit {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
33
|
+
package: string;
|
|
34
|
+
severity: ConflictSeverity;
|
|
35
|
+
kind: ConflictKind;
|
|
36
|
+
evidence: string[];
|
|
37
|
+
/** One-line recommended action for the user. */
|
|
38
|
+
recommendation: string;
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
export interface ConflictReport {
|
|
34
|
-
|
|
35
|
-
|
|
42
|
+
scanned: string[];
|
|
43
|
+
conflicts: ConflictHit[];
|
|
36
44
|
}
|
|
37
45
|
|
|
38
46
|
// Marker sets. A package is flagged when its source matches a marker in a
|
|
39
47
|
// category. File-grep (not AST) keeps this dependency-free and fast.
|
|
40
48
|
const MARKERS = {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
"tool_result",
|
|
62
|
-
"ToolResult",
|
|
63
|
-
],
|
|
49
|
+
// Directly competes with our conversation compaction.
|
|
50
|
+
compaction: [
|
|
51
|
+
"session_before_compact",
|
|
52
|
+
"session_compact",
|
|
53
|
+
"compactSession",
|
|
54
|
+
"autoCompact",
|
|
55
|
+
"auto_compact",
|
|
56
|
+
],
|
|
57
|
+
// Saves durable memory to its own store — the takeover target.
|
|
58
|
+
memory: [
|
|
59
|
+
"MEMORY_TOOL",
|
|
60
|
+
"learn-memory",
|
|
61
|
+
"saveMemory",
|
|
62
|
+
"memoryPolicy",
|
|
63
|
+
"wal_checkpoint",
|
|
64
|
+
"store/db.ts",
|
|
65
|
+
"memoryTool",
|
|
66
|
+
],
|
|
67
|
+
// Tool-output shaping (compact/summarize tool results) — overlap, not a rival.
|
|
68
|
+
toolOutput: ["tool_result", "ToolResult"],
|
|
64
69
|
} as const;
|
|
65
70
|
|
|
66
71
|
/** Resolve the node_modules dir that contains this package (or env override). */
|
|
67
|
-
export function resolveExtensionRoot(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
export function resolveExtensionRoot(
|
|
73
|
+
selfDir: string = dirname(fileURLToPath(import.meta.url)),
|
|
74
|
+
): string | null {
|
|
75
|
+
const override = process.env.MEGACOMPACT_EXT_SCAN_DIR;
|
|
76
|
+
if (override && override.trim() !== "") return override;
|
|
77
|
+
// selfDir is <root>/extensions or <root>/dist/extensions. Walk up to the
|
|
78
|
+
// node_modules that holds pi-mega-compact.
|
|
79
|
+
let dir = selfDir;
|
|
80
|
+
for (let i = 0; i < 6; i++) {
|
|
81
|
+
const candidate = join(dir, "node_modules");
|
|
82
|
+
if (existsSync(candidate) && existsSync(join(candidate, "pi-mega-compact")))
|
|
83
|
+
return candidate;
|
|
84
|
+
const parent = dirname(dir);
|
|
85
|
+
if (parent === dir) break;
|
|
86
|
+
dir = parent;
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve every directory that may hold pi extensions to scan.
|
|
93
|
+
*
|
|
94
|
+
* - `MEGACOMPACT_EXT_SCAN_DIR` (if set) replaces the whole list — a single
|
|
95
|
+
* fixture/override root for tests or custom layouts.
|
|
96
|
+
* - Otherwise: the node_modules that holds this package (classic npm layout) AND
|
|
97
|
+
* the user-level extension dir (`~/.pi/agent`), which is where extensions
|
|
98
|
+
* installed outside npm actually live. The original scanner only walked
|
|
99
|
+
* node_modules, so user-level memory extensions were never flagged — that is
|
|
100
|
+
* the gap that let the 5000-char `MEMORY.md` buffer error slip through.
|
|
101
|
+
*/
|
|
102
|
+
export function collectScanRoots(): string[] {
|
|
103
|
+
const override = process.env.MEGACOMPACT_EXT_SCAN_DIR;
|
|
104
|
+
if (override && override.trim() !== "") return [override];
|
|
105
|
+
const roots: string[] = [];
|
|
106
|
+
const nm = resolveExtensionRoot();
|
|
107
|
+
if (nm && existsSync(nm)) roots.push(nm);
|
|
108
|
+
const userDir =
|
|
109
|
+
process.env.MEGACOMPACT_EXT_USER_DIR?.trim() ||
|
|
110
|
+
join(homedir(), ".pi", "agent");
|
|
111
|
+
if (existsSync(userDir)) roots.push(userDir);
|
|
112
|
+
return roots;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* True when a directory is a pi memory-store container rather than a normal code
|
|
117
|
+
* extension. `pi-hermes-memory` ships as exactly this: no package.json, no
|
|
118
|
+
* source — just `MEMORY.md` + `sessions.db`. The marker-grep path misses it,
|
|
119
|
+
* so we also detect the on-disk memory signature.
|
|
120
|
+
*/
|
|
121
|
+
function isMemoryStoreDir(pkgDir: string): boolean {
|
|
122
|
+
return (
|
|
123
|
+
existsSync(join(pkgDir, "sessions.db")) ||
|
|
124
|
+
existsSync(join(pkgDir, "MEMORY.md"))
|
|
125
|
+
);
|
|
81
126
|
}
|
|
82
127
|
|
|
83
128
|
/** Recursively collect source-ish files under a package, capped to avoid scans. */
|
|
84
129
|
function collectFiles(root: string, max = 400): string[] {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
130
|
+
const out: string[] = [];
|
|
131
|
+
const walk = (dir: string): void => {
|
|
132
|
+
if (out.length >= max) return;
|
|
133
|
+
let entries: string[];
|
|
134
|
+
try {
|
|
135
|
+
entries = readdirSync(dir);
|
|
136
|
+
} catch {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
for (const e of entries) {
|
|
140
|
+
if (out.length >= max) return;
|
|
141
|
+
const full = join(dir, e);
|
|
142
|
+
let st;
|
|
143
|
+
try {
|
|
144
|
+
st = statSync(full);
|
|
145
|
+
} catch {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (st.isDirectory()) {
|
|
149
|
+
if (e === "node_modules" || e === ".git") continue;
|
|
150
|
+
walk(full);
|
|
151
|
+
} else if (/\.(ts|js|mjs|cjs|json|md)$/.test(e)) {
|
|
152
|
+
out.push(full);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
walk(root);
|
|
157
|
+
return out;
|
|
113
158
|
}
|
|
114
159
|
|
|
115
160
|
/** Grep a package's source for any marker in `keys`; return matched markers. */
|
|
116
161
|
function matchMarkers(pkgDir: string, keys: readonly string[]): string[] {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
162
|
+
const found = new Set<string>();
|
|
163
|
+
let files: string[];
|
|
164
|
+
try {
|
|
165
|
+
files = collectFiles(pkgDir);
|
|
166
|
+
} catch {
|
|
167
|
+
return [];
|
|
168
|
+
}
|
|
169
|
+
for (const f of files) {
|
|
170
|
+
let text: string;
|
|
171
|
+
try {
|
|
172
|
+
text = readFileSync(f, "utf-8");
|
|
173
|
+
} catch {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
for (const m of keys) {
|
|
177
|
+
if (text.includes(m)) found.add(m);
|
|
178
|
+
}
|
|
179
|
+
if (found.size === keys.length) break;
|
|
180
|
+
}
|
|
181
|
+
return [...found];
|
|
137
182
|
}
|
|
138
183
|
|
|
139
184
|
/**
|
|
@@ -141,69 +186,109 @@ function matchMarkers(pkgDir: string, keys: readonly string[]): string[] {
|
|
|
141
186
|
* @param selfName package name to skip (defaults to this package's name).
|
|
142
187
|
*/
|
|
143
188
|
export function detectConflicts(selfName = "pi-mega-compact"): ConflictReport {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
189
|
+
const roots = collectScanRoots();
|
|
190
|
+
const scanned: string[] = [];
|
|
191
|
+
const conflicts: ConflictHit[] = [];
|
|
192
|
+
|
|
193
|
+
for (const root of roots) {
|
|
194
|
+
if (!existsSync(root)) continue;
|
|
195
|
+
let entries: string[];
|
|
196
|
+
try {
|
|
197
|
+
entries = readdirSync(root);
|
|
198
|
+
} catch {
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const name of entries) {
|
|
203
|
+
const pkgDir = join(root, name);
|
|
204
|
+
let st;
|
|
205
|
+
try {
|
|
206
|
+
st = statSync(pkgDir);
|
|
207
|
+
} catch {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (!st.isDirectory()) continue;
|
|
211
|
+
|
|
212
|
+
// A candidate is either a real code extension (declares pi.extensions) or a
|
|
213
|
+
// data-only memory store (MEMORY.md / sessions.db at its root).
|
|
214
|
+
const pkgJson = join(pkgDir, "package.json");
|
|
215
|
+
let pkg: { name?: string; pi?: { extensions?: string[] } } | null = null;
|
|
216
|
+
if (existsSync(pkgJson)) {
|
|
217
|
+
try {
|
|
218
|
+
pkg = JSON.parse(readFileSync(pkgJson, "utf-8"));
|
|
219
|
+
} catch {
|
|
220
|
+
pkg = null;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const isCodeExt =
|
|
224
|
+
!!pkg &&
|
|
225
|
+
!!pkg.pi &&
|
|
226
|
+
Array.isArray(pkg.pi.extensions) &&
|
|
227
|
+
pkg.pi.extensions.length > 0;
|
|
228
|
+
const isMemoryStore = isMemoryStoreDir(pkgDir);
|
|
229
|
+
if (!isCodeExt && !isMemoryStore) continue;
|
|
230
|
+
|
|
231
|
+
const pkgName = pkg?.name ?? name;
|
|
232
|
+
if (pkgName === selfName) continue;
|
|
233
|
+
scanned.push(`${pkgName} (${name})`);
|
|
234
|
+
|
|
235
|
+
if (isCodeExt) {
|
|
236
|
+
const memHits = matchMarkers(pkgDir, MARKERS.memory);
|
|
237
|
+
const compHits = matchMarkers(pkgDir, MARKERS.compaction);
|
|
238
|
+
const toolHits = matchMarkers(pkgDir, MARKERS.toolOutput);
|
|
239
|
+
|
|
240
|
+
if (compHits.length > 0) {
|
|
241
|
+
conflicts.push({
|
|
242
|
+
package: pkgName,
|
|
243
|
+
severity: "high",
|
|
244
|
+
kind: "compaction",
|
|
245
|
+
evidence: compHits,
|
|
246
|
+
recommendation:
|
|
247
|
+
"Disabling recommended — competes with pi-mega-compact's conversation compaction.",
|
|
248
|
+
});
|
|
249
|
+
continue; // compaction is the dominant conflict; don't double-flag.
|
|
250
|
+
}
|
|
251
|
+
if (memHits.length > 0) {
|
|
252
|
+
conflicts.push({
|
|
253
|
+
package: pkgName,
|
|
254
|
+
severity: "high",
|
|
255
|
+
kind: "memory",
|
|
256
|
+
evidence: memHits,
|
|
257
|
+
recommendation:
|
|
258
|
+
"pi-mega-compact now owns save-to-memory (/mega-memory, its own SQLite). Disable this to avoid duplicate memory stores.",
|
|
259
|
+
});
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (toolHits.length > 0) {
|
|
263
|
+
conflicts.push({
|
|
264
|
+
package: pkgName,
|
|
265
|
+
severity: "info",
|
|
266
|
+
kind: "tool-output",
|
|
267
|
+
evidence: toolHits,
|
|
268
|
+
recommendation:
|
|
269
|
+
"Shapes tool output (summarize/compact tool results). Generally compatible; no action needed.",
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Data-only memory store: no source to grep, but the on-disk signature
|
|
275
|
+
// (sessions.db / MEMORY.md) is a conflict with our SQLite memory store.
|
|
276
|
+
if (isMemoryStore) {
|
|
277
|
+
const evidence: string[] = [];
|
|
278
|
+
if (existsSync(join(pkgDir, "sessions.db")))
|
|
279
|
+
evidence.push("sessions.db");
|
|
280
|
+
if (existsSync(join(pkgDir, "MEMORY.md"))) evidence.push("MEMORY.md");
|
|
281
|
+
conflicts.push({
|
|
282
|
+
package: pkgName,
|
|
283
|
+
severity: "high",
|
|
284
|
+
kind: "memory",
|
|
285
|
+
evidence,
|
|
286
|
+
recommendation:
|
|
287
|
+
"pi-mega-compact now owns save-to-memory (its own SQLite). This data-only memory store competes with it — disable to avoid a duplicate / capped memory buffer.",
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return { scanned, conflicts };
|
|
209
294
|
}
|
|
@@ -140,11 +140,11 @@ describe("multi-repo /api/index (S19)", () => {
|
|
|
140
140
|
|
|
141
141
|
const { upsertRepoRegistry } = await import("../src/store/sqlite.js");
|
|
142
142
|
upsertRepoRegistry(
|
|
143
|
-
{ repoRoot: "/home/u/repoA", displayName: "repoA", stateDir: dir, checkpointCount: 3, tokensSaved: 1000, compressedOriginalBytes: 0 },
|
|
143
|
+
{ repoRoot: "/home/u/repoA", displayName: "repoA", stateDir: join(dir, "a"), checkpointCount: 3, tokensSaved: 1000, compressedOriginalBytes: 0 },
|
|
144
144
|
indexDir,
|
|
145
145
|
);
|
|
146
146
|
upsertRepoRegistry(
|
|
147
|
-
{ repoRoot: "/home/u/repoB", displayName: "repoB", stateDir: dir, checkpointCount: 5, tokensSaved: 2000, compressedOriginalBytes: 0 },
|
|
147
|
+
{ repoRoot: "/home/u/repoB", displayName: "repoB", stateDir: join(dir, "b"), checkpointCount: 5, tokensSaved: 2000, compressedOriginalBytes: 0 },
|
|
148
148
|
indexDir,
|
|
149
149
|
);
|
|
150
150
|
|