@memtensor/memos-local-openclaw-plugin 1.0.7 → 1.0.8-beta.10
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/.env.example +4 -0
- package/index.ts +137 -87
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -3
- package/scripts/postinstall.cjs +59 -25
- package/skill/memos-memory-guide/SKILL.md +5 -2
- package/src/client/hub.ts +11 -0
- package/src/hub/server.ts +13 -6
- package/src/ingest/providers/anthropic.ts +9 -6
- package/src/ingest/providers/bedrock.ts +9 -6
- package/src/ingest/providers/gemini.ts +9 -6
- package/src/ingest/providers/index.ts +136 -22
- package/src/ingest/providers/openai.ts +141 -6
- package/src/ingest/task-processor.ts +61 -41
- package/src/ingest/worker.ts +32 -11
- package/src/recall/engine.ts +2 -1
- package/src/shared/llm-call.ts +14 -1
- package/src/sharing/types.ts +1 -0
- package/src/storage/sqlite.ts +194 -11
- package/src/types.ts +3 -0
- package/src/viewer/html.ts +953 -281
- package/src/viewer/server.ts +305 -20
package/.env.example
CHANGED
|
@@ -18,6 +18,10 @@ SUMMARIZER_TEMPERATURE=0
|
|
|
18
18
|
# Port for the web-based Memory Viewer (default: 18799)
|
|
19
19
|
# VIEWER_PORT=18799
|
|
20
20
|
|
|
21
|
+
# ─── Tavily Search (optional) ───
|
|
22
|
+
# API key for Tavily web search (get from https://app.tavily.com)
|
|
23
|
+
# TAVILY_API_KEY=tvly-your-tavily-api-key
|
|
24
|
+
|
|
21
25
|
# ─── Telemetry (opt-out) ───
|
|
22
26
|
# Anonymous usage analytics to help improve the plugin.
|
|
23
27
|
# No memory content, queries, or personal data is ever sent — only tool names, latencies, and version info.
|
package/index.ts
CHANGED
|
@@ -53,6 +53,45 @@ function deduplicateHits<T extends { summary: string }>(hits: T[]): T[] {
|
|
|
53
53
|
return kept;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
const NEW_SESSION_PROMPT_RE = /A new session was started via \/new or \/reset\./i;
|
|
57
|
+
const INTERNAL_CONTEXT_RE = /OpenClaw runtime context \(internal\):[\s\S]*/i;
|
|
58
|
+
const CONTINUE_PROMPT_RE = /^Continue where you left off\.[\s\S]*/i;
|
|
59
|
+
|
|
60
|
+
function normalizeAutoRecallQuery(rawPrompt: string): string {
|
|
61
|
+
let query = rawPrompt.trim();
|
|
62
|
+
|
|
63
|
+
const senderTag = "Sender (untrusted metadata):";
|
|
64
|
+
const senderPos = query.indexOf(senderTag);
|
|
65
|
+
if (senderPos !== -1) {
|
|
66
|
+
const afterSender = query.slice(senderPos);
|
|
67
|
+
const fenceStart = afterSender.indexOf("```json");
|
|
68
|
+
const fenceEnd = fenceStart >= 0 ? afterSender.indexOf("```\n", fenceStart + 7) : -1;
|
|
69
|
+
if (fenceEnd > 0) {
|
|
70
|
+
query = afterSender.slice(fenceEnd + 4).replace(/^\s*\n/, "").trim();
|
|
71
|
+
} else {
|
|
72
|
+
const firstDblNl = afterSender.indexOf("\n\n");
|
|
73
|
+
if (firstDblNl > 0) {
|
|
74
|
+
query = afterSender.slice(firstDblNl + 2).trim();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
query = stripInboundMetadata(query);
|
|
80
|
+
query = query.replace(/<[^>]+>/g, "").trim();
|
|
81
|
+
|
|
82
|
+
if (NEW_SESSION_PROMPT_RE.test(query)) {
|
|
83
|
+
query = query.replace(NEW_SESSION_PROMPT_RE, "").trim();
|
|
84
|
+
query = query.replace(/^(Execute|Run) your Session Startup sequence[^\n]*\n?/im, "").trim();
|
|
85
|
+
query = query.replace(/^Current time:[^\n]*(\n|$)/im, "").trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
query = query.replace(INTERNAL_CONTEXT_RE, "").trim();
|
|
89
|
+
query = query.replace(CONTINUE_PROMPT_RE, "").trim();
|
|
90
|
+
|
|
91
|
+
return query;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
56
95
|
const pluginConfigSchema = {
|
|
57
96
|
type: "object" as const,
|
|
58
97
|
additionalProperties: true,
|
|
@@ -278,7 +317,7 @@ const memosLocalPlugin = {
|
|
|
278
317
|
const raw = fs.readFileSync(openclawJsonPath, "utf-8");
|
|
279
318
|
const cfg = JSON.parse(raw);
|
|
280
319
|
const allow: string[] | undefined = cfg?.tools?.allow;
|
|
281
|
-
if (Array.isArray(allow) && allow.length > 0 && !allow.includes("group:plugins")) {
|
|
320
|
+
if (Array.isArray(allow) && allow.length > 0 && !allow.includes("group:plugins") && !allow.includes("*")) {
|
|
282
321
|
const lastEntry = JSON.stringify(allow[allow.length - 1]);
|
|
283
322
|
const patched = raw.replace(
|
|
284
323
|
new RegExp(`(${lastEntry})(\\s*\\])`),
|
|
@@ -307,6 +346,7 @@ const memosLocalPlugin = {
|
|
|
307
346
|
// Current agent ID — updated by hooks, read by tools for owner isolation.
|
|
308
347
|
// Falls back to "main" when no hook has fired yet (single-agent setups).
|
|
309
348
|
let currentAgentId = "main";
|
|
349
|
+
const getCurrentOwner = () => `agent:${currentAgentId}`;
|
|
310
350
|
|
|
311
351
|
// ─── Check allowPromptInjection policy ───
|
|
312
352
|
// When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
|
|
@@ -354,7 +394,6 @@ const memosLocalPlugin = {
|
|
|
354
394
|
}
|
|
355
395
|
};
|
|
356
396
|
|
|
357
|
-
const getCurrentOwner = () => `agent:${currentAgentId}`;
|
|
358
397
|
const resolveMemorySearchScope = (scope?: string): "local" | "group" | "all" =>
|
|
359
398
|
scope === "group" || scope === "all" ? scope : "local";
|
|
360
399
|
const resolveMemoryShareTarget = (target?: string): "agents" | "hub" | "both" =>
|
|
@@ -388,6 +427,7 @@ const memosLocalPlugin = {
|
|
|
388
427
|
body: JSON.stringify({
|
|
389
428
|
memory: {
|
|
390
429
|
sourceChunkId: chunk.id,
|
|
430
|
+
sourceAgent: chunk.owner || "",
|
|
391
431
|
role: chunk.role,
|
|
392
432
|
content: chunk.content,
|
|
393
433
|
summary: chunk.summary,
|
|
@@ -408,6 +448,7 @@ const memosLocalPlugin = {
|
|
|
408
448
|
id: memoryId,
|
|
409
449
|
sourceChunkId: chunk.id,
|
|
410
450
|
sourceUserId: hubClient.userId,
|
|
451
|
+
sourceAgent: chunk.owner || "",
|
|
411
452
|
role: chunk.role,
|
|
412
453
|
content: chunk.content,
|
|
413
454
|
summary: chunk.summary ?? "",
|
|
@@ -445,7 +486,7 @@ const memosLocalPlugin = {
|
|
|
445
486
|
// ─── Tool: memory_search ───
|
|
446
487
|
|
|
447
488
|
api.registerTool(
|
|
448
|
-
{
|
|
489
|
+
(context) => ({
|
|
449
490
|
name: "memory_search",
|
|
450
491
|
label: "Memory Search",
|
|
451
492
|
description:
|
|
@@ -461,7 +502,7 @@ const memosLocalPlugin = {
|
|
|
461
502
|
hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
|
|
462
503
|
userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
|
|
463
504
|
}),
|
|
464
|
-
execute: trackTool("memory_search", async (_toolCallId: any, params: any
|
|
505
|
+
execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
|
|
465
506
|
const {
|
|
466
507
|
query,
|
|
467
508
|
scope: rawScope,
|
|
@@ -482,9 +523,6 @@ const memosLocalPlugin = {
|
|
|
482
523
|
const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
|
|
483
524
|
const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
|
|
484
525
|
let searchScope = resolveMemorySearchScope(rawScope);
|
|
485
|
-
if (searchScope === "local" && ctx.config?.sharing?.enabled) {
|
|
486
|
-
searchScope = "all";
|
|
487
|
-
}
|
|
488
526
|
const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
|
|
489
527
|
|
|
490
528
|
const agentId = context?.agentId ?? currentAgentId;
|
|
@@ -504,7 +542,7 @@ const memosLocalPlugin = {
|
|
|
504
542
|
|
|
505
543
|
// Split local results: pure-local vs hub-memory (Hub role's hub_memories mixed in by RecallEngine)
|
|
506
544
|
const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
|
|
507
|
-
const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
|
|
545
|
+
const hubLocalHits = searchScope !== "local" ? result.hits.filter((h) => h.origin === "hub-memory") : [];
|
|
508
546
|
|
|
509
547
|
const rawLocalCandidates = localHits.map((h) => ({
|
|
510
548
|
chunkId: h.ref.chunkId,
|
|
@@ -513,6 +551,7 @@ const memosLocalPlugin = {
|
|
|
513
551
|
summary: h.summary,
|
|
514
552
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200),
|
|
515
553
|
origin: h.origin || "local",
|
|
554
|
+
owner: h.owner || "",
|
|
516
555
|
}));
|
|
517
556
|
|
|
518
557
|
// Hub remote candidates (from HTTP call) + hub-memory candidates (from RecallEngine for Hub role)
|
|
@@ -649,6 +688,7 @@ const memosLocalPlugin = {
|
|
|
649
688
|
chunkId: h.ref.chunkId, taskId: effectiveTaskId, skillId: h.skillId,
|
|
650
689
|
role: h.source.role, score: h.score, summary: h.summary,
|
|
651
690
|
original_excerpt: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "local",
|
|
691
|
+
owner: h.owner || "",
|
|
652
692
|
};
|
|
653
693
|
}),
|
|
654
694
|
...filteredHubRemoteHits.map((h: any) => ({
|
|
@@ -656,6 +696,7 @@ const memosLocalPlugin = {
|
|
|
656
696
|
role: h.source?.role ?? h.role ?? "assistant", score: h.score ?? 0,
|
|
657
697
|
summary: h.summary ?? "", original_excerpt: (h.excerpt ?? h.summary ?? "").slice(0, 200),
|
|
658
698
|
origin: "hub-remote", ownerName: h.ownerName ?? "", groupName: h.groupName ?? "",
|
|
699
|
+
sourceAgent: h.sourceAgent ?? "",
|
|
659
700
|
})),
|
|
660
701
|
];
|
|
661
702
|
|
|
@@ -669,14 +710,14 @@ const memosLocalPlugin = {
|
|
|
669
710
|
},
|
|
670
711
|
};
|
|
671
712
|
}),
|
|
672
|
-
},
|
|
713
|
+
}),
|
|
673
714
|
{ name: "memory_search" },
|
|
674
715
|
);
|
|
675
716
|
|
|
676
717
|
// ─── Tool: memory_timeline ───
|
|
677
718
|
|
|
678
719
|
api.registerTool(
|
|
679
|
-
{
|
|
720
|
+
(context) => ({
|
|
680
721
|
name: "memory_timeline",
|
|
681
722
|
label: "Memory Timeline",
|
|
682
723
|
description:
|
|
@@ -686,7 +727,7 @@ const memosLocalPlugin = {
|
|
|
686
727
|
chunkId: Type.String({ description: "The chunkId from a memory_search hit" }),
|
|
687
728
|
window: Type.Optional(Type.Number({ description: "Context window ±N (default 2)" })),
|
|
688
729
|
}),
|
|
689
|
-
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any
|
|
730
|
+
execute: trackTool("memory_timeline", async (_toolCallId: any, params: any) => {
|
|
690
731
|
const agentId = context?.agentId ?? currentAgentId;
|
|
691
732
|
ctx.log.debug(`memory_timeline called (agent=${agentId})`);
|
|
692
733
|
const { chunkId, window: win } = params as {
|
|
@@ -730,14 +771,14 @@ const memosLocalPlugin = {
|
|
|
730
771
|
details: { entries, anchorRef: { sessionKey: anchorChunk.sessionKey, chunkId, turnId: anchorChunk.turnId, seq: anchorChunk.seq } },
|
|
731
772
|
};
|
|
732
773
|
}),
|
|
733
|
-
},
|
|
774
|
+
}),
|
|
734
775
|
{ name: "memory_timeline" },
|
|
735
776
|
);
|
|
736
777
|
|
|
737
778
|
// ─── Tool: memory_get ───
|
|
738
779
|
|
|
739
780
|
api.registerTool(
|
|
740
|
-
{
|
|
781
|
+
(context) => ({
|
|
741
782
|
name: "memory_get",
|
|
742
783
|
label: "Memory Get",
|
|
743
784
|
description:
|
|
@@ -748,7 +789,7 @@ const memosLocalPlugin = {
|
|
|
748
789
|
Type.Number({ description: `Max chars (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax})` }),
|
|
749
790
|
),
|
|
750
791
|
}),
|
|
751
|
-
execute: trackTool("memory_get", async (_toolCallId: any, params: any
|
|
792
|
+
execute: trackTool("memory_get", async (_toolCallId: any, params: any) => {
|
|
752
793
|
const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
|
|
753
794
|
const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
|
|
754
795
|
|
|
@@ -774,7 +815,7 @@ const memosLocalPlugin = {
|
|
|
774
815
|
},
|
|
775
816
|
};
|
|
776
817
|
}),
|
|
777
|
-
},
|
|
818
|
+
}),
|
|
778
819
|
{ name: "memory_get" },
|
|
779
820
|
);
|
|
780
821
|
|
|
@@ -1115,6 +1156,10 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1115
1156
|
};
|
|
1116
1157
|
}
|
|
1117
1158
|
|
|
1159
|
+
const disabledWarning = skill.status === "archived"
|
|
1160
|
+
? "\n\n> **Warning:** This skill is currently **disabled** (archived). Its content is shown for reference only — it will not be used in search or auto-recall.\n\n"
|
|
1161
|
+
: "";
|
|
1162
|
+
|
|
1118
1163
|
const manifest = skillInstaller.getCompanionManifest(resolvedSkillId);
|
|
1119
1164
|
let footer = "\n\n---\n";
|
|
1120
1165
|
|
|
@@ -1139,7 +1184,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1139
1184
|
return {
|
|
1140
1185
|
content: [{
|
|
1141
1186
|
type: "text",
|
|
1142
|
-
text: `## Skill: ${skill.name} (v${skill.version})\n\n${sv.content}${footer}`,
|
|
1187
|
+
text: `## Skill: ${skill.name} (v${skill.version})${disabledWarning}\n\n${sv.content}${footer}`,
|
|
1143
1188
|
}],
|
|
1144
1189
|
details: {
|
|
1145
1190
|
skillId: skill.id,
|
|
@@ -1296,7 +1341,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1296
1341
|
const viewerPort = (pluginCfg as any).viewerPort ?? (gatewayPort + 10);
|
|
1297
1342
|
|
|
1298
1343
|
api.registerTool(
|
|
1299
|
-
{
|
|
1344
|
+
(context) => ({
|
|
1300
1345
|
name: "memory_viewer",
|
|
1301
1346
|
label: "Open Memory Viewer",
|
|
1302
1347
|
description:
|
|
@@ -1304,10 +1349,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1304
1349
|
"or access their stored memories, or asks where the memory dashboard is. " +
|
|
1305
1350
|
"Returns the URL the user can open in their browser.",
|
|
1306
1351
|
parameters: Type.Object({}),
|
|
1307
|
-
execute: trackTool("memory_viewer", async () => {
|
|
1352
|
+
execute: trackTool("memory_viewer", async (_toolCallId: any, params: any) => {
|
|
1308
1353
|
ctx.log.debug(`memory_viewer called`);
|
|
1309
1354
|
telemetry.trackViewerOpened();
|
|
1310
|
-
const
|
|
1355
|
+
const agentId = context?.agentId ?? context?.profileId ?? currentAgentId;
|
|
1356
|
+
const url = `http://127.0.0.1:${viewerPort}?agentId=${encodeURIComponent(agentId)}`;
|
|
1311
1357
|
return {
|
|
1312
1358
|
content: [
|
|
1313
1359
|
{
|
|
@@ -1328,7 +1374,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1328
1374
|
details: { viewerUrl: url },
|
|
1329
1375
|
};
|
|
1330
1376
|
}),
|
|
1331
|
-
},
|
|
1377
|
+
}),
|
|
1332
1378
|
{ name: "memory_viewer" },
|
|
1333
1379
|
);
|
|
1334
1380
|
|
|
@@ -1401,9 +1447,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1401
1447
|
name: "memory_share",
|
|
1402
1448
|
label: "Share Memory",
|
|
1403
1449
|
description:
|
|
1404
|
-
"Share an existing memory
|
|
1405
|
-
"
|
|
1406
|
-
"
|
|
1450
|
+
"Share an existing stored memory (requires a real chunkId from the database) to the Hub team, or to both targets. " +
|
|
1451
|
+
"If you want to share content from the conversation, please first retrieve the memories related to that content to obtain the correct chunkId(s), then proceed with the sharing. " +
|
|
1452
|
+
"target='agents' (default): when retrieved memories would clearly help other agents in the same OpenClaw workspace, you may share proactively without asking the user. " +
|
|
1453
|
+
"target='hub' or 'both': do not share to the team Hub without explicit user consent when the content would benefit collaborators—explain briefly, ask first, and only call hub/both after they agree (Hub must be configured). " +
|
|
1454
|
+
"To create a brand-new shared note with no existing chunk, use memory_write_public.",
|
|
1407
1455
|
parameters: Type.Object({
|
|
1408
1456
|
chunkId: Type.String({ description: "Existing local memory chunk ID to share" }),
|
|
1409
1457
|
target: Type.Optional(Type.String({ description: "Share target: 'agents' (default), 'hub', or 'both'" })),
|
|
@@ -1557,7 +1605,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1557
1605
|
// ─── Tool: skill_search ───
|
|
1558
1606
|
|
|
1559
1607
|
api.registerTool(
|
|
1560
|
-
{
|
|
1608
|
+
(context) => ({
|
|
1561
1609
|
name: "skill_search",
|
|
1562
1610
|
label: "Skill Search",
|
|
1563
1611
|
description:
|
|
@@ -1567,10 +1615,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1567
1615
|
query: Type.String({ description: "Natural language description of the needed skill" }),
|
|
1568
1616
|
scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
|
|
1569
1617
|
}),
|
|
1570
|
-
execute: trackTool("skill_search", async (_toolCallId: any, params: any
|
|
1618
|
+
execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
|
|
1571
1619
|
const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
|
|
1572
1620
|
const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
|
|
1573
|
-
const
|
|
1621
|
+
const agentId = context?.agentId ?? currentAgentId;
|
|
1622
|
+
const currentOwner = `agent:${agentId}`;
|
|
1574
1623
|
|
|
1575
1624
|
if (rawScope === "group" || rawScope === "all") {
|
|
1576
1625
|
const [localHits, hub] = await Promise.all([
|
|
@@ -1632,7 +1681,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1632
1681
|
details: { query: skillQuery, scope, hits },
|
|
1633
1682
|
};
|
|
1634
1683
|
}),
|
|
1635
|
-
},
|
|
1684
|
+
}),
|
|
1636
1685
|
{ name: "skill_search" },
|
|
1637
1686
|
);
|
|
1638
1687
|
|
|
@@ -1777,7 +1826,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1777
1826
|
if (!allowPromptInjection) return {};
|
|
1778
1827
|
if (!event.prompt || event.prompt.length < 3) return;
|
|
1779
1828
|
|
|
1780
|
-
const recallAgentId = hookCtx?.agentId ?? "main";
|
|
1829
|
+
const recallAgentId = hookCtx?.agentId ?? (event as any)?.agentId ?? (event as any)?.profileId ?? "main";
|
|
1781
1830
|
currentAgentId = recallAgentId;
|
|
1782
1831
|
const recallOwnerFilter = [`agent:${recallAgentId}`, "public"];
|
|
1783
1832
|
ctx.log.info(`auto-recall: agentId=${recallAgentId} (from hookCtx)`);
|
|
@@ -1789,24 +1838,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1789
1838
|
const rawPrompt = event.prompt;
|
|
1790
1839
|
ctx.log.debug(`auto-recall: rawPrompt="${rawPrompt.slice(0, 300)}"`);
|
|
1791
1840
|
|
|
1792
|
-
|
|
1793
|
-
const senderTag = "Sender (untrusted metadata):";
|
|
1794
|
-
const senderPos = rawPrompt.indexOf(senderTag);
|
|
1795
|
-
if (senderPos !== -1) {
|
|
1796
|
-
const afterSender = rawPrompt.slice(senderPos);
|
|
1797
|
-
const fenceStart = afterSender.indexOf("```json");
|
|
1798
|
-
const fenceEnd = fenceStart >= 0 ? afterSender.indexOf("```\n", fenceStart + 7) : -1;
|
|
1799
|
-
if (fenceEnd > 0) {
|
|
1800
|
-
query = afterSender.slice(fenceEnd + 4).replace(/^\s*\n/, "").trim();
|
|
1801
|
-
} else {
|
|
1802
|
-
const firstDblNl = afterSender.indexOf("\n\n");
|
|
1803
|
-
if (firstDblNl > 0) {
|
|
1804
|
-
query = afterSender.slice(firstDblNl + 2).trim();
|
|
1805
|
-
}
|
|
1806
|
-
}
|
|
1807
|
-
}
|
|
1808
|
-
query = stripInboundMetadata(query);
|
|
1809
|
-
query = query.replace(/<[^>]+>/g, "").trim();
|
|
1841
|
+
const query = normalizeAutoRecallQuery(rawPrompt);
|
|
1810
1842
|
recallQuery = query;
|
|
1811
1843
|
|
|
1812
1844
|
if (query.length < 2) {
|
|
@@ -1845,6 +1877,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1845
1877
|
const rawLocalCandidates = localHits.map((h) => ({
|
|
1846
1878
|
score: h.score, role: h.source.role, summary: h.summary,
|
|
1847
1879
|
content: (h.original_excerpt ?? "").slice(0, 200), origin: h.origin || "local",
|
|
1880
|
+
owner: h.owner || "",
|
|
1848
1881
|
}));
|
|
1849
1882
|
const rawHubCandidates = allHubHits.map((h) => ({
|
|
1850
1883
|
score: h.score, role: h.source.role, summary: h.summary,
|
|
@@ -1988,7 +2021,6 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
1988
2021
|
try {
|
|
1989
2022
|
const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
|
|
1990
2023
|
|
|
1991
|
-
// Source 1: direct skill search based on user query
|
|
1992
2024
|
try {
|
|
1993
2025
|
const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
|
|
1994
2026
|
for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
|
|
@@ -2000,7 +2032,6 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2000
2032
|
ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
|
|
2001
2033
|
}
|
|
2002
2034
|
|
|
2003
|
-
// Source 2: skills linked to tasks from memory hits
|
|
2004
2035
|
const taskIds = new Set<string>();
|
|
2005
2036
|
for (const h of filteredHits) {
|
|
2006
2037
|
if (h.taskId) {
|
|
@@ -2054,7 +2085,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2054
2085
|
store.recordApiLog("memory_search", { type: "auto_recall", query }, JSON.stringify({
|
|
2055
2086
|
candidates: rawLocalCandidates,
|
|
2056
2087
|
hubCandidates: rawHubCandidates,
|
|
2057
|
-
filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local" })),
|
|
2088
|
+
filtered: filteredHits.map(h => ({ score: h.score, role: h.source.role, summary: h.summary, content: h.original_excerpt, origin: h.origin || "local", owner: h.owner || "" })),
|
|
2058
2089
|
}), recallDur, true);
|
|
2059
2090
|
telemetry.trackAutoRecall(filteredHits.length, recallDur);
|
|
2060
2091
|
|
|
@@ -2090,7 +2121,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2090
2121
|
if (!event.success || !event.messages || event.messages.length === 0) return;
|
|
2091
2122
|
|
|
2092
2123
|
try {
|
|
2093
|
-
const captureAgentId = hookCtx?.agentId ?? "main";
|
|
2124
|
+
const captureAgentId = hookCtx?.agentId ?? event?.agentId ?? event?.profileId ?? "main";
|
|
2094
2125
|
currentAgentId = captureAgentId;
|
|
2095
2126
|
const captureOwner = `agent:${captureAgentId}`;
|
|
2096
2127
|
const sessionKey = hookCtx?.sessionKey ?? "default";
|
|
@@ -2308,48 +2339,54 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2308
2339
|
|
|
2309
2340
|
// ─── Service lifecycle ───
|
|
2310
2341
|
|
|
2311
|
-
|
|
2312
|
-
id: "memos-local-openclaw-plugin",
|
|
2313
|
-
start: async () => {
|
|
2314
|
-
if (hubServer) {
|
|
2315
|
-
const hubUrl = await hubServer.start();
|
|
2316
|
-
api.logger.info(`memos-local: hub started at ${hubUrl}`);
|
|
2317
|
-
}
|
|
2342
|
+
let serviceStarted = false;
|
|
2318
2343
|
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
}
|
|
2344
|
+
const startServiceCore = async () => {
|
|
2345
|
+
if (serviceStarted) return;
|
|
2346
|
+
serviceStarted = true;
|
|
2347
|
+
|
|
2348
|
+
if (hubServer) {
|
|
2349
|
+
const hubUrl = await hubServer.start();
|
|
2350
|
+
api.logger.info(`memos-local: hub started at ${hubUrl}`);
|
|
2351
|
+
}
|
|
2328
2352
|
|
|
2353
|
+
if (ctx.config.sharing?.enabled && ctx.config.sharing.role === "client") {
|
|
2329
2354
|
try {
|
|
2330
|
-
const
|
|
2331
|
-
api.logger.info(`memos-local:
|
|
2332
|
-
api.logger.info(`╔══════════════════════════════════════════╗`);
|
|
2333
|
-
api.logger.info(`║ MemOS Memory Viewer ║`);
|
|
2334
|
-
api.logger.info(`║ → ${viewerUrl.padEnd(37)}║`);
|
|
2335
|
-
api.logger.info(`║ Open in browser to manage memories ║`);
|
|
2336
|
-
api.logger.info(`╚══════════════════════════════════════════╝`);
|
|
2337
|
-
api.logger.info(`memos-local: password reset token: ${viewer.getResetToken()}`);
|
|
2338
|
-
api.logger.info(`memos-local: forgot password? Use the reset token on the login page.`);
|
|
2339
|
-
skillEvolver.recoverOrphanedTasks().then((count) => {
|
|
2340
|
-
if (count > 0) api.logger.info(`memos-local: recovered ${count} orphaned skill tasks`);
|
|
2341
|
-
}).catch((err) => {
|
|
2342
|
-
api.logger.warn(`memos-local: skill recovery failed: ${err}`);
|
|
2343
|
-
});
|
|
2355
|
+
const session = await connectToHub(store, ctx.config, ctx.log);
|
|
2356
|
+
api.logger.info(`memos-local: connected to Hub as "${session.username}" (${session.userId})`);
|
|
2344
2357
|
} catch (err) {
|
|
2345
|
-
api.logger.warn(`memos-local:
|
|
2346
|
-
api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
|
|
2358
|
+
api.logger.warn(`memos-local: Hub connection failed: ${err}`);
|
|
2347
2359
|
}
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
);
|
|
2352
|
-
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
try {
|
|
2363
|
+
const viewerUrl = await viewer.start();
|
|
2364
|
+
api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
|
|
2365
|
+
api.logger.info(`╔══════════════════════════════════════════╗`);
|
|
2366
|
+
api.logger.info(`║ MemOS Memory Viewer ║`);
|
|
2367
|
+
api.logger.info(`║ → ${viewerUrl.padEnd(37)}║`);
|
|
2368
|
+
api.logger.info(`║ Open in browser to manage memories ║`);
|
|
2369
|
+
api.logger.info(`╚══════════════════════════════════════════╝`);
|
|
2370
|
+
api.logger.info(`memos-local: password reset token: ${viewer.getResetToken()}`);
|
|
2371
|
+
api.logger.info(`memos-local: forgot password? Use the reset token on the login page.`);
|
|
2372
|
+
skillEvolver.recoverOrphanedTasks().then((count) => {
|
|
2373
|
+
if (count > 0) api.logger.info(`memos-local: recovered ${count} orphaned skill tasks`);
|
|
2374
|
+
}).catch((err) => {
|
|
2375
|
+
api.logger.warn(`memos-local: skill recovery failed: ${err}`);
|
|
2376
|
+
});
|
|
2377
|
+
} catch (err) {
|
|
2378
|
+
api.logger.warn(`memos-local: viewer failed to start: ${err}`);
|
|
2379
|
+
api.logger.info(`memos-local: started (embedding: ${embedder.provider})`);
|
|
2380
|
+
}
|
|
2381
|
+
telemetry.trackPluginStarted(
|
|
2382
|
+
ctx.config.embedding?.provider ?? "local",
|
|
2383
|
+
ctx.config.summarizer?.provider ?? "none",
|
|
2384
|
+
);
|
|
2385
|
+
};
|
|
2386
|
+
|
|
2387
|
+
api.registerService({
|
|
2388
|
+
id: "memos-local-openclaw-plugin",
|
|
2389
|
+
start: async () => { await startServiceCore(); },
|
|
2353
2390
|
stop: async () => {
|
|
2354
2391
|
await worker.flush();
|
|
2355
2392
|
await telemetry.shutdown();
|
|
@@ -2359,6 +2396,19 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
|
|
|
2359
2396
|
api.logger.info("memos-local: stopped");
|
|
2360
2397
|
},
|
|
2361
2398
|
});
|
|
2399
|
+
|
|
2400
|
+
// Fallback: OpenClaw may load this plugin via deferred reload after
|
|
2401
|
+
// startPluginServices has already run, so service.start() never fires.
|
|
2402
|
+
// Self-start the viewer after a grace period if it hasn't been started.
|
|
2403
|
+
const SELF_START_DELAY_MS = 3000;
|
|
2404
|
+
setTimeout(() => {
|
|
2405
|
+
if (!serviceStarted) {
|
|
2406
|
+
api.logger.info("memos-local: service.start() not called by host, self-starting viewer...");
|
|
2407
|
+
startServiceCore().catch((err) => {
|
|
2408
|
+
api.logger.warn(`memos-local: self-start failed: ${err}`);
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
}, SELF_START_DELAY_MS);
|
|
2362
2412
|
},
|
|
2363
2413
|
};
|
|
2364
2414
|
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "MemOS Local Memory",
|
|
4
4
|
"description": "Full-write local conversation memory with hybrid search (RRF + MMR + recency), task summarization, skill evolution, and team sharing (Hub-Client). Provides memory_search, memory_get, task_summary, skill_search, task_share, network_skill_pull, network_team_info, memory_viewer for layered retrieval and team collaboration.",
|
|
5
5
|
"kind": "memory",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.8-beta.9",
|
|
7
7
|
"skills": [
|
|
8
8
|
"skill/memos-memory-guide"
|
|
9
9
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memtensor/memos-local-openclaw-plugin",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8-beta.10",
|
|
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,12 +45,13 @@
|
|
|
45
45
|
],
|
|
46
46
|
"license": "MIT",
|
|
47
47
|
"engines": {
|
|
48
|
-
"node": ">=
|
|
48
|
+
"node": ">=18.0.0 <25.0.0"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@huggingface/transformers": "^3.8.0",
|
|
52
52
|
"@sinclair/typebox": "^0.34.48",
|
|
53
|
-
"better-sqlite3": "^12.6.
|
|
53
|
+
"better-sqlite3": "^12.6.3",
|
|
54
|
+
"posthog-node": "^5.28.0",
|
|
54
55
|
"puppeteer": "^24.38.0",
|
|
55
56
|
"semver": "^7.7.4",
|
|
56
57
|
"uuid": "^10.0.0"
|