pi-mega-compact 0.6.2 → 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/README.md +39 -23
- package/dist/extensions/conflict-scan.js +133 -60
- package/dist/extensions/conflict-scan.test.js +115 -0
- 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/dist/src/store/memoryIndex.js +32 -7
- package/dist/src/store/vectorIndex.js +32 -7
- 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
- package/src/store/memoryIndex.ts +40 -6
- package/src/store/vectorIndex.ts +40 -6
- package/dist/extensions/openclaw-mega-compact.js +0 -291
- package/dist/src/minilm.js +0 -92
- package/dist/src/wordpiece.js +0 -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
|
|