@dyyz1993/pi-coding-agent 0.74.24 → 0.74.25
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 +3 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/extensions/agent-permissions/index.ts +235 -0
- package/dist/extensions/ask-tools/index.ts +115 -0
- package/dist/extensions/auto-memory/contract.d.ts +51 -0
- package/dist/extensions/auto-memory/contract.d.ts.map +1 -0
- package/dist/extensions/auto-memory/contract.js +2 -0
- package/dist/extensions/auto-memory/contract.js.map +1 -0
- package/dist/extensions/auto-memory/contract.ts +56 -0
- package/dist/extensions/auto-memory/index.ts +969 -0
- package/dist/extensions/auto-memory/prompts.ts +202 -0
- package/dist/extensions/auto-memory/skip-rules.ts +297 -0
- package/dist/extensions/auto-memory/utils.ts +208 -0
- package/dist/extensions/auto-session-title/index.ts +83 -0
- package/dist/extensions/bash-ext/contract.d.ts +79 -0
- package/dist/extensions/bash-ext/contract.d.ts.map +1 -0
- package/dist/extensions/bash-ext/contract.js +2 -0
- package/dist/extensions/bash-ext/contract.js.map +1 -0
- package/dist/extensions/bash-ext/contract.ts +69 -0
- package/dist/extensions/bash-ext/index.ts +858 -0
- package/dist/extensions/claude-hooks-compat/config-loader.ts +49 -0
- package/dist/extensions/claude-hooks-compat/handler-runner.ts +377 -0
- package/dist/extensions/claude-hooks-compat/if-parser.ts +53 -0
- package/dist/extensions/claude-hooks-compat/index.ts +178 -0
- package/dist/extensions/claude-hooks-compat/matcher.ts +17 -0
- package/dist/extensions/claude-hooks-compat/stdin-builder.ts +27 -0
- package/dist/extensions/claude-hooks-compat/types.ts +77 -0
- package/dist/extensions/compaction-manager/config.ts +47 -0
- package/dist/extensions/compaction-manager/context-fold.ts +63 -0
- package/dist/extensions/compaction-manager/index.ts +151 -0
- package/dist/extensions/compaction-manager/microcompact.ts +49 -0
- package/dist/extensions/compaction-manager/reactive.ts +9 -0
- package/dist/extensions/compaction-manager/session-memory.ts +48 -0
- package/dist/extensions/coordinator/INTEGRATION.md +376 -0
- package/dist/extensions/coordinator/handler.test.ts +277 -0
- package/dist/extensions/coordinator/handler.ts +189 -0
- package/dist/extensions/coordinator/index.ts +261 -0
- package/dist/extensions/coordinator/types.d.ts +100 -0
- package/dist/extensions/coordinator/types.d.ts.map +1 -0
- package/dist/extensions/coordinator/types.js +2 -0
- package/dist/extensions/coordinator/types.js.map +1 -0
- package/dist/extensions/coordinator/types.ts +72 -0
- package/dist/extensions/file-snapshot/index.ts +131 -0
- package/dist/extensions/file-time-guard/README.md +133 -0
- package/dist/extensions/file-time-guard/config.ts +13 -0
- package/dist/extensions/file-time-guard/index.ts +171 -0
- package/dist/extensions/hooks-engine/index.ts +117 -0
- package/dist/extensions/lsp/lsp/client/file-tracker.ts +70 -0
- package/dist/extensions/lsp/lsp/client/registry.ts +305 -0
- package/dist/extensions/lsp/lsp/client/runtime.ts +832 -0
- package/dist/extensions/lsp/lsp/config/resolver.ts +573 -0
- package/dist/extensions/lsp/lsp/contract.d.ts +101 -0
- package/dist/extensions/lsp/lsp/contract.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/contract.js +2 -0
- package/dist/extensions/lsp/lsp/contract.js.map +1 -0
- package/dist/extensions/lsp/lsp/contract.ts +103 -0
- package/dist/extensions/lsp/lsp/hooks/agent-end.ts +169 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts +10 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js +30 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.ts +41 -0
- package/dist/extensions/lsp/lsp/hooks/writethrough.ts +342 -0
- package/dist/extensions/lsp/lsp/index.ts +307 -0
- package/dist/extensions/lsp/lsp/lsp.test.ts +684 -0
- package/dist/extensions/lsp/lsp/monitoring/server-metrics.ts +176 -0
- package/dist/extensions/lsp/lsp/tools/lsp-tool.ts +402 -0
- package/dist/extensions/lsp/lsp/utils/dependency-resolver.ts +147 -0
- package/dist/extensions/lsp/lsp/utils/diagnostics-wait.ts +41 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts +20 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js +64 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.ts +76 -0
- package/dist/extensions/message-bridge/GUIDE.md +210 -0
- package/dist/extensions/message-bridge/index.ts +222 -0
- package/dist/extensions/output-guard/index.ts +384 -0
- package/dist/extensions/preview/index.ts +278 -0
- package/dist/extensions/rules-engine/MATCH_HISTORY_RECONCILIATION.md +111 -0
- package/dist/extensions/rules-engine/RULES-ENGINE-GUIDE.md +470 -0
- package/dist/extensions/rules-engine/cache.js +232 -0
- package/dist/extensions/rules-engine/cache.ts +38 -0
- package/dist/extensions/rules-engine/config.js +63 -0
- package/dist/extensions/rules-engine/config.ts +70 -0
- package/dist/extensions/rules-engine/index.js +1530 -0
- package/dist/extensions/rules-engine/index.ts +552 -0
- package/dist/extensions/rules-engine/injector.js +68 -0
- package/dist/extensions/rules-engine/injector.ts +74 -0
- package/dist/extensions/rules-engine/loader.js +179 -0
- package/dist/extensions/rules-engine/loader.ts +205 -0
- package/dist/extensions/rules-engine/matcher.js +534 -0
- package/dist/extensions/rules-engine/matcher.ts +52 -0
- package/dist/extensions/rules-engine/types.d.ts +156 -0
- package/dist/extensions/rules-engine/types.d.ts.map +1 -0
- package/dist/extensions/rules-engine/types.js +2 -0
- package/dist/extensions/rules-engine/types.js.map +1 -0
- package/dist/extensions/rules-engine/types.ts +169 -0
- package/dist/extensions/session-supervisor/checker.ts +116 -0
- package/dist/extensions/session-supervisor/config.ts +45 -0
- package/dist/extensions/session-supervisor/index.ts +726 -0
- package/dist/extensions/session-supervisor/prompts.ts +132 -0
- package/dist/extensions/session-supervisor/scheduler.ts +69 -0
- package/dist/extensions/session-supervisor/types.ts +215 -0
- package/dist/extensions/subagent/README.md +172 -0
- package/dist/extensions/subagent/agents/explorer.md +25 -0
- package/dist/extensions/subagent/agents/guide.md +27 -0
- package/dist/extensions/subagent/agents/planner.md +37 -0
- package/dist/extensions/subagent/agents/reviewer.md +35 -0
- package/dist/extensions/subagent/agents/scout.md +50 -0
- package/dist/extensions/subagent/agents/verification.md +35 -0
- package/dist/extensions/subagent/agents/worker.md +24 -0
- package/dist/extensions/subagent/agents.ts +25 -0
- package/dist/extensions/subagent/index.ts +987 -0
- package/dist/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/dist/extensions/subagent/prompts/implement.md +10 -0
- package/dist/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/dist/extensions/subagent-ext/contract.d.ts +2 -0
- package/dist/extensions/subagent-ext/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-ext/contract.js +2 -0
- package/dist/extensions/subagent-ext/contract.js.map +1 -0
- package/dist/extensions/subagent-ext/contract.ts +1 -0
- package/dist/extensions/subagent-ext/index.ts +347 -0
- package/dist/extensions/subagent-shared/contract.d.ts +25 -0
- package/dist/extensions/subagent-shared/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-shared/contract.js +2 -0
- package/dist/extensions/subagent-shared/contract.js.map +1 -0
- package/dist/extensions/subagent-shared/contract.ts +28 -0
- package/dist/extensions/subagent-shared/index.ts +4 -0
- package/dist/extensions/subagent-shared/render.ts +166 -0
- package/dist/extensions/subagent-shared/types.ts +35 -0
- package/dist/extensions/subagent-shared/utils.ts +112 -0
- package/dist/extensions/subagent-v2/contract.d.ts +2 -0
- package/dist/extensions/subagent-v2/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-v2/contract.js +2 -0
- package/dist/extensions/subagent-v2/contract.js.map +1 -0
- package/dist/extensions/subagent-v2/contract.ts +1 -0
- package/dist/extensions/subagent-v2/index.ts +599 -0
- package/dist/extensions/todo-ext/contract.d.ts +27 -0
- package/dist/extensions/todo-ext/contract.d.ts.map +1 -0
- package/dist/extensions/todo-ext/contract.js +2 -0
- package/dist/extensions/todo-ext/contract.js.map +1 -0
- package/dist/extensions/todo-ext/contract.ts +30 -0
- package/dist/extensions/todo-ext/index.ts +419 -0
- package/package.json +6 -5
|
@@ -0,0 +1,969 @@
|
|
|
1
|
+
import { existsSync, type Stats } from "node:fs";
|
|
2
|
+
import { mkdir, readFile, stat, unlink, utimes, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { AgentMessage, AgentToolResult } from "@dyyz1993/pi-agent-core";
|
|
5
|
+
import { Type } from "typebox";
|
|
6
|
+
import type { CallLLMOptions, ExtensionAPI, ExtensionContext } from "@dyyz1993/pi-coding-agent";
|
|
7
|
+
import { createTypedChannel } from "@dyyz1993/pi-coding-agent";
|
|
8
|
+
import type { MemoryChannelContract } from "./contract.js";
|
|
9
|
+
|
|
10
|
+
function stripMarkdownCodeBlock(text: string): string {
|
|
11
|
+
let cleaned = text.trim();
|
|
12
|
+
if (cleaned.startsWith("```")) {
|
|
13
|
+
const firstNewline = cleaned.indexOf("\n");
|
|
14
|
+
if (firstNewline !== -1) cleaned = cleaned.slice(firstNewline + 1);
|
|
15
|
+
const lastBacktick = cleaned.lastIndexOf("```");
|
|
16
|
+
if (lastBacktick !== -1) cleaned = cleaned.slice(0, lastBacktick);
|
|
17
|
+
cleaned = cleaned.trim();
|
|
18
|
+
}
|
|
19
|
+
return cleaned;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
addHistoryEntry,
|
|
24
|
+
applyPurification,
|
|
25
|
+
evaluateRules,
|
|
26
|
+
getGlobalMemoryDir,
|
|
27
|
+
type HistoryEntry,
|
|
28
|
+
loadSkipWordStore,
|
|
29
|
+
type PurificationResult,
|
|
30
|
+
type SkipRule,
|
|
31
|
+
type SkipWordStore,
|
|
32
|
+
saveSkipWordStore,
|
|
33
|
+
} from "./skip-rules.js";
|
|
34
|
+
import {
|
|
35
|
+
BOOKMARK_SUMMARY_PROMPT,
|
|
36
|
+
DREAM_PROMPT,
|
|
37
|
+
EXTRACTION_PROMPT,
|
|
38
|
+
MEMORY_SYSTEM_PROMPT,
|
|
39
|
+
SELECT_MEMORIES_PROMPT,
|
|
40
|
+
} from "./prompts.js";
|
|
41
|
+
import {
|
|
42
|
+
buildBookmarkFrontmatter,
|
|
43
|
+
buildFrontmatter,
|
|
44
|
+
DREAM_MIN_HOURS,
|
|
45
|
+
DREAM_MIN_SESSIONS,
|
|
46
|
+
ENTRYPOINT_NAME,
|
|
47
|
+
formatManifest,
|
|
48
|
+
getEntrypointPath,
|
|
49
|
+
getMemoryDir,
|
|
50
|
+
isBookmarkType,
|
|
51
|
+
MAX_MEMORY_BYTES_PER_FILE,
|
|
52
|
+
MAX_RELEVANT_MEMORIES,
|
|
53
|
+
type MemoryHeader,
|
|
54
|
+
type MemoryType,
|
|
55
|
+
scanMemoryFiles,
|
|
56
|
+
truncateEntrypoint,
|
|
57
|
+
} from "./utils.js";
|
|
58
|
+
|
|
59
|
+
type CallLLMFn = (options: CallLLMOptions) => Promise<string>;
|
|
60
|
+
|
|
61
|
+
function serializeMessages(messages: AgentMessage[], options?: { lastN?: number }): string {
|
|
62
|
+
const slice = options?.lastN ? messages.slice(-options.lastN) : messages;
|
|
63
|
+
return slice
|
|
64
|
+
.map((m) => {
|
|
65
|
+
if (m.role === "user" || m.role === "assistant") {
|
|
66
|
+
const text = Array.isArray(m.content)
|
|
67
|
+
? (m.content as Array<{ type: string; text?: string }>)
|
|
68
|
+
.filter(
|
|
69
|
+
(c: { type: string; text?: string }): c is { type: "text"; text: string } => c.type === "text",
|
|
70
|
+
)
|
|
71
|
+
.map((c: { type: "text"; text: string }) => c.text)
|
|
72
|
+
.join("\n")
|
|
73
|
+
: String(m.content);
|
|
74
|
+
return `[${m.role}]: ${text}`;
|
|
75
|
+
}
|
|
76
|
+
return null;
|
|
77
|
+
})
|
|
78
|
+
.filter(Boolean)
|
|
79
|
+
.join("\n");
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function buildPrefetchUserMessage(query: string, manifest: string, rules: SkipRule[], history: HistoryEntry[]): string {
|
|
83
|
+
const customRules = rules.filter((r) => !r.builtin);
|
|
84
|
+
const rulesSummary = customRules.length > 0
|
|
85
|
+
? customRules
|
|
86
|
+
.map((r) => `{ "pattern": "${r.pattern}", "mode": "${r.mode}", "action": "${r.action}" }`)
|
|
87
|
+
.join("\n")
|
|
88
|
+
: "(no custom rules)";
|
|
89
|
+
|
|
90
|
+
const historySummary = JSON.stringify(
|
|
91
|
+
history.map((h) => ({
|
|
92
|
+
query: h.query,
|
|
93
|
+
selected: h.selected,
|
|
94
|
+
skipped: h.skipped,
|
|
95
|
+
})),
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
return `## 当前查询\n${query}\n\n## 可用文件\n${manifest}\n\n## 自定义规则库\n${rulesSummary}\n\n## 最近 Prefetch 历史\n${historySummary}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
interface PrefetchDebugInfo {
|
|
102
|
+
selectedFiles: string[];
|
|
103
|
+
durationMs: number;
|
|
104
|
+
layer: "skip" | "llm" | "none";
|
|
105
|
+
skipHits: Array<{ pattern: string; mode: string }>;
|
|
106
|
+
guardHits: Array<{ pattern: string; mode: string }>;
|
|
107
|
+
availableFiles: number;
|
|
108
|
+
query: string;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const PREFETCH_MIN_INTERVAL_MS = 30_000;
|
|
112
|
+
const PREFETCH_REPEAT_THRESHOLD = 3;
|
|
113
|
+
|
|
114
|
+
class MemoryPrefetch {
|
|
115
|
+
private promise: Promise<string> | null = null;
|
|
116
|
+
private settled = false;
|
|
117
|
+
private result: string | null = null;
|
|
118
|
+
private lastSelected: string[] = [];
|
|
119
|
+
private resultEntryWritten = false;
|
|
120
|
+
private store: SkipWordStore | null = null;
|
|
121
|
+
private _debugInfo: PrefetchDebugInfo | null = null;
|
|
122
|
+
private lastPrefetchTime = 0;
|
|
123
|
+
private consecutiveSameCount = 0;
|
|
124
|
+
|
|
125
|
+
get debugInfo(): PrefetchDebugInfo | null {
|
|
126
|
+
return this._debugInfo;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
markResultEntryWritten(): boolean {
|
|
130
|
+
if (this.resultEntryWritten) return true;
|
|
131
|
+
this.resultEntryWritten = true;
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
start(query: string, memoryDir: string, callLLM: CallLLMFn): void {
|
|
136
|
+
const now = Date.now();
|
|
137
|
+
const elapsed = now - this.lastPrefetchTime;
|
|
138
|
+
|
|
139
|
+
if (this.lastPrefetchTime > 0 && elapsed < PREFETCH_MIN_INTERVAL_MS) {
|
|
140
|
+
this._debugInfo = {
|
|
141
|
+
selectedFiles: this.lastSelected,
|
|
142
|
+
durationMs: 0,
|
|
143
|
+
layer: "skip",
|
|
144
|
+
skipHits: [{ pattern: `min-interval(${Math.round(elapsed / 1000)}s<${PREFETCH_MIN_INTERVAL_MS / 1000}s)`, mode: "builtin" }],
|
|
145
|
+
guardHits: [],
|
|
146
|
+
availableFiles: 0,
|
|
147
|
+
query: query.slice(0, 200),
|
|
148
|
+
};
|
|
149
|
+
this.settled = true;
|
|
150
|
+
this.result = this.result ?? "";
|
|
151
|
+
this.resultEntryWritten = false;
|
|
152
|
+
this.promise = Promise.resolve(this.result);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (this.consecutiveSameCount >= PREFETCH_REPEAT_THRESHOLD && this.lastSelected.length > 0) {
|
|
157
|
+
this._debugInfo = {
|
|
158
|
+
selectedFiles: this.lastSelected,
|
|
159
|
+
durationMs: 0,
|
|
160
|
+
layer: "skip",
|
|
161
|
+
skipHits: [{ pattern: `repeat-detect(${this.consecutiveSameCount}x)`, mode: "builtin" }],
|
|
162
|
+
guardHits: [],
|
|
163
|
+
availableFiles: 0,
|
|
164
|
+
query: query.slice(0, 200),
|
|
165
|
+
};
|
|
166
|
+
this.settled = true;
|
|
167
|
+
this.resultEntryWritten = false;
|
|
168
|
+
this.lastPrefetchTime = now;
|
|
169
|
+
this.promise = this.runReadCached(this.lastSelected, memoryDir);
|
|
170
|
+
void this.promise.then((r) => {
|
|
171
|
+
this.result = r;
|
|
172
|
+
});
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const store = this.ensureStore();
|
|
177
|
+
const { shouldSkip, skipHits, guardHits } = evaluateRules(query, store.rules);
|
|
178
|
+
if (shouldSkip) {
|
|
179
|
+
const matchedRules = store.rules
|
|
180
|
+
.filter((r) => skipHits.includes(r.pattern) || guardHits.includes(r.pattern))
|
|
181
|
+
.map((r) => ({ pattern: r.pattern, mode: r.mode, action: r.action }));
|
|
182
|
+
const matchedSkip = matchedRules.filter((r) => r.action === "skip").map(({ pattern, mode }) => ({ pattern, mode }));
|
|
183
|
+
const matchedGuard = matchedRules.filter((r) => r.action !== "skip").map(({ pattern, mode }) => ({ pattern, mode }));
|
|
184
|
+
|
|
185
|
+
this._debugInfo = {
|
|
186
|
+
selectedFiles: [],
|
|
187
|
+
durationMs: 0,
|
|
188
|
+
layer: "skip",
|
|
189
|
+
skipHits: matchedSkip,
|
|
190
|
+
guardHits: matchedGuard,
|
|
191
|
+
availableFiles: 0,
|
|
192
|
+
query: query.slice(0, 200),
|
|
193
|
+
};
|
|
194
|
+
this.settled = true;
|
|
195
|
+
this.result = "";
|
|
196
|
+
this.resultEntryWritten = false;
|
|
197
|
+
this.lastPrefetchTime = now;
|
|
198
|
+
this.promise = Promise.resolve("");
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this.lastPrefetchTime = now;
|
|
203
|
+
this.settled = false;
|
|
204
|
+
this.result = null;
|
|
205
|
+
this._debugInfo = null;
|
|
206
|
+
this.resultEntryWritten = false;
|
|
207
|
+
this.promise = this.run(query, memoryDir, callLLM);
|
|
208
|
+
void this.promise.then((r) => {
|
|
209
|
+
this.result = r;
|
|
210
|
+
this.settled = true;
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
get started(): boolean {
|
|
215
|
+
return this.promise !== null;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
collect(): string | null {
|
|
219
|
+
return this.settled ? this.result : null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async awaitResult(timeoutMs = 30_000): Promise<string | null> {
|
|
223
|
+
if (this.promise) {
|
|
224
|
+
await Promise.race([
|
|
225
|
+
this.promise,
|
|
226
|
+
new Promise<void>((resolve) => setTimeout(resolve, timeoutMs)),
|
|
227
|
+
]);
|
|
228
|
+
}
|
|
229
|
+
return this.collect();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private ensureStore(): SkipWordStore {
|
|
233
|
+
if (!this.store) {
|
|
234
|
+
this.store = loadSkipWordStore(getGlobalMemoryDir());
|
|
235
|
+
}
|
|
236
|
+
return this.store;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async run(query: string, memoryDir: string, callLLM: CallLLMFn): Promise<string> {
|
|
240
|
+
try {
|
|
241
|
+
let store = this.ensureStore();
|
|
242
|
+
const { skipHits, guardHits } = evaluateRules(query, store.rules);
|
|
243
|
+
|
|
244
|
+
const matchedRules = store.rules
|
|
245
|
+
.filter((r) => skipHits.includes(r.pattern) || guardHits.includes(r.pattern))
|
|
246
|
+
.map((r) => ({ pattern: r.pattern, mode: r.mode, action: r.action }));
|
|
247
|
+
const matchedSkip = matchedRules.filter((r) => r.action === "skip").map(({ pattern, mode }) => ({ pattern, mode }));
|
|
248
|
+
const matchedGuard = matchedRules.filter((r) => r.action !== "skip").map(({ pattern, mode }) => ({ pattern, mode }));
|
|
249
|
+
|
|
250
|
+
const memories = await scanMemoryFiles(memoryDir);
|
|
251
|
+
if (memories.length === 0) {
|
|
252
|
+
this._debugInfo = {
|
|
253
|
+
selectedFiles: [],
|
|
254
|
+
durationMs: 0,
|
|
255
|
+
layer: "none",
|
|
256
|
+
skipHits: matchedSkip,
|
|
257
|
+
guardHits: matchedGuard,
|
|
258
|
+
availableFiles: 0,
|
|
259
|
+
query: query.slice(0, 200),
|
|
260
|
+
};
|
|
261
|
+
return "";
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const manifest = formatManifest(memories);
|
|
265
|
+
const recentHistory = store.history.slice(-3);
|
|
266
|
+
const startTime = Date.now();
|
|
267
|
+
|
|
268
|
+
const llmResult = await callLLM({
|
|
269
|
+
systemPrompt: SELECT_MEMORIES_PROMPT,
|
|
270
|
+
messages: [
|
|
271
|
+
{
|
|
272
|
+
role: "user",
|
|
273
|
+
content: buildPrefetchUserMessage(query, manifest, store.rules, recentHistory),
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
let parsed: { selected?: string[]; purification?: PurificationResult };
|
|
279
|
+
try {
|
|
280
|
+
parsed = JSON.parse(stripMarkdownCodeBlock(llmResult));
|
|
281
|
+
} catch (err) {
|
|
282
|
+
console.debug("[auto-memory] prefetch LLM parse failed:", err instanceof Error ? err.message : err);
|
|
283
|
+
this._debugInfo = {
|
|
284
|
+
selectedFiles: [],
|
|
285
|
+
durationMs: Date.now() - startTime,
|
|
286
|
+
layer: "llm",
|
|
287
|
+
skipHits: matchedSkip,
|
|
288
|
+
guardHits: matchedGuard,
|
|
289
|
+
availableFiles: memories.length,
|
|
290
|
+
query: query.slice(0, 200),
|
|
291
|
+
};
|
|
292
|
+
return "";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const selected = (parsed.selected ?? []).slice(0, MAX_RELEVANT_MEMORIES);
|
|
296
|
+
|
|
297
|
+
if (this.arraysEqual(selected, this.lastSelected)) {
|
|
298
|
+
this.consecutiveSameCount++;
|
|
299
|
+
} else {
|
|
300
|
+
this.consecutiveSameCount = 0;
|
|
301
|
+
}
|
|
302
|
+
this.lastSelected = selected;
|
|
303
|
+
|
|
304
|
+
if (parsed.purification && typeof parsed.purification === "object") {
|
|
305
|
+
try {
|
|
306
|
+
store = applyPurification(store, parsed.purification);
|
|
307
|
+
} catch (err) {
|
|
308
|
+
console.debug("[auto-memory] purification failed:", err instanceof Error ? err.message : err);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
store = addHistoryEntry(store, {
|
|
313
|
+
query: query.slice(0, 200),
|
|
314
|
+
selected,
|
|
315
|
+
skipped: false,
|
|
316
|
+
skip_hits: skipHits,
|
|
317
|
+
guard_hits: guardHits,
|
|
318
|
+
timestamp: Date.now(),
|
|
319
|
+
});
|
|
320
|
+
this.store = store;
|
|
321
|
+
await saveSkipWordStore(getGlobalMemoryDir(), this.store);
|
|
322
|
+
|
|
323
|
+
this._debugInfo = {
|
|
324
|
+
selectedFiles: selected,
|
|
325
|
+
durationMs: Date.now() - startTime,
|
|
326
|
+
layer: "llm",
|
|
327
|
+
skipHits: matchedSkip,
|
|
328
|
+
guardHits: matchedGuard,
|
|
329
|
+
availableFiles: memories.length,
|
|
330
|
+
query: query.slice(0, 200),
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
if (selected.length === 0) return "";
|
|
334
|
+
|
|
335
|
+
return await this.readFiles(selected, memoryDir);
|
|
336
|
+
} catch (err) {
|
|
337
|
+
console.debug("[auto-memory] prefetch failed:", err instanceof Error ? err.message : err);
|
|
338
|
+
this._debugInfo = {
|
|
339
|
+
selectedFiles: [],
|
|
340
|
+
durationMs: 0,
|
|
341
|
+
layer: "none",
|
|
342
|
+
skipHits: [],
|
|
343
|
+
guardHits: [],
|
|
344
|
+
availableFiles: 0,
|
|
345
|
+
query: query.slice(0, 200),
|
|
346
|
+
};
|
|
347
|
+
return "";
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private async readFiles(filenames: string[], memoryDir: string): Promise<string> {
|
|
352
|
+
const parts: string[] = [];
|
|
353
|
+
for (const name of filenames) {
|
|
354
|
+
try {
|
|
355
|
+
const content = await readFile(join(memoryDir, name), "utf-8");
|
|
356
|
+
parts.push(`### ${name}\n${content}`);
|
|
357
|
+
} catch (err) {
|
|
358
|
+
console.debug("[auto-memory] memory file read failed:", err instanceof Error ? err.message : err);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
return parts.join("\n\n");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
private async runReadCached(filenames: string[], memoryDir: string): Promise<string> {
|
|
365
|
+
return await this.readFiles(filenames, memoryDir);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
private arraysEqual(a: string[], b: string[]): boolean {
|
|
369
|
+
if (a.length !== b.length) return false;
|
|
370
|
+
const setB = new Set(b);
|
|
371
|
+
return a.every((item) => setB.has(item));
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
class MemoryExtractor {
|
|
376
|
+
private inProgress = false;
|
|
377
|
+
private pendingMessages: AgentMessage[] | null = null;
|
|
378
|
+
private turnsSinceLastExtraction = 0;
|
|
379
|
+
private mainAgentWroteMemory = false;
|
|
380
|
+
|
|
381
|
+
onToolCall(toolName: string, args: Record<string, unknown>, memoryDir: string): void {
|
|
382
|
+
if ((toolName === "write" || toolName === "edit") && typeof args.path === "string") {
|
|
383
|
+
const normalizedPath = args.path.replace(/\\/g, "/");
|
|
384
|
+
const normalizedDir = memoryDir.replace(/\\/g, "/");
|
|
385
|
+
if (normalizedPath.startsWith(`${normalizedDir}/`)) {
|
|
386
|
+
this.mainAgentWroteMemory = true;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async maybeExtract(
|
|
392
|
+
messages: AgentMessage[],
|
|
393
|
+
memoryDir: string,
|
|
394
|
+
callLLM: CallLLMFn,
|
|
395
|
+
): Promise<{ created: string[]; updated: string[] } | null> {
|
|
396
|
+
if (this.inProgress) {
|
|
397
|
+
this.pendingMessages = messages;
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (this.mainAgentWroteMemory) {
|
|
402
|
+
this.mainAgentWroteMemory = false;
|
|
403
|
+
this.turnsSinceLastExtraction = 0;
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
this.turnsSinceLastExtraction++;
|
|
408
|
+
if (this.turnsSinceLastExtraction < 2) return null;
|
|
409
|
+
this.turnsSinceLastExtraction = 0;
|
|
410
|
+
|
|
411
|
+
return await this.runExtraction(messages, memoryDir, callLLM);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
private async runExtraction(
|
|
415
|
+
messages: AgentMessage[],
|
|
416
|
+
memoryDir: string,
|
|
417
|
+
callLLM: CallLLMFn,
|
|
418
|
+
): Promise<{ created: string[]; updated: string[] } | null> {
|
|
419
|
+
this.inProgress = true;
|
|
420
|
+
try {
|
|
421
|
+
const recent = serializeMessages(messages, { lastN: 20 });
|
|
422
|
+
const manifest = formatManifest(await scanMemoryFiles(memoryDir));
|
|
423
|
+
|
|
424
|
+
const llmResult = await callLLM({
|
|
425
|
+
systemPrompt: EXTRACTION_PROMPT(manifest),
|
|
426
|
+
messages: [
|
|
427
|
+
{
|
|
428
|
+
role: "user",
|
|
429
|
+
content: `Recent conversation:\n${recent}\n\nExisting memories:\n${manifest}`,
|
|
430
|
+
},
|
|
431
|
+
],
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
let parsed: { actions?: Array<Record<string, string>> };
|
|
435
|
+
try {
|
|
436
|
+
parsed = JSON.parse(stripMarkdownCodeBlock(llmResult));
|
|
437
|
+
} catch (err) {
|
|
438
|
+
console.debug("[auto-memory] extraction LLM parse failed:", err instanceof Error ? err.message : err);
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const actions = parsed.actions ?? [];
|
|
443
|
+
if (actions.length === 0) return null;
|
|
444
|
+
|
|
445
|
+
const result = await this.applyActions(actions, memoryDir);
|
|
446
|
+
return result;
|
|
447
|
+
} finally {
|
|
448
|
+
this.inProgress = false;
|
|
449
|
+
if (this.pendingMessages) {
|
|
450
|
+
const pending = this.pendingMessages;
|
|
451
|
+
this.pendingMessages = null;
|
|
452
|
+
await this.runExtraction(pending, memoryDir, callLLM);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
private async applyActions(
|
|
458
|
+
actions: Array<Record<string, string>>,
|
|
459
|
+
memoryDir: string,
|
|
460
|
+
): Promise<{ created: string[]; updated: string[] }> {
|
|
461
|
+
const created: string[] = [];
|
|
462
|
+
const updated: string[] = [];
|
|
463
|
+
for (const action of actions) {
|
|
464
|
+
const op = action.op;
|
|
465
|
+
|
|
466
|
+
if (op === "create") {
|
|
467
|
+
const filename = action.filename;
|
|
468
|
+
const content = action.content ?? "";
|
|
469
|
+
if (!filename || !content) continue;
|
|
470
|
+
|
|
471
|
+
const name = action.name ?? filename;
|
|
472
|
+
const description = action.description ?? "";
|
|
473
|
+
const type = (action.type as MemoryType) ?? "project";
|
|
474
|
+
const fm = buildFrontmatter({ name, description, type });
|
|
475
|
+
const body = content.slice(0, MAX_MEMORY_BYTES_PER_FILE);
|
|
476
|
+
await writeFile(join(memoryDir, filename), `${fm}\n\n${body}`);
|
|
477
|
+
created.push(filename);
|
|
478
|
+
} else if (op === "update") {
|
|
479
|
+
const filename = action.filename;
|
|
480
|
+
const append = action.append;
|
|
481
|
+
if (!filename || !append) continue;
|
|
482
|
+
|
|
483
|
+
const filePath = join(memoryDir, filename);
|
|
484
|
+
if (!existsSync(filePath)) continue;
|
|
485
|
+
|
|
486
|
+
const existing = await readFile(filePath, "utf-8");
|
|
487
|
+
await writeFile(filePath, existing + append);
|
|
488
|
+
updated.push(filename);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
await updateMemoryIndex(memoryDir);
|
|
492
|
+
return { created, updated };
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
class BookmarkCreator {
|
|
497
|
+
registerTool(pi: ExtensionAPI): void {
|
|
498
|
+
pi.registerTool({
|
|
499
|
+
name: "create_bookmark",
|
|
500
|
+
label: "create_bookmark",
|
|
501
|
+
description:
|
|
502
|
+
"Create a bookmark memory file from analyzed content. Use this tool to save a structured bookmark with title, description, summary and tags.",
|
|
503
|
+
parameters: Type.Object({
|
|
504
|
+
title: Type.String({ description: "Bookmark title, concise and descriptive" }),
|
|
505
|
+
description: Type.String({ description: "One-line description of the bookmark" }),
|
|
506
|
+
summary: Type.String({ description: "Detailed summary of the bookmarked content" }),
|
|
507
|
+
tags: Type.Array(Type.String(), { description: "Relevant tags for categorization" }),
|
|
508
|
+
}),
|
|
509
|
+
execute: async (
|
|
510
|
+
_toolCallId: string,
|
|
511
|
+
_params: { title: string; description: string; summary: string; tags: string[] },
|
|
512
|
+
_signal?: AbortSignal,
|
|
513
|
+
_onUpdate?: unknown,
|
|
514
|
+
_ctx?: ExtensionContext,
|
|
515
|
+
): Promise<AgentToolResult<void>> => {
|
|
516
|
+
return { content: [{ type: "text", text: "Not used in JSON mode" }], details: undefined };
|
|
517
|
+
},
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async create(
|
|
522
|
+
messageContent: string,
|
|
523
|
+
sessionId: string,
|
|
524
|
+
messageIds: string[],
|
|
525
|
+
memoryDir: string,
|
|
526
|
+
callLLM: (opts: CallLLMOptions) => Promise<string>,
|
|
527
|
+
): Promise<{ filename: string; filePath: string } | null> {
|
|
528
|
+
try {
|
|
529
|
+
const manifest = formatManifest((await scanMemoryFiles(memoryDir)).filter((m) => isBookmarkType(m)));
|
|
530
|
+
|
|
531
|
+
const llmResult = await callLLM({
|
|
532
|
+
systemPrompt: BOOKMARK_SUMMARY_PROMPT(messageContent, manifest),
|
|
533
|
+
messages: [{ role: "user", content: "Create a bookmark summary for this content." }],
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
let parsed: { title?: string; description?: string; summary?: string; tags?: string[] };
|
|
537
|
+
try {
|
|
538
|
+
parsed = JSON.parse(stripMarkdownCodeBlock(llmResult));
|
|
539
|
+
} catch (err) {
|
|
540
|
+
console.debug("[auto-memory] bookmark LLM parse failed:", err instanceof Error ? err.message : err);
|
|
541
|
+
return null;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (!parsed.title) return null;
|
|
545
|
+
|
|
546
|
+
const safeTitle = parsed.title.replace(/[^a-zA-Z0-9\u4e00-\u9fff_-]/g, "_").slice(0, 50);
|
|
547
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
548
|
+
const filename = `${timestamp}_${safeTitle}.md`;
|
|
549
|
+
const filePath = join(memoryDir, filename);
|
|
550
|
+
|
|
551
|
+
const fm = buildBookmarkFrontmatter({
|
|
552
|
+
name: parsed.title,
|
|
553
|
+
description: parsed.description ?? "",
|
|
554
|
+
sourceSession: sessionId,
|
|
555
|
+
sourceMessageIds: messageIds,
|
|
556
|
+
tags: parsed.tags ?? [],
|
|
557
|
+
createdAt: new Date().toISOString(),
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
const body = `## ${parsed.title}\n\n${parsed.summary ?? ""}\n\n---\n\n## \u539F\u59CB\u5185\u5BB9\u9884\u89C8\n\n> ${messageContent.slice(0, 500)}${messageContent.length > 500 ? "..." : ""}`;
|
|
561
|
+
|
|
562
|
+
await writeFile(filePath, `${fm}\n\n${body}`);
|
|
563
|
+
await updateMemoryIndex(memoryDir);
|
|
564
|
+
|
|
565
|
+
return { filename, filePath };
|
|
566
|
+
} catch (err) {
|
|
567
|
+
console.debug("[auto-memory] bookmark creation failed:", err instanceof Error ? err.message : err);
|
|
568
|
+
return null;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
class MemoryDream {
|
|
574
|
+
async maybeRun(
|
|
575
|
+
memoryDir: string,
|
|
576
|
+
callLLM: CallLLMFn,
|
|
577
|
+
): Promise<{ merges: number; deletions: number; updates: number } | null> {
|
|
578
|
+
const lockPath = join(memoryDir, ".consolidate-lock");
|
|
579
|
+
|
|
580
|
+
if (!existsSync(lockPath)) {
|
|
581
|
+
await writeFile(lockPath, "");
|
|
582
|
+
await utimes(lockPath, new Date(0), new Date(0));
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
let lockStat: Stats;
|
|
586
|
+
try {
|
|
587
|
+
lockStat = await stat(lockPath);
|
|
588
|
+
} catch (err) {
|
|
589
|
+
console.debug("[auto-memory] dream lock stat failed:", err instanceof Error ? err.message : err);
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
const hoursSince = (Date.now() - lockStat.mtimeMs) / 3_600_000;
|
|
593
|
+
if (hoursSince < DREAM_MIN_HOURS) return null;
|
|
594
|
+
|
|
595
|
+
const sessionCount = await countSessionsSince(memoryDir, lockStat.mtimeMs);
|
|
596
|
+
if (sessionCount < DREAM_MIN_SESSIONS) return null;
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
const result = await this.runDream(memoryDir, callLLM);
|
|
600
|
+
await utimes(lockPath, new Date(), new Date());
|
|
601
|
+
return result;
|
|
602
|
+
} catch (err) {
|
|
603
|
+
console.debug("[auto-memory] dream consolidation failed:", err instanceof Error ? err.message : err);
|
|
604
|
+
await utimes(lockPath, new Date(lockStat.mtimeMs), new Date(lockStat.mtimeMs));
|
|
605
|
+
return null;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
private async runDream(
|
|
610
|
+
memoryDir: string,
|
|
611
|
+
callLLM: CallLLMFn,
|
|
612
|
+
): Promise<{ merges: number; deletions: number; updates: number } | null> {
|
|
613
|
+
const memories = await scanMemoryFiles(memoryDir);
|
|
614
|
+
if (memories.length === 0) return null;
|
|
615
|
+
|
|
616
|
+
const allContent = await readAllMemories(memories);
|
|
617
|
+
const entrypointPath = join(memoryDir, ENTRYPOINT_NAME);
|
|
618
|
+
let indexContent = "";
|
|
619
|
+
try {
|
|
620
|
+
indexContent = await readFile(entrypointPath, "utf-8");
|
|
621
|
+
} catch (err) {
|
|
622
|
+
console.debug("[auto-memory] dream entrypoint read failed:", err instanceof Error ? err.message : err);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const llmResult = await callLLM({
|
|
626
|
+
systemPrompt: DREAM_PROMPT(allContent, indexContent, memoryDir),
|
|
627
|
+
messages: [
|
|
628
|
+
{
|
|
629
|
+
role: "user",
|
|
630
|
+
content: "Perform dream consolidation. Analyze memories and decide what to merge, delete, or update.",
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
let parsed: {
|
|
636
|
+
merges?: Array<{ sources?: string[]; target?: string; content?: string }>;
|
|
637
|
+
deletions?: string[];
|
|
638
|
+
updates?: Array<{ filename?: string; newContent?: string }>;
|
|
639
|
+
newIndex?: string;
|
|
640
|
+
};
|
|
641
|
+
try {
|
|
642
|
+
parsed = JSON.parse(stripMarkdownCodeBlock(llmResult));
|
|
643
|
+
} catch (err) {
|
|
644
|
+
console.debug("[auto-memory] dream LLM parse failed:", err instanceof Error ? err.message : err);
|
|
645
|
+
return null;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return await this.applyDreamActions(parsed, memoryDir);
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
private async applyDreamActions(
|
|
652
|
+
parsed: {
|
|
653
|
+
merges?: Array<{ sources?: string[]; target?: string; content?: string }>;
|
|
654
|
+
deletions?: string[];
|
|
655
|
+
updates?: Array<{ filename?: string; newContent?: string }>;
|
|
656
|
+
newIndex?: string;
|
|
657
|
+
},
|
|
658
|
+
memoryDir: string,
|
|
659
|
+
): Promise<{ merges: number; deletions: number; updates: number }> {
|
|
660
|
+
const allHeaders = await scanMemoryFiles(memoryDir);
|
|
661
|
+
const bookmarkSet = new Set(allHeaders.filter(isBookmarkType).map((h) => h.filename));
|
|
662
|
+
|
|
663
|
+
if (parsed.merges) {
|
|
664
|
+
for (const merge of parsed.merges) {
|
|
665
|
+
if (!merge.sources || !merge.target || merge.content === undefined) continue;
|
|
666
|
+
|
|
667
|
+
const sources = merge.sources;
|
|
668
|
+
const hasBookmark = sources.some((s) => bookmarkSet.has(s));
|
|
669
|
+
const hasNonBookmark = sources.some((s) => !bookmarkSet.has(s));
|
|
670
|
+
if (hasBookmark && hasNonBookmark) continue;
|
|
671
|
+
|
|
672
|
+
await writeFile(join(memoryDir, merge.target), merge.content);
|
|
673
|
+
for (const source of sources) {
|
|
674
|
+
if (source === merge.target) continue;
|
|
675
|
+
const sourcePath = join(memoryDir, source);
|
|
676
|
+
if (existsSync(sourcePath)) {
|
|
677
|
+
await unlink(sourcePath);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
if (parsed.deletions) {
|
|
684
|
+
for (const filename of parsed.deletions) {
|
|
685
|
+
if (bookmarkSet.has(filename)) continue;
|
|
686
|
+
const filePath = join(memoryDir, filename);
|
|
687
|
+
if (existsSync(filePath)) {
|
|
688
|
+
await unlink(filePath);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (parsed.updates) {
|
|
694
|
+
for (const update of parsed.updates) {
|
|
695
|
+
if (!update.filename || !update.newContent) continue;
|
|
696
|
+
await writeFile(join(memoryDir, update.filename), update.newContent);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const mergeCount = parsed.merges?.length ?? 0;
|
|
701
|
+
const deletionCount = parsed.deletions?.length ?? 0;
|
|
702
|
+
const updateCount = parsed.updates?.length ?? 0;
|
|
703
|
+
|
|
704
|
+
if (parsed.newIndex !== undefined) {
|
|
705
|
+
const { content } = truncateEntrypoint(parsed.newIndex);
|
|
706
|
+
await writeFile(join(memoryDir, ENTRYPOINT_NAME), content);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
return { merges: mergeCount, deletions: deletionCount, updates: updateCount };
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async function readAllMemories(memories: MemoryHeader[]): Promise<string> {
|
|
714
|
+
const parts = await Promise.all(
|
|
715
|
+
memories.map(async (m) => {
|
|
716
|
+
const content = await readFile(m.filePath, "utf-8");
|
|
717
|
+
return `=== ${m.filename} ===\n${content}`;
|
|
718
|
+
}),
|
|
719
|
+
);
|
|
720
|
+
return parts.join("\n\n");
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
async function updateMemoryIndex(memoryDir: string): Promise<void> {
|
|
724
|
+
const memories = await scanMemoryFiles(memoryDir);
|
|
725
|
+
const lines = memories.map((m) => {
|
|
726
|
+
const title = m.filename.replace(/\.md$/, "");
|
|
727
|
+
const desc = m.description ? ` — ${m.description}` : "";
|
|
728
|
+
const line = `- [${title}](./${m.filename})${desc}`;
|
|
729
|
+
return line.slice(0, 150);
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const truncated = truncateEntrypoint(lines.join("\n"));
|
|
733
|
+
await writeFile(join(memoryDir, ENTRYPOINT_NAME), truncated.content);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
async function countSessionsSince(memoryDir: string, _sinceMs: number): Promise<number> {
|
|
737
|
+
try {
|
|
738
|
+
const sessionsPath = join(memoryDir, ".session-count");
|
|
739
|
+
if (!existsSync(sessionsPath)) {
|
|
740
|
+
await writeFile(sessionsPath, "1");
|
|
741
|
+
return 1;
|
|
742
|
+
}
|
|
743
|
+
const content = await readFile(sessionsPath, "utf-8");
|
|
744
|
+
const count = Number.parseInt(content.trim(), 10) || 0;
|
|
745
|
+
await writeFile(sessionsPath, String(count + 1));
|
|
746
|
+
return count + 1;
|
|
747
|
+
} catch (err) {
|
|
748
|
+
console.debug("[auto-memory] session count update failed:", err instanceof Error ? err.message : err);
|
|
749
|
+
return DREAM_MIN_SESSIONS;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
export {
|
|
754
|
+
MemoryPrefetch,
|
|
755
|
+
MemoryExtractor,
|
|
756
|
+
MemoryDream,
|
|
757
|
+
BookmarkCreator,
|
|
758
|
+
serializeMessages,
|
|
759
|
+
updateMemoryIndex,
|
|
760
|
+
type CallLLMFn,
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
export default function autoMemoryExtension(pi: ExtensionAPI): void {
|
|
764
|
+
const cwd = process.cwd();
|
|
765
|
+
const memoryDir = getMemoryDir(cwd);
|
|
766
|
+
const prefetch = new MemoryPrefetch();
|
|
767
|
+
const extractor = new MemoryExtractor();
|
|
768
|
+
const dream = new MemoryDream();
|
|
769
|
+
const bookmarkCreator = new BookmarkCreator();
|
|
770
|
+
let draining = false;
|
|
771
|
+
let activeExtraction: Promise<void> | null = null;
|
|
772
|
+
let ctx: ExtensionContext | null = null;
|
|
773
|
+
|
|
774
|
+
const callLLMWithRetry: CallLLMFn = async (opts) => {
|
|
775
|
+
const MAX_RETRIES = 100;
|
|
776
|
+
const RETRY_DELAY_MS = 5_000;
|
|
777
|
+
for (let attempt = 0; ; attempt++) {
|
|
778
|
+
try {
|
|
779
|
+
return await pi.callLLM(opts);
|
|
780
|
+
} catch (err) {
|
|
781
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
782
|
+
const isRateLimit = /429|rate.?limit|too.?many.?request|quota/i.test(msg);
|
|
783
|
+
if (!isRateLimit || attempt >= MAX_RETRIES) throw err;
|
|
784
|
+
console.error(`[callLLM] rate limited (attempt ${attempt + 1}/${MAX_RETRIES}), retrying in ${RETRY_DELAY_MS}ms`);
|
|
785
|
+
await new Promise((r) => setTimeout(r, RETRY_DELAY_MS));
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
};
|
|
789
|
+
|
|
790
|
+
bookmarkCreator.registerTool(pi);
|
|
791
|
+
|
|
792
|
+
const rawMemoryChannel = pi.registerChannel("memory");
|
|
793
|
+
const memoryChannel = createTypedChannel<MemoryChannelContract>(rawMemoryChannel).server;
|
|
794
|
+
|
|
795
|
+
function status(msg?: string): void {
|
|
796
|
+
ctx?.ui.setStatus("auto-memory", msg);
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function notify(message: string, type?: "info" | "warning" | "error"): void {
|
|
800
|
+
ctx?.ui.notify(message, type);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
pi.on("session_start", async (_event, context) => {
|
|
804
|
+
ctx = context as ExtensionContext;
|
|
805
|
+
await mkdir(memoryDir, { recursive: true });
|
|
806
|
+
status("memory ready");
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
pi.on("before_agent_start", async (event) => {
|
|
810
|
+
let memoryContent = "";
|
|
811
|
+
try {
|
|
812
|
+
memoryContent = await readFile(getEntrypointPath(cwd), "utf-8");
|
|
813
|
+
} catch (err) {
|
|
814
|
+
console.debug("[auto-memory] entrypoint read failed:", err instanceof Error ? err.message : err);
|
|
815
|
+
}
|
|
816
|
+
const truncated = truncateEntrypoint(memoryContent);
|
|
817
|
+
const memoryPrompt = MEMORY_SYSTEM_PROMPT(memoryDir, truncated.content);
|
|
818
|
+
|
|
819
|
+
const lastUserText = event.prompt ?? "";
|
|
820
|
+
if (lastUserText) {
|
|
821
|
+
status("selecting memories...");
|
|
822
|
+
pi.appendEntry("memory_prefetch", {
|
|
823
|
+
query: lastUserText.slice(0, 200),
|
|
824
|
+
memoryDir,
|
|
825
|
+
availableFiles: (await scanMemoryFiles(memoryDir)).length,
|
|
826
|
+
});
|
|
827
|
+
prefetch.start(lastUserText, memoryDir, callLLMWithRetry);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
return { systemPrompt: `${event.systemPrompt}\n\n${memoryPrompt}` };
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
pi.on("context", async (event) => {
|
|
834
|
+
const memoryText = await prefetch.awaitResult();
|
|
835
|
+
const debug = prefetch.debugInfo;
|
|
836
|
+
|
|
837
|
+
if (!prefetch.markResultEntryWritten() && prefetch.started) {
|
|
838
|
+
status(memoryText ? "memories injected" : "no memories found");
|
|
839
|
+
pi.appendEntry("memory_prefetch_result", {
|
|
840
|
+
summary: memoryText ? "Injected relevant memories" : "No relevant memories",
|
|
841
|
+
snippet: memoryText ? memoryText.slice(0, 500) : "",
|
|
842
|
+
injectedBytes: memoryText ? memoryText.length : 0,
|
|
843
|
+
selectedFiles: debug?.selectedFiles ?? [],
|
|
844
|
+
durationMs: debug?.durationMs ?? 0,
|
|
845
|
+
layer: debug?.layer ?? "unknown",
|
|
846
|
+
skipHits: debug?.skipHits ?? [],
|
|
847
|
+
guardHits: debug?.guardHits ?? [],
|
|
848
|
+
availableFiles: debug?.availableFiles ?? 0,
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
if (!memoryText) return;
|
|
853
|
+
|
|
854
|
+
const memoryMessage = {
|
|
855
|
+
role: "user" as const,
|
|
856
|
+
content: [{ type: "text" as const, text: `[Memory context — relevant memories]\n\n${memoryText}` }],
|
|
857
|
+
timestamp: Date.now(),
|
|
858
|
+
};
|
|
859
|
+
return { messages: [...event.messages, memoryMessage] };
|
|
860
|
+
});
|
|
861
|
+
|
|
862
|
+
pi.on("tool_call", (event) => {
|
|
863
|
+
const args = (event.input ?? {}) as Record<string, unknown>;
|
|
864
|
+
extractor.onToolCall(event.toolName, args, memoryDir);
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
pi.on("agent_end", (event) => {
|
|
868
|
+
if (draining) return;
|
|
869
|
+
activeExtraction = (async () => {
|
|
870
|
+
try {
|
|
871
|
+
status("extracting memories...");
|
|
872
|
+
const extractResult = await extractor.maybeExtract(event.messages, memoryDir, callLLMWithRetry);
|
|
873
|
+
if (extractResult) {
|
|
874
|
+
pi.appendEntry("memory_extract", {
|
|
875
|
+
status: "completed",
|
|
876
|
+
created: extractResult.created,
|
|
877
|
+
updated: extractResult.updated,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
status("consolidating memories...");
|
|
882
|
+
const dreamResult = await dream.maybeRun(memoryDir, callLLMWithRetry);
|
|
883
|
+
if (dreamResult) {
|
|
884
|
+
pi.appendEntry("memory_dream", {
|
|
885
|
+
status: "completed",
|
|
886
|
+
merges: dreamResult.merges,
|
|
887
|
+
deletions: dreamResult.deletions,
|
|
888
|
+
updates: dreamResult.updates,
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
status("memory idle");
|
|
893
|
+
} catch (e) {
|
|
894
|
+
status("memory error");
|
|
895
|
+
notify(`Auto-memory error: ${e instanceof Error ? e.message : String(e)}`, "warning");
|
|
896
|
+
}
|
|
897
|
+
})();
|
|
898
|
+
});
|
|
899
|
+
|
|
900
|
+
pi.on("session_shutdown", async () => {
|
|
901
|
+
draining = true;
|
|
902
|
+
if (activeExtraction) {
|
|
903
|
+
status("draining memory...");
|
|
904
|
+
const timeout = new Promise<void>((resolve) => setTimeout(resolve, 10_000));
|
|
905
|
+
await Promise.race([activeExtraction, timeout]);
|
|
906
|
+
}
|
|
907
|
+
status(undefined);
|
|
908
|
+
});
|
|
909
|
+
|
|
910
|
+
memoryChannel.handle("memory.list", async () => {
|
|
911
|
+
try {
|
|
912
|
+
const memories = await scanMemoryFiles(memoryDir);
|
|
913
|
+
const files = memories.map((m) => ({
|
|
914
|
+
filename: m.filename,
|
|
915
|
+
filePath: m.filePath,
|
|
916
|
+
description: m.description ?? null,
|
|
917
|
+
type: m.type ?? null,
|
|
918
|
+
mtimeMs: m.mtimeMs,
|
|
919
|
+
}));
|
|
920
|
+
let entrypointContent: string | null = null;
|
|
921
|
+
try {
|
|
922
|
+
entrypointContent = await readFile(getEntrypointPath(cwd), "utf-8");
|
|
923
|
+
} catch (err) {
|
|
924
|
+
console.debug("[auto-memory] entrypoint read failed:", err instanceof Error ? err.message : err);
|
|
925
|
+
}
|
|
926
|
+
return { type: "list_result" as const, files, entrypointContent, memoryDir };
|
|
927
|
+
} catch (err) {
|
|
928
|
+
console.debug("[auto-memory] memory list failed:", err instanceof Error ? err.message : err);
|
|
929
|
+
return { type: "list_result" as const, files: [], entrypointContent: null, memoryDir };
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
memoryChannel.handle("memory.userRemember", async (data) => {
|
|
934
|
+
memoryChannel.emit("bookmark_creating", { type: "bookmark_creating" });
|
|
935
|
+
pi.appendEntry("memory_creating", { content: data.content?.slice(0, 200) });
|
|
936
|
+
try {
|
|
937
|
+
const result = await bookmarkCreator.create(
|
|
938
|
+
data.content ?? "",
|
|
939
|
+
data.sourceSessionId ?? "",
|
|
940
|
+
data.sourceMessageIds ?? [],
|
|
941
|
+
memoryDir,
|
|
942
|
+
callLLMWithRetry,
|
|
943
|
+
);
|
|
944
|
+
if (result) {
|
|
945
|
+
pi.appendEntry("memory_created", result);
|
|
946
|
+
const updatedMemories = await scanMemoryFiles(memoryDir);
|
|
947
|
+
memoryChannel.emit("memory_updated", {
|
|
948
|
+
type: "memory_updated",
|
|
949
|
+
files: updatedMemories.map((m) => ({
|
|
950
|
+
filename: m.filename,
|
|
951
|
+
filePath: m.filePath,
|
|
952
|
+
description: m.description ?? null,
|
|
953
|
+
type: m.type ?? null,
|
|
954
|
+
mtimeMs: m.mtimeMs,
|
|
955
|
+
})),
|
|
956
|
+
});
|
|
957
|
+
} else {
|
|
958
|
+
pi.appendEntry("memory_failed", { reason: "LLM failed" });
|
|
959
|
+
memoryChannel.emit("memory_update_failed", { type: "memory_update_failed", reason: "LLM failed" });
|
|
960
|
+
}
|
|
961
|
+
} catch (e) {
|
|
962
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
963
|
+
pi.appendEntry("memory_failed", { reason: errMsg });
|
|
964
|
+
notify(`Bookmark error: ${errMsg}`, "warning");
|
|
965
|
+
memoryChannel.emit("memory_update_failed", { type: "memory_update_failed", reason: "Error" });
|
|
966
|
+
}
|
|
967
|
+
return { ok: true };
|
|
968
|
+
});
|
|
969
|
+
}
|