@oyasmi/pipiclaw 0.5.1 → 0.5.3

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 (193) hide show
  1. package/README.md +308 -209
  2. package/dist/agent/channel-runner.d.ts +47 -0
  3. package/dist/agent/channel-runner.js +441 -0
  4. package/dist/agent/index.d.ts +3 -0
  5. package/dist/agent/index.js +2 -0
  6. package/dist/agent/progress-formatter.d.ts +4 -0
  7. package/dist/agent/progress-formatter.js +52 -0
  8. package/dist/agent/run-queue.d.ts +7 -0
  9. package/dist/agent/run-queue.js +26 -0
  10. package/dist/agent/runner-factory.d.ts +3 -0
  11. package/dist/agent/runner-factory.js +10 -0
  12. package/dist/agent/session-events.d.ts +14 -0
  13. package/dist/agent/session-events.js +215 -0
  14. package/dist/agent/session-resource-gate.d.ts +10 -0
  15. package/dist/agent/session-resource-gate.js +44 -0
  16. package/dist/agent/type-guards.d.ts +22 -0
  17. package/dist/agent/type-guards.js +106 -0
  18. package/dist/agent/types.d.ts +160 -0
  19. package/dist/agent/types.js +22 -0
  20. package/dist/agent.d.ts +2 -16
  21. package/dist/agent.js +1 -782
  22. package/dist/command-extension.d.ts +0 -1
  23. package/dist/command-extension.js +0 -1
  24. package/dist/commands.d.ts +0 -1
  25. package/dist/commands.js +0 -1
  26. package/dist/config-loader.d.ts +0 -1
  27. package/dist/config-loader.js +1 -2
  28. package/dist/context.d.ts +58 -15
  29. package/dist/context.js +50 -8
  30. package/dist/index.d.ts +12 -13
  31. package/dist/index.js +12 -13
  32. package/dist/log.d.ts +0 -1
  33. package/dist/log.js +0 -1
  34. package/dist/main.d.ts +0 -1
  35. package/dist/main.js +5 -405
  36. package/dist/memory/bootstrap.d.ts +6 -0
  37. package/dist/memory/bootstrap.js +46 -0
  38. package/dist/{memory-candidates.d.ts → memory/candidates.d.ts} +1 -1
  39. package/dist/{memory-candidates.js → memory/candidates.js} +33 -21
  40. package/dist/memory/chinese-words.d.ts +1 -0
  41. package/dist/memory/chinese-words.js +273 -0
  42. package/dist/{memory-consolidation.d.ts → memory/consolidation.d.ts} +0 -1
  43. package/dist/{memory-consolidation.js → memory/consolidation.js} +26 -35
  44. package/dist/{memory-files.d.ts → memory/files.d.ts} +0 -6
  45. package/dist/{memory-files.js → memory/files.js} +11 -36
  46. package/dist/{memory-lifecycle.d.ts → memory/lifecycle.d.ts} +23 -6
  47. package/dist/memory/lifecycle.js +246 -0
  48. package/dist/{memory-recall.d.ts → memory/recall.d.ts} +2 -2
  49. package/dist/memory/recall.js +501 -0
  50. package/dist/{session-memory.d.ts → memory/session.d.ts} +1 -1
  51. package/dist/{session-memory.js → memory/session.js} +31 -62
  52. package/dist/model-utils.d.ts +0 -1
  53. package/dist/model-utils.js +0 -1
  54. package/dist/paths.d.ts +0 -1
  55. package/dist/paths.js +0 -1
  56. package/dist/prompt-builder.d.ts +0 -1
  57. package/dist/prompt-builder.js +0 -1
  58. package/dist/runtime/bootstrap.d.ts +47 -0
  59. package/dist/runtime/bootstrap.js +450 -0
  60. package/dist/{delivery.d.ts → runtime/delivery.d.ts} +0 -1
  61. package/dist/{delivery.js → runtime/delivery.js} +1 -2
  62. package/dist/{dingtalk.d.ts → runtime/dingtalk.d.ts} +10 -1
  63. package/dist/{dingtalk.js → runtime/dingtalk.js} +87 -28
  64. package/dist/{events.d.ts → runtime/events.d.ts} +0 -1
  65. package/dist/{events.js → runtime/events.js} +1 -2
  66. package/dist/{store.d.ts → runtime/store.d.ts} +5 -1
  67. package/dist/{store.js → runtime/store.js} +60 -20
  68. package/dist/sandbox.d.ts +0 -1
  69. package/dist/sandbox.js +1 -2
  70. package/dist/{llm-json.d.ts → shared/llm-json.d.ts} +0 -1
  71. package/dist/{llm-json.js → shared/llm-json.js} +0 -1
  72. package/dist/shared/markdown-sections.d.ts +6 -0
  73. package/dist/{markdown-sections.js → shared/markdown-sections.js} +10 -4
  74. package/dist/{shell-escape.d.ts → shared/shell-escape.d.ts} +0 -1
  75. package/dist/{shell-escape.js → shared/shell-escape.js} +0 -1
  76. package/dist/shared/text-utils.d.ts +9 -0
  77. package/dist/shared/text-utils.js +36 -0
  78. package/dist/shared/type-guards.d.ts +5 -0
  79. package/dist/shared/type-guards.js +12 -0
  80. package/dist/shared/types.d.ts +14 -0
  81. package/dist/shared/types.js +1 -0
  82. package/dist/sidecar-worker.d.ts +0 -1
  83. package/dist/sidecar-worker.js +1 -8
  84. package/dist/{sub-agents.d.ts → subagents/discovery.d.ts} +0 -1
  85. package/dist/{sub-agents.js → subagents/discovery.js} +2 -3
  86. package/dist/{tools/subagent.d.ts → subagents/tool.d.ts} +2 -16
  87. package/dist/{tools/subagent.js → subagents/tool.js} +16 -38
  88. package/dist/tools/attach.d.ts +0 -1
  89. package/dist/tools/attach.js +0 -1
  90. package/dist/tools/bash.d.ts +0 -1
  91. package/dist/tools/bash.js +0 -1
  92. package/dist/tools/edit.d.ts +0 -1
  93. package/dist/tools/edit.js +1 -2
  94. package/dist/tools/index.d.ts +1 -2
  95. package/dist/tools/index.js +1 -2
  96. package/dist/tools/read.d.ts +0 -1
  97. package/dist/tools/read.js +1 -2
  98. package/dist/tools/truncate.d.ts +0 -1
  99. package/dist/tools/truncate.js +0 -1
  100. package/dist/tools/write-content.d.ts +0 -1
  101. package/dist/tools/write-content.js +1 -2
  102. package/dist/tools/write.d.ts +0 -1
  103. package/dist/tools/write.js +0 -1
  104. package/package.json +9 -3
  105. package/CHANGELOG.md +0 -47
  106. package/dist/agent.d.ts.map +0 -1
  107. package/dist/agent.js.map +0 -1
  108. package/dist/command-extension.d.ts.map +0 -1
  109. package/dist/command-extension.js.map +0 -1
  110. package/dist/commands.d.ts.map +0 -1
  111. package/dist/commands.js.map +0 -1
  112. package/dist/config-loader.d.ts.map +0 -1
  113. package/dist/config-loader.js.map +0 -1
  114. package/dist/context.d.ts.map +0 -1
  115. package/dist/context.js.map +0 -1
  116. package/dist/delivery.d.ts.map +0 -1
  117. package/dist/delivery.js.map +0 -1
  118. package/dist/dingtalk.d.ts.map +0 -1
  119. package/dist/dingtalk.js.map +0 -1
  120. package/dist/events.d.ts.map +0 -1
  121. package/dist/events.js.map +0 -1
  122. package/dist/index.d.ts.map +0 -1
  123. package/dist/index.js.map +0 -1
  124. package/dist/llm-json.d.ts.map +0 -1
  125. package/dist/llm-json.js.map +0 -1
  126. package/dist/log.d.ts.map +0 -1
  127. package/dist/log.js.map +0 -1
  128. package/dist/main.d.ts.map +0 -1
  129. package/dist/main.js.map +0 -1
  130. package/dist/markdown-sections.d.ts +0 -6
  131. package/dist/markdown-sections.d.ts.map +0 -1
  132. package/dist/markdown-sections.js.map +0 -1
  133. package/dist/memory-candidates.d.ts.map +0 -1
  134. package/dist/memory-candidates.js.map +0 -1
  135. package/dist/memory-consolidation.d.ts.map +0 -1
  136. package/dist/memory-consolidation.js.map +0 -1
  137. package/dist/memory-files.d.ts.map +0 -1
  138. package/dist/memory-files.js.map +0 -1
  139. package/dist/memory-lifecycle.d.ts.map +0 -1
  140. package/dist/memory-lifecycle.js +0 -150
  141. package/dist/memory-lifecycle.js.map +0 -1
  142. package/dist/memory-recall.d.ts.map +0 -1
  143. package/dist/memory-recall.js +0 -218
  144. package/dist/memory-recall.js.map +0 -1
  145. package/dist/model-utils.d.ts.map +0 -1
  146. package/dist/model-utils.js.map +0 -1
  147. package/dist/paths.d.ts.map +0 -1
  148. package/dist/paths.js.map +0 -1
  149. package/dist/prompt-builder.d.ts.map +0 -1
  150. package/dist/prompt-builder.js.map +0 -1
  151. package/dist/sandbox.d.ts.map +0 -1
  152. package/dist/sandbox.js.map +0 -1
  153. package/dist/session-memory-files.d.ts +0 -2
  154. package/dist/session-memory-files.d.ts.map +0 -1
  155. package/dist/session-memory-files.js +0 -2
  156. package/dist/session-memory-files.js.map +0 -1
  157. package/dist/session-memory.d.ts.map +0 -1
  158. package/dist/session-memory.js.map +0 -1
  159. package/dist/shell-escape.d.ts.map +0 -1
  160. package/dist/shell-escape.js.map +0 -1
  161. package/dist/sidecar-worker.d.ts.map +0 -1
  162. package/dist/sidecar-worker.js.map +0 -1
  163. package/dist/store.d.ts.map +0 -1
  164. package/dist/store.js.map +0 -1
  165. package/dist/sub-agents.d.ts.map +0 -1
  166. package/dist/sub-agents.js.map +0 -1
  167. package/dist/tools/attach.d.ts.map +0 -1
  168. package/dist/tools/attach.js.map +0 -1
  169. package/dist/tools/bash.d.ts.map +0 -1
  170. package/dist/tools/bash.js.map +0 -1
  171. package/dist/tools/edit.d.ts.map +0 -1
  172. package/dist/tools/edit.js.map +0 -1
  173. package/dist/tools/index.d.ts.map +0 -1
  174. package/dist/tools/index.js.map +0 -1
  175. package/dist/tools/read.d.ts.map +0 -1
  176. package/dist/tools/read.js.map +0 -1
  177. package/dist/tools/subagent.d.ts.map +0 -1
  178. package/dist/tools/subagent.js.map +0 -1
  179. package/dist/tools/truncate.d.ts.map +0 -1
  180. package/dist/tools/truncate.js.map +0 -1
  181. package/dist/tools/write-content.d.ts.map +0 -1
  182. package/dist/tools/write-content.js.map +0 -1
  183. package/dist/tools/write.d.ts.map +0 -1
  184. package/dist/tools/write.js.map +0 -1
  185. package/docs/improve-memory/design.md +0 -537
  186. package/docs/improve-memory/interfaces-and-tests.md +0 -473
  187. package/docs/improve-memory/spec.md +0 -357
  188. package/docs/memory-rfc.md +0 -297
  189. package/docs/proj-review.md +0 -188
  190. package/docs/subagent/pi-subagent-analyse.txt +0 -190
  191. package/docs/subagent/pi-subagent-design.txt +0 -266
  192. package/docs/subagent/pi-subagent-phase1-plan.txt +0 -529
  193. package/docs/test-supplementation-plan.md +0 -553
@@ -0,0 +1,501 @@
1
+ import { parseJsonObject } from "../shared/llm-json.js";
2
+ import { HAN_REGEX } from "../shared/text-utils.js";
3
+ import { runSidecarTask } from "../sidecar-worker.js";
4
+ import { buildMemoryCandidates } from "./candidates.js";
5
+ import { COMMON_CHINESE_WORDS } from "./chinese-words.js";
6
+ const RERANK_SYSTEM_PROMPT = `You are selecting which memory snippets are most relevant to the current user turn.
7
+
8
+ Return strict JSON only:
9
+ {
10
+ "selectedIds": ["candidate-id"]
11
+ }
12
+
13
+ Rules:
14
+ - Select only snippets that are clearly useful for answering the current turn.
15
+ - Prefer current work state, constraints, active files, recent corrections, and durable decisions.
16
+ - If nothing is clearly useful, return an empty array.
17
+ - Do not rewrite the candidates. Only return candidate ids.`;
18
+ const TOKEN_PART_REGEX = /[\p{Script=Han}]+|[\p{L}\p{N}_./-]+/gu;
19
+ const ASCII_SPLIT_REGEX = /[._/-]+/g;
20
+ const MEMORY_RECALL_RERANK_TIMEOUT_MS = 8_000;
21
+ const RERANK_CONTENT_CLIP = 800;
22
+ const MAX_HAN_WORD_LENGTH = Array.from(COMMON_CHINESE_WORDS).reduce((max, word) => Math.max(max, word.length), 2);
23
+ const LATIN_STOP_WORDS = new Set([
24
+ "a",
25
+ "an",
26
+ "and",
27
+ "are",
28
+ "as",
29
+ "at",
30
+ "be",
31
+ "been",
32
+ "being",
33
+ "by",
34
+ "can",
35
+ "could",
36
+ "did",
37
+ "do",
38
+ "does",
39
+ "doing",
40
+ "for",
41
+ "from",
42
+ "had",
43
+ "has",
44
+ "have",
45
+ "here",
46
+ "how",
47
+ "i",
48
+ "in",
49
+ "is",
50
+ "it",
51
+ "its",
52
+ "me",
53
+ "my",
54
+ "of",
55
+ "on",
56
+ "or",
57
+ "our",
58
+ "please",
59
+ "should",
60
+ "that",
61
+ "the",
62
+ "their",
63
+ "them",
64
+ "there",
65
+ "these",
66
+ "they",
67
+ "this",
68
+ "to",
69
+ "was",
70
+ "we",
71
+ "were",
72
+ "what",
73
+ "when",
74
+ "where",
75
+ "which",
76
+ "who",
77
+ "why",
78
+ "with",
79
+ "would",
80
+ "you",
81
+ "your",
82
+ ]);
83
+ const CHINESE_STOP_CHARS = new Set([
84
+ "的",
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
+ "其",
113
+ "它",
114
+ "他",
115
+ "她",
116
+ "们",
117
+ "都",
118
+ "要",
119
+ "会",
120
+ "能",
121
+ "很",
122
+ "得",
123
+ "地",
124
+ "着",
125
+ "过",
126
+ "吗",
127
+ "呢",
128
+ "吧",
129
+ "啊",
130
+ "哦",
131
+ "嗯",
132
+ "呀",
133
+ ]);
134
+ function containsHanText(text) {
135
+ return HAN_REGEX.test(text);
136
+ }
137
+ function tokenizeHanPart(part) {
138
+ const chars = Array.from(part);
139
+ const covered = new Uint8Array(chars.length);
140
+ const tokens = [];
141
+ for (let index = 0; index < chars.length; index++) {
142
+ let matchedLength = 0;
143
+ const maxLength = Math.min(MAX_HAN_WORD_LENGTH, chars.length - index);
144
+ for (let size = maxLength; size >= 2; size--) {
145
+ const candidate = chars.slice(index, index + size).join("");
146
+ if (COMMON_CHINESE_WORDS.has(candidate)) {
147
+ tokens.push(candidate);
148
+ matchedLength = size;
149
+ break;
150
+ }
151
+ }
152
+ if (matchedLength > 0) {
153
+ for (let coveredIndex = index; coveredIndex < index + matchedLength; coveredIndex++) {
154
+ covered[coveredIndex] = 1;
155
+ }
156
+ }
157
+ }
158
+ for (let index = 0; index <= chars.length - 2; index++) {
159
+ if (covered[index] || covered[index + 1]) {
160
+ continue;
161
+ }
162
+ tokens.push(chars.slice(index, index + 2).join(""));
163
+ }
164
+ for (let index = 0; index < chars.length; index++) {
165
+ if (covered[index]) {
166
+ continue;
167
+ }
168
+ const char = chars[index];
169
+ if (!CHINESE_STOP_CHARS.has(char)) {
170
+ tokens.push(char);
171
+ }
172
+ }
173
+ return Array.from(new Set(tokens));
174
+ }
175
+ function tokenizeAsciiPart(part) {
176
+ const tokens = [];
177
+ const normalized = part.toLowerCase();
178
+ const segments = normalized.split(ASCII_SPLIT_REGEX).filter(Boolean);
179
+ if (normalized.length >= 2 && !LATIN_STOP_WORDS.has(normalized)) {
180
+ tokens.push(normalized);
181
+ }
182
+ for (const segment of segments) {
183
+ if (segment.length >= 2 && !LATIN_STOP_WORDS.has(segment)) {
184
+ tokens.push(segment);
185
+ }
186
+ }
187
+ return tokens;
188
+ }
189
+ function tokenize(text) {
190
+ const parts = text.toLowerCase().match(TOKEN_PART_REGEX) ?? [];
191
+ const tokens = [];
192
+ for (const part of parts) {
193
+ if (containsHanText(part)) {
194
+ tokens.push(...tokenizeHanPart(part));
195
+ continue;
196
+ }
197
+ tokens.push(...tokenizeAsciiPart(part));
198
+ }
199
+ return Array.from(new Set(tokens));
200
+ }
201
+ export function tokenizeRecallText(text) {
202
+ return tokenize(text);
203
+ }
204
+ function buildTokenSet(text) {
205
+ return new Set(tokenize(text));
206
+ }
207
+ function computeTokenMatchStats(queryTokens, text) {
208
+ if (queryTokens.length === 0 || !text.trim()) {
209
+ return { matchedCount: 0, coverage: 0 };
210
+ }
211
+ const haystack = buildTokenSet(text);
212
+ let matchedCount = 0;
213
+ for (const token of queryTokens) {
214
+ if (haystack.has(token)) {
215
+ matchedCount++;
216
+ }
217
+ }
218
+ return {
219
+ matchedCount,
220
+ coverage: matchedCount / queryTokens.length,
221
+ };
222
+ }
223
+ function collectMatchingQueryTokens(queryTokens, texts) {
224
+ const haystack = new Set();
225
+ for (const text of texts) {
226
+ for (const token of tokenize(text)) {
227
+ haystack.add(token);
228
+ }
229
+ }
230
+ const matches = new Set();
231
+ for (const token of queryTokens) {
232
+ if (haystack.has(token)) {
233
+ matches.add(token);
234
+ }
235
+ }
236
+ return matches;
237
+ }
238
+ function computeExactMatchBoost(query, candidate) {
239
+ const normalizedQuery = query.trim().toLowerCase();
240
+ if (!normalizedQuery) {
241
+ return 0;
242
+ }
243
+ const minLength = containsHanText(normalizedQuery) ? 2 : 4;
244
+ if (normalizedQuery.length < minLength) {
245
+ return 0;
246
+ }
247
+ let boost = 0;
248
+ const scoringFields = [
249
+ [candidate.title, 12],
250
+ [candidate.searchText ?? candidate.content, 8],
251
+ [candidate.path, 4],
252
+ ];
253
+ for (const [field, value] of scoringFields) {
254
+ if (field.toLowerCase().includes(normalizedQuery)) {
255
+ boost += value;
256
+ }
257
+ }
258
+ return boost;
259
+ }
260
+ function computeRecencyBoost(timestamp) {
261
+ if (!timestamp)
262
+ return 0;
263
+ const timestampMs = Date.parse(timestamp);
264
+ if (!Number.isFinite(timestampMs))
265
+ return 0;
266
+ const ageMs = Date.now() - timestampMs;
267
+ const dayMs = 24 * 60 * 60 * 1000;
268
+ if (ageMs <= dayMs)
269
+ return 6;
270
+ if (ageMs <= 7 * dayMs)
271
+ return 4;
272
+ if (ageMs <= 30 * dayMs)
273
+ return 2;
274
+ return 0;
275
+ }
276
+ function detectQueryIntents(query) {
277
+ const intents = new Set();
278
+ if (/\b(now|current|currently|status)\b/i.test(query) || /(现在|当前|目前|正在|状态)/u.test(query)) {
279
+ intents.add("current-state");
280
+ }
281
+ if (/\b(next|follow-?up|todo|plan)\b/i.test(query) || /(下一步|接下来|后续|怎么办|怎么做|该查什么)/u.test(query)) {
282
+ intents.add("next-steps");
283
+ }
284
+ if (/\b(constraint|requirement|guardrail|compatible|compatibility)\b/i.test(query) ||
285
+ /(约束|限制|要求|兼容|注意事项)/u.test(query)) {
286
+ intents.add("constraints");
287
+ }
288
+ if (/\b(decision|decided|why)\b/i.test(query) || /(决策|决定|方案|为什么)/u.test(query)) {
289
+ intents.add("decisions");
290
+ }
291
+ if (/\b(error|bug|failure|issue|regression)\b/i.test(query) || /(错误|异常|失败|问题|缺陷|回归)/u.test(query)) {
292
+ intents.add("errors");
293
+ }
294
+ if (/\b(history|previous|before|earlier|past)\b/i.test(query) || /(历史|之前|以前|过去|早先|曾经)/u.test(query)) {
295
+ intents.add("history");
296
+ }
297
+ return intents;
298
+ }
299
+ function computeSectionIntentBoost(intents, candidate) {
300
+ const kind = candidate.sectionKind ?? "";
301
+ let boost = 0;
302
+ if (intents.has("current-state") && kind === "current state")
303
+ boost += 10;
304
+ if (intents.has("next-steps") && kind === "next steps")
305
+ boost += 10;
306
+ if (intents.has("constraints") && kind.includes("constraint"))
307
+ boost += 8;
308
+ if (intents.has("decisions") && kind.includes("decision"))
309
+ boost += 8;
310
+ if (intents.has("errors") && kind === "errors & corrections")
311
+ boost += 8;
312
+ if (intents.has("history") && candidate.source === "channel-history")
313
+ boost += 8;
314
+ return boost;
315
+ }
316
+ function compareScoredCandidates(a, b) {
317
+ return (b.score - a.score ||
318
+ b.lexicalMatchCount - a.lexicalMatchCount ||
319
+ b.candidate.priority - a.candidate.priority ||
320
+ a.candidate.title.localeCompare(b.candidate.title));
321
+ }
322
+ function scoreCandidate(query, queryTokens, intents, candidate) {
323
+ const searchText = candidate.searchText ?? candidate.content;
324
+ const titleStats = computeTokenMatchStats(queryTokens, candidate.title);
325
+ const contentStats = computeTokenMatchStats(queryTokens, searchText);
326
+ const pathStats = computeTokenMatchStats(queryTokens, candidate.path);
327
+ const matchedTokens = collectMatchingQueryTokens(queryTokens, [candidate.title, searchText, candidate.path]);
328
+ const exactBoost = computeExactMatchBoost(query, candidate);
329
+ const intentBoost = computeSectionIntentBoost(intents, candidate);
330
+ const overallCoverage = queryTokens.length > 0 ? matchedTokens.size / queryTokens.length : 0;
331
+ const lexicalScore = overallCoverage * 48 +
332
+ titleStats.coverage * 18 +
333
+ contentStats.coverage * 22 +
334
+ pathStats.coverage * 8 +
335
+ exactBoost;
336
+ const structuralScore = candidate.priority + intentBoost + computeRecencyBoost(candidate.timestamp);
337
+ if (matchedTokens.size === 0 && exactBoost === 0) {
338
+ return null;
339
+ }
340
+ return {
341
+ candidate,
342
+ score: lexicalScore * (1 + structuralScore / 100),
343
+ lexicalMatchCount: matchedTokens.size,
344
+ intentBoost,
345
+ };
346
+ }
347
+ function seedIntentCandidates(request, candidates, existing, intents, queryTokens) {
348
+ if (intents.size === 0) {
349
+ return existing;
350
+ }
351
+ const seen = new Set(existing.map(({ candidate }) => candidate.id));
352
+ const seeded = [...existing];
353
+ const limit = Math.max(request.maxCandidates, request.maxInjected);
354
+ const intentCandidates = candidates
355
+ .map((candidate) => ({
356
+ candidate,
357
+ intentBoost: computeSectionIntentBoost(intents, candidate),
358
+ }))
359
+ .filter(({ candidate, intentBoost }) => intentBoost > 0 && !seen.has(candidate.id))
360
+ .sort((a, b) => b.intentBoost - a.intentBoost ||
361
+ b.candidate.priority - a.candidate.priority ||
362
+ a.candidate.title.localeCompare(b.candidate.title));
363
+ for (const { candidate, intentBoost } of intentCandidates) {
364
+ const matchedTokens = collectMatchingQueryTokens(queryTokens, [
365
+ candidate.title,
366
+ candidate.searchText ?? candidate.content,
367
+ ]);
368
+ if (matchedTokens.size === 0) {
369
+ continue;
370
+ }
371
+ seeded.push({
372
+ candidate,
373
+ score: (intentBoost + matchedTokens.size * 8) *
374
+ (1 + (candidate.priority + computeRecencyBoost(candidate.timestamp)) / 100),
375
+ lexicalMatchCount: matchedTokens.size,
376
+ intentBoost,
377
+ });
378
+ seen.add(candidate.id);
379
+ if (seeded.length >= limit) {
380
+ break;
381
+ }
382
+ }
383
+ return seeded;
384
+ }
385
+ async function rerankCandidates(request, candidates) {
386
+ if ((!request.rerankWithModel && !request.autoRerank) || candidates.length <= request.maxInjected) {
387
+ return candidates;
388
+ }
389
+ const renderedCandidates = candidates
390
+ .map(({ candidate, score, lexicalMatchCount, intentBoost }) => {
391
+ const clippedContent = candidate.content.length > RERANK_CONTENT_CLIP
392
+ ? `${candidate.content.slice(0, RERANK_CONTENT_CLIP)}...`
393
+ : candidate.content;
394
+ return [
395
+ `id: ${candidate.id}`,
396
+ `source: ${candidate.source}`,
397
+ `title: ${candidate.title}`,
398
+ `path: ${candidate.path}`,
399
+ `score: ${score}`,
400
+ `lexicalMatchCount: ${lexicalMatchCount}`,
401
+ `intentBoost: ${intentBoost}`,
402
+ `content: ${clippedContent}`,
403
+ ].join("\n");
404
+ })
405
+ .join("\n\n---\n\n");
406
+ try {
407
+ const result = await runSidecarTask({
408
+ name: "memory-recall-rerank",
409
+ model: request.model,
410
+ resolveApiKey: request.resolveApiKey,
411
+ systemPrompt: RERANK_SYSTEM_PROMPT,
412
+ prompt: `User turn:\n${request.query.trim()}\n\nCandidates:\n${renderedCandidates}`,
413
+ timeoutMs: MEMORY_RECALL_RERANK_TIMEOUT_MS,
414
+ parse: (text) => {
415
+ const parsed = parseJsonObject(text);
416
+ return Array.isArray(parsed.selectedIds)
417
+ ? parsed.selectedIds.filter((id) => typeof id === "string" && id.trim().length > 0)
418
+ : [];
419
+ },
420
+ });
421
+ const selectedIds = new Set(result.output);
422
+ if (selectedIds.size === 0) {
423
+ return candidates;
424
+ }
425
+ const selected = candidates.filter(({ candidate }) => selectedIds.has(candidate.id));
426
+ return selected.length > 0 ? selected : candidates;
427
+ }
428
+ catch {
429
+ return candidates;
430
+ }
431
+ }
432
+ function renderRecallResult(items, maxChars) {
433
+ if (items.length === 0) {
434
+ return "";
435
+ }
436
+ const lines = ["<runtime_context>", "Relevant context for this turn:"];
437
+ for (const item of items) {
438
+ lines.push("");
439
+ lines.push(`[${item.source}/${item.title}]`);
440
+ lines.push(`Path: ${item.path}`);
441
+ lines.push(item.content);
442
+ }
443
+ lines.push("</runtime_context>");
444
+ const rendered = lines.join("\n");
445
+ if (rendered.length <= maxChars) {
446
+ return rendered;
447
+ }
448
+ const clippedLines = ["<runtime_context>", "Relevant context for this turn:"];
449
+ let usedChars = clippedLines.join("\n").length + "</runtime_context>".length + 2;
450
+ for (const item of items) {
451
+ const block = ["", `[${item.source}/${item.title}]`, `Path: ${item.path}`, item.content].join("\n");
452
+ if (usedChars + block.length > maxChars) {
453
+ break;
454
+ }
455
+ clippedLines.push("", `[${item.source}/${item.title}]`, `Path: ${item.path}`, item.content);
456
+ usedChars += block.length;
457
+ }
458
+ clippedLines.push("</runtime_context>");
459
+ return clippedLines.join("\n");
460
+ }
461
+ export async function recallRelevantMemory(request) {
462
+ const query = request.query.trim();
463
+ if (!query) {
464
+ return { items: [], renderedText: "" };
465
+ }
466
+ const candidates = await buildMemoryCandidates({
467
+ workspaceDir: request.workspaceDir,
468
+ channelDir: request.channelDir,
469
+ cache: request.candidateCache,
470
+ });
471
+ const filteredCandidates = request.allowedSources?.length
472
+ ? candidates.filter((candidate) => request.allowedSources?.includes(candidate.source))
473
+ : candidates;
474
+ if (filteredCandidates.length === 0) {
475
+ return { items: [], renderedText: "" };
476
+ }
477
+ const queryTokens = tokenize(query);
478
+ const queryIntents = detectQueryIntents(query);
479
+ const scored = filteredCandidates
480
+ .map((candidate) => scoreCandidate(query, queryTokens, queryIntents, candidate))
481
+ .filter((candidate) => candidate !== null)
482
+ .sort(compareScoredCandidates);
483
+ const shortlist = seedIntentCandidates(request, filteredCandidates, scored, queryIntents, queryTokens)
484
+ .sort(compareScoredCandidates)
485
+ .slice(0, Math.max(request.maxCandidates, request.maxInjected));
486
+ if (shortlist.length === 0) {
487
+ return { items: [], renderedText: "" };
488
+ }
489
+ const reranked = await rerankCandidates(request, shortlist);
490
+ const items = reranked.slice(0, request.maxInjected).map(({ candidate, score }) => ({
491
+ source: candidate.source,
492
+ path: candidate.path,
493
+ title: candidate.title,
494
+ content: candidate.content,
495
+ score,
496
+ }));
497
+ return {
498
+ items,
499
+ renderedText: renderRecallResult(items, request.maxChars),
500
+ };
501
+ }
@@ -16,7 +16,7 @@ export interface SessionMemoryUpdateOptions {
16
16
  messages: AgentMessage[];
17
17
  model: Model<Api>;
18
18
  resolveApiKey: (model: Model<Api>) => Promise<string>;
19
+ timeoutMs?: number;
19
20
  }
20
21
  export declare function renderSessionMemory(state: SessionMemoryState): string;
21
22
  export declare function updateChannelSessionMemory(options: SessionMemoryUpdateOptions): Promise<SessionMemoryState>;
22
- //# sourceMappingURL=session-memory.d.ts.map
@@ -1,16 +1,17 @@
1
1
  import { serializeConversation } from "@mariozechner/pi-coding-agent";
2
2
  import { writeFile } from "fs/promises";
3
3
  import { join } from "path";
4
- import { parseJsonObject } from "./llm-json.js";
5
- import { splitLevelOneSections } from "./markdown-sections.js";
6
- import { readChannelMemory } from "./memory-files.js";
7
- import { readChannelSession, rewriteChannelSession } from "./session-memory-files.js";
8
- import { runSidecarTask, SidecarParseError } from "./sidecar-worker.js";
4
+ import { parseJsonObject } from "../shared/llm-json.js";
5
+ import { splitH1Sections } from "../shared/markdown-sections.js";
6
+ import { clipText } from "../shared/text-utils.js";
7
+ import { buildStandardMessages, isRecord } from "../shared/type-guards.js";
8
+ import { runSidecarTask, SidecarParseError } from "../sidecar-worker.js";
9
+ import { readChannelMemory, readChannelSession, rewriteChannelSession } from "./files.js";
9
10
  const SESSION_TRANSCRIPT_MAX_CHARS = 20_000;
10
11
  const SESSION_MEMORY_MAX_CHARS = 4_000;
11
12
  const SESSION_ITEM_LIMIT = 12;
12
13
  const SESSION_ITEM_MAX_CHARS = 300;
13
- const SESSION_MEMORY_TIMEOUT_MS = 10_000;
14
+ const DEFAULT_SESSION_MEMORY_TIMEOUT_MS = 30_000;
14
15
  const SESSION_MEMORY_SYSTEM_PROMPT = `You maintain a Pipiclaw SESSION.md file.
15
16
 
16
17
  Return strict JSON only. Do not use Markdown fences.
@@ -37,15 +38,6 @@ Rules:
37
38
  - "nextSteps" should reflect the most likely immediate follow-up actions.
38
39
  - "worklog" must stay terse and recent.
39
40
  - If a field has nothing useful, return an empty string or empty array.`;
40
- function clipText(text, maxChars) {
41
- const normalized = text.replace(/\r/g, "").trim();
42
- if (normalized.length <= maxChars) {
43
- return normalized;
44
- }
45
- const headChars = Math.floor(maxChars * 0.45);
46
- const tailChars = maxChars - headChars;
47
- return `${normalized.slice(0, headChars)}\n\n[... omitted middle section ...]\n\n${normalized.slice(-tailChars)}`;
48
- }
49
41
  function normalizeItem(value) {
50
42
  if (typeof value !== "string") {
51
43
  return null;
@@ -70,33 +62,22 @@ function normalizeItems(value) {
70
62
  function normalizeTitle(value) {
71
63
  return typeof value === "string" ? value.trim().slice(0, 120) : "";
72
64
  }
73
- function isRecord(value) {
74
- return typeof value === "object" && value !== null;
75
- }
76
65
  function parseStateUpdate(text) {
77
66
  const parsed = parseJsonObject(text);
78
67
  if (!isRecord(parsed)) {
79
68
  throw new Error("Session memory response was not a JSON object");
80
69
  }
81
- const next = {};
82
- if ("title" in parsed)
83
- next.title = normalizeTitle(parsed.title);
84
- if ("currentState" in parsed)
85
- next.currentState = normalizeItems(parsed.currentState);
86
- if ("userIntent" in parsed)
87
- next.userIntent = normalizeItems(parsed.userIntent);
88
- if ("activeFiles" in parsed)
89
- next.activeFiles = normalizeItems(parsed.activeFiles);
90
- if ("decisions" in parsed)
91
- next.decisions = normalizeItems(parsed.decisions);
92
- if ("constraints" in parsed)
93
- next.constraints = normalizeItems(parsed.constraints);
94
- if ("errorsAndCorrections" in parsed)
95
- next.errorsAndCorrections = normalizeItems(parsed.errorsAndCorrections);
96
- if ("nextSteps" in parsed)
97
- next.nextSteps = normalizeItems(parsed.nextSteps);
98
- if ("worklog" in parsed)
99
- next.worklog = normalizeItems(parsed.worklog);
70
+ const next = {
71
+ title: normalizeTitle(parsed.title),
72
+ currentState: normalizeItems(parsed.currentState),
73
+ userIntent: normalizeItems(parsed.userIntent),
74
+ activeFiles: normalizeItems(parsed.activeFiles),
75
+ decisions: normalizeItems(parsed.decisions),
76
+ constraints: normalizeItems(parsed.constraints),
77
+ errorsAndCorrections: normalizeItems(parsed.errorsAndCorrections),
78
+ nextSteps: normalizeItems(parsed.nextSteps),
79
+ worklog: normalizeItems(parsed.worklog),
80
+ };
100
81
  return next;
101
82
  }
102
83
  function createEmptySessionMemoryState() {
@@ -138,7 +119,7 @@ function parseSectionItems(content) {
138
119
  }
139
120
  function parseRenderedSessionMemory(markdown) {
140
121
  const state = createEmptySessionMemoryState();
141
- for (const section of splitLevelOneSections(markdown)) {
122
+ for (const section of splitH1Sections(markdown)) {
142
123
  switch (section.heading.toLowerCase()) {
143
124
  case "session title":
144
125
  state.title = stripHtmlComments(section.content).split("\n")[0]?.trim().slice(0, 120) || "";
@@ -171,19 +152,17 @@ function parseRenderedSessionMemory(markdown) {
171
152
  }
172
153
  return state;
173
154
  }
174
- function mergeSessionMemoryState(current, update) {
155
+ function mergeSessionMemoryState(_current, update) {
175
156
  return {
176
- title: typeof update.title === "string" ? update.title : current.title,
177
- currentState: Array.isArray(update.currentState) ? update.currentState : current.currentState,
178
- userIntent: Array.isArray(update.userIntent) ? update.userIntent : current.userIntent,
179
- activeFiles: Array.isArray(update.activeFiles) ? update.activeFiles : current.activeFiles,
180
- decisions: Array.isArray(update.decisions) ? update.decisions : current.decisions,
181
- constraints: Array.isArray(update.constraints) ? update.constraints : current.constraints,
182
- errorsAndCorrections: Array.isArray(update.errorsAndCorrections)
183
- ? update.errorsAndCorrections
184
- : current.errorsAndCorrections,
185
- nextSteps: Array.isArray(update.nextSteps) ? update.nextSteps : current.nextSteps,
186
- worklog: Array.isArray(update.worklog) ? update.worklog : current.worklog,
157
+ title: typeof update.title === "string" ? update.title : "",
158
+ currentState: Array.isArray(update.currentState) ? update.currentState : [],
159
+ userIntent: Array.isArray(update.userIntent) ? update.userIntent : [],
160
+ activeFiles: Array.isArray(update.activeFiles) ? update.activeFiles : [],
161
+ decisions: Array.isArray(update.decisions) ? update.decisions : [],
162
+ constraints: Array.isArray(update.constraints) ? update.constraints : [],
163
+ errorsAndCorrections: Array.isArray(update.errorsAndCorrections) ? update.errorsAndCorrections : [],
164
+ nextSteps: Array.isArray(update.nextSteps) ? update.nextSteps : [],
165
+ worklog: Array.isArray(update.worklog) ? update.worklog : [],
187
166
  };
188
167
  }
189
168
  async function writeSessionMemoryDebugFile(channelDir, error, rawText) {
@@ -200,15 +179,6 @@ async function writeSessionMemoryDebugFile(channelDir, error, rawText) {
200
179
  function renderSection(heading, items) {
201
180
  return [`# ${heading}`, "", ...items.map((item) => `- ${item}`)].join("\n");
202
181
  }
203
- function isStandardAgentMessage(message) {
204
- return (typeof message === "object" &&
205
- message !== null &&
206
- "role" in message &&
207
- (message.role === "user" || message.role === "assistant" || message.role === "toolResult"));
208
- }
209
- function buildMessagesForSessionMemory(messages) {
210
- return messages.filter(isStandardAgentMessage);
211
- }
212
182
  export function renderSessionMemory(state) {
213
183
  const sections = [];
214
184
  if (state.title.trim()) {
@@ -246,7 +216,7 @@ ${transcript || "(empty)"}`;
246
216
  export async function updateChannelSessionMemory(options) {
247
217
  const currentSession = await readChannelSession(options.channelDir);
248
218
  const currentMemory = await readChannelMemory(options.channelDir);
249
- const messages = buildMessagesForSessionMemory(options.messages);
219
+ const messages = buildStandardMessages(options.messages);
250
220
  const currentState = parseRenderedSessionMemory(currentSession);
251
221
  let update;
252
222
  try {
@@ -257,7 +227,7 @@ export async function updateChannelSessionMemory(options) {
257
227
  systemPrompt: SESSION_MEMORY_SYSTEM_PROMPT,
258
228
  prompt: buildSessionPrompt(currentSession, currentMemory, messages),
259
229
  parse: parseStateUpdate,
260
- timeoutMs: SESSION_MEMORY_TIMEOUT_MS,
230
+ timeoutMs: options.timeoutMs ?? DEFAULT_SESSION_MEMORY_TIMEOUT_MS,
261
231
  });
262
232
  update = result.output;
263
233
  }
@@ -271,4 +241,3 @@ export async function updateChannelSessionMemory(options) {
271
241
  await rewriteChannelSession(options.channelDir, rendered);
272
242
  return parseRenderedSessionMemory(rendered);
273
243
  }
274
- //# sourceMappingURL=session-memory.js.map
@@ -8,4 +8,3 @@ export declare function findExactModelReferenceMatch(modelReference: string, ava
8
8
  };
9
9
  export declare function formatModelList(models: Model<Api>[], currentModel: Model<Api> | undefined, limit?: number): string;
10
10
  export declare function resolveInitialModel(modelRegistry: ModelRegistry, settingsManager: PipiclawSettingsManager): Model<Api>;
11
- //# sourceMappingURL=model-utils.d.ts.map
@@ -70,4 +70,3 @@ export function resolveInitialModel(modelRegistry, settingsManager) {
70
70
  }
71
71
  return defaultModel;
72
72
  }
73
- //# sourceMappingURL=model-utils.js.map
package/dist/paths.d.ts CHANGED
@@ -7,4 +7,3 @@ export declare const CHANNEL_CONFIG_PATH: string;
7
7
  export declare const AUTH_CONFIG_PATH: string;
8
8
  export declare const MODELS_CONFIG_PATH: string;
9
9
  export declare const SETTINGS_CONFIG_PATH: string;
10
- //# sourceMappingURL=paths.d.ts.map