@chendpoc/pi-memory 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -0
- package/dist/adapters/piComplete.d.ts +17 -0
- package/dist/adapters/piComplete.d.ts.map +1 -0
- package/dist/adapters/piComplete.js +169 -0
- package/dist/adapters/piComplete.js.map +1 -0
- package/dist/bundle/install.d.ts +34 -0
- package/dist/bundle/install.d.ts.map +1 -0
- package/dist/bundle/install.js +183 -0
- package/dist/bundle/install.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +245 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +27 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +49 -0
- package/dist/config.js.map +1 -0
- package/dist/errclass.d.ts +7 -0
- package/dist/errclass.d.ts.map +1 -0
- package/dist/errclass.js +32 -0
- package/dist/errclass.js.map +1 -0
- package/dist/extension.d.ts +24 -0
- package/dist/extension.d.ts.map +1 -0
- package/dist/extension.js +7 -0
- package/dist/extension.js.map +1 -0
- package/dist/fallback/index.d.ts +11 -0
- package/dist/fallback/index.d.ts.map +1 -0
- package/dist/fallback/index.js +16 -0
- package/dist/fallback/index.js.map +1 -0
- package/dist/fallback/llmRerank.d.ts +19 -0
- package/dist/fallback/llmRerank.d.ts.map +1 -0
- package/dist/fallback/llmRerank.js +60 -0
- package/dist/fallback/llmRerank.js.map +1 -0
- package/dist/fallback/memoryMd.d.ts +6 -0
- package/dist/fallback/memoryMd.d.ts.map +1 -0
- package/dist/fallback/memoryMd.js +35 -0
- package/dist/fallback/memoryMd.js.map +1 -0
- package/dist/fallback/sessionIndex.d.ts +35 -0
- package/dist/fallback/sessionIndex.d.ts.map +1 -0
- package/dist/fallback/sessionIndex.js +222 -0
- package/dist/fallback/sessionIndex.js.map +1 -0
- package/dist/fallback/sessionSearch.d.ts +18 -0
- package/dist/fallback/sessionSearch.d.ts.map +1 -0
- package/dist/fallback/sessionSearch.js +161 -0
- package/dist/fallback/sessionSearch.js.map +1 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/paths.d.ts +7 -0
- package/dist/paths.d.ts.map +1 -0
- package/dist/paths.js +26 -0
- package/dist/paths.js.map +1 -0
- package/dist/pi-extension.d.ts +6 -0
- package/dist/pi-extension.d.ts.map +1 -0
- package/dist/pi-extension.js +224 -0
- package/dist/pi-extension.js.map +1 -0
- package/dist/preflight/detectIntents.d.ts +102 -0
- package/dist/preflight/detectIntents.d.ts.map +1 -0
- package/dist/preflight/detectIntents.js +624 -0
- package/dist/preflight/detectIntents.js.map +1 -0
- package/dist/preflight/hook.d.ts +58 -0
- package/dist/preflight/hook.d.ts.map +1 -0
- package/dist/preflight/hook.js +77 -0
- package/dist/preflight/hook.js.map +1 -0
- package/dist/preflight/render.d.ts +21 -0
- package/dist/preflight/render.d.ts.map +1 -0
- package/dist/preflight/render.js +132 -0
- package/dist/preflight/render.js.map +1 -0
- package/dist/preflight/strip.d.ts +11 -0
- package/dist/preflight/strip.d.ts.map +1 -0
- package/dist/preflight/strip.js +46 -0
- package/dist/preflight/strip.js.map +1 -0
- package/dist/service.d.ts +56 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +158 -0
- package/dist/service.js.map +1 -0
- package/dist/sidecar/bundle.d.ts +19 -0
- package/dist/sidecar/bundle.d.ts.map +1 -0
- package/dist/sidecar/bundle.js +39 -0
- package/dist/sidecar/bundle.js.map +1 -0
- package/dist/sidecar/client.d.ts +17 -0
- package/dist/sidecar/client.d.ts.map +1 -0
- package/dist/sidecar/client.js +107 -0
- package/dist/sidecar/client.js.map +1 -0
- package/dist/sidecar/process.d.ts +14 -0
- package/dist/sidecar/process.d.ts.map +1 -0
- package/dist/sidecar/process.js +126 -0
- package/dist/sidecar/process.js.map +1 -0
- package/dist/tools/memoryAppend.d.ts +37 -0
- package/dist/tools/memoryAppend.d.ts.map +1 -0
- package/dist/tools/memoryAppend.js +99 -0
- package/dist/tools/memoryAppend.js.map +1 -0
- package/dist/tools/memoryRecall.d.ts +113 -0
- package/dist/tools/memoryRecall.d.ts.map +1 -0
- package/dist/tools/memoryRecall.js +325 -0
- package/dist/tools/memoryRecall.js.map +1 -0
- package/dist/trainer/bundleBuilder.d.ts +30 -0
- package/dist/trainer/bundleBuilder.d.ts.map +1 -0
- package/dist/trainer/bundleBuilder.js +106 -0
- package/dist/trainer/bundleBuilder.js.map +1 -0
- package/dist/trainer/bundleLoader.d.ts +12 -0
- package/dist/trainer/bundleLoader.d.ts.map +1 -0
- package/dist/trainer/bundleLoader.js +59 -0
- package/dist/trainer/bundleLoader.js.map +1 -0
- package/dist/trainer/deltaMerge.d.ts +38 -0
- package/dist/trainer/deltaMerge.d.ts.map +1 -0
- package/dist/trainer/deltaMerge.js +183 -0
- package/dist/trainer/deltaMerge.js.map +1 -0
- package/dist/trainer/entityResolver.d.ts +27 -0
- package/dist/trainer/entityResolver.d.ts.map +1 -0
- package/dist/trainer/entityResolver.js +92 -0
- package/dist/trainer/entityResolver.js.map +1 -0
- package/dist/trainer/extractFacts.d.ts +67 -0
- package/dist/trainer/extractFacts.d.ts.map +1 -0
- package/dist/trainer/extractFacts.js +213 -0
- package/dist/trainer/extractFacts.js.map +1 -0
- package/dist/trainer/index.d.ts +54 -0
- package/dist/trainer/index.d.ts.map +1 -0
- package/dist/trainer/index.js +82 -0
- package/dist/trainer/index.js.map +1 -0
- package/dist/trainer/llmExtractor.d.ts +16 -0
- package/dist/trainer/llmExtractor.d.ts.map +1 -0
- package/dist/trainer/llmExtractor.js +146 -0
- package/dist/trainer/llmExtractor.js.map +1 -0
- package/dist/trainer/marker.d.ts +10 -0
- package/dist/trainer/marker.d.ts.map +1 -0
- package/dist/trainer/marker.js +28 -0
- package/dist/trainer/marker.js.map +1 -0
- package/dist/trainer/scheduler.d.ts +31 -0
- package/dist/trainer/scheduler.d.ts.map +1 -0
- package/dist/trainer/scheduler.js +72 -0
- package/dist/trainer/scheduler.js.map +1 -0
- package/dist/trainer/sessionLoader.d.ts +23 -0
- package/dist/trainer/sessionLoader.d.ts.map +1 -0
- package/dist/trainer/sessionLoader.js +106 -0
- package/dist/trainer/sessionLoader.js.map +1 -0
- package/dist/types.d.ts +135 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +78 -0
- package/src/adapters/piComplete.ts +233 -0
- package/src/bundle/install.ts +206 -0
- package/src/cli.ts +254 -0
- package/src/config.ts +92 -0
- package/src/errclass.ts +37 -0
- package/src/extension.ts +23 -0
- package/src/fallback/index.ts +24 -0
- package/src/fallback/llmRerank.ts +90 -0
- package/src/fallback/memoryMd.ts +36 -0
- package/src/fallback/sessionIndex.ts +289 -0
- package/src/fallback/sessionSearch.ts +181 -0
- package/src/index.ts +213 -0
- package/src/paths.ts +28 -0
- package/src/pi-extension.ts +276 -0
- package/src/preflight/detectIntents.ts +654 -0
- package/src/preflight/hook.ts +136 -0
- package/src/preflight/render.ts +185 -0
- package/src/preflight/strip.ts +50 -0
- package/src/service.ts +202 -0
- package/src/sidecar/bundle.ts +52 -0
- package/src/sidecar/client.ts +166 -0
- package/src/sidecar/process.ts +145 -0
- package/src/tools/memoryAppend.ts +113 -0
- package/src/tools/memoryRecall.ts +364 -0
- package/src/trainer/bundleBuilder.ts +192 -0
- package/src/trainer/bundleLoader.ts +105 -0
- package/src/trainer/deltaMerge.ts +221 -0
- package/src/trainer/entityResolver.ts +140 -0
- package/src/trainer/extractFacts.ts +312 -0
- package/src/trainer/index.ts +147 -0
- package/src/trainer/llmExtractor.ts +206 -0
- package/src/trainer/marker.ts +30 -0
- package/src/trainer/scheduler.ts +104 -0
- package/src/trainer/sessionLoader.ts +139 -0
- package/src/types.ts +168 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import http from "node:http";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
classifyHTTP,
|
|
6
|
+
classifyTransportError,
|
|
7
|
+
ErrTransport,
|
|
8
|
+
type ErrorClass,
|
|
9
|
+
} from "../errclass.js";
|
|
10
|
+
import type {
|
|
11
|
+
HealthPayload,
|
|
12
|
+
QueryIntent,
|
|
13
|
+
QueryRequest,
|
|
14
|
+
ReloadResponse,
|
|
15
|
+
ResponseEnvelope,
|
|
16
|
+
} from "../types.js";
|
|
17
|
+
|
|
18
|
+
function requestId(): string {
|
|
19
|
+
return `req-${randomBytes(6).toString("hex")}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class SidecarClient {
|
|
23
|
+
constructor(
|
|
24
|
+
private readonly socketPath: string,
|
|
25
|
+
private readonly timeoutMs: number,
|
|
26
|
+
) {}
|
|
27
|
+
|
|
28
|
+
async health(signal?: AbortSignal): Promise<HealthPayload> {
|
|
29
|
+
return this.do<HealthPayload>("GET", "/health", undefined, signal);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async query(
|
|
33
|
+
intent: QueryIntent,
|
|
34
|
+
signal?: AbortSignal,
|
|
35
|
+
): Promise<{
|
|
36
|
+
env: ResponseEnvelope | null;
|
|
37
|
+
errorClass: ErrorClass;
|
|
38
|
+
transportError?: Error;
|
|
39
|
+
}> {
|
|
40
|
+
const rid = requestId();
|
|
41
|
+
const body: QueryRequest = { intent, request_id: rid };
|
|
42
|
+
try {
|
|
43
|
+
const { status, data } = await this.doRaw<ResponseEnvelope>(
|
|
44
|
+
"POST",
|
|
45
|
+
"/query",
|
|
46
|
+
body,
|
|
47
|
+
signal,
|
|
48
|
+
rid,
|
|
49
|
+
);
|
|
50
|
+
return {
|
|
51
|
+
env: data,
|
|
52
|
+
errorClass: classifyHTTP(status, data),
|
|
53
|
+
};
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (err instanceof Error && err.message.includes("transport")) {
|
|
56
|
+
return {
|
|
57
|
+
env: null,
|
|
58
|
+
errorClass: classifyTransportError(err),
|
|
59
|
+
transportError: err,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
env: null,
|
|
64
|
+
errorClass: "unavailable",
|
|
65
|
+
transportError: err instanceof Error ? err : new Error(String(err)),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async reload(signal?: AbortSignal): Promise<ReloadResponse> {
|
|
71
|
+
return this.do<ReloadResponse>("POST", "/bundle/reload", {}, signal);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private async do<T>(
|
|
75
|
+
method: string,
|
|
76
|
+
path: string,
|
|
77
|
+
body: unknown,
|
|
78
|
+
signal?: AbortSignal,
|
|
79
|
+
requestIdHeader?: string,
|
|
80
|
+
): Promise<T> {
|
|
81
|
+
const { data } = await this.doRaw<T>(
|
|
82
|
+
method,
|
|
83
|
+
path,
|
|
84
|
+
body,
|
|
85
|
+
signal,
|
|
86
|
+
requestIdHeader,
|
|
87
|
+
);
|
|
88
|
+
return data;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private doRaw<T>(
|
|
92
|
+
method: string,
|
|
93
|
+
path: string,
|
|
94
|
+
body: unknown,
|
|
95
|
+
signal?: AbortSignal,
|
|
96
|
+
requestIdHeader?: string,
|
|
97
|
+
): Promise<{ status: number; data: T }> {
|
|
98
|
+
return new Promise((resolve, reject) => {
|
|
99
|
+
const payload = body != null ? JSON.stringify(body) : undefined;
|
|
100
|
+
const req = http.request(
|
|
101
|
+
{
|
|
102
|
+
socketPath: this.socketPath,
|
|
103
|
+
path,
|
|
104
|
+
method,
|
|
105
|
+
headers: {
|
|
106
|
+
"X-Request-ID": requestIdHeader ?? requestId(),
|
|
107
|
+
...(payload
|
|
108
|
+
? {
|
|
109
|
+
"Content-Type": "application/json",
|
|
110
|
+
"Content-Length": Buffer.byteLength(payload),
|
|
111
|
+
}
|
|
112
|
+
: {}),
|
|
113
|
+
},
|
|
114
|
+
timeout: this.timeoutMs,
|
|
115
|
+
},
|
|
116
|
+
(res) => {
|
|
117
|
+
const chunks: Buffer[] = [];
|
|
118
|
+
res.on("data", (c) => chunks.push(c));
|
|
119
|
+
res.on("end", () => {
|
|
120
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
121
|
+
if (!text) {
|
|
122
|
+
reject(new Error(`${ErrTransport.message}: empty body`));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
resolve({
|
|
127
|
+
status: res.statusCode ?? 0,
|
|
128
|
+
data: JSON.parse(text) as T,
|
|
129
|
+
});
|
|
130
|
+
} catch (e) {
|
|
131
|
+
reject(
|
|
132
|
+
new Error(
|
|
133
|
+
`${ErrTransport.message}: decode: ${e instanceof Error ? e.message : e}`,
|
|
134
|
+
),
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
req.on("timeout", () => {
|
|
142
|
+
req.destroy(new Error(`${ErrTransport.message}: timeout`));
|
|
143
|
+
});
|
|
144
|
+
req.on("error", (err) => {
|
|
145
|
+
reject(new Error(`${ErrTransport.message}: ${err.message}`));
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
if (signal) {
|
|
149
|
+
if (signal.aborted) {
|
|
150
|
+
req.destroy(new Error(`${ErrTransport.message}: aborted`));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
signal.addEventListener(
|
|
154
|
+
"abort",
|
|
155
|
+
() => {
|
|
156
|
+
req.destroy(new Error(`${ErrTransport.message}: aborted`));
|
|
157
|
+
},
|
|
158
|
+
{ once: true },
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (payload) req.write(payload);
|
|
163
|
+
req.end();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { access, constants } from "node:fs/promises";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
import type { MemoryConfig } from "../config.js";
|
|
7
|
+
import { SidecarClient } from "./client.js";
|
|
8
|
+
|
|
9
|
+
export class SidecarProcess {
|
|
10
|
+
private child: ChildProcess | null = null;
|
|
11
|
+
private readonly client: SidecarClient;
|
|
12
|
+
|
|
13
|
+
constructor(private readonly cfg: MemoryConfig) {
|
|
14
|
+
this.client = new SidecarClient(
|
|
15
|
+
cfg.socketPath,
|
|
16
|
+
Math.min(cfg.clientRequestTimeoutMs, 5_000),
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
getClient(): SidecarClient {
|
|
21
|
+
return this.client;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async resolveBinary(): Promise<string> {
|
|
25
|
+
const bin = this.cfg.tlmPath;
|
|
26
|
+
if (path.isAbsolute(bin)) {
|
|
27
|
+
await access(bin, constants.X_OK);
|
|
28
|
+
return bin;
|
|
29
|
+
}
|
|
30
|
+
// PATH lookup: spawn with shell on unix
|
|
31
|
+
return bin;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async spawn(): Promise<void> {
|
|
35
|
+
const bin = await this.resolveBinary().catch(() => {
|
|
36
|
+
throw new Error("memory: tlm binary not found");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (this.cfg.socketPath) {
|
|
40
|
+
try {
|
|
41
|
+
fs.unlinkSync(this.cfg.socketPath);
|
|
42
|
+
} catch {
|
|
43
|
+
/* absent */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const args = [
|
|
48
|
+
"serve",
|
|
49
|
+
"--socket",
|
|
50
|
+
this.cfg.socketPath,
|
|
51
|
+
"--bundle-root",
|
|
52
|
+
this.cfg.bundleRoot,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
this.child = spawn(bin, args, {
|
|
56
|
+
stdio: "ignore",
|
|
57
|
+
detached: process.platform !== "win32",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
this.child.on("error", (err) => {
|
|
61
|
+
console.error("[pi-memory] sidecar spawn error:", err.message);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
this.child.unref?.();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async waitReady(signal?: AbortSignal): Promise<void> {
|
|
68
|
+
const deadline = Date.now() + this.cfg.sidecarReadyTimeoutMs;
|
|
69
|
+
let lastCompat = "";
|
|
70
|
+
let lastSub = "";
|
|
71
|
+
|
|
72
|
+
while (Date.now() < deadline) {
|
|
73
|
+
if (signal?.aborted) throw signal.reason ?? new Error("aborted");
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const h = await this.client.health(signal);
|
|
77
|
+
lastCompat = h.compatibility ?? "";
|
|
78
|
+
lastSub = h.error ? String(h.error.details?.sub_code ?? "") : "";
|
|
79
|
+
|
|
80
|
+
if (h.ready === true) return;
|
|
81
|
+
if (h.compatibility === "unknown") return;
|
|
82
|
+
if (h.compatibility === "incompatible") {
|
|
83
|
+
// keep polling until ceiling — supervisor classifies schema mismatch
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
/* sidecar still starting */
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await sleep(500, signal);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
throw new Error(
|
|
93
|
+
`memory: sidecar ready ceiling exceeded` +
|
|
94
|
+
(lastCompat === "incompatible" && lastSub
|
|
95
|
+
? ` (incompatible: ${lastSub})`
|
|
96
|
+
: ""),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async stop(): Promise<void> {
|
|
101
|
+
const child = this.child;
|
|
102
|
+
this.child = null;
|
|
103
|
+
if (!child?.pid) return;
|
|
104
|
+
|
|
105
|
+
if (process.platform === "win32") {
|
|
106
|
+
spawn("taskkill", ["/pid", String(child.pid), "/t", "/f"]);
|
|
107
|
+
} else {
|
|
108
|
+
try {
|
|
109
|
+
process.kill(-child.pid, "SIGTERM");
|
|
110
|
+
} catch {
|
|
111
|
+
try {
|
|
112
|
+
child.kill("SIGTERM");
|
|
113
|
+
} catch {
|
|
114
|
+
/* already dead */
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await new Promise<void>((resolve) => {
|
|
120
|
+
const t = setTimeout(resolve, 2_000);
|
|
121
|
+
child.on("exit", () => {
|
|
122
|
+
clearTimeout(t);
|
|
123
|
+
resolve();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function sleep(ms: number, signal?: AbortSignal): Promise<void> {
|
|
130
|
+
return new Promise((resolve, reject) => {
|
|
131
|
+
if (signal?.aborted) {
|
|
132
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const t = setTimeout(resolve, ms);
|
|
136
|
+
signal?.addEventListener(
|
|
137
|
+
"abort",
|
|
138
|
+
() => {
|
|
139
|
+
clearTimeout(t);
|
|
140
|
+
reject(signal.reason ?? new Error("aborted"));
|
|
141
|
+
},
|
|
142
|
+
{ once: true },
|
|
143
|
+
);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { open } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import type { ToolResult } from "../types.js";
|
|
6
|
+
|
|
7
|
+
export const MEMORY_APPEND_NAME = "memory_append";
|
|
8
|
+
|
|
9
|
+
export const MEMORY_APPEND_DESCRIPTION =
|
|
10
|
+
"Append new entries to MEMORY.md. Use this instead of file_write or file_edit for memory updates. " +
|
|
11
|
+
"Writes are append-only with a simple lock to avoid concurrent clobber.";
|
|
12
|
+
|
|
13
|
+
export const MEMORY_APPEND_PROMPT_SNIPPET = "Append durable notes to MEMORY.md";
|
|
14
|
+
|
|
15
|
+
export const MEMORY_APPEND_PROMPT_GUIDELINES = [
|
|
16
|
+
"Use memory_append to persist user preferences or facts the user explicitly asked to remember — not for transient task state.",
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export const MEMORY_APPEND_PARAMETERS = {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
content: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description:
|
|
25
|
+
"New entries to append (markdown bullet points). Write in English by default.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
required: ["content"],
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
const LOCK_RETRIES = 50;
|
|
32
|
+
const LOCK_DELAY_MS = 20;
|
|
33
|
+
|
|
34
|
+
async function withAppendLock<T>(
|
|
35
|
+
lockPath: string,
|
|
36
|
+
fn: () => Promise<T>,
|
|
37
|
+
): Promise<T> {
|
|
38
|
+
let handle;
|
|
39
|
+
for (let i = 0; i < LOCK_RETRIES; i++) {
|
|
40
|
+
try {
|
|
41
|
+
handle = await open(lockPath, "wx");
|
|
42
|
+
break;
|
|
43
|
+
} catch {
|
|
44
|
+
await new Promise((r) => setTimeout(r, LOCK_DELAY_MS));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!handle) {
|
|
48
|
+
throw new Error("could not acquire MEMORY.md append lock");
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
return await fn();
|
|
52
|
+
} finally {
|
|
53
|
+
await handle.close();
|
|
54
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function appendToMemoryMd(
|
|
59
|
+
memoryMdPath: string,
|
|
60
|
+
content: string,
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
const trimmed = content.trim();
|
|
63
|
+
if (!trimmed) {
|
|
64
|
+
throw new Error("content must not be empty");
|
|
65
|
+
}
|
|
66
|
+
await fs.mkdir(path.dirname(memoryMdPath), { recursive: true, mode: 0o700 });
|
|
67
|
+
const lockPath = `${memoryMdPath}.append.lock`;
|
|
68
|
+
const block = content.endsWith("\n") ? content : `${content}\n`;
|
|
69
|
+
await withAppendLock(lockPath, async () => {
|
|
70
|
+
await fs.appendFile(memoryMdPath, block, { encoding: "utf8", mode: 0o600 });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export class MemoryAppendTool {
|
|
75
|
+
constructor(private readonly memoryMdPath: string) {}
|
|
76
|
+
|
|
77
|
+
info() {
|
|
78
|
+
return {
|
|
79
|
+
name: MEMORY_APPEND_NAME,
|
|
80
|
+
description: MEMORY_APPEND_DESCRIPTION,
|
|
81
|
+
parameters: MEMORY_APPEND_PARAMETERS,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async run(argsJson: string): Promise<ToolResult> {
|
|
86
|
+
let raw: Record<string, unknown>;
|
|
87
|
+
try {
|
|
88
|
+
raw = JSON.parse(argsJson) as Record<string, unknown>;
|
|
89
|
+
} catch (e) {
|
|
90
|
+
return {
|
|
91
|
+
content: `invalid arguments: ${e instanceof Error ? e.message : e}`,
|
|
92
|
+
isError: true,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const content = typeof raw.content === "string" ? raw.content : "";
|
|
96
|
+
if (!content.trim()) {
|
|
97
|
+
return { content: "content must not be empty", isError: true };
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
await appendToMemoryMd(this.memoryMdPath, content);
|
|
101
|
+
return { content: `appended to ${this.memoryMdPath}` };
|
|
102
|
+
} catch (e) {
|
|
103
|
+
return {
|
|
104
|
+
content: `error appending: ${e instanceof Error ? e.message : e}`,
|
|
105
|
+
isError: true,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function createMemoryAppendTool(memoryMdPath: string): MemoryAppendTool {
|
|
112
|
+
return new MemoryAppendTool(memoryMdPath);
|
|
113
|
+
}
|