@remnic/plugin-pi 1.0.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/LICENSE +21 -0
- package/README.md +168 -0
- package/dist/chunk-VGPUF4JQ.js +26 -0
- package/dist/chunk-VGPUF4JQ.js.map +1 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +849 -0
- package/dist/index.js.map +1 -0
- package/dist/publisher.d.ts +13 -0
- package/dist/publisher.js +235 -0
- package/dist/publisher.js.map +1 -0
- package/package.json +67 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,849 @@
|
|
|
1
|
+
// openclaw-engram: Local-first memory plugin
|
|
2
|
+
import {
|
|
3
|
+
REMNIC_PI_EXTENSION_DIR_NAME,
|
|
4
|
+
resolvePiAgentHome
|
|
5
|
+
} from "./chunk-VGPUF4JQ.js";
|
|
6
|
+
|
|
7
|
+
// src/index.ts
|
|
8
|
+
import { Type } from "@sinclair/typebox";
|
|
9
|
+
|
|
10
|
+
// src/config.ts
|
|
11
|
+
import { existsSync, readFileSync } from "fs";
|
|
12
|
+
import path from "path";
|
|
13
|
+
import { expandTildePath } from "@remnic/core";
|
|
14
|
+
var DEFAULT_CONFIG = {
|
|
15
|
+
remnicDaemonUrl: "http://127.0.0.1:4318",
|
|
16
|
+
recallMode: "auto",
|
|
17
|
+
recallTopK: 8,
|
|
18
|
+
recallBudgetChars: 12e3,
|
|
19
|
+
recallEnabled: true,
|
|
20
|
+
observeEnabled: true,
|
|
21
|
+
observeSkipExtraction: false,
|
|
22
|
+
compactionEnabled: true,
|
|
23
|
+
mcpToolsEnabled: true,
|
|
24
|
+
statusEnabled: true,
|
|
25
|
+
requestTimeoutMs: 5e3
|
|
26
|
+
};
|
|
27
|
+
function defaultConfigPath(env) {
|
|
28
|
+
return path.join(resolvePiAgentHome(env), "extensions", REMNIC_PI_EXTENSION_DIR_NAME, "remnic.config.json");
|
|
29
|
+
}
|
|
30
|
+
function coerceBoolean(value, fallback, fieldName) {
|
|
31
|
+
if (value === void 0 || value === null) return fallback;
|
|
32
|
+
if (typeof value === "boolean") return value;
|
|
33
|
+
if (typeof value === "string") {
|
|
34
|
+
const normalized = value.trim().toLowerCase();
|
|
35
|
+
if (["true", "1", "yes", "on"].includes(normalized)) return true;
|
|
36
|
+
if (["false", "0", "no", "off"].includes(normalized)) return false;
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Invalid boolean value for Remnic Pi config field ${fieldName}`);
|
|
39
|
+
}
|
|
40
|
+
function coercePositiveInt(value, fallback, max, fieldName) {
|
|
41
|
+
if (value === void 0 || value === null || value === "") return fallback;
|
|
42
|
+
let parsed;
|
|
43
|
+
if (typeof value === "number") {
|
|
44
|
+
parsed = value;
|
|
45
|
+
} else if (typeof value === "string") {
|
|
46
|
+
const trimmed = value.trim();
|
|
47
|
+
if (trimmed.length === 0) return fallback;
|
|
48
|
+
if (!/^[+-]?\d+$/.test(trimmed)) {
|
|
49
|
+
throw new Error(`Invalid numeric value for Remnic Pi config field ${fieldName}: expected an integer from 1 to ${max}`);
|
|
50
|
+
}
|
|
51
|
+
parsed = Number(trimmed);
|
|
52
|
+
} else {
|
|
53
|
+
throw new Error(`Invalid numeric value for Remnic Pi config field ${fieldName}: expected an integer from 1 to ${max}`);
|
|
54
|
+
}
|
|
55
|
+
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > max) {
|
|
56
|
+
throw new Error(`Invalid numeric value for Remnic Pi config field ${fieldName}: expected an integer from 1 to ${max}`);
|
|
57
|
+
}
|
|
58
|
+
return parsed;
|
|
59
|
+
}
|
|
60
|
+
function coerceOptionalNonEmptyString(value, fieldName) {
|
|
61
|
+
if (value === void 0 || value === null) return void 0;
|
|
62
|
+
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
|
63
|
+
throw new Error(`Invalid string value for Remnic Pi config field ${fieldName}`);
|
|
64
|
+
}
|
|
65
|
+
function coerceOptionalString(value, fieldName) {
|
|
66
|
+
if (value === void 0 || value === null) return void 0;
|
|
67
|
+
if (typeof value === "string") {
|
|
68
|
+
const trimmed = value.trim();
|
|
69
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
70
|
+
}
|
|
71
|
+
throw new Error(`Invalid string value for Remnic Pi config field ${fieldName}`);
|
|
72
|
+
}
|
|
73
|
+
function coerceOptionalHttpUrl(value, fieldName) {
|
|
74
|
+
if (value === void 0 || value === null) return void 0;
|
|
75
|
+
if (typeof value !== "string") {
|
|
76
|
+
throw new Error(`Invalid URL value for Remnic Pi config field ${fieldName}: expected an http or https URL`);
|
|
77
|
+
}
|
|
78
|
+
const trimmed = value.trim();
|
|
79
|
+
if (trimmed.length === 0) return void 0;
|
|
80
|
+
try {
|
|
81
|
+
const parsed = new URL(trimmed);
|
|
82
|
+
if (parsed.protocol === "http:" || parsed.protocol === "https:") return trimTrailingSlashes(trimmed);
|
|
83
|
+
} catch {
|
|
84
|
+
}
|
|
85
|
+
throw new Error(`Invalid URL value for Remnic Pi config field ${fieldName}: expected an http or https URL`);
|
|
86
|
+
}
|
|
87
|
+
function coerceRecallMode(value) {
|
|
88
|
+
if (value === void 0 || value === null || value === "") return DEFAULT_CONFIG.recallMode;
|
|
89
|
+
if (value === "minimal" || value === "full" || value === "graph_mode" || value === "no_recall" || value === "auto") {
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`Invalid recallMode value for Remnic Pi config: ${JSON.stringify(value)}`);
|
|
93
|
+
}
|
|
94
|
+
function readConfigFile(configPath) {
|
|
95
|
+
if (!existsSync(configPath)) return {};
|
|
96
|
+
try {
|
|
97
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
98
|
+
const parsed = JSON.parse(raw);
|
|
99
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
100
|
+
return parsed;
|
|
101
|
+
}
|
|
102
|
+
throw new Error("expected a JSON object");
|
|
103
|
+
} catch (err) {
|
|
104
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
105
|
+
throw new Error(`Failed to load Remnic Pi config at ${configPath}: ${reason}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function trimTrailingSlashes(value) {
|
|
109
|
+
let end = value.length;
|
|
110
|
+
while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1;
|
|
111
|
+
return value.slice(0, end);
|
|
112
|
+
}
|
|
113
|
+
function resolveConfigPath(options = {}) {
|
|
114
|
+
const env = options.env ?? process.env;
|
|
115
|
+
return expandTildePath(options.configPath || env.REMNIC_PI_CONFIG || defaultConfigPath(env));
|
|
116
|
+
}
|
|
117
|
+
function loadConfig(options = {}) {
|
|
118
|
+
const env = options.env ?? process.env;
|
|
119
|
+
const fileConfig = readConfigFile(resolveConfigPath(options));
|
|
120
|
+
const daemonUrl = coerceOptionalHttpUrl(fileConfig.remnicDaemonUrl, "remnicDaemonUrl") ?? coerceOptionalHttpUrl(env.REMNIC_DAEMON_URL, "REMNIC_DAEMON_URL") ?? DEFAULT_CONFIG.remnicDaemonUrl;
|
|
121
|
+
const authToken = coerceOptionalString(fileConfig.authToken, "authToken") ?? coerceOptionalString(env.REMNIC_PI_AUTH_TOKEN, "REMNIC_PI_AUTH_TOKEN");
|
|
122
|
+
const namespace = coerceOptionalNonEmptyString(fileConfig.namespace, "namespace");
|
|
123
|
+
return {
|
|
124
|
+
remnicDaemonUrl: daemonUrl,
|
|
125
|
+
authToken,
|
|
126
|
+
namespace,
|
|
127
|
+
recallMode: coerceRecallMode(fileConfig.recallMode),
|
|
128
|
+
recallTopK: coercePositiveInt(fileConfig.recallTopK, DEFAULT_CONFIG.recallTopK, 50, "recallTopK"),
|
|
129
|
+
recallBudgetChars: coercePositiveInt(fileConfig.recallBudgetChars, DEFAULT_CONFIG.recallBudgetChars, 64e3, "recallBudgetChars"),
|
|
130
|
+
recallEnabled: coerceBoolean(fileConfig.recallEnabled, DEFAULT_CONFIG.recallEnabled, "recallEnabled"),
|
|
131
|
+
observeEnabled: coerceBoolean(fileConfig.observeEnabled, DEFAULT_CONFIG.observeEnabled, "observeEnabled"),
|
|
132
|
+
observeSkipExtraction: coerceBoolean(fileConfig.observeSkipExtraction, DEFAULT_CONFIG.observeSkipExtraction, "observeSkipExtraction"),
|
|
133
|
+
compactionEnabled: coerceBoolean(fileConfig.compactionEnabled, DEFAULT_CONFIG.compactionEnabled, "compactionEnabled"),
|
|
134
|
+
mcpToolsEnabled: coerceBoolean(fileConfig.mcpToolsEnabled, DEFAULT_CONFIG.mcpToolsEnabled, "mcpToolsEnabled"),
|
|
135
|
+
statusEnabled: coerceBoolean(fileConfig.statusEnabled, DEFAULT_CONFIG.statusEnabled, "statusEnabled"),
|
|
136
|
+
requestTimeoutMs: coercePositiveInt(fileConfig.requestTimeoutMs, DEFAULT_CONFIG.requestTimeoutMs, 6e4, "requestTimeoutMs")
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/client.ts
|
|
141
|
+
var RemnicHttpError = class extends Error {
|
|
142
|
+
constructor(status, message) {
|
|
143
|
+
super(message);
|
|
144
|
+
this.status = status;
|
|
145
|
+
}
|
|
146
|
+
status;
|
|
147
|
+
};
|
|
148
|
+
var RemnicClient = class {
|
|
149
|
+
constructor(config) {
|
|
150
|
+
this.config = config;
|
|
151
|
+
}
|
|
152
|
+
config;
|
|
153
|
+
requestId = 0;
|
|
154
|
+
async health() {
|
|
155
|
+
return this.request("GET", "/engram/v1/health");
|
|
156
|
+
}
|
|
157
|
+
async recall(query, sessionKey, cwd) {
|
|
158
|
+
return this.request("POST", "/engram/v1/recall", {
|
|
159
|
+
query,
|
|
160
|
+
sessionKey,
|
|
161
|
+
cwd,
|
|
162
|
+
namespace: this.config.namespace,
|
|
163
|
+
topK: this.config.recallTopK,
|
|
164
|
+
mode: this.config.recallMode
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
async recallExplain(sessionKey) {
|
|
168
|
+
return this.request("POST", "/engram/v1/recall/explain", {
|
|
169
|
+
sessionKey,
|
|
170
|
+
namespace: this.config.namespace
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
async observe(sessionKey, cwd, messages) {
|
|
174
|
+
return this.request("POST", "/engram/v1/observe", {
|
|
175
|
+
sessionKey,
|
|
176
|
+
cwd,
|
|
177
|
+
namespace: this.config.namespace,
|
|
178
|
+
skipExtraction: this.config.observeSkipExtraction,
|
|
179
|
+
messages
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
async storeMemory(content, sessionKey) {
|
|
183
|
+
return this.request("POST", "/engram/v1/memories", {
|
|
184
|
+
content,
|
|
185
|
+
category: "fact",
|
|
186
|
+
sourceReason: "Captured from Pi via Remnic extension",
|
|
187
|
+
sessionKey,
|
|
188
|
+
namespace: this.config.namespace
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
async lcmSearch(query, sessionKey, limit = 10) {
|
|
192
|
+
return this.request("POST", "/engram/v1/lcm/search", {
|
|
193
|
+
query,
|
|
194
|
+
sessionKey,
|
|
195
|
+
namespace: this.config.namespace,
|
|
196
|
+
limit
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
async lcmCompactionFlush(sessionKey) {
|
|
200
|
+
return this.request("POST", "/engram/v1/lcm/compaction/flush", {
|
|
201
|
+
sessionKey,
|
|
202
|
+
namespace: this.config.namespace
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async lcmCompactionRecord(sessionKey, tokensBefore, tokensAfter) {
|
|
206
|
+
return this.request("POST", "/engram/v1/lcm/compaction/record", {
|
|
207
|
+
sessionKey,
|
|
208
|
+
namespace: this.config.namespace,
|
|
209
|
+
tokensBefore,
|
|
210
|
+
tokensAfter
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
async contextCheckpoint(sessionKey, context) {
|
|
214
|
+
return this.mcpTool("remnic.context_checkpoint", {
|
|
215
|
+
sessionKey,
|
|
216
|
+
context,
|
|
217
|
+
namespace: this.config.namespace
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
async mcpListTools() {
|
|
221
|
+
const result = await this.mcpRequest("tools/list", {});
|
|
222
|
+
const tools = result.tools;
|
|
223
|
+
return Array.isArray(tools) ? tools.filter(isMcpTool) : [];
|
|
224
|
+
}
|
|
225
|
+
async mcpTool(name, args) {
|
|
226
|
+
return this.mcpRequest("tools/call", {
|
|
227
|
+
name,
|
|
228
|
+
arguments: args
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
async request(method, pathname, body) {
|
|
232
|
+
const controller = new AbortController();
|
|
233
|
+
const timeout = setTimeout(() => controller.abort(), this.config.requestTimeoutMs);
|
|
234
|
+
try {
|
|
235
|
+
const response = await fetch(`${this.config.remnicDaemonUrl}${pathname}`, {
|
|
236
|
+
method,
|
|
237
|
+
headers: {
|
|
238
|
+
...body === void 0 ? {} : { "Content-Type": "application/json" },
|
|
239
|
+
...this.config.authToken ? { Authorization: `Bearer ${this.config.authToken}` } : {},
|
|
240
|
+
"X-Engram-Client-Id": "pi"
|
|
241
|
+
},
|
|
242
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
243
|
+
signal: controller.signal
|
|
244
|
+
});
|
|
245
|
+
const text = await response.text();
|
|
246
|
+
const payload = text ? JSON.parse(text) : {};
|
|
247
|
+
if (!response.ok) {
|
|
248
|
+
throw new RemnicHttpError(response.status, typeof payload?.error === "string" ? payload.error : response.statusText);
|
|
249
|
+
}
|
|
250
|
+
return payload;
|
|
251
|
+
} finally {
|
|
252
|
+
clearTimeout(timeout);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async mcpRequest(method, params) {
|
|
256
|
+
this.requestId += 1;
|
|
257
|
+
const payload = await this.request("POST", "/mcp", {
|
|
258
|
+
jsonrpc: "2.0",
|
|
259
|
+
id: this.requestId,
|
|
260
|
+
method,
|
|
261
|
+
params
|
|
262
|
+
});
|
|
263
|
+
if (payload.error) {
|
|
264
|
+
throw new Error(JSON.stringify(payload.error));
|
|
265
|
+
}
|
|
266
|
+
return payload.result && typeof payload.result === "object" ? payload.result : payload;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
function isMcpTool(value) {
|
|
270
|
+
return !!value && typeof value === "object" && typeof value.name === "string";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/messages.ts
|
|
274
|
+
import { createHash } from "crypto";
|
|
275
|
+
import { parsePiMessageParts } from "@remnic/core";
|
|
276
|
+
function sessionKeyFromContext(ctx) {
|
|
277
|
+
const id = ctx.sessionManager?.getSessionId?.();
|
|
278
|
+
return id && id.trim().length > 0 ? `pi:${id}` : "pi:default";
|
|
279
|
+
}
|
|
280
|
+
function textFromMessage(message) {
|
|
281
|
+
if (!message || typeof message !== "object") return "";
|
|
282
|
+
const obj = message;
|
|
283
|
+
const role = typeof obj.role === "string" ? obj.role : "message";
|
|
284
|
+
if (role === "bashExecution") {
|
|
285
|
+
const command = typeof obj.command === "string" ? obj.command : "";
|
|
286
|
+
const output = typeof obj.output === "string" ? obj.output : "";
|
|
287
|
+
return [`Ran ${command}`, output].filter(Boolean).join("\n");
|
|
288
|
+
}
|
|
289
|
+
return textFromContent(obj.content).trim();
|
|
290
|
+
}
|
|
291
|
+
function latestUserQuery(messages) {
|
|
292
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
293
|
+
const message = messages[index];
|
|
294
|
+
if (isExcludedFromContext(message)) continue;
|
|
295
|
+
if (message?.role === "user") {
|
|
296
|
+
const text = textFromMessage(message);
|
|
297
|
+
if (text.length > 0) return text;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return "";
|
|
301
|
+
}
|
|
302
|
+
function toObserveMessage(message) {
|
|
303
|
+
if (!message || typeof message !== "object") return null;
|
|
304
|
+
const obj = message;
|
|
305
|
+
if (isExcludedFromContext(obj)) return null;
|
|
306
|
+
const role = obj.role === "user" || obj.role === "bashExecution" ? "user" : "assistant";
|
|
307
|
+
const content = textFromMessage(obj);
|
|
308
|
+
if (content.length === 0) return null;
|
|
309
|
+
return {
|
|
310
|
+
role,
|
|
311
|
+
content,
|
|
312
|
+
sourceFormat: "pi",
|
|
313
|
+
rawContent: obj,
|
|
314
|
+
parts: partsFromMessage(obj, content)
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function hashObservedMessage(message, sessionKey = "", identity = "content") {
|
|
318
|
+
return createHash("sha256").update(sessionKey).update("\0").update(message.role).update("\0").update(identity).update("\0").update(message.content).digest("hex");
|
|
319
|
+
}
|
|
320
|
+
function observedMessageDedupeKey(message, sessionKey = "") {
|
|
321
|
+
const identity = stableObservedMessageIdentity(message.rawContent);
|
|
322
|
+
return identity ? hashObservedMessage(message, sessionKey, identity) : null;
|
|
323
|
+
}
|
|
324
|
+
function summarizeMessages(messages, maxChars) {
|
|
325
|
+
const chunks = [];
|
|
326
|
+
let used = 0;
|
|
327
|
+
for (const message of messages) {
|
|
328
|
+
if (isExcludedFromContext(message)) continue;
|
|
329
|
+
const text = textFromMessage(message);
|
|
330
|
+
if (!text) continue;
|
|
331
|
+
const role = typeof message?.role === "string" ? message.role : "message";
|
|
332
|
+
const line = `[${role}] ${text}`;
|
|
333
|
+
const separatorLength = chunks.length > 0 ? 2 : 0;
|
|
334
|
+
const remaining = maxChars - used - separatorLength;
|
|
335
|
+
if (remaining <= 0) break;
|
|
336
|
+
const clipped = line.length > remaining ? line.slice(0, remaining) : line;
|
|
337
|
+
if (clipped.length > 0) chunks.push(clipped);
|
|
338
|
+
used += separatorLength + clipped.length;
|
|
339
|
+
if (used >= maxChars) break;
|
|
340
|
+
}
|
|
341
|
+
return chunks.join("\n\n");
|
|
342
|
+
}
|
|
343
|
+
function isExcludedFromContext(message) {
|
|
344
|
+
return !!message && typeof message === "object" && message.excludeFromContext === true;
|
|
345
|
+
}
|
|
346
|
+
function textFromContent(content) {
|
|
347
|
+
if (typeof content === "string") return content;
|
|
348
|
+
if (!Array.isArray(content)) return "";
|
|
349
|
+
const chunks = [];
|
|
350
|
+
for (const block of content) {
|
|
351
|
+
if (!block || typeof block !== "object") continue;
|
|
352
|
+
const obj = block;
|
|
353
|
+
if (obj.type === "text" && typeof obj.text === "string") chunks.push(obj.text);
|
|
354
|
+
if (obj.type === "toolCall" && typeof obj.name === "string") {
|
|
355
|
+
chunks.push(`Tool ${obj.name} called with ${JSON.stringify(obj.arguments ?? {})}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
return chunks.join("\n");
|
|
359
|
+
}
|
|
360
|
+
function partsFromMessage(message, renderedContent) {
|
|
361
|
+
return parsePiMessageParts(message, {
|
|
362
|
+
renderedContent,
|
|
363
|
+
allowRenderedFallback: true
|
|
364
|
+
}).map(toObserveMessagePart);
|
|
365
|
+
}
|
|
366
|
+
function toObserveMessagePart(part) {
|
|
367
|
+
return {
|
|
368
|
+
ordinal: part.ordinal ?? void 0,
|
|
369
|
+
kind: part.kind,
|
|
370
|
+
payload: part.payload,
|
|
371
|
+
toolName: part.toolName ?? part.tool_name ?? void 0,
|
|
372
|
+
filePath: part.filePath ?? part.file_path ?? void 0,
|
|
373
|
+
createdAt: part.createdAt ?? part.created_at ?? void 0
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function stableObservedMessageIdentity(rawContent) {
|
|
377
|
+
if (rawContent && typeof rawContent === "object") {
|
|
378
|
+
const obj = rawContent;
|
|
379
|
+
const fields = [
|
|
380
|
+
"id",
|
|
381
|
+
"entryId",
|
|
382
|
+
"entry_id",
|
|
383
|
+
"messageId",
|
|
384
|
+
"message_id",
|
|
385
|
+
"turnId",
|
|
386
|
+
"turn_id",
|
|
387
|
+
"timestamp",
|
|
388
|
+
"createdAt",
|
|
389
|
+
"created_at"
|
|
390
|
+
];
|
|
391
|
+
for (const field of fields) {
|
|
392
|
+
const value = obj[field];
|
|
393
|
+
if (typeof value === "string" && value.length > 0) return `${field}:${value}`;
|
|
394
|
+
if (typeof value === "number" && Number.isFinite(value)) return `${field}:${value}`;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// src/index.ts
|
|
401
|
+
var STATE_CUSTOM_TYPE = "remnic_state";
|
|
402
|
+
var MAX_OBSERVED_HASHES = 2e3;
|
|
403
|
+
var MAX_SESSION_STATES = 50;
|
|
404
|
+
var MAX_CONTEXT_CHARS = 12e3;
|
|
405
|
+
var TRUNCATION_NOTICE = "\n\n[Remnic context truncated]";
|
|
406
|
+
function createRemnicPiExtension(options = {}) {
|
|
407
|
+
const config = options.config ?? loadConfig(options);
|
|
408
|
+
const client = new RemnicClient(config);
|
|
409
|
+
const sessionStates = /* @__PURE__ */ new Map();
|
|
410
|
+
return async function remnicPiExtension2(pi) {
|
|
411
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
412
|
+
const { state } = getSessionState(ctx, sessionStates);
|
|
413
|
+
restoreObservedState(ctx, state.observedHashes);
|
|
414
|
+
if (!config.statusEnabled) return;
|
|
415
|
+
await setStatus(ctx, client, config);
|
|
416
|
+
});
|
|
417
|
+
pi.on("context", async (event, ctx) => {
|
|
418
|
+
if (!config.recallEnabled || !config.authToken) return;
|
|
419
|
+
const query = latestUserQuery(Array.isArray(event.messages) ? event.messages : []);
|
|
420
|
+
if (!query) return;
|
|
421
|
+
const sessionKey = sessionKeyFromContext(ctx);
|
|
422
|
+
const { state } = getSessionState(ctx, sessionStates);
|
|
423
|
+
if (query === state.lastInjectedQuery) return;
|
|
424
|
+
try {
|
|
425
|
+
const recalled = await client.recall(query, sessionKey, ctx.cwd);
|
|
426
|
+
const context = trimContext(recalled.context ?? "", config.recallBudgetChars);
|
|
427
|
+
if (!context) return;
|
|
428
|
+
state.lastInjectedQuery = query;
|
|
429
|
+
return {
|
|
430
|
+
messages: [
|
|
431
|
+
{
|
|
432
|
+
role: "user",
|
|
433
|
+
content: [{ type: "text", text: `Remnic recalled context for this turn:
|
|
434
|
+
|
|
435
|
+
${context}` }],
|
|
436
|
+
timestamp: Date.now()
|
|
437
|
+
},
|
|
438
|
+
...event.messages
|
|
439
|
+
]
|
|
440
|
+
};
|
|
441
|
+
} catch (err) {
|
|
442
|
+
notify(ctx, `Remnic recall unavailable: ${errorMessage(err)}`, "warning");
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
pi.on("message_end", async (event, ctx) => {
|
|
446
|
+
if (!config.observeEnabled || !isUserMessage(event.message)) return;
|
|
447
|
+
const { state } = getSessionState(ctx, sessionStates);
|
|
448
|
+
await observeMessages(ctx, client, [event.message], state.observedHashes, state.liveObservedReplayKeys);
|
|
449
|
+
});
|
|
450
|
+
pi.on("turn_end", async (event, ctx) => {
|
|
451
|
+
if (!config.observeEnabled) return;
|
|
452
|
+
const messages = [event.message, ...Array.isArray(event.toolResults) ? event.toolResults : []];
|
|
453
|
+
const { state } = getSessionState(ctx, sessionStates);
|
|
454
|
+
await observeMessages(ctx, client, messages, state.observedHashes, state.liveObservedReplayKeys);
|
|
455
|
+
});
|
|
456
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
457
|
+
const { sessionKey, state } = getSessionState(ctx, sessionStates);
|
|
458
|
+
if (config.observeEnabled) {
|
|
459
|
+
const branch = safeBranch(ctx);
|
|
460
|
+
const branchMessages = branchMessagesWithEntryIdentity(branch);
|
|
461
|
+
const unobservedBranchMessages = skipLiveObservedReplayMessages(ctx, branchMessages, state.liveObservedReplayKeys);
|
|
462
|
+
if (unobservedBranchMessages.length > 0) await observeMessages(ctx, client, unobservedBranchMessages, state.observedHashes);
|
|
463
|
+
}
|
|
464
|
+
persistObservedState(pi, state.observedHashes);
|
|
465
|
+
sessionStates.delete(sessionKey);
|
|
466
|
+
});
|
|
467
|
+
pi.on("session_before_compact", async (event, ctx) => {
|
|
468
|
+
if (!config.compactionEnabled || !config.authToken) return;
|
|
469
|
+
const sessionKey = sessionKeyFromContext(ctx);
|
|
470
|
+
const preparation = event.preparation ?? {};
|
|
471
|
+
try {
|
|
472
|
+
await client.lcmCompactionFlush(sessionKey);
|
|
473
|
+
} catch (err) {
|
|
474
|
+
notify(ctx, `Remnic LCM flush failed: ${errorMessage(err)}`, "warning");
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
const summary = buildCompactionSummary(preparation);
|
|
478
|
+
if (!summary.trim()) return;
|
|
479
|
+
void client.contextCheckpoint(sessionKey, summary).catch(() => void 0);
|
|
480
|
+
const tokensBefore = finiteTokenCount(preparation.tokensBefore);
|
|
481
|
+
const tokensAfter = finiteTokenCount(preparation.tokensAfter);
|
|
482
|
+
if (tokensBefore !== null && tokensAfter !== null) {
|
|
483
|
+
void client.lcmCompactionRecord(sessionKey, tokensBefore, tokensAfter).catch(() => void 0);
|
|
484
|
+
}
|
|
485
|
+
const details = fileDetailsFromPreparation(preparation);
|
|
486
|
+
return {
|
|
487
|
+
compaction: {
|
|
488
|
+
summary,
|
|
489
|
+
firstKeptEntryId: preparation.firstKeptEntryId,
|
|
490
|
+
tokensBefore: preparation.tokensBefore,
|
|
491
|
+
details: {
|
|
492
|
+
...details,
|
|
493
|
+
remnic: { version: 1, source: "pi" }
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
});
|
|
498
|
+
registerCommands(pi, client, config);
|
|
499
|
+
if (config.mcpToolsEnabled && config.authToken) {
|
|
500
|
+
await registerMcpTools(pi, client, config);
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
async function remnicPiExtension(pi) {
|
|
505
|
+
await createRemnicPiExtension()(pi);
|
|
506
|
+
}
|
|
507
|
+
function registerCommands(pi, client, config) {
|
|
508
|
+
pi.registerCommand("remnic-status", {
|
|
509
|
+
description: "Check Remnic daemon status",
|
|
510
|
+
handler: commandHandler(async (_args, ctx) => {
|
|
511
|
+
const health = await client.health();
|
|
512
|
+
notify(ctx, `Remnic ${health.ok ? "healthy" : "unhealthy"} at ${config.remnicDaemonUrl}`, health.ok ? "success" : "warning");
|
|
513
|
+
})
|
|
514
|
+
});
|
|
515
|
+
pi.registerCommand("remnic-recall", {
|
|
516
|
+
description: "Recall Remnic context for a query",
|
|
517
|
+
handler: commandHandler(async (args, ctx) => {
|
|
518
|
+
const query = args.trim();
|
|
519
|
+
if (!query) {
|
|
520
|
+
notify(ctx, "Usage: /remnic-recall <query>", "warning");
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
const result = await client.recall(query, sessionKeyFromContext(ctx), ctx.cwd);
|
|
524
|
+
notify(ctx, trimContext(result.context ?? "(no Remnic context)", MAX_CONTEXT_CHARS), "info");
|
|
525
|
+
})
|
|
526
|
+
});
|
|
527
|
+
pi.registerCommand("remnic-remember", {
|
|
528
|
+
description: "Store a Remnic memory",
|
|
529
|
+
handler: commandHandler(async (args, ctx) => {
|
|
530
|
+
const content = args.trim();
|
|
531
|
+
if (!content) {
|
|
532
|
+
notify(ctx, "Usage: /remnic-remember <memory>", "warning");
|
|
533
|
+
return;
|
|
534
|
+
}
|
|
535
|
+
await client.storeMemory(content, sessionKeyFromContext(ctx));
|
|
536
|
+
notify(ctx, "Stored Remnic memory", "success");
|
|
537
|
+
})
|
|
538
|
+
});
|
|
539
|
+
pi.registerCommand("remnic-lcm-search", {
|
|
540
|
+
description: "Search Remnic LCM archived Pi context",
|
|
541
|
+
handler: commandHandler(async (args, ctx) => {
|
|
542
|
+
const query = args.trim();
|
|
543
|
+
if (!query) {
|
|
544
|
+
notify(ctx, "Usage: /remnic-lcm-search <query>", "warning");
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
const result = await client.lcmSearch(query, sessionKeyFromContext(ctx));
|
|
548
|
+
notify(ctx, JSON.stringify(result, null, 2), "info");
|
|
549
|
+
})
|
|
550
|
+
});
|
|
551
|
+
pi.registerCommand("remnic-why", {
|
|
552
|
+
description: "Explain the last Remnic recall",
|
|
553
|
+
handler: commandHandler(async (_args, ctx) => {
|
|
554
|
+
const result = await client.recallExplain(sessionKeyFromContext(ctx));
|
|
555
|
+
notify(ctx, JSON.stringify(result, null, 2), "info");
|
|
556
|
+
})
|
|
557
|
+
});
|
|
558
|
+
pi.registerCommand("remnic-compact", {
|
|
559
|
+
description: "Trigger Pi compaction with Remnic LCM coordination",
|
|
560
|
+
handler: commandHandler(async (_args, ctx) => {
|
|
561
|
+
ctx.compact?.();
|
|
562
|
+
notify(ctx, "Compaction requested", "info");
|
|
563
|
+
})
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
function commandHandler(handler) {
|
|
567
|
+
return async (args, ctx) => {
|
|
568
|
+
try {
|
|
569
|
+
await handler(args, ctx);
|
|
570
|
+
} catch (err) {
|
|
571
|
+
notify(ctx, `Remnic command failed: ${errorMessage(err)}`, "warning");
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
async function registerMcpTools(pi, client, config) {
|
|
576
|
+
let tools = [];
|
|
577
|
+
try {
|
|
578
|
+
tools = await client.mcpListTools();
|
|
579
|
+
} catch {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
for (const tool of tools) {
|
|
583
|
+
if (!tool.name.startsWith("remnic.")) continue;
|
|
584
|
+
const piToolName = tool.name.replace(/^remnic\./, "remnic_").replace(/[^a-zA-Z0-9_]/g, "_");
|
|
585
|
+
pi.registerTool({
|
|
586
|
+
name: piToolName,
|
|
587
|
+
label: tool.name,
|
|
588
|
+
description: tool.description ?? `Call ${tool.name}`,
|
|
589
|
+
parameters: toPiToolParametersSchema(tool.inputSchema),
|
|
590
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
591
|
+
const sessionKey = sessionKeyFromContext(ctx);
|
|
592
|
+
const {
|
|
593
|
+
sessionKey: _ignoredSessionKey,
|
|
594
|
+
namespace: _ignoredNamespace,
|
|
595
|
+
cwd: _ignoredCwd,
|
|
596
|
+
...safeParams
|
|
597
|
+
} = params ?? {};
|
|
598
|
+
const result = await client.mcpTool(tool.name, {
|
|
599
|
+
...safeParams,
|
|
600
|
+
sessionKey,
|
|
601
|
+
namespace: config.namespace,
|
|
602
|
+
cwd: ctx.cwd
|
|
603
|
+
});
|
|
604
|
+
return {
|
|
605
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
606
|
+
details: result
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
function toPiToolParametersSchema(inputSchema) {
|
|
613
|
+
return Type.Unsafe(stripSessionOwnedSchemaFields(inputSchema));
|
|
614
|
+
}
|
|
615
|
+
function stripSessionOwnedSchemaFields(inputSchema) {
|
|
616
|
+
if (!isRecord(inputSchema)) {
|
|
617
|
+
return { type: "object", properties: {}, additionalProperties: true };
|
|
618
|
+
}
|
|
619
|
+
const schema = { ...inputSchema };
|
|
620
|
+
const properties = isRecord(inputSchema.properties) ? { ...inputSchema.properties } : {};
|
|
621
|
+
delete properties.sessionKey;
|
|
622
|
+
delete properties.namespace;
|
|
623
|
+
delete properties.cwd;
|
|
624
|
+
schema.properties = properties;
|
|
625
|
+
if (Array.isArray(inputSchema.required)) {
|
|
626
|
+
schema.required = inputSchema.required.filter(
|
|
627
|
+
(field) => field !== "sessionKey" && field !== "namespace" && field !== "cwd"
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
return schema;
|
|
631
|
+
}
|
|
632
|
+
function isRecord(value) {
|
|
633
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
634
|
+
}
|
|
635
|
+
function isUserMessage(message) {
|
|
636
|
+
return isRecord(message) && message.role === "user";
|
|
637
|
+
}
|
|
638
|
+
function getSessionState(ctx, states) {
|
|
639
|
+
const sessionKey = sessionKeyFromContext(ctx);
|
|
640
|
+
let state = states.get(sessionKey);
|
|
641
|
+
if (!state) {
|
|
642
|
+
state = {
|
|
643
|
+
observedHashes: /* @__PURE__ */ new Set(),
|
|
644
|
+
liveObservedReplayKeys: /* @__PURE__ */ new Map(),
|
|
645
|
+
lastInjectedQuery: ""
|
|
646
|
+
};
|
|
647
|
+
states.set(sessionKey, state);
|
|
648
|
+
pruneSessionStates(states);
|
|
649
|
+
}
|
|
650
|
+
return { sessionKey, state };
|
|
651
|
+
}
|
|
652
|
+
function pruneSessionStates(states) {
|
|
653
|
+
while (states.size > MAX_SESSION_STATES) {
|
|
654
|
+
const oldest = states.keys().next().value;
|
|
655
|
+
if (typeof oldest !== "string") return;
|
|
656
|
+
states.delete(oldest);
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
async function observeMessages(ctx, client, rawMessages, observedHashes, liveObservedReplayKeys) {
|
|
660
|
+
const sessionKey = sessionKeyFromContext(ctx);
|
|
661
|
+
const messages = [];
|
|
662
|
+
const pendingHashes = /* @__PURE__ */ new Set();
|
|
663
|
+
for (const raw of rawMessages) {
|
|
664
|
+
const message = toObserveMessage(raw);
|
|
665
|
+
if (!message) continue;
|
|
666
|
+
const hash = observedMessageDedupeKey(message, sessionKey);
|
|
667
|
+
if (hash && (observedHashes.has(hash) || pendingHashes.has(hash))) continue;
|
|
668
|
+
if (hash) pendingHashes.add(hash);
|
|
669
|
+
messages.push(message);
|
|
670
|
+
}
|
|
671
|
+
if (messages.length === 0) return;
|
|
672
|
+
try {
|
|
673
|
+
await client.observe(sessionKey, ctx.cwd, messages);
|
|
674
|
+
for (const hash of pendingHashes) rememberObservedHash(observedHashes, hash);
|
|
675
|
+
if (liveObservedReplayKeys) {
|
|
676
|
+
for (const message of messages) {
|
|
677
|
+
rememberLiveObservedReplayKey(liveObservedReplayKeys, liveReplayKey(message, sessionKey));
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
} catch (err) {
|
|
681
|
+
notify(ctx, `Remnic observe failed: ${errorMessage(err)}`, "warning");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
function buildCompactionSummary(preparation) {
|
|
685
|
+
const previousSummary = typeof preparation.previousSummary === "string" ? preparation.previousSummary.trim() : "";
|
|
686
|
+
const messages = [
|
|
687
|
+
...Array.isArray(preparation.messagesToSummarize) ? preparation.messagesToSummarize : [],
|
|
688
|
+
...Array.isArray(preparation.turnPrefixMessages) ? preparation.turnPrefixMessages : []
|
|
689
|
+
];
|
|
690
|
+
const transcript = summarizeMessages(messages, 24e3);
|
|
691
|
+
const details = fileDetailsFromPreparation(preparation);
|
|
692
|
+
if (!previousSummary && !transcript && details.readFiles.length === 0 && details.modifiedFiles.length === 0) {
|
|
693
|
+
return "";
|
|
694
|
+
}
|
|
695
|
+
const sections = [
|
|
696
|
+
"## Remnic Pi Context Checkpoint",
|
|
697
|
+
"",
|
|
698
|
+
"This checkpoint was created by Remnic during Pi context compaction."
|
|
699
|
+
];
|
|
700
|
+
if (previousSummary) sections.push("", "## Previous Summary", previousSummary);
|
|
701
|
+
if (transcript) sections.push("", "## Conversation Excerpt", transcript);
|
|
702
|
+
if (details.readFiles.length > 0) sections.push("", "<read-files>", ...details.readFiles, "</read-files>");
|
|
703
|
+
if (details.modifiedFiles.length > 0) sections.push("", "<modified-files>", ...details.modifiedFiles, "</modified-files>");
|
|
704
|
+
return sections.join("\n");
|
|
705
|
+
}
|
|
706
|
+
function fileDetailsFromPreparation(preparation) {
|
|
707
|
+
const fileOps = preparation?.fileOps;
|
|
708
|
+
const read = fileOps?.read instanceof Set ? Array.from(fileOps.read).filter(isString) : [];
|
|
709
|
+
const edited = fileOps?.edited instanceof Set ? Array.from(fileOps.edited).filter(isString) : [];
|
|
710
|
+
const written = fileOps?.written instanceof Set ? Array.from(fileOps.written).filter(isString) : [];
|
|
711
|
+
const modified = /* @__PURE__ */ new Set([...edited, ...written]);
|
|
712
|
+
return {
|
|
713
|
+
readFiles: read.filter((file) => !modified.has(file)).sort(),
|
|
714
|
+
modifiedFiles: Array.from(modified).sort()
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function restoreObservedState(ctx, observedHashes) {
|
|
718
|
+
for (const entry of safeEntries(ctx)) {
|
|
719
|
+
if (entry?.type !== "custom" || entry.customType !== STATE_CUSTOM_TYPE) continue;
|
|
720
|
+
const hashes = entry.data?.observedHashes;
|
|
721
|
+
if (Array.isArray(hashes)) {
|
|
722
|
+
for (const hash of hashes) {
|
|
723
|
+
if (typeof hash === "string") rememberObservedHash(observedHashes, hash);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
function rememberObservedHash(observedHashes, hash) {
|
|
729
|
+
if (observedHashes.has(hash)) return;
|
|
730
|
+
while (observedHashes.size >= MAX_OBSERVED_HASHES) {
|
|
731
|
+
const oldest = observedHashes.keys().next().value;
|
|
732
|
+
if (typeof oldest !== "string") break;
|
|
733
|
+
observedHashes.delete(oldest);
|
|
734
|
+
}
|
|
735
|
+
observedHashes.add(hash);
|
|
736
|
+
}
|
|
737
|
+
function rememberLiveObservedReplayKey(liveObservedReplayKeys, key) {
|
|
738
|
+
liveObservedReplayKeys.set(key, (liveObservedReplayKeys.get(key) ?? 0) + 1);
|
|
739
|
+
}
|
|
740
|
+
function consumeLiveObservedReplayKey(liveObservedReplayKeys, key) {
|
|
741
|
+
const count = liveObservedReplayKeys.get(key) ?? 0;
|
|
742
|
+
if (count <= 0) return false;
|
|
743
|
+
if (count === 1) liveObservedReplayKeys.delete(key);
|
|
744
|
+
else liveObservedReplayKeys.set(key, count - 1);
|
|
745
|
+
return true;
|
|
746
|
+
}
|
|
747
|
+
function skipLiveObservedReplayMessages(ctx, rawMessages, liveObservedReplayKeys) {
|
|
748
|
+
if (liveObservedReplayKeys.size === 0) return rawMessages;
|
|
749
|
+
const sessionKey = sessionKeyFromContext(ctx);
|
|
750
|
+
const unobserved = [];
|
|
751
|
+
for (const raw of rawMessages) {
|
|
752
|
+
const message = toObserveMessage(raw);
|
|
753
|
+
if (message && consumeLiveObservedReplayKey(liveObservedReplayKeys, liveReplayKey(message, sessionKey))) {
|
|
754
|
+
continue;
|
|
755
|
+
}
|
|
756
|
+
unobserved.push(raw);
|
|
757
|
+
}
|
|
758
|
+
return unobserved;
|
|
759
|
+
}
|
|
760
|
+
function liveReplayKey(message, sessionKey) {
|
|
761
|
+
return hashObservedMessage(message, sessionKey, "live-replay");
|
|
762
|
+
}
|
|
763
|
+
function persistObservedState(pi, observedHashes) {
|
|
764
|
+
const observed = Array.from(observedHashes).slice(-MAX_OBSERVED_HASHES);
|
|
765
|
+
pi.appendEntry(STATE_CUSTOM_TYPE, {
|
|
766
|
+
observedHashes: observed,
|
|
767
|
+
recordedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
768
|
+
});
|
|
769
|
+
}
|
|
770
|
+
async function setStatus(ctx, client, config) {
|
|
771
|
+
try {
|
|
772
|
+
await client.health();
|
|
773
|
+
ctx.ui?.setStatus?.("remnic", `Remnic ${config.namespace ? `(${config.namespace})` : "ready"}`);
|
|
774
|
+
} catch {
|
|
775
|
+
ctx.ui?.setStatus?.("remnic", "Remnic offline");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function safeEntries(ctx) {
|
|
779
|
+
try {
|
|
780
|
+
const entries = ctx.sessionManager?.getEntries?.();
|
|
781
|
+
return Array.isArray(entries) ? entries : [];
|
|
782
|
+
} catch {
|
|
783
|
+
return [];
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function safeBranch(ctx) {
|
|
787
|
+
try {
|
|
788
|
+
const branch = ctx.sessionManager?.getBranch?.();
|
|
789
|
+
return Array.isArray(branch) ? branch : [];
|
|
790
|
+
} catch {
|
|
791
|
+
return [];
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
function branchMessagesWithEntryIdentity(branch) {
|
|
795
|
+
const messages = [];
|
|
796
|
+
for (const entry of branch) {
|
|
797
|
+
const message = messageWithEntryIdentity(entry);
|
|
798
|
+
if (message) messages.push(message);
|
|
799
|
+
}
|
|
800
|
+
return messages;
|
|
801
|
+
}
|
|
802
|
+
function messageWithEntryIdentity(entry) {
|
|
803
|
+
const message = entry?.message;
|
|
804
|
+
if (!message || typeof message !== "object" || Array.isArray(message)) return message ?? null;
|
|
805
|
+
const source = isRecord(entry) ? entry : {};
|
|
806
|
+
const enriched = { ...message };
|
|
807
|
+
assignMissingIdentity(enriched, "entryId", source.id ?? source.entryId ?? source.entry_id);
|
|
808
|
+
assignMissingIdentity(enriched, "timestamp", source.timestamp);
|
|
809
|
+
assignMissingIdentity(enriched, "createdAt", source.createdAt ?? source.created_at);
|
|
810
|
+
return enriched;
|
|
811
|
+
}
|
|
812
|
+
function assignMissingIdentity(target, field, value) {
|
|
813
|
+
if (target[field] !== void 0) return;
|
|
814
|
+
if (typeof value === "string" && value.length > 0) {
|
|
815
|
+
target[field] = value;
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
819
|
+
target[field] = value;
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
function trimContext(value, budget) {
|
|
823
|
+
if (value.length <= budget) return value;
|
|
824
|
+
if (budget <= TRUNCATION_NOTICE.length) return TRUNCATION_NOTICE.slice(0, budget);
|
|
825
|
+
return `${value.slice(0, budget - TRUNCATION_NOTICE.length)}${TRUNCATION_NOTICE}`;
|
|
826
|
+
}
|
|
827
|
+
function notify(ctx, message, level) {
|
|
828
|
+
if (ctx?.hasUI === false) return;
|
|
829
|
+
ctx?.ui?.notify?.(message, level);
|
|
830
|
+
}
|
|
831
|
+
function errorMessage(err) {
|
|
832
|
+
return err instanceof Error ? err.message : String(err);
|
|
833
|
+
}
|
|
834
|
+
function finiteTokenCount(value) {
|
|
835
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : null;
|
|
836
|
+
}
|
|
837
|
+
function isString(value) {
|
|
838
|
+
return typeof value === "string";
|
|
839
|
+
}
|
|
840
|
+
export {
|
|
841
|
+
buildCompactionSummary,
|
|
842
|
+
createRemnicPiExtension,
|
|
843
|
+
remnicPiExtension as default,
|
|
844
|
+
observeMessages,
|
|
845
|
+
stripSessionOwnedSchemaFields,
|
|
846
|
+
textFromMessage,
|
|
847
|
+
toPiToolParametersSchema
|
|
848
|
+
};
|
|
849
|
+
//# sourceMappingURL=index.js.map
|