@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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@memtensor/memos-local-openclaw-plugin)](https://www.npmjs.com/package/@memtensor/memos-local-openclaw-plugin)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/MemTensor/MemOS/blob/main/LICENSE)
5
- [![Node.js >= 18](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org/)
5
+ [![Node.js >= 22](https://img.shields.io/badge/node-%3E%3D22-brightgreen)](https://nodejs.org/)
6
6
  [![GitHub](https://img.shields.io/badge/GitHub-Source-181717?logo=github)](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:** [docs/troubleshooting.html](https://memtensor.github.io/MemOS/apps/memos-local-openclaw/docs/troubleshooting.html) — 包含逐步排查流程、日志查看方法、完全重装步骤等。
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 **>= 18**)
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 >= 18** (`node -v`)
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://memtensor.github.io/MemOS/apps/memos-local-openclaw/docs/troubleshooting.html).
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 (local preview) | Started automatically by the plugin |
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
- const startServiceCore = async () => {
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
- api.registerService({
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
- viewer.stop();
2436
- store.close();
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 the next tick instead of waiting several seconds; the
2444
- // serviceStarted guard still prevents duplicate startup if the host calls
2445
- // service.start() immediately after registration.
2446
- const SELF_START_DELAY_MS = 0;
2447
- setTimeout(() => {
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
 
@@ -20,7 +20,7 @@
20
20
  }
21
21
  },
22
22
  "requirements": {
23
- "node": ">=18.0.0",
23
+ "node": ">=22.0.0",
24
24
  "openclaw": ">=2026.2.0"
25
25
  },
26
26
  "setup": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.9-beta.1",
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": ">=18.0.0 <25.0.0"
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: 100,
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 ?? 15_000),
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: 10,
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 ?? 15_000),
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
- import type { FilterResult } from "./openai";
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: 200,
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 ?? 15_000),
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: 100,
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: 300,
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 ?? 15_000),
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: 100 },
129
+ inferenceConfig: { temperature: 0, maxTokens: 2000 },
130
130
  }),
131
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
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: 10 },
198
+ inferenceConfig: { temperature: 0, maxTokens: 2000 },
199
199
  }),
200
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
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
- import type { FilterResult } from "./openai";
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: 200 },
268
+ inferenceConfig: { temperature: 0, maxTokens: 2000 },
267
269
  }),
268
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
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: 100,
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: 300 },
352
+ inferenceConfig: { temperature: 0, maxTokens: 2000 },
368
353
  }),
369
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
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: 100 },
127
+ generationConfig: { temperature: 0, maxOutputTokens: 2000 },
128
128
  }),
129
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
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: 10 },
195
+ generationConfig: { temperature: 0, maxOutputTokens: 2000 },
196
196
  }),
197
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
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
- import type { FilterResult } from "./openai";
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: 200 },
264
+ generationConfig: { temperature: 0, maxOutputTokens: 2000 },
263
265
  }),
264
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
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: 100 },
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: 300 },
343
+ generationConfig: { temperature: 0, maxOutputTokens: 2000 },
359
344
  }),
360
- signal: AbortSignal.timeout(cfg.timeoutMs ?? 15_000),
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
- !!s && s.length > 0 && s.length < cleaned.length;
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 {