@memtensor/memos-local-openclaw-plugin 1.0.9-beta.1 → 1.0.9
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 +6 -7
- package/index.ts +64 -13
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/src/ingest/providers/anthropic.ts +14 -29
- package/src/ingest/providers/bedrock.ts +14 -29
- package/src/ingest/providers/gemini.ts +14 -29
- package/src/ingest/providers/index.ts +7 -2
- package/src/ingest/providers/openai.ts +71 -14
- package/src/skill/ALGORITHMS.md +141 -0
- package/src/skill/CHANGELOG-DESIGN.md +24 -0
- package/src/skill/DESIGN.md +72 -0
- package/src/skill/experience-extractor.ts +191 -0
- package/src/skill/feedback-signals.ts +181 -0
- package/src/viewer/html-v2.ts +1631 -0
- package/src/viewer/html.ts +75 -8
- package/src/viewer/server.ts +21 -7
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/darwin-x64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/telemetry.credentials.json +0 -5
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@memtensor/memos-local-openclaw-plugin)
|
|
4
4
|
[](https://github.com/MemTensor/MemOS/blob/main/LICENSE)
|
|
5
|
-
[](https://nodejs.org/)
|
|
6
6
|
[](https://github.com/MemTensor/MemOS/tree/main/apps/memos-local-openclaw)
|
|
7
7
|
|
|
8
8
|
Persistent local conversation memory for [OpenClaw](https://github.com/nicepkg/openclaw) AI Agents. Every conversation is automatically captured, semantically indexed, and instantly recallable — with **task summarization & skill evolution**, **team sharing for memories and skills**, and **multi-agent collaborative memory**.
|
|
@@ -618,7 +618,7 @@ openclaw plugins install @memtensor/memos-local-openclaw-plugin
|
|
|
618
618
|
|
|
619
619
|
## Troubleshooting
|
|
620
620
|
|
|
621
|
-
> 📖 **详细排查指南 / Detailed troubleshooting guide:** [
|
|
621
|
+
> 📖 **详细排查指南 / Detailed troubleshooting guide:** [Troubleshooting](https://memos-claw.openmem.net/docs/troubleshooting.html) — 包含逐步排查流程、日志查看方法、完全重装步骤等。
|
|
622
622
|
>
|
|
623
623
|
> 📦 **better-sqlite3 official troubleshooting:** [better-sqlite3 Troubleshooting](https://github.com/WiseLibs/better-sqlite3/blob/master/docs/troubleshooting.md) — the upstream guide for native module build issues.
|
|
624
624
|
|
|
@@ -640,7 +640,7 @@ openclaw plugins install @memtensor/memos-local-openclaw-plugin
|
|
|
640
640
|
Search for `memos-local`, `failed to load`, `Error`, `Cannot find module`.
|
|
641
641
|
|
|
642
642
|
4. **Check environment**
|
|
643
|
-
- Node version: `node -v` (requires **>=
|
|
643
|
+
- Node version: `node -v` (requires **>= 22**)
|
|
644
644
|
- Plugin directory exists: `ls ~/.openclaw/extensions/memos-local-openclaw-plugin/package.json`
|
|
645
645
|
- Dependencies installed: `ls ~/.openclaw/extensions/memos-local-openclaw-plugin/node_modules/@sinclair/typebox`
|
|
646
646
|
If missing: `cd ~/.openclaw/extensions/memos-local-openclaw-plugin && npm install --omit=dev`
|
|
@@ -693,7 +693,7 @@ This section is for contributors who want to develop, test, or modify the plugin
|
|
|
693
693
|
|
|
694
694
|
### Prerequisites
|
|
695
695
|
|
|
696
|
-
- **Node.js >=
|
|
696
|
+
- **Node.js >= 22** (`node -v`)
|
|
697
697
|
- **npm >= 9** (`npm -v`)
|
|
698
698
|
- **C++ build tools** (for `better-sqlite3` native module):
|
|
699
699
|
- macOS: `xcode-select --install`
|
|
@@ -701,7 +701,7 @@ This section is for contributors who want to develop, test, or modify the plugin
|
|
|
701
701
|
- Windows: usually not needed (prebuilt binaries available for LTS Node.js); if build fails, install [Visual Studio Build Tools](https://visualstudio.microsoft.com/visual-cpp-build-tools/)
|
|
702
702
|
- **OpenClaw CLI** installed and available in PATH (`openclaw --version`)
|
|
703
703
|
|
|
704
|
-
> **`better-sqlite3` build issues?** This is the most common installation problem on macOS and Linux. If `npm install` fails, first install the C++ build tools above, then run `npm rebuild better-sqlite3`. For detailed platform-specific solutions, see the [official better-sqlite3 troubleshooting guide](https://github.com/WiseLibs/better-sqlite3/blob/master/docs/troubleshooting.md) and our [installation troubleshooting page](https://
|
|
704
|
+
> **`better-sqlite3` build issues?** This is the most common installation problem on macOS and Linux. If `npm install` fails, first install the C++ build tools above, then run `npm rebuild better-sqlite3`. For detailed platform-specific solutions, see the [official better-sqlite3 troubleshooting guide](https://github.com/WiseLibs/better-sqlite3/blob/master/docs/troubleshooting.md) and our [installation troubleshooting page](https://memos-claw.openmem.net/docs/troubleshooting.html).
|
|
705
705
|
|
|
706
706
|
### Clone & Setup
|
|
707
707
|
|
|
@@ -761,8 +761,7 @@ apps/memos-local-openclaw/
|
|
|
761
761
|
| `node_modules/` | npm dependencies | `npm install` |
|
|
762
762
|
| `dist/` | Compiled JavaScript output | `npm run build` |
|
|
763
763
|
| `package-lock.json` | Dependency lock file | `npm install` (auto-generated) |
|
|
764
|
-
| `www/` | Memory Viewer static site
|
|
765
|
-
| `docs/` | Documentation HTML pages | Built from source or viewed at the hosted URL |
|
|
764
|
+
| `www/` | Memory Viewer static site & documentation pages | Started automatically by the plugin |
|
|
766
765
|
| `ppt/` | Presentation files (internal use) | Not needed for development |
|
|
767
766
|
| `.env` | Local environment variables | Copy from `.env.example` |
|
|
768
767
|
|
package/index.ts
CHANGED
|
@@ -389,6 +389,8 @@ const memosLocalPlugin = {
|
|
|
389
389
|
let currentAgentId = "main";
|
|
390
390
|
const getCurrentOwner = () => `agent:${currentAgentId}`;
|
|
391
391
|
|
|
392
|
+
const globalRef = globalThis as any;
|
|
393
|
+
|
|
392
394
|
// ─── Check allowPromptInjection policy ───
|
|
393
395
|
// When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
|
|
394
396
|
// will be stripped by the framework. Skip auto-recall to avoid unnecessary LLM/embedding calls.
|
|
@@ -2382,8 +2384,45 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2382
2384
|
|
|
2383
2385
|
let serviceStarted = false;
|
|
2384
2386
|
|
|
2385
|
-
|
|
2387
|
+
function isGatewayStartCommand(): boolean {
|
|
2388
|
+
const args = process.argv.map(a => String(a || "").toLowerCase());
|
|
2389
|
+
const gIdx = args.lastIndexOf("gateway");
|
|
2390
|
+
if (gIdx === -1) return false;
|
|
2391
|
+
const next = args[gIdx + 1];
|
|
2392
|
+
return !next || next.startsWith("-") || next === "start" || next === "restart";
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
const startServiceCore = async (isHostStart = false) => {
|
|
2396
|
+
if (!isGatewayStartCommand()) {
|
|
2397
|
+
api.logger.info("memos-local: not a gateway start command, skipping service startup.");
|
|
2398
|
+
return;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
if (globalRef.__memosLocalPluginStopPromise) {
|
|
2402
|
+
await globalRef.__memosLocalPluginStopPromise;
|
|
2403
|
+
globalRef.__memosLocalPluginStopPromise = undefined;
|
|
2404
|
+
}
|
|
2386
2405
|
if (serviceStarted) return;
|
|
2406
|
+
|
|
2407
|
+
if (!isHostStart) {
|
|
2408
|
+
if (globalRef.__memosLocalPluginActiveService && globalRef.__memosLocalPluginActiveService !== service) {
|
|
2409
|
+
api.logger.info("memos-local: aborting startServiceCore because a newer plugin instance is active.");
|
|
2410
|
+
return;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
|
|
2414
|
+
if (globalRef.__memosLocalPluginActiveService && globalRef.__memosLocalPluginActiveService !== service) {
|
|
2415
|
+
api.logger.info("memos-local: Stopping previous plugin instance due to start of new instance...");
|
|
2416
|
+
const oldService = globalRef.__memosLocalPluginActiveService;
|
|
2417
|
+
globalRef.__memosLocalPluginActiveService = undefined;
|
|
2418
|
+
try {
|
|
2419
|
+
await oldService.stop({ preserveDb: true });
|
|
2420
|
+
} catch (e: any) {
|
|
2421
|
+
api.logger.warn(`memos-local: Error stopping previous instance: ${e}`);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
globalRef.__memosLocalPluginActiveService = service;
|
|
2387
2426
|
serviceStarted = true;
|
|
2388
2427
|
|
|
2389
2428
|
if (hubServer) {
|
|
@@ -2425,33 +2464,45 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2425
2464
|
);
|
|
2426
2465
|
};
|
|
2427
2466
|
|
|
2428
|
-
|
|
2467
|
+
const service = {
|
|
2429
2468
|
id: "memos-local-openclaw-plugin",
|
|
2430
|
-
start: async () => { await startServiceCore(); },
|
|
2431
|
-
stop: async () => {
|
|
2469
|
+
start: async () => { await startServiceCore(true); },
|
|
2470
|
+
stop: async (options?: { preserveDb?: boolean }) => {
|
|
2471
|
+
await viewer.stop();
|
|
2472
|
+
if (globalRef.__memosLocalPluginActiveService === service) {
|
|
2473
|
+
globalRef.__memosLocalPluginActiveService = undefined;
|
|
2474
|
+
}
|
|
2432
2475
|
await worker.flush();
|
|
2433
2476
|
await telemetry.shutdown();
|
|
2434
2477
|
await hubServer?.stop();
|
|
2435
|
-
|
|
2436
|
-
|
|
2478
|
+
|
|
2479
|
+
// If we are hot-reloading, the new instance is already using the SAME
|
|
2480
|
+
// database file on disk. Closing the sqlite store here might kill the
|
|
2481
|
+
// connection for the new instance or cause locking issues depending on
|
|
2482
|
+
// how the native binding manages handles.
|
|
2483
|
+
if (!options?.preserveDb) {
|
|
2484
|
+
store.close();
|
|
2485
|
+
}
|
|
2437
2486
|
api.logger.info("memos-local: stopped");
|
|
2438
2487
|
},
|
|
2439
|
-
}
|
|
2488
|
+
};
|
|
2489
|
+
|
|
2490
|
+
api.registerService(service);
|
|
2440
2491
|
|
|
2441
2492
|
// Fallback: OpenClaw may load this plugin via deferred reload after
|
|
2442
2493
|
// startPluginServices has already run, so service.start() never fires.
|
|
2443
|
-
// Start on
|
|
2444
|
-
//
|
|
2445
|
-
|
|
2446
|
-
const
|
|
2447
|
-
|
|
2448
|
-
if (!serviceStarted) {
|
|
2494
|
+
// Start on a delay instead of next tick so the host has time to call
|
|
2495
|
+
// service.start() during normal startup if this is a fresh launch.
|
|
2496
|
+
const SELF_START_DELAY_MS = 2000;
|
|
2497
|
+
const selfStartTimer = setTimeout(() => {
|
|
2498
|
+
if (!serviceStarted && isGatewayStartCommand()) {
|
|
2449
2499
|
api.logger.info("memos-local: service.start() not called by host, self-starting viewer...");
|
|
2450
2500
|
startServiceCore().catch((err) => {
|
|
2451
2501
|
api.logger.warn(`memos-local: self-start failed: ${err}`);
|
|
2452
2502
|
});
|
|
2453
2503
|
}
|
|
2454
2504
|
}, SELF_START_DELAY_MS);
|
|
2505
|
+
if (selfStartTimer.unref) selfStartTimer.unref();
|
|
2455
2506
|
},
|
|
2456
2507
|
};
|
|
2457
2508
|
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memtensor/memos-local-openclaw-plugin",
|
|
3
|
-
"version": "1.0.9
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
],
|
|
46
46
|
"license": "MIT",
|
|
47
47
|
"engines": {
|
|
48
|
-
"node": ">=
|
|
48
|
+
"node": ">=22.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@huggingface/transformers": "^3.8.0",
|
|
@@ -121,12 +121,12 @@ export async function generateTaskTitleAnthropic(
|
|
|
121
121
|
headers,
|
|
122
122
|
body: JSON.stringify({
|
|
123
123
|
model,
|
|
124
|
-
max_tokens:
|
|
124
|
+
max_tokens: 2000,
|
|
125
125
|
temperature: 0,
|
|
126
126
|
system: TASK_TITLE_PROMPT,
|
|
127
127
|
messages: [{ role: "user", content: text }],
|
|
128
128
|
}),
|
|
129
|
-
signal: AbortSignal.timeout(cfg.timeoutMs ??
|
|
129
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
if (!resp.ok) {
|
|
@@ -189,12 +189,12 @@ export async function judgeNewTopicAnthropic(
|
|
|
189
189
|
headers,
|
|
190
190
|
body: JSON.stringify({
|
|
191
191
|
model,
|
|
192
|
-
max_tokens:
|
|
192
|
+
max_tokens: 2000,
|
|
193
193
|
temperature: 0,
|
|
194
194
|
system: TOPIC_JUDGE_PROMPT,
|
|
195
195
|
messages: [{ role: "user", content: userContent }],
|
|
196
196
|
}),
|
|
197
|
-
signal: AbortSignal.timeout(cfg.timeoutMs ??
|
|
197
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
198
198
|
});
|
|
199
199
|
|
|
200
200
|
if (!resp.ok) {
|
|
@@ -223,10 +223,12 @@ RULES:
|
|
|
223
223
|
|
|
224
224
|
OUTPUT — JSON only:
|
|
225
225
|
{"relevant":[1,3],"sufficient":true}
|
|
226
|
-
- "relevant": candidate numbers whose content helps answer the query. [] if none can help.
|
|
227
|
-
- "sufficient": true only if the selected memories fully answer the query
|
|
226
|
+
- "relevant": candidate numbers whose content helps answer the query. [] if none can help. Duplicates removed — only unique information.
|
|
227
|
+
- "sufficient": true only if the selected memories fully answer the query.
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
IMPORTANT FOR REASONING MODELS: After your analysis, you MUST output a valid JSON object in this exact format. Do not output any text after the JSON object.`;
|
|
230
|
+
|
|
231
|
+
import { parseFilterResult, type FilterResult } from "./openai";
|
|
230
232
|
export type { FilterResult } from "./openai";
|
|
231
233
|
|
|
232
234
|
export async function filterRelevantAnthropic(
|
|
@@ -256,12 +258,12 @@ export async function filterRelevantAnthropic(
|
|
|
256
258
|
headers,
|
|
257
259
|
body: JSON.stringify({
|
|
258
260
|
model,
|
|
259
|
-
max_tokens:
|
|
261
|
+
max_tokens: 2000,
|
|
260
262
|
temperature: 0,
|
|
261
263
|
system: FILTER_RELEVANT_PROMPT,
|
|
262
264
|
messages: [{ role: "user", content: `QUERY: ${query}\n\nCANDIDATES:\n${candidateText}` }],
|
|
263
265
|
}),
|
|
264
|
-
signal: AbortSignal.timeout(cfg.timeoutMs ??
|
|
266
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
265
267
|
});
|
|
266
268
|
|
|
267
269
|
if (!resp.ok) {
|
|
@@ -275,23 +277,6 @@ export async function filterRelevantAnthropic(
|
|
|
275
277
|
return parseFilterResult(raw, log);
|
|
276
278
|
}
|
|
277
279
|
|
|
278
|
-
function parseFilterResult(raw: string, log: Logger): FilterResult {
|
|
279
|
-
try {
|
|
280
|
-
const match = raw.match(/\{[\s\S]*\}/);
|
|
281
|
-
if (match) {
|
|
282
|
-
const obj = JSON.parse(match[0]);
|
|
283
|
-
if (obj && Array.isArray(obj.relevant)) {
|
|
284
|
-
return {
|
|
285
|
-
relevant: obj.relevant.filter((n: any) => typeof n === "number"),
|
|
286
|
-
sufficient: obj.sufficient === true,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
} catch {}
|
|
291
|
-
log.warn(`filterRelevant: failed to parse LLM output: "${raw}", fallback to all+insufficient`);
|
|
292
|
-
return { relevant: [], sufficient: false };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
280
|
export async function summarizeAnthropic(
|
|
296
281
|
text: string,
|
|
297
282
|
cfg: SummarizerConfig,
|
|
@@ -311,7 +296,7 @@ export async function summarizeAnthropic(
|
|
|
311
296
|
headers,
|
|
312
297
|
body: JSON.stringify({
|
|
313
298
|
model,
|
|
314
|
-
max_tokens:
|
|
299
|
+
max_tokens: 2000,
|
|
315
300
|
temperature: cfg.temperature ?? 0,
|
|
316
301
|
system: SYSTEM_PROMPT,
|
|
317
302
|
messages: [{ role: "user", content: `[TEXT TO SUMMARIZE]\n${text}\n[/TEXT TO SUMMARIZE]` }],
|
|
@@ -358,12 +343,12 @@ export async function judgeDedupAnthropic(
|
|
|
358
343
|
headers,
|
|
359
344
|
body: JSON.stringify({
|
|
360
345
|
model,
|
|
361
|
-
max_tokens:
|
|
346
|
+
max_tokens: 2000,
|
|
362
347
|
temperature: 0,
|
|
363
348
|
system: DEDUP_JUDGE_PROMPT,
|
|
364
349
|
messages: [{ role: "user", content: `NEW MEMORY:\n${newSummary}\n\nEXISTING MEMORIES:\n${candidateText}` }],
|
|
365
350
|
}),
|
|
366
|
-
signal: AbortSignal.timeout(cfg.timeoutMs ??
|
|
351
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
367
352
|
});
|
|
368
353
|
|
|
369
354
|
if (!resp.ok) {
|
|
@@ -126,9 +126,9 @@ export async function generateTaskTitleBedrock(
|
|
|
126
126
|
body: JSON.stringify({
|
|
127
127
|
system: [{ text: TASK_TITLE_PROMPT }],
|
|
128
128
|
messages: [{ role: "user", content: [{ text }] }],
|
|
129
|
-
inferenceConfig: { temperature: 0, maxTokens:
|
|
129
|
+
inferenceConfig: { temperature: 0, maxTokens: 2000 },
|
|
130
130
|
}),
|
|
131
|
-
signal: AbortSignal.timeout(cfg.timeoutMs ??
|
|
131
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
132
132
|
});
|
|
133
133
|
|
|
134
134
|
if (!resp.ok) {
|
|
@@ -195,9 +195,9 @@ export async function judgeNewTopicBedrock(
|
|
|
195
195
|
body: JSON.stringify({
|
|
196
196
|
system: [{ text: TOPIC_JUDGE_PROMPT }],
|
|
197
197
|
messages: [{ role: "user", content: [{ text: userContent }] }],
|
|
198
|
-
inferenceConfig: { temperature: 0, maxTokens:
|
|
198
|
+
inferenceConfig: { temperature: 0, maxTokens: 2000 },
|
|
199
199
|
}),
|
|
200
|
-
signal: AbortSignal.timeout(cfg.timeoutMs ??
|
|
200
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
201
201
|
});
|
|
202
202
|
|
|
203
203
|
if (!resp.ok) {
|
|
@@ -226,10 +226,12 @@ RULES:
|
|
|
226
226
|
|
|
227
227
|
OUTPUT — JSON only:
|
|
228
228
|
{"relevant":[1,3],"sufficient":true}
|
|
229
|
-
- "relevant": candidate numbers whose content helps answer the query. [] if none can help.
|
|
230
|
-
- "sufficient": true only if the selected memories fully answer the query
|
|
229
|
+
- "relevant": candidate numbers whose content helps answer the query. [] if none can help. Duplicates removed — only unique information.
|
|
230
|
+
- "sufficient": true only if the selected memories fully answer the query.
|
|
231
231
|
|
|
232
|
-
|
|
232
|
+
IMPORTANT FOR REASONING MODELS: After your analysis, you MUST output a valid JSON object in this exact format. Do not output any text after the JSON object.`;
|
|
233
|
+
|
|
234
|
+
import { parseFilterResult, type FilterResult } from "./openai";
|
|
233
235
|
export type { FilterResult } from "./openai";
|
|
234
236
|
|
|
235
237
|
export async function filterRelevantBedrock(
|
|
@@ -263,9 +265,9 @@ export async function filterRelevantBedrock(
|
|
|
263
265
|
body: JSON.stringify({
|
|
264
266
|
system: [{ text: FILTER_RELEVANT_PROMPT }],
|
|
265
267
|
messages: [{ role: "user", content: [{ text: `QUERY: ${query}\n\nCANDIDATES:\n${candidateText}` }] }],
|
|
266
|
-
inferenceConfig: { temperature: 0, maxTokens:
|
|
268
|
+
inferenceConfig: { temperature: 0, maxTokens: 2000 },
|
|
267
269
|
}),
|
|
268
|
-
signal: AbortSignal.timeout(cfg.timeoutMs ??
|
|
270
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
269
271
|
});
|
|
270
272
|
|
|
271
273
|
if (!resp.ok) {
|
|
@@ -279,23 +281,6 @@ export async function filterRelevantBedrock(
|
|
|
279
281
|
return parseFilterResult(raw, log);
|
|
280
282
|
}
|
|
281
283
|
|
|
282
|
-
function parseFilterResult(raw: string, log: Logger): FilterResult {
|
|
283
|
-
try {
|
|
284
|
-
const match = raw.match(/\{[\s\S]*\}/);
|
|
285
|
-
if (match) {
|
|
286
|
-
const obj = JSON.parse(match[0]);
|
|
287
|
-
if (obj && Array.isArray(obj.relevant)) {
|
|
288
|
-
return {
|
|
289
|
-
relevant: obj.relevant.filter((n: any) => typeof n === "number"),
|
|
290
|
-
sufficient: obj.sufficient === true,
|
|
291
|
-
};
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
} catch {}
|
|
295
|
-
log.warn(`filterRelevant: failed to parse LLM output: "${raw}", fallback to all+insufficient`);
|
|
296
|
-
return { relevant: [], sufficient: false };
|
|
297
|
-
}
|
|
298
|
-
|
|
299
284
|
export async function summarizeBedrock(
|
|
300
285
|
text: string,
|
|
301
286
|
cfg: SummarizerConfig,
|
|
@@ -321,7 +306,7 @@ export async function summarizeBedrock(
|
|
|
321
306
|
messages: [{ role: "user", content: [{ text: `[TEXT TO SUMMARIZE]\n${text}\n[/TEXT TO SUMMARIZE]` }] }],
|
|
322
307
|
inferenceConfig: {
|
|
323
308
|
temperature: cfg.temperature ?? 0,
|
|
324
|
-
maxTokens:
|
|
309
|
+
maxTokens: 2000,
|
|
325
310
|
},
|
|
326
311
|
}),
|
|
327
312
|
signal: AbortSignal.timeout(cfg.timeoutMs ?? 30_000),
|
|
@@ -364,9 +349,9 @@ export async function judgeDedupBedrock(
|
|
|
364
349
|
body: JSON.stringify({
|
|
365
350
|
system: [{ text: DEDUP_JUDGE_PROMPT }],
|
|
366
351
|
messages: [{ role: "user", content: [{ text: `NEW MEMORY:\n${newSummary}\n\nEXISTING MEMORIES:\n${candidateText}` }] }],
|
|
367
|
-
inferenceConfig: { temperature: 0, maxTokens:
|
|
352
|
+
inferenceConfig: { temperature: 0, maxTokens: 2000 },
|
|
368
353
|
}),
|
|
369
|
-
signal: AbortSignal.timeout(cfg.timeoutMs ??
|
|
354
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
370
355
|
});
|
|
371
356
|
|
|
372
357
|
if (!resp.ok) {
|
|
@@ -124,9 +124,9 @@ export async function generateTaskTitleGemini(
|
|
|
124
124
|
body: JSON.stringify({
|
|
125
125
|
systemInstruction: { parts: [{ text: TASK_TITLE_PROMPT }] },
|
|
126
126
|
contents: [{ parts: [{ text }] }],
|
|
127
|
-
generationConfig: { temperature: 0, maxOutputTokens:
|
|
127
|
+
generationConfig: { temperature: 0, maxOutputTokens: 2000 },
|
|
128
128
|
}),
|
|
129
|
-
signal: AbortSignal.timeout(cfg.timeoutMs ??
|
|
129
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
130
130
|
});
|
|
131
131
|
|
|
132
132
|
if (!resp.ok) {
|
|
@@ -192,9 +192,9 @@ export async function judgeNewTopicGemini(
|
|
|
192
192
|
body: JSON.stringify({
|
|
193
193
|
systemInstruction: { parts: [{ text: TOPIC_JUDGE_PROMPT }] },
|
|
194
194
|
contents: [{ parts: [{ text: userContent }] }],
|
|
195
|
-
generationConfig: { temperature: 0, maxOutputTokens:
|
|
195
|
+
generationConfig: { temperature: 0, maxOutputTokens: 2000 },
|
|
196
196
|
}),
|
|
197
|
-
signal: AbortSignal.timeout(cfg.timeoutMs ??
|
|
197
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
198
198
|
});
|
|
199
199
|
|
|
200
200
|
if (!resp.ok) {
|
|
@@ -223,10 +223,12 @@ RULES:
|
|
|
223
223
|
|
|
224
224
|
OUTPUT — JSON only:
|
|
225
225
|
{"relevant":[1,3],"sufficient":true}
|
|
226
|
-
- "relevant": candidate numbers whose content helps answer the query. [] if none can help.
|
|
227
|
-
- "sufficient": true only if the selected memories fully answer the query
|
|
226
|
+
- "relevant": candidate numbers whose content helps answer the query. [] if none can help. Duplicates removed — only unique information.
|
|
227
|
+
- "sufficient": true only if the selected memories fully answer the query.
|
|
228
228
|
|
|
229
|
-
|
|
229
|
+
IMPORTANT FOR REASONING MODELS: After your analysis, you MUST output a valid JSON object in this exact format. Do not output any text after the JSON object.`;
|
|
230
|
+
|
|
231
|
+
import { parseFilterResult, type FilterResult } from "./openai";
|
|
230
232
|
export type { FilterResult } from "./openai";
|
|
231
233
|
|
|
232
234
|
export async function filterRelevantGemini(
|
|
@@ -259,9 +261,9 @@ export async function filterRelevantGemini(
|
|
|
259
261
|
body: JSON.stringify({
|
|
260
262
|
systemInstruction: { parts: [{ text: FILTER_RELEVANT_PROMPT }] },
|
|
261
263
|
contents: [{ parts: [{ text: `QUERY: ${query}\n\nCANDIDATES:\n${candidateText}` }] }],
|
|
262
|
-
generationConfig: { temperature: 0, maxOutputTokens:
|
|
264
|
+
generationConfig: { temperature: 0, maxOutputTokens: 2000 },
|
|
263
265
|
}),
|
|
264
|
-
signal: AbortSignal.timeout(cfg.timeoutMs ??
|
|
266
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
265
267
|
});
|
|
266
268
|
|
|
267
269
|
if (!resp.ok) {
|
|
@@ -275,23 +277,6 @@ export async function filterRelevantGemini(
|
|
|
275
277
|
return parseFilterResult(raw, log);
|
|
276
278
|
}
|
|
277
279
|
|
|
278
|
-
function parseFilterResult(raw: string, log: Logger): FilterResult {
|
|
279
|
-
try {
|
|
280
|
-
const match = raw.match(/\{[\s\S]*\}/);
|
|
281
|
-
if (match) {
|
|
282
|
-
const obj = JSON.parse(match[0]);
|
|
283
|
-
if (obj && Array.isArray(obj.relevant)) {
|
|
284
|
-
return {
|
|
285
|
-
relevant: obj.relevant.filter((n: any) => typeof n === "number"),
|
|
286
|
-
sufficient: obj.sufficient === true,
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
} catch {}
|
|
291
|
-
log.warn(`filterRelevant: failed to parse LLM output: "${raw}", fallback to all+insufficient`);
|
|
292
|
-
return { relevant: [], sufficient: false };
|
|
293
|
-
}
|
|
294
|
-
|
|
295
280
|
export async function summarizeGemini(
|
|
296
281
|
text: string,
|
|
297
282
|
cfg: SummarizerConfig,
|
|
@@ -314,7 +299,7 @@ export async function summarizeGemini(
|
|
|
314
299
|
body: JSON.stringify({
|
|
315
300
|
systemInstruction: { parts: [{ text: SYSTEM_PROMPT }] },
|
|
316
301
|
contents: [{ parts: [{ text: `[TEXT TO SUMMARIZE]\n${text}\n[/TEXT TO SUMMARIZE]` }] }],
|
|
317
|
-
generationConfig: { temperature: cfg.temperature ?? 0, maxOutputTokens:
|
|
302
|
+
generationConfig: { temperature: cfg.temperature ?? 0, maxOutputTokens: 2000 },
|
|
318
303
|
}),
|
|
319
304
|
signal: AbortSignal.timeout(cfg.timeoutMs ?? 30_000),
|
|
320
305
|
});
|
|
@@ -355,9 +340,9 @@ export async function judgeDedupGemini(
|
|
|
355
340
|
body: JSON.stringify({
|
|
356
341
|
systemInstruction: { parts: [{ text: DEDUP_JUDGE_PROMPT }] },
|
|
357
342
|
contents: [{ parts: [{ text: `NEW MEMORY:\n${newSummary}\n\nEXISTING MEMORIES:\n${candidateText}` }] }],
|
|
358
|
-
generationConfig: { temperature: 0, maxOutputTokens:
|
|
343
|
+
generationConfig: { temperature: 0, maxOutputTokens: 2000 },
|
|
359
344
|
}),
|
|
360
|
-
signal: AbortSignal.timeout(cfg.timeoutMs ??
|
|
345
|
+
signal: AbortSignal.timeout(cfg.timeoutMs ?? 60_000),
|
|
361
346
|
});
|
|
362
347
|
|
|
363
348
|
if (!resp.ok) {
|
|
@@ -230,8 +230,13 @@ export class Summarizer {
|
|
|
230
230
|
return ruleFallback(cleaned);
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
const accept = (s: string | undefined): s is string =>
|
|
234
|
-
|
|
233
|
+
const accept = (s: string | undefined): s is string => {
|
|
234
|
+
if (!s || s.length === 0) return false;
|
|
235
|
+
// Allow the LLM result if it's shorter than input, or if the input is quite short itself (<= 20 chars).
|
|
236
|
+
// Summarizing a very short text (e.g. "hi there") might yield a slightly longer formal summary (e.g. "User said hi").
|
|
237
|
+
if (cleaned.length <= 20) return true;
|
|
238
|
+
return s.length < cleaned.length;
|
|
239
|
+
};
|
|
235
240
|
|
|
236
241
|
let llmCalled = false;
|
|
237
242
|
try {
|