@memtensor/memos-local-openclaw-plugin 1.0.6 → 1.0.7-beta.3

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 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
@@ -307,6 +307,7 @@ const memosLocalPlugin = {
307
307
  // Current agent ID — updated by hooks, read by tools for owner isolation.
308
308
  // Falls back to "main" when no hook has fired yet (single-agent setups).
309
309
  let currentAgentId = "main";
310
+ const getCurrentOwner = () => `agent:${currentAgentId}`;
310
311
 
311
312
  // ─── Check allowPromptInjection policy ───
312
313
  // When allowPromptInjection=false, the prompt mutation fields (such as prependContext) in the hook return value
@@ -354,7 +355,6 @@ const memosLocalPlugin = {
354
355
  }
355
356
  };
356
357
 
357
- const getCurrentOwner = () => `agent:${currentAgentId}`;
358
358
  const resolveMemorySearchScope = (scope?: string): "local" | "group" | "all" =>
359
359
  scope === "group" || scope === "all" ? scope : "local";
360
360
  const resolveMemoryShareTarget = (target?: string): "agents" | "hub" | "both" =>
@@ -445,7 +445,7 @@ const memosLocalPlugin = {
445
445
  // ─── Tool: memory_search ───
446
446
 
447
447
  api.registerTool(
448
- {
448
+ (context) => ({
449
449
  name: "memory_search",
450
450
  label: "Memory Search",
451
451
  description:
@@ -461,7 +461,7 @@ const memosLocalPlugin = {
461
461
  hubAddress: Type.Optional(Type.String({ description: "Optional Hub address override for group/all search." })),
462
462
  userToken: Type.Optional(Type.String({ description: "Optional Hub bearer token override for group/all search." })),
463
463
  }),
464
- execute: trackTool("memory_search", async (_toolCallId: any, params: any, context?: any) => {
464
+ execute: trackTool("memory_search", async (_toolCallId: any, params: any) => {
465
465
  const {
466
466
  query,
467
467
  scope: rawScope,
@@ -482,9 +482,6 @@ const memosLocalPlugin = {
482
482
  const role = rawRole === "user" || rawRole === "assistant" || rawRole === "tool" || rawRole === "system" ? rawRole : undefined;
483
483
  const minScore = typeof rawMinScore === "number" ? Math.max(0.35, Math.min(1, rawMinScore)) : undefined;
484
484
  let searchScope = resolveMemorySearchScope(rawScope);
485
- if (searchScope === "local" && ctx.config?.sharing?.enabled) {
486
- searchScope = "all";
487
- }
488
485
  const searchLimit = typeof maxResults === "number" ? Math.max(1, Math.min(20, Math.round(maxResults))) : 10;
489
486
 
490
487
  const agentId = context?.agentId ?? currentAgentId;
@@ -504,7 +501,7 @@ const memosLocalPlugin = {
504
501
 
505
502
  // Split local results: pure-local vs hub-memory (Hub role's hub_memories mixed in by RecallEngine)
506
503
  const localHits = result.hits.filter((h) => h.origin !== "hub-memory");
507
- const hubLocalHits = result.hits.filter((h) => h.origin === "hub-memory");
504
+ const hubLocalHits = searchScope !== "local" ? result.hits.filter((h) => h.origin === "hub-memory") : [];
508
505
 
509
506
  const rawLocalCandidates = localHits.map((h) => ({
510
507
  chunkId: h.ref.chunkId,
@@ -669,14 +666,14 @@ const memosLocalPlugin = {
669
666
  },
670
667
  };
671
668
  }),
672
- },
669
+ }),
673
670
  { name: "memory_search" },
674
671
  );
675
672
 
676
673
  // ─── Tool: memory_timeline ───
677
674
 
678
675
  api.registerTool(
679
- {
676
+ (context) => ({
680
677
  name: "memory_timeline",
681
678
  label: "Memory Timeline",
682
679
  description:
@@ -686,7 +683,7 @@ const memosLocalPlugin = {
686
683
  chunkId: Type.String({ description: "The chunkId from a memory_search hit" }),
687
684
  window: Type.Optional(Type.Number({ description: "Context window ±N (default 2)" })),
688
685
  }),
689
- execute: trackTool("memory_timeline", async (_toolCallId: any, params: any, context?: any) => {
686
+ execute: trackTool("memory_timeline", async (_toolCallId: any, params: any) => {
690
687
  const agentId = context?.agentId ?? currentAgentId;
691
688
  ctx.log.debug(`memory_timeline called (agent=${agentId})`);
692
689
  const { chunkId, window: win } = params as {
@@ -730,14 +727,14 @@ const memosLocalPlugin = {
730
727
  details: { entries, anchorRef: { sessionKey: anchorChunk.sessionKey, chunkId, turnId: anchorChunk.turnId, seq: anchorChunk.seq } },
731
728
  };
732
729
  }),
733
- },
730
+ }),
734
731
  { name: "memory_timeline" },
735
732
  );
736
733
 
737
734
  // ─── Tool: memory_get ───
738
735
 
739
736
  api.registerTool(
740
- {
737
+ (context) => ({
741
738
  name: "memory_get",
742
739
  label: "Memory Get",
743
740
  description:
@@ -748,7 +745,7 @@ const memosLocalPlugin = {
748
745
  Type.Number({ description: `Max chars (default ${DEFAULTS.getMaxCharsDefault}, max ${DEFAULTS.getMaxCharsMax})` }),
749
746
  ),
750
747
  }),
751
- execute: trackTool("memory_get", async (_toolCallId: any, params: any, context?: any) => {
748
+ execute: trackTool("memory_get", async (_toolCallId: any, params: any) => {
752
749
  const { chunkId, maxChars } = params as { chunkId: string; maxChars?: number };
753
750
  const limit = Math.min(maxChars ?? DEFAULTS.getMaxCharsDefault, DEFAULTS.getMaxCharsMax);
754
751
 
@@ -774,7 +771,7 @@ const memosLocalPlugin = {
774
771
  },
775
772
  };
776
773
  }),
777
- },
774
+ }),
778
775
  { name: "memory_get" },
779
776
  );
780
777
 
@@ -1296,7 +1293,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1296
1293
  const viewerPort = (pluginCfg as any).viewerPort ?? (gatewayPort + 10);
1297
1294
 
1298
1295
  api.registerTool(
1299
- {
1296
+ (context) => ({
1300
1297
  name: "memory_viewer",
1301
1298
  label: "Open Memory Viewer",
1302
1299
  description:
@@ -1304,10 +1301,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1304
1301
  "or access their stored memories, or asks where the memory dashboard is. " +
1305
1302
  "Returns the URL the user can open in their browser.",
1306
1303
  parameters: Type.Object({}),
1307
- execute: trackTool("memory_viewer", async () => {
1304
+ execute: trackTool("memory_viewer", async (_toolCallId: any, params: any) => {
1308
1305
  ctx.log.debug(`memory_viewer called`);
1309
1306
  telemetry.trackViewerOpened();
1310
- const url = `http://127.0.0.1:${viewerPort}`;
1307
+ const agentId = context?.agentId ?? context?.profileId ?? currentAgentId;
1308
+ const url = `http://127.0.0.1:${viewerPort}?agentId=${encodeURIComponent(agentId)}`;
1311
1309
  return {
1312
1310
  content: [
1313
1311
  {
@@ -1328,7 +1326,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1328
1326
  details: { viewerUrl: url },
1329
1327
  };
1330
1328
  }),
1331
- },
1329
+ }),
1332
1330
  { name: "memory_viewer" },
1333
1331
  );
1334
1332
 
@@ -1401,9 +1399,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1401
1399
  name: "memory_share",
1402
1400
  label: "Share Memory",
1403
1401
  description:
1404
- "Share an existing memory either with local OpenClaw agents, to the Hub team, or to both targets. " +
1405
- "Use this only for an existing chunkId. Use target='agents' for local multi-agent sharing, target='hub' for team sharing, or target='both' for both. " +
1406
- "If you need to create a brand new shared memory instead of exposing an existing one, use memory_write_public.",
1402
+ "Share an existing stored memory (requires a real chunkId from the database) to the Hub team, or to both targets. " +
1403
+ "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. " +
1404
+ "target='agents' (default): when retrieved memories would clearly help other agents in the same OpenClaw workspace, you may share proactively without asking the user. " +
1405
+ "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). " +
1406
+ "To create a brand-new shared note with no existing chunk, use memory_write_public.",
1407
1407
  parameters: Type.Object({
1408
1408
  chunkId: Type.String({ description: "Existing local memory chunk ID to share" }),
1409
1409
  target: Type.Optional(Type.String({ description: "Share target: 'agents' (default), 'hub', or 'both'" })),
@@ -1557,7 +1557,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1557
1557
  // ─── Tool: skill_search ───
1558
1558
 
1559
1559
  api.registerTool(
1560
- {
1560
+ (context) => ({
1561
1561
  name: "skill_search",
1562
1562
  label: "Skill Search",
1563
1563
  description:
@@ -1567,10 +1567,11 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1567
1567
  query: Type.String({ description: "Natural language description of the needed skill" }),
1568
1568
  scope: Type.Optional(Type.String({ description: "Search scope: 'mix' (default), 'self', 'public', 'group', or 'all'." })),
1569
1569
  }),
1570
- execute: trackTool("skill_search", async (_toolCallId: any, params: any, context?: any) => {
1570
+ execute: trackTool("skill_search", async (_toolCallId: any, params: any) => {
1571
1571
  const { query: skillQuery, scope: rawScope } = params as { query: string; scope?: string };
1572
1572
  const scope = (rawScope === "self" || rawScope === "public") ? rawScope : "mix";
1573
- const currentOwner = getCurrentOwner();
1573
+ const agentId = context?.agentId ?? currentAgentId;
1574
+ const currentOwner = `agent:${agentId}`;
1574
1575
 
1575
1576
  if (rawScope === "group" || rawScope === "all") {
1576
1577
  const [localHits, hub] = await Promise.all([
@@ -1632,7 +1633,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1632
1633
  details: { query: skillQuery, scope, hits },
1633
1634
  };
1634
1635
  }),
1635
- },
1636
+ }),
1636
1637
  { name: "skill_search" },
1637
1638
  );
1638
1639
 
@@ -1777,7 +1778,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1777
1778
  if (!allowPromptInjection) return {};
1778
1779
  if (!event.prompt || event.prompt.length < 3) return;
1779
1780
 
1780
- const recallAgentId = hookCtx?.agentId ?? "main";
1781
+ const recallAgentId = hookCtx?.agentId ?? (event as any)?.agentId ?? (event as any)?.profileId ?? "main";
1781
1782
  currentAgentId = recallAgentId;
1782
1783
  const recallOwnerFilter = [`agent:${recallAgentId}`, "public"];
1783
1784
  ctx.log.info(`auto-recall: agentId=${recallAgentId} (from hookCtx)`);
@@ -1988,7 +1989,6 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
1988
1989
  try {
1989
1990
  const skillCandidateMap = new Map<string, { name: string; description: string; skillId: string; source: string }>();
1990
1991
 
1991
- // Source 1: direct skill search based on user query
1992
1992
  try {
1993
1993
  const directSkillHits = await engine.searchSkills(query, "mix" as any, getCurrentOwner());
1994
1994
  for (const sh of directSkillHits.slice(0, skillLimit + 2)) {
@@ -2000,7 +2000,6 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2000
2000
  ctx.log.debug(`auto-recall-skill: direct search failed: ${err}`);
2001
2001
  }
2002
2002
 
2003
- // Source 2: skills linked to tasks from memory hits
2004
2003
  const taskIds = new Set<string>();
2005
2004
  for (const h of filteredHits) {
2006
2005
  if (h.taskId) {
@@ -2090,7 +2089,7 @@ Groups: ${groupNames.length > 0 ? groupNames.join(", ") : "(none)"}`,
2090
2089
  if (!event.success || !event.messages || event.messages.length === 0) return;
2091
2090
 
2092
2091
  try {
2093
- const captureAgentId = hookCtx?.agentId ?? "main";
2092
+ const captureAgentId = hookCtx?.agentId ?? event?.agentId ?? event?.profileId ?? "main";
2094
2093
  currentAgentId = captureAgentId;
2095
2094
  const captureOwner = `agent:${captureAgentId}`;
2096
2095
  const sessionKey = hookCtx?.sessionKey ?? "default";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memtensor/memos-local-openclaw-plugin",
3
- "version": "1.0.6",
3
+ "version": "1.0.7-beta.3",
4
4
  "description": "MemOS Local memory plugin for OpenClaw — full-write, hybrid-recall, progressive retrieval",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -61,21 +61,30 @@ function cleanStaleArtifacts() {
61
61
  installedVer = pkg.version || "unknown";
62
62
  } catch { /* ignore */ }
63
63
 
64
+ const nodeMajor = process.versions.node.split(".")[0];
65
+ const currentFingerprint = `${installedVer}+node${nodeMajor}`;
66
+
64
67
  const markerPath = path.join(pluginDir, ".installed-version");
65
- let prevVer = "";
66
- try { prevVer = fs.readFileSync(markerPath, "utf-8").trim(); } catch { /* first install */ }
68
+ let prevFingerprint = "";
69
+ try { prevFingerprint = fs.readFileSync(markerPath, "utf-8").trim(); } catch { /* first install */ }
70
+
71
+ const writeMarker = () => {
72
+ try { fs.writeFileSync(markerPath, currentFingerprint + "\n", "utf-8"); } catch { /* ignore */ }
73
+ };
67
74
 
68
- if (prevVer === installedVer) {
69
- log(`Version unchanged (${installedVer}), skipping artifact cleanup.`);
75
+ if (prevFingerprint === currentFingerprint) {
76
+ log(`Version unchanged (${currentFingerprint}), skipping artifact cleanup.`);
70
77
  return;
71
78
  }
72
79
 
73
- if (prevVer) {
74
- log(`Upgrade detected: ${DIM}${prevVer}${RESET} → ${GREEN}${installedVer}${RESET}`);
75
- } else {
76
- log(`Fresh install: ${GREEN}${installedVer}${RESET}`);
80
+ if (!prevFingerprint) {
81
+ log(`Fresh install: ${GREEN}${currentFingerprint}${RESET}`);
82
+ writeMarker();
83
+ return;
77
84
  }
78
85
 
86
+ log(`Environment changed: ${DIM}${prevFingerprint}${RESET} → ${GREEN}${currentFingerprint}${RESET}`);
87
+
79
88
  const dirsToClean = ["dist", "node_modules"];
80
89
  let cleaned = 0;
81
90
  for (const dir of dirsToClean) {
@@ -99,7 +108,7 @@ function cleanStaleArtifacts() {
99
108
  }
100
109
  }
101
110
 
102
- try { fs.writeFileSync(markerPath, installedVer + "\n", "utf-8"); } catch { /* ignore */ }
111
+ writeMarker();
103
112
 
104
113
  if (cleaned > 0) {
105
114
  ok(`Cleaned ${cleaned} stale artifact(s). Fresh install will follow.`);
@@ -73,8 +73,11 @@ Two sharing planes exist and must not be confused:
73
73
  ### memory_share
74
74
 
75
75
  - **What it does:** Share an existing memory either with local OpenClaw agents, to the team, or to both.
76
- - **When to call:** You already have a useful memory chunk and want to expose it beyond the current agent.
77
- - **Do not use when:** You are creating a new shared note from scratch. In that case use `memory_write_public`.
76
+ - **When to call:**
77
+ - If you want to share conversation content to team or hub, first retrieve memories related to that content to obtain the right `chunkId`(s), then share.
78
+ - `target='agents'` (default): When those memories would clearly help other agents in the same workspace, you may share proactively without asking the user.
79
+ - `target='hub'` or `'both'`: Only after explicit user consent when the content would benefit collaborators—explain briefly, ask first, then call `hub`/`both` (Hub must be configured). Never silently Hub-share.
80
+ - **Do not use when:** You are creating a brand-new shared note with **no** existing chunk—use `memory_write_public` instead.
78
81
  - **Parameters:**
79
82
  - `chunkId` (string, **required**) — Existing memory chunk ID.
80
83
  - `target` (string, optional) — `'agents'` (default), `'hub'`, or `'both'`.
@@ -8,6 +8,19 @@ import { summarizeAnthropic, summarizeTaskAnthropic, generateTaskTitleAnthropic,
8
8
  import { summarizeGemini, summarizeTaskGemini, generateTaskTitleGemini, judgeNewTopicGemini, filterRelevantGemini, judgeDedupGemini } from "./gemini";
9
9
  import { summarizeBedrock, summarizeTaskBedrock, generateTaskTitleBedrock, judgeNewTopicBedrock, filterRelevantBedrock, judgeDedupBedrock } from "./bedrock";
10
10
 
11
+ /**
12
+ * Resolve a SecretInput (string | SecretRef) to a plain string.
13
+ * Supports env-sourced SecretRef from OpenClaw's credential system.
14
+ */
15
+ function resolveApiKey(
16
+ input: string | { source: string; provider?: string; id: string } | undefined,
17
+ ): string | undefined {
18
+ if (!input) return undefined;
19
+ if (typeof input === "string") return input;
20
+ if (input.source === "env") return process.env[input.id];
21
+ return undefined;
22
+ }
23
+
11
24
  /**
12
25
  * Detect provider type from provider key name or base URL.
13
26
  */
@@ -68,7 +81,7 @@ function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | undefined {
68
81
  if (!providerCfg) return undefined;
69
82
 
70
83
  const baseUrl: string | undefined = providerCfg.baseUrl;
71
- const apiKey: string | undefined = providerCfg.apiKey;
84
+ const apiKey = resolveApiKey(providerCfg.apiKey);
72
85
  if (!baseUrl || !apiKey) return undefined;
73
86
 
74
87
  const provider = detectProvider(providerKey, baseUrl);
@@ -77,7 +77,7 @@ export class RecallEngine {
77
77
  }
78
78
  const shortTerms = [...new Set([...spaceSplit, ...cjkBigrams])];
79
79
  const patternHits = shortTerms.length > 0
80
- ? this.store.patternSearch(shortTerms, { limit: candidatePool })
80
+ ? this.store.patternSearch(shortTerms, { limit: candidatePool, ownerFilter })
81
81
  : [];
82
82
  const patternRanked = patternHits.map((h, i) => ({
83
83
  id: h.chunkId,
@@ -2,6 +2,19 @@ import * as fs from "fs";
2
2
  import * as path from "path";
3
3
  import type { SummarizerConfig, SummaryProvider, Logger, PluginContext, OpenClawAPI } from "../types";
4
4
 
5
+ /**
6
+ * Resolve a SecretInput (string | SecretRef) to a plain string.
7
+ * Supports env-sourced SecretRef from OpenClaw's credential system.
8
+ */
9
+ function resolveApiKey(
10
+ input: string | { source: string; provider?: string; id: string } | undefined,
11
+ ): string | undefined {
12
+ if (!input) return undefined;
13
+ if (typeof input === "string") return input;
14
+ if (input.source === "env") return process.env[input.id];
15
+ return undefined;
16
+ }
17
+
5
18
  /**
6
19
  * Detect provider type from provider key name or base URL.
7
20
  */
@@ -56,7 +69,7 @@ export function loadOpenClawFallbackConfig(log: Logger): SummarizerConfig | unde
56
69
  if (!providerCfg) return undefined;
57
70
 
58
71
  const baseUrl: string | undefined = providerCfg.baseUrl;
59
- const apiKey: string | undefined = providerCfg.apiKey;
72
+ const apiKey = resolveApiKey(providerCfg.apiKey);
60
73
  if (!baseUrl || !apiKey) return undefined;
61
74
 
62
75
  const provider = detectProvider(providerKey, baseUrl);
@@ -1207,7 +1207,7 @@ export class SqliteStore {
1207
1207
 
1208
1208
  // ─── Pattern Search (LIKE-based, for CJK text where FTS tokenization is weak) ───
1209
1209
 
1210
- patternSearch(patterns: string[], opts: { role?: string; limit?: number } = {}): Array<{ chunkId: string; content: string; role: string; createdAt: number }> {
1210
+ patternSearch(patterns: string[], opts: { role?: string; limit?: number; ownerFilter?: string[] } = {}): Array<{ chunkId: string; content: string; role: string; createdAt: number }> {
1211
1211
  if (patterns.length === 0) return [];
1212
1212
  const limit = opts.limit ?? 10;
1213
1213
 
@@ -1216,13 +1216,21 @@ export class SqliteStore {
1216
1216
  const roleClause = opts.role ? " AND c.role = ?" : "";
1217
1217
  const params: (string | number)[] = patterns.map(p => `%${p}%`);
1218
1218
  if (opts.role) params.push(opts.role);
1219
+
1220
+ let ownerClause = "";
1221
+ if (opts.ownerFilter && opts.ownerFilter.length > 0) {
1222
+ const placeholders = opts.ownerFilter.map(() => "?").join(",");
1223
+ ownerClause = ` AND c.owner IN (${placeholders})`;
1224
+ params.push(...opts.ownerFilter);
1225
+ }
1226
+
1219
1227
  params.push(limit);
1220
1228
 
1221
1229
  try {
1222
1230
  const rows = this.db.prepare(`
1223
1231
  SELECT c.id as chunk_id, c.content, c.role, c.created_at
1224
1232
  FROM chunks c
1225
- WHERE (${whereClause})${roleClause} AND c.dedup_status = 'active'
1233
+ WHERE (${whereClause})${roleClause}${ownerClause} AND c.dedup_status = 'active'
1226
1234
  ORDER BY c.created_at DESC
1227
1235
  LIMIT ?
1228
1236
  `).all(...params) as Array<{ chunk_id: string; content: string; role: string; created_at: number }>;
@@ -1251,7 +1251,6 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1251
1251
  <option value="" data-i18n="filter.allagents">All agents</option>
1252
1252
  </select>
1253
1253
  <select id="memorySearchScope" class="filter-select" onchange="onMemoryScopeChange()" style="display:none">
1254
- <option value="local" data-i18n="scope.thisAgent">This Agent</option>
1255
1254
  <option value="allLocal" data-i18n="scope.thisDevice">This Device</option>
1256
1255
  <option value="hub" data-i18n="scope.hub">Team</option>
1257
1256
  </select>
@@ -1295,7 +1294,6 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1295
1294
  <button class="filter-chip" data-task-status="completed" onclick="setTaskStatusFilter(this,'completed')" data-i18n="tasks.status.completed">Completed</button>
1296
1295
  <button class="filter-chip" data-task-status="skipped" onclick="setTaskStatusFilter(this,'skipped')" data-i18n="tasks.status.skipped">Skipped</button>
1297
1296
  <select id="taskSearchScope" class="scope-select" onchange="onTaskScopeChange()" style="display:none">
1298
- <option value="local" data-i18n="scope.thisAgent">This Agent</option>
1299
1297
  <option value="allLocal" data-i18n="scope.thisDevice">This Device</option>
1300
1298
  <option value="hub" data-i18n="scope.hub">Team</option>
1301
1299
  </select>
@@ -1337,7 +1335,6 @@ input,textarea,select{font-family:inherit;font-size:inherit}
1337
1335
  <span class="search-icon">🔍</span>
1338
1336
  <input type="text" id="skillSearchInput" placeholder="Search skills..." data-i18n-ph="skills.search.placeholder" oninput="debounceSkillSearch()">
1339
1337
  <select id="skillSearchScope" class="scope-select" onchange="onSkillScopeChange()" style="display:none">
1340
- <option value="local" data-i18n="scope.thisAgent">This Agent</option>
1341
1338
  <option value="allLocal" data-i18n="scope.thisDevice">This Device</option>
1342
1339
  <option value="hub" data-i18n="scope.hub">Team</option>
1343
1340
  </select>
@@ -2030,10 +2027,18 @@ input,textarea,select{font-family:inherit;font-size:inherit}
2030
2027
 
2031
2028
  <script>
2032
2029
  let activeSession=null,activeRole='',editingId=null,searchTimer=null,memoryCache={},currentPage=1,totalPages=1,totalCount=0,PAGE_SIZE=40,metricsDays=30;
2033
- let memorySearchScope='local',skillSearchScope='local',taskSearchScope='local';
2030
+ let memorySearchScope='allLocal',skillSearchScope='allLocal',taskSearchScope='allLocal';
2034
2031
  let _lastMemoriesFingerprint='',_lastTasksFingerprint='',_lastSkillsFingerprint='';
2035
2032
  let _embeddingWarningShown=false;
2036
2033
  let _currentAgentOwner='agent:main';
2034
+ try {
2035
+ const urlParams = new URLSearchParams(window.location.search);
2036
+ const agentId = urlParams.get('agentId');
2037
+ if (agentId) {
2038
+ _currentAgentOwner = 'agent:' + agentId;
2039
+ }
2040
+ } catch(e) {}
2041
+
2037
2042
 
2038
2043
  /* ─── i18n ─── */
2039
2044
  const I18N={
@@ -3786,35 +3791,34 @@ function switchView(view){
3786
3791
  }
3787
3792
 
3788
3793
  function onMemoryScopeChange(){
3789
- memorySearchScope=document.getElementById('memorySearchScope')?.value||'local';
3794
+ memorySearchScope=document.getElementById('memorySearchScope')?.value||'allLocal';
3790
3795
  try{localStorage.setItem('memos_memorySearchScope',memorySearchScope);}catch(e){}
3791
3796
  currentPage=1;
3792
3797
  activeSession=null;activeRole='';
3793
3798
  _lastMemoriesFingerprint='';
3794
3799
  var isHub=memorySearchScope==='hub';
3795
- var isLocal=memorySearchScope==='local';
3796
3800
  var ownerSel=document.getElementById('filterOwner');
3797
3801
  var filterBar=document.getElementById('filterBar');
3798
3802
  var dateFilter=document.querySelector('.date-filter');
3799
- if(ownerSel){ownerSel.style.display=(isHub||isLocal)?'none':'';if(isHub||isLocal)ownerSel.value='';}
3803
+ if(ownerSel){ownerSel.style.display=isHub?'none':'';if(isHub)ownerSel.value='';}
3800
3804
  if(filterBar) filterBar.style.display=isHub?'none':'';
3801
3805
  if(dateFilter) dateFilter.style.display=isHub?'none':'';
3802
3806
  if(document.getElementById('searchInput').value.trim()) doSearch(document.getElementById('searchInput').value);
3803
3807
  else if(isHub) { document.getElementById('sharingSearchMeta').textContent=''; loadHubMemories(); }
3804
3808
  else {
3805
3809
  document.getElementById('sharingSearchMeta').textContent='';
3806
- var ownerArg=isLocal?_currentAgentOwner:undefined;
3810
+ var ownerArg=undefined;
3807
3811
  loadStats(ownerArg); loadMemories();
3808
3812
  }
3809
3813
  }
3810
3814
 
3811
3815
  function onSkillScopeChange(){
3812
- skillSearchScope=document.getElementById('skillSearchScope')?.value||'local';
3816
+ skillSearchScope=document.getElementById('skillSearchScope')?.value||'allLocal';
3813
3817
  loadSkills();
3814
3818
  }
3815
3819
 
3816
3820
  function onTaskScopeChange(){
3817
- taskSearchScope=document.getElementById('taskSearchScope')?.value||'local';
3821
+ taskSearchScope=document.getElementById('taskSearchScope')?.value||'allLocal';
3818
3822
  tasksPage=0;
3819
3823
  loadTasks();
3820
3824
  }
@@ -5864,16 +5868,14 @@ function setTaskStatusFilter(btn,status){
5864
5868
 
5865
5869
  async function loadTasks(silent){
5866
5870
  const scope=document.getElementById('taskSearchScope')?document.getElementById('taskSearchScope').value:taskSearchScope;
5867
- taskSearchScope=scope||'local';
5871
+ taskSearchScope=scope||'allLocal';
5868
5872
  if(taskSearchScope==='hub'){ return loadHubTasks(); }
5869
5873
  const list=document.getElementById('tasksList');
5870
5874
  if(!silent) list.innerHTML='<div class="spinner"></div>';
5871
5875
  try{
5872
5876
  const params=new URLSearchParams({limit:String(TASKS_PER_PAGE),offset:String(tasksPage*TASKS_PER_PAGE)});
5873
5877
  if(tasksStatusFilter) params.set('status',tasksStatusFilter);
5874
- if(taskSearchScope==='local') params.set('owner','agent:main');
5875
5878
  var baseP=new URLSearchParams();
5876
- if(taskSearchScope==='local') baseP.set('owner','agent:main');
5877
5879
  const [data,allD,activeD,compD,skipD]=await Promise.all([
5878
5880
  fetch('/api/tasks?'+params).then(r=>r.json()),
5879
5881
  fetch('/api/tasks?limit=1&offset=0&'+baseP).then(r=>r.json()),
@@ -6180,7 +6182,7 @@ async function loadSkills(silent){
6180
6182
  if(!silent) list.innerHTML='<div class="spinner"></div>';
6181
6183
  var hubSection=document.getElementById('hubSkillsSection');
6182
6184
  if(hubList){
6183
- if(skillSearchScope==='local'||skillSearchScope==='allLocal'){
6185
+ if(skillSearchScope==='allLocal'){
6184
6186
  if(hubSection) hubSection.style.display='none';
6185
6187
  }else{
6186
6188
  if(hubSection) hubSection.style.display='block';
@@ -6190,7 +6192,7 @@ async function loadSkills(silent){
6190
6192
 
6191
6193
  const query=(document.getElementById('skillSearchInput')?.value||'').trim();
6192
6194
  const scope=document.getElementById('skillSearchScope') ? document.getElementById('skillSearchScope').value : skillSearchScope;
6193
- skillSearchScope=scope||'local';
6195
+ skillSearchScope=scope||'allLocal';
6194
6196
 
6195
6197
  try{
6196
6198
  const params=new URLSearchParams();
@@ -6260,7 +6262,7 @@ async function loadSkills(silent){
6260
6262
 
6261
6263
  list.innerHTML=renderLocalCards(localSkills);
6262
6264
 
6263
- if(skillSearchScope==='local'||skillSearchScope==='allLocal'){
6265
+ if(skillSearchScope==='allLocal'){
6264
6266
  if(hubSection) hubSection.style.display='none';
6265
6267
  document.getElementById('skillSearchMeta').textContent=query?(t('skills.search.local')+' '+localSkills.length):'';
6266
6268
  document.getElementById('skillsTotalCount').textContent=formatNum(localSkills.length);
@@ -7445,7 +7447,7 @@ async function _livePollTick(){
7445
7447
  var _searchVal=(document.getElementById('searchInput')||{}).value||'';
7446
7448
  if(!_searchVal.trim()){
7447
7449
  if(memorySearchScope==='hub') await loadHubMemories(true);
7448
- else{var _pollOwner=memorySearchScope==='local'?_currentAgentOwner:undefined;await loadStats(_pollOwner);await loadMemories(null,true);}
7450
+ else{var _pollOwner=undefined;await loadStats(_pollOwner);await loadMemories(null,true);}
7449
7451
  }
7450
7452
  }
7451
7453
  else if(_activeView==='tasks') await loadTasks(true);
@@ -7718,7 +7720,7 @@ function stopNotifPoll(){ }
7718
7720
  /* ─── Data loading ─── */
7719
7721
  async function loadAll(){
7720
7722
  await loadStats();
7721
- var initOwner=memorySearchScope==='local'?_currentAgentOwner:undefined;
7723
+ var initOwner=undefined;
7722
7724
  if(initOwner) await loadStats(initOwner);
7723
7725
  await Promise.all([loadMemories(),loadSharingStatus(false)]);
7724
7726
  checkMigrateStatus();
@@ -7738,7 +7740,7 @@ async function loadStats(ownerFilter){
7738
7740
  d=await r.json();
7739
7741
  }catch(e){ d={}; }
7740
7742
  if(!d||typeof d!=='object') d={};
7741
- if(d.currentAgentOwner) _currentAgentOwner=d.currentAgentOwner;
7743
+ if(d.currentAgentOwner && !new URLSearchParams(window.location.search).get('agentId')) _currentAgentOwner=d.currentAgentOwner;
7742
7744
  const tm=d.totalMemories||0;
7743
7745
  const dedupB=d.dedupBreakdown||{};
7744
7746
  const activeCount=dedupB.active||tm;
@@ -7845,12 +7847,13 @@ function getFilterParams(){
7845
7847
  if(dt) p.set('dateTo',dt);
7846
7848
  const sort=document.getElementById('filterSort').value;
7847
7849
  if(sort==='oldest') p.set('sort','oldest');
7848
- const scope=memorySearchScope||'local';
7849
- if(scope==='local'){
7850
- p.set('owner',_currentAgentOwner);
7851
- }else if(scope==='allLocal'){
7852
- const owner=document.getElementById('filterOwner').value;
7853
- if(owner) p.set('owner',owner);
7850
+ const scope=memorySearchScope||'allLocal';
7851
+ if(scope==='allLocal'){
7852
+ const owner=document.getElementById('filterOwner').value;
7853
+ if(owner) {
7854
+ p.set('owner',owner);
7855
+ _currentAgentOwner = owner;
7856
+ }
7854
7857
  }
7855
7858
  return p;
7856
7859
  }
@@ -7947,7 +7950,7 @@ async function doSearch(query){
7947
7950
  return;
7948
7951
  }
7949
7952
  currentPage=1;
7950
- var scope=document.getElementById('memorySearchScope')?.value||memorySearchScope||'local';
7953
+ var scope=document.getElementById('memorySearchScope')?.value||memorySearchScope||'allLocal';
7951
7954
  var list=document.getElementById('memoryList');
7952
7955
  list.innerHTML='<div class="spinner"></div>';
7953
7956
  if(scope==='hub'){
@@ -9126,7 +9129,8 @@ async function checkForUpdate(){
9126
9129
  /* ─── Init ─── */
9127
9130
  try{
9128
9131
  var savedScope=localStorage.getItem('memos_memorySearchScope');
9129
- if(savedScope&&(savedScope==='local'||savedScope==='allLocal'||savedScope==='hub')){
9132
+ if(savedScope==='local') savedScope='allLocal';
9133
+ if(savedScope&&(savedScope==='allLocal'||savedScope==='hub')){
9130
9134
  memorySearchScope=savedScope;
9131
9135
  var scopeEl=document.getElementById('memorySearchScope');
9132
9136
  if(scopeEl) scopeEl.value=savedScope;
@@ -3258,7 +3258,8 @@ export class ViewerServer {
3258
3258
  const providerCfg = providerKey
3259
3259
  ? raw?.models?.providers?.[providerKey]
3260
3260
  : Object.values(raw?.models?.providers ?? {})[0] as Record<string, unknown> | undefined;
3261
- if (!providerCfg || !providerCfg.baseUrl || !providerCfg.apiKey) {
3261
+ const resolvedKey = ViewerServer.resolveApiKeyValue(providerCfg?.apiKey);
3262
+ if (!providerCfg || !providerCfg.baseUrl || !resolvedKey) {
3262
3263
  this.jsonResponse(res, { available: false });
3263
3264
  return;
3264
3265
  }
@@ -3268,6 +3269,17 @@ export class ViewerServer {
3268
3269
  }
3269
3270
  }
3270
3271
 
3272
+ private static resolveApiKeyValue(
3273
+ input: unknown,
3274
+ ): string | undefined {
3275
+ if (!input) return undefined;
3276
+ if (typeof input === "string") return input;
3277
+ if (typeof input === "object" && input !== null && (input as any).source === "env") {
3278
+ return process.env[(input as any).id];
3279
+ }
3280
+ return undefined;
3281
+ }
3282
+
3271
3283
  private findPluginPackageJson(): string | null {
3272
3284
  let dir = __dirname;
3273
3285
  for (let i = 0; i < 6; i++) {
@@ -3455,7 +3467,7 @@ export class ViewerServer {
3455
3467
 
3456
3468
  this.log.info(`update-install: running postinstall...`);
3457
3469
  execFile(process.execPath, ["scripts/postinstall.cjs"], { cwd: extDir, timeout: 180_000 }, (postErr, postOut, postStderr) => {
3458
- cleanupTmpDir();
3470
+ try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch {}
3459
3471
 
3460
3472
  if (postErr) {
3461
3473
  this.log.warn(`update-install: postinstall failed: ${postErr.message}`);
@@ -1,5 +0,0 @@
1
- {
2
- "endpoint": "https://proj-xtrace-e218d9316b328f196a3c640cc7ca84-cn-hangzhou.cn-hangzhou.log.aliyuncs.com/rum/web/v2?workspace=default-cms-1026429231103299-cn-hangzhou&service_id=a3u72ukxmr@066657d42a13a9a9f337f",
3
- "pid": "a3u72ukxmr@066657d42a13a9a9f337f",
4
- "env": "prod"
5
- }