@dyyz1993/pi-coding-agent 0.74.45 → 0.74.47
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/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +13 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/extensions/auto-memory/__tests__/extract-result.test.ts +42 -0
- package/dist/extensions/auto-memory/__tests__/prefetch-history.test.ts +136 -0
- package/dist/extensions/auto-memory/__tests__/prompts.test.ts +29 -0
- package/dist/extensions/auto-memory/__tests__/skip-rules.test.ts +366 -0
- package/dist/extensions/auto-memory/contract.d.ts +16 -0
- package/dist/extensions/auto-memory/contract.d.ts.map +1 -1
- package/dist/extensions/auto-memory/contract.js.map +1 -1
- package/dist/extensions/auto-memory/contract.ts +16 -0
- package/dist/extensions/auto-memory/index.ts +134 -13
- package/dist/extensions/auto-memory/prompts.ts +10 -0
- package/dist/extensions/auto-memory/skip-rules.ts +2 -0
- package/dist/extensions/bash-ext/index.ts +855 -845
- package/dist/extensions/claude-hooks-compat/index.ts +12 -7
- package/dist/extensions/coordinator/handler.test.ts +388 -123
- package/dist/extensions/coordinator/handler.ts +78 -12
- package/dist/extensions/coordinator/index.ts +267 -198
- package/dist/extensions/coordinator/types.d.ts +16 -0
- package/dist/extensions/coordinator/types.d.ts.map +1 -1
- package/dist/extensions/coordinator/types.js.map +1 -1
- package/dist/extensions/coordinator/types.ts +57 -49
- package/dist/extensions/lsp/lsp/index.ts +15 -9
- package/dist/extensions/lsp/lsp/lsp-clangd-e2e.test.ts +229 -0
- package/dist/extensions/message-bridge/index.ts +14 -11
- package/dist/extensions/session-supervisor/index.ts +14 -8
- package/dist/extensions/subagent-v2/index.ts +58 -42
- package/dist/extensions/todo-ext/index.ts +7 -3
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +9 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/package.json +1 -1
|
@@ -92,6 +92,8 @@ export function buildPrefetchUserMessage(query: string, manifest: string, rules:
|
|
|
92
92
|
query: h.query,
|
|
93
93
|
selected: h.selected,
|
|
94
94
|
skipped: h.skipped,
|
|
95
|
+
userMarkedIrrelevant: h.userMarkedIrrelevant ?? false,
|
|
96
|
+
irrelevantFiles: h.irrelevantFiles ?? [],
|
|
95
97
|
})),
|
|
96
98
|
);
|
|
97
99
|
|
|
@@ -101,7 +103,7 @@ export function buildPrefetchUserMessage(query: string, manifest: string, rules:
|
|
|
101
103
|
interface PrefetchDebugInfo {
|
|
102
104
|
selectedFiles: string[];
|
|
103
105
|
durationMs: number;
|
|
104
|
-
layer: "skip" | "llm" | "none";
|
|
106
|
+
layer: "skip" | "llm" | "none" | "auto";
|
|
105
107
|
skipHits: Array<{ pattern: string; mode: string }>;
|
|
106
108
|
guardHits: Array<{ pattern: string; mode: string }>;
|
|
107
109
|
availableFiles: number;
|
|
@@ -121,6 +123,8 @@ class MemoryPrefetch {
|
|
|
121
123
|
private _debugInfo: PrefetchDebugInfo | null = null;
|
|
122
124
|
private lastPrefetchTime = 0;
|
|
123
125
|
private consecutiveSameCount = 0;
|
|
126
|
+
private cachedFileCount = -1;
|
|
127
|
+
private dirtyFiles = true;
|
|
124
128
|
|
|
125
129
|
get debugInfo(): PrefetchDebugInfo | null {
|
|
126
130
|
return this._debugInfo;
|
|
@@ -132,10 +136,15 @@ class MemoryPrefetch {
|
|
|
132
136
|
return false;
|
|
133
137
|
}
|
|
134
138
|
|
|
139
|
+
markDirty(): void {
|
|
140
|
+
this.dirtyFiles = true;
|
|
141
|
+
}
|
|
142
|
+
|
|
135
143
|
start(query: string, memoryDir: string, callLLM: CallLLMFn): void {
|
|
136
144
|
const now = Date.now();
|
|
137
145
|
const elapsed = now - this.lastPrefetchTime;
|
|
138
146
|
|
|
147
|
+
// Layer 0: 30s 内复用上次结果
|
|
139
148
|
if (this.lastPrefetchTime > 0 && elapsed < PREFETCH_MIN_INTERVAL_MS) {
|
|
140
149
|
this._debugInfo = {
|
|
141
150
|
selectedFiles: this.lastSelected,
|
|
@@ -143,7 +152,7 @@ class MemoryPrefetch {
|
|
|
143
152
|
layer: "skip",
|
|
144
153
|
skipHits: [{ pattern: `min-interval(${Math.round(elapsed / 1000)}s<${PREFETCH_MIN_INTERVAL_MS / 1000}s)`, mode: "builtin" }],
|
|
145
154
|
guardHits: [],
|
|
146
|
-
availableFiles: 0,
|
|
155
|
+
availableFiles: this.cachedFileCount >= 0 ? this.cachedFileCount : 0,
|
|
147
156
|
query: query.slice(0, 200),
|
|
148
157
|
};
|
|
149
158
|
this.settled = true;
|
|
@@ -153,6 +162,28 @@ class MemoryPrefetch {
|
|
|
153
162
|
return;
|
|
154
163
|
}
|
|
155
164
|
|
|
165
|
+
// Layer 1: 文件少且没变化 → 直接全注入,不调 LLM
|
|
166
|
+
if (!this.dirtyFiles && this.cachedFileCount >= 0 && this.cachedFileCount <= MAX_RELEVANT_MEMORIES && this.result !== null) {
|
|
167
|
+
this._debugInfo = {
|
|
168
|
+
selectedFiles: this.lastSelected,
|
|
169
|
+
durationMs: 0,
|
|
170
|
+
layer: "auto",
|
|
171
|
+
skipHits: [{ pattern: `all-cached(${this.cachedFileCount}f)`, mode: "builtin" }],
|
|
172
|
+
guardHits: [],
|
|
173
|
+
availableFiles: this.cachedFileCount,
|
|
174
|
+
query: query.slice(0, 200),
|
|
175
|
+
};
|
|
176
|
+
this.settled = true;
|
|
177
|
+
this.resultEntryWritten = false;
|
|
178
|
+
this.lastPrefetchTime = now;
|
|
179
|
+
this.promise = this.runReadCached(this.lastSelected, memoryDir);
|
|
180
|
+
void this.promise.then((r) => {
|
|
181
|
+
this.result = r;
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Layer 2: 连续相同结果检测
|
|
156
187
|
if (this.consecutiveSameCount >= PREFETCH_REPEAT_THRESHOLD && this.lastSelected.length > 0) {
|
|
157
188
|
this._debugInfo = {
|
|
158
189
|
selectedFiles: this.lastSelected,
|
|
@@ -160,7 +191,7 @@ class MemoryPrefetch {
|
|
|
160
191
|
layer: "skip",
|
|
161
192
|
skipHits: [{ pattern: `repeat-detect(${this.consecutiveSameCount}x)`, mode: "builtin" }],
|
|
162
193
|
guardHits: [],
|
|
163
|
-
availableFiles: 0,
|
|
194
|
+
availableFiles: this.cachedFileCount >= 0 ? this.cachedFileCount : 0,
|
|
164
195
|
query: query.slice(0, 200),
|
|
165
196
|
};
|
|
166
197
|
this.settled = true;
|
|
@@ -173,6 +204,7 @@ class MemoryPrefetch {
|
|
|
173
204
|
return;
|
|
174
205
|
}
|
|
175
206
|
|
|
207
|
+
// Layer 3: skip/guard 规则
|
|
176
208
|
const store = this.ensureStore();
|
|
177
209
|
const { shouldSkip, skipHits, guardHits } = evaluateRules(query, store.rules);
|
|
178
210
|
if (shouldSkip) {
|
|
@@ -188,7 +220,7 @@ class MemoryPrefetch {
|
|
|
188
220
|
layer: "skip",
|
|
189
221
|
skipHits: matchedSkip,
|
|
190
222
|
guardHits: matchedGuard,
|
|
191
|
-
availableFiles: 0,
|
|
223
|
+
availableFiles: this.cachedFileCount >= 0 ? this.cachedFileCount : 0,
|
|
192
224
|
query: query.slice(0, 200),
|
|
193
225
|
};
|
|
194
226
|
this.settled = true;
|
|
@@ -199,6 +231,7 @@ class MemoryPrefetch {
|
|
|
199
231
|
return;
|
|
200
232
|
}
|
|
201
233
|
|
|
234
|
+
// Layer 4: 走 LLM(或 auto-inject)
|
|
202
235
|
this.lastPrefetchTime = now;
|
|
203
236
|
this.settled = false;
|
|
204
237
|
this.result = null;
|
|
@@ -248,7 +281,10 @@ class MemoryPrefetch {
|
|
|
248
281
|
const matchedGuard = matchedRules.filter((r) => r.action !== "skip").map(({ pattern, mode }) => ({ pattern, mode }));
|
|
249
282
|
|
|
250
283
|
const memories = await scanMemoryFiles(memoryDir);
|
|
284
|
+
this.cachedFileCount = memories.length;
|
|
285
|
+
|
|
251
286
|
if (memories.length === 0) {
|
|
287
|
+
this.dirtyFiles = false;
|
|
252
288
|
this._debugInfo = {
|
|
253
289
|
selectedFiles: [],
|
|
254
290
|
durationMs: 0,
|
|
@@ -261,8 +297,25 @@ class MemoryPrefetch {
|
|
|
261
297
|
return "";
|
|
262
298
|
}
|
|
263
299
|
|
|
300
|
+
// Auto-inject: 文件少 → 全部注入,不调 LLM
|
|
301
|
+
if (memories.length <= MAX_RELEVANT_MEMORIES) {
|
|
302
|
+
const allFiles = memories.map((m) => m.filename);
|
|
303
|
+
this.lastSelected = allFiles;
|
|
304
|
+
this.dirtyFiles = false;
|
|
305
|
+
this._debugInfo = {
|
|
306
|
+
selectedFiles: allFiles,
|
|
307
|
+
durationMs: 0,
|
|
308
|
+
layer: "auto",
|
|
309
|
+
skipHits: matchedSkip,
|
|
310
|
+
guardHits: matchedGuard,
|
|
311
|
+
availableFiles: memories.length,
|
|
312
|
+
query: query.slice(0, 200),
|
|
313
|
+
};
|
|
314
|
+
return await this.readFiles(allFiles, memoryDir);
|
|
315
|
+
}
|
|
316
|
+
|
|
264
317
|
const manifest = formatManifest(memories);
|
|
265
|
-
const recentHistory = store.history
|
|
318
|
+
const recentHistory = this.buildHistoryForLLM(store.history);
|
|
266
319
|
const startTime = Date.now();
|
|
267
320
|
|
|
268
321
|
const llmResult = await callLLM({
|
|
@@ -370,6 +423,25 @@ class MemoryPrefetch {
|
|
|
370
423
|
const setB = new Set(b);
|
|
371
424
|
return a.every((item) => setB.has(item));
|
|
372
425
|
}
|
|
426
|
+
|
|
427
|
+
private buildHistoryForLLM(history: HistoryEntry[]): HistoryEntry[] {
|
|
428
|
+
const recent = history.slice(-3);
|
|
429
|
+
const marked = history.filter((h) => h.userMarkedIrrelevant);
|
|
430
|
+
const seen = new Set(recent.map((h) => h.timestamp));
|
|
431
|
+
const extra = marked.filter((h) => !seen.has(h.timestamp)).slice(-5);
|
|
432
|
+
return [...recent, ...extra].sort((a, b) => a.timestamp - b.timestamp);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
interface MemoryFileEntry {
|
|
437
|
+
filename: string;
|
|
438
|
+
name: string;
|
|
439
|
+
description: string;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
interface ExtractResult {
|
|
443
|
+
created: MemoryFileEntry[];
|
|
444
|
+
updated: MemoryFileEntry[];
|
|
373
445
|
}
|
|
374
446
|
|
|
375
447
|
class MemoryExtractor {
|
|
@@ -392,7 +464,7 @@ class MemoryExtractor {
|
|
|
392
464
|
messages: AgentMessage[],
|
|
393
465
|
memoryDir: string,
|
|
394
466
|
callLLM: CallLLMFn,
|
|
395
|
-
): Promise<
|
|
467
|
+
): Promise<ExtractResult | null> {
|
|
396
468
|
if (this.inProgress) {
|
|
397
469
|
this.pendingMessages = messages;
|
|
398
470
|
return null;
|
|
@@ -415,7 +487,7 @@ class MemoryExtractor {
|
|
|
415
487
|
messages: AgentMessage[],
|
|
416
488
|
memoryDir: string,
|
|
417
489
|
callLLM: CallLLMFn,
|
|
418
|
-
): Promise<
|
|
490
|
+
): Promise<ExtractResult | null> {
|
|
419
491
|
this.inProgress = true;
|
|
420
492
|
try {
|
|
421
493
|
const recent = serializeMessages(messages, { lastN: 20 });
|
|
@@ -457,9 +529,9 @@ class MemoryExtractor {
|
|
|
457
529
|
private async applyActions(
|
|
458
530
|
actions: Array<Record<string, string>>,
|
|
459
531
|
memoryDir: string,
|
|
460
|
-
): Promise<
|
|
461
|
-
const created:
|
|
462
|
-
const updated:
|
|
532
|
+
): Promise<ExtractResult> {
|
|
533
|
+
const created: MemoryFileEntry[] = [];
|
|
534
|
+
const updated: MemoryFileEntry[] = [];
|
|
463
535
|
for (const action of actions) {
|
|
464
536
|
const op = action.op;
|
|
465
537
|
|
|
@@ -474,7 +546,7 @@ class MemoryExtractor {
|
|
|
474
546
|
const fm = buildFrontmatter({ name, description, type });
|
|
475
547
|
const body = content.slice(0, MAX_MEMORY_BYTES_PER_FILE);
|
|
476
548
|
await writeFile(join(memoryDir, filename), `${fm}\n\n${body}`);
|
|
477
|
-
created.push(filename);
|
|
549
|
+
created.push({ filename, name, description });
|
|
478
550
|
} else if (op === "update") {
|
|
479
551
|
const filename = action.filename;
|
|
480
552
|
const append = action.append;
|
|
@@ -485,7 +557,9 @@ class MemoryExtractor {
|
|
|
485
557
|
|
|
486
558
|
const existing = await readFile(filePath, "utf-8");
|
|
487
559
|
await writeFile(filePath, existing + append);
|
|
488
|
-
|
|
560
|
+
const name = action.name ?? filename;
|
|
561
|
+
const description = action.description ?? append.slice(0, 80);
|
|
562
|
+
updated.push({ filename, name, description });
|
|
489
563
|
}
|
|
490
564
|
}
|
|
491
565
|
await updateMemoryIndex(memoryDir);
|
|
@@ -779,6 +853,7 @@ export default function autoMemoryExtension(pi: ExtensionAPI): void {
|
|
|
779
853
|
return await pi.callLLM(opts);
|
|
780
854
|
} catch (err) {
|
|
781
855
|
const msg = err instanceof Error ? err.message : String(err);
|
|
856
|
+
if (/stale/i.test(msg)) throw err;
|
|
782
857
|
const isRateLimit = /429|rate.?limit|too.?many.?request|quota/i.test(msg);
|
|
783
858
|
if (!isRateLimit || attempt >= MAX_RETRIES) throw err;
|
|
784
859
|
console.error(`[callLLM] rate limited (attempt ${attempt + 1}/${MAX_RETRIES}), retrying in ${RETRY_DELAY_MS}ms`);
|
|
@@ -871,6 +946,7 @@ export default function autoMemoryExtension(pi: ExtensionAPI): void {
|
|
|
871
946
|
status("extracting memories...");
|
|
872
947
|
const extractResult = await extractor.maybeExtract(event.messages, memoryDir, callLLMWithRetry);
|
|
873
948
|
if (extractResult) {
|
|
949
|
+
prefetch.markDirty();
|
|
874
950
|
pi.appendEntry("memory_extract", {
|
|
875
951
|
status: "completed",
|
|
876
952
|
created: extractResult.created,
|
|
@@ -881,6 +957,7 @@ export default function autoMemoryExtension(pi: ExtensionAPI): void {
|
|
|
881
957
|
status("consolidating memories...");
|
|
882
958
|
const dreamResult = await dream.maybeRun(memoryDir, callLLMWithRetry);
|
|
883
959
|
if (dreamResult) {
|
|
960
|
+
prefetch.markDirty();
|
|
884
961
|
pi.appendEntry("memory_dream", {
|
|
885
962
|
status: "completed",
|
|
886
963
|
merges: dreamResult.merges,
|
|
@@ -891,8 +968,12 @@ export default function autoMemoryExtension(pi: ExtensionAPI): void {
|
|
|
891
968
|
|
|
892
969
|
status("memory idle");
|
|
893
970
|
} catch (e) {
|
|
971
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
972
|
+
if (/stale/i.test(msg)) {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
894
975
|
status("memory error");
|
|
895
|
-
notify(`Auto-memory error: ${
|
|
976
|
+
notify(`Auto-memory error: ${msg}`, "warning");
|
|
896
977
|
}
|
|
897
978
|
})();
|
|
898
979
|
});
|
|
@@ -942,6 +1023,7 @@ export default function autoMemoryExtension(pi: ExtensionAPI): void {
|
|
|
942
1023
|
callLLMWithRetry,
|
|
943
1024
|
);
|
|
944
1025
|
if (result) {
|
|
1026
|
+
prefetch.markDirty();
|
|
945
1027
|
pi.appendEntry("memory_created", result);
|
|
946
1028
|
const updatedMemories = await scanMemoryFiles(memoryDir);
|
|
947
1029
|
memoryChannel.emit("memory_updated", {
|
|
@@ -960,10 +1042,49 @@ export default function autoMemoryExtension(pi: ExtensionAPI): void {
|
|
|
960
1042
|
}
|
|
961
1043
|
} catch (e) {
|
|
962
1044
|
const errMsg = e instanceof Error ? e.message : String(e);
|
|
1045
|
+
if (/stale/i.test(errMsg)) {
|
|
1046
|
+
return { ok: true };
|
|
1047
|
+
}
|
|
963
1048
|
pi.appendEntry("memory_failed", { reason: errMsg });
|
|
964
1049
|
notify(`Bookmark error: ${errMsg}`, "warning");
|
|
965
1050
|
memoryChannel.emit("memory_update_failed", { type: "memory_update_failed", reason: "Error" });
|
|
966
1051
|
}
|
|
967
1052
|
return { ok: true };
|
|
968
1053
|
});
|
|
1054
|
+
|
|
1055
|
+
memoryChannel.handle("memory.markIrrelevant", async (data) => {
|
|
1056
|
+
const query = (data.query ?? "").slice(0, 200);
|
|
1057
|
+
const selectedFiles = Array.isArray(data.selectedFiles)
|
|
1058
|
+
? (data.selectedFiles as string[]).slice(0, MAX_RELEVANT_MEMORIES)
|
|
1059
|
+
: [];
|
|
1060
|
+
|
|
1061
|
+
if (!query || selectedFiles.length === 0) {
|
|
1062
|
+
return { ok: false };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
let store = loadSkipWordStore(getGlobalMemoryDir());
|
|
1066
|
+
store = addHistoryEntry(store, {
|
|
1067
|
+
query,
|
|
1068
|
+
selected: selectedFiles,
|
|
1069
|
+
skipped: false,
|
|
1070
|
+
skip_hits: [],
|
|
1071
|
+
guard_hits: [],
|
|
1072
|
+
timestamp: Date.now(),
|
|
1073
|
+
userMarkedIrrelevant: true,
|
|
1074
|
+
irrelevantFiles: selectedFiles,
|
|
1075
|
+
});
|
|
1076
|
+
await saveSkipWordStore(getGlobalMemoryDir(), store);
|
|
1077
|
+
|
|
1078
|
+
pi.appendEntry("memory_irrelevant_marked", {
|
|
1079
|
+
query,
|
|
1080
|
+
selectedFiles,
|
|
1081
|
+
});
|
|
1082
|
+
memoryChannel.emit("memory_irrelevant_marked", {
|
|
1083
|
+
type: "memory_irrelevant_marked",
|
|
1084
|
+
query,
|
|
1085
|
+
selectedFiles,
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
return { ok: true };
|
|
1089
|
+
});
|
|
969
1090
|
}
|
|
@@ -41,6 +41,7 @@ export const SELECT_MEMORIES_PROMPT = `你是记忆系统的文件选择器 +
|
|
|
41
41
|
- 只选择确定有用的
|
|
42
42
|
- 最多 5 个
|
|
43
43
|
- 不确定就不选
|
|
44
|
+
- ⚠️ 注意 history 中 userMarkedIrrelevant=true 的条目:这些文件/查询组合已被用户确认为不相关,不要重复同样的错误选择
|
|
44
45
|
|
|
45
46
|
## 任务 2:关键词净化
|
|
46
47
|
|
|
@@ -67,6 +68,15 @@ export const SELECT_MEMORIES_PROMPT = `你是记忆系统的文件选择器 +
|
|
|
67
68
|
- 如果被跳过的那条 selected 合理
|
|
68
69
|
→ 该关键词可以保留
|
|
69
70
|
|
|
71
|
+
### 用户反馈净化(基于 userMarkedIrrelevant)
|
|
72
|
+
分析 history 中 userMarkedIrrelevant=true 的条目。
|
|
73
|
+
这些是用户明确标记"不相关"的 prefetch 结果:
|
|
74
|
+
- 如果多个不相关条目命中了同一个文件 → 该文件对这类查询无用
|
|
75
|
+
→ 生成 skip 规则(regex 或 contains)排除该查询模式
|
|
76
|
+
- 如果某类查询模式的所有结果都被标记不相关 → 该查询模式不需要记忆
|
|
77
|
+
→ 生成 skip 规则跳过该查询模式
|
|
78
|
+
- 至少需要 2 个不相关标记才能生成规则(避免单次误判)
|
|
79
|
+
|
|
70
80
|
## 回复格式(JSON only)
|
|
71
81
|
{
|
|
72
82
|
"selected": ["file1.md"],
|