@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.
Files changed (33) hide show
  1. package/dist/core/agent-session.d.ts.map +1 -1
  2. package/dist/core/agent-session.js +13 -0
  3. package/dist/core/agent-session.js.map +1 -1
  4. package/dist/extensions/auto-memory/__tests__/extract-result.test.ts +42 -0
  5. package/dist/extensions/auto-memory/__tests__/prefetch-history.test.ts +136 -0
  6. package/dist/extensions/auto-memory/__tests__/prompts.test.ts +29 -0
  7. package/dist/extensions/auto-memory/__tests__/skip-rules.test.ts +366 -0
  8. package/dist/extensions/auto-memory/contract.d.ts +16 -0
  9. package/dist/extensions/auto-memory/contract.d.ts.map +1 -1
  10. package/dist/extensions/auto-memory/contract.js.map +1 -1
  11. package/dist/extensions/auto-memory/contract.ts +16 -0
  12. package/dist/extensions/auto-memory/index.ts +134 -13
  13. package/dist/extensions/auto-memory/prompts.ts +10 -0
  14. package/dist/extensions/auto-memory/skip-rules.ts +2 -0
  15. package/dist/extensions/bash-ext/index.ts +855 -845
  16. package/dist/extensions/claude-hooks-compat/index.ts +12 -7
  17. package/dist/extensions/coordinator/handler.test.ts +388 -123
  18. package/dist/extensions/coordinator/handler.ts +78 -12
  19. package/dist/extensions/coordinator/index.ts +267 -198
  20. package/dist/extensions/coordinator/types.d.ts +16 -0
  21. package/dist/extensions/coordinator/types.d.ts.map +1 -1
  22. package/dist/extensions/coordinator/types.js.map +1 -1
  23. package/dist/extensions/coordinator/types.ts +57 -49
  24. package/dist/extensions/lsp/lsp/index.ts +15 -9
  25. package/dist/extensions/lsp/lsp/lsp-clangd-e2e.test.ts +229 -0
  26. package/dist/extensions/message-bridge/index.ts +14 -11
  27. package/dist/extensions/session-supervisor/index.ts +14 -8
  28. package/dist/extensions/subagent-v2/index.ts +58 -42
  29. package/dist/extensions/todo-ext/index.ts +7 -3
  30. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  31. package/dist/modes/rpc/rpc-mode.js +9 -1
  32. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  33. 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.slice(-3);
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<{ created: string[]; updated: string[] } | null> {
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<{ created: string[]; updated: string[] } | null> {
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<{ created: string[]; updated: string[] }> {
461
- const created: string[] = [];
462
- const updated: string[] = [];
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
- updated.push(filename);
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: ${e instanceof Error ? e.message : String(e)}`, "warning");
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"],
@@ -16,6 +16,8 @@ export interface HistoryEntry {
16
16
  skip_hits: string[];
17
17
  guard_hits: string[];
18
18
  timestamp: number;
19
+ userMarkedIrrelevant?: boolean;
20
+ irrelevantFiles?: string[];
19
21
  }
20
22
 
21
23
  export interface SkipWordStore {